diff --git a/packages/core/client/src/api-client/APIClient.ts b/packages/core/client/src/api-client/APIClient.ts index aec01b572..0b1d7a026 100644 --- a/packages/core/client/src/api-client/APIClient.ts +++ b/packages/core/client/src/api-client/APIClient.ts @@ -42,7 +42,7 @@ export class APIClient extends APIClientSDK { if (redirectTo) { return (window.location.href = redirectTo); } - if (error.response.data.type === 'application/json') { + if (error?.response?.data?.type === 'application/json') { handleErrorMessage(error); } else { notification.error({ diff --git a/packages/core/client/src/block-provider/BlockProvider.tsx b/packages/core/client/src/block-provider/BlockProvider.tsx index 4c9c90188..69925b641 100644 --- a/packages/core/client/src/block-provider/BlockProvider.tsx +++ b/packages/core/client/src/block-provider/BlockProvider.tsx @@ -16,6 +16,7 @@ import { WithoutTableFieldResource, } from '../'; import { CollectionProvider, useCollection, useCollectionManager } from '../collection-manager'; +import { FilterBlockRecord } from '../filter-provider/FilterProvider'; import { useRecordIndex } from '../record-provider'; import { SharedFilterProvider } from './SharedFilterProvider'; @@ -34,7 +35,7 @@ interface UseResourceProps { block?: any; } -const useAssociation = (props) => { +export const useAssociation = (props) => { const { association } = props; const { getCollectionField } = useCollectionManager(); if (typeof association === 'string') { @@ -191,6 +192,7 @@ export const RenderChildrenWithAssociationFilter: React.FC = (props) => { width: 200px; flex: 0 0 auto; `} + style={props.associationFilterStyle} > { - + + {props.children} + diff --git a/packages/core/client/src/block-provider/DetailsBlockProvider.tsx b/packages/core/client/src/block-provider/DetailsBlockProvider.tsx index 6367d70c6..77c18643a 100644 --- a/packages/core/client/src/block-provider/DetailsBlockProvider.tsx +++ b/packages/core/client/src/block-provider/DetailsBlockProvider.tsx @@ -53,7 +53,9 @@ export const useDetailsBlockProps = () => { const ctx = useDetailsBlockContext(); useEffect(() => { if (!ctx.service.loading) { - ctx.form.setValues(ctx.service?.data?.data?.[0] || {}); + ctx.form.reset().then(() => { + ctx.form.setValues(ctx.service?.data?.data?.[0] || {}); + }); } }, [ctx.service.loading]); return { diff --git a/packages/core/client/src/block-provider/FormFieldProvider.tsx b/packages/core/client/src/block-provider/FormFieldProvider.tsx index 0215a14f1..bdb1fec57 100644 --- a/packages/core/client/src/block-provider/FormFieldProvider.tsx +++ b/packages/core/client/src/block-provider/FormFieldProvider.tsx @@ -12,11 +12,11 @@ export const FormFieldContext = createContext({}); 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( @@ -37,8 +37,6 @@ const InternalFormFieldProvider = (props) => { return ; } - console.log('InternalFormFieldProvider', fieldName); - return ( { {props.children} - + ); } export const WithoutFormFieldResource = createContext(null); export const FormFieldProvider = (props) => { - console.log('FormFieldProvider', props); return ( @@ -84,4 +81,4 @@ export const useFormFieldProps = () => { form: ctx.form, }; -} \ No newline at end of file +} diff --git a/packages/core/client/src/block-provider/TableBlockProvider.tsx b/packages/core/client/src/block-provider/TableBlockProvider.tsx index a593ebd69..60a30f261 100644 --- a/packages/core/client/src/block-provider/TableBlockProvider.tsx +++ b/packages/core/client/src/block-provider/TableBlockProvider.tsx @@ -3,19 +3,27 @@ import { FormContext, Schema, useField, useFieldSchema } from '@formily/react'; import uniq from 'lodash/uniq'; import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { useCollectionManager } from '../collection-manager'; -import { SchemaComponentOptions, useFixedSchema } from '../schema-component'; +import { SchemaComponentOptions, useFixedSchema, removeNullCondition } from '../schema-component'; import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider'; +import { useFilterBlock } from '../filter-provider/FilterProvider'; +import { findFilterTargets } from './hooks'; +import { mergeFilter } from './SharedFilterProvider'; export const TableBlockContext = createContext({}); -const InternalTableBlockProvider = (props) => { +interface Props { + params?: any; + showIndex?: boolean; + dragSort?: boolean; + rowKey?: string; + childrenColumnName: any; +} + +const InternalTableBlockProvider = (props: Props) => { const { params, showIndex, dragSort, rowKey, childrenColumnName } = props; const field = useField(); const { resource, service } = useBlockRequestContext(); const [expandFlag, setExpandFlag] = useState(false); - // if (service.loading) { - // return ; - // } useFixedSchema(); return ( { const fieldSchema = useFieldSchema(); const { getCollection, getCollectionField } = useCollectionManager(); const collection = getCollection(props.collection); - const { treeTable } = fieldSchema?.['x-decorator-props']||{}; + const { treeTable } = fieldSchema?.['x-decorator-props'] || {}; if (props.dragSort) { params['sort'] = ['sort']; } @@ -104,7 +112,7 @@ export const TableBlockProvider = (props) => { params['tree'] = true; } } else { - const f = collection.fields.find(f => f.treeChildren); + const f = collection.fields.find((f) => f.treeChildren); if (f) { childrenColumnName = f.name; } @@ -135,6 +143,8 @@ export const useTableBlockProps = () => { const fieldSchema = useFieldSchema(); const ctx = useTableBlockContext(); const globalSort = fieldSchema.parent?.['x-decorator-props']?.['params']?.['sort']; + const { getDataBlocks } = useFilterBlock(); + useEffect(() => { if (!ctx?.service?.loading) { field.value = ctx?.service?.data?.data; @@ -179,5 +189,53 @@ export const useTableBlockProps = () => { : globalSort || ctx.service.params?.[0]?.sort; ctx.service.run({ ...ctx.service.params?.[0], page: current, pageSize, sort }); }, + onClickRow(record, setSelectedRow, selectedRow) { + const { targets, uid } = findFilterTargets(fieldSchema); + + // 如果是之前创建的区块是没有 x-filter-targets 属性的,所以这里需要判断一下避免报错 + if (!targets || !targets.length) return; + + const value = [record[ctx.rowKey]]; + + getDataBlocks().forEach((block) => { + const target = targets.find((target) => target.uid === block.uid); + if (!target) return; + + const param = block.service.params?.[0] || {}; + // 保留原有的 filter + const storedFilter = block.service.params?.[1]?.filters || {}; + + if (selectedRow.includes(record[ctx.rowKey])) { + delete storedFilter[uid]; + } else { + storedFilter[uid] = { + $and: [ + { + [target.field || ctx.rowKey]: { + [target.field ? '$in' : '$eq']: value, + }, + }, + ], + }; + } + + const mergedFilter = mergeFilter([ + ...Object.values(storedFilter).map((filter) => removeNullCondition(filter)), + block.defaultFilter, + ]); + + return block.doFilter( + { + ...param, + page: 1, + filter: mergedFilter, + }, + { filters: storedFilter }, + ); + }); + + // 更新表格的选中状态 + setSelectedRow((prev) => (prev?.includes(record[ctx.rowKey]) ? [] : [...value])); + }, }; }; diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index 2e5b73fe5..fd8a848c1 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -4,19 +4,22 @@ import parse from 'json-templates'; import { cloneDeep } from 'lodash'; import get from 'lodash/get'; import omit from 'lodash/omit'; -import { useContext } from 'react'; +import { ChangeEvent, useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; import { useReactToPrint } from 'react-to-print'; -import { useFormBlockContext, useTableBlockContext } from '../..'; -import { useAPIClient } from '../../api-client'; +import { AssociationFilter, useFormBlockContext, useTableBlockContext } from '../..'; +import { useAPIClient, useRequest } from '../../api-client'; import { useCollection } from '../../collection-manager'; +import { useFilterBlock } from '../../filter-provider/FilterProvider'; +import { transformToFilter } from '../../filter-provider/utils'; import { useRecord } from '../../record-provider'; -import { useActionContext, useCompile } from '../../schema-component'; +import { removeNullCondition, useActionContext, useCompile } from '../../schema-component'; import { BulkEditFormItemValueType } from '../../schema-initializer/components'; import { useCurrentUserContext } from '../../user'; import { useBlockRequestContext, useFilterByTk } from '../BlockProvider'; import { useDetailsBlockContext } from '../DetailsBlockProvider'; +import { mergeFilter } from '../SharedFilterProvider'; import { TableFieldResource } from '../TableFieldProvider'; export const usePickActionProps = () => { @@ -199,6 +202,134 @@ export const useCreateActionProps = () => { }; }; +interface FilterTarget { + targets?: { + /** field uid */ + uid: string; + /** associated fields */ + field?: string; + }[]; + uid?: string; +} + +export const findFilterTargets = (fieldSchema): FilterTarget => { + while (fieldSchema) { + if (fieldSchema['x-filter-targets']) { + return { + targets: fieldSchema['x-filter-targets'], + uid: fieldSchema['x-uid'], + }; + } + fieldSchema = fieldSchema.parent; + } + return {}; +}; + +export const updateFilterTargets = (fieldSchema, targets: FilterTarget['targets']) => { + while (fieldSchema) { + if (fieldSchema['x-filter-targets']) { + fieldSchema['x-filter-targets'] = targets; + return; + } + fieldSchema = fieldSchema.parent; + } +}; + +export const useFilterBlockActionProps = () => { + const form = useForm(); + const actionField = useField(); + const fieldSchema = useFieldSchema(); + const { getDataBlocks } = useFilterBlock(); + + actionField.data = actionField.data || {}; + + return { + async onClick() { + const { targets = [], uid } = findFilterTargets(fieldSchema); + + actionField.data.loading = true; + try { + // 收集 filter 的值 + await Promise.all( + getDataBlocks().map(async (block) => { + const target = targets.find((target) => target.uid === block.uid); + if (!target) return; + + const param = block.service.params?.[0] || {}; + // 保留原有的 filter + const storedFilter = block.service.params?.[1]?.filters || {}; + + storedFilter[uid] = removeNullCondition(transformToFilter(form.values, fieldSchema)); + + const mergedFilter = mergeFilter([ + ...Object.values(storedFilter).map((filter) => removeNullCondition(filter)), + block.defaultFilter, + ]); + + return block.doFilter( + { + ...param, + page: 1, + filter: mergedFilter, + }, + { filters: storedFilter }, + ); + }), + ); + actionField.data.loading = false; + } catch (error) { + actionField.data.loading = false; + } + }, + }; +}; + +export const useResetBlockActionProps = () => { + const form = useForm(); + const actionField = useField(); + const fieldSchema = useFieldSchema(); + const { getDataBlocks } = useFilterBlock(); + + actionField.data = actionField.data || {}; + + return { + async onClick() { + const { targets, uid } = findFilterTargets(fieldSchema); + + form.reset(); + actionField.data.loading = true; + try { + // 收集 filter 的值 + await Promise.all( + getDataBlocks().map(async (block) => { + const target = targets.find((target) => target.uid === block.uid); + if (!target) return; + + const param = block.service.params?.[0] || {}; + // 保留原有的 filter + const storedFilter = block.service.params?.[1]?.filters || {}; + + delete storedFilter[uid]; + const mergedFilter = mergeFilter([...Object.values(storedFilter), block.defaultFilter]); + + return block.doFilter( + { + ...param, + page: 1, + filter: mergedFilter, + }, + { filters: storedFilter }, + ); + }), + ); + actionField.data.loading = false; + } catch (error) { + actionField.data.loading = false; + } + }, + }; +}; + export const useCustomizeUpdateActionProps = () => { const { resource, __parent, service } = useBlockRequestContext(); const filterByTk = useFilterByTk(); @@ -663,3 +794,188 @@ export const useDetailsPaginationProps = () => { }, }; }; + +export const useAssociationFilterProps = () => { + const collectionField = AssociationFilter.useAssociationField(); + const { service, props: blockProps } = useBlockRequestContext(); + const fieldSchema = useFieldSchema(); + const valueKey = collectionField?.targetKey || 'id'; + const labelKey = fieldSchema['x-component-props']?.fieldNames?.label || valueKey; + const collectionFieldName = collectionField.name; + const { data, params, run } = useRequest( + { + resource: collectionField.target, + action: 'list', + params: { + fields: [labelKey, valueKey], + pageSize: 200, + page: 1, + }, + }, + { + refreshDeps: [labelKey, valueKey], + debounceWait: 300, + }, + ); + + const list = data?.data || []; + const onSelected = (value) => { + const filters = service.params?.[1]?.filters || {}; + + if (value.length) { + filters[`af.${collectionFieldName}`] = { + [`${collectionFieldName}.${valueKey}.$in`]: value, + }; + } else { + delete filters[`af.${collectionFieldName}`]; + } + + service.run( + { + ...service.params?.[0], + pageSize: 200, + page: 1, + filter: mergeFilter([...Object.values(filters), blockProps?.params?.filter]), + }, + { filters }, + ); + }; + const handleSearchInput = (e: ChangeEvent) => { + run({ + ...params?.[0], + filter: { + [`${labelKey}.$includes`]: e.target.value, + }, + }); + }; + + return { + /** 渲染 Collapse 的列表数据 */ + list, + onSelected, + handleSearchInput, + params, + run, + }; +}; + +export const useOptionalFieldList = () => { + const { currentFields = [] } = useCollection(); + + return currentFields.filter((field) => isOptionalField(field) && field.uiSchema.enum); +}; + +const isOptionalField = (field) => { + const optionalInterfaces = ['select', 'multipleSelect', 'checkbox', 'checkboxGroup', 'chinaRegion']; + return optionalInterfaces.includes(field.interface); +}; + +export const useAssociationFilterBlockProps = () => { + const collectionField = AssociationFilter.useAssociationField(); + const fieldSchema = useFieldSchema(); + const optionalFieldList = useOptionalFieldList(); + const { getDataBlocks } = useFilterBlock(); + const collectionFieldName = collectionField.name; + + let list, onSelected, handleSearchInput, params, run, data, valueKey, labelKey, filterKey; + + if (isOptionalField(fieldSchema)) { + const field = optionalFieldList.find((field) => field.name === fieldSchema.name); + const operatorMap = { + select: '$in', + multipleSelect: '$anyOf', + checkbox: '$in', + checkboxGroup: '$anyOf', + }; + const _list = field?.uiSchema?.enum || []; + valueKey = 'value'; + labelKey = 'label'; + list = _list; + params = {}; + run = () => {}; + filterKey = `${field.name}.${operatorMap[field.interface]}`; + handleSearchInput = (e) => { + // TODO: 列表没有刷新,在这个 hook 中使用 useState 会产生 re-render 次数过多的错误 + const value = e.target.value; + if (!value) { + list = _list; + return; + } + list = (_list as any[]).filter((item) => item.label.includes(value)); + }; + } else { + valueKey = collectionField?.targetKey || 'id'; + labelKey = fieldSchema['x-component-props']?.fieldNames?.label || valueKey; + ({ data, params, run } = useRequest( + { + resource: collectionField.target, + action: 'list', + params: { + fields: [labelKey, valueKey], + pageSize: 200, + page: 1, + }, + }, + { + refreshDeps: [labelKey, valueKey], + debounceWait: 300, + }, + )); + filterKey = `${collectionFieldName}.${valueKey}.$in`; + + list = data?.data || []; + + handleSearchInput = (e: ChangeEvent) => { + run({ + ...params?.[0], + filter: { + [`${labelKey}.$includes`]: e.target.value, + }, + }); + }; + } + + onSelected = (value) => { + const { targets, uid } = findFilterTargets(fieldSchema); + + getDataBlocks().forEach((block) => { + const target = targets.find((target) => target.uid === block.uid); + if (!target) return; + + const key = `${uid}${fieldSchema.name}`; + const param = block.service.params?.[0] || {}; + // 保留原有的 filter + const storedFilter = block.service.params?.[1]?.filters || {}; + + if (value.length) { + storedFilter[key] = { + [filterKey]: value, + }; + } else { + delete storedFilter[key]; + } + + const mergedFilter = mergeFilter([...Object.values(storedFilter), block.defaultFilter]); + + return block.doFilter( + { + ...param, + page: 1, + filter: mergedFilter, + }, + { filters: storedFilter }, + ); + }); + }; + + return { + /** 渲染 Collapse 的列表数据 */ + list, + onSelected, + handleSearchInput, + params, + run, + valueKey, + labelKey, + }; +}; diff --git a/packages/core/client/src/collection-manager/hooks/useCollection.ts b/packages/core/client/src/collection-manager/hooks/useCollection.ts index b8cee4244..15699b77b 100644 --- a/packages/core/client/src/collection-manager/hooks/useCollection.ts +++ b/packages/core/client/src/collection-manager/hooks/useCollection.ts @@ -6,6 +6,8 @@ import { CollectionContext } from '../context'; import { CollectionFieldOptions } from '../types'; import { useCollectionManager } from './useCollectionManager'; +export type Collection = ReturnType; + export const useCollection = () => { const collection = useContext(CollectionContext); const api = useAPIClient(); diff --git a/packages/core/client/src/filter-provider/FilterProvider.tsx b/packages/core/client/src/filter-provider/FilterProvider.tsx new file mode 100644 index 000000000..a03e56e11 --- /dev/null +++ b/packages/core/client/src/filter-provider/FilterProvider.tsx @@ -0,0 +1,121 @@ +import { useField, useFieldSchema } from '@formily/react'; +import React, { createContext, useEffect, useRef } from 'react'; +import { useBlockRequestContext } from '../block-provider'; +import { SharedFilter } from '../block-provider/SharedFilterProvider'; +import { CollectionFieldOptions, useCollection } from '../collection-manager'; +import { useAssociatedFields } from './utils'; + +type Collection = ReturnType; + +export interface DataBlock { + /** 唯一标识符,schema 中的 name 值 */ + uid: string; + /** 用户自行设置的区块名称 */ + title?: string; + /** 与当前区块相关的数据表信息 */ + collection: Collection; + /** 根据提供的参数执行该方法即可刷新数据区块的数据 */ + doFilter: (params: any, params2?: any) => Promise; + /** 数据区块表中所有的关系字段 */ + associatedFields?: CollectionFieldOptions[]; + /** 通过右上角菜单设置的过滤条件 */ + defaultFilter?: SharedFilter; + service?: any; + /** 区块所对应的 DOM 容器 */ + dom: HTMLElement; +} + +interface FilterContextValue { + dataBlocks: DataBlock[]; + setDataBlocks: React.Dispatch>; +} + +const FilterContext = createContext(null); + +/** + * 主要用于记录当前页面中的数据区块的信息,用于在过滤区块中使用 + * @param props + * @returns + */ +export const FilterBlockProvider: React.FC = ({ children }) => { + const [dataBlocks, setDataBlocks] = React.useState([]); + return {children}; +}; + +export const FilterBlockRecord = ({ + children, + params, +}: { + children: React.ReactNode; + params?: { filter: SharedFilter }; +}) => { + const collection = useCollection(); + const { recordDataBlocks, removeDataBlock } = useFilterBlock(); + const { service } = useBlockRequestContext(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const associatedFields = useAssociatedFields(); + const container = useRef(null); + + const shouldApplyFilter = field.decoratorType !== 'FormBlockProvider' && field.decoratorProps.blockType !== 'filter'; + + const addBlockToDataBlocks = () => { + recordDataBlocks({ + uid: fieldSchema['x-uid'], + title: field.componentProps.title, + doFilter: service.runAsync, + collection, + associatedFields, + defaultFilter: params?.filter || {}, + service, + dom: container.current, + }); + }; + + useEffect(() => { + if (shouldApplyFilter) addBlockToDataBlocks(); + }, [params?.filter, service]); + + useEffect(() => { + return () => { + removeDataBlock(field.props.name as string); + }; + }, []); + + return
{children}
; +}; + +/** + * 返回一些方法用于收集和获取当前页面中的数据区块的信息 + * @returns + */ +export const useFilterBlock = () => { + const ctx = React.useContext(FilterContext); + // 有可能存在页面没有提供 FilterBlockProvider 的情况,比如内部使用的数据表管理页面 + if (!ctx) { + return { + recordDataBlocks: () => {}, + getDataBlocks: () => [] as DataBlock[], + removeDataBlock: () => {}, + }; + } + const { dataBlocks, setDataBlocks } = ctx; + const recordDataBlocks = (block: DataBlock) => { + const existingBlock = dataBlocks.find((item) => item.uid === block.uid); + + if (existingBlock) { + // 这里的值有可能会变化,所以需要更新 + existingBlock.service = block.service; + existingBlock.defaultFilter = block.defaultFilter; + return; + } + + setDataBlocks((prev) => [...prev, block]); + }; + const getDataBlocks = () => dataBlocks; + const removeDataBlock = (name: string) => { + setDataBlocks((prev) => prev.filter((item) => item.uid !== name)); + }; + + return { recordDataBlocks, getDataBlocks, removeDataBlock }; +}; diff --git a/packages/core/client/src/filter-provider/__tests__/useFilter.tsx b/packages/core/client/src/filter-provider/__tests__/useFilter.tsx new file mode 100644 index 000000000..7d085c6fc --- /dev/null +++ b/packages/core/client/src/filter-provider/__tests__/useFilter.tsx @@ -0,0 +1,54 @@ +import { render } from '@testing-library/react'; +import React from 'react'; +import { FilterBlockProvider, useFilterBlock } from '../FilterProvider'; + +describe('useFilter', () => { + test('should get a empty array', () => { + let getDataBlocks = null; + const Comp = () => { + ({ getDataBlocks } = useFilterBlock()); + return null; + }; + const App = () => { + return ( + + + + ); + }; + render(); + expect(getDataBlocks()).toEqual([]); + }); + + test('should not repeat', () => { + let getDataBlocks = null, + recordDataBlocks = null; + const Comp = () => { + ({ getDataBlocks, recordDataBlocks } = useFilterBlock()); + return null; + }; + const App = () => { + return ( + + + + ); + }; + render(); + + recordDataBlocks({ + name: 'test', + collection: {}, + doFilter: () => {}, + }); + expect(getDataBlocks().length).toBe(1); + + // avoid repeat + recordDataBlocks({ + name: 'test', + collection: {}, + doFilter: () => {}, + }); + expect(getDataBlocks().length).toBe(1); + }); +}); diff --git a/packages/core/client/src/filter-provider/utils.ts b/packages/core/client/src/filter-provider/utils.ts new file mode 100644 index 000000000..71e1836a0 --- /dev/null +++ b/packages/core/client/src/filter-provider/utils.ts @@ -0,0 +1,89 @@ +import { Schema, useFieldSchema } from '@formily/react'; +import { isPlainObject, isEmpty } from '@nocobase/utils/client'; +import { Collection, FieldOptions, useCollection } from '../collection-manager'; +import { findFilterOperators } from '../schema-component/antd/form-item/SchemaSettingOptions'; +import { useFilterBlock } from './FilterProvider'; + +export enum FilterBlockType { + FORM, + TABLE, + TREE, + COLLAPSE, +} + +/** + * 根据筛选区块类型筛选出支持的数据区块(同表或具有关系字段的表) + * @param filterBlockType + * @returns + */ +export const useSupportedBlocks = (filterBlockType: FilterBlockType) => { + const { getDataBlocks } = useFilterBlock(); + const fieldSchema = useFieldSchema(); + const collection = useCollection(); + + // Form 和 Collapse 仅支持同表的数据区块 + if (filterBlockType === FilterBlockType.FORM || filterBlockType === FilterBlockType.COLLAPSE) { + return getDataBlocks().filter((block) => { + return isSameCollection(block.collection, collection); + }); + } + + // Table 和 Tree 支持同表或者关系表的数据区块 + if (filterBlockType === FilterBlockType.TABLE || filterBlockType === FilterBlockType.TREE) { + return getDataBlocks().filter((block) => { + return ( + fieldSchema['x-uid'] !== block.uid && + (isSameCollection(block.collection, collection) || + block.associatedFields.some((field) => field.target === collection.name)) + ); + }); + } +}; + +/** + * Recursively flatten an object and generate a query object. + * @param result - The resulting query object. + * @param obj - The object to be flattened and queried. + * @param key - The current key in the object tree. + * @param operators - The operators to use for each field. + * @returns The resulting query object. + */ +const flattenAndQueryObject = (result: Record, obj: any, key: string, operators: Record) => { + if (!obj) return result; + + if (!isPlainObject(obj)) { + result[key] = { + [operators[key] || '$eq']: obj, + }; + } else { + Object.keys(obj).forEach((k) => { + flattenAndQueryObject(result, obj[k], `${key}.${k}`, operators); + }); + } + + return result; +}; + +export const transformToFilter = (values: Record, fieldSchema: Schema) => { + const { operators } = findFilterOperators(fieldSchema); + + return { + $and: Object.keys(values) + .map((key) => flattenAndQueryObject({}, values[key], key, operators)) + .filter((item) => !isEmpty(item)), + }; +}; + +export const useAssociatedFields = () => { + const { fields } = useCollection(); + + return fields.filter((field) => isAssocField(field)) || []; +}; + +export const isAssocField = (field?: FieldOptions) => { + return ['o2o', 'oho', 'obo', 'm2o', 'createdBy', 'updatedBy', 'o2m', 'm2m', 'linkTo'].includes(field?.interface); +}; + +export const isSameCollection = (c1: Collection, c2: Collection) => { + return c1.name === c2.name; +}; diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts index 768902694..ae173a8e7 100644 --- a/packages/core/client/src/locale/en_US.ts +++ b/packages/core/client/src/locale/en_US.ts @@ -15,6 +15,7 @@ export default { "Time": "Time", "Event": "Event", "None": "None", + "Unconnected": "Unconnected", "System settings": "System settings", "System title": "System title", "Logo": "Logo", @@ -73,8 +74,10 @@ export default { "Close": "Close", "Set the data scope": "Set the data scope", "Data blocks": "Data blocks", + "Filter blocks": "Filter blocks", "Table": "Table", "Form": "Form", + "Collapse": "Collapse", "Select data source": "Select data source", "Calendar": "Calendar", "Delete events": "Delete events", @@ -127,6 +130,7 @@ export default { "Are you sure you want to delete it?": "Are you sure you want to delete it?", "This is a demo text, **supports Markdown syntax**.": "This is a demo text, **supports Markdown syntax**.", "Filter": "Filter", + "Connect data blocks": "Connect data blocks", "Action type": "Action type", "Actions": "Actions", "Insert": "Insert", @@ -166,6 +170,7 @@ export default { "Association fields filter": "Association fields filter", "PK & FK fields": "PK & FK fields", "Association fields": "Association fields", + "Optional fields": "Optional fields", "System fields": "System fields", "General fields": "General fields", "Parent collection fields": "Parent collection fields", @@ -244,6 +249,7 @@ export default { "Edit block title": "Edit block title", "Block title": "Block title", "Pattern": "Pattern", + "Operator": "Operator", "Editable": "Editable", "Readonly": "Readonly", "Easy-reading": "Easy-reading", @@ -414,6 +420,7 @@ export default { "Convert reference to duplicate": "Convert reference to duplicate", "Template name": "Template name", "Block type": "Block type", + "No blocks to connect": "No blocks to connect", "Action column": "Action column", "Records per page": "Records per page", "(Fields only)": "(Fields only)", diff --git a/packages/core/client/src/locale/ja_JP.ts b/packages/core/client/src/locale/ja_JP.ts index f5123cf84..263578101 100644 --- a/packages/core/client/src/locale/ja_JP.ts +++ b/packages/core/client/src/locale/ja_JP.ts @@ -15,6 +15,7 @@ export default { "Time": "時間", "Event": "イベント", "None": "なし", + "Unconnected": "未接続", "System settings": "システム設定", "System title": "システム名", "Logo": "ロゴ", @@ -77,8 +78,10 @@ export default { "Close": "閉じる", "Set the data scope": "データ範囲の設定", "Data blocks": "データブロック", + "Filter blocks": "フィルターブロック", "Table": "テーブル", "Form": "フォーム", + "Collapse": "折りたたみ", "Select data source": "データソースを選択", "Calendar": "カレンダー", "Kanban": "かんばん", @@ -120,6 +123,7 @@ export default { "Are you sure you want to delete it?": "本当に削除しますか?", "This is a demo text, **supports Markdown syntax**.": "これはデモテキストです。 **マークダウン構文をサポートしています。**", "Filter": "フィルター", + "Connect data blocks": "データブロックを連結", "Action type": "操作タイプ", "Actions": "操作", "Insert": "作成", @@ -346,6 +350,7 @@ export default { "Convert reference to duplicate": "参照を複製に変換", "Template name": "テンプレート名", "Block type": "ブロックタイプ", + "No blocks to connect": "接続するブロックがありません", "Action column": "操作カラム", "Records per page": "ページごとのレコード数", "(Fields only)": "(フィールドのみ)", @@ -531,6 +536,7 @@ export default { "Enable SMS authentication": "SMS認証を有効にする", "Display association fields": "関連付けられたコレクションのフィールドを表示", "Set default value": "デフォルト値を設定", + "Optional fields": "オプションフィールド", "Editable": "編集可能", "Readonly": "読み取り専用(編集不可)", "Easy-reading": "読取り専用(読取りモード)", diff --git a/packages/core/client/src/locale/ru_RU.ts b/packages/core/client/src/locale/ru_RU.ts index d1e342d18..e3a105844 100644 --- a/packages/core/client/src/locale/ru_RU.ts +++ b/packages/core/client/src/locale/ru_RU.ts @@ -15,6 +15,7 @@ export default { "Time": "Время", "Event": "Событие", "None": "Ничего", + "Unconnected": "Не подключен", "System settings": "Системные настройки", "System title": "Системный заголовок", "Logo": "Логотип", @@ -49,8 +50,10 @@ export default { "Close": "Закрыть", "Set the data scope": "Установить область данных", "Data blocks": "Блоки данных", + "Filter blocks": "Просеивающие блоки", "Table": "Таблица", "Form": "Форма", + "Collapse": "Свернуть", "Select data source": "Выбрать источник данных", "Calendar": "Календарь", "Kanban": "Канбан", @@ -92,6 +95,7 @@ export default { "Are you sure you want to delete it?": "Вы уверены, что хотите удалить это?", "This is a demo text, **supports Markdown syntax**.": "Это демо текст, **поддерживает синтаксис Markdown**.", "Filter": "Фильтр", + "Connect data blocks": "Соединить блоки данных", "Action type": "Тип действия", "Actions": "Действия", "Insert": "Вставить", @@ -307,6 +311,7 @@ export default { "Convert reference to duplicate": "Преобразовать ссылку в дубликат", "Template name": "Имя Шаблона", "Block type": "Тип Блока", + "No blocks to connect": "Нет Блоков для подключения", "Action column": "Колонка действий", "Records per page": "Записей на страницу", "(Fields only)": "(Только поля)", diff --git a/packages/core/client/src/locale/tr_TR.ts b/packages/core/client/src/locale/tr_TR.ts index 7b1216a9f..dfc0ba857 100644 --- a/packages/core/client/src/locale/tr_TR.ts +++ b/packages/core/client/src/locale/tr_TR.ts @@ -15,6 +15,7 @@ export default { "Time": "Saat", "Event": "Olay", "None": "Boş", + "Unconnected": "Bağlantı yok", "System settings": "Sistem ayarları", "System title": "Sistem başlığı", "Logo": "Logo", @@ -49,8 +50,10 @@ export default { "Close": "Kapat", "Set the data scope": "Veri kapsamını ayarla", "Data blocks": "Veri Blokları", + "Filter blocks": "Filtre blokları", "Table": "Tablo", "Form": "Form", + "Collapse": "Daralt", "Select data source": "Veri kaynağını seç", "Calendar": "Takvim", "Kanban": "Kanban", @@ -92,6 +95,7 @@ export default { "Are you sure you want to delete it?": "Silmek istediğinizden emin misiniz?", "This is a demo text, **supports Markdown syntax**.": "Bu bir örnek yazıdır, **işaretleme yazısı destekleniyor**.", "Filter": "Filtre", + "Connect data blocks": "Veri bloklarını bağla", "Action type": "İşlem Türü", "Actions": "İşlemler", "Insert": "Ekle", @@ -306,6 +310,7 @@ export default { "Convert reference to duplicate": "Referansı kopyaya dönüştür", "Template name": "Şablon adı", "Block type": "Blok türü", + "No blocks to connect": "Bağlanacak blok yok", "Action column": "İşlem sütunu", "Records per page": "Sayfa başına kayıt", "(Fields only)": "(Sadece alanlar)", @@ -461,6 +466,7 @@ export default { "Use the same time zone (GMT) for all users": "Tüm kullanıcılar için aynı saat dilimini (GMT) kullanın", "Block title": "Blok başlığı", "Edit block title": "Blok başlığını düzenle", + "operater": "operatör", "Province/city/area name": "Semt/şehir/bölge adı", "Field component": "Alan bileşeni", "Subtable": "Alttablo", diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 5d17f070e..94bde6e7c 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -15,6 +15,7 @@ export default { "Time": "时间", "Event": "事件", "None": "无", + "Unconnected": "未连接", "System settings": "系统设置", "System title": "系统名称", "Logo": "Logo", @@ -83,8 +84,10 @@ export default { "Close": "关闭", "Set the data scope": "设置数据范围", "Data blocks": "数据区块", + "Filter blocks": "筛选区块", "Table": "表格", "Form": "表单", + "Collapse": "折叠面板", "Select data source": "选择数据源", "Calendar": "日历", 'Delete events': '删除日程', @@ -133,6 +136,7 @@ export default { "This is a demo text, **supports Markdown syntax**.": "这是一段演示文本,**支持 Markdown 语法**。", "Filter": "筛选", + "Connect data blocks": "连接数据区块", "Action type": "操作类型", "Actions": "操作", "Insert": "新增", @@ -175,6 +179,7 @@ export default { "Association fields filter": "关系筛选", "PK & FK fields": "主外键字段", "Association fields": "关系字段", + "Optional fields": "可选字段", "System fields": "系统字段", "General fields": "普通字段", "Parent collection fields": "父表字段", @@ -260,6 +265,7 @@ export default { "Edit block title": "编辑区块标题", "Block title": "区块标题", "Pattern": "模式", + "Operator": "运算符", "Editable": "可编辑", "Readonly": "只读(禁止编辑)", "Easy-reading": "只读(阅读模式)", @@ -446,6 +452,7 @@ export default { "Convert reference to duplicate": "模板引用转为复制", "Template name": "模板名称", "Block type": "区块类型", + "No blocks to connect": "没有可连接的区块", "Action column": "操作列", "Records per page": "每页显示数量", "(Fields only)": "(仅字段)", diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.BlockDesigner.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.BlockDesigner.tsx new file mode 100644 index 000000000..36a75475c --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.BlockDesigner.tsx @@ -0,0 +1,30 @@ +import { useFieldSchema } from '@formily/react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useCollection } from '../../../collection-manager'; +import { FilterBlockType } from '../../../filter-provider/utils'; +import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; +import { useSchemaTemplate } from '../../../schema-templates'; + +export const AssociationFilterBlockDesigner = () => { + const { name, title } = useCollection(); + const template = useSchemaTemplate(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + + return ( + + + + + + + + ); +}; diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.FilterBlockInitializer.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.FilterBlockInitializer.tsx new file mode 100644 index 000000000..7dc4a4242 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.FilterBlockInitializer.tsx @@ -0,0 +1,90 @@ +import { css } from '@emotion/css'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useOptionalFieldList } from '../../../block-provider/hooks'; +import { useAssociatedFields } from '../../../filter-provider/utils'; +import { SchemaInitializer, SchemaInitializerItemOptions } from '../../../schema-initializer'; + +export const AssociationFilterFilterBlockInitializer = () => { + const { t } = useTranslation(); + const associatedFields = useAssociatedFields(); + const optionalList = useOptionalFieldList(); + const useProps = '{{useAssociationFilterBlockProps}}'; + const children: SchemaInitializerItemOptions[] = associatedFields.map((field) => ({ + type: 'item', + key: field.key, + title: field.uiSchema?.title, + component: 'AssociationFilterDesignerDisplayField', + schema: { + name: field.name, + title: field.uiSchema?.title, + type: 'void', + 'x-designer': 'AssociationFilter.Item.Designer', + 'x-component': 'AssociationFilter.Item', + 'x-component-props': { + fieldNames: { + label: field.targetKey || 'id', + }, + useProps, + }, + properties: {}, + }, + })); + const optionalChildren: SchemaInitializerItemOptions[] = optionalList.map((field) => ({ + type: 'item', + key: field.key, + title: field.uiSchema.title, + component: 'AssociationFilterDesignerDisplayField', + schema: { + name: field.name, + title: field.uiSchema.title, + interface: field.interface, + type: 'void', + 'x-designer': 'AssociationFilter.Item.Designer', + 'x-component': 'AssociationFilter.Item', + 'x-component-props': { + fieldNames: { + label: field.name, + }, + useProps, + }, + properties: {}, + }, + })); + + const associatedFieldGroup: SchemaInitializerItemOptions = { + type: 'itemGroup', + title: t('Association fields'), + children, + }; + + // 可选项字段 + const optionalFieldGroup: SchemaInitializerItemOptions = { + type: 'itemGroup', + title: t('Optional fields'), + children: optionalChildren, + }; + + const dividerItem: SchemaInitializerItemOptions = { + type: 'divider', + }; + + const deleteItem: SchemaInitializerItemOptions = { + type: 'item', + title: t('Delete'), + component: 'AssociationFilterDesignerDelete', + }; + + const items = [associatedFieldGroup, optionalFieldGroup]; + + return ( + + ); +}; diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Initializer.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Initializer.tsx index 634dd263c..72d331f5b 100644 --- a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Initializer.tsx +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Initializer.tsx @@ -1,18 +1,14 @@ import { css } from '@emotion/css'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useCollection } from '../../../collection-manager'; +import { useAssociatedFields } from '../../../filter-provider/utils'; import { SchemaInitializer, SchemaInitializerItemOptions } from '../../../schema-initializer'; export const AssociationFilterInitializer = () => { const { t } = useTranslation(); - const { fields } = useCollection(); - - const associatedFields = fields.filter((field) => - ['o2o', 'oho', 'obo', 'm2o', 'createdBy', 'updatedBy', 'o2m', 'm2m', 'linkTo'].includes(field.interface), - ); - - const items: SchemaInitializerItemOptions[] = associatedFields.map((field) => ({ + const associatedFields = useAssociatedFields(); + const useProps = '{{useAssociationFilterProps}}'; + const children: SchemaInitializerItemOptions[] = associatedFields.map((field) => ({ type: 'item', key: field.key, title: field.uiSchema?.title, @@ -27,6 +23,7 @@ export const AssociationFilterInitializer = () => { fieldNames: { label: field.targetKey || 'id', }, + useProps, }, properties: {}, }, @@ -35,7 +32,7 @@ export const AssociationFilterInitializer = () => { const associatedFieldGroup: SchemaInitializerItemOptions = { type: 'itemGroup', title: t('Association fields'), - children: items, + children, }; const dividerItem: SchemaInitializerItemOptions = { @@ -48,6 +45,8 @@ export const AssociationFilterInitializer = () => { component: 'AssociationFilterDesignerDelete', }; + const items = [associatedFieldGroup, dividerItem, deleteItem]; + return ( { `} icon={'SettingOutlined'} title={t('Configure fields')} - items={[associatedFieldGroup, dividerItem, deleteItem]} + items={items} /> ); }; diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.tsx index 6065f04f2..dc24114bb 100644 --- a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.tsx +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.tsx @@ -4,9 +4,7 @@ import { useFieldSchema } from '@formily/react'; import { Col, Collapse, Input, Row, Tree } from 'antd'; import cls from 'classnames'; import React, { ChangeEvent, MouseEvent, useState } from 'react'; -import { useRequest } from '../../../api-client'; -import { useBlockRequestContext } from '../../../block-provider'; -import { mergeFilter } from '../../../block-provider/SharedFilterProvider'; +import { useAssociationFilterProps } from '../../../block-provider/hooks'; import { SortableItem } from '../../common'; import { useCompile, useDesigner } from '../../hooks'; import { AssociationFilter } from './AssociationFilter'; @@ -16,49 +14,40 @@ const { Panel } = Collapse; export const AssociationFilterItem = (props) => { const collectionField = AssociationFilter.useAssociationField(); - if (!collectionField) { - return null; - } - + // 把一些可定制的状态通过 hook 提取出去了,为了兼容之前添加的 Table 区块,这里加了个默认值 + const { useProps = useAssociationFilterProps } = props; const fieldSchema = useFieldSchema(); const Designer = useDesigner(); const compile = useCompile(); - const { service, props: blockProps } = useBlockRequestContext(); + + const { + list, + onSelected, + handleSearchInput: _handleSearchInput, + params, + run, + valueKey: _valueKey, + labelKey: _labelKey, + } = useProps(); const [searchVisible, setSearchVisible] = useState(false); - const collectionFieldName = collectionField.name; - - const valueKey = collectionField?.targetKey || 'id'; - const labelKey = fieldSchema['x-component-props']?.fieldNames?.label || valueKey; + const valueKey = _valueKey || collectionField?.targetKey || 'id'; + const labelKey = _labelKey || fieldSchema['x-component-props']?.fieldNames?.label || valueKey; const fieldNames = { title: labelKey || valueKey, key: valueKey, }; - const { data, params, loading, run } = useRequest( - { - resource: collectionField.target, - action: 'list', - params: { - fields: [labelKey, valueKey], - pageSize: 200, - page: 1, - }, - }, - { - refreshDeps: [labelKey, valueKey], - debounceWait: 300, - }, - ); - - const treeData = data?.data || []; - const [expandedKeys, setExpandedKeys] = useState([]); const [selectedKeys, setSelectedKeys] = useState([]); const [autoExpandParent, setAutoExpandParent] = useState(true); + if (!collectionField) { + return null; + } + const onExpand = (expandedKeysValue: React.Key[]) => { setExpandedKeys(expandedKeysValue); setAutoExpandParent(false); @@ -66,26 +55,7 @@ export const AssociationFilterItem = (props) => { const onSelect = (selectedKeysValue: React.Key[]) => { setSelectedKeys(selectedKeysValue); - - const filters = service.params?.[1]?.filters || {}; - - if (selectedKeysValue.length) { - filters[`af.${collectionFieldName}`] = { - [`${collectionFieldName}.${valueKey}.$in`]: selectedKeysValue, - }; - } else { - delete filters[`af.${collectionFieldName}`]; - } - - service.run( - { - ...service.params?.[0], - pageSize: 200, - page: 1, - filter: mergeFilter([...Object.values(filters), blockProps?.params?.filter]), - }, - { filters }, - ); + onSelected(selectedKeysValue); }; const handleSearchToggle = (e: MouseEvent) => { @@ -105,12 +75,7 @@ export const AssociationFilterItem = (props) => { }; const handleSearchInput = (e: ChangeEvent) => { - run({ - ...params?.[0], - filter: { - [`${labelKey}.$includes`]: e.target.value, - }, - }); + _handleSearchInput(e); }; const title = fieldSchema.title ?? collectionField.uiSchema?.title; @@ -258,7 +223,7 @@ export const AssociationFilterItem = (props) => { onExpand={onExpand} expandedKeys={expandedKeys} autoExpandParent={autoExpandParent} - treeData={treeData} + treeData={list} onSelect={onSelect} fieldNames={fieldNames} titleRender={(node) => compile(node[labelKey])} diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.tsx index a1c808bd4..ede4d1d2c 100644 --- a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.tsx +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.tsx @@ -6,15 +6,18 @@ import { useCollection } from '../../../collection-manager'; import { useSchemaInitializer } from '../../../schema-initializer'; import { DndContext, SortableItem } from '../../common'; import { useDesigner } from '../../hooks'; +import { AssociationFilterBlockDesigner } from './AssociationFilter.BlockDesigner'; +import { AssociationFilterFilterBlockInitializer } from './AssociationFilter.FilterBlockInitializer'; import { AssociationFilterInitializer } from './AssociationFilter.Initializer'; import { AssociationFilterItem } from './AssociationFilter.Item'; import { AssociationFilterItemDesigner } from './AssociationFilter.Item.Designer'; +import { AssociationFilterProvider } from './AssociationFilterProvider'; export const AssociationFilter = (props) => { const Designer = useDesigner(); const filedSchema = useFieldSchema(); - const { exists, render } = useSchemaInitializer(filedSchema['x-initializer']); + const { render } = useSchemaInitializer(filedSchema['x-initializer']); return ( @@ -75,11 +78,14 @@ export const AssociationFilter = (props) => { ); }; +AssociationFilter.Provider = AssociationFilterProvider; AssociationFilter.Initializer = AssociationFilterInitializer; +AssociationFilter.FilterBlockInitializer = AssociationFilterFilterBlockInitializer; AssociationFilter.Item = AssociationFilterItem as typeof AssociationFilterItem & { Designer: typeof AssociationFilterItemDesigner; }; AssociationFilter.Item.Designer = AssociationFilterItemDesigner; +AssociationFilter.BlockDesigner = AssociationFilterBlockDesigner; AssociationFilter.useAssociationField = () => { const fieldSchema = useFieldSchema(); diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterProvider.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterProvider.tsx new file mode 100644 index 000000000..cf22c69fc --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterProvider.tsx @@ -0,0 +1,2 @@ +// TODO: 因他们之间功能相同,所以先直接复用,后续有需要再拆分 +export { TableBlockProvider as AssociationFilterProvider } from '../../../block-provider'; diff --git a/packages/core/client/src/schema-component/antd/association-filter/utilts.ts b/packages/core/client/src/schema-component/antd/association-filter/utilts.ts new file mode 100644 index 000000000..0950c2d3e --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-filter/utilts.ts @@ -0,0 +1,5 @@ +import { CollectionFieldOptions } from '../../../collection-manager'; + +export const getTargetKey = (field?: CollectionFieldOptions) => { + return field?.targetKey || 'id'; +}; diff --git a/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx b/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx index 000a134ad..516c253de 100644 --- a/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx +++ b/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx @@ -84,6 +84,7 @@ const InternalAssociationSelect = connect( interface AssociationSelectInterface { (props: any): React.ReactElement; Designer: React.FC; + FilterDesigner: React.FC; } export const AssociationSelect = InternalAssociationSelect as unknown as AssociationSelectInterface; @@ -680,4 +681,481 @@ AssociationSelect.Designer = () => { ); }; +/** + * 用于筛选表单区块 + * @returns + */ +AssociationSelect.FilterDesigner = () => { + const { getCollectionFields, getInterface, getCollectionJoinField } = useCollectionManager(); + const { getField } = useCollection(); + const { form } = useFormBlockContext(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const tk = useFilterByTk(); + const {} = useCollection(); + const { dn, refresh, insertAdjacent } = useDesignable(); + const compile = useCompile(); + const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + const fieldComponentOptions = useFieldComponentOptions(); + const isSubFormAssocitionField = field.address.segments.includes('__form_grid'); + const interfaceConfig = getInterface(collectionField?.interface); + const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema); + const originalTitle = collectionField?.uiSchema?.title; + const targetFields = collectionField?.target ? getCollectionFields(collectionField.target) : []; + const initialValue = { + title: field.title === originalTitle ? undefined : field.title, + }; + const sortFields = useSortFields(collectionField?.target); + + const defaultSort = field.componentProps?.service?.params?.sort || []; + const defaultFilter = field.componentProps?.service?.params?.filter || {}; + const dataSource = useCollectionFilterOptions(collectionField?.target); + + const sort = defaultSort?.map((item: string) => { + return item.startsWith('-') + ? { + field: item.substring(1), + direction: 'desc', + } + : { + field: item, + direction: 'asc', + }; + }); + if (!field.readPretty) { + initialValue['required'] = field.required; + } + + const options = targetFields + .filter((field) => !field?.target && field.type !== 'boolean') + .map((field) => ({ + value: field?.name, + label: compile(field?.uiSchema?.title) || field?.name, + })); + + let readOnlyMode = 'editable'; + if (fieldSchema['x-disabled'] === true) { + readOnlyMode = 'readonly'; + } + if (fieldSchema['x-read-pretty'] === true) { + readOnlyMode = 'read-pretty'; + } + + return ( + + {collectionField && ( + { + if (title) { + field.title = title; + fieldSchema.title = title; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + title: fieldSchema.title, + }, + }); + } + dn.refresh(); + }} + /> + )} + {!field.readPretty && ( + { + field.description = description; + fieldSchema.description = description; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + description: fieldSchema.description, + }, + }); + dn.refresh(); + }} + /> + )} + {field.readPretty && ( + { + field.decoratorProps.tooltip = tooltip; + fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {}; + fieldSchema['x-decorator-props']['tooltip'] = tooltip; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + dn.refresh(); + }} + /> + )} + {form && !form?.readPretty && validateSchema && ( + = 3}}', + }, + }, + }, + }, + }, + }, + }, + } as ISchema + } + onSubmit={(v) => { + const rules = []; + for (const rule of v.rules) { + rules.push(_.pickBy(rule, _.identity)); + } + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + // return; + // if (['number'].includes(collectionField?.interface) && collectionField?.uiSchema?.['x-component-props']?.['stringMode'] === true) { + // rules['numberStringMode'] = true; + // } + if (['percent'].includes(collectionField?.interface)) { + for (const rule of rules) { + if (!!rule.maxValue || !!rule.minValue) { + rule['percentMode'] = true; + } + + if (rule.percentFormat) { + rule['percentFormats'] = true; + } + } + } + const concatValidator = _.concat([], collectionField?.uiSchema?.['x-validator'] || [], rules); + field.validator = concatValidator; + fieldSchema['x-validator'] = rules; + schema['x-validator'] = rules; + dn.emit('patch', { + schema, + }); + refresh(); + }} + /> + )} + {form && !form?.readPretty && collectionField?.uiSchema?.type && ( + { + const schema: ISchema = { + ['x-uid']: fieldSchema['x-uid'], + }; + if (field.value !== v.default) { + field.value = v.default; + } + fieldSchema.default = v.default; + schema.default = v.default; + dn.emit('patch', { + schema, + }); + refresh(); + }} + /> + )} + { + _.set(field.componentProps, 'service.params.filter', filter); + fieldSchema['x-component-props'] = field.componentProps; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-component-props': field.componentProps, + }, + }); + }} + /> + { + const sortArr = sort.map((item) => { + return item.direction === 'desc' ? `-${item.field}` : item.field; + }); + + _.set(field.componentProps, 'service.params.sort', sortArr); + fieldSchema['x-component-props'] = field.componentProps; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-component-props': field.componentProps, + }, + }); + }} + /> + {collectionField?.target && ['CollectionField', 'AssociationSelect'].includes(fieldSchema['x-component']) && ( + { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + const fieldNames = { + ...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'], + ...field.componentProps.fieldNames, + label, + }; + field.componentProps.fieldNames = fieldNames; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['fieldNames'] = fieldNames; + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }} + /> + )} + {collectionField && } + + + ); +}; + export default AssociationSelect; diff --git a/packages/core/client/src/schema-component/antd/card-item/CardItem.tsx b/packages/core/client/src/schema-component/antd/card-item/CardItem.tsx index 93bb2f054..9839da5ae 100644 --- a/packages/core/client/src/schema-component/antd/card-item/CardItem.tsx +++ b/packages/core/client/src/schema-component/antd/card-item/CardItem.tsx @@ -9,6 +9,7 @@ export const CardItem: React.FC = (props) => { const template = useSchemaTemplate(); const fieldSchema = useFieldSchema(); const templateKey = fieldSchema['x-template-key']; + return templateKey && !template ? null : ( diff --git a/packages/core/client/src/schema-component/antd/details/Details.tsx b/packages/core/client/src/schema-component/antd/details/Details.tsx new file mode 100644 index 000000000..ef97a6a1e --- /dev/null +++ b/packages/core/client/src/schema-component/antd/details/Details.tsx @@ -0,0 +1,3 @@ +import { FormV2 } from '../form-v2'; + +export const Details = FormV2; diff --git a/packages/core/client/src/schema-component/antd/details/index.ts b/packages/core/client/src/schema-component/antd/details/index.ts new file mode 100644 index 000000000..2201d1f1d --- /dev/null +++ b/packages/core/client/src/schema-component/antd/details/index.ts @@ -0,0 +1 @@ +export * from './Details'; diff --git a/packages/core/client/src/schema-component/antd/filter/useFilterActionProps.ts b/packages/core/client/src/schema-component/antd/filter/useFilterActionProps.ts index ed3add46a..7fd625f75 100644 --- a/packages/core/client/src/schema-component/antd/filter/useFilterActionProps.ts +++ b/packages/core/client/src/schema-component/antd/filter/useFilterActionProps.ts @@ -92,7 +92,7 @@ export const useFilterActionProps = () => { const { name } = useCollection(); const options = useFilterOptions(name); const { service, props } = useBlockRequestContext(); - return useFilterFieldProps({ options, service, params: props.params }); + return useFilterFieldProps({ options, service, params: props?.params }); }; export const useFilterFieldProps = ({ options, service, params }) => { diff --git a/packages/core/client/src/schema-component/antd/filter/useOperators.ts b/packages/core/client/src/schema-component/antd/filter/useOperators.ts new file mode 100644 index 000000000..ad70eca69 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/filter/useOperators.ts @@ -0,0 +1,21 @@ +import { useFieldSchema } from '@formily/react'; +import { useCollection, useCollectionManager } from '../../../collection-manager'; + +/** + * 获取当前字段所支持的操作符列表 + * @returns + */ +export const useOperatorList = () => { + const schema = useFieldSchema(); + const fieldInterface = schema['x-designer-props']?.interface; + const { name } = useCollection(); + const { getCollectionFields, getInterface } = useCollectionManager(); + const collectionFields = getCollectionFields(name); + + if (fieldInterface) { + return getInterface(fieldInterface)?.filterable?.operators || []; + } + + const field = collectionFields.find((item) => item.name === schema.name); + return getInterface(field?.interface)?.filterable?.operators || []; +}; diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.FilterFormDesigner.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.FilterFormDesigner.tsx new file mode 100644 index 000000000..755b57510 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.FilterFormDesigner.tsx @@ -0,0 +1,45 @@ +import { useFieldSchema } from '@formily/react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useCollection, useCollectionManager } from '../../../collection-manager'; +import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; +import { + EditComponent, + EditDescription, + EditOperator, + EditTitle, + EditTitleField, + EditTooltip, + EditValidationRules, +} from './SchemaSettingOptions'; + +export const FilterFormDesigner = () => { + const { getCollectionJoinField } = useCollectionManager(); + const { getField } = useCollection(); + const { t } = useTranslation(); + const fieldSchema = useFieldSchema(); + const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + + return ( + + + + + + + + + {collectionField ? : null} + + + ); +}; diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx index b9ee245b1..8e7dec4c7 100644 --- a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx @@ -13,6 +13,8 @@ import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings' import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks'; import { BlockItem } from '../block-item'; import { HTMLEncode } from '../input/shared'; +import { FilterFormDesigner } from './FormItem.FilterFormDesigner'; +import { useEnsureOperatorsValid } from './SchemaSettingOptions'; const divWrap = (schema: ISchema) => { return { @@ -25,9 +27,12 @@ const divWrap = (schema: ISchema) => { }; export const FormItem: any = observer((props: any) => { + useEnsureOperatorsValid(); + const field = useField(); const ctx = useContext(BlockRequestContext); const schema = useFieldSchema(); + useEffect(() => { if (ctx?.block === 'form') { ctx.field.data = ctx.field.data || {}; @@ -62,8 +67,8 @@ export const FormItem: any = observer((props: any) => { ); }); -FormItem.Designer = (props) => { - const { getCollectionFields, getCollection, getInterface, getCollectionJoinField } = useCollectionManager(); +FormItem.Designer = () => { + const { getCollectionFields, getInterface, getCollectionJoinField } = useCollectionManager(); const { getField } = useCollection(); const tk = useFilterByTk(); const { form } = useFormBlockContext(); @@ -537,3 +542,5 @@ FormItem.Designer = (props) => { ); }; + +FormItem.FilterFormDesigner = FilterFormDesigner; diff --git a/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx b/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx new file mode 100644 index 000000000..28b2a8325 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx @@ -0,0 +1,598 @@ +import { ArrayCollapse, FormLayout } from '@formily/antd'; +import { Field } from '@formily/core'; +import { ISchema, Schema, useField, useFieldSchema } from '@formily/react'; +import { uid } from '@formily/shared'; +import _ from 'lodash'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useFilterByTk, useFormBlockContext } from '../../../block-provider'; +import { useCollection, useCollectionManager } from '../../../collection-manager'; +import { SchemaSettings } from '../../../schema-settings'; +import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks'; +import { useOperatorList } from '../filter/useOperators'; + +export const findFilterOperators = (schema: Schema) => { + while (schema) { + if (schema['x-filter-operators']) { + return { + operators: schema['x-filter-operators'], + uid: schema['x-uid'], + }; + } + schema = schema.parent; + } + return {}; +}; + +const divWrap = (schema: ISchema) => { + return { + type: 'void', + 'x-component': 'div', + properties: { + [schema.name || uid()]: schema, + }, + }; +}; + +export const EditTitle = () => { + const { getCollectionJoinField } = useCollectionManager(); + const { getField } = useCollection(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + + return collectionField ? ( + { + if (title) { + field.title = title; + fieldSchema.title = title; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + title: fieldSchema.title, + }, + }); + } + dn.refresh(); + }} + /> + ) : null; +}; + +export const EditDescription = () => { + const field = useField(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + + return !field.readPretty ? ( + { + field.description = description; + fieldSchema.description = description; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + description: fieldSchema.description, + }, + }); + dn.refresh(); + }} + /> + ) : null; +}; + +export const EditTooltip = () => { + const field = useField(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + + return field.readPretty ? ( + { + field.decoratorProps.tooltip = tooltip; + fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {}; + fieldSchema['x-decorator-props']['tooltip'] = tooltip; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + dn.refresh(); + }} + /> + ) : null; +}; + +export const EditRequired = () => { + const field = useField(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { dn, refresh } = useDesignable(); + + return !field.readPretty && fieldSchema['x-component'] !== 'FormField' ? ( + { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + field.required = required; + fieldSchema['required'] = required; + schema['required'] = required; + dn.emit('patch', { + schema, + }); + refresh(); + }} + /> + ) : null; +}; + +export const EditValidationRules = () => { + const { getInterface, getCollectionJoinField } = useCollectionManager(); + const { getField } = useCollection(); + const { form } = useFormBlockContext(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { dn, refresh } = useDesignable(); + const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + const interfaceConfig = getInterface(collectionField?.interface); + const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema); + + return form && !form?.readPretty && validateSchema ? ( + = 3}}', + }, + }, + }, + }, + }, + }, + }, + } as ISchema + } + onSubmit={(v) => { + const rules = []; + for (const rule of v.rules) { + rules.push(_.pickBy(rule, _.identity)); + } + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + // return; + // if (['number'].includes(collectionField?.interface) && collectionField?.uiSchema?.['x-component-props']?.['stringMode'] === true) { + // rules['numberStringMode'] = true; + // } + if (['percent'].includes(collectionField?.interface)) { + for (const rule of rules) { + if (!!rule.maxValue || !!rule.minValue) { + rule['percentMode'] = true; + } + + if (rule.percentFormat) { + rule['percentFormats'] = true; + } + } + } + const concatValidator = _.concat([], collectionField?.uiSchema?.['x-validator'] || [], rules); + field.validator = concatValidator; + fieldSchema['x-validator'] = rules; + schema['x-validator'] = rules; + dn.emit('patch', { + schema, + }); + refresh(); + }} + /> + ) : null; +}; + +export const EditDefaultValue = () => { + const { getCollectionJoinField } = useCollectionManager(); + const { getField } = useCollection(); + const { form } = useFormBlockContext(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { dn, refresh } = useDesignable(); + const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + + return form && !form?.readPretty && collectionField?.uiSchema?.type ? ( + { + const schema: ISchema = { + ['x-uid']: fieldSchema['x-uid'], + }; + if (field.value !== v.default) { + field.value = v.default; + } + fieldSchema.default = v.default; + schema.default = v.default; + dn.emit('patch', { + schema, + }); + refresh(); + }} + /> + ) : null; +}; + +export const EditComponent = () => { + const { getInterface, getCollectionJoinField } = useCollectionManager(); + const { getField } = useCollection(); + const tk = useFilterByTk(); + const { form } = useFormBlockContext(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { dn, insertAdjacent } = useDesignable(); + const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + const interfaceConfig = getInterface(collectionField?.interface); + const fieldComponentOptions = useFieldComponentOptions(); + const isSubFormAssociationField = field.address.segments.includes('__form_grid'); + + return form && !isSubFormAssociationField && fieldComponentOptions ? ( + { + const schema: ISchema = { + name: collectionField.name, + type: 'void', + required: fieldSchema['required'], + description: fieldSchema['description'], + default: fieldSchema['default'], + 'x-decorator': 'FormItem', + 'x-designer': 'FormItem.Designer', + 'x-component': type, + 'x-validator': fieldSchema['x-validator'], + 'x-collection-field': fieldSchema['x-collection-field'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + 'x-component-props': { + ...collectionField?.uiSchema?.['x-component-props'], + ...fieldSchema['x-component-props'], + }, + }; + + 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', + }, + }); + }, + }); + }} + /> + ) : null; +}; + +export const EditPattern = () => { + const { getCollectionJoinField } = useCollectionManager(); + const { getField } = useCollection(); + const { form } = useFormBlockContext(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + let readOnlyMode = 'editable'; + if (fieldSchema['x-disabled'] === true) { + readOnlyMode = 'readonly'; + } + if (fieldSchema['x-read-pretty'] === true) { + readOnlyMode = 'read-pretty'; + } + + return form && + !form?.readPretty && + collectionField?.interface !== 'o2m' && + fieldSchema?.['x-component-props']?.['pattern-disable'] != true ? ( + { + const schema: ISchema = { + ['x-uid']: fieldSchema['x-uid'], + }; + + switch (v) { + case 'readonly': { + fieldSchema['x-read-pretty'] = false; + fieldSchema['x-disabled'] = true; + schema['x-read-pretty'] = false; + schema['x-disabled'] = true; + field.readPretty = false; + field.disabled = true; + break; + } + case 'read-pretty': { + fieldSchema['x-read-pretty'] = true; + fieldSchema['x-disabled'] = false; + schema['x-read-pretty'] = true; + schema['x-disabled'] = false; + field.readPretty = true; + break; + } + default: { + fieldSchema['x-read-pretty'] = false; + fieldSchema['x-disabled'] = false; + schema['x-read-pretty'] = false; + schema['x-disabled'] = false; + field.readPretty = false; + field.disabled = false; + break; + } + } + + dn.emit('patch', { + schema, + }); + + dn.refresh(); + }} + /> + ) : null; +}; + +/** + * 如果用户没有手动设置过 operator,那么在筛选的时候 operator 会是空的, + * 该方法确保 operator 一定有值(需要在 FormItem 中调用) + */ +export const useEnsureOperatorsValid = () => { + const fieldSchema = useFieldSchema(); + const operatorList = useOperatorList(); + const { operators: storedOperators } = findFilterOperators(fieldSchema); + + if (storedOperators && operatorList.length && !storedOperators[fieldSchema.name]) { + storedOperators[fieldSchema.name] = operatorList[0].value; + } +}; + +export const EditOperator = () => { + const compile = useCompile(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + const operatorList = useOperatorList(); + const { operators: storedOperators, uid } = findFilterOperators(fieldSchema); + + if (operatorList.length && !storedOperators[fieldSchema.name]) { + storedOperators[fieldSchema.name] = operatorList[0].value; + } + + return operatorList.length ? ( + { + storedOperators[fieldSchema.name] = v; + const schema: ISchema = { + ['x-uid']: uid, + ['x-filter-operators']: storedOperators, + }; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }} + /> + ) : null; +}; + +export const EditTitleField = () => { + const { getCollectionFields, getCollectionJoinField } = useCollectionManager(); + const { getField } = useCollection(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + const compile = useCompile(); + const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + const targetFields = collectionField?.target + ? getCollectionFields(collectionField.target) + : getCollectionFields(collectionField?.targetCollection) ?? []; + const options = targetFields + .filter((field) => !field?.target && field.type !== 'boolean') + .map((field) => ({ + value: field?.name, + label: compile(field?.uiSchema?.title) || field?.name, + })); + + return options.length > 0 && fieldSchema['x-component'] === 'CollectionField' ? ( + { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + const fieldNames = { + ...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'], + ...field.componentProps.fieldNames, + label, + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['fieldNames'] = fieldNames; + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }} + /> + ) : null; +}; diff --git a/packages/core/client/src/schema-component/antd/form-v2/Form.Designer.tsx b/packages/core/client/src/schema-component/antd/form-v2/Form.Designer.tsx index a8a19b44f..f06e53f54 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Form.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/Form.Designer.tsx @@ -21,11 +21,16 @@ export const FormDesigner = () => { const { t } = useTranslation(); const { visible } = useActionContext(); const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + return ( {/* */} - + { + const { name, title } = useCollection(); + const template = useSchemaTemplate(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + + return ( + + + + + + + + + ); +}; diff --git a/packages/core/client/src/schema-component/antd/form-v2/Form.tsx b/packages/core/client/src/schema-component/antd/form-v2/Form.tsx index 1f6b1400b..f68181653 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Form.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/Form.tsx @@ -162,21 +162,23 @@ const WithoutForm = (props) => { ); }; -export const Form: React.FC & { Designer?: any; ReadPrettyDesigner?: any } = observer((props) => { - const field = useField(); - const { form, disabled, ...others } = useProps(props); - const formDisabled = disabled || field.disabled; - return ( - -
- - {form ? ( - - ) : ( - - )} - -
-
- ); -}); +export const Form: React.FC & { Designer?: any; FilterDesigner?: any; ReadPrettyDesigner?: any } = observer( + (props) => { + const field = useField(); + const { form, disabled, ...others } = useProps(props); + const formDisabled = disabled || field.disabled; + return ( + +
+ + {form ? ( + + ) : ( + + )} + +
+
+ ); + }, +); diff --git a/packages/core/client/src/schema-component/antd/form-v2/index.ts b/packages/core/client/src/schema-component/antd/form-v2/index.ts index d2a9a8a48..bb80fb354 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/index.ts +++ b/packages/core/client/src/schema-component/antd/form-v2/index.ts @@ -1,7 +1,9 @@ +import { FilterDesigner } from './Form.FilterDesigner'; import { Form as FormV2 } from './Form'; import { DetailsDesigner, FormDesigner, ReadPrettyFormDesigner } from './Form.Designer'; FormV2.Designer = FormDesigner; +FormV2.FilterDesigner = FilterDesigner; FormV2.ReadPrettyDesigner = ReadPrettyFormDesigner; export { FormV2, DetailsDesigner }; diff --git a/packages/core/client/src/schema-component/antd/grid/Grid.tsx b/packages/core/client/src/schema-component/antd/grid/Grid.tsx index 6bde552f6..b7540e71c 100644 --- a/packages/core/client/src/schema-component/antd/grid/Grid.tsx +++ b/packages/core/client/src/schema-component/antd/grid/Grid.tsx @@ -358,7 +358,7 @@ export const Grid: any = observer((props: any) => { ); }); -Grid.Row = observer((props) => { +Grid.Row = observer(() => { const field = useField(); const fieldSchema = useFieldSchema(); const addr = field.address.toString(); @@ -416,12 +416,12 @@ Grid.Col = observer((props: any) => { const { cols = [] } = useContext(GridRowContext); const schema = useFieldSchema(); const field = useField(); - let width = '100%'; + let width = ''; if (cols?.length) { const w = schema?.['x-component-props']?.['width'] || 100 / cols.length; width = `calc(${w}% - 24px - 24px / ${cols.length})`; } - const { isOver, setNodeRef } = useDroppable({ + const { setNodeRef } = useDroppable({ id: field.address.toString(), data: { insertAdjacent: 'beforeEnd', @@ -431,17 +431,7 @@ Grid.Col = observer((props: any) => { }); return ( -
+
{props.children}
diff --git a/packages/core/client/src/schema-component/antd/index.ts b/packages/core/client/src/schema-component/antd/index.ts index d8ba6518a..f5960a436 100644 --- a/packages/core/client/src/schema-component/antd/index.ts +++ b/packages/core/client/src/schema-component/antd/index.ts @@ -7,6 +7,7 @@ export * from './checkbox'; export * from './color-select'; export * from './cron'; export * from './date-picker'; +export * from './details'; export * from './filter'; export * from './form'; export * from './form-item'; diff --git a/packages/core/client/src/schema-component/antd/page/FixedBlock.tsx b/packages/core/client/src/schema-component/antd/page/FixedBlock.tsx index cca938a1b..dc192dfc0 100644 --- a/packages/core/client/src/schema-component/antd/page/FixedBlock.tsx +++ b/packages/core/client/src/schema-component/antd/page/FixedBlock.tsx @@ -104,36 +104,31 @@ const FixedBlock: React.FC = (props) => { {schema ? (
- +
) : ( props.children diff --git a/packages/core/client/src/schema-component/antd/page/Page.tsx b/packages/core/client/src/schema-component/antd/page/Page.tsx index 00a46c475..6d7ac076e 100644 --- a/packages/core/client/src/schema-component/antd/page/Page.tsx +++ b/packages/core/client/src/schema-component/antd/page/Page.tsx @@ -8,6 +8,7 @@ import React, { useContext, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useLocation } from 'react-router-dom'; import { useDocumentTitle } from '../../../document-title'; +import { FilterBlockProvider } from '../../../filter-provider/FilterProvider'; import { Icon } from '../../../icon'; import { DndContext } from '../../common'; import { SortableItem } from '../../common/sortable-item'; @@ -138,168 +139,170 @@ export const Page = (props) => { const [height, setHeight] = useState(0); return ( -
- -
{ - setHeight(Math.floor(ref?.getBoundingClientRect().height || 0) + 1); - }} - > - {!disablePageHeader && ( - - { - setLoading(true); - setActiveKey(activeKey); - window.history.pushState({}, '', window.location.pathname + `?tab=` + activeKey); - setTimeout(() => { - setLoading(false); - }, 50); - }} - tabBarExtraContent={ - dn.designable && ( - - ) - } - > - {fieldSchema.mapProperties((schema) => { - return ( - - {schema['x-icon'] && } - {schema.title || t('Unnamed')} - - - } - key={schema.name} - /> - ); - })} - - - ) - } - /> - )} -
-
- {loading ? ( - - ) : !disablePageHeader && enablePageTabs ? ( - fieldSchema.mapProperties((schema) => { - if (schema.name !== activeKey) return null; - return ( - - - - ); - }) - ) : ( - -
+
+ +
{ + setHeight(Math.floor(ref?.getBoundingClientRect().height || 0) + 1); + }} + > + {!disablePageHeader && ( + .nb-grid:not(:last-child) { - > .nb-schema-initializer-button { - display: none; + &.has-footer { + padding-top: 12px; + .ant-page-header-heading-left { + /* margin: 0; */ + } + .ant-page-header-footer { + margin-top: 0; } } `} + ghost={false} + title={hidePageTitle ? undefined : fieldSchema.title || compile(title)} + {...others} + footer={ + enablePageTabs && ( + + { + setLoading(true); + setActiveKey(activeKey); + window.history.pushState({}, '', window.location.pathname + `?tab=` + activeKey); + setTimeout(() => { + setLoading(false); + }, 50); + }} + tabBarExtraContent={ + dn.designable && ( + + ) + } + > + {fieldSchema.mapProperties((schema) => { + return ( + + {schema['x-icon'] && } + {schema.title || t('Unnamed')} + + + } + key={schema.name} + /> + ); + })} + + + ) + } + /> + )} +
+
+ {loading ? ( + + ) : !disablePageHeader && enablePageTabs ? ( + fieldSchema.mapProperties((schema) => { + if (schema.name !== activeKey) return null; + return ( + + + + ); + }) + ) : ( + - {props.children} -
- - )} +
.nb-grid:not(:last-child) { + > .nb-schema-initializer-button { + display: none; + } + } + `} + > + {props.children} +
+ + )} +
-
+ ); }; diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx index 3a462be90..bdbd6b762 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx @@ -7,7 +7,7 @@ import { reaction } from '@formily/reactive'; import { useEventListener, useMemoizedFn } from 'ahooks'; import { Table as AntdTable, TableColumnProps } from 'antd'; import { default as classNames, default as cls } from 'classnames'; -import React, { RefCallback, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { DndContext, useDesignable } from '../..'; import { @@ -15,7 +15,7 @@ import { RecordProvider, useSchemaInitializer, useTableBlockContext, - useTableSelectorContext + useTableSelectorContext, } from '../../../'; import { useACLFieldWhitelist } from '../../../acl/ACLProvider'; import { extractIndex, getIdsWithChildren, isCollectionFieldComponent, isColumnComponent } from './utils'; @@ -96,7 +96,7 @@ const SortableRow = (props) => { ); }; @@ -157,7 +157,7 @@ export const Table: any = observer((props: any) => { const field = useField(); const columns = useTableColumns(); const { pagination: pagination1, useProps, onChange, ...others1 } = props; - const { pagination: pagination2, ...others2 } = useProps?.() || {}; + const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {}; const { dragSort = false, showIndex = true, @@ -178,6 +178,28 @@ export const Table: any = observer((props: any) => { const { treeTable } = schema?.parent?.['x-decorator-props'] || {}; const [expandedKeys, setExpandesKeys] = useState([]); const [allIncludesChildren, setAllIncludesChildren] = useState([]); + const [selectedRowKeys, setSelectedRowKeys] = useState(field?.data?.selectedRowKeys || []); + const [selectedRow, setSelectedRow] = useState([]); + + let onRow = null, + highlightRow = ''; + + if (onClickRow) { + onRow = (record) => { + return { + onClick: () => onClickRow(record, setSelectedRow, selectedRow), + }; + }; + highlightRow = css` + & > td { + background-color: #caedff !important; + } + &:hover > td { + background-color: #caedff !important; + } + `; + } + useEffect(() => { field.setValidator((value) => { if (requiredValidator) { @@ -294,10 +316,11 @@ export const Table: any = observer((props: any) => { rowSelection: rowSelection ? { type: 'checkbox', - selectedRowKeys: field?.data?.selectedRowKeys || [], + selectedRowKeys: selectedRowKeys, onChange(selectedRowKeys: any[], selectedRows: any[]) { field.data = field.data || {}; field.data.selectedRowKeys = selectedRowKeys; + setSelectedRowKeys(selectedRowKeys); onRowSelectionChange?.(selectedRowKeys, selectedRows); }, renderCell: (checked, record, index, originNode) => { @@ -396,8 +419,10 @@ export const Table: any = observer((props: any) => { const fieldSchema = useFieldSchema(); const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock; const [tableHeight, setTableHeight] = useState(0); - const [headerAndPaginationHeight, setHeaderAndPaginationHeight] = useState(0); + const tableRef = useRef(null); + const containerRef = useRef(null); + const scroll = useMemo(() => { return fixedBlock ? { @@ -409,22 +434,21 @@ export const Table: any = observer((props: any) => { }; }, [fixedBlock, tableHeight, headerAndPaginationHeight]); - const elementRef = useRef(); const calcTableSize = () => { - if (!elementRef.current) return; - const clientRect = elementRef.current?.getBoundingClientRect(); - setTableHeight(Math.ceil(clientRect?.height || 0)); - }; - useEventListener('resize', calcTableSize); + if (!containerRef.current || !tableRef.current) return; + setTableHeight(Math.ceil(containerRef.current.clientHeight || 0)); - const mountedRef: RefCallback = (ref) => { - elementRef.current = ref; - calcTableSize(); + const headerHeight = tableRef.current.querySelector('.ant-table-header')?.clientHeight || 0; + const paginationHeight = tableRef.current.querySelector('.ant-table-pagination')?.clientHeight || 0; + setHeaderAndPaginationHeight(Math.ceil(headerHeight + paginationHeight + 18)); }; + useEffect(calcTableSize, [field.value]); + useEventListener('resize', calcTableSize); + return (
{ > { - const headerHeight = ref?.querySelector('.ant-table-header')?.getBoundingClientRect().height || 0; - const paginationHeight = ref?.querySelector('.ant-table-pagination')?.getBoundingClientRect().height || 0; - setHeaderAndPaginationHeight(Math.ceil(headerHeight + paginationHeight + 16)); - }} + ref={tableRef} rowKey={rowKey ?? defaultRowKey} {...others} {...restProps} @@ -452,6 +472,8 @@ export const Table: any = observer((props: any) => { onChange={(pagination, filters, sorter, extra) => { onTableChange?.(pagination, filters, sorter, extra); }} + onRow={onRow} + rowClassName={(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : '')} tableLayout={'auto'} scroll={scroll} columns={columns} diff --git a/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx b/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx index 28fcb6b2b..c5767e0c5 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/TableBlockDesigner.tsx @@ -6,6 +6,7 @@ import { useTableBlockContext } from '../../../block-provider'; import { mergeFilter } from '../../../block-provider/SharedFilterProvider'; import { useCollection, useCollectionManager } from '../../../collection-manager'; import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks'; +import { FilterBlockType } from '../../../filter-provider/utils'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { useSchemaTemplate } from '../../../schema-templates'; import { useDesignable } from '../../hooks'; @@ -234,6 +235,7 @@ export const TableBlockDesigner = () => { }); }} /> + {supportTemplate && } {supportTemplate && ( diff --git a/packages/core/client/src/schema-component/core/SchemaComponent.tsx b/packages/core/client/src/schema-component/core/SchemaComponent.tsx index fce2913f6..bb1a2e11e 100644 --- a/packages/core/client/src/schema-component/core/SchemaComponent.tsx +++ b/packages/core/client/src/schema-component/core/SchemaComponent.tsx @@ -6,7 +6,7 @@ function toSchema(schema?: any) { if (Schema.isSchemaInstance(schema)) { return schema; } - if (schema.name) { + if (schema?.name) { return new Schema({ type: 'object', properties: { diff --git a/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx index 55daaa6db..0ead126e8 100644 --- a/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx @@ -44,6 +44,25 @@ export const BlockInitializers = { }, ], }, + { + key: 'filterBlocks', + type: 'itemGroup', + title: '{{t("Filter blocks")}}', + children: [ + { + key: 'filterForm', + type: 'item', + title: '{{t("Form")}}', + component: 'FilterFormBlockInitializer', + }, + { + key: 'filterCollapse', + type: 'item', + title: '{{t("Collapse")}}', + component: 'FilterCollapseBlockInitializer', + }, + ], + }, { key: 'media', type: 'itemGroup', diff --git a/packages/core/client/src/schema-initializer/buttons/FilterFormActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/FilterFormActionInitializers.tsx new file mode 100644 index 000000000..d41d84a02 --- /dev/null +++ b/packages/core/client/src/schema-initializer/buttons/FilterFormActionInitializers.tsx @@ -0,0 +1,29 @@ +// 表单的操作配置 +export const FilterFormActionInitializers = { + title: '{{t("Configure actions")}}', + icon: 'SettingOutlined', + items: [ + { + type: 'itemGroup', + title: '{{t("Enable actions")}}', + children: [ + { + type: 'item', + title: '{{t("Filter")}}', + component: 'CreateFilterActionInitializer', + schema: { + 'x-action-settings': {}, + }, + }, + { + type: 'item', + title: '{{t("Reset")}}', + component: 'CreateResetActionInitializer', + schema: { + 'x-action-settings': {}, + }, + }, + ], + }, + ], +}; diff --git a/packages/core/client/src/schema-initializer/buttons/FormItemInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/FormItemInitializers.tsx index 818580dcb..438a0448b 100644 --- a/packages/core/client/src/schema-initializer/buttons/FormItemInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/FormItemInitializers.tsx @@ -1,21 +1,25 @@ -import { union } from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { SchemaInitializer } from '../SchemaInitializer'; import { gridRowColWrap, useAssociatedFormItemInitializerFields, + useFilterAssociatedFormItemInitializerFields, + useFilterFormItemInitializerFields, + useFilterInheritsFormItemInitializerFields, useFormItemInitializerFields, useInheritsFormItemInitializerFields, } from '../utils'; import { useCompile } from '../../schema-component'; - // 表单里配置字段 export const FormItemInitializers = (props: any) => { const { t } = useTranslation(); const { insertPosition, component } = props; - const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form' }); + const associationFields = useAssociatedFormItemInitializerFields({ + readPretty: true, + block: 'Form', + }); const inheritFields = useInheritsFormItemInitializerFields(); const compile = useCompile(); const fieldItems: any[] = [ @@ -27,16 +31,17 @@ export const FormItemInitializers = (props: any) => { ]; if (inheritFields?.length > 0) { inheritFields.forEach((inherit) => { - Object.values(inherit)[0].length&&fieldItems.push( - { - type: 'divider', - }, - { - type: 'itemGroup', - title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', - children: Object.values(inherit)[0], - }, - ); + Object.values(inherit)[0].length && + fieldItems.push( + { + type: 'divider', + }, + { + type: 'itemGroup', + title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', + children: Object.values(inherit)[0], + }, + ); }); } associationFields.length > 0 && @@ -82,3 +87,76 @@ export const FormItemInitializers = (props: any) => { /> ); }; + +export const FilterFormItemInitializers = (props: any) => { + const { t } = useTranslation(); + const { insertPosition, component } = props; + const associationFields = useFilterAssociatedFormItemInitializerFields(); + const inheritFields = useFilterInheritsFormItemInitializerFields(); + const compile = useCompile(); + const fieldItems: any[] = [ + { + type: 'itemGroup', + title: t('Display fields'), + children: useFilterFormItemInitializerFields(), + }, + ]; + if (inheritFields?.length > 0) { + inheritFields.forEach((inherit) => { + Object.values(inherit)[0].length && + fieldItems.push( + { + type: 'divider', + }, + { + type: 'itemGroup', + title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', + children: Object.values(inherit)[0], + }, + ); + }); + } + + associationFields.length > 0 && + fieldItems.push( + { + type: 'divider', + }, + { + type: 'itemGroup', + title: t('Display association fields'), + children: associationFields, + }, + ); + + fieldItems.push( + { + type: 'divider', + }, + { + type: 'item', + title: t('Add text'), + component: 'BlockInitializer', + schema: { + type: 'void', + 'x-editable': false, + 'x-decorator': 'FormItem', + 'x-designer': 'Markdown.Void.Designer', + 'x-component': 'Markdown.Void', + 'x-component-props': { + content: t('This is a demo text, **supports Markdown syntax**.'), + }, + }, + }, + ); + return ( + + ); +}; diff --git a/packages/core/client/src/schema-initializer/buttons/TableColumnInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TableColumnInitializers.tsx index fbe00c3f9..ca12ab15c 100644 --- a/packages/core/client/src/schema-initializer/buttons/TableColumnInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/TableColumnInitializers.tsx @@ -80,7 +80,7 @@ export const TableColumnInitializers = (props: any) => { }, }; }} - items={itemsMerge(fieldItems, items)} + items={itemsMerge(fieldItems)} > {t('Configure columns')} diff --git a/packages/core/client/src/schema-initializer/buttons/index.ts b/packages/core/client/src/schema-initializer/buttons/index.ts index 7a62f77cd..2cfddd22e 100644 --- a/packages/core/client/src/schema-initializer/buttons/index.ts +++ b/packages/core/client/src/schema-initializer/buttons/index.ts @@ -6,6 +6,7 @@ export * from './CreateFormBulkEditBlockInitializers'; export * from './CustomFormItemInitializers'; export * from './DetailsActionInitializers'; export * from './FormActionInitializers'; +export * from './FilterFormActionInitializers'; export * from './FormItemInitializers'; export * from './KanbanActionInitializers'; export * from './ReadPrettyFormActionInitializers'; diff --git a/packages/core/client/src/schema-initializer/index.ts b/packages/core/client/src/schema-initializer/index.ts index bd08edb76..5e95eac96 100644 --- a/packages/core/client/src/schema-initializer/index.ts +++ b/packages/core/client/src/schema-initializer/index.ts @@ -6,6 +6,7 @@ export { gridRowColWrap, useRecordCollectionDataSourceItems, createTableBlockSchema, + createFilterFormBlockSchema, useAssociatedTableColumnInitializerFields, useInheritsTableColumnInitializerFields, useTableColumnInitializerFields, diff --git a/packages/core/client/src/schema-initializer/items/CreateFilterActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/CreateFilterActionInitializer.tsx new file mode 100644 index 000000000..5e622745f --- /dev/null +++ b/packages/core/client/src/schema-initializer/items/CreateFilterActionInitializer.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +import { ActionInitializer } from './ActionInitializer'; + +export const CreateFilterActionInitializer = (props) => { + const schema = { + title: '{{ t("Filter") }}', + 'x-action': 'submit', + 'x-component': 'Action', + 'x-designer': 'Action.Designer', + 'x-component-props': { + type: 'primary', + useProps: '{{ useFilterBlockActionProps }}', + }, + }; + return ; +}; diff --git a/packages/core/client/src/schema-initializer/items/CreateResetActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/CreateResetActionInitializer.tsx new file mode 100644 index 000000000..f9af63735 --- /dev/null +++ b/packages/core/client/src/schema-initializer/items/CreateResetActionInitializer.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import { ActionInitializer } from './ActionInitializer'; + +export const CreateResetActionInitializer = (props) => { + const schema = { + title: '{{ t("Reset") }}', + 'x-component': 'Action', + 'x-designer': 'Action.Designer', + 'x-component-props': { + useProps: '{{ useResetBlockActionProps }}', + }, + }; + return ; +}; diff --git a/packages/core/client/src/schema-initializer/items/FilterBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/FilterBlockInitializer.tsx new file mode 100644 index 000000000..0edd04d40 --- /dev/null +++ b/packages/core/client/src/schema-initializer/items/FilterBlockInitializer.tsx @@ -0,0 +1,33 @@ +import { TableOutlined } from '@ant-design/icons'; +import React, { useContext } from 'react'; + +import { SchemaInitializer, SchemaInitializerButtonContext } from '..'; +import { useSchemaTemplateManager } from '../../schema-templates'; +import { useCollectionDataSourceItems } from '../utils'; + +export const FilterBlockInitializer = (props) => { + const { templateWrap, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; + const { getTemplateSchemaByMode } = useSchemaTemplateManager(); + const { setVisible } = useContext(SchemaInitializerButtonContext); + + return ( + } + {...others} + onClick={async ({ item }) => { + if (item.template) { + const s = await getTemplateSchemaByMode(item); + templateWrap ? insert(templateWrap(s, { item })) : insert(s); + } else { + if (onCreateBlockSchema) { + onCreateBlockSchema({ item }); + } else if (createBlockSchema) { + insert(createBlockSchema({ collection: item.name })); + } + } + setVisible(false); + }} + items={useCollectionDataSourceItems(componentType)} + /> + ); +}; diff --git a/packages/core/client/src/schema-initializer/items/FilterCollapseBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/FilterCollapseBlockInitializer.tsx new file mode 100644 index 000000000..180ec1981 --- /dev/null +++ b/packages/core/client/src/schema-initializer/items/FilterCollapseBlockInitializer.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { TableOutlined } from '@ant-design/icons'; + +import { DataBlockInitializer } from './DataBlockInitializer'; +import { createCollapseBlockSchema } from '../utils'; + +export const FilterCollapseBlockInitializer = (props) => { + const { insert } = props; + return ( + } + componentType={'FilterCollapse'} + onCreateBlockSchema={async ({ item }) => { + const schema = createCollapseBlockSchema({ + collection: item.name, + // 与数据区块做区分 + blockType: 'filter', + }); + insert(schema); + }} + /> + ); +}; diff --git a/packages/core/client/src/schema-initializer/items/FilterFormBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/FilterFormBlockInitializer.tsx new file mode 100644 index 000000000..e63714549 --- /dev/null +++ b/packages/core/client/src/schema-initializer/items/FilterFormBlockInitializer.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { FormOutlined } from '@ant-design/icons'; +import { createFilterFormBlockSchema } from '../utils'; +import { FilterBlockInitializer } from './FilterBlockInitializer'; + +export const FilterFormBlockInitializer = (props) => { + return ( + } + componentType={'FilterFormItem'} + templateWrap={(templateSchema, { item }) => { + const s = createFilterFormBlockSchema({ + template: templateSchema, + collection: item.name, + }); + if (item.template && item.mode === 'reference') { + s['x-template-key'] = item.template.key; + } + return s; + }} + createBlockSchema={createFilterFormBlockSchema} + /> + ); +}; diff --git a/packages/core/client/src/schema-initializer/items/index.tsx b/packages/core/client/src/schema-initializer/items/index.tsx index f8e7a76f9..968c1df26 100644 --- a/packages/core/client/src/schema-initializer/items/index.tsx +++ b/packages/core/client/src/schema-initializer/items/index.tsx @@ -13,6 +13,8 @@ export * from './CreateActionInitializer'; export * from './CreateFormBlockInitializer'; export * from './CreateFormBulkEditBlockInitializer'; export * from './CreateSubmitActionInitializer'; +export * from './CreateFilterActionInitializer'; +export * from './CreateResetActionInitializer'; export * from './CustomizeActionInitializer'; export * from './CustomizeBulkEditActionInitializer'; export * from './DataBlockInitializer'; @@ -20,6 +22,7 @@ export * from './DeleteEventActionInitializer'; export * from './DestroyActionInitializer'; export * from './DetailsBlockInitializer'; export * from './FilterActionInitializer'; +export * from './FilterFormBlockInitializer'; export * from './FormBlockInitializer'; export * from './G2PlotInitializer'; export * from './InitializerWithSwitch'; @@ -37,6 +40,7 @@ export * from './RefreshActionInitializer'; export * from './SubmitActionInitializer'; export * from './TableActionColumnInitializer'; export * from './TableBlockInitializer'; +export * from './FilterCollapseBlockInitializer'; export * from './TableCollectionFieldInitializer'; export * from './TableSelectorInitializer'; export * from './UpdateActionInitializer'; diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index 156254738..fc18d4409 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -3,12 +3,13 @@ import { uid } from '@formily/shared'; import React, { useContext, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { BlockRequestContext, SchemaInitializerItemOptions } from '../'; -import { useCollection, useCollectionManager } from '../collection-manager'; +import { FieldOptions, useCollection, useCollectionManager } from '../collection-manager'; +import { isAssocField } from '../filter-provider/utils'; import { useActionContext, useDesignable } from '../schema-component'; import { useSchemaTemplateManager } from '../schema-templates'; import { SelectCollection } from './SelectCollection'; -export const itemsMerge = (items1, items2) => { +export const itemsMerge = (items1) => { return items1; }; @@ -197,9 +198,8 @@ export const useFormItemInitializerFields = (options?: any) => { const { getInterface } = useCollectionManager(); const form = useForm(); const { readPretty = form.readPretty, block = 'Form' } = options || {}; - const actionCtx = useActionContext(); - const action = actionCtx?.fieldSchema?.['x-action']; - const { snapshot } = useActionContext(); + const { snapshot, fieldSchema } = useActionContext(); + const action = fieldSchema?.['x-action']; return currentFields ?.filter((field) => field?.interface && !field?.isForeignKey && !field?.treeChildren) @@ -237,6 +237,56 @@ export const useFormItemInitializerFields = (options?: any) => { }); }; +// 筛选表单相关 +export const useFilterFormItemInitializerFields = (options?: any) => { + const { name, currentFields } = useCollection(); + const { getInterface } = useCollectionManager(); + const form = useForm(); + const { readPretty = form.readPretty, block = 'FilterForm' } = options || {}; + const { snapshot, fieldSchema } = useActionContext(); + const action = fieldSchema?.['x-action']; + + return currentFields + ?.filter((field) => field?.interface && !field?.isForeignKey && getInterface(field.interface)?.filterable) + ?.map((field) => { + const interfaceConfig = getInterface(field.interface); + let schema = { + type: 'string', + name: field.name, + required: false, + 'x-designer': 'FormItem.FilterFormDesigner', + 'x-component': field.interface === 'o2m' && !snapshot ? 'TableField' : 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': `${name}.${field.name}`, + 'x-component-props': {}, + }; + if (isAssocField(field)) { + schema = { + type: 'string', + name: field.name, + required: false, + 'x-designer': 'AssociationSelect.FilterDesigner', + 'x-component': 'AssociationSelect', + 'x-decorator': 'FormItem', + 'x-collection-field': `${name}.${field.name}`, + 'x-component-props': field.uiSchema?.['x-component-props'], + }; + } + const resultItem = { + type: 'item', + title: field?.uiSchema?.title || field.name, + component: 'CollectionFieldInitializer', + remove: removeGridFormItem, + schemaInitialize: (s) => { + interfaceConfig?.schemaInitialize?.(s, { field, block, readPretty, action }); + }, + schema, + } as SchemaInitializerItemOptions; + + return resultItem; + }); +}; + export const useAssociatedFormItemInitializerFields = (options?: any) => { const { name, fields } = useCollection(); const { getInterface, getCollectionFields } = useCollectionManager(); @@ -288,6 +338,57 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => { return groups; }; +const getItem = (field: FieldOptions, schemaName: string, collectionName: string, getCollectionFields) => { + if (field.interface === 'm2o') { + const subFields = getCollectionFields(field.target); + + return { + type: 'subMenu', + title: field.uiSchema?.title, + children: subFields + .map((subField) => getItem(subField, `${schemaName}.${subField.name}`, collectionName, getCollectionFields)) + .filter(Boolean), + } as SchemaInitializerItemOptions; + } + + if (isAssocField(field)) return null; + + const schema = { + type: 'string', + name: schemaName, + 'x-designer': 'FormItem.FilterFormDesigner', + 'x-designer-props': { + // 在 useOperatorList 中使用,用于获取对应的操作符列表 + interface: field.interface, + }, + 'x-component': 'CollectionField', + 'x-read-pretty': false, + 'x-decorator': 'FormItem', + 'x-collection-field': `${collectionName}.${schemaName}`, + }; + + return { + type: 'item', + title: field.uiSchema?.title || field.name, + component: 'CollectionFieldInitializer', + remove: removeGridFormItem, + schema, + } as SchemaInitializerItemOptions; +}; + +// 筛选表单相关 +export const useFilterAssociatedFormItemInitializerFields = () => { + const { name, fields } = useCollection(); + const { getCollectionFields } = useCollectionManager(); + const interfaces = ['m2o']; + const groups = fields + ?.filter((field) => { + return interfaces.includes(field.interface); + }) + ?.map((field) => getItem(field, field.name, name, getCollectionFields)); + return groups; +}; + export const useInheritsFormItemInitializerFields = (options?) => { const { name } = useCollection(); const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = useCollectionManager(); @@ -329,6 +430,50 @@ export const useInheritsFormItemInitializerFields = (options?) => { }; }); }; + +// 筛选表单相关 +export const useFilterInheritsFormItemInitializerFields = (options?) => { + const { name } = useCollection(); + const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = useCollectionManager(); + const inherits = getInheritCollections(name); + const { snapshot } = useActionContext(); + + return inherits?.map((v) => { + const fields = getParentCollectionFields(v, name); + const form = useForm(); + const { readPretty = form.readPretty, block = 'Form' } = options || {}; + const targetCollection = getCollection(v); + return { + [targetCollection.title]: fields + ?.filter((field) => field?.interface && !field?.isForeignKey && getInterface(field.interface)?.filterable) + ?.map((field) => { + const interfaceConfig = getInterface(field.interface); + const schema = { + type: 'string', + name: field.name, + title: field?.uiSchema?.title || field.name, + required: false, + 'x-designer': 'FormItem.FilterFormDesigner', + 'x-component': field.interface === 'o2m' && !snapshot ? 'TableField' : 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': `${name}.${field.name}`, + 'x-component-props': {}, + 'x-read-pretty': field?.uiSchema?.['x-read-pretty'], + }; + return { + type: 'item', + title: field?.uiSchema?.title || field.name, + component: 'CollectionFieldInitializer', + remove: removeGridFormItem, + schemaInitialize: (s) => { + interfaceConfig?.schemaInitialize?.(s, { field, block, readPretty }); + }, + schema, + } as SchemaInitializerItemOptions; + }), + }; + }); +}; export const useCustomFormItemInitializerFields = (options?: any) => { const { name, currentFields } = useCollection(); const { getInterface } = useCollectionManager(); @@ -434,7 +579,6 @@ export const useCurrentSchema = (action: string, key: string, find = findSchema, const { remove } = useDesignable(); const schema = find(fieldSchema, key, action); const ctx = useContext(BlockRequestContext); - const exists = !!schema; return { schema, @@ -658,7 +802,7 @@ export const createDetailsBlockSchema = (options) => { properties: { [uid()]: { type: 'void', - 'x-component': 'FormV2', + 'x-component': 'Details', 'x-read-pretty': true, 'x-component-props': { useProps: '{{ useDetailsBlockProps }}', @@ -760,6 +904,67 @@ export const createFormBlockSchema = (options) => { return schema; }; +export const createFilterFormBlockSchema = (options) => { + const { + formItemInitializers = 'FilterFormItemInitializers', + actionInitializers = 'FilterFormActionInitializers', + collection, + resource, + association, + action, + template, + ...others + } = options; + const resourceName = resource || association || collection; + const schema: ISchema = { + type: 'void', + 'x-decorator': 'FormBlockProvider', + 'x-decorator-props': { + ...others, + action, + resource: resourceName, + collection, + association, + }, + 'x-designer': 'FormV2.FilterDesigner', + 'x-component': 'CardItem', + // 保存当前筛选区块所能过滤的数据区块 + 'x-filter-targets': [], + // 用于存储用户设置的每个字段的运算符,目前仅筛选表单区块支持自定义 + 'x-filter-operators': {}, + properties: { + [uid()]: { + type: 'void', + 'x-component': 'FormV2', + 'x-component-props': { + useProps: '{{ useFormBlockProps }}', + }, + properties: { + grid: template || { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': formItemInitializers, + properties: {}, + }, + actions: { + type: 'void', + 'x-initializer': actionInitializers, + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + style: { + float: 'right', + }, + }, + properties: {}, + }, + }, + }, + }, + }; + return schema; +}; + export const createReadPrettyFormBlockSchema = (options) => { const { formItemInitializers = 'ReadPrettyFormItemInitializers', @@ -830,6 +1035,9 @@ export const createTableBlockSchema = (options) => { tableActionColumnInitializers, tableBlockProvider, disableTemplate, + TableBlockDesigner, + blockType, + pageSize = 20, ...others } = options; const schema: ISchema = { @@ -841,16 +1049,18 @@ export const createTableBlockSchema = (options) => { resource: resource || collection, action: 'list', params: { - pageSize: 20, + pageSize, }, rowKey, showIndex: true, dragSort: false, disableTemplate: disableTemplate ?? false, + blockType, ...others, }, - 'x-designer': 'TableBlockDesigner', + 'x-designer': TableBlockDesigner ?? 'TableBlockDesigner', 'x-component': 'CardItem', + 'x-filter-targets': [], properties: { actions: { type: 'void', @@ -903,6 +1113,35 @@ export const createTableBlockSchema = (options) => { return schema; }; +export const createCollapseBlockSchema = (options) => { + const { collection, blockType } = options; + const schema: ISchema = { + type: 'void', + 'x-decorator': 'AssociationFilter.Provider', + 'x-decorator-props': { + collection, + blockType, + associationFilterStyle: { + width: '100%', + }, + }, + 'x-designer': 'AssociationFilter.BlockDesigner', + 'x-component': 'CardItem', + 'x-filter-targets': [], + properties: { + [uid()]: { + type: 'void', + 'x-action': 'associateFilter', + 'x-initializer': 'AssociationFilter.FilterBlockInitializer', + 'x-component': 'AssociationFilter', + properties: {}, + }, + }, + }; + + return schema; +}; + export const createTableSelectorSchema = (options) => { const { collection, resource, rowKey, ...others } = options; const schema: ISchema = { diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx index 5a4e19aaa..3af944491 100644 --- a/packages/core/client/src/schema-settings/SchemaSettings.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx @@ -10,6 +10,7 @@ import { Cascader, CascaderProps, Dropdown, + Empty, Menu, MenuItemProps, Modal, @@ -19,7 +20,7 @@ import { } from 'antd'; import classNames from 'classnames'; import { cloneDeep } from 'lodash'; -import React, { createContext, useContext, useMemo, useState } from 'react'; +import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { @@ -43,7 +44,10 @@ import { useSchemaTemplateManager } from '../schema-templates'; import { useBlockTemplateContext } from '../schema-templates/BlockTemplate'; import { FormLinkageRules } from './LinkageRules'; import { useLinkageCollectionFieldOptions } from './LinkageRules/action-hooks'; +import { FilterBlockType, isSameCollection, useSupportedBlocks } from '../filter-provider/utils'; +import { findFilterTargets, updateFilterTargets } from '../block-provider/hooks'; import { EnableChildCollections } from './EnableChildCollections'; +import { getTargetKey } from '../schema-component/antd/association-filter/utilts'; interface SchemaSettingsProps { title?: any; @@ -448,8 +452,142 @@ SchemaSettings.Remove = (props: any) => { ); }; +SchemaSettings.ConnectDataBlocks = (props: { type: FilterBlockType; emptyDescription?: string }) => { + const { type, emptyDescription } = props; + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + const { t } = useTranslation(); + const collection = useCollection(); + const dataBlocks = useSupportedBlocks(type); + let { targets = [], uid } = findFilterTargets(fieldSchema); + const compile = useCompile(); + + const Content = dataBlocks.map((block) => { + const title = `${compile(block.collection.title)} #${block.uid.slice(0, 4)}`; + const onHover = () => { + const dom = block.dom; + const designer = dom.querySelector('.general-schema-designer') as HTMLElement; + if (designer) { + designer.style.display = 'block'; + } + dom.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.2)'; + dom.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + }; + const onLeave = () => { + const dom = block.dom; + const designer = dom.querySelector('.general-schema-designer') as HTMLElement; + if (designer) { + designer.style.display = null; + } + dom.style.boxShadow = 'none'; + }; + if (isSameCollection(block.collection, collection)) { + return ( + target.uid === block.uid)} + onChange={(checked) => { + if (checked) { + targets.push({ uid: block.uid }); + } else { + targets = targets.filter((target) => target.uid !== block.uid); + } + + updateFilterTargets(fieldSchema, targets); + dn.emit('patch', { + schema: { + ['x-uid']: uid, + 'x-filter-targets': targets, + }, + }); + dn.refresh(); + }} + onMouseEnter={onHover} + onMouseLeave={onLeave} + /> + ); + } + + const target = targets.find((target) => target.uid === block.uid); + // 与筛选区块的数据表具有关系的表 + return ( + field.target === collection.name) + .map((field) => { + return { + label: compile(field.uiSchema.title) || field.name, + value: `${field.name}.${getTargetKey(field)}`, + }; + }), + { + label: t('Unconnected'), + value: '', + }, + ]} + onChange={(value) => { + if (value === '') { + targets = targets.filter((target) => target.uid !== block.uid); + } else { + targets = targets.filter((target) => target.uid !== block.uid); + targets.push({ uid: block.uid, field: value }); + } + updateFilterTargets(fieldSchema, targets); + dn.emit('patch', { + schema: { + ['x-uid']: uid, + 'x-filter-targets': targets, + }, + }); + dn.refresh(); + }} + onClick={(e) => e.stopPropagation()} + onMouseEnter={onHover} + onMouseLeave={onLeave} + /> + ); + }); + + return ( + + {Content.length ? ( + Content + ) : ( + + )} + + ); +}; + SchemaSettings.SelectItem = (props) => { - const { title, options, value, onChange, ...others } = props; + const { title, options, value, onChange, openOnHover, onClick: _onClick, ...others } = props; + const [open, setOpen] = useState(false); + + const onClick = (...args) => { + setOpen(false); + _onClick?.(...args); + }; + + // 鼠标 hover 时,打开下拉框 + const moreProps = openOnHover + ? { + onMouseEnter: useCallback(() => setOpen(true), []), + open, + } + : {}; + return (
@@ -457,9 +595,11 @@ SchemaSettings.SelectItem = (props) => {