diff --git a/packages/core/client/src/block-provider/BlockProvider.tsx b/packages/core/client/src/block-provider/BlockProvider.tsx index 692d98e38..8f76da2c6 100644 --- a/packages/core/client/src/block-provider/BlockProvider.tsx +++ b/packages/core/client/src/block-provider/BlockProvider.tsx @@ -62,6 +62,7 @@ const useResource = (props: UseResourceProps) => { }; return new TableFieldResource(options); } + const withoutTableFieldResource = useContext(WithoutTableFieldResource); const __parent = useContext(BlockRequestContext); if ( diff --git a/packages/core/client/src/block-provider/DetailsBlockProvider.tsx b/packages/core/client/src/block-provider/DetailsBlockProvider.tsx index 77c18643a..e7e5c3052 100644 --- a/packages/core/client/src/block-provider/DetailsBlockProvider.tsx +++ b/packages/core/client/src/block-provider/DetailsBlockProvider.tsx @@ -4,6 +4,7 @@ import { Spin } from 'antd'; import React, { createContext, useContext, useEffect, useMemo } from 'react'; import { RecordProvider } from '../record-provider'; import { BlockProvider, useBlockRequestContext } from './BlockProvider'; +import { useAssociationNames } from './hooks'; export const DetailsBlockContext = createContext({}); @@ -38,9 +39,15 @@ const InternalDetailsBlockProvider = (props) => { }; export const DetailsBlockProvider = (props) => { + const params = { ...props.params }; + const { collection } = props; + const { appends } = useAssociationNames(collection); + if (!Object.keys(params).includes('appends')) { + params['appends'] = appends; + } return ( - - + + ); }; diff --git a/packages/core/client/src/block-provider/FormBlockProvider.tsx b/packages/core/client/src/block-provider/FormBlockProvider.tsx index abaa1aa81..20c14905b 100644 --- a/packages/core/client/src/block-provider/FormBlockProvider.tsx +++ b/packages/core/client/src/block-provider/FormBlockProvider.tsx @@ -1,18 +1,18 @@ import { createForm } from '@formily/core'; import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; -import { Spin } from 'antd'; -import isEmpty from 'lodash/isEmpty'; +import { isEmpty } from 'lodash'; import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react'; import { useCollection } from '../collection-manager'; import { RecordProvider, useRecord } from '../record-provider'; import { useActionContext, useDesignable } from '../schema-component'; import { Templates as DataTemplateSelect } from '../schema-component/antd/form-v2/Templates'; import { BlockProvider, useBlockRequestContext } from './BlockProvider'; +import { useAssociationNames } from './hooks'; export const FormBlockContext = createContext({}); const InternalFormBlockProvider = (props) => { - const { action, readPretty } = props; + const { action, readPretty, params, updateAssociationValues } = props; const field = useField(); const form = useMemo( () => @@ -24,18 +24,19 @@ const InternalFormBlockProvider = (props) => { const { resource, service } = useBlockRequestContext(); const formBlockRef = useRef(); const record = useRecord(); - if (service.loading) { - return ; - } + // if (service.loading) { + // return ; + // } return ( @@ -67,9 +68,14 @@ export const FormBlockProvider = (props) => { const record = useRecord(); const { collection } = props; const { __collection } = record; + const params = { ...props.params }; const currentCollection = useCollection(); const { designable } = useDesignable(); const isEmptyRecord = useIsEmptyRecord(); + const { appends, updateAssociationValues } = useAssociationNames(collection); + if (!Object.keys(params).includes('appends')) { + params['appends'] = appends; + } let detailFlag = false; if (isEmptyRecord) { detailFlag = true; @@ -81,8 +87,8 @@ export const FormBlockProvider = (props) => { (currentCollection.name === (collection?.name || collection) && !isEmptyRecord) || !currentCollection.name; return ( (detailFlag || createFlag) && ( - - + + ) ); @@ -107,8 +113,10 @@ export const useFormBlockProps = () => { }); useEffect(() => { - ctx.form?.setInitialValues(ctx.service?.data?.data); - }, []); + if (!ctx?.service?.loading) { + ctx.form?.setInitialValues(ctx.service?.data?.data); + } + }, [ctx?.service?.loading]); return { form: ctx.form, }; diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index 28adb3435..3d7c96f41 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -1,6 +1,6 @@ -import { SchemaExpressionScopeContext, useField, useFieldSchema, useForm } from '@formily/react'; -import { Modal, message } from 'antd'; +import { Schema, SchemaExpressionScopeContext, useField, useFieldSchema, useForm } from '@formily/react'; import { parse } from '@nocobase/utils/client'; +import { Modal, message } from 'antd'; import { cloneDeep } from 'lodash'; import get from 'lodash/get'; import omit from 'lodash/omit'; @@ -166,7 +166,7 @@ export const useCreateActionProps = () => { actionField.data = field.data || {}; actionField.data.loading = true; try { - await resource.create({ + const data = await resource.create({ values: { ...values, ...overwriteValues, @@ -174,6 +174,7 @@ export const useCreateActionProps = () => { }, }); actionField.data.loading = false; + actionField.data.data = data; __parent?.service?.refresh?.(); setVisible?.(false); if (!onSuccess?.successMessage) { @@ -203,6 +204,62 @@ export const useCreateActionProps = () => { }; }; +export const useAssociationCreateActionProps = () => { + const form = useForm(); + const { field, resource } = useBlockRequestContext(); + const { setVisible, fieldSchema } = useActionContext(); + const actionSchema = useFieldSchema(); + const actionField = useField(); + const { fields, getField, getTreeParentField } = useCollection(); + const compile = useCompile(); + const filterByTk = useFilterByTk(); + const currentRecord = useRecord(); + const currentUserContext = useCurrentUserContext(); + const currentUser = currentUserContext?.data?.data; + return { + async onClick() { + const fieldNames = fields.map((field) => field.name); + const { + assignedValues: originalAssignedValues = {}, + onSuccess, + overwriteValues, + skipValidator, + } = actionSchema?.['x-action-settings'] ?? {}; + const addChild = fieldSchema?.['x-component-props']?.addChild; + const assignedValues = parse(originalAssignedValues)({ currentTime: new Date(), currentRecord, currentUser }); + if (!skipValidator) { + await form.submit(); + } + const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource); + if (addChild) { + const treeParentField = getTreeParentField(); + values[treeParentField?.name ?? 'parent'] = currentRecord; + values[treeParentField?.foreignKey ?? 'parentId'] = currentRecord.id; + } + actionField.data = field.data || {}; + actionField.data.loading = true; + try { + const data = await resource.create({ + values: { + ...values, + ...overwriteValues, + ...assignedValues, + }, + }); + actionField.data.loading = false; + actionField.data.data = data; + setVisible?.(false); + if (!onSuccess?.successMessage) { + return; + } + message.success(compile(onSuccess?.successMessage)); + } catch (error) { + actionField.data.loading = false; + } + }, + }; +}; + export interface FilterTarget { targets?: { /** field uid */ @@ -740,6 +797,19 @@ export const useDestroyActionProps = () => { }; }; +export const useRemoveActionProps = (associationName) => { + const filterByTk = useFilterByTk(); + const api = useAPIClient(); + const resource = api.resource(associationName, filterByTk); + return { + async onClick(value) { + await resource.remove({ + values: [value.id], + }); + }, + }; +}; + export const useDetailPrintActionProps = () => { const { formBlockRef } = useFormBlockContext(); @@ -969,7 +1039,6 @@ export const useAssociationFilterBlockProps = () => { const param = block.service.params?.[0] || {}; // 保留原有的 filter const storedFilter = block.service.params?.[1]?.filters || {}; - if (value.length) { storedFilter[key] = { [filterKey]: value, @@ -1002,3 +1071,83 @@ export const useAssociationFilterBlockProps = () => { labelKey, }; }; + +export const useAssociationNames = (collection) => { + const { getCollectionFields, getCollectionJoinField } = 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 associationValues = []; + const formSchema = fieldSchema.reduceProperties((buf, schema) => { + if (['FormV2', 'Details'].includes(schema['x-component'])) { + return schema; + } + return buf; + }, new Schema({})); + + const getAssociationAppends = (schema, arr = []) => { + return schema.reduceProperties((buf, s) => { + const collectionfield = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field']); + if ( + collectionfield && + ['createdBy', 'updatedBy', 'o2m', 'obo', 'oho', 'm2o', 'm2m'].includes(collectionfield.interface) + ) { + if (['Nester', 'SubTable'].includes(s['x-component-props']?.mode)) { + associationValues.push(s.name); + } + + buf.push(s.name); + if (s['x-component-props'].mode === 'Nester' || s['x-component'] === 'TableField') { + return getAssociationAppends(s, buf); + } + return buf; + } else { + if (s['x-component'] === 'Grid' || s['x-component'] === 'TableV2') { + const kk = buf?.concat?.(); + return getNesterAppends(s, kk || []); + } else { + return !s['x-component']?.includes('Action.') && getAssociationAppends(s, buf); + } + } + }, arr); + }; + function flattenNestedList(nestedList) { + const flattenedList = []; + function flattenHelper(list, prefix) { + for (let i = 0; i < list.length; i++) { + if (Array.isArray(list[i])) { + `${prefix}` !== `${list[i][0]}` && flattenHelper(list[i], `${prefix}.${list[i][0]}`); + } else { + const str = prefix.replaceAll(`${list[i]}`, '').replace('..', '.').trim(); + !list.includes(str) && flattenedList.push(`${str}${list[i]}`); + } + } + } + for (let i = 0; i < nestedList.length; i++) { + flattenHelper(nestedList[i], nestedList[i][0]); + } + return flattenedList.filter((obj) => !obj.startsWith('.')); + } + const getNesterAppends = (gridSchema, data) => { + gridSchema.reduceProperties((buf, s) => { + buf.push(getAssociationAppends(s)); + return buf; + }, data); + return data.filter((g) => g.length); + }; + const data = getAssociationAppends(formSchema); + const associations = data.filter((g) => g.length); + const appends = flattenNestedList(associations); + return { appends, updateAssociationValues: appends.filter((v) => associationValues.includes(v)) }; +}; diff --git a/packages/core/client/src/collection-manager/interfaces/createdBy.ts b/packages/core/client/src/collection-manager/interfaces/createdBy.ts index e329a90f7..9f3db8de2 100644 --- a/packages/core/client/src/collection-manager/interfaces/createdBy.ts +++ b/packages/core/client/src/collection-manager/interfaces/createdBy.ts @@ -18,7 +18,7 @@ export const createdBy: IField = { uiSchema: { type: 'object', title: '{{t("Created by")}}', - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { fieldNames: { value: 'id', diff --git a/packages/core/client/src/collection-manager/interfaces/linkTo.ts b/packages/core/client/src/collection-manager/interfaces/linkTo.ts index 94d459b28..e0701d74e 100644 --- a/packages/core/client/src/collection-manager/interfaces/linkTo.ts +++ b/packages/core/client/src/collection-manager/interfaces/linkTo.ts @@ -17,7 +17,7 @@ export const linkTo: IField = { // name, uiSchema: { // title, - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { // mode: 'tags', multiple: true, @@ -33,7 +33,7 @@ export const linkTo: IField = { // name, uiSchema: { // title, - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { // mode: 'tags', multiple: true, @@ -51,45 +51,9 @@ export const linkTo: IField = { schema['x-component-props'].fieldNames = schema['x-component-props'].fieldNames || { value: 'id' }; schema['x-component-props'].fieldNames.label = targetCollection.titleField; } - - if (targetCollection?.template === 'file') { - const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' }; - fieldNames.label = 'preview'; - schema['x-component-props']['fieldNames'] = fieldNames; - schema['x-component-props'].quickUpload = true; - schema['x-component-props'].selectFile = true; - } - - if (block === 'Form') { - if (schema['x-component'] === 'AssociationSelect') { - Object.assign(schema, { - type: 'string', - 'x-designer': 'AssociationSelect.Designer', - }); - } else { - schema.type = 'string'; - schema['properties'] = { - viewer: cloneDeep(recordPickerViewer), - selector: cloneDeep(recordPickerSelector), - }; - } - return schema; - } else { - if (readPretty) { - schema['properties'] = { - viewer: cloneDeep(recordPickerViewer), - }; - } else { - schema['properties'] = { - selector: cloneDeep(recordPickerSelector), - }; - } - } - if (['Table', 'Kanban'].includes(block)) { schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props']['ellipsis'] = true; - // 预览文件时需要的参数 schema['x-component-props']['size'] = 'small'; } diff --git a/packages/core/client/src/collection-manager/interfaces/m2m.tsx b/packages/core/client/src/collection-manager/interfaces/m2m.tsx index d8ca366b8..1998730ec 100644 --- a/packages/core/client/src/collection-manager/interfaces/m2m.tsx +++ b/packages/core/client/src/collection-manager/interfaces/m2m.tsx @@ -23,7 +23,7 @@ export const m2m: IField = { // name, uiSchema: { // title, - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { // mode: 'tags', multiple: true, @@ -39,7 +39,7 @@ export const m2m: IField = { // name, uiSchema: { // title, - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { // mode: 'tags', multiple: true, @@ -53,47 +53,14 @@ export const m2m: IField = { }, availableTypes: ['belongsToMany'], schemaInitialize(schema: ISchema, { readPretty, block, targetCollection }) { + schema['type'] = 'array'; if (targetCollection?.titleField && schema['x-component-props']) { schema['x-component-props'].fieldNames = schema['x-component-props'].fieldNames || { value: 'id' }; schema['x-component-props'].fieldNames.label = targetCollection.titleField; } - - if (targetCollection?.template === 'file') { - const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' }; - fieldNames.label = 'preview'; - schema['x-component-props']['fieldNames'] = fieldNames; - schema['x-component-props'].quickUpload = true; - schema['x-component-props'].selectFile = true; - } - - if (block === 'Form') { - if (schema['x-component'] === 'AssociationSelect') { - Object.assign(schema, { - type: 'string', - 'x-designer': 'AssociationSelect.Designer', - }); - } else { - schema.type = 'string'; - schema['properties'] = { - viewer: cloneDeep(recordPickerViewer), - selector: cloneDeep(recordPickerSelector), - }; - } - return schema; - } - if (readPretty) { - schema['properties'] = { - viewer: cloneDeep(recordPickerViewer), - }; - } else { - schema['properties'] = { - selector: cloneDeep(recordPickerSelector), - }; - } if (['Table', 'Kanban'].includes(block)) { schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props']['ellipsis'] = true; - // 预览文件时需要的参数 schema['x-component-props']['size'] = 'small'; } diff --git a/packages/core/client/src/collection-manager/interfaces/m2o.tsx b/packages/core/client/src/collection-manager/interfaces/m2o.tsx index 9b3c8d193..69c28a5a0 100644 --- a/packages/core/client/src/collection-manager/interfaces/m2o.tsx +++ b/packages/core/client/src/collection-manager/interfaces/m2o.tsx @@ -22,7 +22,7 @@ export const m2o: IField = { // name, uiSchema: { // title, - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { // mode: 'tags', multiple: false, @@ -38,7 +38,7 @@ export const m2o: IField = { // name, uiSchema: { // title, - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { // mode: 'tags', multiple: true, @@ -52,47 +52,14 @@ export const m2o: IField = { }, availableTypes: ['belongsTo'], schemaInitialize(schema: ISchema, { block, readPretty, targetCollection }) { + schema['type'] = 'object'; if (targetCollection?.titleField && schema['x-component-props']) { schema['x-component-props'].fieldNames = schema['x-component-props'].fieldNames || { value: 'id' }; schema['x-component-props'].fieldNames.label = targetCollection.titleField; } - - if (targetCollection?.template === 'file') { - const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' }; - fieldNames.label = 'preview'; - schema['x-component-props']['fieldNames'] = fieldNames; - schema['x-component-props'].quickUpload = true; - schema['x-component-props'].selectFile = true; - } - - if (block === 'Form') { - if (schema['x-component'] === 'AssociationSelect') { - Object.assign(schema, { - type: 'string', - 'x-designer': 'AssociationSelect.Designer', - }); - } else { - schema.type = 'string'; - schema['properties'] = { - viewer: cloneDeep(recordPickerViewer), - selector: cloneDeep(recordPickerSelector), - }; - } - return schema; - } - if (readPretty) { - schema['properties'] = { - viewer: cloneDeep(recordPickerViewer), - }; - } else { - schema['properties'] = { - selector: cloneDeep(recordPickerSelector), - }; - } if (['Table', 'Kanban'].includes(block)) { schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props']['ellipsis'] = true; - // 预览文件时需要的参数 schema['x-component-props']['size'] = 'small'; } diff --git a/packages/core/client/src/collection-manager/interfaces/o2m.tsx b/packages/core/client/src/collection-manager/interfaces/o2m.tsx index 32b6df46b..4267cca52 100644 --- a/packages/core/client/src/collection-manager/interfaces/o2m.tsx +++ b/packages/core/client/src/collection-manager/interfaces/o2m.tsx @@ -22,7 +22,7 @@ export const o2m: IField = { // name, uiSchema: { // title, - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { // mode: 'tags', multiple: true, @@ -38,7 +38,7 @@ export const o2m: IField = { // name, uiSchema: { // title, - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { // mode: 'tags', multiple: false, @@ -52,82 +52,14 @@ export const o2m: IField = { }, availableTypes: ['hasMany'], schemaInitialize(schema: ISchema, { field, block, readPretty, targetCollection }) { + schema['type'] = 'array'; if (targetCollection?.titleField && schema['x-component-props']) { schema['x-component-props'].fieldNames = schema['x-component-props'].fieldNames || { value: 'id' }; schema['x-component-props'].fieldNames.label = targetCollection.titleField; } - - if (block === 'Form') { - if (schema['x-component'] === 'TableField') { - const association = `${field.collectionName}.${field.name}`; - schema['type'] = 'void'; - schema['properties'] = { - block: { - type: 'void', - 'x-decorator': 'TableFieldProvider', - 'x-acl-action': `${association}:list`, - 'x-decorator-props': { - collection: field.target, - association: association, - resource: association, - action: 'list', - params: { - paginate: false, - }, - showIndex: true, - dragSort: false, - fieldName: field.name, - }, - properties: { - actions: { - type: 'void', - 'x-initializer': 'SubTableActionInitializers', - 'x-component': 'TableField.ActionBar', - 'x-component-props': {}, - }, - [field.name]: { - type: 'array', - 'x-initializer': 'TableColumnInitializers', - 'x-component': 'TableV2', - 'x-component-props': { - rowSelection: { - type: 'checkbox', - }, - useProps: '{{ useTableFieldProps }}', - }, - }, - }, - }, - }; - } else if (schema['x-component'] === 'AssociationSelect') { - Object.assign(schema, { - type: 'string', - 'x-designer': 'AssociationSelect.Designer', - }); - } else { - schema.type = 'string'; - schema['properties'] = { - viewer: cloneDeep(recordPickerViewer), - selector: cloneDeep(recordPickerSelector), - }; - } - return schema; - } - - if (readPretty) { - schema['properties'] = { - viewer: cloneDeep(recordPickerViewer), - }; - } else { - schema['properties'] = { - selector: cloneDeep(recordPickerSelector), - }; - } - if (['Table', 'Kanban'].includes(block)) { schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props']['ellipsis'] = true; - // 预览文件时需要的参数 schema['x-component-props']['size'] = 'small'; } diff --git a/packages/core/client/src/collection-manager/interfaces/o2o.tsx b/packages/core/client/src/collection-manager/interfaces/o2o.tsx index 6b2700e28..f72aa926e 100644 --- a/packages/core/client/src/collection-manager/interfaces/o2o.tsx +++ b/packages/core/client/src/collection-manager/interfaces/o2o.tsx @@ -9,72 +9,6 @@ import { } from './properties'; import { IField } from './types'; -const internalSchameInitialize = (schema: ISchema, { field, block, readPretty, action }) => { - if (block === 'Form') { - if (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 if (schema['x-component'] === 'AssociationSelect') { - Object.assign(schema, { - type: 'string', - 'x-designer': 'AssociationSelect.Designer', - }); - } else { - schema.type = 'string'; - schema['properties'] = { - viewer: cloneDeep(recordPickerViewer), - selector: cloneDeep(recordPickerSelector), - }; - } - return schema; - } - schema.type = 'string'; - if (readPretty) { - schema['properties'] = { - viewer: cloneDeep(recordPickerViewer), - }; - } else { - schema['properties'] = { - selector: cloneDeep(recordPickerSelector), - }; - } -}; - export const o2o: IField = { name: 'o2o', type: 'object', @@ -88,7 +22,7 @@ export const o2o: IField = { // name, uiSchema: { // title, - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { // mode: 'tags', multiple: false, @@ -105,7 +39,7 @@ export const o2o: IField = { // name, uiSchema: { // title, - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { // mode: 'tags', multiple: false, @@ -119,7 +53,6 @@ export const o2o: IField = { }, availableTypes: ['hasOne'], schemaInitialize(schema: ISchema, { field, block, readPretty, action }) { - internalSchameInitialize(schema, { field, block, readPretty, action }); if (['Table', 'Kanban'].includes(block)) { schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props']['ellipsis'] = true; @@ -268,7 +201,7 @@ export const oho: IField = { // name, uiSchema: { // title, - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { // mode: 'tags', multiple: false, @@ -285,7 +218,7 @@ export const oho: IField = { // name, uiSchema: { // title, - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { // mode: 'tags', multiple: false, @@ -298,7 +231,7 @@ export const oho: IField = { }, }, schemaInitialize(schema: ISchema, { field, block, readPretty, action }) { - internalSchameInitialize(schema, { field, block, readPretty, action }); + schema['type'] = 'object'; if (['Table', 'Kanban'].includes(block)) { schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props']['ellipsis'] = true; @@ -439,7 +372,7 @@ export const obo: IField = { // name, uiSchema: { // title, - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { // mode: 'tags', multiple: false, @@ -455,7 +388,7 @@ export const obo: IField = { // name, uiSchema: { // title, - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { // mode: 'tags', multiple: false, @@ -468,11 +401,10 @@ export const obo: IField = { }, }, schemaInitialize(schema: ISchema, { field, block, readPretty, action, targetCollection }) { - internalSchameInitialize(schema, { field, block, readPretty, action }); + schema['type'] = 'object'; if (['Table', 'Kanban'].includes(block)) { schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props']['ellipsis'] = true; - // 预览文件时需要的参数 schema['x-component-props']['size'] = 'small'; } @@ -481,14 +413,6 @@ export const obo: IField = { schema['x-component-props'].fieldNames = schema['x-component-props'].fieldNames || { value: 'id' }; schema['x-component-props'].fieldNames.label = targetCollection.titleField; } - - if (targetCollection?.template === 'file') { - const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' }; - fieldNames.label = 'preview'; - schema['x-component-props']['fieldNames'] = fieldNames; - schema['x-component-props'].quickUpload = true; - schema['x-component-props'].selectFile = true; - } }, properties: { 'uiSchema.title': { diff --git a/packages/core/client/src/collection-manager/interfaces/updatedBy.ts b/packages/core/client/src/collection-manager/interfaces/updatedBy.ts index 32ddff4e6..45560ebbb 100644 --- a/packages/core/client/src/collection-manager/interfaces/updatedBy.ts +++ b/packages/core/client/src/collection-manager/interfaces/updatedBy.ts @@ -17,7 +17,7 @@ export const updatedBy: IField = { uiSchema: { type: 'object', title: '{{t("Last updated by")}}', - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { fieldNames: { value: 'id', diff --git a/packages/core/client/src/collection-manager/templates/tree.tsx b/packages/core/client/src/collection-manager/templates/tree.tsx index 0cc7b47ad..31fc3c17a 100644 --- a/packages/core/client/src/collection-manager/templates/tree.tsx +++ b/packages/core/client/src/collection-manager/templates/tree.tsx @@ -30,7 +30,7 @@ export const tree: ICollectionTemplate = { onDelete: 'CASCADE', uiSchema: { title: '{{t("Parent")}}', - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { // mode: 'tags', multiple: false, @@ -50,7 +50,7 @@ export const tree: ICollectionTemplate = { onDelete: 'CASCADE', uiSchema: { title: '{{t("Children")}}', - 'x-component': 'RecordPicker', + 'x-component': 'AssociationField', 'x-component-props': { // mode: 'tags', multiple: true, diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts index 04afd2004..fab4007c0 100644 --- a/packages/core/client/src/locale/en_US.ts +++ b/packages/core/client/src/locale/en_US.ts @@ -241,6 +241,7 @@ export default { "Link to": "Link to", "Link to description": "Used to create collection relationships quickly and compatible with most common scenarios. Suitable for non-developer use. When present as a field, it is a drop-down selection used to select records from the target collection. Once created, it will simultaneously generate the associated fields of the current collection in the target collection.", "Sub-table": "Sub-table", + "Sub-details":"Sub-details", "System info": "System info", "Created at": "Created at", "Last updated at": "Last updated at", @@ -281,12 +282,14 @@ export default { "Display field title": "Display field title", "Field component": "Field component", "Subtable": "Subtable", - "Subform": "Subform", + "Subform": "Sub-form", + "Field mode":"Field mode", + "Allow add new data":"Allow add new data", "Record picker": "Record picker", "Toggles the subfield mode": "Toggles the subfield mode", "Selector mode": "Selector mode", "Subtable mode": "Subtable mode", - "Subform mode": "Subform mode", + "Subform mode": "Sub-form mode", "Edit block title": "Edit block title", "Block title": "Block title", "Pattern": "Pattern", diff --git a/packages/core/client/src/locale/ja_JP.ts b/packages/core/client/src/locale/ja_JP.ts index ae0ce03cf..cb2521f51 100644 --- a/packages/core/client/src/locale/ja_JP.ts +++ b/packages/core/client/src/locale/ja_JP.ts @@ -556,6 +556,9 @@ export default { "Field component": "フィールドコンポーネント", "Subtable": "サブテーブル", "Subform": "サブフォーム", + "Sub-details":"サブリスト", + "Field mode":"フィールドコンポーネント", + "Allow add new data":"データの追加を許可", "Regular expression": "正規表現", "Enabled languages": "利用可能な言語", "View all plugins": "すべてのプラグラインを見る", diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 7c498bc09..464c309a9 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -304,11 +304,14 @@ export default { "Field component": "字段组件", "Subtable": "子表格", "Subform": "子表单", + "Sub-details":"子详情", "Record picker": "数据选择器", "Toggles the subfield mode": "切换子字段模式", "Selector mode": "选择器模式", "Subtable mode": "子表格模式", "Subform mode": "子表单模式", + "Field mode":"字段组件", + "Allow add new data":"允许添加数据", "Edit block title": "编辑区块标题", "Block title": "区块标题", "Pattern": "模式", diff --git a/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx b/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx new file mode 100644 index 000000000..0f0dda20d --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx @@ -0,0 +1,19 @@ +import { useField, useFieldSchema } from '@formily/react'; +import React, { useMemo } from 'react'; +import { useCollectionManager } from '../../../collection-manager'; +import { AssociationFieldContext } from './context'; + +export function AssociationFieldProvider(props) { + const field = useField(); + const { getCollectionField } = useCollectionManager(); + const fieldSchema = useFieldSchema(); + const collectionField = useMemo( + () => getCollectionField(fieldSchema['x-collection-field']), + [fieldSchema['x-collection-field']], + ); + return ( + + {props.children} + + ); +} diff --git a/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx b/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx new file mode 100644 index 000000000..a07444a69 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx @@ -0,0 +1,122 @@ +import { LoadingOutlined } from '@ant-design/icons'; +import { RecursionField, connect, mapProps, observer, useField, useFieldSchema } from '@formily/react'; +import { Button, Input } from 'antd'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { CollectionProvider, useCollection } from '../../../collection-manager'; +import { useFieldTitle } from '../../hooks'; +import { ActionContext } from '../action'; +import { RemoteSelect, RemoteSelectProps } from '../remote-select'; +import useServiceOptions, { useInsertSchema } from './hooks'; +import schema from './schema'; + +export type AssociationSelectProps

= RemoteSelectProps

& { + action?: string; + multiple?: boolean; +}; + +const InternalAssociationSelect = observer((props: AssociationSelectProps) => { + const { fieldNames, objectValue = true } = props; + const field: any = useField(); + const [visibleAddNewer, setVisibleAddNewer] = useState(false); + const { getField } = useCollection(); + const collectionField = getField(field.props.name); + const service = useServiceOptions(props); + const fieldSchema = useFieldSchema(); + const isFilterForm = fieldSchema['x-designer'] === 'FormItem.FilterFormDesigner'; + const isAllowAddNew = fieldSchema['x-add-new']; + const insertAddNewer = useInsertSchema('AddNewer'); + const { t } = useTranslation(); + const normalizeValues = useCallback( + (obj) => { + if (!objectValue && typeof obj === 'object') { + return obj[fieldNames?.value]; + } + return obj; + }, + [objectValue, fieldNames?.value], + ); + + const value = useMemo(() => { + if (props.value === undefined || props.value === null || !Object.keys(props.value).length) { + return; + } + if (Array.isArray(props.value)) { + return props.value; + } else { + return props.value; + } + }, [props.value, normalizeValues]); + useEffect(() => { + field.value = value; + }, []); + return ( +

+ + + {isAllowAddNew && !field.readPretty && !isFilterForm && ( + + )} + + + + + { + return s['x-component'] === 'AssociationField.AddNewer'; + }} + /> + + +
+ ); +}); + +interface AssociationSelectInterface { + (props: any): React.ReactElement; + Designer: React.FC; + FilterDesigner: React.FC; +} + +export const AssociationSelect = InternalAssociationSelect as unknown as AssociationSelectInterface; + +export const AssociationSelectReadPretty = connect( + (props: any) => { + if (props.fieldNames) { + const service = useServiceOptions(props); + useFieldTitle(); + return ; + } + return null; + }, + mapProps( + { + dataSource: 'options', + loading: true, + }, + (props, field) => { + return { + ...props, + fieldNames: props.fieldNames && { ...props.fieldNames, ...field.componentProps.fieldNames }, + suffixIcon: field?.['loading'] || field?.['validating'] ? : props.suffixIcon, + }; + }, + ), +); diff --git a/packages/core/client/src/schema-component/antd/association-field/Editable.tsx b/packages/core/client/src/schema-component/antd/association-field/Editable.tsx new file mode 100644 index 000000000..980d347cb --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-field/Editable.tsx @@ -0,0 +1,63 @@ +import { observer, useField, useFieldSchema, useForm } from '@formily/react'; +import React, { useEffect, useState } from 'react'; +import { AssociationFieldProvider } from './AssociationFieldProvider'; +import { InternalNester } from './InternalNester'; +import { InternalPicker } from './InternalPicker'; +import { AssociationSelect } from './AssociationSelect'; +import { useAssociationCreateActionProps as useCAP } from '../../../block-provider/hooks'; +import { useCollection, useCollectionManager } from '../../../collection-manager'; +import { SchemaComponentOptions } from '../../'; +import { InternalSubTable } from './InternalSubTable'; +import { InternalFileManager } from './FileManager'; + +export const Editable = observer((props: any) => { + useEffect(() => { + props.mode && setCurrentMode(props.mode); + }, [props.mode]); + const { multiple } = props; + const field: any = useField(); + const form = useForm(); + const fieldSchema = useFieldSchema(); + const { getField } = useCollection(); + const { getCollection } = useCollectionManager(); + const collectionField = getField(field.props.name); + const isFileCollection = getCollection(collectionField?.target)?.template === 'file'; + const [currentMode, setCurrentMode] = useState(props.mode || (isFileCollection ? 'FileManager' : 'Select')); + const useCreateActionProps = () => { + const { onClick } = useCAP(); + const actionField: any = useField(); + return { + async onClick() { + await onClick(); + const { data } = actionField.data?.data?.data || {}; + if (['m2m', 'o2m'].includes(collectionField.interface) && multiple !== false) { + const values = JSON.parse(JSON.stringify(form.values[fieldSchema.name] || [])); + values.push({ + ...data, + }); + setTimeout(() => { + form.setValuesIn(field.props.name, values); + }, 100); + } else { + const value = { + ...data, + }; + setTimeout(() => { + form.setValuesIn(field.props.name, value); + }, 100); + } + }, + }; + }; + return ( + + + {currentMode === 'Picker' && } + {currentMode === 'Nester' && } + {currentMode === 'Select' && } + {currentMode === 'SubTable' && } + {currentMode === 'FileManager' && } + + + ); +}); diff --git a/packages/core/client/src/schema-component/antd/association-field/FileManager.tsx b/packages/core/client/src/schema-component/antd/association-field/FileManager.tsx new file mode 100644 index 000000000..5e8b24c49 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-field/FileManager.tsx @@ -0,0 +1,194 @@ +import { RecursionField, connect, useField, useFieldSchema } from '@formily/react'; +import { differenceBy, unionBy } from 'lodash'; +import React, { useContext, useEffect, useState } from 'react'; +import { + FormProvider, + RecordPickerContext, + RecordPickerProvider, + SchemaComponentOptions, + useActionContext, +} from '../..'; +import { + TableSelectorParamsProvider, + useTableSelectorProps as useTsp, +} from '../../../block-provider/TableSelectorProvider'; +import { CollectionProvider, useCollection } from '../../../collection-manager'; +import { useCompile } from '../../hooks'; +import { ActionContext } from '../action'; +import { FileSelector, Preview } from '../preview'; +import { ReadPrettyInternalViewer } from './InternalViewer'; +import { useFieldNames, useInsertSchema } from './hooks'; +import schema from './schema'; +import { flatData, getLabelFormatValue, isShowFilePicker, useLabelUiSchema } from './util'; + +const useTableSelectorProps = () => { + const field: any = useField(); + const { + multiple, + options = [], + setSelectedRows, + selectedRows: rcSelectRows = [], + onChange, + } = useContext(RecordPickerContext); + const { onRowSelectionChange, rowKey = 'id', ...others } = useTsp(); + const { setVisible } = useActionContext(); + return { + ...others, + rowKey, + rowSelection: { + type: multiple ? 'checkbox' : 'radio', + selectedRowKeys: rcSelectRows + ?.filter((item) => options.every((row) => row[rowKey] !== item[rowKey])) + .map((item) => item[rowKey]), + }, + onRowSelectionChange(selectedRowKeys, selectedRows) { + if (multiple) { + const scopeRows = flatData(field.value) || []; + const allSelectedRows = rcSelectRows || []; + const otherRows = differenceBy(allSelectedRows, scopeRows, rowKey); + const unionSelectedRows = unionBy(otherRows, selectedRows, rowKey); + const unionSelectedRowKeys = unionSelectedRows.map((item) => item[rowKey]); + setSelectedRows?.(unionSelectedRows); + onRowSelectionChange?.(unionSelectedRowKeys, unionSelectedRows); + } else { + setSelectedRows?.(selectedRows); + onRowSelectionChange?.(selectedRowKeys, selectedRows); + onChange(selectedRows?.[0] || null); + setVisible(false); + } + }, + }; +}; +const InternalFileManager = (props) => { + const { value, multiple, onChange, quickUpload, selectFile, ...others } = props; + const fieldSchema = useFieldSchema(); + const [visibleSelector, setVisibleSelector] = useState(false); + const [selectedRows, setSelectedRows] = useState([]); + const insertSelector = useInsertSchema('Selector'); + const fieldNames = useFieldNames(props); + const field: any = useField(); + const [options, setOptions] = useState([]); + const { getField } = useCollection(); + const collectionField = getField(field.props.name); + const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label'); + const compile = useCompile(); + const getFilter = () => { + const targetKey = collectionField?.targetKey || 'id'; + const list = options.map((option) => option[targetKey]).filter(Boolean); + const filter = list.length ? { $and: [{ [`${targetKey}.$ne`]: list }] } : {}; + return filter; + }; + const handleSelect = () => { + insertSelector(schema.Selector); + setVisibleSelector(true); + setSelectedRows([]); + }; + + useEffect(() => { + if (value && Object.keys(value).length > 0) { + const opts = (Array.isArray(value) ? value : value ? [value] : []).map((option) => { + const label = option[fieldNames.label]; + return { + ...option, + [fieldNames.label]: getLabelFormatValue(compile(labelUiSchema), compile(label)), + }; + }); + setOptions(opts); + } + }, [value, fieldNames?.label]); + + const handleRemove = (file) => { + const newOptions = options.filter((option) => option.id !== file.id); + setOptions(newOptions); + if (newOptions.length === 0) { + return onChange(null); + } + onChange(newOptions); + }; + const pickerProps = { + size: 'small', + fieldNames, + multiple: ['o2m', 'm2m'].includes(collectionField?.interface), + association: { + target: collectionField?.target, + }, + onChange: props?.onChange, + selectedRows, + setSelectedRows, + collectionField, + }; + const usePickActionProps = () => { + const { setVisible } = useActionContext(); + const { multiple, selectedRows, onChange, options, collectionField } = useContext(RecordPickerContext); + return { + onClick() { + if (multiple) { + onChange(unionBy(selectedRows, options, collectionField?.targetKey || 'id')); + } else { + onChange(selectedRows?.[0] || null); + } + setVisible(false); + }, + }; + }; + return ( +
+ { + if (changed.every((file) => file.status !== 'uploading')) { + changed = changed.filter((file) => file.status === 'done').map((file) => file.response.data); + if (multiple) { + onChange([...options, ...changed]); + } else { + onChange(changed[0]); + } + } + }} + /> + + + + + + + { + return s['x-component'] === 'AssociationField.Selector'; + }} + /> + + + + + + +
+ ); +}; + +const FileManageReadPretty = connect((props) => { + const field: any = useField(); + const fieldNames = useFieldNames(props); + const { getField } = useCollection(); + const collectionField = getField(field.props.name); + const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label'); + const showFilePicker = isShowFilePicker(labelUiSchema); + + if (showFilePicker) { + return collectionField ? : null; + } else { + return ; + } +}); + +export { InternalFileManager, FileManageReadPretty }; diff --git a/packages/core/client/src/schema-component/antd/association-field/InternalNester.tsx b/packages/core/client/src/schema-component/antd/association-field/InternalNester.tsx new file mode 100644 index 000000000..cec806071 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-field/InternalNester.tsx @@ -0,0 +1,30 @@ +import { FormLayout } from '@formily/antd'; +import { RecursionField, useField, useFieldSchema } from '@formily/react'; +import React, { useEffect } from 'react'; +import { CollectionProvider } from '../../../collection-manager'; +import { useAssociationFieldContext, useInsertSchema } from './hooks'; +import schema from './schema'; + +export const InternalNester = () => { + const field = useField(); + const fieldSchema = useFieldSchema(); + const insertNester = useInsertSchema('Nester'); + const { options } = useAssociationFieldContext(); + useEffect(() => { + insertNester(schema.Nester); + }, []); + return ( + + + { + return s['x-component'] === 'AssociationField.Nester'; + }} + /> + + + ); +}; diff --git a/packages/core/client/src/schema-component/antd/association-field/InternalPicker.tsx b/packages/core/client/src/schema-component/antd/association-field/InternalPicker.tsx new file mode 100644 index 000000000..d02c99a07 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-field/InternalPicker.tsx @@ -0,0 +1,216 @@ +import { RecursionField, observer, useField, useFieldSchema } from '@formily/react'; +import { Button, Input, Select } from 'antd'; +import { differenceBy, unionBy } from 'lodash'; +import React, { useContext, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + FormProvider, + RecordPickerContext, + RecordPickerProvider, + SchemaComponentOptions, + useActionContext, +} from '../..'; +import { + TableSelectorParamsProvider, + useTableSelectorProps as useTsp, +} from '../../../block-provider/TableSelectorProvider'; +import { CollectionProvider, useCollection } from '../../../collection-manager'; +import { useCompile } from '../../hooks'; +import { ActionContext } from '../action'; +import { useFieldNames, useInsertSchema } from './hooks'; +import schema from './schema'; +import { flatData, getLabelFormatValue, useLabelUiSchema } from './util'; + +const useTableSelectorProps = () => { + const field: any = useField(); + const { + multiple, + options = [], + setSelectedRows, + selectedRows: rcSelectRows = [], + onChange, + } = useContext(RecordPickerContext); + const { onRowSelectionChange, rowKey = 'id', ...others } = useTsp(); + const { setVisible } = useActionContext(); + return { + ...others, + rowKey, + rowSelection: { + type: multiple ? 'checkbox' : 'radio', + selectedRowKeys: rcSelectRows + ?.filter((item) => options.every((row) => row[rowKey] !== item[rowKey])) + .map((item) => item[rowKey]), + }, + onRowSelectionChange(selectedRowKeys, selectedRows) { + if (multiple) { + const scopeRows = flatData(field.value) || []; + const allSelectedRows = rcSelectRows || []; + const otherRows = differenceBy(allSelectedRows, scopeRows, rowKey); + const unionSelectedRows = unionBy(otherRows, selectedRows, rowKey); + const unionSelectedRowKeys = unionSelectedRows.map((item) => item[rowKey]); + setSelectedRows?.(unionSelectedRows); + onRowSelectionChange?.(unionSelectedRowKeys, unionSelectedRows); + } else { + setSelectedRows?.(selectedRows); + onRowSelectionChange?.(selectedRowKeys, selectedRows); + onChange(selectedRows?.[0] || null); + setVisible(false); + } + }, + }; +}; + +export const InternalPicker = observer((props: any) => { + const { value, multiple, onChange, quickUpload, selectFile, ...others } = props; + const field: any = useField(); + const fieldNames = useFieldNames(props); + const [visibleAddNewer, setVisibleAddNewer] = useState(false); + const [visibleSelector, setVisibleSelector] = useState(false); + const fieldSchema = useFieldSchema(); + const insertAddNewer = useInsertSchema('AddNewer'); + const insertSelector = useInsertSchema('Selector'); + const { getField } = useCollection(); + const { t } = useTranslation(); + const collectionField = getField(field.props.name); + const addbuttonClick = () => { + insertAddNewer(schema.AddNewer); + setVisibleAddNewer(true); + }; + const compile = useCompile(); + const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label'); + const isAllowAddNew = fieldSchema['x-add-new']; + const [selectedRows, setSelectedRows] = useState([]); + const [options, setOptions] = useState([]); + const pickerProps = { + size: 'small', + fieldNames, + multiple: multiple !== false && ['o2m', 'm2m'].includes(collectionField?.interface), + association: { + target: collectionField?.target, + }, + options, + onChange: props?.onChange, + selectedRows, + setSelectedRows, + collectionField, + }; + useEffect(() => { + if (value && Object.keys(value).length > 0) { + const opts = (Array.isArray(value) ? value : value ? [value] : []).map((option) => { + const label = option[fieldNames.label]; + return { + ...option, + [fieldNames.label]: getLabelFormatValue(compile(labelUiSchema), compile(label)), + }; + }); + setOptions(opts); + } + }, [value, fieldNames?.label]); + + const getValue = () => { + if (multiple == null) return null; + return Array.isArray(value) ? value?.map((v) => v[fieldNames.value]) : value?.[fieldNames.value]; + }; + + const getFilter = () => { + const targetKey = collectionField?.targetKey || 'id'; + const list = options.map((option) => option[targetKey]).filter(Boolean); + const filter = list.length ? { $and: [{ [`${targetKey}.$ne`]: list }] } : {}; + return filter; + }; + const usePickActionProps = () => { + const { setVisible } = useActionContext(); + const { multiple, selectedRows, onChange, options, collectionField } = useContext(RecordPickerContext); + return { + onClick() { + if (multiple) { + onChange(unionBy(selectedRows, options, collectionField?.targetKey || 'id')); + } else { + onChange(selectedRows?.[0] || null); + } + setVisible(false); + }, + }; + }; + return ( + <> + +
+