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:
金昶 2022-06-14 15:46:48 +08:00 committed by GitHub
parent d831a9b889
commit b91ca4420b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1080 additions and 361 deletions

View File

@ -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'),
}); });

View File

@ -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'];

View File

@ -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>({});

View File

@ -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;
}
}, },
}; };
}; };

View File

@ -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>
); );
}; };

View File

@ -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,

View File

@ -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>
);
});

View File

@ -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")}}');

View File

@ -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")}}',

View File

@ -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")}}',

View File

@ -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: '留空时,自动生成中间表'
}, },
}, },
}, },

View File

@ -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',

View File

@ -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',

View File

@ -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',
// },
// },
],
},
};

View File

@ -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',

View File

@ -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)",
} }

View File

@ -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"
} }

View File

@ -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

View File

@ -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>
)
});

View File

@ -1 +0,0 @@
export * from './FieldSummary';

View File

@ -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

View File

@ -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';

View File

@ -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}

View File

@ -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: {

View File

@ -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

View File

@ -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) => {

View File

@ -1,3 +1,7 @@
.nb-markdown {
line-height: 1.612;
}
.nb-markdown > *:last-child { .nb-markdown > *:last-child {
margin-bottom: 0; margin-bottom: 0;
} }

View File

@ -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}

View File

@ -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

View File

@ -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>

View File

@ -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) => {

View File

@ -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)}
/> />
); );
}; };

View File

@ -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',

View File

@ -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'],

View File

@ -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 || [];
}, },
}; };
}; };

View File

@ -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) {

View File

@ -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()));
}) });
}); });

View File

@ -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);

View File

@ -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,

View File

@ -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);

View 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();
});
};

View File

@ -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;
} }
} }

View File

@ -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) {}
};
} }

View File

@ -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();
// } // }
} }
};
} }

View File

@ -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();
// } // }
} }
};
} }

View File

@ -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() {

View File

@ -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();
}
}
}
}

View File

@ -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);
} }
} }

View File

@ -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',

View File

@ -52,7 +52,7 @@ export default {
}, },
}, },
{ {
interface: 'linkTo', interface: 'm2m',
type: 'belongsToMany', type: 'belongsToMany',
name: 'roles', name: 'roles',
target: 'roles', target: 'roles',