feat: configure fields support serach and filter fields (#5471)

* feat: configure fields support serach and filter fields

* feat: configure fields support serach and filter fields

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: test

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug
This commit is contained in:
Katherine 2024-11-01 14:28:46 +08:00 committed by GitHub
parent 5685b545fe
commit c639e7d403
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 196 additions and 5 deletions

View File

@ -71,11 +71,13 @@ describe('useGetSchemaInitializerMenuItems', () => {
"key": "parent-2-item1-0", "key": "parent-2-item1-0",
"label": "item1", "label": "item1",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
{ {
"key": "parent-2-item2-1", "key": "parent-2-item2-1",
"label": "item2", "label": "item2",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
{ {
"associationField": "a.b", "associationField": "a.b",
@ -139,6 +141,7 @@ describe('useGetSchemaInitializerMenuItems', () => {
"key": "group-0-Item 1-0", "key": "group-0-Item 1-0",
"label": "Item 1", "label": "Item 1",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
], ],
"key": "group-0", "key": "group-0",
@ -151,6 +154,7 @@ describe('useGetSchemaInitializerMenuItems', () => {
"key": "parent-item-group-1-Item 1-0", "key": "parent-item-group-1-Item 1-0",
"label": "Item 1", "label": "Item 1",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
], ],
"key": "parent-item-group-1", "key": "parent-item-group-1",
@ -204,6 +208,7 @@ describe('useGetSchemaInitializerMenuItems', () => {
"key": "submenu-1-SubItem 1-0", "key": "submenu-1-SubItem 1-0",
"label": "SubItem 1", "label": "SubItem 1",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
], ],
"key": "submenu-1", "key": "submenu-1",
@ -215,6 +220,7 @@ describe('useGetSchemaInitializerMenuItems', () => {
"key": "submenu-2-SubItem 1-0", "key": "submenu-2-SubItem 1-0",
"label": "SubItem 1", "label": "SubItem 1",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
], ],
"key": "submenu-2", "key": "submenu-2",
@ -226,6 +232,7 @@ describe('useGetSchemaInitializerMenuItems', () => {
"key": "submenu-3-SubItem 1-0", "key": "submenu-3-SubItem 1-0",
"label": "SubItem 1", "label": "SubItem 1",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
], ],
"key": "submenu-3", "key": "submenu-3",
@ -289,11 +296,13 @@ describe('useSchemaInitializerMenuItems', () => {
"key": 1, "key": 1,
"label": "item1", "label": "item1",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
{ {
"key": 2, "key": 2,
"label": "item2", "label": "item2",
"onClick": [Function], "onClick": [Function],
"style": undefined,
}, },
] ]
`); `);

View File

@ -15,7 +15,7 @@ import { SchemaInitializerOptions } from '../types';
import { SchemaInitializerChildren } from './SchemaInitializerChildren'; import { SchemaInitializerChildren } from './SchemaInitializerChildren';
import { SchemaInitializerDivider } from './SchemaInitializerDivider'; import { SchemaInitializerDivider } from './SchemaInitializerDivider';
import { useSchemaInitializerStyles } from './style'; import { useSchemaInitializerStyles } from './style';
import { useMenuSearch } from './SchemaInitializerItemSearchFields';
export interface SchemaInitializerItemGroupProps { export interface SchemaInitializerItemGroupProps {
title: string; title: string;
children?: SchemaInitializerOptions['items']; children?: SchemaInitializerOptions['items'];
@ -45,6 +45,8 @@ export const SchemaInitializerItemGroup: FC<SchemaInitializerItemGroupProps> = (
* @internal * @internal
*/ */
export const SchemaInitializerItemGroupInternal = () => { export const SchemaInitializerItemGroupInternal = () => {
const itemConfig = useSchemaInitializerItem<SchemaInitializerItemGroupProps>(); const itemConfig: any = useSchemaInitializerItem<SchemaInitializerItemGroupProps>();
return <SchemaInitializerItemGroup {...itemConfig} />; const searchedChildren = useMenuSearch(itemConfig);
/* eslint-disable react/no-children-prop */
return <SchemaInitializerItemGroup {...itemConfig} children={searchedChildren} />;
}; };

View File

@ -0,0 +1,180 @@
/**
* 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 { uid } from '@formily/shared';
import { Divider, Empty, Input, MenuProps } from 'antd';
import React, { useEffect, useMemo, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
function getPrefixAndCompare(a, b) {
const prefixA = a.replace(/-displayCollectionFields$/, '');
const prefixB = b.replace(/-displayCollectionFields$/, '');
// 判断 a 是否包含 b如果包含则返回 false否则返回 true
return !prefixA.includes(prefixB);
}
export const SearchFields = ({ value: outValue, onChange, name }) => {
const { t } = useTranslation();
const [value, setValue] = useState<string>(outValue);
const inputRef = useRef<any>('');
// 生成唯一的ID用于区分不同层级的SearchFields
const uniqueId = useRef(`${name || Math.random().toString(10).substr(2, 9)}`);
useEffect(() => {
setValue(outValue);
}, [outValue]);
useEffect(() => {
const focusInput = () => {
if (
document.activeElement?.id !== inputRef.current.input.id &&
getPrefixAndCompare(document.activeElement?.id, inputRef.current.input.id)
) {
inputRef.current?.focus();
}
};
// 观察当前元素是否在视图中
const observer = new IntersectionObserver((entries) => {
if (entries.some((v) => v.isIntersecting)) {
focusInput();
}
});
if (inputRef.current?.input) {
inputRef.current.input.id = uniqueId.current; // 设置唯一ID
observer.observe(inputRef.current.input);
}
return () => {
observer.disconnect();
};
}, []);
const compositionRef = useRef<boolean>(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!compositionRef.current) {
onChange(e.target.value);
setValue(e.target.value);
}
};
const Composition = (e: React.CompositionEvent<HTMLInputElement> | any) => {
if (e.type === 'compositionend') {
compositionRef.current = false;
handleChange(e);
} else {
compositionRef.current = true;
}
};
return (
<div onClick={(e) => e.stopPropagation()}>
<Input
ref={inputRef}
allowClear
style={{ padding: '0 4px 6px 16px', boxShadow: 'none' }}
bordered={false}
placeholder={t('Search')}
defaultValue={value}
onClick={(e) => {
e.stopPropagation();
}}
onChange={handleChange}
onCompositionStart={Composition}
onCompositionEnd={Composition}
onCompositionUpdate={Composition}
/>
<Divider style={{ margin: 0 }} />
</div>
);
};
export const useMenuSearch = (props: { children: any[]; showType?: boolean; hideSearch?: boolean; name?: string }) => {
const { children, showType, hideSearch, name } = props;
const items = children?.concat?.() || [];
const [searchValue, setSearchValue] = useState(null);
// 处理搜索逻辑
const limitedSearchedItems = useMemo(() => {
if (!searchValue || searchValue === '') {
return items;
}
const lowerSearchValue = searchValue.toLocaleLowerCase();
return items.filter(
(item) =>
(item.label || item.title) &&
String(item.label || item.title)
.toLocaleLowerCase()
.includes(lowerSearchValue),
);
}, [searchValue, items]);
// 最终结果项
const resultItems = useMemo<MenuProps['items']>(() => {
const res = [];
if (!hideSearch && (items.length > 10 || searchValue)) {
res.push({
key: `search-${uid()}`,
Component: () => (
<SearchFields
name={name}
value={searchValue}
onChange={(val: string) => {
setSearchValue(val);
}}
/>
),
onClick({ domEvent }) {
domEvent.stopPropagation();
},
...(showType ? { isMenuType: true } : {}),
});
}
if (limitedSearchedItems.length > 0) {
res.push(...limitedSearchedItems);
} else {
res.push({
key: 'empty',
style: {
height: 150,
},
Component: () => (
<div onClick={(e) => e.stopPropagation()}>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
</div>
),
...(showType ? { isMenuType: true } : {}),
});
}
return res;
}, [hideSearch, limitedSearchedItems, searchValue, showType]);
const result = processedResult(resultItems, showType, hideSearch, name);
return children ? result : undefined;
};
// 处理嵌套子菜单
const processedResult = (resultItems, showType, hideSearch, name) => {
return resultItems.map((item: any) => {
if (['subMenu', 'itemGroup'].includes(item.type)) {
const childItems = useMenuSearch({
children: item.children,
showType,
hideSearch,
name: item.name,
});
return { ...item, children: childItems };
}
return item;
});
};

View File

@ -101,6 +101,7 @@ export function useGetSchemaInitializerMenuItems(onClick?: (args: any) => void)
onClick: handleClick, onClick: handleClick,
} }
: { : {
style: item.style,
key, key,
label, label,
onClick: handleClick, onClick: handleClick,

View File

@ -547,7 +547,7 @@ export class PluginACLServer extends Plugin {
const hasFilterByTk = (params) => { const hasFilterByTk = (params) => {
return JSON.stringify(params).includes('filterByTk'); return JSON.stringify(params).includes('filterByTk');
} };
if (!hasFilterByTk(ctx.permission.mergedParams) || !hasFilterByTk(ctx.permission.rawParams)) { if (!hasFilterByTk(ctx.permission.mergedParams) || !hasFilterByTk(ctx.permission.rawParams)) {
await next(); await next();
@ -574,7 +574,6 @@ export class PluginACLServer extends Plugin {
}, },
); );
const withACLMeta = createWithACLMetaMiddleware(); const withACLMeta = createWithACLMetaMiddleware();
// append allowedActions to list & get response // append allowedActions to list & get response