feat: association field block (#493)
* feat: association field block * feat: association details block * feat: template add resource name * feat: add association calendar * fix: update yarn.lock * fix: remove useAssociationNames * fix: restore useFilterByTk logic * feat: client doc * fix: resolveNocobasePackagesAlias * fix: input textarea readpretty * feat: styling * fix: oho & obo * fix: field-summary component remove to collection manager * fix: translation * feat: improve code * fix(audit-logs): skip when collection does not exist * feat: m2m * fix: improve code * fix: title field Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
		
							parent
							
								
									d831a9b889
								
							
						
					
					
						commit
						b91ca4420b
					
				| @ -2,6 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| const dotenv = require('dotenv'); | const dotenv = require('dotenv'); | ||||||
| const { resolve } = require('path'); | const { resolve } = require('path'); | ||||||
|  | const { existsSync } = require('fs'); | ||||||
| 
 | 
 | ||||||
| const env = { | const env = { | ||||||
|   APP_PACKAGE_ROOT: 'app', |   APP_PACKAGE_ROOT: 'app', | ||||||
| @ -20,6 +21,12 @@ if ('v18' === process.version.split('.').shift()) { | |||||||
|   process.env.NODE_OPTIONS = '--openssl-legacy-provider'; |   process.env.NODE_OPTIONS = '--openssl-legacy-provider'; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | if (!process.env.APP_ENV_PATH && process.argv[2] && process.argv[2] === 'test') { | ||||||
|  |   if (existsSync(resolve(process.cwd(), '.env.test'))) { | ||||||
|  |     process.env.APP_ENV_PATH = '.env.test'; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| dotenv.config({ | dotenv.config({ | ||||||
|   path: resolve(process.cwd(), process.env.APP_ENV_PATH || '.env'), |   path: resolve(process.cwd(), process.env.APP_ENV_PATH || '.env'), | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -39,6 +39,7 @@ const useResource = (props: UseResourceProps) => { | |||||||
|   const api = useAPIClient(); |   const api = useAPIClient(); | ||||||
|   const association = useAssociation(props); |   const association = useAssociation(props); | ||||||
|   const sourceId = useSourceId?.(); |   const sourceId = useSourceId?.(); | ||||||
|  | 
 | ||||||
|   const field = useField<Field>(); |   const field = useField<Field>(); | ||||||
|   if (block === 'TableField') { |   if (block === 'TableField') { | ||||||
|     const options = { |     const options = { | ||||||
| @ -60,6 +61,7 @@ const useResource = (props: UseResourceProps) => { | |||||||
|   if (sourceId) { |   if (sourceId) { | ||||||
|     return api.resource(resource, sourceId); |     return api.resource(resource, sourceId); | ||||||
|   } |   } | ||||||
|  |    | ||||||
|   return api.resource(resource, record[association?.sourceKey || 'id']); |   return api.resource(resource, record[association?.sourceKey || 'id']); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| @ -150,6 +152,7 @@ export const useFilterByTk = () => { | |||||||
|       return recordIndex; |       return recordIndex; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |    | ||||||
|   if (assoc) {  |   if (assoc) {  | ||||||
|     const association = getCollectionField(assoc); |     const association = getCollectionField(assoc); | ||||||
|     return record?.[association.targetKey || 'id']; |     return record?.[association.targetKey || 'id']; | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ import { useField } from '@formily/react'; | |||||||
| import { Spin } from 'antd'; | import { Spin } from 'antd'; | ||||||
| import React, { createContext, useContext, useEffect, useMemo } from 'react'; | import React, { createContext, useContext, useEffect, useMemo } from 'react'; | ||||||
| import { BlockProvider, useBlockRequestContext } from './BlockProvider'; | import { BlockProvider, useBlockRequestContext } from './BlockProvider'; | ||||||
|  | import { useCollectionManager } from '../collection-manager'; | ||||||
| 
 | 
 | ||||||
| export const FormBlockContext = createContext<any>({}); | export const FormBlockContext = createContext<any>({}); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -61,7 +61,7 @@ function getFormValues(filterByTk, field, form, fieldNames, getField, resource) | |||||||
|     if (fieldNames.includes(key)) { |     if (fieldNames.includes(key)) { | ||||||
|       const collectionField = getField(key); |       const collectionField = getField(key); | ||||||
|       if (filterByTk) { |       if (filterByTk) { | ||||||
|         if (collectionField.interface === 'subTable') { |         if (['subTable', 'o2m'].includes(collectionField.interface)) { | ||||||
|           values[key] = form.values[key]; |           values[key] = form.values[key]; | ||||||
|           continue; |           continue; | ||||||
|         } |         } | ||||||
| @ -70,7 +70,7 @@ function getFormValues(filterByTk, field, form, fieldNames, getField, resource) | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       const items = form.values[key]; |       const items = form.values[key]; | ||||||
|       if (collectionField.interface === 'linkTo') { |       if (['linkTo', 'm2o', 'm2m'].includes(collectionField.interface)) { | ||||||
|         const targetKey = collectionField.targetKey || 'id'; |         const targetKey = collectionField.targetKey || 'id'; | ||||||
|         if (resource instanceof TableFieldResource) { |         if (resource instanceof TableFieldResource) { | ||||||
|           if (Array.isArray(items)) { |           if (Array.isArray(items)) { | ||||||
| @ -121,6 +121,7 @@ export const useCreateActionProps = () => { | |||||||
|       const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource); |       const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource); | ||||||
|       actionField.data = field.data || {}; |       actionField.data = field.data || {}; | ||||||
|       actionField.data.loading = true; |       actionField.data.loading = true; | ||||||
|  |       try { | ||||||
|         await resource.create({ |         await resource.create({ | ||||||
|           values: { |           values: { | ||||||
|             ...values, |             ...values, | ||||||
| @ -151,6 +152,9 @@ export const useCreateActionProps = () => { | |||||||
|         } else { |         } else { | ||||||
|           message.success(compile(onSuccess?.successMessage)); |           message.success(compile(onSuccess?.successMessage)); | ||||||
|         } |         } | ||||||
|  |       } catch (error) { | ||||||
|  |         actionField.data.loading = false; | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| @ -281,6 +285,7 @@ export const useUpdateActionProps = () => { | |||||||
|       const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource); |       const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource); | ||||||
|       actionField.data = field.data || {}; |       actionField.data = field.data || {}; | ||||||
|       actionField.data.loading = true; |       actionField.data.loading = true; | ||||||
|  |       try { | ||||||
|         await resource.update({ |         await resource.update({ | ||||||
|           filterByTk, |           filterByTk, | ||||||
|           values: { |           values: { | ||||||
| @ -315,6 +320,9 @@ export const useUpdateActionProps = () => { | |||||||
|         } else { |         } else { | ||||||
|           message.success(compile(onSuccess?.successMessage)); |           message.success(compile(onSuccess?.successMessage)); | ||||||
|         } |         } | ||||||
|  |       } catch (error) { | ||||||
|  |         actionField.data.loading = false; | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ import { PlusOutlined } from '@ant-design/icons'; | |||||||
| import { ArrayTable } from '@formily/antd'; | import { ArrayTable } from '@formily/antd'; | ||||||
| import { ISchema, useForm } from '@formily/react'; | import { ISchema, useForm } from '@formily/react'; | ||||||
| import { uid } from '@formily/shared'; | import { uid } from '@formily/shared'; | ||||||
| import { Button, Dropdown, Menu, Typography } from 'antd'; | import { Button, Dropdown, Menu } from 'antd'; | ||||||
| import { cloneDeep } from 'lodash'; | import { cloneDeep } from 'lodash'; | ||||||
| import React, { useState } from 'react'; | import React, { useState } from 'react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| @ -50,8 +50,8 @@ const getSchema = (schema: IField, record: any, compile): ISchema => { | |||||||
|             type: 'void', |             type: 'void', | ||||||
|             'x-component': 'FieldSummary', |             'x-component': 'FieldSummary', | ||||||
|             'x-component-props': { |             'x-component-props': { | ||||||
|               schemaKey: schema.name |               schemaKey: schema.name, | ||||||
|             } |             }, | ||||||
|           }, |           }, | ||||||
|           // @ts-ignore
 |           // @ts-ignore
 | ||||||
|           ...properties, |           ...properties, | ||||||
| @ -120,9 +120,10 @@ const useCreateCollectionField = () => { | |||||||
| 
 | 
 | ||||||
|       recursiveChildren(form?.values?.children); |       recursiveChildren(form?.values?.children); | ||||||
| 
 | 
 | ||||||
|       if (['o2o', 'o2m', 'm2o', 'm2m', 'linkTo'].includes(form?.values?.interface) && title) { |       if (['obo', 'oho', 'o2o', 'o2m', 'm2o', 'm2m', 'linkTo'].includes(form?.values?.interface) && title) { | ||||||
|         form.setValuesIn('reverseField.uiSchema.title', title); |         form.setValuesIn('reverseField.uiSchema.title', title); | ||||||
|       } |       } | ||||||
|  | 
 | ||||||
|       await run(); |       await run(); | ||||||
|       await refreshCM(); |       await refreshCM(); | ||||||
|     }, |     }, | ||||||
| @ -155,7 +156,9 @@ export const AddFieldAction = () => { | |||||||
|               return ( |               return ( | ||||||
|                 option.children.length > 0 && ( |                 option.children.length > 0 && ( | ||||||
|                   <Menu.ItemGroup key={option.label} title={compile(option.label)}> |                   <Menu.ItemGroup key={option.label} title={compile(option.label)}> | ||||||
|                     {option.children.map((child) => { |                     {option.children | ||||||
|  |                       .filter((child) => !['o2o', 'subTable'].includes(child.name)) | ||||||
|  |                       .map((child) => { | ||||||
|                         return <Menu.Item key={child.name}>{compile(child.title)}</Menu.Item>; |                         return <Menu.Item key={child.name}>{compile(child.title)}</Menu.Item>; | ||||||
|                       })} |                       })} | ||||||
|                   </Menu.ItemGroup> |                   </Menu.ItemGroup> | ||||||
| @ -169,7 +172,11 @@ export const AddFieldAction = () => { | |||||||
|           {t('Add field')} |           {t('Add field')} | ||||||
|         </Button> |         </Button> | ||||||
|       </Dropdown> |       </Dropdown> | ||||||
|       <SchemaComponent schema={schema} components={{ ...components, ArrayTable }} scope={{ createOnly: true, useCreateCollectionField }} /> |       <SchemaComponent | ||||||
|  |         schema={schema} | ||||||
|  |         components={{ ...components, ArrayTable }} | ||||||
|  |         scope={{ createOnly: true, useCreateCollectionField }} | ||||||
|  |       /> | ||||||
|     </ActionContext.Provider> |     </ActionContext.Provider> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ import { SchemaComponent, useActionContext, useCompile } from '../../schema-comp | |||||||
| import { useCollectionManager } from '../hooks/useCollectionManager'; | import { useCollectionManager } from '../hooks/useCollectionManager'; | ||||||
| import { DataSourceContext } from '../sub-table'; | import { DataSourceContext } from '../sub-table'; | ||||||
| import { AddSubFieldAction } from './AddSubFieldAction'; | import { AddSubFieldAction } from './AddSubFieldAction'; | ||||||
|  | import { FieldSummary } from './components/FieldSummary'; | ||||||
| import { EditSubFieldAction } from './EditSubFieldAction'; | import { EditSubFieldAction } from './EditSubFieldAction'; | ||||||
| import { collectionSchema } from './schemas/collections'; | import { collectionSchema } from './schemas/collections'; | ||||||
| 
 | 
 | ||||||
| @ -185,6 +186,7 @@ export const ConfigurationTable = () => { | |||||||
|         components={{ |         components={{ | ||||||
|           AddSubFieldAction, |           AddSubFieldAction, | ||||||
|           EditSubFieldAction, |           EditSubFieldAction, | ||||||
|  |           FieldSummary, | ||||||
|         }} |         }} | ||||||
|         scope={{ |         scope={{ | ||||||
|           useDestroySubField, |           useDestroySubField, | ||||||
|  | |||||||
| @ -0,0 +1,39 @@ | |||||||
|  | import { css } from '@emotion/css'; | ||||||
|  | import { observer } from '@formily/react'; | ||||||
|  | import { Tag } from 'antd'; | ||||||
|  | import React from 'react'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { useCompile } from '../../../schema-component'; | ||||||
|  | import { useCollectionManager } from '../../hooks'; | ||||||
|  | 
 | ||||||
|  | export const FieldSummary = observer((props: any) => { | ||||||
|  |   const { schemaKey } = props; | ||||||
|  |   const { getInterface } = useCollectionManager(); | ||||||
|  |   const compile = useCompile(); | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const schema = getInterface(schemaKey); | ||||||
|  | 
 | ||||||
|  |   if (!schema) return null; | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div | ||||||
|  |       className={css` | ||||||
|  |         background: #f6f6f6; | ||||||
|  |         margin-bottom: 24px; | ||||||
|  |         padding: 16px; | ||||||
|  |       `}
 | ||||||
|  |     > | ||||||
|  |       <div className={css``}>{t('Field interface')}: <Tag>{compile(schema.title)}</Tag></div> | ||||||
|  |       {schema.description ? ( | ||||||
|  |         <div | ||||||
|  |           className={css` | ||||||
|  |             margin-top: 8px; | ||||||
|  |             color: rgba(0, 0, 0, 0.45); | ||||||
|  |           `}
 | ||||||
|  |         > | ||||||
|  |           {compile(schema.description)} | ||||||
|  |         </div> | ||||||
|  |       ) : null} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }); | ||||||
| @ -17,7 +17,7 @@ export function registerGroupLabel(key: string, label: string) { | |||||||
|   groupLabels[key] = label; |   groupLabels[key] = label; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Object.keys(types).filter((type) => !['subTable'].includes(type)).forEach((type) => { | Object.keys(types).forEach((type) => { | ||||||
|   const schema = types[type]; |   const schema = types[type]; | ||||||
|   registerField(schema.group || 'others', type, { order: 0, ...schema }); |   registerField(schema.group || 'others', type, { order: 0, ...schema }); | ||||||
| }); | }); | ||||||
| @ -27,7 +27,7 @@ registerGroupLabel('choices', '{{t("Choices")}}'); | |||||||
| registerGroupLabel('media', '{{t("Media")}}'); | registerGroupLabel('media', '{{t("Media")}}'); | ||||||
| registerGroupLabel('datetime', '{{t("Date & Time")}}'); | registerGroupLabel('datetime', '{{t("Date & Time")}}'); | ||||||
| registerGroupLabel('relation', '{{t("Relation")}}'); | registerGroupLabel('relation', '{{t("Relation")}}'); | ||||||
| registerGroupLabel('advance', '{{t("Advance type")}}'); | registerGroupLabel('advanced', '{{t("Advanced type")}}'); | ||||||
| registerGroupLabel('systemInfo', '{{t("System info")}}'); | registerGroupLabel('systemInfo', '{{t("System info")}}'); | ||||||
| registerGroupLabel('others', '{{t("Others")}}'); | registerGroupLabel('others', '{{t("Others")}}'); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ import { IField } from './types'; | |||||||
| export const formula: IField = { | export const formula: IField = { | ||||||
|   name: 'formula', |   name: 'formula', | ||||||
|   type: 'object', |   type: 'object', | ||||||
|   group: 'advance', |   group: 'advanced', | ||||||
|   order: 1, |   order: 1, | ||||||
|   title: '{{t("Formula")}}', |   title: '{{t("Formula")}}', | ||||||
|   description: '{{t("Formula description")}}', |   description: '{{t("Formula description")}}', | ||||||
|  | |||||||
| @ -86,17 +86,17 @@ export const linkTo: IField = { | |||||||
|       'x-component': 'Select', |       'x-component': 'Select', | ||||||
|       'x-disabled': '{{ !createOnly }}', |       'x-disabled': '{{ !createOnly }}', | ||||||
|     }, |     }, | ||||||
|     through: { |     // through: {
 | ||||||
|       type: 'string', |     //   type: 'string',
 | ||||||
|       title: '{{t("Junction collection")}}', |     //   title: '{{t("Junction collection")}}',
 | ||||||
|       'x-disabled': '{{ !createOnly }}', |     //   'x-disabled': '{{ !createOnly }}',
 | ||||||
|       'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'], |     //   'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
 | ||||||
|       'x-decorator': 'FormItem', |     //   'x-decorator': 'FormItem',
 | ||||||
|       'x-component': 'Select', |     //   'x-component': 'Select',
 | ||||||
|       'x-component-props': { |     //   'x-component-props': {
 | ||||||
|         placeholder: '{{t("Leave it blank, unless you need a custom intermediate table")}}', |     //     placeholder: '{{t("Leave it blank, unless you need a custom intermediate table")}}',
 | ||||||
|       }, |     //   },
 | ||||||
|     }, |     // },
 | ||||||
|     // 'reverseField.uiSchema.title': {
 |     // 'reverseField.uiSchema.title': {
 | ||||||
|     //   type: 'string',
 |     //   type: 'string',
 | ||||||
|     //   title: '{{t("Reverse field display name")}}',
 |     //   title: '{{t("Reverse field display name")}}',
 | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| import { ISchema } from '@formily/react'; | import { ISchema } from '@formily/react'; | ||||||
| import { uid } from '@formily/shared'; | import { uid } from '@formily/shared'; | ||||||
| import { cloneDeep } from 'lodash'; | import { cloneDeep } from 'lodash'; | ||||||
| import { defaultProps, recordPickerSelector, recordPickerViewer } from './properties'; | import { defaultProps, recordPickerSelector, recordPickerViewer, relationshipType } from './properties'; | ||||||
| import { IField } from './types'; | import { IField } from './types'; | ||||||
| 
 | 
 | ||||||
| export const m2m: IField = { | export const m2m: IField = { | ||||||
| @ -77,20 +77,7 @@ export const m2m: IField = { | |||||||
|   }, |   }, | ||||||
|   properties: { |   properties: { | ||||||
|     ...defaultProps, |     ...defaultProps, | ||||||
|     type: { |     type: relationshipType, | ||||||
|       type: 'string', |  | ||||||
|       title: '{{t("Relationship type")}}', |  | ||||||
|       required: true, |  | ||||||
|       'x-disabled': true, |  | ||||||
|       'x-decorator': 'FormItem', |  | ||||||
|       'x-component': 'Select', |  | ||||||
|       enum: [ |  | ||||||
|         { label: "{{t('One to one')}}", value: 'hasOne' }, |  | ||||||
|         { label: "{{t('One to many')}}", value: 'hasMany' }, |  | ||||||
|         { label: "{{t('Many to one')}}", value: 'belongsTo' }, |  | ||||||
|         { label: "{{t('Many to many')}}", value: 'belongsToMany' }, |  | ||||||
|       ], |  | ||||||
|     }, |  | ||||||
|     grid: { |     grid: { | ||||||
|       type: 'void', |       type: 'void', | ||||||
|       'x-component': 'Grid', |       'x-component': 'Grid', | ||||||
| @ -119,13 +106,13 @@ export const m2m: IField = { | |||||||
|                 through: { |                 through: { | ||||||
|                   type: 'string', |                   type: 'string', | ||||||
|                   title: '{{t("Through collection")}}', |                   title: '{{t("Through collection")}}', | ||||||
|  |                   description: '{{ t("Generated automatically if left blank") }}', | ||||||
|                   'x-decorator': 'FormItem', |                   'x-decorator': 'FormItem', | ||||||
|                   'x-disabled': '{{ !createOnly }}', |                   'x-disabled': '{{ !createOnly }}', | ||||||
|                   'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'], |                   'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'], | ||||||
|                   'x-component': 'Select', |                   'x-component': 'Select', | ||||||
|                   'x-component-props': { |                   'x-component-props': { | ||||||
|                     allowClear: true, |                     allowClear: true, | ||||||
|                     placeholder: '留空时,自动生成中间表' |  | ||||||
|                   }, |                   }, | ||||||
|                 }, |                 }, | ||||||
|               }, |               }, | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { ISchema } from '@formily/react'; | import { ISchema } from '@formily/react'; | ||||||
| import { cloneDeep } from 'lodash'; | import { cloneDeep } from 'lodash'; | ||||||
| import { recordPickerSelector, recordPickerViewer } from './properties'; | import { recordPickerSelector, recordPickerViewer, relationshipType } from './properties'; | ||||||
| import { IField } from './types'; | import { IField } from './types'; | ||||||
| 
 | 
 | ||||||
| export const m2o: IField = { | export const m2o: IField = { | ||||||
| @ -73,20 +73,7 @@ export const m2o: IField = { | |||||||
|       description: |       description: | ||||||
|         "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}", |         "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}", | ||||||
|     }, |     }, | ||||||
|     type: { |     type: relationshipType, | ||||||
|       type: 'string', |  | ||||||
|       title: '{{t("Relationship type")}}', |  | ||||||
|       required: true, |  | ||||||
|       'x-disabled': true, |  | ||||||
|       'x-decorator': 'FormItem', |  | ||||||
|       'x-component': 'Select', |  | ||||||
|       enum: [ |  | ||||||
|         { label: "{{t('One to one')}}", value: 'hasOne' }, |  | ||||||
|         { label: "{{t('One to many')}}", value: 'hasMany' }, |  | ||||||
|         { label: "{{t('Many to one')}}", value: 'belongsTo' }, |  | ||||||
|         { label: "{{t('Many to many')}}", value: 'belongsToMany' }, |  | ||||||
|       ], |  | ||||||
|     }, |  | ||||||
|     grid: { |     grid: { | ||||||
|       type: 'void', |       type: 'void', | ||||||
|       'x-component': 'Grid', |       'x-component': 'Grid', | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| import { ISchema } from '@formily/react'; | import { ISchema } from '@formily/react'; | ||||||
|  | import { relationshipType } from './properties'; | ||||||
| import { IField } from './types'; | import { IField } from './types'; | ||||||
| 
 | 
 | ||||||
| export const o2m: IField = { | export const o2m: IField = { | ||||||
| @ -111,20 +112,7 @@ export const o2m: IField = { | |||||||
|       description: |       description: | ||||||
|         "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}", |         "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}", | ||||||
|     }, |     }, | ||||||
|     type: { |     type: relationshipType, | ||||||
|       type: 'string', |  | ||||||
|       title: '{{t("Relationship type")}}', |  | ||||||
|       required: true, |  | ||||||
|       'x-disabled': true, |  | ||||||
|       'x-decorator': 'FormItem', |  | ||||||
|       'x-component': 'Select', |  | ||||||
|       enum: [ |  | ||||||
|         { label: "{{t('One to one')}}", value: 'hasOne' }, |  | ||||||
|         { label: "{{t('One to many')}}", value: 'hasMany' }, |  | ||||||
|         { label: "{{t('Many to one')}}", value: 'belongsTo' }, |  | ||||||
|         { label: "{{t('Many to many')}}", value: 'belongsToMany' }, |  | ||||||
|       ], |  | ||||||
|     }, |  | ||||||
|     grid: { |     grid: { | ||||||
|       type: 'void', |       type: 'void', | ||||||
|       'x-component': 'Grid', |       'x-component': 'Grid', | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { ISchema } from '@formily/react'; | import { ISchema } from '@formily/react'; | ||||||
| import { cloneDeep } from 'lodash'; | import { cloneDeep } from 'lodash'; | ||||||
| import { recordPickerSelector, recordPickerViewer } from './properties'; | import { recordPickerSelector, recordPickerViewer, relationshipType } from './properties'; | ||||||
| import { IField } from './types'; | import { IField } from './types'; | ||||||
| 
 | 
 | ||||||
| export const o2o: IField = { | export const o2o: IField = { | ||||||
| @ -27,9 +27,9 @@ export const o2o: IField = { | |||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     reverseField: { |     reverseField: { | ||||||
|       interface: 'm2o', |       interface: 'obo', | ||||||
|       type: 'belongsTo', |       type: 'belongsTo', | ||||||
|       title: '{{t("One to one")}}', |       // title: '{{t("One to one (belongs to)")}}',
 | ||||||
|       // name,
 |       // name,
 | ||||||
|       uiSchema: { |       uiSchema: { | ||||||
|         // title,
 |         // title,
 | ||||||
| @ -74,20 +74,7 @@ export const o2o: IField = { | |||||||
|       description: |       description: | ||||||
|         "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}", |         "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}", | ||||||
|     }, |     }, | ||||||
|     type: { |     type: relationshipType, | ||||||
|       type: 'string', |  | ||||||
|       title: '{{t("Relationship type")}}', |  | ||||||
|       required: true, |  | ||||||
|       'x-disabled': true, |  | ||||||
|       'x-decorator': 'FormItem', |  | ||||||
|       'x-component': 'Select', |  | ||||||
|       enum: [ |  | ||||||
|         { label: "{{t('One to one')}}", value: 'hasOne' }, |  | ||||||
|         { label: "{{t('One to many')}}", value: 'hasMany' }, |  | ||||||
|         { label: "{{t('Many to one')}}", value: 'belongsTo' }, |  | ||||||
|         { label: "{{t('Many to many')}}", value: 'belongsToMany' }, |  | ||||||
|       ], |  | ||||||
|     }, |  | ||||||
|     grid: { |     grid: { | ||||||
|       type: 'void', |       type: 'void', | ||||||
|       'x-component': 'Grid', |       'x-component': 'Grid', | ||||||
| @ -183,3 +170,339 @@ export const o2o: IField = { | |||||||
|     ], |     ], | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export const oho: IField = { | ||||||
|  |   name: 'oho', | ||||||
|  |   type: 'object', | ||||||
|  |   group: 'relation', | ||||||
|  |   order: 3, | ||||||
|  |   title: '{{t("One to one (has one)")}}', | ||||||
|  |   description: '{{t("One to one description")}}', | ||||||
|  |   isAssociation: true, | ||||||
|  |   default: { | ||||||
|  |     type: 'hasOne', | ||||||
|  |     // name,
 | ||||||
|  |     uiSchema: { | ||||||
|  |       // title,
 | ||||||
|  |       'x-component': 'RecordPicker', | ||||||
|  |       'x-component-props': { | ||||||
|  |         // mode: 'tags',
 | ||||||
|  |         multiple: false, | ||||||
|  |         fieldNames: { | ||||||
|  |           label: 'id', | ||||||
|  |           value: 'id', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     reverseField: { | ||||||
|  |       interface: 'obo', | ||||||
|  |       type: 'belongsTo', | ||||||
|  |       // title: '{{t("One to one (belongs to)")}}',
 | ||||||
|  |       // name,
 | ||||||
|  |       uiSchema: { | ||||||
|  |         // title,
 | ||||||
|  |         'x-component': 'RecordPicker', | ||||||
|  |         'x-component-props': { | ||||||
|  |           // mode: 'tags',
 | ||||||
|  |           multiple: false, | ||||||
|  |           fieldNames: { | ||||||
|  |             label: 'id', | ||||||
|  |             value: 'id', | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   schemaInitialize(schema: ISchema, { readPretty }) { | ||||||
|  |     if (readPretty) { | ||||||
|  |       schema['properties'] = { | ||||||
|  |         viewer: cloneDeep(recordPickerViewer), | ||||||
|  |       }; | ||||||
|  |     } else { | ||||||
|  |       schema['properties'] = { | ||||||
|  |         selector: cloneDeep(recordPickerSelector), | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   properties: { | ||||||
|  |     'uiSchema.title': { | ||||||
|  |       type: 'string', | ||||||
|  |       title: '{{t("Field display name")}}', | ||||||
|  |       required: true, | ||||||
|  |       'x-decorator': 'FormItem', | ||||||
|  |       'x-component': 'Input', | ||||||
|  |     }, | ||||||
|  |     name: { | ||||||
|  |       type: 'string', | ||||||
|  |       title: '{{t("Field name")}}', | ||||||
|  |       required: true, | ||||||
|  |       'x-disabled': '{{ !createOnly }}', | ||||||
|  |       'x-decorator': 'FormItem', | ||||||
|  |       'x-component': 'Input', | ||||||
|  |       description: | ||||||
|  |         "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}", | ||||||
|  |     }, | ||||||
|  |     type: relationshipType, | ||||||
|  |     grid: { | ||||||
|  |       type: 'void', | ||||||
|  |       'x-component': 'Grid', | ||||||
|  |       properties: { | ||||||
|  |         row1: { | ||||||
|  |           type: 'void', | ||||||
|  |           'x-component': 'Grid.Row', | ||||||
|  |           properties: { | ||||||
|  |             col11: { | ||||||
|  |               type: 'void', | ||||||
|  |               'x-component': 'Grid.Col', | ||||||
|  |               properties: { | ||||||
|  |                 source: { | ||||||
|  |                   type: 'void', | ||||||
|  |                   title: '{{t("Source collection")}}', | ||||||
|  |                   'x-decorator': 'FormItem', | ||||||
|  |                   'x-component': 'SourceCollection', | ||||||
|  |                   'x-disabled': true, | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |             col12: { | ||||||
|  |               type: 'void', | ||||||
|  |               'x-component': 'Grid.Col', | ||||||
|  |               properties: { | ||||||
|  |                 target: { | ||||||
|  |                   type: 'string', | ||||||
|  |                   title: '{{t("Target collection")}}', | ||||||
|  |                   required: true, | ||||||
|  |                   'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'], | ||||||
|  |                   'x-decorator': 'FormItem', | ||||||
|  |                   'x-component': 'Select', | ||||||
|  |                   'x-disabled': '{{ !createOnly }}', | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |         row2: { | ||||||
|  |           type: 'void', | ||||||
|  |           'x-component': 'Grid.Row', | ||||||
|  |           properties: { | ||||||
|  |             col21: { | ||||||
|  |               type: 'void', | ||||||
|  |               'x-component': 'Grid.Col', | ||||||
|  |               properties: { | ||||||
|  |                 sourceKey: { | ||||||
|  |                   type: 'void', | ||||||
|  |                   title: '{{t("Source key")}}', | ||||||
|  |                   'x-decorator': 'FormItem', | ||||||
|  |                   'x-component': 'SourceKey', | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |             col22: { | ||||||
|  |               type: 'void', | ||||||
|  |               'x-component': 'Grid.Col', | ||||||
|  |               properties: { | ||||||
|  |                 foreignKey: { | ||||||
|  |                   type: 'string', | ||||||
|  |                   title: '{{t("Foreign key")}}', | ||||||
|  |                   required: true, | ||||||
|  |                   default: '{{ useNewId("f_") }}', | ||||||
|  |                   description: | ||||||
|  |         "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}", | ||||||
|  |                   'x-decorator': 'FormItem', | ||||||
|  |                   'x-component': 'Input', | ||||||
|  |                   'x-disabled': '{{ !createOnly }}', | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   filterable: { | ||||||
|  |     nested: true, | ||||||
|  |     children: [ | ||||||
|  |       // {
 | ||||||
|  |       //   name: 'id',
 | ||||||
|  |       //   title: '{{t("Exists")}}',
 | ||||||
|  |       //   operators: [
 | ||||||
|  |       //     { label: '{{t("exists")}}', value: '$exists', noValue: true },
 | ||||||
|  |       //     { label: '{{t("not exists")}}', value: '$notExists', noValue: true },
 | ||||||
|  |       //   ],
 | ||||||
|  |       //   schema: {
 | ||||||
|  |       //     title: '{{t("Exists")}}',
 | ||||||
|  |       //     type: 'string',
 | ||||||
|  |       //     'x-component': 'Input',
 | ||||||
|  |       //   },
 | ||||||
|  |       // },
 | ||||||
|  |     ], | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const obo: IField = { | ||||||
|  |   name: 'obo', | ||||||
|  |   type: 'object', | ||||||
|  |   group: 'relation', | ||||||
|  |   order: 3, | ||||||
|  |   title: '{{t("One to one (belongs to)")}}', | ||||||
|  |   description: '{{t("One to one description")}}', | ||||||
|  |   isAssociation: true, | ||||||
|  |   default: { | ||||||
|  |     type: 'belongsTo', | ||||||
|  |     // name,
 | ||||||
|  |     uiSchema: { | ||||||
|  |       // title,
 | ||||||
|  |       'x-component': 'RecordPicker', | ||||||
|  |       'x-component-props': { | ||||||
|  |         // mode: 'tags',
 | ||||||
|  |         multiple: false, | ||||||
|  |         fieldNames: { | ||||||
|  |           label: 'id', | ||||||
|  |           value: 'id', | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     reverseField: { | ||||||
|  |       interface: 'oho', | ||||||
|  |       type: 'hasOne', | ||||||
|  |       // name,
 | ||||||
|  |       uiSchema: { | ||||||
|  |         // title,
 | ||||||
|  |         'x-component': 'RecordPicker', | ||||||
|  |         'x-component-props': { | ||||||
|  |           // mode: 'tags',
 | ||||||
|  |           multiple: true, | ||||||
|  |           fieldNames: { | ||||||
|  |             label: 'id', | ||||||
|  |             value: 'id', | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   schemaInitialize(schema: ISchema, { readPretty }) { | ||||||
|  |     if (readPretty) { | ||||||
|  |       schema['properties'] = { | ||||||
|  |         viewer: cloneDeep(recordPickerViewer), | ||||||
|  |       }; | ||||||
|  |     } else { | ||||||
|  |       schema['properties'] = { | ||||||
|  |         selector: cloneDeep(recordPickerSelector), | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   properties: { | ||||||
|  |     'uiSchema.title': { | ||||||
|  |       type: 'string', | ||||||
|  |       title: '{{t("Field display name")}}', | ||||||
|  |       required: true, | ||||||
|  |       'x-decorator': 'FormItem', | ||||||
|  |       'x-component': 'Input', | ||||||
|  |     }, | ||||||
|  |     name: { | ||||||
|  |       type: 'string', | ||||||
|  |       title: '{{t("Field name")}}', | ||||||
|  |       required: true, | ||||||
|  |       'x-disabled': '{{ !createOnly }}', | ||||||
|  |       'x-decorator': 'FormItem', | ||||||
|  |       'x-component': 'Input', | ||||||
|  |       description: | ||||||
|  |         "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}", | ||||||
|  |     }, | ||||||
|  |     type: relationshipType, | ||||||
|  |     grid: { | ||||||
|  |       type: 'void', | ||||||
|  |       'x-component': 'Grid', | ||||||
|  |       properties: { | ||||||
|  |         row1: { | ||||||
|  |           type: 'void', | ||||||
|  |           'x-component': 'Grid.Row', | ||||||
|  |           properties: { | ||||||
|  |             col11: { | ||||||
|  |               type: 'void', | ||||||
|  |               'x-component': 'Grid.Col', | ||||||
|  |               properties: { | ||||||
|  |                 source: { | ||||||
|  |                   type: 'void', | ||||||
|  |                   title: '{{t("Source collection")}}', | ||||||
|  |                   'x-decorator': 'FormItem', | ||||||
|  |                   'x-component': 'SourceCollection', | ||||||
|  |                   'x-disabled': true, | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |             col12: { | ||||||
|  |               type: 'void', | ||||||
|  |               'x-component': 'Grid.Col', | ||||||
|  |               properties: { | ||||||
|  |                 target: { | ||||||
|  |                   type: 'string', | ||||||
|  |                   title: '{{t("Target collection")}}', | ||||||
|  |                   required: true, | ||||||
|  |                   'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'], | ||||||
|  |                   'x-decorator': 'FormItem', | ||||||
|  |                   'x-component': 'Select', | ||||||
|  |                   'x-disabled': '{{ !createOnly }}', | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |         row2: { | ||||||
|  |           type: 'void', | ||||||
|  |           'x-component': 'Grid.Row', | ||||||
|  |           properties: { | ||||||
|  |             col21: { | ||||||
|  |               type: 'void', | ||||||
|  |               'x-component': 'Grid.Col', | ||||||
|  |               properties: { | ||||||
|  |                 foreignKey: { | ||||||
|  |                   type: 'string', | ||||||
|  |                   title: '{{t("Foreign key")}}', | ||||||
|  |                   required: true, | ||||||
|  |                   default: '{{ useNewId("f_") }}', | ||||||
|  |                   description: | ||||||
|  |         "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}", | ||||||
|  |                   'x-decorator': 'FormItem', | ||||||
|  |                   'x-component': 'Input', | ||||||
|  |                   'x-disabled': '{{ !createOnly }}', | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |             col22: { | ||||||
|  |               type: 'void', | ||||||
|  |               'x-component': 'Grid.Col', | ||||||
|  |               properties: { | ||||||
|  |                 targetKey: { | ||||||
|  |                   type: 'void', | ||||||
|  |                   title: '{{t("Target key")}}', | ||||||
|  |                   'x-decorator': 'FormItem', | ||||||
|  |                   'x-component': 'TargetKey', | ||||||
|  |                   'x-disabled': '{{ !createOnly }}', | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   filterable: { | ||||||
|  |     nested: true, | ||||||
|  |     children: [ | ||||||
|  |       // {
 | ||||||
|  |       //   name: 'id',
 | ||||||
|  |       //   title: '{{t("Exists")}}',
 | ||||||
|  |       //   operators: [
 | ||||||
|  |       //     { label: '{{t("exists")}}', value: '$exists', noValue: true },
 | ||||||
|  |       //     { label: '{{t("not exists")}}', value: '$notExists', noValue: true },
 | ||||||
|  |       //   ],
 | ||||||
|  |       //   schema: {
 | ||||||
|  |       //     title: '{{t("Exists")}}',
 | ||||||
|  |       //     type: 'string',
 | ||||||
|  |       //     'x-component': 'Input',
 | ||||||
|  |       //   },
 | ||||||
|  |       // },
 | ||||||
|  |     ], | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | |||||||
| @ -29,6 +29,21 @@ export const type: ISchema = { | |||||||
|   ], |   ], | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | export const relationshipType: ISchema ={ | ||||||
|  |   type: 'string', | ||||||
|  |   title: '{{t("Relationship type")}}', | ||||||
|  |   required: true, | ||||||
|  |   'x-disabled': true, | ||||||
|  |   'x-decorator': 'FormItem', | ||||||
|  |   'x-component': 'Select', | ||||||
|  |   enum: [ | ||||||
|  |     { label: "{{t('HasOne')}}", value: 'hasOne' }, | ||||||
|  |     { label: "{{t('HasMany')}}", value: 'hasMany' }, | ||||||
|  |     { label: "{{t('BelongsTo')}}", value: 'belongsTo' }, | ||||||
|  |     { label: "{{t('BelongsToMany')}}", value: 'belongsToMany' }, | ||||||
|  |   ], | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| export const dateTimeProps: { [key: string]: ISchema } = { | export const dateTimeProps: { [key: string]: ISchema } = { | ||||||
|   'uiSchema.x-component-props.dateFormat': { |   'uiSchema.x-component-props.dateFormat': { | ||||||
|     type: 'string', |     type: 'string', | ||||||
|  | |||||||
| @ -456,8 +456,8 @@ export default { | |||||||
|   "By month": "By month", |   "By month": "By month", | ||||||
|   "By field": "By field", |   "By field": "By field", | ||||||
|   "By custom date": "By custom date", |   "By custom date": "By custom date", | ||||||
|   "Advance": "Advance", |   "Advanced": "Advanced", | ||||||
|   "Advance type": "Advance type", |   "Advanced type": "Advanced", | ||||||
|   "End": "End", |   "End": "End", | ||||||
|   "Trigger context": "Trigger context", |   "Trigger context": "Trigger context", | ||||||
|   "Node result": "Node result", |   "Node result": "Node result", | ||||||
| @ -538,4 +538,6 @@ export default { | |||||||
|   "User": "User", |   "User": "User", | ||||||
|   "Field": "Field", |   "Field": "Field", | ||||||
|   "Field value changes": "Field value changes", |   "Field value changes": "Field value changes", | ||||||
|  |   "One to one (has one)": "One to one (has one)", | ||||||
|  |   "One to one (belongs to)": "One to one (belongs to)", | ||||||
| } | } | ||||||
|  | |||||||
| @ -172,9 +172,10 @@ export default { | |||||||
|   "Foreign key 1": "外键1", |   "Foreign key 1": "外键1", | ||||||
|   "Foreign key 2": "外键2", |   "Foreign key 2": "外键2", | ||||||
|   "One to one description": "用于创建一对一关系,比如一个用户会有一套个人资料。", |   "One to one description": "用于创建一对一关系,比如一个用户会有一套个人资料。", | ||||||
|   "One to many description": "用于创建一对多关系,比如一个国家会有多个城市。作为字段存在时,它是一个子表格用于显示目标数据表的数据。创建后,会在目标数据表里自动生成一个 Many to one 字段。", |   "One to many description": "用于创建一对多关系,比如一个国家会有多个城市。作为字段存在时,它是一个子表格用于显示目标数据表的数据。创建后,会在目标数据表里自动生成一个多对一字段。", | ||||||
|   "Many to one description": "用于创建多对一关系,比如一个城市只能属于一个国家,一个国家可以有多个城市。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。创建后,会在目标数据表里自动生成一个 One to many 字段。", |   "Many to one description": "用于创建多对一关系,比如一个城市只能属于一个国家,一个国家可以有多个城市。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。创建后,会在目标数据表里自动生成一个多对一字段。", | ||||||
|   "Many to many description": "用于创建多对多关系,比如一个学生会有多个老师,一个老师也会有多个学生。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。", |   "Many to many description": "用于创建多对多关系,比如一个学生会有多个老师,一个老师也会有多个学生。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。", | ||||||
|  |   "Generated automatically if left blank": "留空时,自动生成中间表", | ||||||
|   "Add filter": "添加筛选条件", |   "Add filter": "添加筛选条件", | ||||||
|   "Add filter group": "添加筛选分组", |   "Add filter group": "添加筛选分组", | ||||||
|   "is": "等于", |   "is": "等于", | ||||||
| @ -493,8 +494,8 @@ export default { | |||||||
|   'By field': '数据表字段', |   'By field': '数据表字段', | ||||||
|   'By custom date': '自定义时间', |   'By custom date': '自定义时间', | ||||||
| 
 | 
 | ||||||
|   'Advance': '高级模式', |   'Advanced': '高级模式', | ||||||
|   'Advance type': '高级类型', |   'Advanced type': '高级类型', | ||||||
| 
 | 
 | ||||||
|   'End': '结束', |   'End': '结束', | ||||||
| 
 | 
 | ||||||
| @ -593,4 +594,6 @@ export default { | |||||||
|   "Select": "选择", |   "Select": "选择", | ||||||
|   "Select Field": "选择字段", |   "Select Field": "选择字段", | ||||||
|   "Field value changes": "变更记录", |   "Field value changes": "变更记录", | ||||||
|  |   "One to one (has one)": "一对一(has one)", | ||||||
|  |   "One to one (belongs to)": "一对一(belongs to)" | ||||||
| } | } | ||||||
|  | |||||||
| @ -33,6 +33,7 @@ export const CalendarDesigner = () => { | |||||||
|   const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {}; |   const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {}; | ||||||
|   const options = useOptions(); |   const options = useOptions(); | ||||||
|   const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {}; |   const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {}; | ||||||
|  |   const defaultResource = fieldSchema?.['x-decorator-props']?.resource; | ||||||
|   return ( |   return ( | ||||||
|     <GeneralSchemaDesigner template={template} title={title || name}> |     <GeneralSchemaDesigner template={template} title={title || name}> | ||||||
|       <SchemaSettings.SelectItem |       <SchemaSettings.SelectItem | ||||||
| @ -129,7 +130,7 @@ export const CalendarDesigner = () => { | |||||||
|         }} |         }} | ||||||
|       /> |       /> | ||||||
|       <SchemaSettings.Divider /> |       <SchemaSettings.Divider /> | ||||||
|       <SchemaSettings.Template componentName={'Calendar'} collectionName={name} /> |       <SchemaSettings.Template componentName={'Calendar'} collectionName={name} resourceName={defaultResource} /> | ||||||
|       <SchemaSettings.Divider /> |       <SchemaSettings.Divider /> | ||||||
|       <SchemaSettings.Remove |       <SchemaSettings.Remove | ||||||
|         removeParentsIfNoChildren |         removeParentsIfNoChildren | ||||||
|  | |||||||
| @ -1,21 +0,0 @@ | |||||||
| import { observer, RecursionField, useFieldSchema } from '@formily/react'; |  | ||||||
| import { Space, Typography } from 'antd'; |  | ||||||
| import React from 'react'; |  | ||||||
| import { useCollectionManager } from '../../../collection-manager/hooks'; |  | ||||||
| import { useCompile } from '../../hooks'; |  | ||||||
| 
 |  | ||||||
| export const FieldSummary = observer((props: any) => { |  | ||||||
|   const { schemaKey } = props; |  | ||||||
|   const { getInterface } = useCollectionManager(); |  | ||||||
|   const compile = useCompile(); |  | ||||||
|   const schema = getInterface(schemaKey); |  | ||||||
| 
 |  | ||||||
|   if (!schema) return (null); |  | ||||||
| 
 |  | ||||||
|   return ( |  | ||||||
|     <div style={{marginBottom: '22px'}}> |  | ||||||
|       <Typography.Title level={3} style={{ margin: '0 0 10px 0' }}>{compile(schema.title)}</Typography.Title> |  | ||||||
|       { schema.description ? (<Typography.Text type="secondary">{compile(schema.description)}</Typography.Text>) : (null)} |  | ||||||
|     </div> |  | ||||||
|   ) |  | ||||||
| }); |  | ||||||
| @ -1 +0,0 @@ | |||||||
| export * from './FieldSummary'; |  | ||||||
| @ -20,10 +20,11 @@ export const FormDesigner = () => { | |||||||
|   const { dn } = useDesignable(); |   const { dn } = useDesignable(); | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { visible } = useActionContext(); |   const { visible } = useActionContext(); | ||||||
|  |   const defaultResource = fieldSchema?.['x-decorator-props']?.resource; | ||||||
|   return ( |   return ( | ||||||
|     <GeneralSchemaDesigner template={template} title={title || name}> |     <GeneralSchemaDesigner template={template} title={title || name}> | ||||||
|       {/* <SchemaSettings.Template componentName={'FormItem'} collectionName={name} /> */} |       {/* <SchemaSettings.Template componentName={'FormItem'} collectionName={name} /> */} | ||||||
|       <SchemaSettings.FormItemTemplate componentName={'FormItem'} collectionName={name} /> |       <SchemaSettings.FormItemTemplate componentName={'FormItem'} collectionName={name} resourceName={defaultResource} /> | ||||||
|       <SchemaSettings.Divider /> |       <SchemaSettings.Divider /> | ||||||
|       <SchemaSettings.Remove |       <SchemaSettings.Remove | ||||||
|         removeParentsIfNoChildren |         removeParentsIfNoChildren | ||||||
| @ -38,6 +39,8 @@ export const FormDesigner = () => { | |||||||
| export const ReadPrettyFormDesigner = () => { | export const ReadPrettyFormDesigner = () => { | ||||||
|   const { name, title } = useCollection(); |   const { name, title } = useCollection(); | ||||||
|   const template = useSchemaTemplate(); |   const template = useSchemaTemplate(); | ||||||
|  |   const fieldSchema = useFieldSchema(); | ||||||
|  |   const defaultResource = fieldSchema?.['x-decorator-props']?.resource; | ||||||
|   return ( |   return ( | ||||||
|     <GeneralSchemaDesigner template={template} title={title || name}> |     <GeneralSchemaDesigner template={template} title={title || name}> | ||||||
|       {/* <SchemaSettings.Template componentName={'ReadPrettyForm'} collectionName={name} /> */} |       {/* <SchemaSettings.Template componentName={'ReadPrettyForm'} collectionName={name} /> */} | ||||||
| @ -45,6 +48,7 @@ export const ReadPrettyFormDesigner = () => { | |||||||
|         insertAdjacentPosition={'beforeEnd'} |         insertAdjacentPosition={'beforeEnd'} | ||||||
|         componentName={'ReadPrettyFormItem'} |         componentName={'ReadPrettyFormItem'} | ||||||
|         collectionName={name} |         collectionName={name} | ||||||
|  |         resourceName={defaultResource} | ||||||
|       /> |       /> | ||||||
|       <SchemaSettings.Divider /> |       <SchemaSettings.Divider /> | ||||||
|       <SchemaSettings.Remove |       <SchemaSettings.Remove | ||||||
| @ -69,6 +73,7 @@ export const DetailsDesigner = () => { | |||||||
|   const sortFields = useSortFields(name); |   const sortFields = useSortFields(name); | ||||||
|   const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {}; |   const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {}; | ||||||
|   const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; |   const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; | ||||||
|  |   const defaultResource = fieldSchema?.['x-decorator-props']?.resource; | ||||||
|   const sort = defaultSort?.map((item: string) => { |   const sort = defaultSort?.map((item: string) => { | ||||||
|     return item.startsWith('-') |     return item.startsWith('-') | ||||||
|       ? { |       ? { | ||||||
| @ -204,7 +209,7 @@ export const DetailsDesigner = () => { | |||||||
|           service.run({ ...service.params?.[0], sort: sortArr }); |           service.run({ ...service.params?.[0], sort: sortArr }); | ||||||
|         }} |         }} | ||||||
|       /> |       /> | ||||||
|       <SchemaSettings.Template componentName={'Details'} collectionName={name} /> |       <SchemaSettings.Template componentName={'Details'} collectionName={name} resourceName={defaultResource} /> | ||||||
|       <SchemaSettings.Divider /> |       <SchemaSettings.Divider /> | ||||||
|       <SchemaSettings.Remove |       <SchemaSettings.Remove | ||||||
|         removeParentsIfNoChildren |         removeParentsIfNoChildren | ||||||
|  | |||||||
| @ -35,5 +35,4 @@ export * from './tabs'; | |||||||
| export * from './time-picker'; | export * from './time-picker'; | ||||||
| export * from './tree-select'; | export * from './tree-select'; | ||||||
| export * from './upload'; | export * from './upload'; | ||||||
| export * from './field-summary'; |  | ||||||
| import './index.less'; | import './index.less'; | ||||||
|  | |||||||
| @ -38,16 +38,17 @@ ReadPretty.TextArea = (props) => { | |||||||
|   const { autop = true, ellipsis, text } = props; |   const { autop = true, ellipsis, text } = props; | ||||||
|   const html = ( |   const html = ( | ||||||
|     <div |     <div | ||||||
|  |       style={{lineHeight: 1.612}} | ||||||
|       dangerouslySetInnerHTML={{ |       dangerouslySetInnerHTML={{ | ||||||
|         __html: HTMLEncode(value).split('\n').join('<br/>'), |         __html: HTMLEncode(value).split('\n').join('<br/>'), | ||||||
|       }} |       }} | ||||||
|     /> |     /> | ||||||
|   ); |   ); | ||||||
|   console.log('value', value); | 
 | ||||||
|   const content = ellipsis ? |   const content = ellipsis ? | ||||||
|     (<EllipsisWithTooltip ellipsis={ellipsis} popoverContent={autop ? html : value}> |     (<EllipsisWithTooltip ellipsis={ellipsis} popoverContent={autop ? html : value}> | ||||||
|       {text} |       {text || value} | ||||||
|     </EllipsisWithTooltip>) : value || html; |     </EllipsisWithTooltip>) : (autop ? html : value); | ||||||
|   return ( |   return ( | ||||||
|     <div className={cls(prefixCls, props.className)} style={props.style}> |     <div className={cls(prefixCls, props.className)} style={props.style}> | ||||||
|       {props.addonBefore} |       {props.addonBefore} | ||||||
|  | |||||||
| @ -41,6 +41,9 @@ const schema = { | |||||||
|       'x-component': 'Input.TextArea', |       'x-component': 'Input.TextArea', | ||||||
|       'x-component-props': { |       'x-component-props': { | ||||||
|         ellipsis: true, |         ellipsis: true, | ||||||
|  |         style: { | ||||||
|  |           width: '100px' | ||||||
|  |         } | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     read3: { |     read3: { | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ export const KanbanDesigner = () => { | |||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { dn } = useDesignable(); |   const { dn } = useDesignable(); | ||||||
|   const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {}; |   const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {}; | ||||||
|  |   const defaultResource = fieldSchema?.['x-decorator-props']?.resource; | ||||||
|   const template = useSchemaTemplate(); |   const template = useSchemaTemplate(); | ||||||
|   return ( |   return ( | ||||||
|     <GeneralSchemaDesigner template={template} title={title || name}> |     <GeneralSchemaDesigner template={template} title={title || name}> | ||||||
| @ -51,7 +52,7 @@ export const KanbanDesigner = () => { | |||||||
|         }} |         }} | ||||||
|       /> |       /> | ||||||
|       <SchemaSettings.Divider /> |       <SchemaSettings.Divider /> | ||||||
|       <SchemaSettings.Template componentName={'Kanban'} collectionName={name} /> |       <SchemaSettings.Template componentName={'Kanban'} collectionName={name} resourceName={defaultResource} /> | ||||||
|       <SchemaSettings.Divider /> |       <SchemaSettings.Divider /> | ||||||
|       <SchemaSettings.Remove |       <SchemaSettings.Remove | ||||||
|         removeParentsIfNoChildren |         removeParentsIfNoChildren | ||||||
|  | |||||||
| @ -6,6 +6,8 @@ import { ReadPretty as InputReadPretty } from '../input'; | |||||||
| import { MarkdownVoid } from './Markdown.Void'; | import { MarkdownVoid } from './Markdown.Void'; | ||||||
| import { convertToText, markdown } from './util'; | import { convertToText, markdown } from './util'; | ||||||
| 
 | 
 | ||||||
|  | import './style.less'; | ||||||
|  | 
 | ||||||
| export const Markdown: any = connect( | export const Markdown: any = connect( | ||||||
|   AntdInput.TextArea, |   AntdInput.TextArea, | ||||||
|   mapProps((props: any, field) => { |   mapProps((props: any, field) => { | ||||||
|  | |||||||
| @ -1,3 +1,7 @@ | |||||||
|  | .nb-markdown { | ||||||
|  |   line-height: 1.612; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .nb-markdown > *:last-child { | .nb-markdown > *:last-child { | ||||||
|   margin-bottom: 0; |   margin-bottom: 0; | ||||||
| } | } | ||||||
|  | |||||||
| @ -65,7 +65,7 @@ export const TableColumnDesigner = (props) => { | |||||||
|           dn.refresh(); |           dn.refresh(); | ||||||
|         }} |         }} | ||||||
|       /> |       /> | ||||||
|       {collectionField?.interface === 'linkTo' && ( |       {['linkTo', 'm2m', 'm2o', 'obo', 'oho'].includes(collectionField?.interface) && ( | ||||||
|         <SchemaSettings.SelectItem |         <SchemaSettings.SelectItem | ||||||
|           title={t('Title field')} |           title={t('Title field')} | ||||||
|           options={options} |           options={options} | ||||||
|  | |||||||
| @ -20,6 +20,7 @@ export const TableBlockDesigner = () => { | |||||||
|   const { dn } = useDesignable(); |   const { dn } = useDesignable(); | ||||||
|   const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {}; |   const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {}; | ||||||
|   const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; |   const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; | ||||||
|  |   const defaultResource = fieldSchema?.['x-decorator-props']?.resource; | ||||||
|   const sort = defaultSort?.map((item: string) => { |   const sort = defaultSort?.map((item: string) => { | ||||||
|     return item.startsWith('-') |     return item.startsWith('-') | ||||||
|       ? { |       ? { | ||||||
| @ -200,7 +201,7 @@ export const TableBlockDesigner = () => { | |||||||
|         }} |         }} | ||||||
|       /> |       /> | ||||||
|       <SchemaSettings.Divider /> |       <SchemaSettings.Divider /> | ||||||
|       <SchemaSettings.Template componentName={'Table'} collectionName={name} /> |       <SchemaSettings.Template componentName={'Table'} collectionName={name} resourceName={defaultResource} /> | ||||||
|       <SchemaSettings.Divider /> |       <SchemaSettings.Divider /> | ||||||
|       <SchemaSettings.Remove |       <SchemaSettings.Remove | ||||||
|         removeParentsIfNoChildren |         removeParentsIfNoChildren | ||||||
|  | |||||||
| @ -52,7 +52,7 @@ SchemaInitializer.Button = observer((props: SchemaInitializerButtonProps) => { | |||||||
|         return ( |         return ( | ||||||
|           Component && ( |           Component && ( | ||||||
|             <SchemaInitializerItemContext.Provider |             <SchemaInitializerItemContext.Provider | ||||||
|               key={`item-${indexA}`} |               key={`${item.key || 'item'}-${indexA}`} | ||||||
|               value={{ |               value={{ | ||||||
|                 index: indexA, |                 index: indexA, | ||||||
|                 item, |                 item, | ||||||
| @ -164,7 +164,6 @@ SchemaInitializer.Item = (props: SchemaInitializerItemProps) => { | |||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
|         if (item.type === 'subMenu') { |         if (item.type === 'subMenu') { | ||||||
|           console.log('item.key', item.key); |  | ||||||
|           return ( |           return ( | ||||||
|             <Menu.SubMenu |             <Menu.SubMenu | ||||||
|               // @ts-ignore
 |               // @ts-ignore
 | ||||||
| @ -192,8 +191,8 @@ SchemaInitializer.Item = (props: SchemaInitializerItemProps) => { | |||||||
|     return ( |     return ( | ||||||
|       <Menu.SubMenu |       <Menu.SubMenu | ||||||
|         // @ts-ignore
 |         // @ts-ignore
 | ||||||
|         eventKey={eventKey ? `${eventKey}-${index}` : index} |         eventKey={eventKey ? `${eventKey}-${index}` : info.key} | ||||||
|         key={eventKey ? `${eventKey}-${index}` : index} |         key={info.key} | ||||||
|         title={compile(children)} |         title={compile(children)} | ||||||
|         icon={typeof icon === 'string' ? <Icon type={icon as string} /> : icon} |         icon={typeof icon === 'string' ? <Icon type={icon as string} /> : icon} | ||||||
|       > |       > | ||||||
| @ -203,12 +202,13 @@ SchemaInitializer.Item = (props: SchemaInitializerItemProps) => { | |||||||
|   } |   } | ||||||
|   return ( |   return ( | ||||||
|     <Menu.Item |     <Menu.Item | ||||||
|       eventKey={eventKey ? `${eventKey}-${index}` : index} |       // {...others}
 | ||||||
|  |       key={info.key} | ||||||
|  |       eventKey={eventKey ? `${eventKey}-${index}` : info.key} | ||||||
|       icon={typeof icon === 'string' ? <Icon type={icon as string} /> : icon} |       icon={typeof icon === 'string' ? <Icon type={icon as string} /> : icon} | ||||||
|       onClick={(opts) => { |       onClick={(opts) => { | ||||||
|         onClick({ ...opts, item: info }); |         onClick({ ...opts, item: info }); | ||||||
|       }} |       }} | ||||||
|       {...others} |  | ||||||
|     > |     > | ||||||
|       {compile(children)} |       {compile(children)} | ||||||
|     </Menu.Item> |     </Menu.Item> | ||||||
|  | |||||||
| @ -5,9 +5,71 @@ import { gridRowColWrap } from '../utils'; | |||||||
| 
 | 
 | ||||||
| const useRelationFields = () => { | const useRelationFields = () => { | ||||||
|   const { fields } = useCollection(); |   const { fields } = useCollection(); | ||||||
|   return fields |   const relationFields = fields | ||||||
|     .filter((field) => ['linkTo', 'subTable'].includes(field.interface)) |     .filter((field) => ['linkTo', 'subTable', 'o2m', 'm2m', 'obo', 'oho', 'o2o', 'm2o'].includes(field.interface)) | ||||||
|     .map((field) => { |     .map((field) => { | ||||||
|  |       if (['hasOne', 'belongsTo'].includes(field.type)) { | ||||||
|  |         return { | ||||||
|  |           key: field.name, | ||||||
|  |           type: 'subMenu', | ||||||
|  |           title: field?.uiSchema?.title || field.name, | ||||||
|  |           children: [ | ||||||
|  |             { | ||||||
|  |               key: `${field.name}_details`, | ||||||
|  |               type: 'item', | ||||||
|  |               title: '{{t("Details")}}', | ||||||
|  |               field, | ||||||
|  |               component: 'RecordReadPrettyAssociationFormBlockInitializer', | ||||||
|  |             }, | ||||||
|  |             // {
 | ||||||
|  |             //   key: `${field.name}_form`,
 | ||||||
|  |             //   type: 'item',
 | ||||||
|  |             //   title: '{{t("Form")}}',
 | ||||||
|  |             //   field,
 | ||||||
|  |             //   component: 'RecordAssociationFormBlockInitializer',
 | ||||||
|  |             // },
 | ||||||
|  |           ], | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (['hasMany', 'belongsToMany'].includes(field.type)) { | ||||||
|  |         return { | ||||||
|  |           key: field.name, | ||||||
|  |           type: 'subMenu', | ||||||
|  |           title: field?.uiSchema?.title || field.name, | ||||||
|  |           children: [ | ||||||
|  |             { | ||||||
|  |               key: `${field.name}_table`, | ||||||
|  |               type: 'item', | ||||||
|  |               title: '{{t("Table")}}', | ||||||
|  |               field, | ||||||
|  |               component: 'RecordAssociationBlockInitializer', | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               key: `${field.name}_details`, | ||||||
|  |               type: 'item', | ||||||
|  |               title: '{{t("Details")}}', | ||||||
|  |               field, | ||||||
|  |               component: 'RecordAssociationDetailsBlockInitializer', | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               key: `${field.name}_form`, | ||||||
|  |               type: 'item', | ||||||
|  |               title: '{{t("Form")}}', | ||||||
|  |               field, | ||||||
|  |               component: 'RecordAssociationFormBlockInitializer', | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               key: `${field.name}_calendar`, | ||||||
|  |               type: 'item', | ||||||
|  |               title: '{{t("Calendar")}}', | ||||||
|  |               field, | ||||||
|  |               component: 'RecordAssociationCalendarBlockInitializer', | ||||||
|  |             }, | ||||||
|  |           ], | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       return { |       return { | ||||||
|         key: field.name, |         key: field.name, | ||||||
|         type: 'item', |         type: 'item', | ||||||
| @ -16,6 +78,7 @@ const useRelationFields = () => { | |||||||
|         component: 'RecordAssociationBlockInitializer', |         component: 'RecordAssociationBlockInitializer', | ||||||
|       }; |       }; | ||||||
|     }) as any; |     }) as any; | ||||||
|  |     return relationFields; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const RecordBlockInitializers = (props: any) => { | export const RecordBlockInitializers = (props: any) => { | ||||||
|  | |||||||
| @ -663,6 +663,7 @@ export const RecordReadPrettyFormBlockInitializer = (props) => { | |||||||
|     <SchemaInitializer.Item |     <SchemaInitializer.Item | ||||||
|       icon={<FormOutlined />} |       icon={<FormOutlined />} | ||||||
|       {...others} |       {...others} | ||||||
|  |       key={'123'} | ||||||
|       onClick={async ({ item }) => { |       onClick={async ({ item }) => { | ||||||
|         if (item.template) { |         if (item.template) { | ||||||
|           const s = await getTemplateSchemaByMode(item); |           const s = await getTemplateSchemaByMode(item); | ||||||
| @ -699,34 +700,256 @@ export const RecordReadPrettyFormBlockInitializer = (props) => { | |||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const RecordAssociationBlockInitializer = (props) => { | export const RecordAssociationFormBlockInitializer = (props) => { | ||||||
|  |   const { item, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; | ||||||
|  |   const { getTemplateSchemaByMode } = useSchemaTemplateManager(); | ||||||
|  |   const field = item.field; | ||||||
|  |   const collection = field.target; | ||||||
|  |   const resource = `${field.collectionName}.${field.name}`; | ||||||
|  |   return ( | ||||||
|  |     <SchemaInitializer.Item | ||||||
|  |       icon={<FormOutlined />} | ||||||
|  |       {...others} | ||||||
|  |       onClick={async ({ item }) => { | ||||||
|  |          | ||||||
|  |         const action = ['hasOne', 'belongsTo'].includes(field.type) ? 'get' : null; | ||||||
|  |         const actionInitializers = ['hasOne', 'belongsTo'].includes(field.type) ? 'UpdateFormActionInitializers' : 'CreateFormActionInitializers'; | ||||||
|  | 
 | ||||||
|  |         if (item.template) { | ||||||
|  |           const s = await getTemplateSchemaByMode(item); | ||||||
|  |           if (item.template.componentName === 'FormItem') { | ||||||
|  |             const blockSchema = createFormBlockSchema({ | ||||||
|  |               collection, | ||||||
|  |               resource, | ||||||
|  |               association: resource, | ||||||
|  |               action, | ||||||
|  |               useSourceId: '{{ useSourceIdFromParentRecord }}', | ||||||
|  |               useParams: '{{ useParamsFromRecord }}', | ||||||
|  |               actionInitializers, | ||||||
|  |               template: s, | ||||||
|  |             }); | ||||||
|  |             if (item.mode === 'reference') { | ||||||
|  |               blockSchema['x-template-key'] = item.template.key; | ||||||
|  |             } | ||||||
|  |             insert(blockSchema); | ||||||
|  |           } else { | ||||||
|  |             insert(s); | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           insert( | ||||||
|  |             createFormBlockSchema({ | ||||||
|  |               collection, | ||||||
|  |               resource, | ||||||
|  |               association: resource, | ||||||
|  |               action, | ||||||
|  |               useSourceId: '{{ useSourceIdFromParentRecord }}', | ||||||
|  |               useParams: '{{ useParamsFromRecord }}', | ||||||
|  |               actionInitializers, | ||||||
|  |             }), | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       }} | ||||||
|  |       items={useRecordCollectionDataSourceItems('FormItem', item, collection, resource)} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const RecordReadPrettyAssociationFormBlockInitializer = (props) => { | ||||||
|  |   const { item, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; | ||||||
|  |   const { getTemplateSchemaByMode } = useSchemaTemplateManager(); | ||||||
|  | 
 | ||||||
|  |   const field = item.field; | ||||||
|  |   const collection = field.target; | ||||||
|  |   const resource = `${field.collectionName}.${field.name}`; | ||||||
|  |   return ( | ||||||
|  |     <SchemaInitializer.Item | ||||||
|  |       icon={<FormOutlined />} | ||||||
|  |       {...others} | ||||||
|  |       onClick={async ({ item }) => { | ||||||
|  |         if (item.template) { | ||||||
|  |           const s = await getTemplateSchemaByMode(item); | ||||||
|  |           if (item.template.componentName === 'ReadPrettyFormItem') { | ||||||
|  |             const blockSchema = createReadPrettyFormBlockSchema({ | ||||||
|  |               collection, | ||||||
|  |               resource, | ||||||
|  |               association: resource, | ||||||
|  |               action: 'get', | ||||||
|  |               useSourceId: '{{ useSourceIdFromParentRecord }}', | ||||||
|  |               useParams: '{{ useParamsFromRecord }}', | ||||||
|  |               template: s, | ||||||
|  |             }); | ||||||
|  |             if (item.mode === 'reference') { | ||||||
|  |               blockSchema['x-template-key'] = item.template.key; | ||||||
|  |             } | ||||||
|  |             insert(blockSchema); | ||||||
|  |           } else { | ||||||
|  |             insert(s); | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           insert( | ||||||
|  |             createReadPrettyFormBlockSchema({ | ||||||
|  |               collection, | ||||||
|  |               resource, | ||||||
|  |               association: resource, | ||||||
|  |               action: 'get', | ||||||
|  |               useSourceId: '{{ useSourceIdFromParentRecord }}', | ||||||
|  |               useParams: '{{ useParamsFromRecord }}', | ||||||
|  |             }), | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       }} | ||||||
|  |       items={useRecordCollectionDataSourceItems('ReadPrettyFormItem', item, collection, resource)} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const RecordAssociationDetailsBlockInitializer = (props) => { | ||||||
|   const { item, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; |   const { item, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; | ||||||
|   const { getTemplateSchemaByMode } = useSchemaTemplateManager(); |   const { getTemplateSchemaByMode } = useSchemaTemplateManager(); | ||||||
|   const { getCollection } = useCollectionManager(); |   const { getCollection } = useCollectionManager(); | ||||||
|  |   const field = item.field; | ||||||
|  |   const collection = getCollection(field.target); | ||||||
|  |   const resource = `${field.collectionName}.${field.name}`; | ||||||
|  |   return ( | ||||||
|  |     <SchemaInitializer.Item | ||||||
|  |       icon={<FormOutlined />} | ||||||
|  |       {...others} | ||||||
|  |       onClick={async ({ item }) => { | ||||||
|  |         if (item.template) { | ||||||
|  |           const s = await getTemplateSchemaByMode(item); | ||||||
|  |           insert(s); | ||||||
|  |         } else { | ||||||
|  |           insert( | ||||||
|  |             createDetailsBlockSchema({ | ||||||
|  |               collection: field.target, | ||||||
|  |               resource, | ||||||
|  |               association: resource, | ||||||
|  |               rowKey: collection.filterTargetKey || 'id', | ||||||
|  |             }), | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       }} | ||||||
|  |       items={useRecordCollectionDataSourceItems('Details', item, field.target, resource)} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const RecordAssociationCalendarBlockInitializer = (props) => { | ||||||
|  |   const { item, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; | ||||||
|  |   const { getTemplateSchemaByMode } = useSchemaTemplateManager(); | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const options = useContext(SchemaOptionsContext); | ||||||
|  |   const { getCollection } = useCollectionManager(); | ||||||
|  |   const field = item.field; | ||||||
|  |   const collection = getCollection(field.target); | ||||||
|  |   const resource = `${field.collectionName}.${field.name}`; | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <SchemaInitializer.Item |     <SchemaInitializer.Item | ||||||
|       icon={<TableOutlined />} |       icon={<TableOutlined />} | ||||||
|       {...others} |       {...others} | ||||||
|       onClick={async ({ item }) => { |       onClick={async ({ item }) => { | ||||||
|         console.log('RecordAssociationBlockInitializer', item); |         if (item.template) { | ||||||
|  |           const s = await getTemplateSchemaByMode(item); | ||||||
|  |           insert(s); | ||||||
|  |         } else { | ||||||
|  |         const stringFields = collection?.fields | ||||||
|  |           ?.filter((field) => field.type === 'string') | ||||||
|  |           ?.map((field) => { | ||||||
|  |             return { | ||||||
|  |               label: field?.uiSchema?.title, | ||||||
|  |               value: field.name, | ||||||
|  |             }; | ||||||
|  |           }); | ||||||
|  |         const dateFields = collection?.fields | ||||||
|  |           ?.filter((field) => field.type === 'date') | ||||||
|  |           ?.map((field) => { | ||||||
|  |             return { | ||||||
|  |               label: field?.uiSchema?.title, | ||||||
|  |               value: field.name, | ||||||
|  |             }; | ||||||
|  |           }); | ||||||
|  |         const values = await FormDialog(t('Create calendar block'), () => { | ||||||
|  |           return ( | ||||||
|  |             <SchemaComponentOptions scope={options.scope} components={{ ...options.components }}> | ||||||
|  |               <FormLayout layout={'vertical'}> | ||||||
|  |                 <SchemaComponent | ||||||
|  |                   schema={{ | ||||||
|  |                     properties: { | ||||||
|  |                       title: { | ||||||
|  |                         title: t('Title field'), | ||||||
|  |                         enum: stringFields, | ||||||
|  |                         required: true, | ||||||
|  |                         'x-component': 'Select', | ||||||
|  |                         'x-decorator': 'FormItem', | ||||||
|  |                       }, | ||||||
|  |                       start: { | ||||||
|  |                         title: t('Start date field'), | ||||||
|  |                         enum: dateFields, | ||||||
|  |                         required: true, | ||||||
|  |                         default: 'createdAt', | ||||||
|  |                         'x-component': 'Select', | ||||||
|  |                         'x-decorator': 'FormItem', | ||||||
|  |                       }, | ||||||
|  |                       end: { | ||||||
|  |                         title: t('End date field'), | ||||||
|  |                         enum: dateFields, | ||||||
|  |                         'x-component': 'Select', | ||||||
|  |                         'x-decorator': 'FormItem', | ||||||
|  |                       }, | ||||||
|  |                     }, | ||||||
|  |                   }} | ||||||
|  |                 /> | ||||||
|  |               </FormLayout> | ||||||
|  |             </SchemaComponentOptions> | ||||||
|  |           ); | ||||||
|  |         }).open({ | ||||||
|  |           initialValues: {}, | ||||||
|  |         }); | ||||||
|  |         insert( | ||||||
|  |           createCalendarBlockSchema({ | ||||||
|  |             collection: field.target, | ||||||
|  |             resource, | ||||||
|  |             association: resource, | ||||||
|  |             fieldNames: { | ||||||
|  |               ...values, | ||||||
|  |             }, | ||||||
|  |           }), | ||||||
|  |         ); | ||||||
|  |         } | ||||||
|  |       }} | ||||||
|  |       items={useRecordCollectionDataSourceItems('Calendar', item, field.target, resource)} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const RecordAssociationBlockInitializer = (props) => { | ||||||
|  |   const { item, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; | ||||||
|  |   const { getTemplateSchemaByMode } = useSchemaTemplateManager(); | ||||||
|  |   const { getCollection } = useCollectionManager(); | ||||||
|   const field = item.field; |   const field = item.field; | ||||||
|   const collection = getCollection(field.target); |   const collection = getCollection(field.target); | ||||||
|  |   const resource = `${field.collectionName}.${field.name}`; | ||||||
|  |   return ( | ||||||
|  |     <SchemaInitializer.Item | ||||||
|  |       icon={<TableOutlined />} | ||||||
|  |       {...others} | ||||||
|  |       onClick={async ({ item }) => { | ||||||
|  |         if (item.template) { | ||||||
|  |           const s = await getTemplateSchemaByMode(item); | ||||||
|  |           insert(s); | ||||||
|  |         } else { | ||||||
|           insert( |           insert( | ||||||
|             createTableBlockSchema({ |             createTableBlockSchema({ | ||||||
|               rowKey: collection.filterTargetKey, |               rowKey: collection.filterTargetKey, | ||||||
|               collection: field.target, |               collection: field.target, | ||||||
|             resource: `${field.collectionName}.${field.name}`, |               resource, | ||||||
|             association: `${field.collectionName}.${field.name}`, |               association: resource, | ||||||
|             }), |             }), | ||||||
|           ); |           ); | ||||||
|         // if (item.template) {
 |         } | ||||||
|         //   const s = await getTemplateSchemaByMode(item);
 |  | ||||||
|         //   insert(s);
 |  | ||||||
|         // } else {
 |  | ||||||
|         //   insert(createTableBlockSchema({ collection: item.name }));
 |  | ||||||
|         // }
 |  | ||||||
|       }} |       }} | ||||||
|       // items={useRecordCollectionDataSourceItems('Table')}
 |       items={useRecordCollectionDataSourceItems('Table', item, field.target, resource)} | ||||||
|     /> |     /> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -197,11 +197,11 @@ export const useCurrentSchema = (action: string, key: string, find = findSchema, | |||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const useRecordCollectionDataSourceItems = (componentName) => { | export const useRecordCollectionDataSourceItems = (componentName, item = null, collectionName = null, resourceName = null) => { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const collection = useCollection(); |   const collection = useCollection(); | ||||||
|   const { getTemplatesByCollection } = useSchemaTemplateManager(); |   const { getTemplatesByCollection } = useSchemaTemplateManager(); | ||||||
|   const templates = getTemplatesByCollection(collection.name).filter((template) => { |   const templates = getTemplatesByCollection(collectionName || collection.name, resourceName || collectionName || collection.name).filter((template) => { | ||||||
|     return componentName && template.componentName === componentName; |     return componentName && template.componentName === componentName; | ||||||
|   }); |   }); | ||||||
|   if (!templates.length) { |   if (!templates.length) { | ||||||
| @ -210,15 +210,17 @@ export const useRecordCollectionDataSourceItems = (componentName) => { | |||||||
|   const index = 0; |   const index = 0; | ||||||
|   return [ |   return [ | ||||||
|     { |     { | ||||||
|  |       key: `${collectionName || componentName}_table_blank`, | ||||||
|       type: 'item', |       type: 'item', | ||||||
|       name: collection.name, |       name: collection.name, | ||||||
|       title: t('Blank block'), |       title: t('Blank block'), | ||||||
|  |       item, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       type: 'divider', |       type: 'divider', | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       key: `${componentName}_table_subMenu_${index}_copy`, |       key: `${collectionName || componentName}_table_subMenu_${index}_copy`, | ||||||
|       type: 'subMenu', |       type: 'subMenu', | ||||||
|       name: 'copy', |       name: 'copy', | ||||||
|       title: t('Duplicate template'), |       title: t('Duplicate template'), | ||||||
| @ -230,12 +232,13 @@ export const useRecordCollectionDataSourceItems = (componentName) => { | |||||||
|           mode: 'copy', |           mode: 'copy', | ||||||
|           name: collection.name, |           name: collection.name, | ||||||
|           template, |           template, | ||||||
|  |           item, | ||||||
|           title: templateName || t('Untitled'), |           title: templateName || t('Untitled'), | ||||||
|         }; |         }; | ||||||
|       }), |       }), | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       key: `${componentName}_table_subMenu_${index}_ref`, |       key: `${collectionName || componentName}_table_subMenu_${index}_ref`, | ||||||
|       type: 'subMenu', |       type: 'subMenu', | ||||||
|       name: 'ref', |       name: 'ref', | ||||||
|       title: t('Reference template'), |       title: t('Reference template'), | ||||||
| @ -247,6 +250,7 @@ export const useRecordCollectionDataSourceItems = (componentName) => { | |||||||
|           mode: 'reference', |           mode: 'reference', | ||||||
|           name: collection.name, |           name: collection.name, | ||||||
|           template, |           template, | ||||||
|  |           item, | ||||||
|           title: templateName || t('Untitled'), |           title: templateName || t('Untitled'), | ||||||
|         }; |         }; | ||||||
|       }), |       }), | ||||||
| @ -266,7 +270,7 @@ export const useCollectionDataSourceItems = (componentName) => { | |||||||
|       children: collections |       children: collections | ||||||
|         ?.filter((item) => !item.inherit) |         ?.filter((item) => !item.inherit) | ||||||
|         ?.map((item, index) => { |         ?.map((item, index) => { | ||||||
|           const templates = getTemplatesByCollection(item.name).filter((template) => { |           const templates = getTemplatesByCollection(item.name, item.name).filter((template) => { | ||||||
|             return componentName && template.componentName === componentName; |             return componentName && template.componentName === componentName; | ||||||
|           }); |           }); | ||||||
|           if (!templates.length) { |           if (!templates.length) { | ||||||
| @ -412,6 +416,7 @@ export const createFormBlockSchema = (options) => { | |||||||
|     template, |     template, | ||||||
|     ...others |     ...others | ||||||
|   } = options; |   } = options; | ||||||
|  |   console.log('createFormBlockSchema', options); | ||||||
|   const resourceName = resource || association || collection; |   const resourceName = resource || association || collection; | ||||||
|   const schema: ISchema = { |   const schema: ISchema = { | ||||||
|     type: 'void', |     type: 'void', | ||||||
|  | |||||||
| @ -117,7 +117,7 @@ export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNeste | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| SchemaSettings.Template = (props) => { | SchemaSettings.Template = (props) => { | ||||||
|   const { componentName, collectionName } = props; |   const { componentName, collectionName, resourceName } = props; | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { dn, setVisible, template, fieldSchema } = useSchemaSettings(); |   const { dn, setVisible, template, fieldSchema } = useSchemaSettings(); | ||||||
|   const api = useAPIClient(); |   const api = useAPIClient(); | ||||||
| @ -178,6 +178,7 @@ SchemaSettings.Template = (props) => { | |||||||
|         sdn.loadAPIClientEvents(); |         sdn.loadAPIClientEvents(); | ||||||
|         const { key } = await saveAsTemplate({ |         const { key } = await saveAsTemplate({ | ||||||
|           collectionName, |           collectionName, | ||||||
|  |           resourceName, | ||||||
|           componentName, |           componentName, | ||||||
|           name: values.name, |           name: values.name, | ||||||
|           uid: fieldSchema['x-uid'], |           uid: fieldSchema['x-uid'], | ||||||
| @ -232,7 +233,8 @@ const findBlockTemplateSchema = (fieldSchema) => { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| SchemaSettings.FormItemTemplate = (props) => { | SchemaSettings.FormItemTemplate = (props) => { | ||||||
|   const { insertAdjacentPosition = 'afterBegin', componentName, collectionName } = props; |   const { insertAdjacentPosition = 'afterBegin', componentName, collectionName, resourceName } = props; | ||||||
|  |   console.log('SchemaSettings.Template', props); | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { dn, setVisible, template, fieldSchema } = useSchemaSettings(); |   const { dn, setVisible, template, fieldSchema } = useSchemaSettings(); | ||||||
|   const api = useAPIClient(); |   const api = useAPIClient(); | ||||||
| @ -312,6 +314,7 @@ SchemaSettings.FormItemTemplate = (props) => { | |||||||
|         sdn.loadAPIClientEvents(); |         sdn.loadAPIClientEvents(); | ||||||
|         const { key } = await saveAsTemplate({ |         const { key } = await saveAsTemplate({ | ||||||
|           collectionName, |           collectionName, | ||||||
|  |           resourceName, | ||||||
|           componentName, |           componentName, | ||||||
|           name: values.name, |           name: values.name, | ||||||
|           uid: gridSchema['x-uid'], |           uid: gridSchema['x-uid'], | ||||||
|  | |||||||
| @ -119,10 +119,11 @@ export const useSchemaTemplateManager = () => { | |||||||
|     getTemplateById(key) { |     getTemplateById(key) { | ||||||
|       return templates?.find((template) => template.key === key); |       return templates?.find((template) => template.key === key); | ||||||
|     }, |     }, | ||||||
|     getTemplatesByCollection(collectionName: string) { |     getTemplatesByCollection(collectionName: string, resourceName: string = null) { | ||||||
|       const items = templates?.filter?.((template) => template.collectionName === collectionName); |       const items = templates?.filter?.((template) => (!template.resourceName && template.collectionName === collectionName) || (template.resourceName && template.resourceName === resourceName)); | ||||||
|       return items || []; |       return items || []; | ||||||
|     }, |     }, | ||||||
|  | 
 | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,11 +1,11 @@ | |||||||
| import React from "react"; |  | ||||||
| import { css } from "@emotion/css"; | import { css } from "@emotion/css"; | ||||||
| import { InputNumber, Select } from "antd"; | import { InputNumber, Select } from "antd"; | ||||||
|  | import React from "react"; | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
| import { Cron } from 'react-js-cron'; | import { Cron } from 'react-js-cron'; | ||||||
| 
 |  | ||||||
| import CronZhCN from './locale/Cron.zh-CN'; | import CronZhCN from './locale/Cron.zh-CN'; | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| const languages = { | const languages = { | ||||||
|   'zh-CN': CronZhCN, |   'zh-CN': CronZhCN, | ||||||
| }; | }; | ||||||
| @ -18,7 +18,7 @@ const RepeatOptions = [ | |||||||
|   { value: 86400_000, text: 'By day', unitText: 'Days' }, |   { value: 86400_000, text: 'By day', unitText: 'Days' }, | ||||||
|   { value: 604800_000, text: 'By week', unitText: 'Weeks' }, |   { value: 604800_000, text: 'By week', unitText: 'Weeks' }, | ||||||
|   // { value: 18144_000_000, text: 'By 30 days' },
 |   // { value: 18144_000_000, text: 'By 30 days' },
 | ||||||
|   { value: 'cron', text: 'Advance' } |   { value: 'cron', text: 'Advanced' } | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| function getNumberOption(v) { | function getNumberOption(v) { | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import { Database } from '../../database'; |  | ||||||
| import { mockDatabase } from '..'; | import { mockDatabase } from '..'; | ||||||
|  | import { Database } from '../../database'; | ||||||
| import { FormulaField } from '../../fields'; | import { FormulaField } from '../../fields'; | ||||||
| 
 | 
 | ||||||
| describe('formula field', () => { | describe('formula field', () => { | ||||||
| @ -16,7 +16,10 @@ describe('formula field', () => { | |||||||
|   it('add formula field with old table, already has data.', async () => { |   it('add formula field with old table, already has data.', async () => { | ||||||
|     const Test = db.collection({ |     const Test = db.collection({ | ||||||
|       name: 'tests', |       name: 'tests', | ||||||
|       fields: [{ type: 'float', name: 'price' }, { type: 'float', name: 'count' }], |       fields: [ | ||||||
|  |         { type: 'float', name: 'price' }, | ||||||
|  |         { type: 'float', name: 'count' }, | ||||||
|  |       ], | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     await db.sync(); |     await db.sync(); | ||||||
| @ -42,7 +45,11 @@ describe('formula field', () => { | |||||||
|     const expression = 'price*count'; |     const expression = 'price*count'; | ||||||
|     const Test = db.collection({ |     const Test = db.collection({ | ||||||
|       name: 'tests', |       name: 'tests', | ||||||
|       fields: [{ type: 'float', name: 'price' }, { type: 'float', name: 'count' }, {name: 'sum', type: 'formula', expression}], |       fields: [ | ||||||
|  |         { type: 'float', name: 'price' }, | ||||||
|  |         { type: 'float', name: 'count' }, | ||||||
|  |         { name: 'sum', type: 'formula', expression }, | ||||||
|  |       ], | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     await db.sync(); |     await db.sync(); | ||||||
| @ -58,5 +65,5 @@ describe('formula field', () => { | |||||||
|     test.set('count', '6'); |     test.set('count', '6'); | ||||||
|     await test.save(); |     await test.save(); | ||||||
|     expect(test.get('sum')).toEqual(sumField.caculate(expression, test.toJSON())); |     expect(test.get('sum')).toEqual(sumField.caculate(expression, test.toJSON())); | ||||||
|   }) |   }); | ||||||
| }); | }); | ||||||
| @ -1,7 +1,7 @@ | |||||||
| import lodash from 'lodash'; | import lodash from 'lodash'; | ||||||
|  | import type { SequelizeHooks } from 'sequelize/types/lib/hooks'; | ||||||
| import Database from './database'; | import Database from './database'; | ||||||
| import { Model } from './model'; | import { Model } from './model'; | ||||||
| import type { SequelizeHooks } from 'sequelize/types/lib/hooks'; |  | ||||||
| 
 | 
 | ||||||
| const { hooks } = require('sequelize/lib/hooks'); | const { hooks } = require('sequelize/lib/hooks'); | ||||||
| 
 | 
 | ||||||
| @ -60,6 +60,7 @@ export class ModelHook { | |||||||
|   buildSequelizeHook(type) { |   buildSequelizeHook(type) { | ||||||
|     return async (...args: any[]) => { |     return async (...args: any[]) => { | ||||||
|       const modelName = this.findModelName(args); |       const modelName = this.findModelName(args); | ||||||
|  | 
 | ||||||
|       if (modelName) { |       if (modelName) { | ||||||
|         // emit model event
 |         // emit model event
 | ||||||
|         await this.database.emitAsync(`${modelName}.${type}`, ...args); |         await this.database.emitAsync(`${modelName}.${type}`, ...args); | ||||||
|  | |||||||
| @ -59,7 +59,7 @@ export abstract class SingleRelationRepository extends RelationRepository { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async findOne(options?: SingleRelationFindOption): Promise<Model<any>> { |   async findOne(options?: SingleRelationFindOption): Promise<Model<any>> { | ||||||
|     return this.find(options); |     return this.find({ ...options, filterByTk: null } as any); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @transaction() |   @transaction() | ||||||
| @ -85,6 +85,10 @@ export abstract class SingleRelationRepository extends RelationRepository { | |||||||
|       transaction, |       transaction, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     if (!target) { | ||||||
|  |       throw new Error('The record does not exist'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     await updateModelByValues(target, options?.values, { |     await updateModelByValues(target, options?.values, { | ||||||
|       ...lodash.omit(options, 'values'), |       ...lodash.omit(options, 'values'), | ||||||
|       transaction, |       transaction, | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ export function registerCli(app: Application) { | |||||||
|   require('./db-clean').default(app); |   require('./db-clean').default(app); | ||||||
|   require('./db-sync').default(app); |   require('./db-sync').default(app); | ||||||
|   require('./install').default(app); |   require('./install').default(app); | ||||||
|  |   require('./migrator').default(app); | ||||||
|   require('./start').default(app); |   require('./start').default(app); | ||||||
|   require('./upgrade').default(app); |   require('./upgrade').default(app); | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								packages/core/server/src/commands/migrator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								packages/core/server/src/commands/migrator.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | import Application from '../application'; | ||||||
|  | 
 | ||||||
|  | export default (app: Application) => { | ||||||
|  |   app | ||||||
|  |     .command('migrator') | ||||||
|  |     .action(async (opts) => { | ||||||
|  |       await app.start(); | ||||||
|  |       console.log('migrating...'); | ||||||
|  |       await app.db.migrator.runAsCLI(process.argv.slice(3)); | ||||||
|  |       await app.stop(); | ||||||
|  |     }); | ||||||
|  | }; | ||||||
| @ -18,7 +18,7 @@ export function dataWrapping() { | |||||||
|   |   | ||||||
|     if (!ctx.body) { |     if (!ctx.body) { | ||||||
|       if (ctx.action.actionName == 'get') { |       if (ctx.action.actionName == 'get') { | ||||||
|         ctx.status = 404; |         ctx.status = 200; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,10 +1,11 @@ | |||||||
| import Database from '@nocobase/database'; | import Application from '@nocobase/server'; | ||||||
| import { LOG_TYPE_CREATE } from '../constants'; | import { LOG_TYPE_CREATE } from '../constants'; | ||||||
| 
 | 
 | ||||||
| export async function afterCreate(model, options) { | export function afterCreate(app: Application) { | ||||||
|   const db = model.constructor.database as Database; |   return async (model, options) => { | ||||||
|  |     const db = app.db; | ||||||
|     const collection = db.getCollection(model.constructor.name); |     const collection = db.getCollection(model.constructor.name); | ||||||
|   if (!collection.options.logging) { |     if (!collection || !collection.options.logging) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     const transaction = options.transaction; |     const transaction = options.transaction; | ||||||
| @ -39,4 +40,5 @@ export async function afterCreate(model, options) { | |||||||
|         hooks: false, |         hooks: false, | ||||||
|       }); |       }); | ||||||
|     } catch (error) {} |     } catch (error) {} | ||||||
|  |   }; | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,10 +1,11 @@ | |||||||
| import Database from '@nocobase/database'; | import Application from '@nocobase/server'; | ||||||
| import { LOG_TYPE_DESTROY } from '../constants'; | import { LOG_TYPE_DESTROY } from '../constants'; | ||||||
| 
 | 
 | ||||||
| export async function afterDestroy(model, options) { | export function afterDestroy(app: Application) { | ||||||
|   const db = model.constructor.database as Database; |   return async (model, options) => { | ||||||
|  |     const db = app.db; | ||||||
|     const collection = db.getCollection(model.constructor.name); |     const collection = db.getCollection(model.constructor.name); | ||||||
|   if (!collection.options.logging) { |     if (!collection || !collection.options.logging) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     const transaction = options.transaction; |     const transaction = options.transaction; | ||||||
| @ -42,4 +43,5 @@ export async function afterDestroy(model, options) { | |||||||
|       //   await transaction.rollback();
 |       //   await transaction.rollback();
 | ||||||
|       // }
 |       // }
 | ||||||
|     } |     } | ||||||
|  |   }; | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,10 +1,11 @@ | |||||||
| import Database from '@nocobase/database'; | import Application from '@nocobase/server'; | ||||||
| import { LOG_TYPE_UPDATE } from '../constants'; | import { LOG_TYPE_UPDATE } from '../constants'; | ||||||
| 
 | 
 | ||||||
| export async function afterUpdate(model, options) { | export function afterUpdate(app: Application) { | ||||||
|   const db = model.constructor.database as Database; |   return async (model, options) => { | ||||||
|  |     const db = app.db; | ||||||
|     const collection = db.getCollection(model.constructor.name); |     const collection = db.getCollection(model.constructor.name); | ||||||
|   if (!collection.options.logging) { |     if (!collection || !collection.options.logging) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     const changed = model.changed(); |     const changed = model.changed(); | ||||||
| @ -51,4 +52,5 @@ export async function afterUpdate(model, options) { | |||||||
|       //   await transaction.rollback();
 |       //   await transaction.rollback();
 | ||||||
|       // }
 |       // }
 | ||||||
|     } |     } | ||||||
|  |   }; | ||||||
| } | } | ||||||
|  | |||||||
| @ -4,9 +4,9 @@ import { afterCreate, afterDestroy, afterUpdate } from './hooks'; | |||||||
| 
 | 
 | ||||||
| export default class PluginActionLogs extends Plugin { | export default class PluginActionLogs extends Plugin { | ||||||
|   async beforeLoad() { |   async beforeLoad() { | ||||||
|     this.db.on('afterCreate', afterCreate); |     this.db.on('afterCreate', afterCreate(this.app)); | ||||||
|     this.db.on('afterUpdate', afterUpdate); |     this.db.on('afterUpdate', afterUpdate(this.app)); | ||||||
|     this.db.on('afterDestroy', afterDestroy); |     this.db.on('afterDestroy', afterDestroy(this.app)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async load() { |   async load() { | ||||||
|  | |||||||
| @ -0,0 +1,16 @@ | |||||||
|  | import { Migration } from '@nocobase/database'; | ||||||
|  | 
 | ||||||
|  | export default class AlertSubTableMigration extends Migration { | ||||||
|  |   versionRange = '<=0.7.0-alpha.83'; | ||||||
|  | 
 | ||||||
|  |   async up() { | ||||||
|  |     const repository = this.context.db.getRepository('fields'); | ||||||
|  |     const fields = await repository.find(); | ||||||
|  |     for (const field of fields) { | ||||||
|  |       if (field.get('interface') === 'subTable') { | ||||||
|  |         field.set('interface', 'o2m'); | ||||||
|  |         await field.save(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -9,6 +9,7 @@ import { | |||||||
|   beforeCreateForReverseField, |   beforeCreateForReverseField, | ||||||
|   beforeInitOptions |   beforeInitOptions | ||||||
| } from './hooks'; | } from './hooks'; | ||||||
|  | import AlertSubTableMigration from './migrations/20220613103214-alert-sub-table'; | ||||||
| import { CollectionModel, FieldModel } from './models'; | import { CollectionModel, FieldModel } from './models'; | ||||||
| 
 | 
 | ||||||
| export class CollectionManagerPlugin extends Plugin { | export class CollectionManagerPlugin extends Plugin { | ||||||
| @ -18,6 +19,11 @@ export class CollectionManagerPlugin extends Plugin { | |||||||
|       FieldModel, |       FieldModel, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     this.db.addMigration({ | ||||||
|  |       name: 'collection-manager/20220613103214-alert-sub-table', | ||||||
|  |       migration: AlertSubTableMigration, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     this.app.db.registerRepositories({ |     this.app.db.registerRepositories({ | ||||||
|       CollectionRepository, |       CollectionRepository, | ||||||
|     }); |     }); | ||||||
| @ -244,7 +250,7 @@ export class CollectionManagerPlugin extends Plugin { | |||||||
|           const collection: Collection = ctx.db.getCollection(collectionName); |           const collection: Collection = ctx.db.getCollection(collectionName); | ||||||
|           if (collection) { |           if (collection) { | ||||||
|             for (const [, field] of collection.fields) { |             for (const [, field] of collection.fields) { | ||||||
|               if (field.options.interface === 'subTable') { |               if (['subTable', 'o2m'].includes(field.options.interface)) { | ||||||
|                 updateAssociationValues.push(field.name); |                 updateAssociationValues.push(field.name); | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
| @ -255,7 +261,7 @@ export class CollectionManagerPlugin extends Plugin { | |||||||
|             const collection: Collection = ctx.db.getCollection(association?.target); |             const collection: Collection = ctx.db.getCollection(association?.target); | ||||||
|             if (collection) { |             if (collection) { | ||||||
|               for (const [, field] of collection.fields) { |               for (const [, field] of collection.fields) { | ||||||
|                 if (field.options.interface === 'subTable') { |                 if (['subTable', 'o2m'].includes(field.options.interface)) { | ||||||
|                   updateAssociationValues.push(field.name); |                   updateAssociationValues.push(field.name); | ||||||
|                 } |                 } | ||||||
|               } |               } | ||||||
|  | |||||||
| @ -21,6 +21,10 @@ export default defineCollection({ | |||||||
|       type: 'string', |       type: 'string', | ||||||
|       name: 'associationName', |       name: 'associationName', | ||||||
|     }, |     }, | ||||||
|  |     { | ||||||
|  |       type: 'string', | ||||||
|  |       name: 'resourceName', | ||||||
|  |     }, | ||||||
|     { |     { | ||||||
|       type: 'belongsTo', |       type: 'belongsTo', | ||||||
|       name: 'uiSchema', |       name: 'uiSchema', | ||||||
|  | |||||||
| @ -52,7 +52,7 @@ export default { | |||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       interface: 'linkTo', |       interface: 'm2m', | ||||||
|       type: 'belongsToMany', |       type: 'belongsToMany', | ||||||
|       name: 'roles', |       name: 'roles', | ||||||
|       target: 'roles', |       target: 'roles', | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user