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 { useAPIClient, useRequest } from '../../api-client';
import { useNavigateNoUpdate } from '../../application/CustomRouterContextProvider'; import { useNavigateNoUpdate } from '../../application/CustomRouterContextProvider';
import { useFormBlockContext } from '../../block-provider/FormBlockProvider'; 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 { DataBlock, useFilterBlock } from '../../filter-provider/FilterProvider';
import { mergeFilter, transformToFilter } from '../../filter-provider/utils'; import { mergeFilter, transformToFilter } from '../../filter-provider/utils';
import { useTreeParentRecord } from '../../modules/blocks/data-blocks/table/TreeRecordProvider'; import { useTreeParentRecord } from '../../modules/blocks/data-blocks/table/TreeRecordProvider';
@ -1494,90 +1494,137 @@ export function getAssociationPath(str) {
return str; return str;
} }
export const useAssociationNames = (dataSource?: string) => { export const getAppends = ({
let updateAssociationValues = new Set([]); schema,
let appends = new Set([]); prefix: defaultPrefix,
const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated(dataSource); updateAssociationValues,
const fieldSchema = useFieldSchema(); appends,
const _getAssociationAppends = (schema, str) => { getCollectionJoinField,
schema.reduceProperties((pre, s) => { getCollection,
const prefix = pre || str; dataSource,
const collectionField = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field'], dataSource); }: {
const isAssociationSubfield = s.name.includes('.'); schema: any;
const isAssociationField = prefix: string;
collectionField && updateAssociationValues: Set<string>;
['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(collectionField.type); 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 // 根据联动规则中条件的字段获取一些 appends
// 需要排除掉子表格和子表单中的联动规则 // 需要排除掉子表格和子表单中的联动规则
if (s['x-linkage-rules'] && !isSubMode(s)) { if (s['x-linkage-rules'] && !isSubMode(s)) {
const collectAppends = (obj) => { const collectAppends = (obj) => {
const type = Object.keys(obj)[0] || '$and'; const type = Object.keys(obj)[0] || '$and';
const list = obj[type]; const list = obj[type];
list.forEach((item) => { list.forEach((item) => {
if ('$and' in item || '$or' in item) { if ('$and' in item || '$or' in item) {
return collectAppends(item); return collectAppends(item);
} }
const fieldNames = getTargetField(item); const fieldNames = getTargetField(item);
// 只应该收集关系字段,只有大于 1 的时候才是关系字段 // 只应该收集关系字段,只有大于 1 的时候才是关系字段
if (fieldNames.length > 1) { if (fieldNames.length > 1) {
appends.add(fieldNames.join('.')); appends.add(fieldNames.join('.'));
} }
}); });
}; };
const rules = s['x-linkage-rules']; const rules = s['x-linkage-rules'];
rules.forEach(({ condition }) => { rules.forEach(({ condition }) => {
collectAppends(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 = } else if (
isAssociationField && getCollection(collectionField.target, dataSource)?.template === 'tree'; ![
if (collectionField && (isAssociationField || isAssociationSubfield) && s['x-component'] !== 'TableField') { 'ActionBar',
const fieldPath = !isAssociationField && isAssociationSubfield ? getAssociationPath(s.name) : s.name; 'Action',
const path = prefix === '' || !prefix ? fieldPath : prefix + '.' + fieldPath; 'Action.Link',
if (isTreeCollection) { 'Action.Modal',
appends.add(path); 'Selector',
appends.add(`${path}.parent` + '(recursively=true)'); 'Viewer',
} else { 'AddNewer',
if (s['x-component-props']?.sortArr) { 'AssociationField.Selector',
const sort = s['x-component-props']?.sortArr; 'AssociationField.AddNewer',
appends.add(`${path}(sort=${sort})`); 'TableField',
} else { 'Kanban.CardViewer',
appends.add(path); ].includes(s['x-component'])
} ) {
} getAppends({
if (['Nester', 'SubTable', 'PopoverNester'].includes(s['x-component-props']?.mode)) { schema: s,
updateAssociationValues.add(path); prefix: defaultPrefix,
const bufPrefix = prefix && prefix !== '' ? prefix + '.' + s.name : s.name; updateAssociationValues,
_getAssociationAppends(s, bufPrefix); appends,
} getCollectionJoinField,
} else if ( getCollection,
![ dataSource,
'ActionBar', });
'Action', }
'Action.Link', }, defaultPrefix);
'Action.Modal', };
'Selector',
'Viewer', export const useAssociationNames = (dataSource?: string) => {
'AddNewer', const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated(dataSource);
'AssociationField.Selector', const fieldSchema = useFieldSchema();
'AssociationField.AddNewer',
'TableField',
].includes(s['x-component'])
) {
_getAssociationAppends(s, str);
}
}, str);
};
const getAssociationAppends = () => { const getAssociationAppends = () => {
updateAssociationValues = new Set([]); const updateAssociationValues = new Set([]);
appends = new Set([]); let appends = new Set([]);
_getAssociationAppends(fieldSchema, '');
getAppends({
schema: fieldSchema,
prefix: '',
updateAssociationValues,
appends,
getCollectionJoinField,
getCollection,
dataSource,
});
appends = fillParentFields(appends); appends = fillParentFields(appends);
console.log('appends', appends);
return { appends: [...appends], updateAssociationValues: [...updateAssociationValues] }; return { appends: [...appends], updateAssociationValues: [...updateAssociationValues] };
}; };

View File

@ -10,17 +10,17 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { FormLayout } from '@formily/antd-v5'; import { FormLayout } from '@formily/antd-v5';
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { observer, RecursionField, useFieldSchema } from '@formily/react'; import { RecursionField, useFieldSchema } from '@formily/react';
import { import {
DndContext, DndContext,
FormProvider, FormProvider,
getCardItemSchema,
PopupContextProvider, PopupContextProvider,
useCollection, useCollection,
useCollectionRecordData, useCollectionRecordData,
usePopupSettings, usePopupSettings,
usePopupUtils, usePopupUtils,
VariablePopupRecordProvider, VariablePopupRecordProvider,
getCardItemSchema,
} from '@nocobase/client'; } from '@nocobase/client';
import { Schema } from '@nocobase/utils'; import { Schema } from '@nocobase/utils';
import { Card } from 'antd'; import { Card } from 'antd';
@ -68,98 +68,95 @@ const cardCss = css`
const MemorizedRecursionField = React.memo(RecursionField); const MemorizedRecursionField = React.memo(RecursionField);
MemorizedRecursionField.displayName = 'MemorizedRecursionField'; MemorizedRecursionField.displayName = 'MemorizedRecursionField';
export const KanbanCard: any = observer( export const KanbanCard: any = () => {
() => { const collection = useCollection();
const collection = useCollection(); const { setDisableCardDrag } = useContext(KanbanCardContext) || {};
const { setDisableCardDrag } = useContext(KanbanCardContext) || {}; const fieldSchema = useFieldSchema();
const fieldSchema = useFieldSchema(); const { openPopup, getPopupSchemaFromSchema } = usePopupUtils();
const { openPopup, getPopupSchemaFromSchema } = usePopupUtils(); const recordData = useCollectionRecordData();
const recordData = useCollectionRecordData(); const popupSchema = getPopupSchemaFromSchema(fieldSchema) || getPopupSchemaFromParent(fieldSchema);
const popupSchema = getPopupSchemaFromSchema(fieldSchema) || getPopupSchemaFromParent(fieldSchema); const [visible, setVisible] = useState(false);
const [visible, setVisible] = useState(false); const { isPopupVisibleControlledByURL } = usePopupSettings();
const { isPopupVisibleControlledByURL } = usePopupSettings(); const handleCardClick = useCallback(
const handleCardClick = useCallback( (e: React.MouseEvent) => {
(e: React.MouseEvent) => { const targetElement = e.target as Element; // 将事件目标转换为Element类型
const targetElement = e.target as Element; // 将事件目标转换为Element类型 const currentTargetElement = e.currentTarget as Element;
const currentTargetElement = e.currentTarget as Element; if (currentTargetElement.contains(targetElement)) {
if (currentTargetElement.contains(targetElement)) { if (!isPopupVisibleControlledByURL()) {
if (!isPopupVisibleControlledByURL()) { setVisible(true);
setVisible(true);
} else {
openPopup({
popupUidUsedInURL: popupSchema?.['x-uid'],
});
}
e.stopPropagation();
} else { } 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], };
); }, [popupSchema]);
const cardStyle = useMemo(() => { const cardItemSchema = getCardItemSchema?.(fieldSchema);
return { const {
cursor: 'pointer', layout = 'vertical',
overflow: 'hidden', labelAlign = 'left',
}; labelWidth = 120,
}, []); labelWrap = true,
} = cardItemSchema?.['x-component-props'] || {};
const form = useMemo(() => { return (
return createForm({ <>
values: recordData, <Card onClick={handleCardClick} bordered={false} hoverable style={cardStyle} className={cardCss}>
}); <DndContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
}, [recordData]); <FormLayout
layout={layout}
const onDragStart = useCallback(() => { labelAlign={labelAlign}
setDisableCardDrag?.(true); labelWidth={layout === 'horizontal' ? labelWidth : null}
}, [setDisableCardDrag]); labelWrap={labelWrap}
const onDragEnd = useCallback(() => { >
setDisableCardDrag?.(false); <FormProvider form={form}>
}, [setDisableCardDrag]); <MemorizedRecursionField schema={fieldSchema} onlyRenderProperties />
</FormProvider>
// if not wrapped, only Tab component's content will be rendered, Drawer component's content will not be rendered </FormLayout>
const wrappedPopupSchema = useMemo(() => { </DndContext>
return { </Card>
type: 'void', <PopupContextProvider visible={visible} setVisible={setVisible}>
properties: { <VariablePopupRecordProvider recordData={recordData} collection={collection}>
drawer: popupSchema, <MemorizedRecursionField schema={wrappedPopupSchema} />
}, </VariablePopupRecordProvider>
}; </PopupContextProvider>
}, [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' },
);
function getPopupSchemaFromParent(fieldSchema: Schema) { function getPopupSchemaFromParent(fieldSchema: Schema) {
if (fieldSchema.parent?.properties?.cardViewer?.properties?.drawer) { if (fieldSchema.parent?.properties?.cardViewer?.properties?.drawer) {