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
|
||||
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"
|
||||
- "${SERVER_PORT}:${SERVER_PORT}"
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
@ -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') {
|
||||
|
@ -37,6 +37,7 @@ export const RemoteCollectionManagerProvider = (props: any) => {
|
||||
filter: {
|
||||
// inherit: false,
|
||||
},
|
||||
sort: ['sort'],
|
||||
},
|
||||
};
|
||||
const service = useRequest(options);
|
||||
|
@ -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")}}',
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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<CollectionRepository>('collections').load({
|
||||
filter: {
|
||||
'name.$in': [throughName, sourceName, targetName],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
this.app.on('beforeStart', async () => {
|
||||
await this.app.db.getRepository<CollectionRepository>('collections').load();
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user