mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 21:49:25 +08:00
feat: junction collection for linkTo field (#296)
This commit is contained in:
parent
4510242651
commit
da9e08a59f
@ -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}"
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
@ -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")}}',
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user