feat: junction collection for linkTo field (#296)

This commit is contained in:
chenos 2022-04-18 18:57:21 +08:00 committed by GitHub
parent 4510242651
commit da9e08a59f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 174 additions and 11 deletions

View File

@ -42,12 +42,12 @@ services:
dockerfile: ./docker/nocobase/Dockerfile dockerfile: ./docker/nocobase/Dockerfile
networks: networks:
- nocobase - nocobase
command: [ "yarn", "start" ] command: [ "yarn", "start-pm2" ]
working_dir: /app working_dir: /app
env_file: ./.env env_file: ./.env
volumes: volumes:
- ./:/app - ./:/app
expose: expose:
- 8000 - ${SERVER_PORT}
ports: ports:
- "${APP_PORT}:8000" - "${SERVER_PORT}:${SERVER_PORT}"

View File

@ -2,9 +2,9 @@ import { useFieldSchema, useForm } from '@formily/react';
import { Modal } from 'antd'; import { Modal } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useCollection } from '../../collection-manager';
import { useActionContext } from '../../schema-component'; import { useActionContext } from '../../schema-component';
import { useBlockRequestContext, useFilterByTk } from '../BlockProvider'; import { useBlockRequestContext, useFilterByTk } from '../BlockProvider';
import { useFormBlockContext } from '../FormBlockProvider';
import { TableFieldResource } from '../TableFieldProvider'; import { TableFieldResource } from '../TableFieldProvider';
export const usePickActionProps = () => { export const usePickActionProps = () => {
@ -30,22 +30,38 @@ function isURL(string) {
export const useCreateActionProps = () => { export const useCreateActionProps = () => {
const form = useForm(); const form = useForm();
const { resource, __parent } = useBlockRequestContext(); const { field, resource, __parent } = useBlockRequestContext();
const { visible, setVisible } = useActionContext(); const { visible, setVisible } = useActionContext();
const { field } = useFormBlockContext();
const history = useHistory(); const history = useHistory();
const { t } = useTranslation(); const { t } = useTranslation();
const actionSchema = useFieldSchema(); const actionSchema = useFieldSchema();
const { fields, getField } = useCollection();
return { return {
async onClick() { async onClick() {
const fieldNames = fields.map((field) => field.name);
const skipValidator = actionSchema?.['x-action-settings']?.skipValidator; const skipValidator = actionSchema?.['x-action-settings']?.skipValidator;
const overwriteValues = actionSchema?.['x-action-settings']?.overwriteValues; const overwriteValues = actionSchema?.['x-action-settings']?.overwriteValues;
if (!skipValidator) { if (!skipValidator) {
await form.submit(); 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({ await resource.create({
values: { values: {
...form.values, ...values,
...overwriteValues, ...overwriteValues,
}, },
}); });
@ -75,10 +91,11 @@ export const useCreateActionProps = () => {
export const useUpdateActionProps = () => { export const useUpdateActionProps = () => {
const form = useForm(); const form = useForm();
const filterByTk = useFilterByTk(); const filterByTk = useFilterByTk();
const { resource, __parent } = useBlockRequestContext(); const { field, resource, __parent } = useBlockRequestContext();
const { setVisible } = useActionContext(); const { setVisible } = useActionContext();
const actionSchema = useFieldSchema(); const actionSchema = useFieldSchema();
const history = useHistory(); const history = useHistory();
const { fields, getField } = useCollection();
return { return {
async onClick() { async onClick() {
const skipValidator = actionSchema?.['x-action-settings']?.skipValidator; const skipValidator = actionSchema?.['x-action-settings']?.skipValidator;
@ -86,10 +103,33 @@ export const useUpdateActionProps = () => {
if (!skipValidator) { if (!skipValidator) {
await form.submit(); 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({ await resource.update({
filterByTk, filterByTk,
values: { values: {
...form.values, ...values,
...overwriteValues, ...overwriteValues,
}, },
}); });

View File

@ -2,7 +2,7 @@ import { Field } from '@formily/core';
import { connect, useField, useFieldSchema } from '@formily/react'; import { connect, useField, useFieldSchema } from '@formily/react';
import { merge } from '@formily/shared'; import { merge } from '@formily/shared';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useCompile, useComponent } from '..'; import { useCompile, useComponent, useFormBlockContext } from '..';
import { CollectionFieldProvider } from './CollectionFieldProvider'; import { CollectionFieldProvider } from './CollectionFieldProvider';
import { useCollectionField } from './hooks'; import { useCollectionField } from './hooks';
@ -21,6 +21,13 @@ const InternalField: React.FC = (props) => {
field.required = !!uiSchema['required']; 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: 初步适配 // TODO: 初步适配
useEffect(() => { useEffect(() => {
if (!uiSchema) { if (!uiSchema) {

View File

@ -37,6 +37,7 @@ export const RemoteCollectionManagerProvider = (props: any) => {
filter: { filter: {
// inherit: false, // inherit: false,
}, },
sort: ['sort'],
}, },
}; };
const service = useRequest(options); const service = useRequest(options);

View File

@ -84,6 +84,13 @@ export const linkTo: IField = {
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Select', 'x-component': 'Select',
}, },
through: {
type: 'string',
title: '{{t("Junction collection")}}',
'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
'x-decorator': 'FormItem',
'x-component': 'Select',
},
// 'reverseField.uiSchema.title': { // 'reverseField.uiSchema.title': {
// type: 'string', // type: 'string',
// title: '{{t("Reverse field display name")}}', // title: '{{t("Reverse field display name")}}',

View File

@ -61,6 +61,7 @@ export abstract class RelationRepository {
await updateAssociations(instance, values, options); await updateAssociations(instance, values, options);
if (options.hooks !== false) { if (options.hooks !== false) {
await this.db.emitAsync(`${this.targetCollection.name}.afterCreateWithAssociations`, instance, options);
const eventName = `${this.targetCollection.name}.afterSaveWithAssociations`; const eventName = `${this.targetCollection.name}.afterSaveWithAssociations`;
await this.db.emitAsync(eventName, instance, options); await this.db.emitAsync(eventName, instance, options);
} }

View File

@ -1,5 +1,6 @@
import { Collection } from '@nocobase/database'; import { Collection } from '@nocobase/database';
import { Plugin } from '@nocobase/server'; import { Plugin } from '@nocobase/server';
import { uid } from '@nocobase/utils';
import lodash from 'lodash'; import lodash from 'lodash';
import path from 'path'; import path from 'path';
import { CollectionRepository } from '.'; 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<CollectionRepository>('collections').load({
filter: {
'name.$in': [throughName, sourceName, targetName],
},
});
});
this.app.on('beforeStart', async () => { this.app.on('beforeStart', async () => {
await this.app.db.getRepository<CollectionRepository>('collections').load(); await this.app.db.getRepository<CollectionRepository>('collections').load();
}); });