diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index 980bc0146ffbb..ca0ca45325d74 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -32,7 +32,7 @@ import { import { useAPIClient, useRequest } from '../../api-client'; import { useFormBlockContext } from '../../block-provider/FormBlockProvider'; import { useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager'; -import { useFilterBlock } from '../../filter-provider/FilterProvider'; +import { DataBlock, useFilterBlock } from '../../filter-provider/FilterProvider'; import { mergeFilter, transformToFilter } from '../../filter-provider/utils'; import { useTreeParentRecord } from '../../modules/blocks/data-blocks/table/TreeRecordProvider'; import { useRecord } from '../../record-provider'; @@ -430,6 +430,10 @@ const useDoFilter = () => { const { name } = useCollection(); const { targets = [], uid } = useMemo(() => findFilterTargets(fieldSchema), [fieldSchema]); + const getFilterFromCurrentForm = useCallback(() => { + return removeNullCondition(transformToFilter(form.values, getOperators(), getCollectionJoinField, name)); + }, [form.values, getCollectionJoinField, getOperators, name]); + const doFilter = useCallback( async ({ doNothingWhenFilterIsEmpty = false } = {}) => { try { @@ -443,20 +447,19 @@ const useDoFilter = () => { // 保留原有的 filter const storedFilter = block.service.params?.[1]?.filters || {}; - storedFilter[uid] = removeNullCondition( - transformToFilter(form.values, getOperators(), getCollectionJoinField, name), - ); + // 由当前表单转换而来的 filter + storedFilter[uid] = getFilterFromCurrentForm(); const mergedFilter = mergeFilter([ ...Object.values(storedFilter).map((filter) => removeNullCondition(filter)), block.defaultFilter, ]); - if (doNothingWhenFilterIsEmpty && _.isEmpty(mergedFilter)) { + if (doNothingWhenFilterIsEmpty && _.isEmpty(storedFilter[uid])) { return; } - if (block.dataLoadingMode === 'manual' && _.isEmpty(mergedFilter)) { + if (block.dataLoadingMode === 'manual' && _.isEmpty(storedFilter[uid])) { return block.clearData(); } @@ -474,7 +477,7 @@ const useDoFilter = () => { console.error(error); } }, - [form.values, getCollectionJoinField, getDataBlocks, getOperators, name, targets, uid], + [getDataBlocks, getFilterFromCurrentForm, targets, uid], ); // 这里的代码是为了实现:筛选表单的筛选操作在首次渲染时自动执行一次 @@ -487,6 +490,10 @@ const useDoFilter = () => { * 用于执行筛选表单的筛选操作 */ doFilter, + /** + * 根据当前表单的值获取 filter + */ + getFilterFromCurrentForm, }; }; @@ -504,52 +511,35 @@ export const useFilterBlockActionProps = () => { }; }; -export const useResetBlockActionProps = () => { +const useDoReset = () => { const form = useForm(); - const actionField = useField(); const fieldSchema = useFieldSchema(); const { getDataBlocks } = useFilterBlock(); + const { targets, uid } = findFilterTargets(fieldSchema); + const { doFilter, getFilterFromCurrentForm } = useDoFilter(); + + return { + doReset: async () => { + await form.reset(); + if (_.isEmpty(getFilterFromCurrentForm())) { + return doReset({ getDataBlocks, targets, uid }); + } + await doFilter(); + }, + }; +}; + +export const useResetBlockActionProps = () => { + const actionField = useField(); + const { doReset } = useDoReset(); 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; - - if (block.dataLoadingMode === 'manual') { - return block.clearData(); - } - - 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; - } + await doReset(); + actionField.data.loading = false; }, }; }; @@ -1347,6 +1337,52 @@ export const useAssociationFilterBlockProps = () => { labelKey, }; }; +async function doReset({ + getDataBlocks, + targets, + uid, +}: { + getDataBlocks: () => DataBlock[]; + targets: { + /** field uid */ + uid: string; + /** associated field */ + field?: string; + }[]; + uid: string; +}) { + try { + await Promise.all( + getDataBlocks().map(async (block) => { + const target = targets.find((target) => target.uid === block.uid); + if (!target) return; + + if (block.dataLoadingMode === 'manual') { + return block.clearData(); + } + + 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 }, + ); + }), + ); + } catch (error) { + console.error(error); + } +} + export function getAssociationPath(str) { const lastIndex = str.lastIndexOf('.'); if (lastIndex !== -1) { diff --git a/packages/core/client/src/data-source/__tests__/utils.test.ts b/packages/core/client/src/data-source/__tests__/utils.test.ts index 5f00ddede94ca..2ad4873dd2b6a 100644 --- a/packages/core/client/src/data-source/__tests__/utils.test.ts +++ b/packages/core/client/src/data-source/__tests__/utils.test.ts @@ -8,11 +8,11 @@ */ import { + Application, CollectionFieldInterface, + DEFAULT_DATA_SOURCE_KEY, isTitleField, - Application, useDataSourceHeaders, - DEFAULT_DATA_SOURCE_KEY, } from '@nocobase/client'; import { renderHook } from '@nocobase/test/client'; diff --git a/packages/core/client/src/filter-provider/__tests__/utiles.test.ts b/packages/core/client/src/filter-provider/__tests__/utiles.test.ts index 024d20ab655fd..7da0fc2a42960 100644 --- a/packages/core/client/src/filter-provider/__tests__/utiles.test.ts +++ b/packages/core/client/src/filter-provider/__tests__/utiles.test.ts @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { getSupportFieldsByAssociation, getSupportFieldsByForeignKey } from '../utils'; +import { getSupportFieldsByAssociation, getSupportFieldsByForeignKey, transformToFilter } from '../utils'; describe('getSupportFieldsByAssociation', () => { it('should return all associated fields matching the inherited collections chain', () => { @@ -140,3 +140,80 @@ describe('getSupportFieldsByForeignKey', () => { ]); }); }); + +describe('transformToFilter', () => { + const values = { + field1: 'value1', + field2: 'value2', + field3: [ + { + id: 'value3', + }, + { + id: 'value4', + }, + ], + }; + + const operators = { + field1: '$eq', + field2: '$ne', + field3: '$in', + }; + + const collectionName = 'collection'; + + const getCollectionJoinField = vi.fn((name: string) => { + if (name === `${collectionName}.field1`) return {}; + if (name === `${collectionName}.field2`) return {}; + if (name === `${collectionName}.field3`) return { target: 'targetCollection', targetKey: 'id' }; + return {}; + }); + + it('should transform values to filter', () => { + const expectedFilter = { + $and: [ + { + field1: { + $eq: 'value1', + }, + }, + { + field2: { + $ne: 'value2', + }, + }, + { + 'field3.id': { + $eq: ['value3', 'value4'], + }, + }, + ], + }; + + const filter = transformToFilter(values, operators, getCollectionJoinField, collectionName); + + expect(filter).toEqual(expectedFilter); + }); + + it('should handle null values', () => { + const valuesWithNull = { + field1: null, + field2: 'value2', + }; + + const expectedFilter = { + $and: [ + { + field2: { + $ne: 'value2', + }, + }, + ], + }; + + const filter = transformToFilter(valuesWithNull, operators, getCollectionJoinField, collectionName); + + expect(filter).toEqual(expectedFilter); + }); +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/setDataLoadingModeSettingsItem.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/setDataLoadingModeSettingsItem.test.ts index a53ec7dfa6d5b..27be89640d664 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/setDataLoadingModeSettingsItem.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/setDataLoadingModeSettingsItem.test.ts @@ -8,7 +8,7 @@ */ import { expect, test } from '@nocobase/test/e2e'; -import { tableListDetailsGridCardWithUsers } from './templatesOfBug'; +import { TableBlockWithDataScope, tableListDetailsGridCardWithUsers } from './templatesOfBug'; test.describe('setDataLoadingModeSettingsItem', () => { test('basic', async ({ page, mockPage }) => { @@ -72,4 +72,30 @@ test.describe('setDataLoadingModeSettingsItem', () => { await expect(page.getByLabel('block-item-CardItem-users-list').getByText('No data')).toBeVisible(); await expect(page.getByLabel('block-item-BlockItem-users-').getByText('No data')).toBeVisible(); }); + + test('When the data block has data scope settings and dataLoadingMode is manual, data should not be displayed after the first page load', async ({ + page, + mockPage, + }) => { + await mockPage(TableBlockWithDataScope).goto(); + await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible(); + + // 此时点击 filter 按钮,应该还是没数据,因为表单没有值 + await page.getByLabel('action-Action-Filter-submit-').click({ + position: { + x: 10, + y: 10, + }, + }); + await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible(); + + // 点击 Reset 按钮,也是一样 + await page.getByLabel('action-Action-Reset-users-').click({ + position: { + x: 10, + y: 10, + }, + }); + await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible(); + }); }); diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/templatesOfBug.ts index de9bf8cd84c2f..98bfa21417c28 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/templatesOfBug.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/templatesOfBug.ts @@ -1558,3 +1558,366 @@ export const detailsBlockWithLinkageRule = { 'x-index': 1, }, }; +export const TableBlockWithDataScope = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + an34615vknp: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + '62fssedpl0w': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.0.1-alpha.2', + properties: { + '93tyrk65qym': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.0.1-alpha.2', + properties: { + '0b7maevxkfs': { + 'x-uid': 'uqnziogzhkq', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'FilterFormBlockProvider', + 'x-use-decorator-props': 'useFilterFormBlockDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + collection: 'users', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:filterForm', + 'x-component': 'CardItem', + 'x-filter-targets': [ + { + uid: 'yg26txxpq4l', + }, + ], + 'x-app-version': '1.0.1-alpha.2', + properties: { + swmsovm7d4t: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useFilterFormBlockProps', + 'x-app-version': '1.0.1-alpha.2', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'filterForm:configureFields', + 'x-app-version': '1.0.1-alpha.2', + properties: { + mzd4ludncd8: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.0.1-alpha.2', + properties: { + w67kg0y8wn6: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.0.1-alpha.2', + properties: { + nickname: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + required: false, + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FilterFormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-use-decorator-props': 'useFormItemProps', + 'x-collection-field': 'users.nickname', + 'x-component-props': {}, + 'x-app-version': '1.0.1-alpha.2', + 'x-uid': '3azj6wv3v2r', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '1jmg205s2sa', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'wjoskqiuk5b', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 't3gxqyl52ca', + 'x-async': false, + 'x-index': 1, + }, + ceahs5hahgg: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'filterForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + style: { + float: 'right', + }, + }, + 'x-app-version': '1.0.1-alpha.2', + properties: { + xgjhwz2ln8l: { + _isJSONSchemaObject: true, + version: '2.0', + title: '{{ t("Filter") }}', + 'x-action': 'submit', + 'x-component': 'Action', + 'x-use-component-props': 'useFilterBlockActionProps', + 'x-designer': 'Action.Designer', + 'x-component-props': { + type: 'primary', + htmlType: 'submit', + }, + 'x-action-settings': {}, + type: 'void', + 'x-app-version': '1.0.1-alpha.2', + 'x-uid': 'ev40o2gk87b', + 'x-async': false, + 'x-index': 1, + }, + qxhdilp319s: { + _isJSONSchemaObject: true, + version: '2.0', + title: '{{ t("Reset") }}', + 'x-component': 'Action', + 'x-use-component-props': 'useResetBlockActionProps', + 'x-designer': 'Action.Designer', + 'x-action-settings': {}, + type: 'void', + 'x-app-version': '1.0.1-alpha.2', + 'x-uid': '559bivcwabh', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '8q25gzurfeh', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'v3mwerc709f', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '952up9gdrhf', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'da58ptz1cie', + 'x-async': false, + 'x-index': 1, + }, + dxd8oaoh4a7: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.0.1-alpha.2', + properties: { + '27ijajdwyd2': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.0.1-alpha.2', + properties: { + '7yz5da41nt3': { + 'x-uid': 'yg26txxpq4l', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + filter: { + $and: [ + { + nickname: { + $includes: '{{$user.nickname}}', + }, + }, + ], + }, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + dataLoadingMode: 'manual', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.0.1-alpha.2', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.0.1-alpha.2', + 'x-uid': '73fy8oxxxk9', + 'x-async': false, + 'x-index': 1, + }, + wo6mmz3gm4e: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.0.1-alpha.2', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.0.1-alpha.2', + properties: { + k96lkyh071r: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.0.1-alpha.2', + 'x-uid': 'e0u8ctx63xa', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'kscb8lws4pt', + 'x-async': false, + 'x-index': 1, + }, + tpeettygzf9: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-app-version': '1.0.1-alpha.2', + properties: { + nickname: { + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'users.nickname', + 'x-component': 'CollectionField', + 'x-component-props': { + ellipsis: true, + }, + 'x-read-pretty': true, + 'x-decorator': null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': '1.0.1-alpha.2', + 'x-uid': 'fyeg48brfk6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '1zzxomha3yb', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'f8zia156lux', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'j9c5x3pl112', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'qkazgqmj5zk', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'iabe2fdu20i', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'dxf2yvxqvra', + 'x-async': true, + 'x-index': 1, + }, + keepUid: true, +}; diff --git a/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/autoFilterWhenSettingDefaultValue.test.ts b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/autoFilterWhenSettingDefaultValue.test.ts index 5d6a05a865a4e..9eb292e1217a0 100644 --- a/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/autoFilterWhenSettingDefaultValue.test.ts +++ b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/autoFilterWhenSettingDefaultValue.test.ts @@ -8,7 +8,7 @@ */ import { expect, test } from '@nocobase/test/e2e'; -import { oneFilterFormAndTable } from './templates'; +import { oneFilterFormAndTable, oneFilterFormAndTableWithManualLoadingData } from './templates'; test.describe('filter form', () => { test('When the filter form field is set with a default value, it should trigger a filtering action on the first page load', async ({ @@ -29,7 +29,7 @@ test.describe('filter form', () => { page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'Super Admin' }), ).not.toBeVisible(); - // 2. 点击重置按钮后,会显示出全部数据 + // 2. 点击重置按钮后,数据不变 await page.getByLabel('action-Action-Reset-users-').click({ position: { x: 10, @@ -37,24 +37,92 @@ test.describe('filter form', () => { }, }); await expect( - page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'Super Admin' }), + page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'test name' }), ).toBeVisible(); + await expect( + page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'Super Admin' }), + ).not.toBeVisible(); + + // 3. 清空 nickname 的值后,点击筛选按钮,应该只显示出所有数据 + await page.getByLabel('block-item-CollectionField-').getByRole('textbox').clear(); + await page.getByLabel('action-Action-Filter-submit-').click({ + position: { + x: 10, + y: 10, + }, + }); await expect( page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'test name' }), ).toBeVisible(); + await expect( + page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'Super Admin' }), + ).toBeVisible(); - // 3. 再次点击筛选按钮,应该只显示出符合条件的数据 - await page.getByLabel('action-Action-Filter-submit-').click({ + // 4. 此时点击 Reset 按钮,应该只显示一条数据,因为会把 nickname 的值重置为 {{$user.nickname}} + await page.getByLabel('action-Action-Reset-users-').click({ position: { x: 10, y: 10, }, }); + await expect( + page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'test name' }), + ).toBeVisible(); await expect( page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'Super Admin' }), ).not.toBeVisible(); + }); + + test('with dataLoadingMode is manual', async ({ page, mockPage, mockRecord }) => { + // nickname 字段的默认值是 {{$user.nickname}} + const nocoPage = await mockPage(oneFilterFormAndTableWithManualLoadingData).waitForInit(); + await mockRecord('users', { nickname: 'test name' }); + await nocoPage.goto(); + + // 1. 首次加载,应该已经触发过一次筛选的动作 + await expect( + page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'Super Admin' }), + ).toBeVisible(); + await expect( + page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'test name' }), + ).not.toBeVisible(); + + // 2. 点击重置按钮后,数据不变 + await page.getByLabel('action-Action-Reset-users-').click({ + position: { + x: 10, + y: 10, + }, + }); + await expect( + page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'Super Admin' }), + ).toBeVisible(); await expect( page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'test name' }), + ).not.toBeVisible(); + + // 3. 清空 nickname 输入框,然后点击 Filter 按钮,应该显示空数据 + await page.getByLabel('block-item-CollectionField-').getByRole('textbox').clear(); + await page.getByLabel('action-Action-Filter-submit-').click({ + position: { + x: 10, + y: 10, + }, + }); + await expect(page.getByLabel('block-item-CardItem-users-table').getByText('No data')).toBeVisible(); + + // 4. 此时点击 Reset 按钮,应该只显示一条数据,因为会把 nickname 的值重置为 {{$user.nickname}} + await page.getByLabel('action-Action-Reset-users-').click({ + position: { + x: 10, + y: 10, + }, + }); + await expect( + page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'Super Admin' }), ).toBeVisible(); + await expect( + page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'test name' }), + ).not.toBeVisible(); }); }); diff --git a/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/templates.ts b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/templates.ts index 75d71d1a5b9fb..b7cdd138aa88a 100644 --- a/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/templates.ts +++ b/packages/core/client/src/modules/blocks/filter-blocks/form/__e2e__/templates.ts @@ -501,3 +501,367 @@ export const oneFilterFormAndTable = { }, keepUid: true, }; +export const oneFilterFormAndTableWithManualLoadingData = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + 'x-index': 1, + properties: { + an34615vknp: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + 'x-index': 1, + properties: { + '62fssedpl0w': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 1, + properties: { + '93tyrk65qym': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 1, + properties: { + '0b7maevxkfs': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'FilterFormBlockProvider', + 'x-use-decorator-props': 'useFilterFormBlockDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + collection: 'users', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:filterForm', + 'x-component': 'CardItem', + 'x-filter-targets': [ + { + uid: 'yg26txxpq4l', + }, + ], + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 1, + properties: { + swmsovm7d4t: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useFilterFormBlockProps', + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 1, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'filterForm:configureFields', + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 1, + properties: { + mzd4ludncd8: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 1, + properties: { + w67kg0y8wn6: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 1, + properties: { + nickname: { + 'x-uid': '3azj6wv3v2r', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + required: false, + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FilterFormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-use-decorator-props': 'useFormItemProps', + 'x-collection-field': 'users.nickname', + 'x-component-props': {}, + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 1, + default: '{{$user.nickname}}', + 'x-async': false, + }, + }, + 'x-uid': '1jmg205s2sa', + 'x-async': false, + }, + }, + 'x-uid': 'wjoskqiuk5b', + 'x-async': false, + }, + }, + 'x-uid': 't3gxqyl52ca', + 'x-async': false, + }, + ceahs5hahgg: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'filterForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + style: { + float: 'right', + }, + }, + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 2, + properties: { + xgjhwz2ln8l: { + _isJSONSchemaObject: true, + version: '2.0', + title: '{{ t("Filter") }}', + 'x-action': 'submit', + 'x-component': 'Action', + 'x-use-component-props': 'useFilterBlockActionProps', + 'x-designer': 'Action.Designer', + 'x-component-props': { + type: 'primary', + htmlType: 'submit', + }, + 'x-action-settings': {}, + type: 'void', + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 1, + 'x-uid': 'ev40o2gk87b', + 'x-async': false, + }, + qxhdilp319s: { + _isJSONSchemaObject: true, + version: '2.0', + title: '{{ t("Reset") }}', + 'x-component': 'Action', + 'x-use-component-props': 'useResetBlockActionProps', + 'x-designer': 'Action.Designer', + 'x-action-settings': {}, + type: 'void', + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 2, + 'x-uid': '559bivcwabh', + 'x-async': false, + }, + }, + 'x-uid': '8q25gzurfeh', + 'x-async': false, + }, + }, + 'x-uid': 'v3mwerc709f', + 'x-async': false, + }, + }, + 'x-uid': 'uqnziogzhkq', + 'x-async': false, + }, + }, + 'x-uid': '952up9gdrhf', + 'x-async': false, + }, + }, + 'x-uid': 'da58ptz1cie', + 'x-async': false, + }, + dxd8oaoh4a7: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 2, + properties: { + '27ijajdwyd2': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 1, + properties: { + '7yz5da41nt3': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'users', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + filter: { + $and: [ + { + nickname: { + $includes: '{{$user.nickname}}', + }, + }, + ], + }, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + dataLoadingMode: 'manual', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 1, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 1, + 'x-uid': '73fy8oxxxk9', + 'x-async': false, + }, + wo6mmz3gm4e: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 2, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 1, + properties: { + k96lkyh071r: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 1, + 'x-uid': 'e0u8ctx63xa', + 'x-async': false, + }, + }, + 'x-uid': 'kscb8lws4pt', + 'x-async': false, + }, + tpeettygzf9: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 2, + properties: { + nickname: { + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'users.nickname', + 'x-component': 'CollectionField', + 'x-component-props': { + ellipsis: true, + }, + 'x-read-pretty': true, + 'x-decorator': null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': '1.0.1-alpha.2', + 'x-index': 1, + 'x-uid': 'fyeg48brfk6', + 'x-async': false, + }, + }, + 'x-uid': '1zzxomha3yb', + 'x-async': false, + }, + }, + 'x-uid': 'f8zia156lux', + 'x-async': false, + }, + }, + 'x-uid': 'yg26txxpq4l', + 'x-async': false, + }, + }, + 'x-uid': 'j9c5x3pl112', + 'x-async': false, + }, + }, + 'x-uid': 'qkazgqmj5zk', + 'x-async': false, + }, + }, + 'x-uid': 'iabe2fdu20i', + 'x-async': false, + }, + }, + 'x-uid': 'dxf2yvxqvra', + 'x-async': true, + }, + keepUid: true, +}; diff --git a/packages/core/client/src/schema-component/antd/filter/__tests__/useFilterActionProps.test.ts b/packages/core/client/src/schema-component/antd/filter/__tests__/useFilterActionProps.test.ts new file mode 100644 index 0000000000000..2880aeec5c175 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/filter/__tests__/useFilterActionProps.test.ts @@ -0,0 +1,68 @@ +/** + * 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 { removeNullCondition } from '../useFilterActionProps'; + +describe('removeNullCondition', () => { + it('should remove null conditions', () => { + const filter = { + field1: null, + field2: 'value2', + field3: null, + field4: 'value4', + }; + const expected = { + field2: 'value2', + field4: 'value4', + }; + const result = removeNullCondition(filter); + expect(result).toEqual(expected); + }); + + it('should remove undefined conditions', () => { + const filter = { + field1: undefined, + field2: 'value2', + field3: undefined, + field4: 'value4', + }; + const expected = { + field2: 'value2', + field4: 'value4', + }; + const result = removeNullCondition(filter); + expect(result).toEqual(expected); + }); + + it('should handle empty filter', () => { + const filter = {}; + const expected = {}; + const result = removeNullCondition(filter); + expect(result).toEqual(expected); + }); + + it('should handle nested filter', () => { + const filter = { + field1: null, + field2: 'value2', + field3: { + subfield1: null, + subfield2: 'value2', + }, + }; + const expected = { + field2: 'value2', + field3: { + subfield2: 'value2', + }, + }; + const result = removeNullCondition(filter); + expect(result).toEqual(expected); + }); +}); 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 7485a81036574..19a8f50a4ad05 100644 --- a/packages/core/client/src/schema-component/antd/filter/useFilterActionProps.ts +++ b/packages/core/client/src/schema-component/antd/filter/useFilterActionProps.ts @@ -10,12 +10,12 @@ import { Field } from '@formily/core'; import { useField, useFieldSchema } from '@formily/react'; import flat from 'flat'; +import _ from 'lodash'; import { useTranslation } from 'react-i18next'; import { useBlockRequestContext } from '../../../block-provider'; -import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; +import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager'; import { mergeFilter } from '../../../filter-provider/utils'; import { useDataLoadingMode } from '../../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem'; -import _ from 'lodash'; export const useGetFilterOptions = () => { const { getCollectionFields } = useCollectionManager_deprecated(); @@ -157,8 +157,8 @@ const isEmpty = (obj) => { ); }; -export const removeNullCondition = (filter) => { - const items = flat(filter || {}); +export const removeNullCondition = (filter, customFlat = flat) => { + const items = customFlat(filter || {}); const values = {}; for (const key in items) { const value = items[key]; @@ -166,7 +166,7 @@ export const removeNullCondition = (filter) => { values[key] = value; } } - return flat.unflatten(values); + return customFlat.unflatten(values); }; export const useFilterActionProps = () => {