From b91ca4420b3808bd97f485a3e99d20a87686140d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=91=E6=98=B6?= Date: Tue, 14 Jun 2022 15:46:48 +0800 Subject: [PATCH] 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 --- packages/core/cli/bin/index.js | 7 + .../src/block-provider/BlockProvider.tsx | 5 +- .../src/block-provider/FormBlockProvider.tsx | 1 + .../client/src/block-provider/hooks/index.ts | 128 ++++--- .../Configuration/AddFieldAction.tsx | 23 +- .../Configuration/ConfigurationTable.tsx | 2 + .../Configuration/components/FieldSummary.tsx | 39 ++ .../Configuration/interfaces.tsx | 4 +- .../collection-manager/interfaces/formula.ts | 2 +- .../collection-manager/interfaces/linkTo.ts | 22 +- .../src/collection-manager/interfaces/m2m.tsx | 19 +- .../src/collection-manager/interfaces/m2o.tsx | 17 +- .../src/collection-manager/interfaces/o2m.tsx | 16 +- .../src/collection-manager/interfaces/o2o.tsx | 357 +++++++++++++++++- .../interfaces/properties/index.ts | 15 + packages/core/client/src/locale/en_US.ts | 6 +- packages/core/client/src/locale/zh_CN.ts | 11 +- .../antd/calendar/Calendar.Designer.tsx | 3 +- .../antd/field-summary/FieldSummary.tsx | 21 -- .../antd/field-summary/index.ts | 1 - .../antd/form-v2/Form.Designer.tsx | 9 +- .../client/src/schema-component/antd/index.ts | 1 - .../antd/input/ReadPretty.tsx | 7 +- .../antd/input/demos/demo2.tsx | 3 + .../antd/kanban/Kanban.Designer.tsx | 3 +- .../antd/markdown/Markdown.tsx | 2 + .../schema-component/antd/markdown/style.less | 4 + .../antd/table-v2/Table.Column.Designer.tsx | 2 +- .../antd/table-v2/TableBlockDesigner.tsx | 3 +- .../schema-initializer/SchemaInitializer.tsx | 12 +- .../buttons/RecordBlockInitializers.tsx | 67 +++- .../src/schema-initializer/items/index.tsx | 253 ++++++++++++- .../client/src/schema-initializer/utils.ts | 15 +- .../src/schema-settings/SchemaSettings.tsx | 7 +- .../SchemaTemplateManagerProvider.tsx | 5 +- .../triggers/schedule/RepeatField.tsx | 6 +- .../__tests__/fields/formula-field.test.ts | 19 +- packages/core/database/src/model-hook.ts | 3 +- .../single-relation-repository.ts | 6 +- packages/core/server/src/commands/index.ts | 1 + packages/core/server/src/commands/migrator.ts | 12 + .../server/src/middlewares/data-wrapping.ts | 2 +- .../src/server/hooks/after-create.ts | 78 ++-- .../src/server/hooks/after-destroy.ts | 84 +++-- .../src/server/hooks/after-update.ts | 100 ++--- .../plugins/audit-logs/src/server/index.ts | 6 +- .../20220613103214-alert-sub-table.ts | 16 + .../plugins/collection-manager/src/server.ts | 10 +- .../src/collections/uiSchemaTemplates.ts | 4 + .../plugins/users/src/collections/users.ts | 2 +- 50 files changed, 1080 insertions(+), 361 deletions(-) create mode 100644 packages/core/client/src/collection-manager/Configuration/components/FieldSummary.tsx delete mode 100644 packages/core/client/src/schema-component/antd/field-summary/FieldSummary.tsx delete mode 100644 packages/core/client/src/schema-component/antd/field-summary/index.ts create mode 100644 packages/core/server/src/commands/migrator.ts create mode 100644 packages/plugins/collection-manager/src/migrations/20220613103214-alert-sub-table.ts diff --git a/packages/core/cli/bin/index.js b/packages/core/cli/bin/index.js index 94b39fd6a..1544d5ef7 100755 --- a/packages/core/cli/bin/index.js +++ b/packages/core/cli/bin/index.js @@ -2,6 +2,7 @@ const dotenv = require('dotenv'); const { resolve } = require('path'); +const { existsSync } = require('fs'); const env = { APP_PACKAGE_ROOT: 'app', @@ -20,6 +21,12 @@ if ('v18' === process.version.split('.').shift()) { 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({ path: resolve(process.cwd(), process.env.APP_ENV_PATH || '.env'), }); diff --git a/packages/core/client/src/block-provider/BlockProvider.tsx b/packages/core/client/src/block-provider/BlockProvider.tsx index 082f3d9c2..414118bb3 100644 --- a/packages/core/client/src/block-provider/BlockProvider.tsx +++ b/packages/core/client/src/block-provider/BlockProvider.tsx @@ -39,6 +39,7 @@ const useResource = (props: UseResourceProps) => { const api = useAPIClient(); const association = useAssociation(props); const sourceId = useSourceId?.(); + const field = useField(); if (block === 'TableField') { const options = { @@ -60,6 +61,7 @@ const useResource = (props: UseResourceProps) => { if (sourceId) { return api.resource(resource, sourceId); } + return api.resource(resource, record[association?.sourceKey || 'id']); }; @@ -150,7 +152,8 @@ export const useFilterByTk = () => { return recordIndex; } } - if (assoc) { + + if (assoc) { const association = getCollectionField(assoc); return record?.[association.targetKey || 'id']; } diff --git a/packages/core/client/src/block-provider/FormBlockProvider.tsx b/packages/core/client/src/block-provider/FormBlockProvider.tsx index 7d87bd548..818c7c217 100644 --- a/packages/core/client/src/block-provider/FormBlockProvider.tsx +++ b/packages/core/client/src/block-provider/FormBlockProvider.tsx @@ -3,6 +3,7 @@ import { useField } from '@formily/react'; import { Spin } from 'antd'; import React, { createContext, useContext, useEffect, useMemo } from 'react'; import { BlockProvider, useBlockRequestContext } from './BlockProvider'; +import { useCollectionManager } from '../collection-manager'; export const FormBlockContext = createContext({}); diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index 0b47957dd..65c5755b7 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -61,7 +61,7 @@ function getFormValues(filterByTk, field, form, fieldNames, getField, resource) if (fieldNames.includes(key)) { const collectionField = getField(key); if (filterByTk) { - if (collectionField.interface === 'subTable') { + if (['subTable', 'o2m'].includes(collectionField.interface)) { values[key] = form.values[key]; continue; } @@ -70,7 +70,7 @@ function getFormValues(filterByTk, field, form, fieldNames, getField, resource) } } const items = form.values[key]; - if (collectionField.interface === 'linkTo') { + if (['linkTo', 'm2o', 'm2m'].includes(collectionField.interface)) { const targetKey = collectionField.targetKey || 'id'; if (resource instanceof TableFieldResource) { if (Array.isArray(items)) { @@ -121,35 +121,39 @@ export const useCreateActionProps = () => { const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource); actionField.data = field.data || {}; actionField.data.loading = true; - await resource.create({ - values: { - ...values, - ...overwriteValues, - ...assignedValues, - }, - }); - actionField.data.loading = false; - __parent?.service?.refresh?.(); - setVisible?.(false); - if (!onSuccess?.successMessage) { - return; - } - if (onSuccess?.manualClose) { - Modal.success({ - title: compile(onSuccess?.successMessage), - onOk: async () => { - await form.reset(); - if (onSuccess?.redirecting && onSuccess?.redirectTo) { - if (isURL(onSuccess.redirectTo)) { - window.location.href = onSuccess.redirectTo; - } else { - history.push(onSuccess.redirectTo); - } - } + try { + await resource.create({ + values: { + ...values, + ...overwriteValues, + ...assignedValues, }, }); - } else { - message.success(compile(onSuccess?.successMessage)); + actionField.data.loading = false; + __parent?.service?.refresh?.(); + setVisible?.(false); + if (!onSuccess?.successMessage) { + return; + } + if (onSuccess?.manualClose) { + Modal.success({ + title: compile(onSuccess?.successMessage), + onOk: async () => { + await form.reset(); + if (onSuccess?.redirecting && onSuccess?.redirectTo) { + if (isURL(onSuccess.redirectTo)) { + window.location.href = onSuccess.redirectTo; + } else { + history.push(onSuccess.redirectTo); + } + } + }, + }); + } else { + message.success(compile(onSuccess?.successMessage)); + } + } catch (error) { + actionField.data.loading = false; } }, }; @@ -281,39 +285,43 @@ export const useUpdateActionProps = () => { const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource); actionField.data = field.data || {}; actionField.data.loading = true; - await resource.update({ - filterByTk, - values: { - ...values, - ...overwriteValues, - ...assignedValues, - }, - }); - actionField.data.loading = false; - __parent?.service?.refresh?.(); - if (!(resource instanceof TableFieldResource)) { - __parent?.__parent?.service?.refresh?.(); - } - setVisible?.(false); - if (!onSuccess?.successMessage) { - return; - } - if (onSuccess?.manualClose) { - Modal.success({ - title: compile(onSuccess?.successMessage), - onOk: async () => { - await form.reset(); - if (onSuccess?.redirecting && onSuccess?.redirectTo) { - if (isURL(onSuccess.redirectTo)) { - window.location.href = onSuccess.redirectTo; - } else { - history.push(onSuccess.redirectTo); - } - } + try { + await resource.update({ + filterByTk, + values: { + ...values, + ...overwriteValues, + ...assignedValues, }, }); - } else { - message.success(compile(onSuccess?.successMessage)); + actionField.data.loading = false; + __parent?.service?.refresh?.(); + if (!(resource instanceof TableFieldResource)) { + __parent?.__parent?.service?.refresh?.(); + } + setVisible?.(false); + if (!onSuccess?.successMessage) { + return; + } + if (onSuccess?.manualClose) { + Modal.success({ + title: compile(onSuccess?.successMessage), + onOk: async () => { + await form.reset(); + if (onSuccess?.redirecting && onSuccess?.redirectTo) { + if (isURL(onSuccess.redirectTo)) { + window.location.href = onSuccess.redirectTo; + } else { + history.push(onSuccess.redirectTo); + } + } + }, + }); + } else { + message.success(compile(onSuccess?.successMessage)); + } + } catch (error) { + actionField.data.loading = false; } }, }; diff --git a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx index 25664427f..03dab46d3 100644 --- a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx @@ -2,7 +2,7 @@ import { PlusOutlined } from '@ant-design/icons'; import { ArrayTable } from '@formily/antd'; import { ISchema, useForm } from '@formily/react'; import { uid } from '@formily/shared'; -import { Button, Dropdown, Menu, Typography } from 'antd'; +import { Button, Dropdown, Menu } from 'antd'; import { cloneDeep } from 'lodash'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -50,8 +50,8 @@ const getSchema = (schema: IField, record: any, compile): ISchema => { type: 'void', 'x-component': 'FieldSummary', 'x-component-props': { - schemaKey: schema.name - } + schemaKey: schema.name, + }, }, // @ts-ignore ...properties, @@ -120,9 +120,10 @@ const useCreateCollectionField = () => { 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); } + await run(); await refreshCM(); }, @@ -155,9 +156,11 @@ export const AddFieldAction = () => { return ( option.children.length > 0 && ( - {option.children.map((child) => { - return {compile(child.title)}; - })} + {option.children + .filter((child) => !['o2o', 'subTable'].includes(child.name)) + .map((child) => { + return {compile(child.title)}; + })} ) ); @@ -169,7 +172,11 @@ export const AddFieldAction = () => { {t('Add field')} - + ); }; diff --git a/packages/core/client/src/collection-manager/Configuration/ConfigurationTable.tsx b/packages/core/client/src/collection-manager/Configuration/ConfigurationTable.tsx index 076bb3118..b37945355 100644 --- a/packages/core/client/src/collection-manager/Configuration/ConfigurationTable.tsx +++ b/packages/core/client/src/collection-manager/Configuration/ConfigurationTable.tsx @@ -8,6 +8,7 @@ import { SchemaComponent, useActionContext, useCompile } from '../../schema-comp import { useCollectionManager } from '../hooks/useCollectionManager'; import { DataSourceContext } from '../sub-table'; import { AddSubFieldAction } from './AddSubFieldAction'; +import { FieldSummary } from './components/FieldSummary'; import { EditSubFieldAction } from './EditSubFieldAction'; import { collectionSchema } from './schemas/collections'; @@ -185,6 +186,7 @@ export const ConfigurationTable = () => { components={{ AddSubFieldAction, EditSubFieldAction, + FieldSummary, }} scope={{ useDestroySubField, diff --git a/packages/core/client/src/collection-manager/Configuration/components/FieldSummary.tsx b/packages/core/client/src/collection-manager/Configuration/components/FieldSummary.tsx new file mode 100644 index 000000000..16c0d5991 --- /dev/null +++ b/packages/core/client/src/collection-manager/Configuration/components/FieldSummary.tsx @@ -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 ( +
+
{t('Field interface')}: {compile(schema.title)}
+ {schema.description ? ( +
+ {compile(schema.description)} +
+ ) : null} +
+ ); +}); diff --git a/packages/core/client/src/collection-manager/Configuration/interfaces.tsx b/packages/core/client/src/collection-manager/Configuration/interfaces.tsx index e62ca4a2a..373e6e3b8 100644 --- a/packages/core/client/src/collection-manager/Configuration/interfaces.tsx +++ b/packages/core/client/src/collection-manager/Configuration/interfaces.tsx @@ -17,7 +17,7 @@ export function registerGroupLabel(key: string, label: string) { groupLabels[key] = label; } -Object.keys(types).filter((type) => !['subTable'].includes(type)).forEach((type) => { +Object.keys(types).forEach((type) => { const schema = types[type]; registerField(schema.group || 'others', type, { order: 0, ...schema }); }); @@ -27,7 +27,7 @@ registerGroupLabel('choices', '{{t("Choices")}}'); registerGroupLabel('media', '{{t("Media")}}'); registerGroupLabel('datetime', '{{t("Date & Time")}}'); registerGroupLabel('relation', '{{t("Relation")}}'); -registerGroupLabel('advance', '{{t("Advance type")}}'); +registerGroupLabel('advanced', '{{t("Advanced type")}}'); registerGroupLabel('systemInfo', '{{t("System info")}}'); registerGroupLabel('others', '{{t("Others")}}'); diff --git a/packages/core/client/src/collection-manager/interfaces/formula.ts b/packages/core/client/src/collection-manager/interfaces/formula.ts index a34f65daf..949c44d5e 100644 --- a/packages/core/client/src/collection-manager/interfaces/formula.ts +++ b/packages/core/client/src/collection-manager/interfaces/formula.ts @@ -4,7 +4,7 @@ import { IField } from './types'; export const formula: IField = { name: 'formula', type: 'object', - group: 'advance', + group: 'advanced', order: 1, title: '{{t("Formula")}}', description: '{{t("Formula description")}}', diff --git a/packages/core/client/src/collection-manager/interfaces/linkTo.ts b/packages/core/client/src/collection-manager/interfaces/linkTo.ts index d683b758a..730be6c33 100644 --- a/packages/core/client/src/collection-manager/interfaces/linkTo.ts +++ b/packages/core/client/src/collection-manager/interfaces/linkTo.ts @@ -86,17 +86,17 @@ export const linkTo: IField = { 'x-component': 'Select', 'x-disabled': '{{ !createOnly }}', }, - through: { - type: 'string', - title: '{{t("Junction collection")}}', - 'x-disabled': '{{ !createOnly }}', - 'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'], - 'x-decorator': 'FormItem', - 'x-component': 'Select', - 'x-component-props': { - placeholder: '{{t("Leave it blank, unless you need a custom intermediate table")}}', - }, - }, + // through: { + // type: 'string', + // title: '{{t("Junction collection")}}', + // 'x-disabled': '{{ !createOnly }}', + // 'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'], + // 'x-decorator': 'FormItem', + // 'x-component': 'Select', + // 'x-component-props': { + // placeholder: '{{t("Leave it blank, unless you need a custom intermediate table")}}', + // }, + // }, // 'reverseField.uiSchema.title': { // type: 'string', // title: '{{t("Reverse field display name")}}', diff --git a/packages/core/client/src/collection-manager/interfaces/m2m.tsx b/packages/core/client/src/collection-manager/interfaces/m2m.tsx index 03c4a09e5..03a19a983 100644 --- a/packages/core/client/src/collection-manager/interfaces/m2m.tsx +++ b/packages/core/client/src/collection-manager/interfaces/m2m.tsx @@ -1,7 +1,7 @@ import { ISchema } from '@formily/react'; import { uid } from '@formily/shared'; import { cloneDeep } from 'lodash'; -import { defaultProps, recordPickerSelector, recordPickerViewer } from './properties'; +import { defaultProps, recordPickerSelector, recordPickerViewer, relationshipType } from './properties'; import { IField } from './types'; export const m2m: IField = { @@ -77,20 +77,7 @@ export const m2m: IField = { }, properties: { ...defaultProps, - type: { - 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' }, - ], - }, + type: relationshipType, grid: { type: 'void', 'x-component': 'Grid', @@ -119,13 +106,13 @@ export const m2m: IField = { through: { type: 'string', title: '{{t("Through collection")}}', + description: '{{ t("Generated automatically if left blank") }}', 'x-decorator': 'FormItem', 'x-disabled': '{{ !createOnly }}', 'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'], 'x-component': 'Select', 'x-component-props': { allowClear: true, - placeholder: '留空时,自动生成中间表' }, }, }, diff --git a/packages/core/client/src/collection-manager/interfaces/m2o.tsx b/packages/core/client/src/collection-manager/interfaces/m2o.tsx index 531cd5a87..bc05b3bb8 100644 --- a/packages/core/client/src/collection-manager/interfaces/m2o.tsx +++ b/packages/core/client/src/collection-manager/interfaces/m2o.tsx @@ -1,6 +1,6 @@ import { ISchema } from '@formily/react'; import { cloneDeep } from 'lodash'; -import { recordPickerSelector, recordPickerViewer } from './properties'; +import { recordPickerSelector, recordPickerViewer, relationshipType } from './properties'; import { IField } from './types'; export const m2o: IField = { @@ -73,20 +73,7 @@ export const m2o: IField = { description: "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}", }, - type: { - 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' }, - ], - }, + type: relationshipType, grid: { type: 'void', 'x-component': 'Grid', diff --git a/packages/core/client/src/collection-manager/interfaces/o2m.tsx b/packages/core/client/src/collection-manager/interfaces/o2m.tsx index f250703ba..a57a6996f 100644 --- a/packages/core/client/src/collection-manager/interfaces/o2m.tsx +++ b/packages/core/client/src/collection-manager/interfaces/o2m.tsx @@ -1,4 +1,5 @@ import { ISchema } from '@formily/react'; +import { relationshipType } from './properties'; import { IField } from './types'; export const o2m: IField = { @@ -111,20 +112,7 @@ export const o2m: IField = { description: "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}", }, - type: { - 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' }, - ], - }, + type: relationshipType, grid: { type: 'void', 'x-component': 'Grid', diff --git a/packages/core/client/src/collection-manager/interfaces/o2o.tsx b/packages/core/client/src/collection-manager/interfaces/o2o.tsx index 32d762e4c..871cbf897 100644 --- a/packages/core/client/src/collection-manager/interfaces/o2o.tsx +++ b/packages/core/client/src/collection-manager/interfaces/o2o.tsx @@ -1,6 +1,6 @@ import { ISchema } from '@formily/react'; import { cloneDeep } from 'lodash'; -import { recordPickerSelector, recordPickerViewer } from './properties'; +import { recordPickerSelector, recordPickerViewer, relationshipType } from './properties'; import { IField } from './types'; export const o2o: IField = { @@ -27,9 +27,9 @@ export const o2o: IField = { }, }, reverseField: { - interface: 'm2o', + interface: 'obo', type: 'belongsTo', - title: '{{t("One to one")}}', + // title: '{{t("One to one (belongs to)")}}', // name, uiSchema: { // title, @@ -74,20 +74,7 @@ export const o2o: IField = { description: "{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}", }, - type: { - 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' }, - ], - }, + type: relationshipType, grid: { type: 'void', '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', + // }, + // }, + ], + }, +}; diff --git a/packages/core/client/src/collection-manager/interfaces/properties/index.ts b/packages/core/client/src/collection-manager/interfaces/properties/index.ts index 56e4bc296..6c1261d59 100644 --- a/packages/core/client/src/collection-manager/interfaces/properties/index.ts +++ b/packages/core/client/src/collection-manager/interfaces/properties/index.ts @@ -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 } = { 'uiSchema.x-component-props.dateFormat': { type: 'string', diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts index 7e84f7615..31fc60e05 100644 --- a/packages/core/client/src/locale/en_US.ts +++ b/packages/core/client/src/locale/en_US.ts @@ -456,8 +456,8 @@ export default { "By month": "By month", "By field": "By field", "By custom date": "By custom date", - "Advance": "Advance", - "Advance type": "Advance type", + "Advanced": "Advanced", + "Advanced type": "Advanced", "End": "End", "Trigger context": "Trigger context", "Node result": "Node result", @@ -538,4 +538,6 @@ export default { "User": "User", "Field": "Field", "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)", } diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 969dc6548..beb1f089c 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -172,9 +172,10 @@ export default { "Foreign key 1": "外键1", "Foreign key 2": "外键2", "One to one description": "用于创建一对一关系,比如一个用户会有一套个人资料。", - "One to many description": "用于创建一对多关系,比如一个国家会有多个城市。作为字段存在时,它是一个子表格用于显示目标数据表的数据。创建后,会在目标数据表里自动生成一个 Many to one 字段。", - "Many to one description": "用于创建多对一关系,比如一个城市只能属于一个国家,一个国家可以有多个城市。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。创建后,会在目标数据表里自动生成一个 One to many 字段。", + "One to many description": "用于创建一对多关系,比如一个国家会有多个城市。作为字段存在时,它是一个子表格用于显示目标数据表的数据。创建后,会在目标数据表里自动生成一个多对一字段。", + "Many to one description": "用于创建多对一关系,比如一个城市只能属于一个国家,一个国家可以有多个城市。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。创建后,会在目标数据表里自动生成一个多对一字段。", "Many to many description": "用于创建多对多关系,比如一个学生会有多个老师,一个老师也会有多个学生。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。", + "Generated automatically if left blank": "留空时,自动生成中间表", "Add filter": "添加筛选条件", "Add filter group": "添加筛选分组", "is": "等于", @@ -493,8 +494,8 @@ export default { 'By field': '数据表字段', 'By custom date': '自定义时间', - 'Advance': '高级模式', - 'Advance type': '高级类型', + 'Advanced': '高级模式', + 'Advanced type': '高级类型', 'End': '结束', @@ -593,4 +594,6 @@ export default { "Select": "选择", "Select Field": "选择字段", "Field value changes": "变更记录", + "One to one (has one)": "一对一(has one)", + "One to one (belongs to)": "一对一(belongs to)" } diff --git a/packages/core/client/src/schema-component/antd/calendar/Calendar.Designer.tsx b/packages/core/client/src/schema-component/antd/calendar/Calendar.Designer.tsx index a1219566f..19cffdf99 100644 --- a/packages/core/client/src/schema-component/antd/calendar/Calendar.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/calendar/Calendar.Designer.tsx @@ -33,6 +33,7 @@ export const CalendarDesigner = () => { const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {}; const options = useOptions(); const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {}; + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; return ( { }} /> - + { - const { schemaKey } = props; - const { getInterface } = useCollectionManager(); - const compile = useCompile(); - const schema = getInterface(schemaKey); - - if (!schema) return (null); - - return ( -
- {compile(schema.title)} - { schema.description ? ({compile(schema.description)}) : (null)} -
- ) -}); \ No newline at end of file diff --git a/packages/core/client/src/schema-component/antd/field-summary/index.ts b/packages/core/client/src/schema-component/antd/field-summary/index.ts deleted file mode 100644 index 6ae64606d..000000000 --- a/packages/core/client/src/schema-component/antd/field-summary/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './FieldSummary'; \ No newline at end of file diff --git a/packages/core/client/src/schema-component/antd/form-v2/Form.Designer.tsx b/packages/core/client/src/schema-component/antd/form-v2/Form.Designer.tsx index 65eb7c3f4..e4b173fb5 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Form.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/Form.Designer.tsx @@ -20,10 +20,11 @@ export const FormDesigner = () => { const { dn } = useDesignable(); const { t } = useTranslation(); const { visible } = useActionContext(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; return ( {/* */} - + { export const ReadPrettyFormDesigner = () => { const { name, title } = useCollection(); const template = useSchemaTemplate(); + const fieldSchema = useFieldSchema(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; return ( {/* */} @@ -45,6 +48,7 @@ export const ReadPrettyFormDesigner = () => { insertAdjacentPosition={'beforeEnd'} componentName={'ReadPrettyFormItem'} collectionName={name} + resourceName={defaultResource} /> { const sortFields = useSortFields(name); const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {}; const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const sort = defaultSort?.map((item: string) => { return item.startsWith('-') ? { @@ -204,7 +209,7 @@ export const DetailsDesigner = () => { service.run({ ...service.params?.[0], sort: sortArr }); }} /> - + { const { autop = true, ellipsis, text } = props; const html = (
'), }} /> ); - console.log('value', value); + const content = ellipsis ? ( - {text} - ) : value || html; + {text || value} + ) : (autop ? html : value); return (
{props.addonBefore} diff --git a/packages/core/client/src/schema-component/antd/input/demos/demo2.tsx b/packages/core/client/src/schema-component/antd/input/demos/demo2.tsx index c4a041f4e..f8f555bf5 100644 --- a/packages/core/client/src/schema-component/antd/input/demos/demo2.tsx +++ b/packages/core/client/src/schema-component/antd/input/demos/demo2.tsx @@ -41,6 +41,9 @@ const schema = { 'x-component': 'Input.TextArea', 'x-component-props': { ellipsis: true, + style: { + width: '100px' + } }, }, read3: { diff --git a/packages/core/client/src/schema-component/antd/kanban/Kanban.Designer.tsx b/packages/core/client/src/schema-component/antd/kanban/Kanban.Designer.tsx index 99eae5a02..5fc5a82d4 100644 --- a/packages/core/client/src/schema-component/antd/kanban/Kanban.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/kanban/Kanban.Designer.tsx @@ -17,6 +17,7 @@ export const KanbanDesigner = () => { const { t } = useTranslation(); const { dn } = useDesignable(); const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {}; + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const template = useSchemaTemplate(); return ( @@ -51,7 +52,7 @@ export const KanbanDesigner = () => { }} /> - + { diff --git a/packages/core/client/src/schema-component/antd/markdown/style.less b/packages/core/client/src/schema-component/antd/markdown/style.less index 2ac3b79fc..0e6489227 100644 --- a/packages/core/client/src/schema-component/antd/markdown/style.less +++ b/packages/core/client/src/schema-component/antd/markdown/style.less @@ -1,3 +1,7 @@ +.nb-markdown { + line-height: 1.612; +} + .nb-markdown > *:last-child { margin-bottom: 0; } diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx index 6a8002c38..b9a6a0f46 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Designer.tsx @@ -65,7 +65,7 @@ export const TableColumnDesigner = (props) => { dn.refresh(); }} /> - {collectionField?.interface === 'linkTo' && ( + {['linkTo', 'm2m', 'm2o', 'obo', 'oho'].includes(collectionField?.interface) && ( { const { dn } = useDesignable(); const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {}; const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const sort = defaultSort?.map((item: string) => { return item.startsWith('-') ? { @@ -200,7 +201,7 @@ export const TableBlockDesigner = () => { }} /> - + { return ( Component && ( { ); } if (item.type === 'subMenu') { - console.log('item.key', item.key); return ( { return ( : icon} > @@ -203,12 +202,13 @@ SchemaInitializer.Item = (props: SchemaInitializerItemProps) => { } return ( : icon} onClick={(opts) => { onClick({ ...opts, item: info }); }} - {...others} > {compile(children)} diff --git a/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx index b61067611..c7e47ed38 100644 --- a/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx @@ -5,9 +5,71 @@ import { gridRowColWrap } from '../utils'; const useRelationFields = () => { const { fields } = useCollection(); - return fields - .filter((field) => ['linkTo', 'subTable'].includes(field.interface)) + const relationFields = fields + .filter((field) => ['linkTo', 'subTable', 'o2m', 'm2m', 'obo', 'oho', 'o2o', 'm2o'].includes(field.interface)) .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 { key: field.name, type: 'item', @@ -16,6 +78,7 @@ const useRelationFields = () => { component: 'RecordAssociationBlockInitializer', }; }) as any; + return relationFields; }; export const RecordBlockInitializers = (props: any) => { diff --git a/packages/core/client/src/schema-initializer/items/index.tsx b/packages/core/client/src/schema-initializer/items/index.tsx index 397e7b884..1191e8c2b 100644 --- a/packages/core/client/src/schema-initializer/items/index.tsx +++ b/packages/core/client/src/schema-initializer/items/index.tsx @@ -663,6 +663,7 @@ export const RecordReadPrettyFormBlockInitializer = (props) => { } {...others} + key={'123'} onClick={async ({ item }) => { if (item.template) { 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 ( + } + {...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 ( + } + {...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 { getTemplateSchemaByMode } = useSchemaTemplateManager(); const { getCollection } = useCollectionManager(); + const field = item.field; + const collection = getCollection(field.target); + const resource = `${field.collectionName}.${field.name}`; + return ( + } + {...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 ( } {...others} onClick={async ({ item }) => { - console.log('RecordAssociationBlockInitializer', item); - const field = item.field; - const collection = getCollection(field.target); + 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 ( + + + + + + ); + }).open({ + initialValues: {}, + }); insert( - createTableBlockSchema({ - rowKey: collection.filterTargetKey, + createCalendarBlockSchema({ collection: field.target, - resource: `${field.collectionName}.${field.name}`, - association: `${field.collectionName}.${field.name}`, + resource, + association: resource, + fieldNames: { + ...values, + }, }), ); - // if (item.template) { - // const s = await getTemplateSchemaByMode(item); - // insert(s); - // } else { - // insert(createTableBlockSchema({ collection: item.name })); - // } + } }} - // items={useRecordCollectionDataSourceItems('Table')} + 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 collection = getCollection(field.target); + const resource = `${field.collectionName}.${field.name}`; + return ( + } + {...others} + onClick={async ({ item }) => { + if (item.template) { + const s = await getTemplateSchemaByMode(item); + insert(s); + } else { + insert( + createTableBlockSchema({ + rowKey: collection.filterTargetKey, + collection: field.target, + resource, + association: resource, + }), + ); + } + }} + items={useRecordCollectionDataSourceItems('Table', item, field.target, resource)} /> ); }; diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index e039f468b..d7e827633 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -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 collection = useCollection(); 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; }); if (!templates.length) { @@ -210,15 +210,17 @@ export const useRecordCollectionDataSourceItems = (componentName) => { const index = 0; return [ { + key: `${collectionName || componentName}_table_blank`, type: 'item', name: collection.name, title: t('Blank block'), + item, }, { type: 'divider', }, { - key: `${componentName}_table_subMenu_${index}_copy`, + key: `${collectionName || componentName}_table_subMenu_${index}_copy`, type: 'subMenu', name: 'copy', title: t('Duplicate template'), @@ -230,12 +232,13 @@ export const useRecordCollectionDataSourceItems = (componentName) => { mode: 'copy', name: collection.name, template, + item, title: templateName || t('Untitled'), }; }), }, { - key: `${componentName}_table_subMenu_${index}_ref`, + key: `${collectionName || componentName}_table_subMenu_${index}_ref`, type: 'subMenu', name: 'ref', title: t('Reference template'), @@ -247,6 +250,7 @@ export const useRecordCollectionDataSourceItems = (componentName) => { mode: 'reference', name: collection.name, template, + item, title: templateName || t('Untitled'), }; }), @@ -266,7 +270,7 @@ export const useCollectionDataSourceItems = (componentName) => { children: collections ?.filter((item) => !item.inherit) ?.map((item, index) => { - const templates = getTemplatesByCollection(item.name).filter((template) => { + const templates = getTemplatesByCollection(item.name, item.name).filter((template) => { return componentName && template.componentName === componentName; }); if (!templates.length) { @@ -412,6 +416,7 @@ export const createFormBlockSchema = (options) => { template, ...others } = options; + console.log('createFormBlockSchema', options); const resourceName = resource || association || collection; const schema: ISchema = { type: 'void', diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx index 70de57616..c5d734763 100644 --- a/packages/core/client/src/schema-settings/SchemaSettings.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx @@ -117,7 +117,7 @@ export const SchemaSettings: React.FC & SchemaSettingsNeste }; SchemaSettings.Template = (props) => { - const { componentName, collectionName } = props; + const { componentName, collectionName, resourceName } = props; const { t } = useTranslation(); const { dn, setVisible, template, fieldSchema } = useSchemaSettings(); const api = useAPIClient(); @@ -178,6 +178,7 @@ SchemaSettings.Template = (props) => { sdn.loadAPIClientEvents(); const { key } = await saveAsTemplate({ collectionName, + resourceName, componentName, name: values.name, uid: fieldSchema['x-uid'], @@ -232,7 +233,8 @@ const findBlockTemplateSchema = (fieldSchema) => { }; 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 { dn, setVisible, template, fieldSchema } = useSchemaSettings(); const api = useAPIClient(); @@ -312,6 +314,7 @@ SchemaSettings.FormItemTemplate = (props) => { sdn.loadAPIClientEvents(); const { key } = await saveAsTemplate({ collectionName, + resourceName, componentName, name: values.name, uid: gridSchema['x-uid'], diff --git a/packages/core/client/src/schema-templates/SchemaTemplateManagerProvider.tsx b/packages/core/client/src/schema-templates/SchemaTemplateManagerProvider.tsx index 387300cfa..6c94ba2f9 100644 --- a/packages/core/client/src/schema-templates/SchemaTemplateManagerProvider.tsx +++ b/packages/core/client/src/schema-templates/SchemaTemplateManagerProvider.tsx @@ -119,10 +119,11 @@ export const useSchemaTemplateManager = () => { getTemplateById(key) { return templates?.find((template) => template.key === key); }, - getTemplatesByCollection(collectionName: string) { - const items = templates?.filter?.((template) => template.collectionName === collectionName); + getTemplatesByCollection(collectionName: string, resourceName: string = null) { + const items = templates?.filter?.((template) => (!template.resourceName && template.collectionName === collectionName) || (template.resourceName && template.resourceName === resourceName)); return items || []; }, + }; }; diff --git a/packages/core/client/src/workflow/triggers/schedule/RepeatField.tsx b/packages/core/client/src/workflow/triggers/schedule/RepeatField.tsx index f8a0da4d6..874e5aae0 100644 --- a/packages/core/client/src/workflow/triggers/schedule/RepeatField.tsx +++ b/packages/core/client/src/workflow/triggers/schedule/RepeatField.tsx @@ -1,11 +1,11 @@ -import React from "react"; import { css } from "@emotion/css"; import { InputNumber, Select } from "antd"; +import React from "react"; import { useTranslation } from "react-i18next"; import { Cron } from 'react-js-cron'; - import CronZhCN from './locale/Cron.zh-CN'; + const languages = { 'zh-CN': CronZhCN, }; @@ -18,7 +18,7 @@ const RepeatOptions = [ { value: 86400_000, text: 'By day', unitText: 'Days' }, { value: 604800_000, text: 'By week', unitText: 'Weeks' }, // { value: 18144_000_000, text: 'By 30 days' }, - { value: 'cron', text: 'Advance' } + { value: 'cron', text: 'Advanced' } ]; function getNumberOption(v) { diff --git a/packages/core/database/src/__tests__/fields/formula-field.test.ts b/packages/core/database/src/__tests__/fields/formula-field.test.ts index bf5af3b1a..92f561fb2 100644 --- a/packages/core/database/src/__tests__/fields/formula-field.test.ts +++ b/packages/core/database/src/__tests__/fields/formula-field.test.ts @@ -1,5 +1,5 @@ -import { Database } from '../../database'; import { mockDatabase } from '..'; +import { Database } from '../../database'; import { FormulaField } from '../../fields'; describe('formula field', () => { @@ -13,10 +13,13 @@ describe('formula field', () => { await db.close(); }); - 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({ name: 'tests', - fields: [{ type: 'float', name: 'price' }, { type: 'float', name: 'count' }], + fields: [ + { type: 'float', name: 'price' }, + { type: 'float', name: 'count' }, + ], }); await db.sync(); @@ -42,7 +45,11 @@ describe('formula field', () => { const expression = 'price*count'; const Test = db.collection({ 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(); @@ -58,5 +65,5 @@ describe('formula field', () => { test.set('count', '6'); await test.save(); expect(test.get('sum')).toEqual(sumField.caculate(expression, test.toJSON())); - }) -}); \ No newline at end of file + }); +}); diff --git a/packages/core/database/src/model-hook.ts b/packages/core/database/src/model-hook.ts index a7c1bb749..17aa755f1 100644 --- a/packages/core/database/src/model-hook.ts +++ b/packages/core/database/src/model-hook.ts @@ -1,7 +1,7 @@ import lodash from 'lodash'; +import type { SequelizeHooks } from 'sequelize/types/lib/hooks'; import Database from './database'; import { Model } from './model'; -import type { SequelizeHooks } from 'sequelize/types/lib/hooks'; const { hooks } = require('sequelize/lib/hooks'); @@ -60,6 +60,7 @@ export class ModelHook { buildSequelizeHook(type) { return async (...args: any[]) => { const modelName = this.findModelName(args); + if (modelName) { // emit model event await this.database.emitAsync(`${modelName}.${type}`, ...args); diff --git a/packages/core/database/src/relation-repository/single-relation-repository.ts b/packages/core/database/src/relation-repository/single-relation-repository.ts index 9c59a215c..899f7d5e0 100644 --- a/packages/core/database/src/relation-repository/single-relation-repository.ts +++ b/packages/core/database/src/relation-repository/single-relation-repository.ts @@ -59,7 +59,7 @@ export abstract class SingleRelationRepository extends RelationRepository { } async findOne(options?: SingleRelationFindOption): Promise> { - return this.find(options); + return this.find({ ...options, filterByTk: null } as any); } @transaction() @@ -85,6 +85,10 @@ export abstract class SingleRelationRepository extends RelationRepository { transaction, }); + if (!target) { + throw new Error('The record does not exist'); + } + await updateModelByValues(target, options?.values, { ...lodash.omit(options, 'values'), transaction, diff --git a/packages/core/server/src/commands/index.ts b/packages/core/server/src/commands/index.ts index 1142ddd7b..d95d4b36e 100644 --- a/packages/core/server/src/commands/index.ts +++ b/packages/core/server/src/commands/index.ts @@ -6,6 +6,7 @@ export function registerCli(app: Application) { require('./db-clean').default(app); require('./db-sync').default(app); require('./install').default(app); + require('./migrator').default(app); require('./start').default(app); require('./upgrade').default(app); diff --git a/packages/core/server/src/commands/migrator.ts b/packages/core/server/src/commands/migrator.ts new file mode 100644 index 000000000..58f526348 --- /dev/null +++ b/packages/core/server/src/commands/migrator.ts @@ -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(); + }); +}; diff --git a/packages/core/server/src/middlewares/data-wrapping.ts b/packages/core/server/src/middlewares/data-wrapping.ts index 7258194c7..dfdf5ccc7 100644 --- a/packages/core/server/src/middlewares/data-wrapping.ts +++ b/packages/core/server/src/middlewares/data-wrapping.ts @@ -18,7 +18,7 @@ export function dataWrapping() { if (!ctx.body) { if (ctx.action.actionName == 'get') { - ctx.status = 404; + ctx.status = 200; } } diff --git a/packages/plugins/audit-logs/src/server/hooks/after-create.ts b/packages/plugins/audit-logs/src/server/hooks/after-create.ts index e2df66f75..4776ca534 100644 --- a/packages/plugins/audit-logs/src/server/hooks/after-create.ts +++ b/packages/plugins/audit-logs/src/server/hooks/after-create.ts @@ -1,42 +1,44 @@ -import Database from '@nocobase/database'; +import Application from '@nocobase/server'; import { LOG_TYPE_CREATE } from '../constants'; -export async function afterCreate(model, options) { - const db = model.constructor.database as Database; - const collection = db.getCollection(model.constructor.name); - if (!collection.options.logging) { - return; - } - const transaction = options.transaction; - const AuditLog = db.getCollection('auditLogs'); - const currentUserId = options?.context?.state?.currentUser?.id; - try { - const changes = []; - const changed = model.changed(); - if (changed) { - changed.forEach((key: string) => { - const field = collection.findField((field) => { - return field.name === key || field.options.field === key; - }); - if (field && !field.options.hidden) { - changes.push({ - field: field.options, - after: model.get(key), - }); - } - }); +export function afterCreate(app: Application) { + return async (model, options) => { + const db = app.db; + const collection = db.getCollection(model.constructor.name); + if (!collection || !collection.options.logging) { + return; } - await AuditLog.repository.create({ - values: { - type: LOG_TYPE_CREATE, - collectionName: model.constructor.name, - recordId: model.get(model.constructor.primaryKeyAttribute), - createdAt: model.get('createdAt'), - userId: currentUserId, - changes, - }, - transaction, - hooks: false, - }); - } catch (error) {} + const transaction = options.transaction; + const AuditLog = db.getCollection('auditLogs'); + const currentUserId = options?.context?.state?.currentUser?.id; + try { + const changes = []; + const changed = model.changed(); + if (changed) { + changed.forEach((key: string) => { + const field = collection.findField((field) => { + return field.name === key || field.options.field === key; + }); + if (field && !field.options.hidden) { + changes.push({ + field: field.options, + after: model.get(key), + }); + } + }); + } + await AuditLog.repository.create({ + values: { + type: LOG_TYPE_CREATE, + collectionName: model.constructor.name, + recordId: model.get(model.constructor.primaryKeyAttribute), + createdAt: model.get('createdAt'), + userId: currentUserId, + changes, + }, + transaction, + hooks: false, + }); + } catch (error) {} + }; } diff --git a/packages/plugins/audit-logs/src/server/hooks/after-destroy.ts b/packages/plugins/audit-logs/src/server/hooks/after-destroy.ts index 03e1d7e6a..42677f933 100644 --- a/packages/plugins/audit-logs/src/server/hooks/after-destroy.ts +++ b/packages/plugins/audit-logs/src/server/hooks/after-destroy.ts @@ -1,45 +1,47 @@ -import Database from '@nocobase/database'; +import Application from '@nocobase/server'; import { LOG_TYPE_DESTROY } from '../constants'; -export async function afterDestroy(model, options) { - const db = model.constructor.database as Database; - const collection = db.getCollection(model.constructor.name); - if (!collection.options.logging) { - return; - } - const transaction = options.transaction; - const AuditLog = db.getCollection('auditLogs'); - const currentUserId = options?.context?.state?.currentUser?.id; - try { - const changes = []; - Object.keys(model.get()).forEach((key: string) => { - const field = collection.findField((field) => { - return field.name === key || field.options.field === key; - }); - if (field) { - changes.push({ - field: field.options, - before: model.get(key), +export function afterDestroy(app: Application) { + return async (model, options) => { + const db = app.db; + const collection = db.getCollection(model.constructor.name); + if (!collection || !collection.options.logging) { + return; + } + const transaction = options.transaction; + const AuditLog = db.getCollection('auditLogs'); + const currentUserId = options?.context?.state?.currentUser?.id; + try { + const changes = []; + Object.keys(model.get()).forEach((key: string) => { + const field = collection.findField((field) => { + return field.name === key || field.options.field === key; }); - } - }); - await AuditLog.repository.create({ - values: { - type: LOG_TYPE_DESTROY, - collectionName: model.constructor.name, - recordId: model.get(model.constructor.primaryKeyAttribute), - userId: currentUserId, - changes, - }, - transaction, - hooks: false, - }); - // if (!options.transaction) { - // await transaction.commit(); - // } - } catch (error) { - // if (!options.transaction) { - // await transaction.rollback(); - // } - } + if (field) { + changes.push({ + field: field.options, + before: model.get(key), + }); + } + }); + await AuditLog.repository.create({ + values: { + type: LOG_TYPE_DESTROY, + collectionName: model.constructor.name, + recordId: model.get(model.constructor.primaryKeyAttribute), + userId: currentUserId, + changes, + }, + transaction, + hooks: false, + }); + // if (!options.transaction) { + // await transaction.commit(); + // } + } catch (error) { + // if (!options.transaction) { + // await transaction.rollback(); + // } + } + }; } diff --git a/packages/plugins/audit-logs/src/server/hooks/after-update.ts b/packages/plugins/audit-logs/src/server/hooks/after-update.ts index a687f996a..693b4630a 100644 --- a/packages/plugins/audit-logs/src/server/hooks/after-update.ts +++ b/packages/plugins/audit-logs/src/server/hooks/after-update.ts @@ -1,54 +1,56 @@ -import Database from '@nocobase/database'; +import Application from '@nocobase/server'; import { LOG_TYPE_UPDATE } from '../constants'; -export async function afterUpdate(model, options) { - const db = model.constructor.database as Database; - const collection = db.getCollection(model.constructor.name); - if (!collection.options.logging) { - return; - } - const changed = model.changed(); - if (!changed) { - return; - } - const transaction = options.transaction; - const AuditLog = db.getCollection('auditLogs'); - const currentUserId = options?.context?.state?.currentUser?.id; - const changes = []; - changed.forEach((key: string) => { - const field = collection.findField((field) => { - return field.name === key || field.options.field === key; - }); - if (field && !field.options.hidden) { - changes.push({ - field: field.options, - after: model.get(key), - before: model.previous(key), - }); +export function afterUpdate(app: Application) { + return async (model, options) => { + const db = app.db; + const collection = db.getCollection(model.constructor.name); + if (!collection || !collection.options.logging) { + return; } - }); - if (!changes.length) { - return; - } - try { - await AuditLog.repository.create({ - values: { - type: LOG_TYPE_UPDATE, - collectionName: model.constructor.name, - recordId: model.get(model.constructor.primaryKeyAttribute), - createdAt: model.get('updatedAt'), - userId: currentUserId, - changes, - }, - transaction, - hooks: false, + const changed = model.changed(); + if (!changed) { + return; + } + const transaction = options.transaction; + const AuditLog = db.getCollection('auditLogs'); + const currentUserId = options?.context?.state?.currentUser?.id; + const changes = []; + changed.forEach((key: string) => { + const field = collection.findField((field) => { + return field.name === key || field.options.field === key; + }); + if (field && !field.options.hidden) { + changes.push({ + field: field.options, + after: model.get(key), + before: model.previous(key), + }); + } }); - // if (!options.transaction) { - // await transaction.commit(); - // } - } catch (error) { - // if (!options.transaction) { - // await transaction.rollback(); - // } - } + if (!changes.length) { + return; + } + try { + await AuditLog.repository.create({ + values: { + type: LOG_TYPE_UPDATE, + collectionName: model.constructor.name, + recordId: model.get(model.constructor.primaryKeyAttribute), + createdAt: model.get('updatedAt'), + userId: currentUserId, + changes, + }, + transaction, + hooks: false, + }); + // if (!options.transaction) { + // await transaction.commit(); + // } + } catch (error) { + // if (!options.transaction) { + // await transaction.rollback(); + // } + } + }; } diff --git a/packages/plugins/audit-logs/src/server/index.ts b/packages/plugins/audit-logs/src/server/index.ts index da304f973..60e333d56 100644 --- a/packages/plugins/audit-logs/src/server/index.ts +++ b/packages/plugins/audit-logs/src/server/index.ts @@ -4,9 +4,9 @@ import { afterCreate, afterDestroy, afterUpdate } from './hooks'; export default class PluginActionLogs extends Plugin { async beforeLoad() { - this.db.on('afterCreate', afterCreate); - this.db.on('afterUpdate', afterUpdate); - this.db.on('afterDestroy', afterDestroy); + this.db.on('afterCreate', afterCreate(this.app)); + this.db.on('afterUpdate', afterUpdate(this.app)); + this.db.on('afterDestroy', afterDestroy(this.app)); } async load() { diff --git a/packages/plugins/collection-manager/src/migrations/20220613103214-alert-sub-table.ts b/packages/plugins/collection-manager/src/migrations/20220613103214-alert-sub-table.ts new file mode 100644 index 000000000..521190881 --- /dev/null +++ b/packages/plugins/collection-manager/src/migrations/20220613103214-alert-sub-table.ts @@ -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(); + } + } + } +} diff --git a/packages/plugins/collection-manager/src/server.ts b/packages/plugins/collection-manager/src/server.ts index 6980b8538..a94ae3ccb 100644 --- a/packages/plugins/collection-manager/src/server.ts +++ b/packages/plugins/collection-manager/src/server.ts @@ -9,6 +9,7 @@ import { beforeCreateForReverseField, beforeInitOptions } from './hooks'; +import AlertSubTableMigration from './migrations/20220613103214-alert-sub-table'; import { CollectionModel, FieldModel } from './models'; export class CollectionManagerPlugin extends Plugin { @@ -18,6 +19,11 @@ export class CollectionManagerPlugin extends Plugin { FieldModel, }); + this.db.addMigration({ + name: 'collection-manager/20220613103214-alert-sub-table', + migration: AlertSubTableMigration, + }); + this.app.db.registerRepositories({ CollectionRepository, }); @@ -244,7 +250,7 @@ export class CollectionManagerPlugin extends Plugin { const collection: Collection = ctx.db.getCollection(collectionName); if (collection) { for (const [, field] of collection.fields) { - if (field.options.interface === 'subTable') { + if (['subTable', 'o2m'].includes(field.options.interface)) { updateAssociationValues.push(field.name); } } @@ -255,7 +261,7 @@ export class CollectionManagerPlugin extends Plugin { const collection: Collection = ctx.db.getCollection(association?.target); if (collection) { for (const [, field] of collection.fields) { - if (field.options.interface === 'subTable') { + if (['subTable', 'o2m'].includes(field.options.interface)) { updateAssociationValues.push(field.name); } } diff --git a/packages/plugins/ui-schema-storage/src/collections/uiSchemaTemplates.ts b/packages/plugins/ui-schema-storage/src/collections/uiSchemaTemplates.ts index 61c59f1bd..a7b37c190 100644 --- a/packages/plugins/ui-schema-storage/src/collections/uiSchemaTemplates.ts +++ b/packages/plugins/ui-schema-storage/src/collections/uiSchemaTemplates.ts @@ -21,6 +21,10 @@ export default defineCollection({ type: 'string', name: 'associationName', }, + { + type: 'string', + name: 'resourceName', + }, { type: 'belongsTo', name: 'uiSchema', diff --git a/packages/plugins/users/src/collections/users.ts b/packages/plugins/users/src/collections/users.ts index 8d6fc7408..42cd619bc 100644 --- a/packages/plugins/users/src/collections/users.ts +++ b/packages/plugins/users/src/collections/users.ts @@ -52,7 +52,7 @@ export default { }, }, { - interface: 'linkTo', + interface: 'm2m', type: 'belongsToMany', name: 'roles', target: 'roles',