From 2aee640fb4554e1f2c9319e9b42fd9c678494750 Mon Sep 17 00:00:00 2001 From: RUNZE LU <36724300+lurunze1226@users.noreply.github.com> Date: Wed, 20 Dec 2023 20:10:22 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20CRUD=E7=BB=84=E4=BB=B6=E8=A7=A6=E5=8F=91?= =?UTF-8?q?reload=E5=90=8E=E5=B7=B2=E9=80=89=E9=A1=B9=E6=9C=AA=E6=B8=85?= =?UTF-8?q?=E7=A9=BA=E9=97=AE=E9=A2=98=20(#9196)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh-CN/components/crud.md | 242 ++++++++++++++-- packages/amis-core/src/store/crud.ts | 21 +- .../amis/__tests__/renderers/CRUD.test.tsx | 273 +++++++++++++++++- .../renderers/Form/conditionBuilder.test.tsx | 2 +- packages/amis/src/renderers/CRUD.tsx | 27 +- 5 files changed, 527 insertions(+), 38 deletions(-) diff --git a/docs/zh-CN/components/crud.md b/docs/zh-CN/components/crud.md index 3d15ef221b5..62dc42d5dd5 100755 --- a/docs/zh-CN/components/crud.md +++ b/docs/zh-CN/components/crud.md @@ -2921,36 +2921,234 @@ interface CRUDMatchFunc { } ``` -它其实是个简化的 `button` 组件,可以参考 `button` 组件的文档做调整。`reload`支持两种触发方式: +它其实是个简化的 `button` 组件,可以参考 `button` 组件的文档做调整。 -- `"type": "reload"`,CRUD 内置的方法 -- `{"actionType": "reload", "target": "targetName"}`,动作触发 +#### 刷新CRUD触发方式 + +触发CRUD刷新的方式有3种: +1. **reload类型按钮**:使用`{"type": "reload", ...}`,CRUD内部会对点击事件做处理 +2. **reload动作按钮**:使用`{"type": "action", "actionType": "reload", "target": "targetName", ...}`,指定`target`为要刷新的CRUD组件的`name` +3. **reload事件动作**:使用[事件动作](../../docs/concepts/event-action),指定`id`为要刷新的CRUD组件的`id` + +```schema +{ + "type": "page", + "body": [ + { + "type": "button", + "icon": "iconfont icon-refresh", + "tooltip": "", + "label": "CRUD外层按钮", + "level": "enhance", + "onEvent": { + "click": { + "weight": 0, + "actions": [ + { + "componentId": "crudId", + "ignoreError": false, + "actionType": "reload", + "dataMergeMode": "override", + "data": { + }, + "args": { + "resetPage": true + } + } + ] + } + } + }, + { + "type": "crud", + "name": "crudName", + "id": "crudId", + "syncLocation": false, + "api": "/api/mock2/crud/table", + "headerToolbar": [ + "bulkActions", + { + "type": "reload", + "align": "right", + "icon": "iconfont icon-refresh", + "label": "刷新(type)", + "tooltip": "", + "level": "primary" + }, + { + "type": "action", + "align": "right", + "icon": "iconfont icon-refresh", + "label": "刷新(actionType)", + "tooltip": "", + "level": "primary", + "actionType": "reload", + "target": "crudName" + }, + { + "type": "button", + "align": "right", + "icon": "iconfont icon-refresh", + "tooltip": "", + "label": "事件动作(onEvent)", + "level": "primary", + "onEvent": { + "click": { + "weight": 0, + "actions": [ + { + "componentId": "crudId", + "groupType": "component", + "actionType": "reload", + "dataMergeMode": "override" + } + ] + } + } + } + ], + "bulkActions": [ + { + "label": "批量删除", + "actionType": "ajax", + "api": "delete:/api/mock2/sample/${ids|raw}", + "confirmText": "确定要批量删除?" + }, + { + "label": "批量修改", + "actionType": "dialog", + "dialog": { + "title": "批量编辑", + "body": { + "type": "form", + "api": "/api/mock2/sample/bulkUpdate2", + "body": [ + { + "type": "hidden", + "name": "ids" + }, + { + "type": "input-text", + "name": "engine", + "label": "Engine" + } + ] + } + } + } + ], + "columns": [ + { + "name": "id", + "label": "ID" + }, + { + "name": "engine", + "label": "Rendering engine" + }, + { + "name": "browser", + "label": "Browser" + }, + { + "name": "platform", + "label": "Platform(s)" + }, + { + "name": "version", + "label": "Engine version" + }, + { + "name": "grade", + "label": "CSS grade" + } + ] + } + ] +} +``` + +刷新后默认会重置当前已选行数据,即使设置了 `keepItemSelectionOnPageChange` 为 `true`,也会重置。 ```schema: scope="body" { "type": "crud", - "name": "crud", + "name": "crudName", + "id": "crudId", "syncLocation": false, - "api": "/api/mock2/sample", + "api": "/api/mock2/crud/table", + "keepItemSelectionOnPageChange": true, "headerToolbar": [ - { - "type": "action", - "align": "right", - "icon": "iconfont icon-refresh", - "label": "刷新(actionType)", - "tooltip": "", - "level": "primary", - "actionType": 'reload', - "target": 'crud' - }, - { - "type": "reload", - "align": "right", - "icon": "iconfont icon-refresh", - "label": "刷新(type)", - "tooltip": "", - "level": "primary" + "bulkActions", + { + "type": "reload", + "align": "right", + "icon": "iconfont icon-refresh", + "label": "刷新(type)", + "tooltip": "", + "level": "primary" + }, + { + "type": "action", + "align": "right", + "icon": "iconfont icon-refresh", + "label": "刷新(actionType)", + "tooltip": "", + "level": "primary", + "actionType": "reload", + "target": "crudName" + }, + { + "type": "button", + "align": "right", + "icon": "iconfont icon-refresh", + "tooltip": "", + "label": "事件动作(onEvent)", + "level": "primary", + "onEvent": { + "click": { + "weight": 0, + "actions": [ + { + "componentId": "crudId", + "groupType": "component", + "actionType": "reload", + "dataMergeMode": "override" + } + ] + } + } + } + ], + "bulkActions": [ + { + "label": "批量删除", + "actionType": "ajax", + "api": "delete:/api/mock2/sample/${ids|raw}", + "confirmText": "确定要批量删除?" + }, + { + "label": "批量修改", + "actionType": "dialog", + "dialog": { + "title": "批量编辑", + "body": { + "type": "form", + "api": "/api/mock2/sample/bulkUpdate2", + "body": [ + { + "type": "hidden", + "name": "ids" + }, + { + "type": "input-text", + "name": "engine", + "label": "Engine" + } + ] + } } + } ], "columns": [ { diff --git a/packages/amis-core/src/store/crud.ts b/packages/amis-core/src/store/crud.ts index 99cf9d9d338..77c8a6f1c5a 100644 --- a/packages/amis-core/src/store/crud.ts +++ b/packages/amis-core/src/store/crud.ts @@ -17,7 +17,9 @@ import {resolveVariableAndFilter} from '../utils/tpl-builtin'; import {normalizeApiResponseData} from '../utils/api'; import {matchSorter} from 'match-sorter'; import {filter} from '../utils/tpl'; +import {TableStore} from './table'; +import type {ITableStore} from './table'; import type {MatchSorterOptions} from 'match-sorter'; interface MatchFunc { @@ -835,6 +837,22 @@ export const CRUDStore = ServiceStore.named('CRUDStore') self.total = total || 0; }; + /** 非Picker模式下,重置当前CRUD的所有的已选择项目 */ + const resetSelection = (): void => { + // 初始化CRUD记录的已选择项目和未选择项目 + setSelectedItems([]); + setUnSelectedItems([]); + + const tableStore = self?.children?.find?.( + (s: any) => s.storeType === TableStore.name + ); + + if (tableStore) { + // 清空Table记录的已选择项目 + (tableStore as ITableStore).clear?.(); + } + }; + return { getData, updateSelectData, @@ -853,7 +871,8 @@ export const CRUDStore = ServiceStore.named('CRUDStore') initFromScope, exportAsCSV, updateColumns, - updateTotal + updateTotal, + resetSelection }; }); diff --git a/packages/amis/__tests__/renderers/CRUD.test.tsx b/packages/amis/__tests__/renderers/CRUD.test.tsx index 558da689491..5a1d0ce8591 100644 --- a/packages/amis/__tests__/renderers/CRUD.test.tsx +++ b/packages/amis/__tests__/renderers/CRUD.test.tsx @@ -22,6 +22,7 @@ * 19. fetchInitData silent 静默请求 * 20. CRUD表头查询字段更新后严格比较场景 * 21. 通过reUseRow为false强制清空表格状态 + * 22. reload 后清空选项 */ import { @@ -1392,7 +1393,277 @@ test('21. CRUD reUseRow set false to reset crud state when api return same data' const btn = container.querySelectorAll('.cxd-Button')!; fireEvent.click(btn[0]); await wait(300); - + const checks2 = container.querySelectorAll('.is-checked'); expect(checks2.length).toEqual(0); }); + +describe('22. CRUD reload and reset selcted rows', () => { + test('CRUD reload with selection', async () => { + const mockFetcher = jest.fn().mockImplementation(() => { + return new Promise(resolve => + resolve({ + data: { + status: 0, + msg: 'ok', + data: { + count: 2, + items: [ + { + "engine": "Trident", + "browser": "Internet Explorer 4.0", + "platform": "Win 95+", + "version": "4", + "grade": "X", + "id": 1 + }, + { + "engine": "Trident", + "browser": "Internet Explorer 5.0", + "platform": "Win 95+", + "version": "5", + "grade": "C", + "id": 2 + } + ] + } + } + }) + ); + }); + const {container} = render( + amisRender( + { + "type": "page", + "body": [ + { + "type": "button", + "icon": "iconfont icon-refresh", + "tooltip": "", + "label": "CRUD外层按钮", + "level": "enhance", + "className": "reload-btn", + "onEvent": { + "click": { + "weight": 0, + "actions": [ + { + "componentId": "crudId", + "ignoreError": false, + "actionType": "reload", + "dataMergeMode": "override", + "data": { + }, + "args": { + "resetPage": true + } + } + ] + } + } + }, + { + "type": "crud", + "name": "crudName", + "id": "crudId", + "syncLocation": false, + "api": "/api/mock2/crud/table", + "bulkActions": [ + { + "label": "批量删除", + "actionType": "ajax", + "api": "delete:/api/mock2/sample/${ids|raw}", + "confirmText": "确定要批量删除?" + }, + { + "label": "批量修改", + "actionType": "dialog", + "dialog": { + "title": "批量编辑", + "body": { + "type": "form", + "api": "/api/mock2/sample/bulkUpdate2", + "body": [ + { + "type": "hidden", + "name": "ids" + }, + { + "type": "input-text", + "name": "engine", + "label": "Engine" + } + ] + } + } + } + ], + "columns": [ + { + "name": "id", + "label": "ID" + }, + { + "name": "engine", + "label": "Rendering engine" + } + ] + } + ] + }, + {}, + makeEnv({fetcher: mockFetcher}) + ) + ); + + await wait(300); + + // 全选数据, 选中2条数据 + const checkbox = container.querySelector('.cxd-Table-checkCell input[type="checkbox"]')!; + fireEvent.click(checkbox); + expect(container.querySelectorAll('.cxd-Table-table-tr.is-checked')?.length).toEqual(2); + + // 刷新数据,选中数据清空 + const reloadBtn = container.querySelector('[type=button].reload-btn')!; + expect(reloadBtn).toBeInTheDocument(); + fireEvent.click(reloadBtn); + await wait(200); + + expect(container.querySelectorAll('.cxd-Table-table-tr.is-checked')?.length).toEqual(0); + }); + + test('CRUD reload with selection and keepItemSelectionOnPageChange is enabled', async () => { + const mockFetcher = jest.fn().mockImplementation(() => { + return new Promise(resolve => + resolve({ + data: { + status: 0, + msg: 'ok', + data: { + count: 2, + items: [ + { + "engine": "Trident", + "browser": "Internet Explorer 4.0", + "platform": "Win 95+", + "version": "4", + "grade": "X", + "id": 1 + }, + { + "engine": "Trident", + "browser": "Internet Explorer 5.0", + "platform": "Win 95+", + "version": "5", + "grade": "C", + "id": 2 + } + ] + } + } + }) + ); + }); + const {container} = render( + amisRender( + { + "type": "page", + "body": [ + { + "type": "button", + "icon": "iconfont icon-refresh", + "tooltip": "", + "label": "CRUD外层按钮", + "level": "enhance", + "className": "reload-btn", + "onEvent": { + "click": { + "weight": 0, + "actions": [ + { + "componentId": "crudId", + "ignoreError": false, + "actionType": "reload", + "dataMergeMode": "override", + "data": { + }, + "args": { + "resetPage": true + } + } + ] + } + } + }, + { + "type": "crud", + "name": "crudName", + "id": "crudId", + "syncLocation": false, + "api": "/api/mock2/crud/table", + "keepItemSelectionOnPageChange": true, + "bulkActions": [ + { + "label": "批量删除", + "actionType": "ajax", + "api": "delete:/api/mock2/sample/${ids|raw}", + "confirmText": "确定要批量删除?" + }, + { + "label": "批量修改", + "actionType": "dialog", + "dialog": { + "title": "批量编辑", + "body": { + "type": "form", + "api": "/api/mock2/sample/bulkUpdate2", + "body": [ + { + "type": "hidden", + "name": "ids" + }, + { + "type": "input-text", + "name": "engine", + "label": "Engine" + } + ] + } + } + } + ], + "columns": [ + { + "name": "id", + "label": "ID" + }, + { + "name": "engine", + "label": "Rendering engine" + } + ] + } + ] + }, + {}, + makeEnv({fetcher: mockFetcher}) + ) + ); + + await wait(300); + + // 全选数据, 选中2条数据 + const checkbox = container.querySelector('.cxd-Table-checkCell input[type="checkbox"]')!; + fireEvent.click(checkbox); + expect(container.querySelectorAll('.cxd-Table-table-tr.is-checked')?.length).toEqual(2); + + // 刷新数据,选中数据清空 + const reloadBtn = container.querySelector('[type=button].reload-btn')!; + expect(reloadBtn).toBeInTheDocument(); + fireEvent.click(reloadBtn); + await wait(200); + + expect(container.querySelectorAll('.cxd-Table-table-tr.is-checked')?.length).toEqual(0); + }); + +}); diff --git a/packages/amis/__tests__/renderers/Form/conditionBuilder.test.tsx b/packages/amis/__tests__/renderers/Form/conditionBuilder.test.tsx index 5d6a9dc4f64..d33cae5275e 100644 --- a/packages/amis/__tests__/renderers/Form/conditionBuilder.test.tsx +++ b/packages/amis/__tests__/renderers/Form/conditionBuilder.test.tsx @@ -815,7 +815,7 @@ test('Renderer:condition-builder with not embed', async () => { * 2. 选项类型(select)字段,切换操作符(包含 -> 等于),字段值清空且正常渲染(等于和包含对应的multiple不一样,所以值格式不一样) * 3. 先使用其他类型字段,再切换到select类型,条件选择包含,值正常渲染 */ -describe.only('Renderer: condition-builder with formula', () => { +describe('Renderer: condition-builder with formula', () => { const onSubmit = jest.fn(); test('condition-builder with different fields', async () => { const {container} = render(amisRender({ diff --git a/packages/amis/src/renderers/CRUD.tsx b/packages/amis/src/renderers/CRUD.tsx index 37d56baddaf..3dd7c43de25 100644 --- a/packages/amis/src/renderers/CRUD.tsx +++ b/packages/amis/src/renderers/CRUD.tsx @@ -1240,7 +1240,6 @@ export default class CRUD extends React.Component { silentPolling, syncLocation, syncResponse2Query, - keepItemSelectionOnPageChange, pickerMode, env, loadDataOnce, @@ -1249,10 +1248,9 @@ export default class CRUD extends React.Component { dispatchEvent } = this.props; - // reload 需要清空用户选择。 - if (keepItemSelectionOnPageChange && clearSelection && !pickerMode) { - store.setSelectedItems([]); - store.setUnSelectedItems([]); + // reload 需要清空用户选择,无论是否开启keepItemSelectionOnPageChange + if (clearSelection && !pickerMode) { + store.resetSelection(); } let loadDataMode = ''; @@ -1736,7 +1734,8 @@ export default class CRUD extends React.Component { values: object, forceReload?: boolean, replace?: boolean, - resetPage?: boolean + resetPage?: boolean, + clearSelection?: boolean ) { const { store, @@ -1746,6 +1745,7 @@ export default class CRUD extends React.Component { perPageField, loadDataOnceFetchOnFilter } = this.props; + store.updateQuery( resetPage ? { @@ -1764,7 +1764,7 @@ export default class CRUD extends React.Component { this.search( undefined, undefined, - replace, + clearSelection ?? replace, forceReload ?? loadDataOnceFetchOnFilter === true ); } @@ -1776,7 +1776,7 @@ export default class CRUD extends React.Component { resetPage?: boolean ) { if (query) { - return this.receive(query, undefined, replace, resetPage); + return this.receive(query, undefined, replace, resetPage, true); } else { this.search(undefined, undefined, true, true); } @@ -1786,9 +1786,10 @@ export default class CRUD extends React.Component { values: object, subPath?: string, replace?: boolean, - resetPage?: boolean + resetPage?: boolean, + clearSelection?: boolean ) { - this.handleQuery(values, true, replace, resetPage); + this.handleQuery(values, true, replace, resetPage, clearSelection); } reloadTarget(target: string, data: any) { @@ -1950,7 +1951,6 @@ export default class CRUD extends React.Component { } ) )} - {itemBtns.map((btn, index) => render( `bulk-action/${index}`, @@ -2674,14 +2674,15 @@ export class CRUDRenderer extends CRUD { values: any, subPath?: string, replace?: boolean, - resetPage?: boolean + resetPage?: boolean, + clearSelection?: boolean ) { const scoped = this.context as IScopedContext; if (subPath) { return scoped.send(subPath, values); } - return super.receive(values, undefined, replace, resetPage); + return super.receive(values, undefined, replace, resetPage, clearSelection); } reloadTarget(target: string, data: any) {