From d8eaf2bfd75ab5dfdfc9ae463548a6225aaf7278 Mon Sep 17 00:00:00 2001 From: 2betop <2698393+2betop@users.noreply.github.com> Date: Tue, 17 Dec 2024 19:20:34 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20CRUD=20=E8=B0=83=E6=95=B4=E6=96=B9?= =?UTF-8?q?=E4=BE=BF=E5=A4=96=E5=9B=B4=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh-CN/components/crud.md | 536 +++++++++++++++++- docs/zh-CN/components/table.md | 54 +- packages/amis-core/src/RootRenderer.tsx | 12 +- packages/amis-core/src/store/crud.ts | 47 +- packages/amis-core/src/store/list.ts | 67 ++- packages/amis-core/src/store/table.ts | 91 ++- packages/amis-editor/src/plugin/CRUD.tsx | 8 + packages/amis-ui/scss/_mixins.scss | 13 +- packages/amis-ui/scss/_properties.scss | 1 + packages/amis-ui/scss/components/_crud.scss | 5 + .../amis-ui/scss/components/form/_picker.scss | 25 +- packages/amis-ui/src/locale/de-DE.ts | 2 + packages/amis-ui/src/locale/en-US.ts | 2 + packages/amis-ui/src/locale/zh-CN.ts | 1 + .../amis/__tests__/renderers/Picker.test.tsx | 8 +- .../__snapshots__/Picker.test.tsx.snap | 34 +- packages/amis/src/renderers/CRUD.tsx | 384 +++++++++---- packages/amis/src/renderers/Cards.tsx | 50 +- .../amis/src/renderers/Form/InputTable.tsx | 1 + packages/amis/src/renderers/Form/Picker.tsx | 44 +- packages/amis/src/renderers/List.tsx | 28 +- packages/amis/src/renderers/Table/index.tsx | 65 ++- 22 files changed, 1164 insertions(+), 314 deletions(-) diff --git a/docs/zh-CN/components/crud.md b/docs/zh-CN/components/crud.md index b47ac32e74c..8e4fe9b1521 100755 --- a/docs/zh-CN/components/crud.md +++ b/docs/zh-CN/components/crud.md @@ -401,7 +401,62 @@ interface ParsePrimitiveQueryOptions { ### 查 -查,就不单独介绍了,这个文档绝大部分都是关于查的。 +除了列表查询外,还支持查看详情场景,与编辑不同的地方主要在于弹窗中改成放展示类组件,或者表单项配置静态展示。 + +```schema: scope="body" +{ + "type": "crud", + "api": "/api/mock2/sample?orderBy=id&orderDir=desc", + "syncLocation": false, + "columns": [ + { + "name": "id", + "label": "ID" + }, + { + "name": "engine", + "label": "Rendering engine" + }, + { + "name": "browser", + "label": "Browser" + }, + { + "type": "operation", + "label": "操作", + "buttons": [ + { + "label": "详情", + "type": "button", + "actionType": "dialog", + "dialog": { + "title": "查看数据「${id}」", + "body": { + "type": "form", + "initApi": "/api/mock2/sample/${id}", + "body": [ + { + "type": "static", + "name": "engine", + "label": "Engine" + }, + { + "type": "input-text", + "name": "browser", + "label": "Browser", + "static": true + } + ] + } + } + } + ] + } + ] +} +``` + +弹框里面可用数据自动就是点击的那一行的行数据,如果列表没有返回,可以在 form 里面再配置个 initApi 初始化数据,如果行数据里面有倒是不需要再拉取了。表单项的 name 跟数据 key 对应上便自动回显了。 ## 展示模式 @@ -2192,16 +2247,17 @@ interface CRUDMatchFunc { 批量操作会默认将下面数据添加到数据域中以供**按钮行为**使用,需要注意的是**静态**和**批量操作**时的数据域是不同的。**静态数据域**是指渲染批量操作区域时能够获取到的数据,**批量操作数据域**是指触发按钮动作时能够获取到的数据,具体区别参考下表: -| 属性名 | 类型 | 所属数据域 | 说明 | 版本 | -| ----------------- | --------------------- | -------------- | ------------------------------------------------------------------------------------ | ------- | -| `currentPageData` | `Array` | 静态, 批量操作 | 当前分页数据集合,`Column`为当前 Table 数据结构定义 | `2.4.0` | -| `selectedItems` | `Array` | 静态, 批量操作 | 选中的行数据集合 | -| `unSelectedItems` | `Array` | 静态, 批量操作 | 未选中的行数据集合 | -| `items` | `Array` | 批量操作 | `selectedItems` 的别名 | -| `rows` | `Array` | 批量操作 | `selectedItems` 的别名,推荐用 `items` | -| `ids` | `string` | 批量操作 | 多个 id 值用英文逗号隔开,前提是行数据中有 id 字段,或者有指定的 `primaryField` 字段 | -| `event` | `object` | 事件动作 | 可以通过`event.data`获取批量操作按钮上绑定的事件动作产生的数据 | -| `...rest` | `Record` | 批量操作 | 选中的行数据集合的首个元素的字段,注意列字段如果和以上字段重名时,会被上述字段值覆盖 | +| 属性名 | 类型 | 所属数据域 | 说明 | 版本 | +| ----------------- | ------------------------- | -------------- | ------------------------------------------------------------------------------------ | ------- | +| `currentPageData` | `Array` | 静态, 批量操作 | 当前分页数据集合,`Column`为当前 Table 数据结构定义 | `2.4.0` | +| `selectedItems` | `Array` | 静态, 批量操作 | 选中的行数据集合 | +| `selectedIndexes` | `Array` | 静态, 批量操作 | 选中的行数据索引集合 | +| `unSelectedItems` | `Array` | 静态, 批量操作 | 未选中的行数据集合 | +| `items` | `Array` | 批量操作 | `selectedItems` 的别名 | +| `rows` | `Array` | 批量操作 | `selectedItems` 的别名,推荐用 `items` | +| `ids` | `string` | 批量操作 | 多个 id 值用英文逗号隔开,前提是行数据中有 id 字段,或者有指定的 `primaryField` 字段 | +| `event` | `object` | 事件动作 | 可以通过`event.data`获取批量操作按钮上绑定的事件动作产生的数据 | +| `...rest` | `Record` | 批量操作 | 选中的行数据集合的首个元素的字段,注意列字段如果和以上字段重名时,会被上述字段值覆盖 | 你可以通过[数据映射](../../docs/concepts/data-mapping),在`api`中获取这些参数。 @@ -2503,6 +2559,261 @@ interface CRUDMatchFunc { } ``` +### 悬浮操作栏 + +通过配置 `itemActions` 可以启用悬浮操作栏,鼠标悬停到行上,右侧会出现对应的操作按钮。 + +```schema: scope="body" +{ + "type": "crud", + "syncLocation": false, + "api": "/api/mock2/sample", + "checkOnItemClick": true, + "itemActions": [ + { + "type": "button", + "label": "按钮 1", + "actionType": "toast", + "toast": { + "items": [ + { + "level": "info", + "body": "${&|json}" + } + ] + } + }, + { + "type": "button", + "label": "按钮 2", + "actionType": "toast", + "toast": { + "items": [ + { + "level": "info", + "body": "${&|json}" + } + ] + } + } + ], + "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" + } + ] +} +``` + +当同时配置 `itemActions` 和 `bulkActions`, 顶部工具栏会根据选择的条数来切换显示按钮。 + +```schema: scope="body" +{ + "type": "crud", + "syncLocation": false, + "api": "/api/mock2/sample", + "checkOnItemClick": true, + "bulkActions": [ + { + "type": "button", + "label": "批量按钮 1", + "actionType": "toast", + "toast": { + "items": [ + { + "level": "info", + "body": "${&|json}" + } + ] + } + }, + { + "type": "button", + "label": "批量按钮 2", + "actionType": "toast", + "toast": { + "items": [ + { + "level": "info", + "body": "${&|json}" + } + ] + } + } + ], + "itemActions": [ + { + "type": "button", + "label": "按钮 1", + "actionType": "toast", + "toast": { + "items": [ + { + "level": "info", + "body": "${&|json}" + } + ] + } + }, + { + "type": "button", + "label": "按钮 2", + "actionType": "toast", + "toast": { + "items": [ + { + "level": "info", + "body": "${&|json}" + } + ] + } + } + ], + "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" + } + ] +} +``` + +如果同时启用时,只想把按钮展示在顶部,而不是悬浮,则需要给按钮上配置 `hiddenOnHover`。 + +```schema: scope="body" +{ + "type": "crud", + "syncLocation": false, + "api": "/api/mock2/sample", + "checkOnItemClick": true, + "bulkActions": [ + { + "type": "button", + "label": "批量按钮 1", + "actionType": "toast", + "toast": { + "items": [ + { + "level": "info", + "body": "${&|json}" + } + ] + } + }, + { + "type": "button", + "label": "批量按钮 2", + "actionType": "toast", + "toast": { + "items": [ + { + "level": "info", + "body": "${&|json}" + } + ] + } + } + ], + "itemActions": [ + { + "type": "button", + "label": "按钮 1", + "hiddenOnHover": true, + "actionType": "toast", + "toast": { + "items": [ + { + "level": "info", + "body": "${&|json}" + } + ] + } + }, + { + "type": "button", + "label": "按钮 2", + "hiddenOnHover": true, + "actionType": "toast", + "toast": { + "items": [ + { + "level": "info", + "body": "${&|json}" + } + ] + } + } + ], + "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" + } + ] +} +``` + ### 数据统计 在`headerToolbar`或者`footerToolbar`数组中添加`statistics`字符串,可以实现简单的数据统计功能 @@ -3918,6 +4229,130 @@ itemAction 里的 onClick 还能通过 `data` 参数拿到当前行的数据, } ``` +## 开启点选 + +当配置了 `bulkActions` 后,CRUD 会自动变成可点选状态。如果想直接开启可点选,可以配置 `selectable`,同时可以配置 `multiple` 来配置是单选还是多选。但是这个时候没有任何交互,需要配置事件动作,或者在工具栏中添加行为按钮完成交互逻辑。 + +```schema: scope="body" +{ + "type": "crud", + "api": "/api/mock2/sample", + "syncLocation": false, + "selectable": true, + "headerToolbar": [ + { + "type": "button", + "label": "按钮", + "visibleOn": "${selectedItems.length}", + "actionType": "toast", + "toast": { + "items": [ + { + "level": "info", + "body": "${selectedItems|json}" + } + ] + } + } + ], + "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" + } + ] +} +``` + +多选场景且支持跨页面选择同时支持快速修改 + +```schema: scope="body" +{ + "type": "crud", + "api": "/api/mock2/sample", + "syncLocation": false, + "loadDataOnce": true, + "keepItemSelectionOnPageChange": true, + "onEvent": { + "selectedChange": { + "actions": [ + { + "actionType": "toast", + "args": { + "msg": "已选择数据 ${selectedItems.length}
未选 ${event.data.unSelectedItems.length} 条
已选 indexes ${ENCODEJSON(selectedIndexes)}" + } + } + ] + } + }, + "bulkActions": [ + { + "label": "Button(${selectedItems.length})", + "type": "button", + "onEvent": { + "click": { + "actions": [ + { + "actionType": "toast", + "args": { + "msg": "已选择数据 ${ENCODEJSON(ARRAYMAP(selectedItems, item => `${item.id}: ${item.engine}`))}
未选 ${event.data.unSelectedItems.length} 条
已选 indexes ${ENCODEJSON(selectedIndexes)}" + } + } + ] + } + } + } + ], + "columns": [ + { + "name": "id", + "label": "ID" + }, + { + "name": "engine", + "label": "Rendering engine", + "quickEdit": true + }, + { + "name": "browser", + "label": "Browser" + }, + { + "name": "platform", + "label": "Platform(s)" + }, + { + "name": "version", + "label": "Engine version" + }, + { + "name": "grade", + "label": "CSS grade" + } + ] +} +``` + ## 属性表 | 属性名 | 类型 | 默认值 | 说明 | 版本 | @@ -3992,13 +4427,13 @@ itemAction 里的 onClick 还能通过 `data` 参数拿到当前行的数据, 除了 Table 组件默认支持的列配置,CRUD 的列配置还额外支持以下属性: -| 属性名 | 类型 | 默认值 | 说明 | 版本 | -| ------------------ | ------------------------------------------------------------ | ------- | --------------------------------------------------------------------------- | ---- | -| sortable | `boolean` | `false` | 是否可排序 | -| searchable | `boolean` \| `Schema` | `false` | 是否可快速搜索,开启`autoGenerateFilter`后,`searchable`支持配置`Schema` | -| filterable | `boolean` \| [`QuickFilterConfig`](./crud#quickfilterconfig) | `false` | 是否可快速搜索,`options`属性为静态选项,支持设置`source`属性从接口获取选项 | -| quickEdit | `boolean` \| [`QuickEditConfig`](./crud#quickeditconfig) | - | 快速编辑,一般需要配合`quickSaveApi`接口使用 | -| quickEditEnabledOn | `SchemaExpression` | - | 开启快速编辑条件[表达式](../../docs/concepts/expression) | | +| 属性名 | 类型 | 默认值 | 说明 | 版本 | +| ------------------ | ------------------------------------------------------------ | --------- | -------------------------------------------------------------------------------------------------------------- | ------- | +| sortable | `boolean` | `false` | 是否可排序 | +| searchable | `boolean` \| `Schema` | `false` | 是否可快速搜索,开启`autoGenerateFilter`后,`searchable`支持配置`Schema` | +| filterable | `boolean` \| [`QuickFilterConfig`](./crud#quickfilterconfig) | `false` | 是否可快速搜索,`options`属性为静态选项,支持设置`source`属性从接口获取选项 | +| quickEdit | `boolean` \| [`QuickEditConfig`](./crud#quickeditconfig) | - | 快速编辑,一般需要配合`quickSaveApi`接口使用 | +| quickEditEnabledOn | `SchemaExpression` | - | 开启快速编辑条件[表达式](../../docs/concepts/expression) | | | textOverflow | `string` | `default` | 文本溢出后展示形式,默认换行处理。可选值 `ellipsis` 溢出隐藏展示, `noWrap` 不换行展示(仅在列为静态文本时生效) | `6.9.0` | #### QuickFilterConfig @@ -4057,13 +4492,14 @@ itemAction 里的 onClick 还能通过 `data` 参数拿到当前行的数据, | quickSaveItemFail | `error` 错误原因 | 快速编辑单条保存失败后触发 | | saveOrderSucc | `result` 接口数据返回
`其他` 请参考 [拖拽排序](#拖拽排序) 章节说明 | 拖拽排序保存成功后触发 | | saveOrderFail | `error` 错误原因 | 拖拽排序保存失败后触发 | -| selectedChange | `selectedItems: item[]` 已选择行
`unSelectedItems: item[]` 未选择行 | 手动选择表格项时触发 | +| selectedChange | `selectedItems: item[]` 已选择行
`selectedIndexes: string[]` 已选择行索引
`unSelectedItems: item[]` 未选择行 | 手动选择表格项时触发 | | columnSort | `orderBy: string` 列排序列名
`orderDir: string` 列排序值 | 点击列排序时触发 | | columnFilter | `filterName: string` 列筛选列名
`filterValue: string \| undefined` 列筛选值 | 点击列筛选时触发,点击重置后事件参数`filterValue`为`undefined` | | columnSearch | `searchName: string` 列搜索列名
`searchValue: object` 列搜索数据 | 点击列搜索时触发 | | orderChange | `movedItems: item[]` 已排序数据 | 手动拖拽行排序时触发 | | columnToggled | `columns: item[]` 当前显示的列配置数据 | 点击自定义列时触发 | | rowClick | `item: object` 行点击数据
`index: number` 行索引
`indexPath: string` 行索引路径 | 点击整行时触发 | +| rowDbClick | `item: object` 行点击数据
`index: number` 行索引
`indexPath: string` 行索引路径 | 双击整行时触发 | | rowMouseEnter | `item: object` 行移入数据
`index: number` 行索引
`indexPath: string` 行索引路径 | 移入整行时触发 | | rowMouseLeave | `item: object` 行移出数据
`index: number` 行索引
`indexPath: string` 行索引路径 | 移出整行时触发 | @@ -4549,6 +4985,68 @@ itemAction 里的 onClick 还能通过 `data` 参数拿到当前行的数据, } ``` +### rowDbClick + +双击行记录。 + +```schema: scope="body" +{ + "type": "page", + "body": { + "type": "crud", + "api": "/api/mock2/sample", + "syncLocation": false, + "onEvent": { + "rowDbClick": { + "actions": [ + { + "actionType": "toast", + "args": { + "msgType": "info", + "msg": "行单击数据:${event.data.item|json};行索引:${event.data.index}" + } + } + ] + } + }, + "columns": [ + { + "name": "id", + "label": "ID", + "searchable": true + }, + { + "name": "engine", + "label": "Rendering engine", + "filterable": { + "options": [ + "Internet Explorer 4.0", + "Internet Explorer 5.0" + ] + } + }, + { + "name": "browser", + "label": "Browser", + "sortable": true + }, + { + "name": "platform", + "label": "Platform(s)" + }, + { + "name": "version", + "label": "Engine version" + }, + { + "name": "grade", + "label": "CSS grade" + } + ] + } +} +``` + ### rowMouseEnter 鼠标移入行记录。 diff --git a/docs/zh-CN/components/table.md b/docs/zh-CN/components/table.md index c5146570f47..aed260da04a 100755 --- a/docs/zh-CN/components/table.md +++ b/docs/zh-CN/components/table.md @@ -1860,38 +1860,38 @@ popOver 的其它配置请参考 [popover](./popover) ### 列配置属性表 -| 属性名 | 类型 | 默认值 | 说明 | 版本 | -| ----------- | --------------------------------------------- | ------ | ------------------------ | -------- | -| label | [模板](../../docs/concepts/template) | | 表头文本内容 | | -| name | `string` | | 通过名称关联数据 | | -| width | `number` \| `string` | | 列宽 | | -| remark | | | 提示信息 | | -| fixed | `left` \| `right` \| `none` | | 是否固定当前列 | | -| popOver | | | 弹出框 | | -| copyable | `boolean` 或 `{icon: string, content:string}` | | 是否可复制 | | -| style | `object` | | 单元格自定义样式 | | -| innerStyle | `object` | | 单元格内部组件自定义样式 | `2.8.1` | -| align | `left` \| `right` \| `center` \| `justify` | | 单元格对齐方式 | ` 1.4.0` | -| headerAlign | `left` \| `right` \| `center` \| `justify` | | 表头单元格对齐方式 | `6.7.0` | -| vAlign | `top` \| `middle` \| `bottom` | | 单元格垂直对齐方式 | `6.7.0` | -| textOverflow | `string` |`default`| 文本溢出后展示形式,默认换行处理。可选值 `ellipsis` 溢出隐藏展示, `noWrap` 不换行展示(仅在列为静态文本时生效) | `6.10.0` | +| 属性名 | 类型 | 默认值 | 说明 | 版本 | +| ------------ | --------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------- | -------- | +| label | [模板](../../docs/concepts/template) | | 表头文本内容 | | +| name | `string` | | 通过名称关联数据 | | +| width | `number` \| `string` | | 列宽 | | +| remark | | | 提示信息 | | +| fixed | `left` \| `right` \| `none` | | 是否固定当前列 | | +| popOver | | | 弹出框 | | +| copyable | `boolean` 或 `{icon: string, content:string}` | | 是否可复制 | | +| style | `object` | | 单元格自定义样式 | | +| innerStyle | `object` | | 单元格内部组件自定义样式 | `2.8.1` | +| align | `left` \| `right` \| `center` \| `justify` | | 单元格对齐方式 | ` 1.4.0` | +| headerAlign | `left` \| `right` \| `center` \| `justify` | | 表头单元格对齐方式 | `6.7.0` | +| vAlign | `top` \| `middle` \| `bottom` | | 单元格垂直对齐方式 | `6.7.0` | +| textOverflow | `string` | `default` | 文本溢出后展示形式,默认换行处理。可选值 `ellipsis` 溢出隐藏展示, `noWrap` 不换行展示(仅在列为静态文本时生效) | `6.10.0` | ## 事件表 当前组件会对外派发以下事件,可以通过`onEvent`来监听这些事件,并通过`actions`来配置执行的动作,在`actions`中可以通过`${事件参数名}`或`${event.data.[事件参数名]}`来获取事件产生的数据,详细查看[事件动作](../../docs/concepts/event-action)。 -| 事件名称 | 事件参数 | 说明 | -| -------------- | ----------------------------------------------------------------------------------------- | -------------------- | -| selectedChange | `selectedItems: item[]` 已选择行
`unSelectedItems: item[]` 未选择行 | 手动选择表格项时触发 | -| columnSort | `orderBy: string` 列排序列名
`orderDir: string` 列排序值 | 点击列排序时触发 | -| columnFilter | `filterName: string` 列筛选列名
`filterValue: string` 列筛选值 | 点击列筛选时触发 | -| columnSearch | `searchName: string` 列搜索列名
`searchValue: string` 列搜索数据 | 点击列搜索时触发 | -| orderChange | `movedItems: item[]` 已排序数据 | 手动拖拽行排序时触发 | -| columnToggled | `columns: item[]` 当前显示的列配置数据 | 点击自定义列时触发 | -| rowClick | `item: object` 行点击数据
`index: number` 行索引
`indexPath: string` 行索引路径 | 单击整行时触发 | -| rowDbClick | `item: object` 行点击数据
`index: number` 行索引
`indexPath: string` 行索引路径 | 双击整行时触发 | -| rowMouseEnter | `item: object` 行移入数据
`index: number` 行索引
`indexPath: string` 行索引路径 | 移入整行时触发 | -| rowMouseLeave | `item: object` 行移出数据
`index: number` 行索引
`indexPath: string` 行索引路径 | 移出整行时触发 | +| 事件名称 | 事件参数 | 说明 | +| -------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------- | +| selectedChange | `selectedItems: item[]` 已选择行
`selectedIndexes: string[]` 已选择行索引
`unSelectedItems: item[]` 未选择行 | 手动选择表格项时触发 | +| columnSort | `orderBy: string` 列排序列名
`orderDir: string` 列排序值 | 点击列排序时触发 | +| columnFilter | `filterName: string` 列筛选列名
`filterValue: string` 列筛选值 | 点击列筛选时触发 | +| columnSearch | `searchName: string` 列搜索列名
`searchValue: string` 列搜索数据 | 点击列搜索时触发 | +| orderChange | `movedItems: item[]` 已排序数据 | 手动拖拽行排序时触发 | +| columnToggled | `columns: item[]` 当前显示的列配置数据 | 点击自定义列时触发 | +| rowClick | `item: object` 行点击数据
`index: number` 行索引
`indexPath: string` 行索引路径 | 单击整行时触发 | +| rowDbClick | `item: object` 行点击数据
`index: number` 行索引
`indexPath: string` 行索引路径 | 双击整行时触发 | +| rowMouseEnter | `item: object` 行移入数据
`index: number` 行索引
`indexPath: string` 行索引路径 | 移入整行时触发 | +| rowMouseLeave | `item: object` 行移出数据
`index: number` 行索引
`indexPath: string` 行索引路径 | 移出整行时触发 | ### selectedChange diff --git a/packages/amis-core/src/RootRenderer.tsx b/packages/amis-core/src/RootRenderer.tsx index 847784e6c0d..36cc911046a 100644 --- a/packages/amis-core/src/RootRenderer.tsx +++ b/packages/amis-core/src/RootRenderer.tsx @@ -234,11 +234,11 @@ export class RootRenderer extends React.Component { ); }); } else if (action.actionType === 'toast') { - action.toast?.items?.forEach((item: any) => { + action.toast?.items?.forEach(({level, body, title, ...item}: any) => { env.notify( - item.level || 'info', - item.body - ? render('body', item.body, { + level || 'info', + body + ? render('body', body, { ...this.props, data: ctx, context: store.context @@ -247,8 +247,8 @@ export class RootRenderer extends React.Component { { ...action.toast, ...item, - title: item.title - ? render('title', item.title, { + title: title + ? render('title', title, { ...this.props, data: ctx, context: store.context diff --git a/packages/amis-core/src/store/crud.ts b/packages/amis-core/src/store/crud.ts index 1a18cb4aa5e..ac40f9a0420 100644 --- a/packages/amis-core/src/store/crud.ts +++ b/packages/amis-core/src/store/crud.ts @@ -10,6 +10,7 @@ import { applyFilters, isEmpty, qsstringify, + findTreeIndex, getVariable } from '../utils/helper'; import {Api, Payload, fetchOptions, ActionObject, ApiObject} from '../types'; @@ -94,22 +95,16 @@ export const CRUDStore = ServiceStore.named('CRUDStore') // 包两层,主要是为了处理以下 case // 里面放了个 form,form 提交过来的时候不希望把 items 这些发送过来。 // 因为会把数据呈现在地址栏上。 - return createObject( - createObject(self.data, { - items: self.items.concat(), - selectedItems: self.selectedItems.concat(), - unSelectedItems: self.unSelectedItems.concat() - }), - {...self.query} - ); + return createObject(createObject(self.data, this.eventContext), { + ...self.query + }); }, get mergedData() { return extendObject(self.data, { ...self.query, - ...self.data, - selectedItems: self.selectedItems, - unSelectedItems: self.unSelectedItems + ...this.eventContext, + ...self.data }); }, @@ -121,6 +116,10 @@ export const CRUDStore = ServiceStore.named('CRUDStore') return self.selectedItems.concat(); }, + get itemsAsArray() { + return self.items.concat(); + }, + fetchCtxOf( data: any, options: { @@ -134,6 +133,23 @@ export const CRUDStore = ServiceStore.named('CRUDStore') [options.perPageField || 'perPage']: self.perPage, ...data }); + }, + + get eventContext() { + const context = { + items: self.items.concat(), + selectedItems: self.selectedItems.concat(), + unSelectedItems: self.unSelectedItems.concat(), + selectedIndexes: self.selectedItems.map( + item => + findTreeIndex( + self.items, + i => (item.__pristine || item) === (i.__pristine || i) + )?.join('.') || '-1' + ) + }; + + return context; } })) .actions(self => { @@ -727,9 +743,7 @@ export const CRUDStore = ServiceStore.named('CRUDStore') total: self.total, page: self.page, perPage: self.perPage, - items: self.items.concat(), - selectedItems: self.selectedItems.concat(), - unSelectedItems: self.unSelectedItems.concat() + ...self.eventContext }); }; @@ -776,7 +790,10 @@ export const CRUDStore = ServiceStore.named('CRUDStore') exportAsCSV, updateColumns, updateTotal, - resetSelection + resetSelection, + replaceItems(items: Array) { + self.items.replace(items); + } }; }); diff --git a/packages/amis-core/src/store/list.ts b/packages/amis-core/src/store/list.ts index 7ada604fbbb..ab2a00f42a0 100644 --- a/packages/amis-core/src/store/list.ts +++ b/packages/amis-core/src/store/list.ts @@ -46,8 +46,7 @@ export const Item = types index: self.index, // 只有table时,也可以获取选中行 - selectedItems: listStore.selectedItems.map(item => item.data), - unSelectedItems: listStore.unSelectedItems.map(item => item.data) + ...listStore.eventContext }), self.data ); @@ -101,6 +100,13 @@ export const ListStore = iRendererStore .named('ListStore') .props({ items: types.array(Item), + + // 记录原始列表和原始选中的列表 + // 因为如果是前端分页,上层 crud 或者 input-table 下发到这层的 + // 是某个页区间的数据,这个时候 items 和 selectedItems 会少很多条 + fullItems: types.optional(types.array(types.frozen()), []), + fullSelectedItems: types.optional(types.array(types.frozen()), []), + selectedItems: types.array(types.reference(Item)), primaryField: 'id', orderBy: '', @@ -171,6 +177,45 @@ export const ListStore = iRendererStore get movedItems() { return getMovedItems(); + }, + + /** + * 构建事件的上下文数据 + * @param buildChain + * @returns + */ + get eventContext() { + const context = { + selectedItems: self.selectedItems.map(item => item.data), + selectedIndexes: self.selectedItems.map(item => item.index), + items: self.items.map(item => item.data), + unSelectedItems: this.unSelectedItems.map(item => item.data) + }; + + // 如果是前端分页情况,需要根据全量数据计算 + // 如果不是前端分页,数据都没有返回,那种就没办法支持全量数据信息了 + if (self.fullItems.length > self.items.length) { + // todo 这里的选择顺序会一直变,这个有影响吗? + const selectedItems = self.fullSelectedItems + .filter( + item => + !self.items.find( + row => row.pristine === (item.__pristine || item) + ) + ) + .concat(context.selectedItems); + + context.selectedItems = selectedItems; + context.items = self.fullItems.concat(); + context.unSelectedItems = self.fullItems.filter( + item => !selectedItems.includes(item) + ); + context.selectedIndexes = selectedItems.map(item => + self.fullItems.indexOf(item.__pristine || item) + ); + } + + return context; } }; }) @@ -196,7 +241,11 @@ export const ListStore = iRendererStore (self.itemDraggableOn = config.itemDraggableOn); } - function initItems(items: Array) { + function initItems( + items: Array, + fullItems?: Array, + fullSelectedItems?: Array + ) { let arr = items.map((item, key) => { item = isObject(item) ? item @@ -209,14 +258,16 @@ export const ListStore = iRendererStore id: guid(), index: key, newIndex: key, - pristine: item, - data: item, - modified: false + pristine: (item as any).__pristine || item, + data: item }; }); self.selectedItems.clear(); self.items.replace(arr as Array); self.dragging = false; + Array.isArray(fullItems) && self.fullItems.replace(fullItems); + Array.isArray(fullSelectedItems) && + self.fullSelectedItems.replace(fullSelectedItems); } function updateSelected(selected: Array, valueField?: string) { @@ -319,9 +370,7 @@ export const ListStore = iRendererStore function getData(superData: any): any { return createObject(superData, { - items: self.items.map(item => item.data), - selectedItems: self.selectedItems.map(item => item.data), - unSelectedItems: self.unSelectedItems.map(item => item.data) + ...self.eventContext }); } diff --git a/packages/amis-core/src/store/table.ts b/packages/amis-core/src/store/table.ts index 2ec945b03c1..1a264282b9a 100644 --- a/packages/amis-core/src/store/table.ts +++ b/packages/amis-core/src/store/table.ts @@ -22,6 +22,7 @@ import { isVisible, guid, findTree, + findTreeIndex, flattenTree, eachTree, difference, @@ -318,12 +319,12 @@ export const Row = types return createObject( extendObject((getParent(self, self.depth * 2) as ITableStore).data, { index: self.index, + path: self.path, // todo 以后再支持多层,目前先一层 parent: parent.storeType === Row.name ? parent.data : undefined, // 只有table时,也可以获取选中行 - selectedItems: table.selectedRows.map(item => item.data), - unSelectedItems: table.unSelectedRows.map(item => item.data) + ...table.eventContext }), children ? { @@ -410,8 +411,18 @@ export const Row = types }, change(values: object, savePristine?: boolean) { - self.data = immutableExtends(self.data, values); - savePristine && (self.pristine = self.data); + let data = immutableExtends(self.data, values); + + Object.isExtensible(data) && + Object.defineProperty(data, '__pristine', { + value: savePristine ? data : self.pristine, + enumerable: false, + configurable: false, + writable: false + }); + + self.data = data; + savePristine && (self.pristine = data); }, reset() { @@ -510,6 +521,13 @@ export const TableStore = iRendererStore .props({ columns: types.array(Column), rows: types.array(Row), + + // 记录原始列表和原始选中的列表 + // 因为如果是前端分页,上层 crud 或者 input-table 下发到这层的 + // 是某个页区间的数据,这个时候 items 和 selectedItems 会少很多条 + fullItems: types.optional(types.array(types.frozen()), []), + fullSelectedItems: types.optional(types.array(types.frozen()), []), + selectedRows: types.array(types.reference(Row)), expandedRows: types.array(types.string), primaryField: 'id', @@ -683,14 +701,6 @@ export const TableStore = iRendererStore return flattenTree(self.rows).filter((item: IRow) => !item.checked); } - function getData(superData: any): any { - return createObject(superData, { - items: self.rows.map(item => item.data), - selectedItems: self.selectedRows.map(item => item.data), - unSelectedItems: getUnSelectedRows().map(item => item.data) - }); - } - function hasColumnHidden() { return self.columns.findIndex(column => !column.toggled) !== -1; } @@ -934,7 +944,9 @@ export const TableStore = iRendererStore return getFirstToggledColumnIndex(); }, - getData, + getData(superData: any): any { + return createObject(superData, this.eventContext); + }, get columnGroup() { return getColumnGroup(); @@ -1052,6 +1064,49 @@ export const TableStore = iRendererStore }); return style; + }, + + /** + * 构建事件的上下文数据 + * @param buildChain + * @returns + */ + get eventContext() { + const context = { + selectedItems: self.selectedRows.map(item => item.data), + selectedIndexes: self.selectedRows.map(item => item.path), + items: self.rows.map(item => item.data), + unSelectedItems: this.unSelectedRows.map(item => item.data) + }; + + // 如果是前端分页情况,需要根据全量数据计算 + // 如果不是前端分页,数据都没有返回,那种就没办法支持全量数据信息了 + if (self.fullItems.length > self.rows.length) { + // todo 这里的选择顺序会一直变,这个有影响吗? + const selectedItems = self.fullSelectedItems + .filter( + item => + !self.rows.find( + row => row.pristine === (item.__pristine || item) + ) + ) + .concat(context.selectedItems); + + context.selectedItems = selectedItems; + context.items = self.fullItems.concat(); + context.unSelectedItems = self.fullItems.filter( + item => !selectedItems.includes(item) + ); + context.selectedIndexes = selectedItems.map( + item => + findTreeIndex( + self.fullItems, + i => (item.__pristine || item) === (i.__pristine || i) + )?.join('.') || '-1' + ); + } + + return context; } }; }) @@ -1444,7 +1499,9 @@ export const TableStore = iRendererStore function initRows( rows: Array, getEntryId?: (entry: any, index: number) => string, - reUseRow?: boolean | 'match' + reUseRow?: boolean | 'match', + fullItems?: Array, + fullSelectedItems?: Array ) { self.selectedRows.clear(); // self.expandedRows.clear(); @@ -1469,7 +1526,7 @@ export const TableStore = iRendererStore depth: 1, // 最大父节点默认为第一层,逐层叠加 index: index, newIndex: index, - pristine: item, + pristine: item.__pristine || item, path: `${index}`, data: item, rowSpans: {}, @@ -1537,6 +1594,10 @@ export const TableStore = iRendererStore } self.dragging = false; + + Array.isArray(fullItems) && self.fullItems.replace(fullItems); + Array.isArray(fullSelectedItems) && + self.fullSelectedItems.replace(fullSelectedItems); } // 获取所有层级的子节点id diff --git a/packages/amis-editor/src/plugin/CRUD.tsx b/packages/amis-editor/src/plugin/CRUD.tsx index a000dfb73fd..488f9274087 100644 --- a/packages/amis-editor/src/plugin/CRUD.tsx +++ b/packages/amis-editor/src/plugin/CRUD.tsx @@ -144,6 +144,10 @@ export class CRUDPlugin extends BasePlugin { unSelectedItems: { type: 'array', title: '未选择行记录' + }, + selectedIndexes: { + type: 'array', + title: '已选择行索引' } } } @@ -2331,6 +2335,10 @@ export class CRUDPlugin extends BasePlugin { properties: itemsSchema } }, + selectedIndexes: { + type: 'array', + title: '已选择行索引' + }, count: { type: 'number', title: '总行数' diff --git a/packages/amis-ui/scss/_mixins.scss b/packages/amis-ui/scss/_mixins.scss index f4c902b12d1..8cda387d219 100644 --- a/packages/amis-ui/scss/_mixins.scss +++ b/packages/amis-ui/scss/_mixins.scss @@ -661,7 +661,8 @@ line-height: calc( var(--Form-input-lineHeight) * var(--Form-input-fontSize) - #{px2rem(2px)} ); - display: inline-block; + display: inline-flex; + align-items: center; font-size: var(--Pick-base-value-fontSize); color: var(--Pick-base-value-color); font-weight: var(--Pick-base-value-fontWeight); @@ -682,9 +683,6 @@ var(--Pick-base-top-right-border-radius) var(--Pick-base-bottom-right-border-radius) var(--Pick-base-bottom-left-border-radius); - margin-right: var(--gap-xs); - margin-bottom: var(--gap-xs); - margin-top: var(--gap-xs); max-width: px2rem(150px); overflow: hidden; text-overflow: ellipsis; @@ -696,7 +694,10 @@ &.is-disabled { pointer-events: none; - opacity: var(--Button-onDisabled-opacity); + + .#{$ns}#{$component-prefix}-valueIcon { + opacity: var(--Button-onDisabled-opacity); + } } } @@ -704,7 +705,7 @@ color: var(--Pick-base-value-icon-color); cursor: pointer; border-right: px2rem(1px) solid var(--Form-selectValue-borderColor); - padding: 1px 5px; + padding: 0 5px; &:hover { background: var(--Pick-base-value-hover-icon-color); diff --git a/packages/amis-ui/scss/_properties.scss b/packages/amis-ui/scss/_properties.scss index 668498456d4..51873ee173e 100644 --- a/packages/amis-ui/scss/_properties.scss +++ b/packages/amis-ui/scss/_properties.scss @@ -179,6 +179,7 @@ $Table-strip-bg: transparent; --ButtonGroup-divider-width: #{px2rem(1px)}; --ButtonGroup-divider-color: #fff; --ButtonGroup-borderWidth: var(--borders-width-2); + --Button-onDisabled-opacity: 0.3; --Breadcrumb-item-fontSize: var(--fontSizeMd); --Breadcrumb-item-default-color: var(--colors-neutral-text-5); diff --git a/packages/amis-ui/scss/components/_crud.scss b/packages/amis-ui/scss/components/_crud.scss index fceeddf2f25..d8fb7d24ad4 100644 --- a/packages/amis-ui/scss/components/_crud.scss +++ b/packages/amis-ui/scss/components/_crud.scss @@ -9,6 +9,10 @@ &-selection { margin-bottom: var(--gap-base); + display: flex; + flex-wrap: wrap; + gap: var(--gap-xs); + line-height: 1; &-overflow { &-wrapper { @@ -25,6 +29,7 @@ (var(--Picker-tag-height) + var(--Picker-tag-marginBottom)) * 5 ); + gap: var(--gap-xs); @include tag-item(Crud); } } diff --git a/packages/amis-ui/scss/components/form/_picker.scss b/packages/amis-ui/scss/components/form/_picker.scss index 44f2bc5cfc2..d1ddd720bae 100644 --- a/packages/amis-ui/scss/components/form/_picker.scss +++ b/packages/amis-ui/scss/components/form/_picker.scss @@ -71,7 +71,8 @@ font-size: var(--Pick-base-placeholder-fontSize); font-weight: var(--Pick-base-placeholder-fontWeight); user-select: none; - position: absolute; + flex: 1; + min-width: 0; // margin-top: 2 * var(--Form-input-borderWidth); line-height: var(--Form-input-lineHeight); padding: var(--Pick-base-paddingTop) var(--Pick-base-paddingRight) @@ -95,15 +96,15 @@ var(--Pick-base-left-border-color); } - .#{$ns}Picker-values { - display: inline; + // .#{$ns}Picker-values { + // display: inline; - .#{$ns}OverflowTpl { - .#{$ns}Picker-valueLabel { - pointer-events: auto; - } - } - } + // .#{$ns}OverflowTpl { + // .#{$ns}Picker-valueLabel { + // pointer-events: auto; + // } + // } + // } &-valueWrap { flex-grow: 1; @@ -117,8 +118,9 @@ } .#{$ns}Picker-valueWrap { - margin-bottom: calc(var(--gap-xs) * -1); - line-height: 1; + display: flex; + flex-wrap: wrap; + gap: var(--gap-xs); } /* tag 样式 */ @@ -176,6 +178,7 @@ (var(--Picker-tag-height) + var(--Picker-tag-marginBottom)) * 5 ); + gap: var(--gap-xs); @include tag-item(Picker); } } diff --git a/packages/amis-ui/src/locale/de-DE.ts b/packages/amis-ui/src/locale/de-DE.ts index 6a3cba02c89..68f9e152189 100644 --- a/packages/amis-ui/src/locale/de-DE.ts +++ b/packages/amis-ui/src/locale/de-DE.ts @@ -50,6 +50,8 @@ register('de-DE', { 'CRUD.stat': '{{page}} von {{lastPage}} insgesamt: {{total}}.', 'CRUD.paginationGoText': 'Wechseln zu', 'CRUD.paginationPageText': 'Seite', + 'CRUD.confirmLeaveUnSavedPage': + 'Ändern Seite wird nicht gespeicherte Daten verlieren, bestätigen Sie bitte.', 'PaginationWrapper.placeholder': 'Textkörper konfigurieren', 'Pagination.select': '{{count}} items/page', 'Pagination.goto': 'goto', diff --git a/packages/amis-ui/src/locale/en-US.ts b/packages/amis-ui/src/locale/en-US.ts index 2d2719450cb..9263f68557c 100644 --- a/packages/amis-ui/src/locale/en-US.ts +++ b/packages/amis-ui/src/locale/en-US.ts @@ -45,6 +45,8 @@ register('en-US', { 'CRUD.stat': '{{page}} of {{lastPage}} total: {{total}}.', 'CRUD.paginationGoText': 'Go to', 'CRUD.paginationPageText': 'page', + 'CRUD.confirmLeaveUnSavedPage': + 'Change page will loss unsaved data, please confirm.', 'PaginationWrapper.placeholder': 'please config body', 'Pagination.select': '{{count}} items/page', 'Pagination.goto': 'goto', diff --git a/packages/amis-ui/src/locale/zh-CN.ts b/packages/amis-ui/src/locale/zh-CN.ts index 333193870ac..511b8c3a501 100644 --- a/packages/amis-ui/src/locale/zh-CN.ts +++ b/packages/amis-ui/src/locale/zh-CN.ts @@ -48,6 +48,7 @@ register('zh-CN', { 'CRUD.stat': '{{page}}/{{lastPage}} 共:{{total}} 项', 'CRUD.paginationGoText': '前往', 'CRUD.paginationPageText': '页', + 'CRUD.confirmLeaveUnSavedPage': '分页会丢失未保存的数据,请确认', 'PaginationWrapper.placeholder': '请配置内容', 'Pagination.select': '{{count}}条/页', 'Pagination.goto': '跳转至', diff --git a/packages/amis/__tests__/renderers/Picker.test.tsx b/packages/amis/__tests__/renderers/Picker.test.tsx index ff47b73e018..389ad81ecb4 100644 --- a/packages/amis/__tests__/renderers/Picker.test.tsx +++ b/packages/amis/__tests__/renderers/Picker.test.tsx @@ -296,13 +296,15 @@ describe('5. Renderer:Picker with overflowConfig', () => { await wait(500); - const tags = container.querySelector('.cxd-Picker-values'); + const tags = container.querySelector('.cxd-Picker-valueWrap'); expect(tags).toBeInTheDocument(); /** tag 元素数量正确 */ - expect(tags?.childElementCount).toEqual(3); + expect(tags?.childElementCount).toEqual(4); // 还有个 input /** 收纳标签文案正确 */ - expect(tags?.lastElementChild).toHaveTextContent('+ 1 ...'); + expect(tags?.lastElementChild?.previousSibling).toHaveTextContent( + '+ 1 ...' + ); }); test('5-2. Renderer:Picker embeded', async () => { diff --git a/packages/amis/__tests__/renderers/__snapshots__/Picker.test.tsx.snap b/packages/amis/__tests__/renderers/__snapshots__/Picker.test.tsx.snap index 49cf54bec3e..1a7a4ed50a7 100644 --- a/packages/amis/__tests__/renderers/__snapshots__/Picker.test.tsx.snap +++ b/packages/amis/__tests__/renderers/__snapshots__/Picker.test.tsx.snap @@ -38,16 +38,6 @@ exports[`1. Renderer:Picker base 1`] = ` > picker-placeholder -
-
- -
@@ -267,22 +257,18 @@ exports[`1. Renderer:Picker base 2`] = ` class="cxd-Picker-valueWrap" >
-
- - × - - - B - -
+ × + + + B +
= [ 'selected' ]; -export default class CRUD extends React.Component { +export default class CRUD extends React.Component { static propsList: Array = [ 'bulkActions', 'itemActions', @@ -528,7 +550,7 @@ export default class CRUD extends React.Component { omitBy(onEvent, (event, key: any) => !INNER_EVENTS.includes(key)) ); - constructor(props: CRUDProps) { + constructor(props: T) { super(props); this.controlRef = this.controlRef.bind(this); @@ -541,6 +563,7 @@ export default class CRUD extends React.Component { this.handleBulkGo = this.handleBulkGo.bind(this); this.handleDialogConfirm = this.handleDialogConfirm.bind(this); this.handleDialogClose = this.handleDialogClose.bind(this); + this.handleItemChange = this.handleItemChange.bind(this); this.handleSave = this.handleSave.bind(this); this.handleSaveOrder = this.handleSaveOrder.bind(this); this.handleSelect = this.handleSelect.bind(this); @@ -599,7 +622,7 @@ export default class CRUD extends React.Component { // 因此需要将componentDidMount中的设置选中项提前到constructor,否则handleSelect里拿不到的选中项 let val: any; if (this.props.pickerMode && (val = getPropValue(this.props))) { - store.setSelectedItems(val); + this.syncSelectedFromPicker(val); } } @@ -649,7 +672,7 @@ export default class CRUD extends React.Component { * 更新链:Table -> CRUD -> Picker -> Form * 对于Picker模式来说,执行到这里的时候store.selectedItems已经更新过了,所以需要额外判断一下 */ - store.setSelectedItems(val); + this.syncSelectedFromPicker(val); } if (!!this.props.filterTogglable !== !!prevProps.filterTogglable) { @@ -708,6 +731,11 @@ export default class CRUD extends React.Component { store.initFromScope(props.data, props.source, { columns: store.columns ?? props.columns }); + + if (this.props.pickerMode && (val = getPropValue(this.props))) { + this.syncSelectedFromPicker(val); + } + this.lastData = next; } } @@ -811,6 +839,11 @@ export default class CRUD extends React.Component { const redirect = action.redirect && filter(action.redirect, data); redirect && action.blank && env.jumpTo(redirect, action, data); + // 如果 api 无效,或者不满足发送条件,则直接返回 + if (!isEffectiveApi(action.api, data)) { + return; + } + return store .saveRemote(action.api!, data, { successMessage: @@ -898,11 +931,10 @@ export default class CRUD extends React.Component { }, { ...selectedItems[0], + ...store.eventContext, currentPageData: (store.mergedData?.items || []).concat(), rows: selectedItems, items: selectedItems, - selectedItems, - unSelectedItems: unSelectedItems, ids } ]); @@ -975,9 +1007,16 @@ export default class CRUD extends React.Component { } handleFilterInit(values: object) { - const {defaultParams, data, store, orderBy, orderDir, dispatchEvent} = - this.props; - const params = {...defaultParams}; + const { + defaultParams, + columns, + matchFunc, + store, + orderBy, + orderDir, + dispatchEvent + } = this.props; + const params: any = {...defaultParams}; if (orderBy) { params['orderBy'] = orderBy; @@ -999,11 +1038,22 @@ export default class CRUD extends React.Component { store.setPristineQuery(); const {pickerMode, options} = this.props; - - pickerMode && - store.updateData({ - items: options || [] - }); + if (pickerMode) { + store.initFromScope( + { + items: options || [] + }, + '${items}', + { + columns: store.columns ?? columns, + matchFunc + } + ); + let val: any; + if ((val = getPropValue(this.props))) { + this.syncSelectedFromPicker(val); + } + } } handleFilterReset(values: object, action: any) { @@ -1370,7 +1420,7 @@ export default class CRUD extends React.Component { page > 1 && lastPage < page ) { - this.search( + await this.search( { ...store.query, [pageField || 'page']: lastPage @@ -1404,6 +1454,11 @@ export default class CRUD extends React.Component { }); } + let val: any; + if (this.props.pickerMode && (val = getPropValue(this.props))) { + this.syncSelectedFromPicker(val); + } + return store.data; } @@ -1411,7 +1466,7 @@ export default class CRUD extends React.Component { return this.search(values, true, clearSelection, forceReload); } - handleChangePage( + async handleChangePage( page: number, perPage?: number, dir?: 'forward' | 'backward' @@ -1423,9 +1478,19 @@ export default class CRUD extends React.Component { pageField, perPageField, pageDirectionField, - autoJumpToTopOnPagerChange + autoJumpToTopOnPagerChange, + translate: __, + api, + loadDataOnce } = this.props; + if (api && !loadDataOnce && this.control?.hasModifiedItems()) { + const confirmed = await confirm(__('CRUD.confirmLeaveUnSavedPage')); + if (!confirmed) { + return; + } + } + let query: any = { [pageField || 'page']: page }; @@ -1458,6 +1523,36 @@ export default class CRUD extends React.Component { } } + syncSelectedFromPicker(value: Array) { + const {store, primaryField, strictMode} = this.props; + const isSameValue = ( + a: Record, + item: Record + ) => { + const oldValue = a[primaryField || 'id']; + const itemValue = item[primaryField || 'id']; + const isSame = strictMode + ? oldValue === itemValue + : oldValue == itemValue; + return !!(a === item || (oldValue && isSame)); + }; + + const selectedItems = value.map( + item => findTree(store.items, a => isSameValue(a, item)) || item + ); + + this.props.store.setSelectedItems(selectedItems); + } + + handleItemChange(item: object, diff: object, index: string | number) { + const {store} = this.props; + + const indexes = `${index}`.split('.').map(item => parseInt(item, 10)); + const items = spliceTree(store.items, indexes, 1, item); + + store.replaceItems(items); + } + handleSave( rows: Array | object, diff: Array | object, @@ -1741,75 +1836,114 @@ export default class CRUD extends React.Component { onSelect } = this.props; let newItems = items; - let newUnSelectedItems = unSelectedItems; - if (keepItemSelectionOnPageChange && store.selectedItems.length) { - const oldItems = store.selectedItems.concat(); - const oldUnselectedItems = store.unSelectedItems.concat(); - - const isSameValue = ( - a: Record, - item: Record - ) => { - const oldValue = a[primaryField || 'id']; - const itemValue = item[primaryField || 'id']; - const isSame = strictMode - ? oldValue === itemValue - : oldValue == itemValue; - return a === item || (oldValue && isSame); - }; - - items.forEach(item => { - const idx = findIndex(oldItems, a => isSameValue(a, item)); - if (~idx) { - oldItems[idx] = item; - } else { - oldItems.push(item); - } - - const idx2 = findIndex(oldUnselectedItems, a => isSameValue(a, item)); - - if (~idx2) { - oldUnselectedItems.splice(idx2, 1); - } - }); - - unSelectedItems.forEach(item => { - const idx = findIndex(oldUnselectedItems, a => isSameValue(a, item)); - - const idx2 = findIndex(oldItems, a => isSameValue(a, item)); - - if (~idx) { - oldUnselectedItems[idx] = item; - } else { - oldUnselectedItems.push(item); - } - !~idx && ~idx2 && oldItems.splice(idx2, 1); - }); - - newItems = oldItems; - newUnSelectedItems = oldUnselectedItems; + if (keepItemSelectionOnPageChange && store.selectedItems.length) { + const rangeItems = Array.isArray(store.data.items) + ? (store.data.items as Array) + : []; + const itemsRest = items.concat(); - // const thisBatch = items.concat(unSelectedItems); - // let notInThisBatch = (item: any) => - // !find( - // thisBatch, - // a => a[primaryField || 'id'] == item[primaryField || 'id'] - // ); + // todo 懒加载新加进来的行不在 rangeItems 里面,所以可能清除不了 - // newItems = store.selectedItems.filter(notInThisBatch); - // newUnSelectedItems = store.unSelectedItems.filter(notInThisBatch); + newItems = store.selectedItems + .map(item => { + const idx = itemsRest.findIndex( + a => (a.__pristine || a) === (item.__pristine || item) + ); - // newItems.push(...items); - // newUnSelectedItems.push(...unSelectedItems); - } + if (~idx) { + return itemsRest.splice(idx, 1)[0]; + } else if ( + !rangeItems.length || + store.items.length === rangeItems.length + ) { + // 如果没有分页,那么不需要保留其他页面的已选 + return null; + } - if (pickerMode && multiple === false && newItems.length > 1) { - newUnSelectedItems.push.apply( - newUnSelectedItems, - newItems.splice(0, newItems.length - 1) - ); - } + return findTree( + rangeItems, + a => (a.__pristine || a) === (item.__pristine || item) + ) + ? null + : item; + }) + .filter(item => item) + .concat(itemsRest); + } + + const newUnSelectedItems = store.items + .filter(item => !newItems.find(a => (a.__pristine || a) === item)) + .map(item => unSelectedItems.find(a => a.__pristine === item) || item); + + // if (keepItemSelectionOnPageChange && store.selectedItems.length) { + // const oldItems = store.selectedItems.concat(); + // const oldUnselectedItems = store.unSelectedItems.concat(); + + // const isSameValue = ( + // a: Record, + // item: Record + // ) => { + // const oldValue = a[primaryField || 'id']; + // const itemValue = item[primaryField || 'id']; + // const isSame = strictMode + // ? oldValue === itemValue + // : oldValue == itemValue; + // return a === item || (oldValue && isSame); + // }; + + // items.forEach(item => { + // const idx = findIndex(oldItems, a => isSameValue(a, item)); + + // if (~idx) { + // oldItems[idx] = item; + // } else { + // oldItems.push(item); + // } + + // const idx2 = findIndex(oldUnselectedItems, a => isSameValue(a, item)); + + // if (~idx2) { + // oldUnselectedItems.splice(idx2, 1); + // } + // }); + + // unSelectedItems.forEach(item => { + // const idx = findIndex(oldUnselectedItems, a => isSameValue(a, item)); + + // const idx2 = findIndex(oldItems, a => isSameValue(a, item)); + + // if (~idx) { + // oldUnselectedItems[idx] = item; + // } else { + // oldUnselectedItems.push(item); + // } + // !~idx && ~idx2 && oldItems.splice(idx2, 1); + // }); + + // newItems = oldItems; + // newUnSelectedItems = oldUnselectedItems; + + // // const thisBatch = items.concat(unSelectedItems); + // // let notInThisBatch = (item: any) => + // // !find( + // // thisBatch, + // // a => a[primaryField || 'id'] == item[primaryField || 'id'] + // // ); + + // // newItems = store.selectedItems.filter(notInThisBatch); + // // newUnSelectedItems = store.unSelectedItems.filter(notInThisBatch); + + // // newItems.push(...items); + // // newUnSelectedItems.push(...unSelectedItems); + // } + + // if (pickerMode && multiple === false && newItems.length > 1) { + // newUnSelectedItems.push.apply( + // newUnSelectedItems, + // newItems.splice(0, newItems.length - 1) + // ); + // } // 用 updateSelectData 导致 CRUD 无限刷新 // store.updateSelectData(newItems, newUnSelectedItems); store.setSelectedItems(newItems); @@ -1930,14 +2064,12 @@ export default class CRUD extends React.Component { 'toggleExpanded', 'setExpanded', 'initDrag', - 'cancelDrag' + 'cancelDrag', + 'selectAll', + 'clearAll' ].includes(action.actionType) ) { return this.control?.doAction(action, data, throwErrors, args); - } else if (action.actionType === 'selectAll') { - return this.handleSelect(store.items.concat(), []); - } else if (action.actionType === 'clearAll') { - return this.handleSelect([], store.items.concat()); } else if (action.actionType === 'select') { const selectedItems = await getMatchedEventTargets( store.items, @@ -1948,6 +2080,8 @@ export default class CRUD extends React.Component { const unSelectedItems = store.items.filter( item => !selectedItems.includes(item) ); + // todo 这里的 selected 和 unselected 不是修改后的 + // return this.handleSelect(selectedItems, unSelectedItems); } @@ -1967,11 +2101,14 @@ export default class CRUD extends React.Component { } clearSelection() { - const {store} = this.props; - const selected = store.selectedItems.concat(); - const unSelected = store.unSelectedItems.concat(selected); + const {store, itemCheckableOn} = this.props; + const [unchecked, checked] = partition( + store.selectedItems, + item => !itemCheckableOn || evalExpression(itemCheckableOn, item) + ); + const unSelected = store.unSelectedItems.concat(unchecked); - store.setSelectedItems([]); + store.setSelectedItems(checked); store.setUnSelectedItems(unSelected); } @@ -2029,10 +2166,9 @@ export default class CRUD extends React.Component { let itemBtns: Array = []; const ctx = createObject(store.mergedData, { currentPageData: (store.mergedData?.items || []).concat(), + ...store.eventContext, rows: selectedItems.concat(), items: selectedItems.concat(), - selectedItems: selectedItems.concat(), - unSelectedItems: unSelectedItems.concat(), ids: selectedItems .map(item => item.hasOwnProperty(primaryField) @@ -2395,7 +2531,7 @@ export default class CRUD extends React.Component { }); } else if (Array.isArray(toolbar)) { const children: Array = toolbar - .filter((toolbar: any) => isVisible(toolbar, store.filterData)) + .filter((toolbar: any) => isVisible(toolbar, store.toolbarData)) .map((toolbar, index) => ({ dom: this.renderToolbar(toolbar, index, childProps, toolbarRenderer), toolbar @@ -2468,12 +2604,12 @@ export default class CRUD extends React.Component { if (toolbar) { if (Array.isArray(headerToolbar)) { headerToolbar = toolbarInline - ? headerToolbar.concat(toolbar) - : [headerToolbar, toolbar]; + ? headerToolbar.concat(toolbar as any) + : ([headerToolbar, toolbar] as any); } else if (headerToolbar) { - headerToolbar = [headerToolbar, toolbar]; + headerToolbar = [headerToolbar, toolbar] as any; } else { - headerToolbar = toolbar; + headerToolbar = toolbar as any; } } @@ -2493,13 +2629,15 @@ export default class CRUD extends React.Component { if (toolbar) { if (Array.isArray(footerToolbar)) { - footerToolbar = toolbarInline - ? footerToolbar.concat(toolbar) - : [footerToolbar, toolbar]; + footerToolbar = ( + toolbarInline + ? footerToolbar.concat(toolbar as any) + : [footerToolbar, toolbar] + ) as any; } else if (footerToolbar) { - footerToolbar = [footerToolbar, toolbar]; + footerToolbar = [footerToolbar, toolbar] as any; } else { - footerToolbar = toolbar; + footerToolbar = toolbar as any; } } @@ -2514,11 +2652,19 @@ export default class CRUD extends React.Component { primaryField, valueField, translate: __, - env + env, + itemCheckableOn } = this.props; + const checkable = itemCheckableOn + ? evalExpression(itemCheckableOn, item) + : true; + return ( -
+
{ testIdBuilder, id, filterCanAccessSuperData = true, + selectable = false, ...rest } = this.props; @@ -2744,7 +2891,8 @@ export default class CRUD extends React.Component { autoFillHeight: autoFillHeight, selectable: !!( (this.hasBulkActionsToolbar() && this.hasBulkActions()) || - pickerMode + pickerMode || + selectable ), itemActions, multiple: @@ -2762,11 +2910,13 @@ export default class CRUD extends React.Component { primaryField: primaryField, hideQuickSaveBtn, items: store.data.items, + fullItems: store.itemsAsArray, query: store.query, orderBy: store.query.orderBy, orderDir: store.query.orderDir, popOverContainer, onAction: this.handleAction, + onItemChange: this.handleItemChange, onSave: this.handleSave, onSaveOrder: this.handleSaveOrder, onQuery: this.handleQuery, @@ -2804,15 +2954,10 @@ export default class CRUD extends React.Component { } } -@Renderer({ - type: 'crud', - storeType: CRUDStore.name, - isolateScope: true -}) -export class CRUDRenderer extends CRUD { +export class CRUDRendererBase extends CRUD { static contextType = ScopedContext; - constructor(props: CRUDProps, context: IScopedContext) { + constructor(props: T, context: IScopedContext) { super(props); const scoped = context; @@ -2908,3 +3053,10 @@ export class CRUDRenderer extends CRUD { return store.getData(data); } } + +@Renderer({ + type: 'crud', + storeType: CRUDStore.name, + isolateScope: true +}) +export class CRUDRenderer extends CRUDRendererBase {} diff --git a/packages/amis/src/renderers/Cards.tsx b/packages/amis/src/renderers/Cards.tsx index 02c3988300e..0f23617ae59 100644 --- a/packages/amis/src/renderers/Cards.tsx +++ b/packages/amis/src/renderers/Cards.tsx @@ -165,16 +165,28 @@ export interface GridProps Omit { store: IListStore; selectable?: boolean; + // 已选清单 selected?: Array; checkAll?: boolean; multiple?: boolean; valueField?: string; draggable?: boolean; dragIcon?: SVGAElement; + // 行数据集合 + items?: Array; + + // 原始数据集合,前端分页时用来保存原始数据 + fullItems?: Array; onSelect: ( selectedItems: Array, unSelectedItems: Array ) => void; + // 单条修改时触发 + onItemChange?: ( + item: object, + diff: object, + rowIndex: string | number + ) => void; onSave?: ( items: Array | object, diff: Array | object, @@ -299,7 +311,7 @@ export default class Cards extends React.Component { } } - updateItems && store.initItems(items); + updateItems && store.initItems(items, props.fullItems, props.selected); Array.isArray(props.selected) && store.updateSelected(props.selected, props.valueField); return updateItems; @@ -370,15 +382,12 @@ export default class Cards extends React.Component { this.syncSelected(); const {store, dispatchEvent} = this.props; - const selectItems = store.selectedItems.map(row => row.data); - const unSelectItems = store.unSelectedItems.map(row => row.data); dispatchEvent( //增删改查卡片模式选择表格项 'selectedChange', createObject(store.data, { - selectedItems: selectItems, - unSelectedItems: unSelectItems, + ...store.eventContext, item: item.data }) ); @@ -445,10 +454,18 @@ export default class Cards extends React.Component { ) { item.change(values, savePristine); - if (!saveImmediately || savePristine) { + const {onSave, onItemChange, primaryField} = this.props; + + if (savePristine) { return; } + onItemChange?.( + item.data, + difference(item.data, item.pristine, ['id', primaryField]), + item.index + ); + if (saveImmediately && saveImmediately.api) { this.props.onAction( null, @@ -462,9 +479,7 @@ export default class Cards extends React.Component { return; } - const {onSave, primaryField} = this.props; - - if (!onSave) { + if (!saveImmediately || !onSave) { return; } @@ -733,9 +748,7 @@ export default class Cards extends React.Component { ? headerToolbarRender( { ...this.props, - selectedItems: store.selectedItems.map(item => item.data), - items: store.items.map(item => item.data), - unSelectedItems: store.unSelectedItems.map(item => item.data) + ...store.eventContext }, this.renderToolbar ) @@ -784,9 +797,7 @@ export default class Cards extends React.Component { ? footerToolbarRender( { ...this.props, - selectedItems: store.selectedItems.map(item => item.data), - items: store.items.map(item => item.data), - unSelectedItems: store.unSelectedItems.map(item => item.data) + ...store.eventContext }, this.renderToolbar ) @@ -902,7 +913,7 @@ export default class Cards extends React.Component { return this.renderCheckAll(); } - return void 0; + return; } // editor中重写,请勿更改前两个参数 @@ -1231,6 +1242,10 @@ export class CardsRenderer extends Cards { return store.getData(data); } + hasModifiedItems() { + return this.props.store.modified; + } + async doAction( action: ActionObject, ctx: any, @@ -1244,9 +1259,11 @@ export class CardsRenderer extends Cards { case 'selectAll': store.clear(); store.toggleAll(); + this.syncSelected(); break; case 'clearAll': store.clear(); + this.syncSelected(); break; case 'select': const rows = await getMatchedEventTargets( @@ -1260,6 +1277,7 @@ export class CardsRenderer extends Cards { rows.map(item => item.data), valueField ); + this.syncSelected(); break; case 'initDrag': store.startDragging(); diff --git a/packages/amis/src/renderers/Form/InputTable.tsx b/packages/amis/src/renderers/Form/InputTable.tsx index 07ef3f19761..32a29399fa8 100644 --- a/packages/amis/src/renderers/Form/InputTable.tsx +++ b/packages/amis/src/renderers/Form/InputTable.tsx @@ -1992,6 +1992,7 @@ export default class FormTable< const query = this.state.query; const filteredItems = this.state.filteredItems; + const items = this.state.items; let showPager = typeof perPage === 'number'; let page = this.state.page || 1; diff --git a/packages/amis/src/renderers/Form/Picker.tsx b/packages/amis/src/renderers/Form/Picker.tsx index 5bae47343d3..02ed3994a81 100644 --- a/packages/amis/src/renderers/Form/Picker.tsx +++ b/packages/amis/src/renderers/Form/Picker.tsx @@ -157,13 +157,13 @@ export default class PickerControl extends React.PureComponent< placement: 'top', trigger: 'hover', showArrow: false, - offset: [0, -10] + offset: [0, -5] }, overflowTagPopoverInCRUD: { placement: 'bottom', trigger: 'hover', showArrow: false, - offset: [0, 10] + offset: [0, 0] } } }; @@ -641,7 +641,7 @@ export default class PickerControl extends React.PureComponent< } return ( -
+ <> {tags.map((item, index) => { if (enableOverflow && index === maxTagCount) { return ( @@ -697,7 +697,7 @@ export default class PickerControl extends React.PureComponent< return this.renderTag(item, index); })} -
+ ); } @@ -804,24 +804,24 @@ export default class PickerControl extends React.PureComponent<
{__(placeholder)}
- ) : null} - -
- {this.renderValues()} - - -
+ ) : ( +
+ {this.renderValues()} + + +
+ )} {clearable && !disabled && selectedOptions.length ? ( diff --git a/packages/amis/src/renderers/List.tsx b/packages/amis/src/renderers/List.tsx index f0487541d7f..02d02c952f7 100644 --- a/packages/amis/src/renderers/List.tsx +++ b/packages/amis/src/renderers/List.tsx @@ -254,12 +254,23 @@ export interface ListProps SpinnerExtraProps { store: IListStore; selectable?: boolean; + + // 已选清单 selected?: Array; draggable?: boolean; + + // 行数据集合 + items?: Array; + + // 原始数据集合,前端分页时用来保存原始数据 + fullItems?: Array; + onSelect: ( selectedItems: Array, unSelectedItems: Array ) => void; + // 单条修改时触发 + onItemChange?: (item: object, diff: object, rowIndex: string) => void; onSave?: ( items: Array | object, diff: Array | object, @@ -381,7 +392,7 @@ export default class List extends React.Component { } } - updateItems && store.initItems(items); + updateItems && store.initItems(items, props.fullItems, props.selected); Array.isArray(props.selected) && store.updateSelected(props.selected, props.valueField); return updateItems; @@ -788,9 +799,7 @@ export default class List extends React.Component { ? headerToolbarRender( { ...this.props, - selectedItems: store.selectedItems.map(item => item.data), - items: store.items.map(item => item.data), - unSelectedItems: store.unSelectedItems.map(item => item.data) + ...store.eventContext }, this.renderToolbar ) @@ -843,9 +852,7 @@ export default class List extends React.Component { ? footerToolbarRender( { ...this.props, - selectedItems: store.selectedItems.map(item => item.data), - items: store.items.map(item => item.data), - unSelectedItems: store.unSelectedItems.map(item => item.data) + ...store.eventContext }, this.renderToolbar ) @@ -1188,6 +1195,10 @@ export class ListRenderer extends List { return store.getData(data); } + hasModifiedItems() { + return this.props.store.modified; + } + async doAction( action: ActionObject, ctx: any, @@ -1201,9 +1212,11 @@ export class ListRenderer extends List { case 'selectAll': store.clear(); store.toggleAll(); + this.syncSelected(); break; case 'clearAll': store.clear(); + this.syncSelected(); break; case 'select': const rows = await getMatchedEventTargets( @@ -1217,6 +1230,7 @@ export class ListRenderer extends List { rows.map(item => item.data), valueField ); + this.syncSelected(); break; case 'initDrag': store.startDragging(); diff --git a/packages/amis/src/renderers/Table/index.tsx b/packages/amis/src/renderers/Table/index.tsx index a54f27d44b4..7f38a317cd0 100644 --- a/packages/amis/src/renderers/Table/index.tsx +++ b/packages/amis/src/renderers/Table/index.tsx @@ -389,6 +389,8 @@ export interface TableProps extends RendererProps, SpinnerExtraProps { tableClassName?: string; source?: string; selectable?: boolean; + + // 已选清单 selected?: Array; maxKeepItemSelectionLength?: number; maxItemSelectionLength?: number; @@ -419,6 +421,15 @@ export interface TableProps extends RendererProps, SpinnerExtraProps { unSelectedItems: Array ) => void; onPristineChange?: (data: object, rowIndexe: string) => void; + // 行数据集合 + items?: Array; + + // 原始数据集合,前端分页时用来保存原始数据 + fullItems?: Array; + + // 单条修改时触发 + onItemChange?: (item: object, diff: object, rowIndex: string) => void; + onSave?: ( items: Array | object, diff: Array | object, @@ -739,14 +750,26 @@ export default class Table< } if (updateRows) { - store.initRows(rows, props.getEntryId, props.reUseRow); + store.initRows( + rows, + props.getEntryId, + props.reUseRow, + props.fullItems, + props.selected + ); } else if (props.reUseRow === false) { /** * 在reUseRow为false情况下,支持强制刷新表格行状态 * 适用的情况:用户每次刷新,调用接口,返回的数据都是一样的,导致updateRows为false,故针对每次返回数据一致的情况,需要强制表格更新 */ updateRows = true; - store.initRows(value, props.getEntryId, props.reUseRow); + store.initRows( + value, + props.getEntryId, + props.reUseRow, + props.fullItems, + props.selected + ); } Array.isArray(props.selected) && @@ -1062,8 +1085,7 @@ export default class Table< const rendererEvent = await dispatchEvent( 'selectedChange', createObject(data, { - selectedItems: store.selectedRows.map(row => row.data), - unSelectedItems: store.unSelectedRows.map(row => row.data), + ...store.eventContext, item: item.data }) ); @@ -1126,16 +1148,13 @@ export default class Table< async handleCheckAll() { const {store, data, dispatchEvent} = this.props; - const items = store.rows.map((row: any) => row.data); store.toggleAll(); const rendererEvent = await dispatchEvent( 'selectedChange', createObject(data, { - selectedItems: store.selectedRows.map(row => row.data), - unSelectedItems: store.unSelectedRows.map(row => row.data), - items + ...store.eventContext }) ); @@ -1164,7 +1183,8 @@ export default class Table< onSave, onPristineChange, saveImmediately: propsSaveImmediately, - primaryField + primaryField, + onItemChange } = this.props; item.change(values, savePristine); @@ -1188,11 +1208,17 @@ export default class Table< if (savePristine) { onPristineChange?.(item.data, item.path); return; - } else if (!saveImmediately && !propsSaveImmediately) { - return; } - if (saveImmediately && saveImmediately.api) { + onItemChange?.( + item.data, + difference(item.data, item.pristine, ['id', primaryField]), + item.path + ); + + if (!saveImmediately && !propsSaveImmediately) { + return; + } else if (saveImmediately && saveImmediately.api) { this.props.onAction( null, { @@ -2635,9 +2661,7 @@ export default class Table< ? headerToolbarRender( { ...this.props, - selectedItems: store.selectedRows.map(item => item.data), - items: store.rows.map(item => item.data), - unSelectedItems: store.unSelectedRows.map(item => item.data), + ...store.eventContext, ...otherProps }, this.renderToolbar @@ -2702,9 +2726,7 @@ export default class Table< ? footerToolbarRender( { ...this.props, - selectedItems: store.selectedRows.map(item => item.data), - unSelectedItems: store.unSelectedRows.map(item => item.data), - items: store.rows.map(item => item.data) + ...store.eventContext }, this.renderToolbar ) @@ -3020,6 +3042,10 @@ export class TableRenderer extends Table { return store.getData(data); } + hasModifiedItems() { + return this.props.store.modified; + } + async doAction( action: ActionObject, ctx: any, @@ -3033,9 +3059,11 @@ export class TableRenderer extends Table { case 'selectAll': store.clear(); store.toggleAll(); + this.syncSelected(); break; case 'clearAll': store.clear(); + this.syncSelected(); break; case 'select': const rows = await this.getEventTargets( @@ -3048,6 +3076,7 @@ export class TableRenderer extends Table { rows.map(item => item.data), valueField ); + this.syncSelected(); break; case 'initDrag': store.startDragging();