From a6f16acf1ce939de06dcbb74cdbb4023ce64cefc Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Wed, 6 Nov 2024 09:28:20 +0800 Subject: [PATCH] fix(kanban): fix incorrect appends parameter issue (#5592) * refactor(kanban): optimize get appends function * fix(kanban): fix incorrect appends parameter issue * chore: remove useless code --- .../__tests__/hooks/getAppends.test.ts | 248 ++++++++++++++++++ .../client/src/block-provider/hooks/index.ts | 199 ++++++++------ .../plugin-kanban/src/client/Kanban.Card.tsx | 177 ++++++------- 3 files changed, 458 insertions(+), 166 deletions(-) create mode 100644 packages/core/client/src/block-provider/__tests__/hooks/getAppends.test.ts diff --git a/packages/core/client/src/block-provider/__tests__/hooks/getAppends.test.ts b/packages/core/client/src/block-provider/__tests__/hooks/getAppends.test.ts new file mode 100644 index 0000000000..2ae35a9187 --- /dev/null +++ b/packages/core/client/src/block-provider/__tests__/hooks/getAppends.test.ts @@ -0,0 +1,248 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Schema } from '@formily/json-schema'; +import { describe, expect, it } from 'vitest'; +import { getAppends } from '../../hooks/index'; + +describe('getAppends', () => { + const mockGetCollectionJoinField = (name: string) => { + const fields = { + 'users.profile': { + type: 'hasOne', + target: 'profiles', + }, + 'users.posts': { + type: 'hasMany', + target: 'posts', + }, + 'posts.author': { + type: 'belongsTo', + target: 'users', + }, + 'users.roles': { + type: 'belongsToMany', + target: 'roles', + }, + 'users.categories': { + type: 'belongsToArray', + target: 'categories', + }, + }; + return fields[name]; + }; + + const mockGetCollection = (name: string) => { + const collections = { + categories: { + template: 'tree', + }, + users: { + template: 'general', + }, + }; + return collections[name]; + }; + + const createSchema = (properties) => { + return new Schema({ + properties, + }); + }; + + it('should handle basic association fields', () => { + const schema = createSchema({ + profile: { + 'x-component': 'Input', + 'x-collection-field': 'users.profile', + name: 'profile', + }, + }); + + const updateAssociationValues = new Set(); + const appends = new Set(); + + getAppends({ + schema, + prefix: '', + updateAssociationValues, + appends, + getCollectionJoinField: mockGetCollectionJoinField, + getCollection: mockGetCollection, + dataSource: 'main', + }); + + expect(Array.from(appends)).toEqual(['profile']); + expect(Array.from(updateAssociationValues)).toEqual([]); + }); + + it('should handle tree collection fields', () => { + const schema = createSchema({ + categories: { + 'x-component': 'Input', + 'x-collection-field': 'users.categories', + name: 'categories', + }, + }); + + const updateAssociationValues = new Set(); + const appends = new Set(); + + getAppends({ + schema, + prefix: '', + updateAssociationValues, + appends, + getCollectionJoinField: mockGetCollectionJoinField, + getCollection: mockGetCollection, + dataSource: 'main', + }); + + expect(Array.from(appends)).toEqual(['categories', 'categories.parent(recursively=true)']); + }); + + it('should handle nested fields with sorting', () => { + const schema = createSchema({ + posts: { + 'x-component': 'Input', + 'x-collection-field': 'users.posts', + 'x-component-props': { + sortArr: 'createdAt', + }, + name: 'posts', + }, + }); + + const updateAssociationValues = new Set(); + const appends = new Set(); + + getAppends({ + schema, + prefix: '', + updateAssociationValues, + appends, + getCollectionJoinField: mockGetCollectionJoinField, + getCollection: mockGetCollection, + dataSource: 'main', + }); + + expect(Array.from(appends)).toEqual(['posts(sort=createdAt)']); + }); + + it('should handle nested SubTable mode', () => { + const schema = createSchema({ + posts: { + 'x-component': 'Input', + 'x-collection-field': 'users.posts', + 'x-component-props': { + mode: 'SubTable', + }, + name: 'posts', + properties: { + author: { + 'x-component': 'Input', + 'x-collection-field': 'posts.author', + name: 'author', + }, + }, + }, + }); + + const updateAssociationValues = new Set(); + const appends = new Set(); + + getAppends({ + schema, + prefix: '', + updateAssociationValues, + appends, + getCollectionJoinField: mockGetCollectionJoinField, + getCollection: mockGetCollection, + dataSource: 'main', + }); + + expect(Array.from(appends)).toEqual(['posts', 'posts.author']); + expect(Array.from(updateAssociationValues)).toEqual(['posts']); + }); + + it('should ignore TableField components', () => { + const schema = createSchema({ + posts: { + 'x-component': 'TableField', + 'x-collection-field': 'users.posts', + name: 'posts', + }, + }); + + const updateAssociationValues = new Set(); + const appends = new Set(); + + getAppends({ + schema, + prefix: '', + updateAssociationValues, + appends, + getCollectionJoinField: mockGetCollectionJoinField, + getCollection: mockGetCollection, + dataSource: 'main', + }); + + expect(Array.from(appends)).toEqual([]); + expect(Array.from(updateAssociationValues)).toEqual([]); + }); + + it('should ignore Kanban.CardViewer components', () => { + const schema = createSchema({ + cardViewer: { + 'x-component': 'Kanban.CardViewer', + name: 'cardViewer', + properties: { + drawer: { + name: 'drawer', + type: 'void', + properties: { + grid: { + name: 'grid', + type: 'void', + properties: { + field1: { + 'x-component': 'Input', + 'x-collection-field': 'users.posts', + name: 'field1', + }, + field2: { + 'x-component': 'Input', + 'x-collection-field': 'posts.author', + name: 'field2', + }, + }, + }, + }, + }, + }, + }, + }); + + const updateAssociationValues = new Set(); + const appends = new Set(); + + getAppends({ + schema, + prefix: '', + updateAssociationValues, + appends, + getCollectionJoinField: mockGetCollectionJoinField, + getCollection: mockGetCollection, + dataSource: 'main', + }); + + expect(Array.from(appends)).toEqual([]); + expect(Array.from(updateAssociationValues)).toEqual([]); + }); +}); diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index bf8090bfbe..f06e3f72b8 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -35,7 +35,7 @@ import { import { useAPIClient, useRequest } from '../../api-client'; import { useNavigateNoUpdate } from '../../application/CustomRouterContextProvider'; import { useFormBlockContext } from '../../block-provider/FormBlockProvider'; -import { useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager'; +import { CollectionOptions, useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager'; import { DataBlock, useFilterBlock } from '../../filter-provider/FilterProvider'; import { mergeFilter, transformToFilter } from '../../filter-provider/utils'; import { useTreeParentRecord } from '../../modules/blocks/data-blocks/table/TreeRecordProvider'; @@ -1494,90 +1494,137 @@ export function getAssociationPath(str) { return str; } -export const useAssociationNames = (dataSource?: string) => { - let updateAssociationValues = new Set([]); - let appends = new Set([]); - const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated(dataSource); - const fieldSchema = useFieldSchema(); - const _getAssociationAppends = (schema, str) => { - schema.reduceProperties((pre, s) => { - const prefix = pre || str; - const collectionField = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field'], dataSource); - const isAssociationSubfield = s.name.includes('.'); - const isAssociationField = - collectionField && - ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(collectionField.type); +export const getAppends = ({ + schema, + prefix: defaultPrefix, + updateAssociationValues, + appends, + getCollectionJoinField, + getCollection, + dataSource, +}: { + schema: any; + prefix: string; + updateAssociationValues: Set; + appends: Set; + getCollectionJoinField: (name: string, dataSource: string) => any; + getCollection: (name: any, customDataSource?: string) => CollectionOptions; + dataSource: string; +}) => { + schema.reduceProperties((pre, s) => { + const prefix = pre || defaultPrefix; + const collectionField = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field'], dataSource); + const isAssociationSubfield = s.name.includes('.'); + const isAssociationField = + collectionField && + ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(collectionField.type); - // 根据联动规则中条件的字段获取一些 appends - // 需要排除掉子表格和子表单中的联动规则 - if (s['x-linkage-rules'] && !isSubMode(s)) { - const collectAppends = (obj) => { - const type = Object.keys(obj)[0] || '$and'; - const list = obj[type]; + // 根据联动规则中条件的字段获取一些 appends + // 需要排除掉子表格和子表单中的联动规则 + if (s['x-linkage-rules'] && !isSubMode(s)) { + const collectAppends = (obj) => { + const type = Object.keys(obj)[0] || '$and'; + const list = obj[type]; - list.forEach((item) => { - if ('$and' in item || '$or' in item) { - return collectAppends(item); - } + list.forEach((item) => { + if ('$and' in item || '$or' in item) { + return collectAppends(item); + } - const fieldNames = getTargetField(item); + const fieldNames = getTargetField(item); - // 只应该收集关系字段,只有大于 1 的时候才是关系字段 - if (fieldNames.length > 1) { - appends.add(fieldNames.join('.')); - } - }); - }; + // 只应该收集关系字段,只有大于 1 的时候才是关系字段 + if (fieldNames.length > 1) { + appends.add(fieldNames.join('.')); + } + }); + }; - const rules = s['x-linkage-rules']; - rules.forEach(({ condition }) => { - collectAppends(condition); + const rules = s['x-linkage-rules']; + rules.forEach(({ condition }) => { + collectAppends(condition); + }); + } + + const isTreeCollection = + isAssociationField && getCollection(collectionField.target, dataSource)?.template === 'tree'; + + if (collectionField && (isAssociationField || isAssociationSubfield) && s['x-component'] !== 'TableField') { + const fieldPath = !isAssociationField && isAssociationSubfield ? getAssociationPath(s.name) : s.name; + const path = prefix === '' || !prefix ? fieldPath : prefix + '.' + fieldPath; + if (isTreeCollection) { + appends.add(path); + appends.add(`${path}.parent` + '(recursively=true)'); + } else { + if (s['x-component-props']?.sortArr) { + const sort = s['x-component-props']?.sortArr; + appends.add(`${path}(sort=${sort})`); + } else { + appends.add(path); + } + } + if (isSubMode(s)) { + updateAssociationValues.add(path); + const bufPrefix = prefix && prefix !== '' ? prefix + '.' + s.name : s.name; + getAppends({ + schema: s, + prefix: bufPrefix, + updateAssociationValues, + appends, + getCollectionJoinField, + getCollection, + dataSource, }); } - const isTreeCollection = - isAssociationField && getCollection(collectionField.target, dataSource)?.template === 'tree'; - if (collectionField && (isAssociationField || isAssociationSubfield) && s['x-component'] !== 'TableField') { - const fieldPath = !isAssociationField && isAssociationSubfield ? getAssociationPath(s.name) : s.name; - const path = prefix === '' || !prefix ? fieldPath : prefix + '.' + fieldPath; - if (isTreeCollection) { - appends.add(path); - appends.add(`${path}.parent` + '(recursively=true)'); - } else { - if (s['x-component-props']?.sortArr) { - const sort = s['x-component-props']?.sortArr; - appends.add(`${path}(sort=${sort})`); - } else { - appends.add(path); - } - } - if (['Nester', 'SubTable', 'PopoverNester'].includes(s['x-component-props']?.mode)) { - updateAssociationValues.add(path); - const bufPrefix = prefix && prefix !== '' ? prefix + '.' + s.name : s.name; - _getAssociationAppends(s, bufPrefix); - } - } else if ( - ![ - 'ActionBar', - 'Action', - 'Action.Link', - 'Action.Modal', - 'Selector', - 'Viewer', - 'AddNewer', - 'AssociationField.Selector', - 'AssociationField.AddNewer', - 'TableField', - ].includes(s['x-component']) - ) { - _getAssociationAppends(s, str); - } - }, str); - }; + } else if ( + ![ + 'ActionBar', + 'Action', + 'Action.Link', + 'Action.Modal', + 'Selector', + 'Viewer', + 'AddNewer', + 'AssociationField.Selector', + 'AssociationField.AddNewer', + 'TableField', + 'Kanban.CardViewer', + ].includes(s['x-component']) + ) { + getAppends({ + schema: s, + prefix: defaultPrefix, + updateAssociationValues, + appends, + getCollectionJoinField, + getCollection, + dataSource, + }); + } + }, defaultPrefix); +}; + +export const useAssociationNames = (dataSource?: string) => { + const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated(dataSource); + const fieldSchema = useFieldSchema(); + const getAssociationAppends = () => { - updateAssociationValues = new Set([]); - appends = new Set([]); - _getAssociationAppends(fieldSchema, ''); + const updateAssociationValues = new Set([]); + let appends = new Set([]); + + getAppends({ + schema: fieldSchema, + prefix: '', + updateAssociationValues, + appends, + getCollectionJoinField, + getCollection, + dataSource, + }); appends = fillParentFields(appends); + + console.log('appends', appends); + return { appends: [...appends], updateAssociationValues: [...updateAssociationValues] }; }; diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Card.tsx b/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Card.tsx index 9fe462e9b8..d554caef17 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Card.tsx +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/Kanban.Card.tsx @@ -10,17 +10,17 @@ import { css } from '@emotion/css'; import { FormLayout } from '@formily/antd-v5'; import { createForm } from '@formily/core'; -import { observer, RecursionField, useFieldSchema } from '@formily/react'; +import { RecursionField, useFieldSchema } from '@formily/react'; import { DndContext, FormProvider, + getCardItemSchema, PopupContextProvider, useCollection, useCollectionRecordData, usePopupSettings, usePopupUtils, VariablePopupRecordProvider, - getCardItemSchema, } from '@nocobase/client'; import { Schema } from '@nocobase/utils'; import { Card } from 'antd'; @@ -68,98 +68,95 @@ const cardCss = css` const MemorizedRecursionField = React.memo(RecursionField); MemorizedRecursionField.displayName = 'MemorizedRecursionField'; -export const KanbanCard: any = observer( - () => { - const collection = useCollection(); - const { setDisableCardDrag } = useContext(KanbanCardContext) || {}; - const fieldSchema = useFieldSchema(); - const { openPopup, getPopupSchemaFromSchema } = usePopupUtils(); - const recordData = useCollectionRecordData(); - const popupSchema = getPopupSchemaFromSchema(fieldSchema) || getPopupSchemaFromParent(fieldSchema); - const [visible, setVisible] = useState(false); - const { isPopupVisibleControlledByURL } = usePopupSettings(); - const handleCardClick = useCallback( - (e: React.MouseEvent) => { - const targetElement = e.target as Element; // 将事件目标转换为Element类型 - const currentTargetElement = e.currentTarget as Element; - if (currentTargetElement.contains(targetElement)) { - if (!isPopupVisibleControlledByURL()) { - setVisible(true); - } else { - openPopup({ - popupUidUsedInURL: popupSchema?.['x-uid'], - }); - } - e.stopPropagation(); +export const KanbanCard: any = () => { + const collection = useCollection(); + const { setDisableCardDrag } = useContext(KanbanCardContext) || {}; + const fieldSchema = useFieldSchema(); + const { openPopup, getPopupSchemaFromSchema } = usePopupUtils(); + const recordData = useCollectionRecordData(); + const popupSchema = getPopupSchemaFromSchema(fieldSchema) || getPopupSchemaFromParent(fieldSchema); + const [visible, setVisible] = useState(false); + const { isPopupVisibleControlledByURL } = usePopupSettings(); + const handleCardClick = useCallback( + (e: React.MouseEvent) => { + const targetElement = e.target as Element; // 将事件目标转换为Element类型 + const currentTargetElement = e.currentTarget as Element; + if (currentTargetElement.contains(targetElement)) { + if (!isPopupVisibleControlledByURL()) { + setVisible(true); } else { - e.stopPropagation(); + openPopup({ + popupUidUsedInURL: popupSchema?.['x-uid'], + }); } + e.stopPropagation(); + } else { + e.stopPropagation(); + } + }, + [openPopup, popupSchema], + ); + const cardStyle = useMemo(() => { + return { + cursor: 'pointer', + overflow: 'hidden', + }; + }, []); + + const form = useMemo(() => { + return createForm({ + values: recordData, + }); + }, [recordData]); + + const onDragStart = useCallback(() => { + setDisableCardDrag?.(true); + }, [setDisableCardDrag]); + const onDragEnd = useCallback(() => { + setDisableCardDrag?.(false); + }, [setDisableCardDrag]); + + // if not wrapped, only Tab component's content will be rendered, Drawer component's content will not be rendered + const wrappedPopupSchema = useMemo(() => { + return { + type: 'void', + properties: { + drawer: popupSchema, }, - [openPopup, popupSchema], - ); - const cardStyle = useMemo(() => { - return { - cursor: 'pointer', - overflow: 'hidden', - }; - }, []); + }; + }, [popupSchema]); + const cardItemSchema = getCardItemSchema?.(fieldSchema); + const { + layout = 'vertical', + labelAlign = 'left', + labelWidth = 120, + labelWrap = true, + } = cardItemSchema?.['x-component-props'] || {}; - const form = useMemo(() => { - return createForm({ - values: recordData, - }); - }, [recordData]); - - const onDragStart = useCallback(() => { - setDisableCardDrag?.(true); - }, [setDisableCardDrag]); - const onDragEnd = useCallback(() => { - setDisableCardDrag?.(false); - }, [setDisableCardDrag]); - - // if not wrapped, only Tab component's content will be rendered, Drawer component's content will not be rendered - const wrappedPopupSchema = useMemo(() => { - return { - type: 'void', - properties: { - drawer: popupSchema, - }, - }; - }, [popupSchema]); - const cardItemSchema = getCardItemSchema?.(fieldSchema); - const { - layout = 'vertical', - labelAlign = 'left', - labelWidth = 120, - labelWrap = true, - } = cardItemSchema?.['x-component-props'] || {}; - - return ( - <> - - - - - - - - - - - - - - - - ); - }, - { displayName: 'KanbanCard' }, -); + return ( + <> + + + + + + + + + + + + + + + + ); +}; function getPopupSchemaFromParent(fieldSchema: Schema) { if (fieldSchema.parent?.properties?.cardViewer?.properties?.drawer) {