feat: display association fields (#512)

* feat: association field features

* fix: remove comments

* fix: association field in creation form will trigger error

* fix: column decorator title

* fix: column designer title

* fix: association field in table

* feat: adjust documents

* fix: remove m2o subfield mode

* fix: adjust title field display condition

* fix: relation field title bug

* fix: o2m multiple is true

* feat: association fields are loaded on demand

* fix: support sub field

* feat: remove FormField require config

* fix: two lines in Columns config menu of table block

* fix: could not find schema node

* fix: add form context to internal table block

* fix(client): non-empty judgment

* feat: translations

* fix: add / edit field title compile

* fix: unique

* fix: association feature bugs

* feat: add oho & o2m selector filter

* fix: add field added logic in FormField and TableField

* fix: remove updateAssociationValues middleware

* feat: recordprovider in association fields

* feat: add kanban association appends

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
金昶 2022-06-29 12:57:43 +08:00 committed by GitHub
parent 95d0b524d0
commit 32e744319e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 829 additions and 225 deletions

View File

@ -9,6 +9,7 @@ import { KanbanBlockProvider, useKanbanBlockProps } from './KanbanBlockProvider'
import { TableBlockProvider, useTableBlockProps } from './TableBlockProvider'; import { TableBlockProvider, useTableBlockProps } from './TableBlockProvider';
import { TableFieldProvider, useTableFieldProps } from './TableFieldProvider'; import { TableFieldProvider, useTableFieldProps } from './TableFieldProvider';
import { TableSelectorProvider, useTableSelectorProps } from './TableSelectorProvider'; import { TableSelectorProvider, useTableSelectorProps } from './TableSelectorProvider';
import { FormFieldProvider, useFormFieldProps } from './FormFieldProvider';
export const BlockSchemaComponentProvider: React.FC = (props) => { export const BlockSchemaComponentProvider: React.FC = (props) => {
return ( return (
@ -19,6 +20,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
TableBlockProvider, TableBlockProvider,
TableSelectorProvider, TableSelectorProvider,
FormBlockProvider, FormBlockProvider,
FormFieldProvider,
DetailsBlockProvider, DetailsBlockProvider,
KanbanBlockProvider, KanbanBlockProvider,
RecordLink, RecordLink,
@ -30,6 +32,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
useParamsFromRecord, useParamsFromRecord,
useCalendarBlockProps, useCalendarBlockProps,
useFormBlockProps, useFormBlockProps,
useFormFieldProps,
useDetailsBlockProps, useDetailsBlockProps,
useTableFieldProps, useTableFieldProps,
useTableBlockProps, useTableBlockProps,

View File

@ -1,5 +1,5 @@
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { useField } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import { Spin } from 'antd'; import { Spin } from 'antd';
import React, { createContext, useContext, useEffect, useMemo } from 'react'; import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { BlockProvider, useBlockRequestContext } from './BlockProvider';
@ -29,6 +29,7 @@ const InternalFormBlockProvider = (props) => {
field, field,
service, service,
resource, resource,
updateAssociationValues: [],
}} }}
> >
{props.children} {props.children}

View File

@ -0,0 +1,87 @@
import { createForm, onFieldReact, onFormInputChange, onFormValuesChange } from '@formily/core';
import { ArrayField, Field, ObjectField } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react';
import { Spin } from 'antd';
import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { RecordProvider } from '../record-provider';
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
import { useFormBlockContext } from './FormBlockProvider';
export const FormFieldContext = createContext<any>({});
const InternalFormFieldProvider = (props) => {
const { action, readPretty, fieldName } = props;
const formBlockCtx = useFormBlockContext();
if (!formBlockCtx?.updateAssociationValues?.includes(fieldName)) {
formBlockCtx?.updateAssociationValues?.push(fieldName);
}
const field = useField();
const form = useMemo(
() =>
createForm({
effects() {
onFormValuesChange((form) => {
formBlockCtx?.form?.setValuesIn(fieldName, form.values);
});
},
readPretty,
}),
[],
);
const { resource, service } = useBlockRequestContext();
if (service.loading) {
return <Spin />;
}
console.log('InternalFormFieldProvider', fieldName);
return (
<RecordProvider record={service?.data?.data}>
<FormFieldContext.Provider
value={{
action,
form,
field,
service,
resource,
fieldName,
}}
>
{props.children}
</FormFieldContext.Provider>
</RecordProvider>
);
}
export const WithoutFormFieldResource = createContext(null);
export const FormFieldProvider = (props) => {
console.log('FormFieldProvider', props);
return (
<WithoutFormFieldResource.Provider value={false}>
<BlockProvider block={'FormField'} {...props}>
<InternalFormFieldProvider {...props} />
</BlockProvider>
</WithoutFormFieldResource.Provider>
)
}
export const useFormFieldContext = () => {
return useContext(FormFieldContext);
};
export const useFormFieldProps = () => {
const ctx = useFormFieldContext();
useEffect(() => {
ctx?.form?.setInitialValues(ctx.service?.data?.data);
}, []);
return {
form: ctx.form,
};
}

View File

@ -1,9 +1,10 @@
import { ArrayField } from '@formily/core'; import { ArrayField } from '@formily/core';
import { useField } from '@formily/react'; import { Schema, useField, useFieldSchema } from '@formily/react';
import { Spin } from 'antd'; import { Spin } from 'antd';
import uniq from 'lodash/uniq';
import React, { createContext, useContext, useEffect } from 'react'; import React, { createContext, useContext, useEffect } from 'react';
import { useACLRoleContext } from '../acl'; import { useACLRoleContext } from '../acl';
import { useCollection } from '../collection-manager'; import { useCollection, useCollectionManager } from '../collection-manager';
import { toColumns } from '../schema-component/antd/kanban/Kanban'; import { toColumns } from '../schema-component/antd/kanban/Kanban';
import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { BlockProvider, useBlockRequestContext } from './BlockProvider';
@ -48,9 +49,61 @@ const InternalKanbanBlockProvider = (props) => {
); );
}; };
const recursiveProperties = (schema: Schema, component = 'CollectionField', associationFields, appends = []) => {
schema.mapProperties((s) => {
const name = s.name.toString();
if (s['x-component'] === component && !appends.includes(name)) {
// 关联字段和关联的关联字段
const [firstName] = name.split('.');
if (associationFields.has(name)) {
appends.push(name);
} else if (associationFields.has(firstName) && !appends.includes(firstName)) {
appends.push(firstName);
}
} else {
recursiveProperties(s, component, associationFields, appends);
}
})
}
export const useAssociationNames = (collection) => {
const { getCollectionFields } = useCollectionManager();
const collectionFields = getCollectionFields(collection);
const associationFields = new Set();
for (const collectionField of collectionFields) {
if (collectionField.target) {
associationFields.add(collectionField.name);
const fields = getCollectionFields(collectionField.target);
for (const field of fields) {
if (field.target) {
associationFields.add(`${collectionField.name}.${field.name}`);
}
}
}
}
const fieldSchema = useFieldSchema();
const kanbanSchema = fieldSchema.reduceProperties((buf, schema) => {
if (schema['x-component'] === 'KanbanV2') {
return schema;
}
return buf;
}, new Schema({}));
const gridSchema = kanbanSchema?.properties?.card?.properties?.grid;
const appends = [];
recursiveProperties(gridSchema, 'CollectionField', associationFields, appends);
console.log('useAssociationNames', appends);
return uniq(appends);
};
export const KanbanBlockProvider = (props) => { export const KanbanBlockProvider = (props) => {
const params = { ...props.params };
const appends = useAssociationNames(props.collection);
console.log('KanbanBlockProvider', appends);
if (!Object.keys(params).includes('appends')) {
params['appends'] = appends;
}
return ( return (
<BlockProvider {...props}> <BlockProvider {...props} params={params}>
<InternalKanbanBlockProvider {...props} /> <InternalKanbanBlockProvider {...props} />
</BlockProvider> </BlockProvider>
); );

View File

@ -1,15 +1,26 @@
import { ArrayField, Field } from '@formily/core'; import { ArrayField, Field } from '@formily/core';
import { useField } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import React, { createContext, useContext, useEffect } from 'react'; import React, { createContext, useContext, useEffect } from 'react';
import { APIClient } from '../api-client'; import { APIClient } from '../api-client';
import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { BlockProvider, useBlockRequestContext } from './BlockProvider';
import { useFormBlockContext } from './FormBlockProvider';
import { useFormFieldContext } from './FormFieldProvider';
export const TableFieldContext = createContext<any>({}); export const TableFieldContext = createContext<any>({});
const InternalTableFieldProvider = (props) => { const InternalTableFieldProvider = (props) => {
const { params = {}, showIndex, dragSort } = props; const { params = {}, showIndex, dragSort, fieldName } = props;
const field = useField(); const field = useField();
const { resource, service } = useBlockRequestContext(); const { resource, service } = useBlockRequestContext();
const formBlockCtx = useFormBlockContext();
const formFieldCtx = useFormFieldContext();
const fullFieldName = formFieldCtx && formFieldCtx.fieldName ? `${formFieldCtx.fieldName}.${fieldName}` : fieldName;
if (!formBlockCtx?.updateAssociationValues?.includes(fullFieldName)) {
formBlockCtx?.updateAssociationValues?.push(fullFieldName);
}
// if (service.loading) { // if (service.loading) {
// return <Spin />; // return <Spin />;
// } // }

View File

@ -1,8 +1,10 @@
import { ArrayField } from '@formily/core'; import { ArrayField } from '@formily/core';
import { useField } from '@formily/react'; import { Schema, useField, useFieldSchema } from '@formily/react';
import React, { createContext, useContext, useEffect } from 'react'; import React, { createContext, useContext, useEffect } from 'react';
import { useCollectionManager } from '../collection-manager'; import { useCollection, useCollectionManager } from '../collection-manager';
import { useRecord } from '../record-provider';
import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { BlockProvider, useBlockRequestContext } from './BlockProvider';
import { useFormBlockContext } from './FormBlockProvider';
export const TableSelectorContext = createContext<any>({}); export const TableSelectorContext = createContext<any>({});
@ -36,7 +38,23 @@ const useAssociationNames = (collection) => {
return names; return names;
}; };
const recursiveParent = (schema: Schema, component) => {
return schema['x-component'] === component
? schema
: (schema.parent ? recursiveParent(schema.parent, component) : null);
}
export const TableSelectorProvider = (props) => { export const TableSelectorProvider = (props) => {
const fieldSchema = useFieldSchema();
const ctx = useFormBlockContext()
const { getCollectionJoinField } = useCollectionManager();
const record = useRecord();
const collectionFieldSchema = recursiveParent(fieldSchema, 'CollectionField');
// const value = ctx.form.query(collectionFieldSchema?.name).value();
const collectionField = getCollectionJoinField(collectionFieldSchema?.['x-collection-field']);
console.log('TableSelectorProvider', collectionFieldSchema, collectionField, record);
const params = { ...props.params }; const params = { ...props.params };
const appends = useAssociationNames(props.collection); const appends = useAssociationNames(props.collection);
if (props.dragSort) { if (props.dragSort) {
@ -45,6 +63,23 @@ export const TableSelectorProvider = (props) => {
if (appends?.length) { if (appends?.length) {
params['appends'] = appends; params['appends'] = appends;
} }
if (collectionField) {
if (['oho', 'o2m'].includes(collectionField.interface)) {
params['filter'] = {
$or: [{
[collectionField.foreignKey]: {
$is: null,
}
}, {
[collectionField.foreignKey]: {
$eq: record?.[collectionField.sourceKey],
}
}]
}
}
if (['obo'].includes(collectionField.interface)) {
}
}
return ( return (
<BlockProvider {...props} params={params}> <BlockProvider {...props} params={params}>
<InternalTableSelectorProvider {...props} params={params} /> <InternalTableSelectorProvider {...props} params={params} />

View File

@ -1,5 +1,6 @@
import { Schema as SchemaCompiler } from '@formily/json-schema'; import { Schema as SchemaCompiler } from '@formily/json-schema';
import { useField, useFieldSchema, useForm } from '@formily/react'; import { useField, useFieldSchema, useForm } from '@formily/react';
import { useFormBlockContext } from '@nocobase/client';
import { message, Modal } from 'antd'; import { message, Modal } from 'antd';
import get from 'lodash/get'; import get from 'lodash/get';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -73,7 +74,7 @@ function getFormValues(filterByTk, field, form, fieldNames, getField, resource)
if (field.added && !field.added.has(key)) { if (field.added && !field.added.has(key)) {
continue; continue;
} }
if (['subTable', 'o2m'].includes(collectionField.interface)) { if (['subTable', 'o2m', 'o2o', 'oho', 'obo', 'm2o'].includes(collectionField.interface)) {
values[key] = form.values[key]; values[key] = form.values[key];
continue; continue;
} }
@ -304,9 +305,11 @@ export const useUpdateActionProps = () => {
const { setVisible } = useActionContext(); const { setVisible } = useActionContext();
const actionSchema = useFieldSchema(); const actionSchema = useFieldSchema();
const history = useHistory(); const history = useHistory();
const record = useRecord();
const { fields, getField } = useCollection(); const { fields, getField } = useCollection();
const compile = useCompile(); const compile = useCompile();
const actionField = useField(); const actionField = useField();
const { updateAssociationValues } = useFormBlockContext();
return { return {
async onClick() { async onClick() {
const { assignedValues, onSuccess, overwriteValues, skipValidator } = actionSchema?.['x-action-settings'] ?? {}; const { assignedValues, onSuccess, overwriteValues, skipValidator } = actionSchema?.['x-action-settings'] ?? {};
@ -315,7 +318,7 @@ export const useUpdateActionProps = () => {
await form.submit(); await form.submit();
} }
const fieldNames = fields.map((field) => field.name); const fieldNames = fields.map((field) => field.name);
const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource); let values = getFormValues(filterByTk, field, form, fieldNames, getField, resource);
actionField.data = field.data || {}; actionField.data = field.data || {};
actionField.data.loading = true; actionField.data.loading = true;
try { try {
@ -326,6 +329,7 @@ export const useUpdateActionProps = () => {
...overwriteValues, ...overwriteValues,
...assignedValues, ...assignedValues,
}, },
updateAssociationValues
}); });
actionField.data.loading = false; actionField.data.loading = false;
if (!(resource instanceof TableFieldResource)) { if (!(resource instanceof TableFieldResource)) {

View File

@ -7,4 +7,5 @@ export * from './KanbanBlockProvider';
export * from './TableBlockProvider'; export * from './TableBlockProvider';
export * from './TableFieldProvider'; export * from './TableFieldProvider';
export * from './TableSelectorProvider'; export * from './TableSelectorProvider';
export * from './FormFieldProvider';

View File

@ -9,7 +9,6 @@ import { useCollectionField } from './hooks';
// TODO: 初步适配 // TODO: 初步适配
const InternalField: React.FC = (props) => { const InternalField: React.FC = (props) => {
const field = useField<Field>(); const field = useField<Field>();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { name, interface: interfaceType, uiSchema } = useCollectionField(); const { name, interface: interfaceType, uiSchema } = useCollectionField();
const component = useComponent(uiSchema?.['x-component']); const component = useComponent(uiSchema?.['x-component']);
@ -68,13 +67,15 @@ const InternalField: React.FC = (props) => {
if (!uiSchema) { if (!uiSchema) {
return null; return null;
} }
return React.createElement(component, props, props.children); return React.createElement(component, props, props.children);
}; };
export const CollectionField = connect((props) => { export const CollectionField = connect((props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const field = fieldSchema?.['x-component-props']?.['field'];
return ( return (
<CollectionFieldProvider name={fieldSchema.name}> <CollectionFieldProvider name={fieldSchema.name} field={field}>
<InternalField {...props} /> <InternalField {...props} />
</CollectionFieldProvider> </CollectionFieldProvider>
); );

View File

@ -1,13 +1,15 @@
import { SchemaKey } from '@formily/react'; import { SchemaKey, useFieldSchema } from '@formily/react';
import React from 'react'; import React from 'react';
import { CollectionFieldContext } from './context'; import { CollectionFieldContext } from './context';
import { useCollection } from './hooks'; import { useCollection, useCollectionManager } from './hooks';
import { CollectionFieldOptions } from './types'; import { CollectionFieldOptions } from './types';
export const CollectionFieldProvider: React.FC<{ name?: SchemaKey; field?: CollectionFieldOptions }> = (props) => { export const CollectionFieldProvider: React.FC<{ name?: SchemaKey; field?: CollectionFieldOptions }> = (props) => {
const { name, field, children } = props; const { name, field, children } = props;
const fieldSchema = useFieldSchema();
const { getField } = useCollection(); const { getField } = useCollection();
const value = field || getField(field?.name || name); const { getCollectionJoinField } = useCollectionManager();
const value = field || getField(field?.name || name) || getCollectionJoinField(fieldSchema?.['x-collection-field']);
if (!value) { if (!value) {
return null; return null;
} }

View File

@ -44,7 +44,7 @@ const getSchema = (schema: IField, record: any, compile): ISchema => {
); );
}, },
}, },
title: `${record.title} - ${compile('{{ t("Add field") }}')}`, title: `${compile(record.title)} - ${compile('{{ t("Add field") }}')}`,
properties: { properties: {
summary: { summary: {
type: 'void', type: 'void',

View File

@ -37,7 +37,7 @@ const getSchema = (schema: IField, record: any, compile): ISchema => {
); );
}, },
}, },
title: `${record.__parent?.title} - ${compile('{{ t("Edit field") }}')}`, title: `${compile(record.__parent?.title)} - ${compile('{{ t("Edit field") }}')}`,
properties: { properties: {
summary: { summary: {
type: 'void', type: 'void',

View File

@ -45,15 +45,22 @@ export const linkTo: IField = {
}, },
}, },
}, },
schemaInitialize(schema: ISchema, { readPretty }) { schemaInitialize(schema: ISchema, { readPretty, block }) {
if (readPretty) { if (block === 'Form') {
schema['properties'] = { schema['properties'] = {
viewer: cloneDeep(recordPickerViewer), viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector), selector: cloneDeep(recordPickerSelector),
}; };
} else {
if (readPretty) {
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector),
}
}
} }
}, },
initialize: (values: any) => { initialize: (values: any) => {

View File

@ -46,14 +46,21 @@ export const m2m: IField = {
}, },
}, },
schemaInitialize(schema: ISchema, { readPretty, block }) { schemaInitialize(schema: ISchema, { readPretty, block }) {
if (readPretty) { if (block === 'Form') {
schema['properties'] = { schema['properties'] = {
viewer: cloneDeep(recordPickerViewer), viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector), selector: cloneDeep(recordPickerSelector),
}; };
} else {
if (readPretty) {
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector),
}
}
} }
if (['Table', 'Kanban'].includes(block)) { if (['Table', 'Kanban'].includes(block)) {
schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props'] = schema['x-component-props'] || {};

View File

@ -44,15 +44,63 @@ export const m2o: IField = {
}, },
}, },
}, },
schemaInitialize(schema: ISchema, { readPretty, block }) { schemaInitialize(schema: ISchema, { field, block, readPretty, action }) {
if (readPretty) { if (block === 'Form' && schema['x-component'] === 'FormField') {
schema['properties'] = { const association = `${field.collectionName}.${field.name}`;
viewer: cloneDeep(recordPickerViewer), schema.type = 'void';
}; schema.properties = {
block: {
type: 'void',
'x-decorator': 'FormFieldProvider',
'x-decorator-props': {
collection: field.target,
association: association,
resource: association,
action,
fieldName: field.name,
readPretty
},
'x-component': 'CardItem',
'x-component-props': {
bordered: true,
},
properties: {
[field.name]: {
type: 'object',
'x-component': 'FormV2',
'x-component-props': {
useProps: '{{ useFormFieldProps }}',
},
properties: {
__form_grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'FormItemInitializers',
properties: {},
},
}
},
},
},
}
} else { } else {
schema['properties'] = { schema.type = 'string';
selector: cloneDeep(recordPickerSelector), if (block === 'Form') {
}; schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
selector: cloneDeep(recordPickerSelector),
};
} else {
if (readPretty) {
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector),
}
}
}
} }
if (['Table', 'Kanban'].includes(block)) { if (['Table', 'Kanban'].includes(block)) {
schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props'] = schema['x-component-props'] || {};

View File

@ -1,5 +1,6 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { relationshipType } from './properties'; import { cloneDeep } from 'lodash';
import { recordPickerSelector, recordPickerViewer, relationshipType } from './properties';
import { IField } from './types'; import { IField } from './types';
export const o2m: IField = { export const o2m: IField = {
@ -15,14 +16,14 @@ export const o2m: IField = {
// name, // name,
uiSchema: { uiSchema: {
// title, // title,
'x-component': 'TableField', 'x-component': 'RecordPicker',
'x-component-props': { 'x-component-props': {
// mode: 'tags', // mode: 'tags',
// multiple: true, multiple: true,
// fieldNames: { fieldNames: {
// label: 'id', label: 'id',
// value: 'id', value: 'id',
// }, },
}, },
}, },
reverseField: { reverseField: {
@ -43,47 +44,68 @@ export const o2m: IField = {
}, },
}, },
}, },
schemaInitialize(schema: ISchema, { field, readPretty, block }) { schemaInitialize(schema: ISchema, { field, block, readPretty }) {
const association = `${field.collectionName}.${field.name}`; if (block === 'Form' && schema['x-component'] === 'TableField') {
schema['type'] = 'void'; const association = `${field.collectionName}.${field.name}`;
schema['x-component'] = 'TableField'; schema['type'] = 'void';
schema['properties'] = { schema['properties'] = {
block: { block: {
type: 'void', type: 'void',
'x-decorator': 'TableFieldProvider', 'x-decorator': 'TableFieldProvider',
'x-decorator-props': { 'x-decorator-props': {
collection: field.target, collection: field.target,
association: association, association: association,
resource: association, resource: association,
action: 'list', action: 'list',
params: { params: {
paginate: false, paginate: false,
},
showIndex: true,
dragSort: false,
fieldName: field.name,
}, },
showIndex: true, properties: {
dragSort: false, actions: {
}, type: 'void',
properties: { 'x-initializer': 'SubTableActionInitializers',
actions: { 'x-component': 'TableField.ActionBar',
type: 'void', 'x-component-props': {},
'x-initializer': 'SubTableActionInitializers', },
'x-component': 'TableField.ActionBar', [field.name]: {
'x-component-props': {}, type: 'array',
}, 'x-initializer': 'TableColumnInitializers',
[field.name]: { 'x-component': 'TableV2',
type: 'array', 'x-component-props': {
'x-initializer': 'TableColumnInitializers', rowSelection: {
'x-component': 'TableV2', type: 'checkbox',
'x-component-props': { },
rowSelection: { useProps: '{{ useTableFieldProps }}',
type: 'checkbox',
}, },
useProps: '{{ useTableFieldProps }}',
}, },
}, },
}, },
}, };
}; } else {
schema['x-component'] = 'CollectionField';
schema.type = 'object';
if (block === 'Form') {
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
selector: cloneDeep(recordPickerSelector),
};
} else {
if (readPretty) {
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector),
}
}
}
}
// if (readPretty) { // if (readPretty) {
// schema['properties'] = { // schema['properties'] = {
// viewer: cloneDeep(recordPickerViewer), // viewer: cloneDeep(recordPickerViewer),

View File

@ -3,6 +3,66 @@ import { cloneDeep } from 'lodash';
import { recordPickerSelector, recordPickerViewer, relationshipType } from './properties'; import { recordPickerSelector, recordPickerViewer, relationshipType } from './properties';
import { IField } from './types'; import { IField } from './types';
const internalSchameInitialize = (schema: ISchema, { field, block, readPretty, action }) => {
if (block === 'Form' && schema['x-component'] === 'FormField') {
const association = `${field.collectionName}.${field.name}`;
schema.type = 'void';
schema.properties = {
block: {
type: 'void',
'x-decorator': 'FormFieldProvider',
'x-decorator-props': {
collection: field.target,
association: association,
resource: association,
action: action,
fieldName: field.name,
readPretty,
},
'x-component': 'CardItem',
'x-component-props': {
bordered: true,
},
properties: {
[field.name]: {
type: 'object',
'x-component': 'FormV2',
'x-component-props': {
useProps: '{{ useFormFieldProps }}',
},
properties: {
__form_grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'FormItemInitializers',
properties: {},
},
}
},
},
},
}
} else {
schema.type = 'string';
if (block === 'Form') {
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
selector: cloneDeep(recordPickerSelector),
};
} else {
if (readPretty) {
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector),
}
}
}
}
}
export const o2o: IField = { export const o2o: IField = {
name: 'o2o', name: 'o2o',
type: 'object', type: 'object',
@ -45,16 +105,8 @@ export const o2o: IField = {
}, },
}, },
}, },
schemaInitialize(schema: ISchema, { readPretty, block }) { schemaInitialize(schema: ISchema, { field, block, readPretty, action }) {
if (readPretty) { internalSchameInitialize(schema, { field, block, readPretty, action });
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector),
};
}
if (['Table', 'Kanban'].includes(block)) { if (['Table', 'Kanban'].includes(block)) {
schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props'] = schema['x-component-props'] || {};
schema['x-component-props']['ellipsis'] = true; schema['x-component-props']['ellipsis'] = true;
@ -217,16 +269,8 @@ export const oho: IField = {
}, },
}, },
}, },
schemaInitialize(schema: ISchema, { readPretty, block }) { schemaInitialize(schema: ISchema, { field, block, readPretty, action }) {
if (readPretty) { internalSchameInitialize(schema, { field, block, readPretty, action });
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector),
};
}
if (['Table', 'Kanban'].includes(block)) { if (['Table', 'Kanban'].includes(block)) {
schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props'] = schema['x-component-props'] || {};
schema['x-component-props']['ellipsis'] = true; schema['x-component-props']['ellipsis'] = true;
@ -388,16 +432,8 @@ export const obo: IField = {
}, },
}, },
}, },
schemaInitialize(schema: ISchema, { readPretty, block }) { schemaInitialize(schema: ISchema, { field, block, readPretty, action }) {
if (readPretty) { internalSchameInitialize(schema, { field, block, readPretty, action });
schema['properties'] = {
viewer: cloneDeep(recordPickerViewer),
};
} else {
schema['properties'] = {
selector: cloneDeep(recordPickerSelector),
};
}
if (['Table', 'Kanban'].includes(block)) { if (['Table', 'Kanban'].includes(block)) {
schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props'] = schema['x-component-props'] || {};
schema['x-component-props']['ellipsis'] = true; schema['x-component-props']['ellipsis'] = true;

View File

@ -79,7 +79,7 @@ export default {
"Add new": "Add new", "Add new": "Add new",
"Add record": "Add record", "Add record": "Add record",
"Custom field display name": "Custom field display name", "Custom field display name": "Custom field display name",
"Display fields": "Display fields", "Display fields": "Display collection fields",
"Edit record": "Edit record", "Edit record": "Edit record",
"Delete menu item": "Delete menu item", "Delete menu item": "Delete menu item",
"Add page": "Add page", "Add page": "Add page",
@ -541,5 +541,10 @@ export default {
"One to one (has one)": "One to one (has one)", "One to one (has one)": "One to one (has one)",
"One to one (belongs to)": "One to one (belongs to)", "One to one (belongs to)": "One to one (belongs to)",
"Use the same time zone (GMT) for all users": "Use the same time zone (GMT) for all users", "Use the same time zone (GMT) for all users": "Use the same time zone (GMT) for all users",
"Block title": "Block title",
"Edit block title": "Edit block title",
"Province/city/area name": "Province/city/area name", "Province/city/area name": "Province/city/area name",
"Field component": "Field component",
"Subtable": "Subtable",
"Subform": "Subform",
} }

View File

@ -176,7 +176,11 @@ export default {
"Many to one description": "用于创建多对一关系,比如一个城市只能属于一个国家,一个国家可以有多个城市。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。创建后,会在目标数据表里自动生成一个多对一字段。", "Many to one description": "用于创建多对一关系,比如一个城市只能属于一个国家,一个国家可以有多个城市。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。创建后,会在目标数据表里自动生成一个多对一字段。",
"Many to many description": "用于创建多对多关系,比如一个学生会有多个老师,一个老师也会有多个学生。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。", "Many to many description": "用于创建多对多关系,比如一个学生会有多个老师,一个老师也会有多个学生。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。",
"Generated automatically if left blank": "留空时,自动生成中间表", "Generated automatically if left blank": "留空时,自动生成中间表",
"References fields of associated table": "引用关联表的字段", "Display association fields": "显示关联表的字段",
"Field component": "字段组件",
"Subtable": "子表格",
"Subform": "子表单",
"Record picker": "数据选择器",
"Toggles the subfield mode": "切换子字段模式", "Toggles the subfield mode": "切换子字段模式",
"Selector mode": "选择器模式", "Selector mode": "选择器模式",
"Subtable mode": "子表格模式", "Subtable mode": "子表格模式",

View File

@ -2,15 +2,26 @@ import { css } from '@emotion/css';
import { FormItem as Item } from '@formily/antd'; import { FormItem as Item } from '@formily/antd';
import { Field } from '@formily/core'; import { Field } from '@formily/core';
import { ISchema, useField, useFieldSchema } from '@formily/react'; import { ISchema, useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCompile, useDesignable } from '../..'; import { useCompile, useDesignable } from '../..';
import { useFormBlockContext } from '../../../block-provider'; import { useFilterByTk, useFormBlockContext } from '../../../block-provider';
import { useCollection, useCollectionManager } from '../../../collection-manager'; import { useCollection, useCollectionManager } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { BlockItem } from '../block-item'; import { BlockItem } from '../block-item';
import { HTMLEncode } from '../input/shared'; import { HTMLEncode } from '../input/shared';
const divWrap = (schema: ISchema) => {
return {
type: 'void',
'x-component': 'div',
properties: {
[schema.name || uid()]: schema,
},
};
};
export const FormItem: any = (props) => { export const FormItem: any = (props) => {
const field = useField(); const field = useField();
return ( return (
@ -36,18 +47,21 @@ export const FormItem: any = (props) => {
); );
}; };
FormItem.Designer = () => { FormItem.Designer = (props) => {
const { getCollectionFields } = useCollectionManager(); const { getCollectionFields, getCollection, getInterface, getCollectionJoinField } = useCollectionManager();
const { getField } = useCollection(); const { getField } = useCollection();
const tk = useFilterByTk();
const { form } = useFormBlockContext(); const { form } = useFormBlockContext();
const field = useField<Field>(); const field = useField<Field>();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { t } = useTranslation(); const { t } = useTranslation();
const { dn, refresh } = useDesignable(); const { dn, refresh, insertAdjacent, insertBeforeBegin } = useDesignable();
const compile = useCompile(); const compile = useCompile();
const collectionField = getField(fieldSchema['name']); const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
const interfaceConfig = getInterface(collectionField?.interface);
const originalTitle = collectionField?.uiSchema?.title; const originalTitle = collectionField?.uiSchema?.title;
const targetFields = collectionField?.target ? getCollectionFields(collectionField.target) : []; const targetFields = collectionField?.target ? getCollectionFields(collectionField.target) : [];
const isSubFormAssocitionField = field.address.segments.includes('__form_grid');
const initialValue = { const initialValue = {
title: field.title === originalTitle ? undefined : field.title, title: field.title === originalTitle ? undefined : field.title,
}; };
@ -168,7 +182,7 @@ FormItem.Designer = () => {
}} }}
/> />
)} )}
{!field.readPretty && ( {!field.readPretty && fieldSchema['x-component'] !== 'FormField' && (
<SchemaSettings.SwitchItem <SchemaSettings.SwitchItem
key="required" key="required"
title={t('Required')} title={t('Required')}
@ -187,6 +201,53 @@ FormItem.Designer = () => {
}} }}
/> />
)} )}
{form && !isSubFormAssocitionField && ['o2o', 'oho', 'obo', 'o2m'].includes(collectionField?.interface) && (
<SchemaSettings.SelectItem
title={t('Field component')}
options={
collectionField?.interface === 'o2m'
? [
{ label: t('Record picker'), value: 'CollectionField' },
{ label: t('Subtable'), value: 'TableField' },
]
: [
{ label: t('Record picker'), value: 'CollectionField' },
{ label: t('Subform'), value: 'FormField' },
]
}
value={fieldSchema['x-component']}
onChange={(v) => {
const schema: ISchema = {
name: collectionField.name,
type: 'void',
// title: compile(collectionField.uiSchema?.title),
'x-decorator': 'FormItem',
'x-designer': 'FormItem.Designer',
'x-component': v,
'x-component-props': {},
'x-collection-field': fieldSchema['x-collection-field'],
};
interfaceConfig?.schemaInitialize?.(schema, {
field: collectionField,
block: 'Form',
readPretty: field.readPretty,
action: tk ? 'get' : null,
});
insertAdjacent('beforeBegin', divWrap(schema), {
onSuccess: () => {
dn.remove(null, {
removeParentsIfNoChildren: true,
breakRemoveOn: {
'x-component': 'Grid',
},
})
}
});
}}
/>
)}
{form && !form?.readPretty && collectionField?.interface !== 'o2m' && ( {form && !form?.readPretty && collectionField?.interface !== 'o2m' && (
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
key="pattern" key="pattern"
@ -198,7 +259,6 @@ FormItem.Designer = () => {
]} ]}
value={readOnlyMode} value={readOnlyMode}
onChange={(v) => { onChange={(v) => {
console.log('v', v);
const schema: ISchema = { const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'], ['x-uid']: fieldSchema['x-uid'],
}; };
@ -219,7 +279,6 @@ FormItem.Designer = () => {
schema['x-read-pretty'] = true; schema['x-read-pretty'] = true;
schema['x-disabled'] = false; schema['x-disabled'] = false;
field.readPretty = true; field.readPretty = true;
// field.disabled = true;
break; break;
} }
default: { default: {
@ -232,6 +291,7 @@ FormItem.Designer = () => {
break; break;
} }
} }
dn.emit('patch', { dn.emit('patch', {
schema, schema,
}); });
@ -240,7 +300,7 @@ FormItem.Designer = () => {
}} }}
/> />
)} )}
{collectionField?.target && ( {collectionField?.target && fieldSchema['x-component'] === 'CollectionField' && (
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
key="title-field" key="title-field"
title={t('Title field')} title={t('Title field')}
@ -254,12 +314,15 @@ FormItem.Designer = () => {
...field.componentProps.fieldNames, ...field.componentProps.fieldNames,
label, label,
}; };
// if (fieldSchema['x-component-props']?.['field']?.['uiSchema']?.['x-component-props']) {
// fieldSchema['x-component-props']['field']['uiSchema']['x-component-props']['fieldNames'] = fieldNames;
// } else {
// }
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props']['fieldNames'] = fieldNames; fieldSchema['x-component-props']['fieldNames'] = fieldNames;
field.componentProps.fieldNames = fieldNames; schema['x-component-props'] = fieldSchema['x-component-props'];
schema['x-component-props'] = {
fieldNames,
};
dn.emit('patch', { dn.emit('patch', {
schema, schema,
}); });

View File

@ -0,0 +1,24 @@
import { observer, useField, useFieldSchema, useForm } from '@formily/react';
import React, { useEffect } from 'react';
import { useFormBlockContext } from '../../../block-provider';
import { useCollection } from '../../../collection-manager';
import { useCompile } from '../../hooks';
export const FormField: any = observer((props) => {
const fieldSchema = useFieldSchema();
const { getField } = useCollection();
const field = useField();
const collectionField = getField(fieldSchema.name);
const compile = useCompile();
const ctx = useFormBlockContext();
useEffect(() => {
if (!field.title) {
field.title = compile(collectionField?.uiSchema?.title);
}
if (ctx?.field) {
ctx.field.added = ctx.field.added || new Set();
ctx.field.added.add(fieldSchema.name);
}
}, []);
return <div>{props.children}</div>;
});

View File

@ -5,3 +5,4 @@ FormV2.Designer = FormDesigner;
FormV2.ReadPrettyDesigner = ReadPrettyFormDesigner; FormV2.ReadPrettyDesigner = ReadPrettyFormDesigner;
export { FormV2, DetailsDesigner }; export { FormV2, DetailsDesigner };
export * from './FormField';

View File

@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
import { useAPIClient } from '../../../api-client'; import { useAPIClient } from '../../../api-client';
import { createDesignable, useDesignable } from '../../../schema-component'; import { createDesignable, useDesignable } from '../../../schema-component';
import { SchemaInitializer } from '../../../schema-initializer'; import { SchemaInitializer } from '../../../schema-initializer';
import { useFormItemInitializerFields } from '../../../schema-initializer/utils'; import { useAssociatedFormItemInitializerFields, useFormItemInitializerFields } from '../../../schema-initializer/utils';
const titleCss = css` const titleCss = css`
pointer-events: none; pointer-events: none;
@ -93,6 +93,14 @@ export const KanbanCardDesigner = (props: any) => {
title: t('Display fields'), title: t('Display fields'),
children: fields, children: fields,
}, },
{
type: 'divider',
},
{
type: 'itemGroup',
title: t('Display association fields'),
children: useAssociatedFormItemInitializerFields({readPretty: true, block: 'KanbanV2'}),
},
]} ]}
component={<MenuOutlined style={{ cursor: 'pointer', fontSize: 12 }} />} component={<MenuOutlined style={{ cursor: 'pointer', fontSize: 12 }} />}
/> />
@ -101,3 +109,4 @@ export const KanbanCardDesigner = (props: any) => {
</div> </div>
); );
}; };

