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
This commit is contained in:
Zeke Zhang 2024-11-06 09:28:20 +08:00 committed by GitHub
parent ee1dd2375f
commit a6f16acf1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 458 additions and 166 deletions

View File

@ -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<string>();
const appends = new Set<string>();
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<string>();
const appends = new Set<string>();
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<string>();
const appends = new Set<string>();
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<string>();
const appends = new Set<string>();
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<string>();
const appends = new Set<string>();
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<string>();
const appends = new Set<string>();
getAppends({
schema,
prefix: '',
updateAssociationValues,
appends,
getCollectionJoinField: mockGetCollectionJoinField,
getCollection: mockGetCollection,
dataSource: 'main',
});
expect(Array.from(appends)).toEqual([]);
expect(Array.from(updateAssociationValues)).toEqual([]);
});
});

View File

@ -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<string>;
appends: Set<string>;
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] };
};

View File

@ -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 (
<>
<Card onClick={handleCardClick} bordered={false} hoverable style={cardStyle} className={cardCss}>
<DndContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
<FormLayout
layout={layout}
labelAlign={labelAlign}
labelWidth={layout === 'horizontal' ? labelWidth : null}
labelWrap={labelWrap}
>
<FormProvider form={form}>
<MemorizedRecursionField schema={fieldSchema} onlyRenderProperties />
</FormProvider>
</FormLayout>
</DndContext>
</Card>
<PopupContextProvider visible={visible} setVisible={setVisible}>
<VariablePopupRecordProvider recordData={recordData} collection={collection}>
<MemorizedRecursionField schema={wrappedPopupSchema} />
</VariablePopupRecordProvider>
</PopupContextProvider>
</>
);
},
{ displayName: 'KanbanCard' },
);
return (
<>
<Card onClick={handleCardClick} bordered={false} hoverable style={cardStyle} className={cardCss}>
<DndContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
<FormLayout
layout={layout}
labelAlign={labelAlign}
labelWidth={layout === 'horizontal' ? labelWidth : null}
labelWrap={labelWrap}
>
<FormProvider form={form}>
<MemorizedRecursionField schema={fieldSchema} onlyRenderProperties />
</FormProvider>
</FormLayout>
</DndContext>
</Card>
<PopupContextProvider visible={visible} setVisible={setVisible}>
<VariablePopupRecordProvider recordData={recordData} collection={collection}>
<MemorizedRecursionField schema={wrappedPopupSchema} />
</VariablePopupRecordProvider>
</PopupContextProvider>
</>
);
};
function getPopupSchemaFromParent(fieldSchema: Schema) {
if (fieldSchema.parent?.properties?.cardViewer?.properties?.drawer) {