From da9e08a59faaff4fa8d2189b8290a7828c6c7217 Mon Sep 17 00:00:00 2001 From: chenos Date: Mon, 18 Apr 2022 18:57:21 +0800 Subject: [PATCH] feat: junction collection for linkTo field (#296) --- docker-compose.yml | 6 +- .../client/src/block-provider/hooks/index.ts | 52 ++++++++- .../collection-manager/CollectionField.tsx | 11 +- .../CollectionManagerProvider.tsx | 1 + .../collection-manager/interfaces/linkTo.ts | 7 ++ .../relation-repository.ts | 1 + .../plugins/collection-manager/src/plugin.ts | 107 ++++++++++++++++++ 7 files changed, 174 insertions(+), 11 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d6f6d1cd2..fcaf2875b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,12 +42,12 @@ services: dockerfile: ./docker/nocobase/Dockerfile networks: - nocobase - command: [ "yarn", "start" ] + command: [ "yarn", "start-pm2" ] working_dir: /app env_file: ./.env volumes: - ./:/app expose: - - 8000 + - ${SERVER_PORT} ports: - - "${APP_PORT}:8000" \ No newline at end of file + - "${SERVER_PORT}:${SERVER_PORT}" \ No newline at end of file diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index 406942c66..2c55b3c2d 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -2,9 +2,9 @@ import { useFieldSchema, useForm } from '@formily/react'; import { Modal } from 'antd'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; +import { useCollection } from '../../collection-manager'; import { useActionContext } from '../../schema-component'; import { useBlockRequestContext, useFilterByTk } from '../BlockProvider'; -import { useFormBlockContext } from '../FormBlockProvider'; import { TableFieldResource } from '../TableFieldProvider'; export const usePickActionProps = () => { @@ -30,22 +30,38 @@ function isURL(string) { export const useCreateActionProps = () => { const form = useForm(); - const { resource, __parent } = useBlockRequestContext(); + const { field, resource, __parent } = useBlockRequestContext(); const { visible, setVisible } = useActionContext(); - const { field } = useFormBlockContext(); const history = useHistory(); const { t } = useTranslation(); const actionSchema = useFieldSchema(); + const { fields, getField } = useCollection(); return { async onClick() { + const fieldNames = fields.map((field) => field.name); const skipValidator = actionSchema?.['x-action-settings']?.skipValidator; const overwriteValues = actionSchema?.['x-action-settings']?.overwriteValues; if (!skipValidator) { await form.submit(); } + const values = {}; + for (const key in form.values) { + if (fieldNames.includes(key)) { + const items = form.values[key]; + const collectionField = getField(key); + const targetKey = collectionField.targetKey || 'id'; + if (Array.isArray(items)) { + values[key] = items.map((item) => item[targetKey]); + } else if (items && typeof items === 'object') { + values[key] = items[targetKey]; + } + } else { + values[key] = form.values[key]; + } + } await resource.create({ values: { - ...form.values, + ...values, ...overwriteValues, }, }); @@ -75,10 +91,11 @@ export const useCreateActionProps = () => { export const useUpdateActionProps = () => { const form = useForm(); const filterByTk = useFilterByTk(); - const { resource, __parent } = useBlockRequestContext(); + const { field, resource, __parent } = useBlockRequestContext(); const { setVisible } = useActionContext(); const actionSchema = useFieldSchema(); const history = useHistory(); + const { fields, getField } = useCollection(); return { async onClick() { const skipValidator = actionSchema?.['x-action-settings']?.skipValidator; @@ -86,10 +103,33 @@ export const useUpdateActionProps = () => { if (!skipValidator) { await form.submit(); } + const fieldNames = fields.map((field) => field.name); + const values = {}; + for (const key in form.values) { + if (fieldNames.includes(key)) { + if (!field.added.has(key)) { + continue; + } + const items = form.values[key]; + const collectionField = getField(key); + if (collectionField.interface === 'linkTo') { + const targetKey = collectionField.targetKey || 'id'; + if (Array.isArray(items)) { + values[key] = items.map((item) => item[targetKey]); + } else if (items && typeof items === 'object') { + values[key] = items[targetKey]; + } + } else { + values[key] = form.values[key]; + } + } else { + values[key] = form.values[key]; + } + } await resource.update({ filterByTk, values: { - ...form.values, + ...values, ...overwriteValues, }, }); diff --git a/packages/core/client/src/collection-manager/CollectionField.tsx b/packages/core/client/src/collection-manager/CollectionField.tsx index 76ca8981a..ca1365033 100644 --- a/packages/core/client/src/collection-manager/CollectionField.tsx +++ b/packages/core/client/src/collection-manager/CollectionField.tsx @@ -2,7 +2,7 @@ import { Field } from '@formily/core'; import { connect, useField, useFieldSchema } from '@formily/react'; import { merge } from '@formily/shared'; import React, { useEffect } from 'react'; -import { useCompile, useComponent } from '..'; +import { useCompile, useComponent, useFormBlockContext } from '..'; import { CollectionFieldProvider } from './CollectionFieldProvider'; import { useCollectionField } from './hooks'; @@ -21,6 +21,13 @@ const InternalField: React.FC = (props) => { field.required = !!uiSchema['required']; } }; + const ctx = useFormBlockContext(); + useEffect(() => { + if (ctx?.field) { + ctx.field.added = ctx.field.added || new Set(); + ctx.field.added.add(fieldSchema.name); + } + }); // TODO: 初步适配 useEffect(() => { if (!uiSchema) { @@ -40,7 +47,7 @@ const InternalField: React.FC = (props) => { const originalProps = compile(uiSchema['x-component-props']) || {}; const componentProps = merge(originalProps, field.componentProps || {}); field.component = [component, componentProps]; - + // if (interfaceType === 'input') { // field.componentProps.ellipsis = true; // } else if (interfaceType === 'textarea') { diff --git a/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx b/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx index 862a33e83..9b800c0e5 100644 --- a/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx +++ b/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx @@ -37,6 +37,7 @@ export const RemoteCollectionManagerProvider = (props: any) => { filter: { // inherit: false, }, + sort: ['sort'], }, }; const service = useRequest(options); diff --git a/packages/core/client/src/collection-manager/interfaces/linkTo.ts b/packages/core/client/src/collection-manager/interfaces/linkTo.ts index 9b7c6b0b6..e321491a6 100644 --- a/packages/core/client/src/collection-manager/interfaces/linkTo.ts +++ b/packages/core/client/src/collection-manager/interfaces/linkTo.ts @@ -84,6 +84,13 @@ export const linkTo: IField = { 'x-decorator': 'FormItem', 'x-component': 'Select', }, + through: { + type: 'string', + title: '{{t("Junction collection")}}', + 'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'], + 'x-decorator': 'FormItem', + 'x-component': 'Select', + }, // 'reverseField.uiSchema.title': { // type: 'string', // title: '{{t("Reverse field display name")}}', diff --git a/packages/core/database/src/relation-repository/relation-repository.ts b/packages/core/database/src/relation-repository/relation-repository.ts index 3fa5b5bec..2fd2f770d 100644 --- a/packages/core/database/src/relation-repository/relation-repository.ts +++ b/packages/core/database/src/relation-repository/relation-repository.ts @@ -61,6 +61,7 @@ export abstract class RelationRepository { await updateAssociations(instance, values, options); if (options.hooks !== false) { + await this.db.emitAsync(`${this.targetCollection.name}.afterCreateWithAssociations`, instance, options); const eventName = `${this.targetCollection.name}.afterSaveWithAssociations`; await this.db.emitAsync(eventName, instance, options); } diff --git a/packages/plugins/collection-manager/src/plugin.ts b/packages/plugins/collection-manager/src/plugin.ts index c4c1ed7e0..0886cb923 100644 --- a/packages/plugins/collection-manager/src/plugin.ts +++ b/packages/plugins/collection-manager/src/plugin.ts @@ -1,5 +1,6 @@ import { Collection } from '@nocobase/database'; import { Plugin } from '@nocobase/server'; +import { uid } from '@nocobase/utils'; import lodash from 'lodash'; import path from 'path'; import { CollectionRepository } from '.'; @@ -63,6 +64,112 @@ export class CollectionManagerPlugin extends Plugin { } }); + this.app.db.on('fields.afterCreateWithAssociations', async (model, { context, transaction }) => { + if (!context) { + return; + } + if (!model.get('through')) { + return; + } + const [throughName, sourceName, targetName] = [ + model.get('through'), + model.get('collectionName'), + model.get('target'), + ]; + const db = this.app.db; + const through = await db.getRepository('collections').findOne({ + filter: { + name: throughName, + }, + transaction, + }); + if (!through) { + return; + } + const repository = db.getRepository('collections.fields', throughName); + await repository.create({ + transaction, + values: { + name: `f_${uid()}`, + type: 'belongsTo', + target: sourceName, + targetKey: model.get('sourceKey'), + foreignKey: model.get('foreignKey'), + interface: 'linkTo', + reverseField: { + interface: 'linkTo', + uiSchema: { + title: through.get('title'), + 'x-component': 'RecordPicker', + 'x-component-props': { + // mode: 'tags', + multiple: true, + fieldNames: { + label: 'id', + value: 'id', + }, + }, + }, + }, + uiSchema: { + title: db.getCollection(sourceName)?.options?.title || sourceName, + 'x-component': 'RecordPicker', + 'x-component-props': { + // mode: 'tags', + multiple: false, + fieldNames: { + label: 'id', + value: 'id', + }, + }, + }, + }, + }); + await repository.create({ + transaction, + values: { + name: `f_${uid()}`, + type: 'belongsTo', + target: targetName, + targetKey: model.get('targetKey'), + foreignKey: model.get('otherKey'), + interface: 'linkTo', + reverseField: { + interface: 'linkTo', + uiSchema: { + title: through.get('title'), + 'x-component': 'RecordPicker', + 'x-component-props': { + // mode: 'tags', + multiple: true, + fieldNames: { + label: 'id', + value: 'id', + }, + }, + }, + }, + uiSchema: { + title: db.getCollection(targetName)?.options?.title || targetName, + 'x-component': 'RecordPicker', + 'x-component-props': { + // mode: 'tags', + multiple: false, + fieldNames: { + label: 'id', + value: 'id', + }, + }, + }, + }, + }); + await db.getRepository('collections').load({ + filter: { + 'name.$in': [throughName, sourceName, targetName], + }, + }); + }); + this.app.on('beforeStart', async () => { await this.app.db.getRepository('collections').load(); });