View File

@ -1,6 +1,6 @@
import { RecursionField, useFieldSchema } from '@formily/react'; import { RecursionField, useFieldSchema } from '@formily/react';
import { Select } from 'antd'; import { Select } from 'antd';
import React, { createContext, useContext, useState } from 'react'; import React, { createContext, useContext, useEffect, useState } from 'react';
import { useTableSelectorProps as useTsp } from '../../../block-provider/TableSelectorProvider'; import { useTableSelectorProps as useTsp } from '../../../block-provider/TableSelectorProvider';
import { CollectionProvider, useCollection } from '../../../collection-manager'; import { CollectionProvider, useCollection } from '../../../collection-manager';
import { FormProvider, SchemaComponentOptions } from '../../core'; import { FormProvider, SchemaComponentOptions } from '../../core';
@ -60,15 +60,31 @@ export const InputRecordPicker: React.FC<any> = (props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const collectionField = useAssociation(props); const collectionField = useAssociation(props);
const compile = useCompile(); const compile = useCompile();
const options = (Array.isArray(value) ? value : value ? [value] : []).map(option => {
const label = option[fieldNames.label]; const [selectedRows, setSelectedRows] = useState([]);
return { const [options, setOptions] = useState([]);
...option,
[fieldNames.label]: compile(label), useEffect(() => {
}; if (value) {
}); const opts = (Array.isArray(value) ? value : value ? [value] : []).map(option => {
const [selectedRows, setSelectedRows] = useState(options); const label = option[fieldNames.label];
const values = options?.map((option) => option[fieldNames.value]); return {
...option,
[fieldNames.label]: compile(label),
};
});
setOptions(opts);
setSelectedRows(opts);
}
}, [value])
useEffect(() => {})
const getValue = () => {
if (multiple == null) return null;
return multiple ? (Array.isArray(value) ? value?.map(v => v[fieldNames.value]) : []) : value?.[fieldNames.value];
}
return ( return (
<div> <div>
<Select <Select
@ -93,7 +109,7 @@ export const InputRecordPicker: React.FC<any> = (props) => {
} }
}} }}
options={options} options={options}
value={multiple ? values : values?.[0]} value={getValue()}
open={false} open={false}
/> />
<RecordPickerContext.Provider value={{ multiple, onChange, selectedRows, setSelectedRows }}> <RecordPickerContext.Provider value={{ multiple, onChange, selectedRows, setSelectedRows }}>
@ -101,7 +117,13 @@ export const InputRecordPicker: React.FC<any> = (props) => {
<ActionContext.Provider value={{ openMode: 'drawer', visible, setVisible }}> <ActionContext.Provider value={{ openMode: 'drawer', visible, setVisible }}>
<FormProvider> <FormProvider>
<SchemaComponentOptions scope={{ useTableSelectorProps, usePickActionProps }}> <SchemaComponentOptions scope={{ useTableSelectorProps, usePickActionProps }}>
<RecursionField schema={fieldSchema} onlyRenderProperties /> <RecursionField
schema={fieldSchema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] === 'RecordPicker.Selector';
}}
/>
</SchemaComponentOptions> </SchemaComponentOptions>
</FormProvider> </FormProvider>
</ActionContext.Provider> </ActionContext.Provider>

View File

@ -3,8 +3,8 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea
import { toArr } from '@formily/shared'; import { toArr } from '@formily/shared';
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider'; import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
import { CollectionProvider, useCollection } from '../../../collection-manager'; import { CollectionProvider, useCollection, useCollectionManager } from '../../../collection-manager';
import { RecordProvider } from '../../../record-provider'; import { RecordProvider, useRecord } from '../../../record-provider';
import { FormProvider } from '../../core'; import { FormProvider } from '../../core';
import { useCompile } from '../../hooks'; import { useCompile } from '../../hooks';
import { ActionContext } from '../action'; import { ActionContext } from '../action';
@ -17,14 +17,17 @@ interface IEllipsisWithTooltipRef {
export const ReadPrettyRecordPicker: React.FC = observer((props: any) => { export const ReadPrettyRecordPicker: React.FC = observer((props: any) => {
const { ellipsis } = props; const { ellipsis } = props;
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const recordCtx = useRecord();
const { getCollectionJoinField } = useCollectionManager();
const field = useField<Field>(); const field = useField<Field>();
const fieldNames = useFieldNames(props); const fieldNames = useFieldNames(props);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [popoverVisible, setPopoverVisible] = useState<boolean>(); const [popoverVisible, setPopoverVisible] = useState<boolean>();
const { getField } = useCollection(); const { getField } = useCollection();
const collectionField = getField(fieldSchema.name); const collectionField = getField(fieldSchema.name) || getCollectionJoinField(fieldSchema?.['x-collection-field']);
const [record, setRecord] = useState({}); const [record, setRecord] = useState({});
const compile = useCompile(); const compile = useCompile();
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>(); const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
const renderRecords = () => const renderRecords = () =>
toArr(field.value).map((record, index, arr) => { toArr(field.value).map((record, index, arr) => {
@ -47,6 +50,37 @@ export const ReadPrettyRecordPicker: React.FC = observer((props: any) => {
</> </>
); );
}); });
const renderWithoutTableFieldResourceProvider = () => (
<WithoutTableFieldResource.Provider value={true}>
<FormProvider>
<RecursionField
schema={fieldSchema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] === 'RecordPicker.Viewer';
}}
/>
</FormProvider>
</WithoutTableFieldResource.Provider>
);
const renderRecordProvider = () => {
const collectionFieldNames = fieldSchema?.['x-collection-field']?.split('.');
return collectionFieldNames && collectionFieldNames.length > 2 ? (
<RecordProvider record={recordCtx[collectionFieldNames[1]]}>
<RecordProvider record={record}>
{renderWithoutTableFieldResourceProvider()}
</RecordProvider>
</RecordProvider>
) : (
<RecordProvider record={record}>
{renderWithoutTableFieldResourceProvider()}
</RecordProvider>
);
}
return collectionField ? ( return collectionField ? (
<div> <div>
<BlockAssociationContext.Provider value={`${collectionField.collectionName}.${collectionField.name}`}> <BlockAssociationContext.Provider value={`${collectionField.collectionName}.${collectionField.name}`}>
@ -55,13 +89,7 @@ export const ReadPrettyRecordPicker: React.FC = observer((props: any) => {
{renderRecords()} {renderRecords()}
</EllipsisWithTooltip> </EllipsisWithTooltip>
<ActionContext.Provider value={{ visible, setVisible, openMode: 'drawer' }}> <ActionContext.Provider value={{ visible, setVisible, openMode: 'drawer' }}>
<RecordProvider record={record}> {renderRecordProvider()}
<WithoutTableFieldResource.Provider value={true}>
<FormProvider>
<RecursionField schema={fieldSchema} onlyRenderProperties />
</FormProvider>
</WithoutTableFieldResource.Provider>
</RecordProvider>
</ActionContext.Provider> </ActionContext.Provider>
</CollectionProvider> </CollectionProvider>
</BlockAssociationContext.Provider> </BlockAssociationContext.Provider>

View File

@ -2,6 +2,8 @@ import { useFieldSchema } from "@formily/react";
export const useFieldNames = (props) => { export const useFieldNames = (props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const fieldNames = fieldSchema?.['x-component-props']?.['fieldNames'] || props.fieldNames; const fieldNames = fieldSchema['x-component-props']?.['field']?.['uiSchema']?.['x-component-props']?.['fieldNames']
|| fieldSchema?.['x-component-props']?.['fieldNames']
|| props.fieldNames;
return { label: 'label', value: 'value', ...fieldNames }; return { label: 'label', value: 'value', ...fieldNames };
}; };

View File

@ -1,12 +1,13 @@
import { useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import React, { useLayoutEffect } from 'react'; import React, { useLayoutEffect } from 'react';
import { SortableItem, useCollection, useCompile, useDesignable, useDesigner } from '../../../'; import { SortableItem, useCollection, useCollectionManager, useCompile, useDesignable, useDesigner } from '../../../';
import { designerCss } from './Table.Column.ActionBar'; import { designerCss } from './Table.Column.ActionBar';
export const useColumnSchema = () => { export const useColumnSchema = () => {
const { getField } = useCollection(); const { getField } = useCollection();
const compile = useCompile(); const compile = useCompile();
const columnSchema = useFieldSchema(); const columnSchema = useFieldSchema();
const { getCollectionJoinField } = useCollectionManager();
const fieldSchema = columnSchema.reduceProperties((buf, s) => { const fieldSchema = columnSchema.reduceProperties((buf, s) => {
if (s['x-component'] === 'CollectionField') { if (s['x-component'] === 'CollectionField') {
return s; return s;
@ -16,7 +17,8 @@ export const useColumnSchema = () => {
if (!fieldSchema) { if (!fieldSchema) {
return {}; return {};
} }
const collectionField = getField(fieldSchema.name);
const collectionField = getField(fieldSchema.name) || getCollectionJoinField(fieldSchema?.['x-collection-field']);
return { columnSchema, fieldSchema, collectionField, uiSchema: compile(collectionField?.uiSchema) }; return { columnSchema, fieldSchema, collectionField, uiSchema: compile(collectionField?.uiSchema) };
}; };
@ -41,7 +43,7 @@ export const TableColumnDecorator = (props) => {
<SortableItem className={designerCss}> <SortableItem className={designerCss}>
<Designer fieldSchema={fieldSchema} uiSchema={uiSchema} collectionField={collectionField}/> <Designer fieldSchema={fieldSchema} uiSchema={uiSchema} collectionField={collectionField}/>
{/* <RecursionField name={columnSchema.name} schema={columnSchema}/> */} {/* <RecursionField name={columnSchema.name} schema={columnSchema}/> */}
{field.title || compile(uiSchema?.title)} {field?.title || compile(uiSchema?.title)}
{/* <div {/* <div
onClick={() => { onClick={() => {
field.title = uid(); field.title = uid();

View File

@ -24,6 +24,7 @@ const useLabelFields = (collectionName?: any) => {
export const TableColumnDesigner = (props) => { export const TableColumnDesigner = (props) => {
const { uiSchema, fieldSchema, collectionField } = props; const { uiSchema, fieldSchema, collectionField } = props;
const { getInterface } = useCollectionManager();
const field = useField(); const field = useField();
const { t } = useTranslation(); const { t } = useTranslation();
const columnSchema = useFieldSchema(); const columnSchema = useFieldSchema();
@ -31,6 +32,8 @@ export const TableColumnDesigner = (props) => {
const fieldNames = const fieldNames =
fieldSchema?.['x-component-props']?.['fieldNames'] || uiSchema?.['x-component-props']?.['fieldNames']; fieldSchema?.['x-component-props']?.['fieldNames'] || uiSchema?.['x-component-props']?.['fieldNames'];
const options = useLabelFields(collectionField?.target); const options = useLabelFields(collectionField?.target);
const intefaceCfg = getInterface(collectionField?.interface);
return ( return (
<GeneralSchemaDesigner disableInitializer> <GeneralSchemaDesigner disableInitializer>
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
@ -43,7 +46,7 @@ export const TableColumnDesigner = (props) => {
title: { title: {
// title: t('Column title'), // title: t('Column title'),
default: columnSchema?.title, default: columnSchema?.title,
description: `${t('Original title: ')}${collectionField?.uiSchema?.title}`, description: `${t('Original title: ')}${collectionField?.uiSchema?.title || fieldSchema?.title}`,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Input', 'x-component': 'Input',
'x-component-props': {}, 'x-component-props': {},
@ -65,25 +68,29 @@ export const TableColumnDesigner = (props) => {
dn.refresh(); dn.refresh();
}} }}
/> />
<SchemaSettings.SwitchItem {
title={t('Sortable')} intefaceCfg && intefaceCfg.sortable === true && (
checked={field.componentProps.sorter} <SchemaSettings.SwitchItem
onChange={(v) => { title={t('Sortable')}
const schema: ISchema = { checked={field.componentProps.sorter}
['x-uid']: columnSchema['x-uid'], onChange={(v) => {
}; const schema: ISchema = {
columnSchema['x-component-props'] = { ['x-uid']: columnSchema['x-uid'],
...columnSchema['x-component-props'], };
sorter: v, columnSchema['x-component-props'] = {
}; ...columnSchema['x-component-props'],
schema['x-component-props'] = columnSchema['x-component-props']; sorter: v
field.componentProps.sorter = v; }
dn.emit('patch', { schema['x-component-props'] = columnSchema['x-component-props'];
schema, field.componentProps.sorter = v;
}); dn.emit('patch', {
dn.refresh(); schema
}} });
/> dn.refresh();
}}
/>
)
}
{['linkTo', 'm2m', 'm2o', 'o2m', 'obo', 'oho'].includes(collectionField?.interface) && ( {['linkTo', 'm2m', 'm2o', 'o2m', 'obo', 'oho'].includes(collectionField?.interface) && (
<SchemaSettings.SelectItem <SchemaSettings.SelectItem
title={t('Title field')} title={t('Title field')}

View File

@ -38,6 +38,7 @@ const useTableColumns = () => {
} }
}, []); }, []);
const dataIndex = collectionFields?.length > 0 ? collectionFields[0].name : s.name; const dataIndex = collectionFields?.length > 0 ? collectionFields[0].name : s.name;
console.log('useTableColumns', s.name, s, field.value);
return { return {
title: <RecursionField name={s.name} schema={s} onlyRenderSelf />, title: <RecursionField name={s.name} schema={s} onlyRenderSelf />,
dataIndex, dataIndex,
@ -46,7 +47,8 @@ const useTableColumns = () => {
// width: 300, // width: 300,
render: (v, record) => { render: (v, record) => {
const index = field.value?.indexOf(record); const index = field.value?.indexOf(record);
console.log((Date.now() - start) / 1000); // console.log((Date.now() - start) / 1000);
console.log('useTableColumns.index', index, record);
return ( return (
<RecordIndexProvider index={index}> <RecordIndexProvider index={index}>
<RecordProvider record={record}> <RecordProvider record={record}>

View File

@ -1,5 +1,6 @@
import { observer, useField, useFieldSchema, useForm } from '@formily/react'; import { observer, useField, useFieldSchema, useForm } from '@formily/react';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useFormBlockContext } from '../../../block-provider';
import { useCollection } from '../../../collection-manager'; import { useCollection } from '../../../collection-manager';
import { useCompile } from '../../hooks'; import { useCompile } from '../../hooks';
import { ActionBar } from '../action'; import { ActionBar } from '../action';
@ -10,10 +11,15 @@ export const TableField: any = observer((props) => {
const field = useField(); const field = useField();
const collectionField = getField(fieldSchema.name); const collectionField = getField(fieldSchema.name);
const compile = useCompile(); const compile = useCompile();
const ctx = useFormBlockContext();
useEffect(() => { useEffect(() => {
if (!field.title) { if (!field.title) {
field.title = compile(collectionField?.uiSchema?.title); field.title = compile(collectionField?.uiSchema?.title);
} }
if (ctx?.field) {
ctx.field.added = ctx.field.added || new Set();
ctx.field.added.add(fieldSchema.name);
}
}, []); }, []);
return <div>{props.children}</div>; return <div>{props.children}</div>;
}); });

View File

@ -1,12 +1,34 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SchemaInitializer } from '../SchemaInitializer'; import { SchemaInitializer } from '../SchemaInitializer';
import { itemsMerge, useTableColumnInitializerFields } from '../utils'; import { itemsMerge, useAssociatedTableColumnInitializerFields, useTableColumnInitializerFields } from '../utils';
// 表格列配置 // 表格列配置
export const TableColumnInitializers = (props: any) => { export const TableColumnInitializers = (props: any) => {
const { items = [] } = props; const { items = [] } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const associatedFields = useAssociatedTableColumnInitializerFields();
const fieldItems: any[] = [{
type: 'itemGroup',
title: t('Display fields'),
children: useTableColumnInitializerFields(),
}];
if (associatedFields?.length > 0) {
fieldItems.push({
type: 'divider',
}, {
type: 'itemGroup',
title: t('Display association fields'),
children: associatedFields,
})
}
fieldItems.push({
type: 'divider',
}, {
type: 'item',
title: t('Action column'),
component: 'TableActionColumnInitializer',
})
return ( return (
<SchemaInitializer.Button <SchemaInitializer.Button
insertPosition={'beforeEnd'} insertPosition={'beforeEnd'}
@ -28,21 +50,7 @@ export const TableColumnInitializers = (props: any) => {
}; };
}} }}
items={itemsMerge( items={itemsMerge(
[ fieldItems,
{
type: 'itemGroup',
title: t('Display fields'),
children: useTableColumnInitializerFields(),
},
{
type: 'divider',
},
{
type: 'item',
title: t('Action column'),
component: 'TableActionColumnInitializer',
},
],
items, items,
)} )}
> >

View File

@ -82,7 +82,8 @@ export const useTableColumnInitializerFields = () => {
'x-collection-field': `${name}.${field.name}`, 'x-collection-field': `${name}.${field.name}`,
'x-component': 'CollectionField', 'x-component': 'CollectionField',
'x-read-pretty': true, 'x-read-pretty': true,
'x-component-props': {}, 'x-component-props': {
},
}; };
// interfaceConfig?.schemaInitialize?.(schema, { field, readPretty: true, block: 'Table' }); // interfaceConfig?.schemaInitialize?.(schema, { field, readPretty: true, block: 'Table' });
return { return {
@ -100,6 +101,57 @@ export const useTableColumnInitializerFields = () => {
}); });
}; };
export const useAssociatedTableColumnInitializerFields = () => {
const { name, fields } = useCollection();
const { getInterface, getCollectionFields } = useCollectionManager();
const groups = fields
?.filter((field) => {
return ['o2o', 'oho', 'obo', 'm2o'].includes(field.interface);
})
?.map((field) => {
const subFields = getCollectionFields(field.target);
const items = subFields
// ?.filter((subField) => subField?.interface && !['o2o', 'oho', 'obo', 'o2m', 'm2o', 'subTable', 'linkTo'].includes(subField?.interface))
?.filter((subField) => subField?.interface && !['subTable'].includes(subField?.interface))
?.map((subField) => {
const interfaceConfig = getInterface(subField.interface);
const schema = {
// type: 'string',
name: `${field.name}.${subField.name}`,
// title: subField?.uiSchema?.title || subField.name,
'x-component': 'CollectionField',
'x-read-pretty': true,
'x-collection-field': `${name}.${field.name}.${subField.name}`,
'x-component-props': {
},
};
return {
type: 'item',
title: subField?.uiSchema?.title || subField.name,
component: 'TableCollectionFieldInitializer',
find: findTableColumn,
remove: removeTableColumn,
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, { field: subField, readPretty: true, block: 'Table' });
},
field: subField,
schema,
} as SchemaInitializerItemOptions;
});
return {
type: 'subMenu',
title: field.uiSchema?.title,
children: items,
} as SchemaInitializerItemOptions;
});
return groups;
}
export const useFormItemInitializerFields = (options?: any) => { export const useFormItemInitializerFields = (options?: any) => {
const { name, fields } = useCollection(); const { name, fields } = useCollection();
const { getInterface } = useCollectionManager(); const { getInterface } = useCollectionManager();
@ -110,14 +162,17 @@ export const useFormItemInitializerFields = (options?: any) => {
?.filter((field) => field?.interface) ?.filter((field) => field?.interface)
?.map((field) => { ?.map((field) => {
const interfaceConfig = getInterface(field.interface); const interfaceConfig = getInterface(field.interface);
const schema = { const schema = {
type: 'string', type: 'string',
name: field.name, name: field.name,
// title: field?.uiSchema?.title || field.name, // title: field?.uiSchema?.title || field.name,
'x-designer': 'FormItem.Designer', 'x-designer': 'FormItem.Designer',
'x-component': 'CollectionField', 'x-component': field.interface === 'o2m' ? 'TableField' : 'CollectionField',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-collection-field': `${name}.${field.name}`, 'x-collection-field': `${name}.${field.name}`,
'x-component-props': {
},
}; };
// interfaceConfig?.schemaInitialize?.(schema, { field, block: 'Form', readPretty: form.readPretty }); // interfaceConfig?.schemaInitialize?.(schema, { field, block: 'Form', readPretty: form.readPretty });
return { return {
@ -133,6 +188,55 @@ export const useFormItemInitializerFields = (options?: any) => {
}); });
}; };
export const useAssociatedFormItemInitializerFields = (options?: any) => {
const { name, fields } = useCollection();
const { getInterface, getCollectionFields } = useCollectionManager();
const form = useForm();
const { readPretty = form.readPretty, block = 'Form' } = options || {};
const groups = fields
?.filter((field) => {
return ['o2o', 'oho', 'obo', 'm2o'].includes(field.interface);
})
?.map((field) => {
const subFields = getCollectionFields(field.target);
const items = subFields
?.filter((subField) => subField?.interface && !['subTable'].includes(subField?.interface))
?.map((subField) => {
const interfaceConfig = getInterface(subField.interface);
const schema = {
type: 'string',
name: `${field.name}.${subField.name}`,
// title: subField?.uiSchema?.title || subField.name,
'x-designer': 'FormItem.Designer',
'x-component': 'CollectionField',
'x-component-props': {
},
'x-decorator': 'FormItem',
'x-collection-field': `${name}.${field.name}.${subField.name}`,
};
// interfaceConfig?.schemaInitialize?.(schema, { field, block: 'Form', readPretty: form.readPretty });
return {
type: 'item',
title: subField?.uiSchema?.title || subField.name,
component: 'CollectionFieldInitializer',
remove: removeGridFormItem,
schemaInitialize: (s) => {
interfaceConfig?.schemaInitialize?.(s, { field: subField, block, readPretty });
},
schema,
} as SchemaInitializerItemOptions;
});
return {
type: 'subMenu',
title: field.uiSchema?.title,
children: items,
} as SchemaInitializerItemOptions;
});
return groups;
}
export const useCustomFormItemInitializerFields = (options?: any) => { export const useCustomFormItemInitializerFields = (options?: any) => {
const { name, fields } = useCollection(); const { name, fields } = useCollection();
const { getInterface } = useCollectionManager(); const { getInterface } = useCollectionManager();
@ -168,6 +272,7 @@ export const useCustomFormItemInitializerFields = (options?: any) => {
}; };
const findSchema = (schema: Schema, key: string, action: string) => { const findSchema = (schema: Schema, key: string, action: string) => {
if (!Schema.isSchemaInstance(schema)) return null;
return schema.reduceProperties((buf, s) => { return schema.reduceProperties((buf, s) => {
if (s[key] === action) { if (s[key] === action) {
return s; return s;
@ -420,7 +525,6 @@ export const createFormBlockSchema = (options) => {
template, template,
...others ...others
} = options; } = options;
console.log('createFormBlockSchema', options);
const resourceName = resource || association || collection; const resourceName = resource || association || collection;
const schema: ISchema = { const schema: ISchema = {
type: 'void', type: 'void',

View File

@ -1,4 +1,3 @@
import { Collection } from '@nocobase/database';
import { Plugin } from '@nocobase/server'; import { Plugin } from '@nocobase/server';
import lodash from 'lodash'; import lodash from 'lodash';
import path from 'path'; import path from 'path';
@ -101,41 +100,41 @@ export class CollectionManagerPlugin extends Plugin {
await next(); await next();
}); });
this.app.resourcer.use(async (ctx, next) => { // this.app.resourcer.use(async (ctx, next) => {
const { resourceName, actionName } = ctx.action; // const { resourceName, actionName } = ctx.action;
if (actionName === 'update') { // if (actionName === 'update') {
const { updateAssociationValues = [] } = ctx.action.params; // const { updateAssociationValues = [] } = ctx.action.params;
const [collectionName, associationName] = resourceName.split('.'); // const [collectionName, associationName] = resourceName.split('.');
if (!associationName) { // if (!associationName) {
const collection: Collection = ctx.db.getCollection(collectionName); // const collection: Collection = ctx.db.getCollection(collectionName);
if (collection) { // if (collection) {
for (const [, field] of collection.fields) { // for (const [, field] of collection.fields) {
if (['subTable', 'o2m'].includes(field.options.interface)) { // if (['subTable', 'o2m'].includes(field.options.interface)) {
updateAssociationValues.push(field.name); // updateAssociationValues.push(field.name);
} // }
} // }
} // }
} else { // } else {
const association = ctx.db.getCollection(collectionName)?.getField?.(associationName); // const association = ctx.db.getCollection(collectionName)?.getField?.(associationName);
if (association?.target) { // if (association?.target) {
const collection: Collection = ctx.db.getCollection(association?.target); // const collection: Collection = ctx.db.getCollection(association?.target);
if (collection) { // if (collection) {
for (const [, field] of collection.fields) { // for (const [, field] of collection.fields) {
if (['subTable', 'o2m'].includes(field.options.interface)) { // if (['subTable', 'o2m'].includes(field.options.interface)) {
updateAssociationValues.push(field.name); // updateAssociationValues.push(field.name);
} // }
} // }
} // }
} // }
} // }
if (updateAssociationValues.length) { // if (updateAssociationValues.length) {
ctx.action.mergeParams({ // ctx.action.mergeParams({
updateAssociationValues, // updateAssociationValues,
}); // });
} // }
} // }
await next(); // await next();
}); // });
this.app.acl.allow('collections', 'list', 'loggedIn'); this.app.acl.allow('collections', 'list', 'loggedIn');
this.app.acl.allow('collections', ['create', 'update', 'destroy'], 'allowConfigure'); this.app.acl.allow('collections', ['create', 'update', 'destroy'], 'allowConfigure');