From 3efadbbfb68e87208072715360fed06967ac417c Mon Sep 17 00:00:00 2001 From: lurunze1226 Date: Mon, 21 Aug 2023 17:28:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Picker=E7=BB=84=E4=BB=B6=E5=B7=B2?= =?UTF-8?q?=E9=80=89=E9=A1=B9=E6=94=AF=E6=8C=81=E9=99=90=E5=88=B6=E6=9C=80?= =?UTF-8?q?=E5=A4=A7=E5=B1=95=E7=A4=BA=E6=95=B0=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh-CN/components/form/picker.md | 523 +++++++++++++++++- packages/amis-core/src/utils/math.ts | 37 ++ packages/amis-ui/scss/_components.scss | 2 + packages/amis-ui/scss/_mixins.scss | 60 ++ packages/amis-ui/scss/components/_crud.scss | 76 +-- .../amis-ui/scss/components/form/_picker.scss | 68 +-- .../amis/__tests__/renderers/Picker.test.tsx | 80 ++- .../__snapshots__/CRUD.test.tsx.snap | 8 - .../__snapshots__/Picker.test.tsx.snap | 6 +- packages/amis/src/renderers/CRUD.tsx | 137 ++++- packages/amis/src/renderers/Form/Picker.tsx | 221 ++++++-- 11 files changed, 1021 insertions(+), 197 deletions(-) diff --git a/docs/zh-CN/components/form/picker.md b/docs/zh-CN/components/form/picker.md index 2cc0c3937..bd6065f2f 100755 --- a/docs/zh-CN/components/form/picker.md +++ b/docs/zh-CN/components/form/picker.md @@ -684,12 +684,520 @@ order: 35 } ``` +## 限制标签最大展示数量 + +设置`overflowConfig`后可以限制标签的最大展示数量,该属性仅在多选模式开启后生效,包含以下几个配置项: +- `maxTagCount`:最大展示数量,是范围为0 - 选项总数量的整数,超出数量的部分会收纳到 Popover 中。 +- `displayPosition`:收纳标签生效的位置,类型为字符串数组,未开启内嵌模式默认为**选择器**, 开启后默认为**选择器**和**CRUD 顶部**,可选值为`'select'`(选择器)、`'crud'`(增删改查)。 +- `overflowTagPopover`配置收纳标签 Popover 相关[属性](../tooltip#属性表)。 +- `overflowTagPopoverInCRUD`可以配置**CRUD 顶部**收纳标签的 Popover相关[属性](../tooltip#属性表)。 + +> `3.4.0` 及以上版本 + +```schema: scope="body" +{ + "type": "form", + "api": "/api/mock2/form/saveForm", + "debug": true, + "body": [ + { + "type": "picker", + "overflowConfig": { + "maxTagCount": 3, + }, + "name": "maxTagCount1", + "joinValues": true, + "valueField": "id", + "labelField": "engine", + "label": "多选", + "source": "/api/mock2/sample", + "size": "lg", + "value": "1,2,3,4,5,6,7", + "multiple": true, + "pickerSchema": { + "mode": "table", + "name": "thelist", + "quickSaveApi": "/api/mock2/sample/bulkUpdate", + "quickSaveItemApi": "/api/mock2/sample/$id", + "draggable": true, + "headerToolbar": { + "wrapWithPanel": false, + "type": "form", + "className": "text-right", + "target": "thelist", + "mode": "inline", + "body": [ + { + "type": "input-text", + "name": "keywords", + "addOn": { + "type": "submit", + "label": "搜索", + "level": "primary", + "icon": "fa fa-search pull-left" + } + } + ] + }, + "footerToolbar": [ + "statistics", + { + "type": "pagination", + "showPageInput": true, + "layout": "perPage,pager,go" + } + ], + "columns": [ + { + "name": "engine", + "label": "Rendering engine", + "sortable": true, + "searchable": true, + "type": "text", + "toggled": true + }, + { + "name": "browser", + "label": "Browser", + "sortable": true, + "type": "text", + "toggled": true + }, + { + "name": "platform", + "label": "Platform(s)", + "sortable": true, + "type": "text", + "toggled": true + }, + { + "name": "version", + "label": "Engine version", + "quickEdit": true, + "type": "text", + "toggled": true + }, + { + "name": "grade", + "label": "CSS grade", + "quickEdit": { + "mode": "inline", + "type": "select", + "options": [ + "A", + "B", + "C", + "D", + "X" + ], + "saveImmediately": true + }, + "type": "text", + "toggled": true + }, + { + "type": "operation", + "label": "操作", + "width": 100, + "buttons": [ + { + "type": "button", + "icon": "fa fa-eye", + "actionType": "dialog", + "dialog": { + "title": "查看", + "body": { + "type": "form", + "body": [ + { + "type": "static", + "name": "engine", + "label": "Engine" + }, + { + "type": "divider" + }, + { + "type": "static", + "name": "browser", + "label": "Browser" + }, + { + "type": "divider" + }, + { + "type": "static", + "name": "platform", + "label": "Platform(s)" + }, + { + "type": "divider" + }, + { + "type": "static", + "name": "version", + "label": "Engine version" + }, + { + "type": "divider" + }, + { + "type": "static", + "name": "grade", + "label": "CSS grade" + }, + { + "type": "divider" + }, + { + "type": "html", + "html": "

添加其他 Html 片段 需要支持变量替换(todo).

" + } + ] + } + } + }, + { + "type": "button", + "icon": "fa fa-pencil", + "actionType": "dialog", + "dialog": { + "position": "left", + "size": "lg", + "title": "编辑", + "body": { + "type": "form", + "name": "sample-edit-form", + "api": "/api/mock2/sample/$id", + "body": [ + { + "type": "input-text", + "name": "engine", + "label": "Engine", + "required": true + }, + { + "type": "divider" + }, + { + "type": "input-text", + "name": "browser", + "label": "Browser", + "required": true + }, + { + "type": "divider" + }, + { + "type": "input-text", + "name": "platform", + "label": "Platform(s)", + "required": true + }, + { + "type": "divider" + }, + { + "type": "input-text", + "name": "version", + "label": "Engine version" + }, + { + "type": "divider" + }, + { + "type": "select", + "name": "grade", + "label": "CSS grade", + "options": [ + "A", + "B", + "C", + "D", + "X" + ] + } + ] + } + } + }, + { + "type": "button", + "icon": "fa fa-times text-danger", + "actionType": "ajax", + "confirmText": "您确认要删除?", + "api": "delete:/api/mock2/sample/$id" + } + ], + "toggled": true + } + ] + } + } + + ] +} +``` + +内嵌模式下 + +```schema: scope="body" +{ + "type": "form", + "api": "/api/mock2/form/saveForm", + "debug": true, + "body": [ + { + "type": "picker", + "overflowConfig": { + "maxTagCount": 3, + "overflowTagPopoverInCRUD": { + "placement": "top" + } + }, + "embed": true, + "name": "maxTagCount2", + "joinValues": true, + "valueField": "id", + "labelField": "engine", + "label": "多选", + "source": "/api/mock2/sample", + "size": "lg", + "value": "1,2,3,4,5,6,7", + "multiple": true, + "pickerSchema": { + "mode": "table", + "name": "thelist", + "quickSaveApi": "/api/mock2/sample/bulkUpdate", + "quickSaveItemApi": "/api/mock2/sample/$id", + "draggable": true, + "headerToolbar": { + "wrapWithPanel": false, + "type": "form", + "className": "text-right", + "target": "thelist", + "mode": "inline", + "body": [ + { + "type": "input-text", + "name": "keywords", + "addOn": { + "type": "submit", + "label": "搜索", + "level": "primary", + "icon": "fa fa-search pull-left" + } + } + ] + }, + "footerToolbar": [ + "statistics", + { + "type": "pagination", + "showPageInput": true, + "layout": "perPage,pager,go" + } + ], + "columns": [ + { + "name": "engine", + "label": "Rendering engine", + "sortable": true, + "searchable": true, + "type": "text", + "toggled": true + }, + { + "name": "browser", + "label": "Browser", + "sortable": true, + "type": "text", + "toggled": true + }, + { + "name": "platform", + "label": "Platform(s)", + "sortable": true, + "type": "text", + "toggled": true + }, + { + "name": "version", + "label": "Engine version", + "quickEdit": true, + "type": "text", + "toggled": true + }, + { + "name": "grade", + "label": "CSS grade", + "quickEdit": { + "mode": "inline", + "type": "select", + "options": [ + "A", + "B", + "C", + "D", + "X" + ], + "saveImmediately": true + }, + "type": "text", + "toggled": true + }, + { + "type": "operation", + "label": "操作", + "width": 100, + "buttons": [ + { + "type": "button", + "icon": "fa fa-eye", + "actionType": "dialog", + "dialog": { + "title": "查看", + "body": { + "type": "form", + "body": [ + { + "type": "static", + "name": "engine", + "label": "Engine" + }, + { + "type": "divider" + }, + { + "type": "static", + "name": "browser", + "label": "Browser" + }, + { + "type": "divider" + }, + { + "type": "static", + "name": "platform", + "label": "Platform(s)" + }, + { + "type": "divider" + }, + { + "type": "static", + "name": "version", + "label": "Engine version" + }, + { + "type": "divider" + }, + { + "type": "static", + "name": "grade", + "label": "CSS grade" + }, + { + "type": "divider" + }, + { + "type": "html", + "html": "

添加其他 Html 片段 需要支持变量替换(todo).

" + } + ] + } + } + }, + { + "type": "button", + "icon": "fa fa-pencil", + "actionType": "dialog", + "dialog": { + "position": "left", + "size": "lg", + "title": "编辑", + "body": { + "type": "form", + "name": "sample-edit-form", + "api": "/api/mock2/sample/$id", + "body": [ + { + "type": "input-text", + "name": "engine", + "label": "Engine", + "required": true + }, + { + "type": "divider" + }, + { + "type": "input-text", + "name": "browser", + "label": "Browser", + "required": true + }, + { + "type": "divider" + }, + { + "type": "input-text", + "name": "platform", + "label": "Platform(s)", + "required": true + }, + { + "type": "divider" + }, + { + "type": "input-text", + "name": "version", + "label": "Engine version" + }, + { + "type": "divider" + }, + { + "type": "select", + "name": "grade", + "label": "CSS grade", + "options": [ + "A", + "B", + "C", + "D", + "X" + ] + } + ] + } + } + }, + { + "type": "button", + "icon": "fa fa-times text-danger", + "actionType": "ajax", + "confirmText": "您确认要删除?", + "api": "delete:/api/mock2/sample/$id" + } + ], + "toggled": true + } + ] + } + } + + ] +} +``` + + + ## 属性表 当做选择器表单项使用时,除了支持 [普通表单项属性表](./formitem#%E5%B1%9E%E6%80%A7%E8%A1%A8) 中的配置以外,还支持下面一些配置 -| 属性名 | 类型 | 默认值 | 说明 | -| ------------ | -------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------- | +| 属性名 | 类型 | 默认值 | 说明 | 版本 | +| ------------ | -------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------- | --- | | options | `Array`或`Array` | | [选项组](./options#%E9%9D%99%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-options) | | source | `string`或 [API](../../../docs/types/api) 或 [数据映射](../../../docs/concepts/data-mapping) | | [动态选项组](./options#%E5%8A%A8%E6%80%81%E9%80%89%E9%A1%B9%E7%BB%84-source) | | multiple | `boolean` | | 是否为多选。 | @@ -703,6 +1211,17 @@ order: 35 | modalMode | `string` | `"dialog"` | 设置 `dialog` 或者 `drawer`,用来配置弹出方式。 | | pickerSchema | `string` | `{mode: 'list', listItem: {title: '${label}'}}` | 即用 List 类型的渲染,来展示列表信息。更多配置参考 [CRUD](../crud) | | embed | `boolean` | `false` | 是否使用内嵌模式 | +| overflowConfig | `OverflowConfig` | 参考[OverflowConfig](./#overflowconfig) | 开启最大标签展示数量的相关配置 | `3.4.0` | + +### OverflowConfig + +| 属性名 | 类型 | 默认值 | 说明 | +| ------------ | -------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------- | +| maxTagCount | `number` | `-1` | 标签的最大展示数量,超出数量后以收纳浮层的方式展示,仅在多选模式开启后生效,默认为`-1` 不开启 | `3.4.0` | +| displayPosition | `('select' \| 'crud')[]` | `['select', 'crud']` | 收纳标签生效的位置,未开启内嵌模式默认为选择器, 开启后默认为选择器和CRUD 顶部,可选值为`'select'`(选择器)、`'crud'`(增删改查) | `3.4.0` | +| overflowTagPopover | `TooltipObject` | `{"placement": "top", "trigger": "hover", "showArrow": false, "offset": [0, -10]}` | 选择器内收纳标签的Popover配置,详细配置参考[Tooltip](../tooltip#属性表) | `3.4.0` | +| overflowTagPopoverInCRUD | `TooltipObject` | `{"placement": "bottom", "trigger": "hover", "showArrow": false, "offset": [0, 10]}` | CRUD顶部内收纳标签的Popover配置,详细配置参考[Tooltip](../tooltip#属性表) | `3.4.0` | + ## 事件表 diff --git a/packages/amis-core/src/utils/math.ts b/packages/amis-core/src/utils/math.ts index d77c29027..5a8baa540 100644 --- a/packages/amis-core/src/utils/math.ts +++ b/packages/amis-core/src/utils/math.ts @@ -40,3 +40,40 @@ export function numberFormatter(num: number | string, precision: number = 0) { } return ZERO.toFixed(precision); } + +/** + * 判断一个数字是否为整数,且在给定范围内 + * + * @param num 要判断的数字 + * @param options 范围选项,包括 start、end、left、right + * @param options.start 范围起始值 + * @param options.end 范围结束值 + * @param options.left 范围的左边界类型,默认为 'inclusive',可选值为 'inclusive'(闭区间) 或 'exclusive'(开区间) + * @param options.right 范围的右边界类型,默认为 'inclusive',可选值为 'inclusive'(闭区间) 或 'exclusive'(开区间) + * @returns 如果数字在给定范围内则返回 true,否则返回 false + */ +export function isIntegerInRange( + num: number, + options: { + start: number; + end: number; + left: 'inclusive' | 'exclusive'; + right: 'inclusive' | 'exclusive'; + } +) { + const {start, end, left = 'inclusive', right = 'inclusive'} = options || {}; + + if (num == null || typeof num !== 'number' || !Number.isSafeInteger(num)) { + return false; + } + + if (left === 'exclusive' && right === 'exclusive') { + return num > start && num < end; + } else if (left === 'inclusive' && right === 'exclusive') { + return num >= start && num < end; + } else if (left === 'exclusive' && right === 'inclusive') { + return num > start && num <= end; + } else { + return num >= start && num <= end; + } +} diff --git a/packages/amis-ui/scss/_components.scss b/packages/amis-ui/scss/_components.scss index b637d5375..1fb20fe13 100644 --- a/packages/amis-ui/scss/_components.scss +++ b/packages/amis-ui/scss/_components.scss @@ -3620,6 +3620,8 @@ --Pick-base-value-icon-color: var(--colors-other-5); --Picker-iconColor: var(--Pick-base-icon-color); --Picker-onHover-iconColor: var(--icon-onHover-color); + --Picker-tag-height: #{px2rem(24px)}; + --Picker-tag-marginBottom: var(--select-multiple-marginBottom); --Pick-status-hover-top-border-color: var(--colors-other-5); --Pick-status-hover-top-border-width: var(--borders-width-2); --Pick-status-hover-top-border-style: var(--borders-style-2); diff --git a/packages/amis-ui/scss/_mixins.scss b/packages/amis-ui/scss/_mixins.scss index ab7bdd5b8..226afb437 100644 --- a/packages/amis-ui/scss/_mixins.scss +++ b/packages/amis-ui/scss/_mixins.scss @@ -616,3 +616,63 @@ text-overflow: ellipsis; white-space: nowrap; } + +@mixin tag-item($component-prefix) { + .#{$ns}#{$component-prefix}-value { + cursor: pointer; + user-select: none; + white-space: nowrap; + vertical-align: middle; + line-height: calc( + var(--Form-input-lineHeight) * var(--Form-input-fontSize) - #{px2rem(2px)} + ); + display: inline-block; + font-size: var(--Pick-base-value-fontSize); + color: var(--Pick-base-value-color); + font-weight: var(--Pick-base-value-fontWeight); + background: var(--Pick-base-value-bgColor); + border-width: var(--Pick-base-value-top-border-width) + var(--Pick-base-value-right-border-width) + var(--Pick-base-value-bottom-border-width) + var(--Pick-base-value-left-border-width); + border-style: var(--Pick-base-value-top-border-style) + var(--Pick-base-value-right-border-style) + var(--Pick-base-value-bottom-border-style) + var(--Pick-base-value-left-border-style); + border-color: var(--Pick-base-value-top-border-color) + var(--Pick-base-value-right-border-color) + var(--Pick-base-value-bottom-border-color) + var(--Pick-base-value-left-border-color); + border-radius: var(--Pick-base-top-left-border-radius) + 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); + + &:hover { + background: var(--Form-selectValue-onHover-bg); + } + + &.is-disabled { + pointer-events: none; + opacity: var(--Button-onDisabled-opacity); + } + } + + .#{$ns}#{$component-prefix}-valueIcon { + color: var(--Pick-base-value-icon-color); + cursor: pointer; + border-right: px2rem(1px) solid var(--Form-selectValue-borderColor); + padding: 1px 5px; + + &:hover { + background: var(--Pick-base-value-hover-icon-color); + } + } + + .#{$ns}#{$component-prefix}-valueLabel { + padding: 0 var(--gap-xs); + } +} diff --git a/packages/amis-ui/scss/components/_crud.scss b/packages/amis-ui/scss/components/_crud.scss index 078a63bc7..ee9ee0999 100644 --- a/packages/amis-ui/scss/components/_crud.scss +++ b/packages/amis-ui/scss/components/_crud.scss @@ -9,6 +9,25 @@ &-selection { margin-bottom: var(--gap-base); + + &-overflow { + &-wrapper { + display: flex; + flex-flow: column nowrap; + justify-content: flex-start; + align-items: flex-start; + overflow-x: hidden; + overflow-y: auto; + height: calc( + (var(--Picker-tag-height) + var(--Picker-tag-marginBottom) * 4) * 3 + ); + max-height: calc( + (var(--Picker-tag-height) + var(--Picker-tag-marginBottom)) * 5 + ); + + @include tag-item(Crud); + } + } } &-selectionLabel { @@ -17,61 +36,8 @@ margin-top: var(--gap-xs); } - &-value { - cursor: pointer; - vertical-align: middle; - user-select: none; - line-height: calc( - var(--Form-input-lineHeight) * var(--Form-input-fontSize) - #{px2rem(2px)} - ); - display: inline-block; - font-size: var(--Pick-base-value-fontSize); - color: var(--Pick-base-value-color); - font-weight: var(--Pick-base-value-fontWeight); - background: var(--Pick-base-value-bgColor); - border-width: var(--Pick-base-value-top-border-width) - var(--Pick-base-value-right-border-width) - var(--Pick-base-value-bottom-border-width) - var(--Pick-base-value-left-border-width); - border-style: var(--Pick-base-value-top-border-style) - var(--Pick-base-value-right-border-style) - var(--Pick-base-value-bottom-border-style) - var(--Pick-base-value-left-border-style); - border-color: var(--Pick-base-value-top-border-color) - var(--Pick-base-value-right-border-color) - var(--Pick-base-value-bottom-border-color) - var(--Pick-base-value-left-border-color); - border-radius: var(--Pick-base-top-left-border-radius) - 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-top: var(--gap-xs); - - &:hover { - background: var(--Form-selectValue-onHover-bg); - } - - &.is-disabled { - pointer-events: none; - opacity: var(--Button-onDisabled-opacity); - } - } - - &-valueIcon { - color: var(--Pick-base-value-icon-color); - cursor: pointer; - border-right: px2rem(1px) solid var(--Form-selectValue-borderColor); - padding: 1px 5px; - - &:hover { - background: var(--Form-selectValue-onHover-bg); - } - } - - &-valueLabel { - padding: 0 var(--gap-xs); - } + /* tag 样式 */ + @include tag-item(Crud); &-selectionClear { display: inline-block; diff --git a/packages/amis-ui/scss/components/form/_picker.scss b/packages/amis-ui/scss/components/form/_picker.scss index 0ddfd4547..451bfa5dc 100644 --- a/packages/amis-ui/scss/components/form/_picker.scss +++ b/packages/amis-ui/scss/components/form/_picker.scss @@ -115,53 +115,8 @@ line-height: 1; } - .#{$ns}Picker-value { - cursor: pointer; - user-select: none; - white-space: nowrap; - vertical-align: middle; - line-height: calc( - var(--Form-input-lineHeight) * var(--Form-input-fontSize) - #{px2rem(2px)} - ); - display: inline-block; - font-size: var(--Pick-base-value-fontSize); - color: var(--Pick-base-value-color); - font-weight: var(--Pick-base-value-fontWeight); - background: var(--Pick-base-value-bgColor); - border-width: var(--Pick-base-value-top-border-width) - var(--Pick-base-value-right-border-width) - var(--Pick-base-value-bottom-border-width) - var(--Pick-base-value-left-border-width); - border-style: var(--Pick-base-value-top-border-style) - var(--Pick-base-value-right-border-style) - var(--Pick-base-value-bottom-border-style) - var(--Pick-base-value-left-border-style); - border-color: var(--Pick-base-value-top-border-color) - var(--Pick-base-value-right-border-color) - var(--Pick-base-value-bottom-border-color) - var(--Pick-base-value-left-border-color); - border-radius: var(--Pick-base-top-left-border-radius) - 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); - } - - .#{$ns}Picker-valueIcon { - color: var(--Pick-base-value-icon-color); - cursor: pointer; - border-right: px2rem(1px) solid var(--Form-selectValue-borderColor); - padding: 1px 5px; - - &:hover { - background: var(--Pick-base-value-hover-icon-color); - } - } - - .#{$ns}Picker-valueLabel { - padding: 0 var(--gap-xs); - } + /* tag 样式 */ + @include tag-item(Picker); &-btn { cursor: pointer; @@ -198,6 +153,25 @@ top: 0; } } + + &-overflow { + &-wrapper { + display: flex; + flex-flow: column nowrap; + justify-content: flex-start; + align-items: flex-start; + overflow-x: hidden; + overflow-y: auto; + height: calc( + (var(--Picker-tag-height) + var(--Picker-tag-marginBottom) * 4) * 3 + ); + max-height: calc( + (var(--Picker-tag-height) + var(--Picker-tag-marginBottom)) * 5 + ); + + @include tag-item(Picker); + } + } } .#{$ns}PickerControl.is-inline { diff --git a/packages/amis/__tests__/renderers/Picker.test.tsx b/packages/amis/__tests__/renderers/Picker.test.tsx index c18491dff..a91cd596c 100644 --- a/packages/amis/__tests__/renderers/Picker.test.tsx +++ b/packages/amis/__tests__/renderers/Picker.test.tsx @@ -12,8 +12,7 @@ import { render, fireEvent, cleanup, - waitFor, - getByText + screen } from '@testing-library/react'; import '../../src'; import {render as amisRender} from '../../src'; @@ -25,7 +24,7 @@ afterEach(() => { clearStoresCache(); }); -test('Renderer:Picker base', async () => { +test('1. Renderer:Picker base', async () => { const {container, rerender, getByText, getByPlaceholderText, baseElement} = render( amisRender({ @@ -73,7 +72,7 @@ test('Renderer:Picker base', async () => { expect(container).toMatchSnapshot(); }); -test('Renderer:Picker with pickerSchema & valueField & labelField & multiple & value & size', async () => { +test('2. Renderer:Picker with pickerSchema & valueField & labelField & multiple & value & size', async () => { const fetcher = jest.fn().mockImplementation(() => Promise.resolve({ data: { @@ -168,7 +167,7 @@ test('Renderer:Picker with pickerSchema & valueField & labelField & multiple & v }); }); -test('Renderer:Picker with embed', async () => { +test('3. Renderer:Picker with embed', async () => { const fetcher = jest.fn().mockImplementation(() => Promise.resolve({ data: { @@ -241,7 +240,7 @@ test('Renderer:Picker with embed', async () => { ).toBeInTheDocument(); }); -test('Renderer:Picker with drawer modalMode', async () => { +test('4. Renderer:Picker with drawer modalMode', async () => { const {container, rerender, getByText, getByPlaceholderText, baseElement} = render( amisRender({ @@ -274,3 +273,72 @@ test('Renderer:Picker with drawer modalMode', async () => { baseElement.querySelector('.cxd-Drawer .cxd-Crud')! ).toBeInTheDocument(); }); + +describe('5. Renderer:Picker with overflowConfig', () => { + test('5-1. Renderer:Picker select', async () => { + const {container, rerender, getByText, getByPlaceholderText, baseElement} = + render( + amisRender({ + type: 'picker', + name: 'picker', + label: 'picker', + modalMode: 'dialog', + placeholder: 'picker-placeholder', + multiple: true, + overflowConfig: { + maxTagCount: 2 + }, + value: 'a,b,c', + options: [ + {label: 'A', value: 'a'}, + {label: 'B', value: 'b'}, + {label: 'C', value: 'c'}, + {label: 'D', value: 'd'} + ] + }) + ); + + await wait(500); + + const tags = container.querySelector('.cxd-Picker-values'); + + expect(tags).toBeInTheDocument(); + /** tag 元素数量正确 */ + expect(tags?.childElementCount).toEqual(3); + /** 收纳标签文案正确 */ + expect(tags?.lastElementChild).toHaveTextContent('+ 1 ...'); + }); + + test('5-2. Renderer:Picker embeded', async () => { + const {container, rerender, getByText, getByPlaceholderText, baseElement} = + render( + amisRender({ + type: 'picker', + name: 'picker', + label: 'picker', + modalMode: 'dialog', + placeholder: 'picker-placeholder', + embed: true, + multiple: true, + overflowConfig: { + maxTagCount: 2 + }, + value: 'a,b,c', + options: [ + {label: 'A', value: 'a'}, + {label: 'B', value: 'b'}, + {label: 'C', value: 'c'}, + {label: 'D', value: 'd'} + ] + }) + ); + + await wait(500); + + const tags = container.querySelectorAll('.cxd-Crud-selection .cxd-Crud-value'); + /** tag 元素数量正确 */ + expect(tags?.length).toEqual(3); + /** 收纳标签文案正确 */ + expect(tags[tags?.length - 1]).toHaveTextContent('+ 1 ...'); + }); +}); diff --git a/packages/amis/__tests__/renderers/__snapshots__/CRUD.test.tsx.snap b/packages/amis/__tests__/renderers/__snapshots__/CRUD.test.tsx.snap index 5bcbec564..bef310410 100644 --- a/packages/amis/__tests__/renderers/__snapshots__/CRUD.test.tsx.snap +++ b/packages/amis/__tests__/renderers/__snapshots__/CRUD.test.tsx.snap @@ -2721,8 +2721,6 @@ exports[`13. enderer: crud keepItemSelectionOnPageChange & maxKeepItemSelectionL > × @@ -2741,8 +2739,6 @@ exports[`13. enderer: crud keepItemSelectionOnPageChange & maxKeepItemSelectionL > × @@ -2761,8 +2757,6 @@ exports[`13. enderer: crud keepItemSelectionOnPageChange & maxKeepItemSelectionL > × @@ -2781,8 +2775,6 @@ exports[`13. enderer: crud keepItemSelectionOnPageChange & maxKeepItemSelectionL > × diff --git a/packages/amis/__tests__/renderers/__snapshots__/Picker.test.tsx.snap b/packages/amis/__tests__/renderers/__snapshots__/Picker.test.tsx.snap index 05574303b..77a7edc1e 100644 --- a/packages/amis/__tests__/renderers/__snapshots__/Picker.test.tsx.snap +++ b/packages/amis/__tests__/renderers/__snapshots__/Picker.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Renderer:Picker base 1`] = ` +exports[`1. Renderer:Picker base 1`] = ` `; -exports[`Renderer:Picker base 2`] = ` +exports[`1. Renderer:Picker base 2`] = `
× diff --git a/packages/amis/src/renderers/CRUD.tsx b/packages/amis/src/renderers/CRUD.tsx index 794745793..4841ab6ea 100644 --- a/packages/amis/src/renderers/CRUD.tsx +++ b/packages/amis/src/renderers/CRUD.tsx @@ -15,10 +15,11 @@ import { getVariable, qsstringify, qsparse, - isArrayChildrenModified + isArrayChildrenModified, + isIntegerInRange } from 'amis-core'; import {ScopedContext, IScopedContext} from 'amis-core'; -import {Button, SpinnerExtraProps} from 'amis-ui'; +import {Button, SpinnerExtraProps, TooltipWrapper} from 'amis-ui'; import {Select} from 'amis-ui'; import {getExprProperties} from 'amis-core'; import pick from 'lodash/pick'; @@ -441,7 +442,9 @@ export default class CRUD extends React.Component { 'onSave', 'onQuery', 'formStore', - 'autoFillHeight' + 'autoFillHeight', + 'maxTagCount', + 'overflowTagPopover' ]; static defaultProps = { toolbarInline: true, @@ -2221,9 +2224,8 @@ export default class CRUD extends React.Component { return this.renderToolbar(footerToolbar, 0, childProps, toolbarRenderer); } - renderSelection(): React.ReactNode { + renderTag(item: any, index: number) { const { - store, classnames: cx, labelField, labelTpl, @@ -2233,38 +2235,117 @@ export default class CRUD extends React.Component { env } = this.props; + return ( +
+ + × + + + {labelTpl ? ( + + ) : ( + getVariable(item, labelField || 'label') || + getVariable(item, valueField || primaryField || 'id') + )} + +
+ ); + } + + renderSelection(): React.ReactNode { + const { + store, + classPrefix: ns, + classnames: cx, + labelField, + labelTpl, + primaryField, + valueField, + translate: __, + env, + popOverContainer, + multiple, + maxTagCount, + overflowTagPopover + } = this.props; + if (!store.selectedItems.length) { return null; } + const totalCount = store.selectedItems.length; + let tags: any[] = store.selectedItems; + const enableOverflow = + multiple !== false && + isIntegerInRange(maxTagCount, { + start: 0, + end: totalCount, + left: 'inclusive', + right: 'exclusive' + }); + + if (enableOverflow) { + tags = [ + ...store.selectedItems.slice(0, maxTagCount), + {label: `+ ${totalCount - maxTagCount} ...`, value: '__overflow_tag__'} + ]; + } + return (
{__('CRUD.selected', {total: store.selectedItems.length})}
- {store.selectedItems.map((item, index) => ( -
- - × - - - {labelTpl ? ( - - ) : ( - getVariable(item, labelField || 'label') || - getVariable(item, valueField || primaryField || 'id') - )} - -
- ))} + {tags.map((item, index) => { + if (enableOverflow && index === maxTagCount) { + return ( + { + return ( +
+ {store.selectedItems + .slice(maxTagCount, totalCount) + .map((overflowItem, rawIndex) => { + const key = rawIndex + maxTagCount; + + return this.renderTag(overflowItem, key); + })} +
+ ); + } + }} + > +
+ {item.label} +
+
+ ); + } + + return this.renderTag(item, index); + })} {__('clear')} diff --git a/packages/amis/src/renderers/Form/Picker.tsx b/packages/amis/src/renderers/Form/Picker.tsx index f14331357..009b99940 100644 --- a/packages/amis/src/renderers/Form/Picker.tsx +++ b/packages/amis/src/renderers/Form/Picker.tsx @@ -4,6 +4,7 @@ import omit from 'lodash/omit'; import find from 'lodash/find'; import isEqual from 'lodash/isEqual'; import findIndex from 'lodash/findIndex'; +import merge from 'lodash/merge'; import { OptionsControl, OptionsControlProps, @@ -20,13 +21,14 @@ import { resolveVariableAndFilter, isApiOutdated, isEffectiveApi, - resolveEventData + resolveEventData, + isIntegerInRange } from 'amis-core'; -import {Html, Icon} from 'amis-ui'; -import {isMobile} from 'amis-core'; +import {Html, Icon, TooltipWrapper} from 'amis-ui'; import {FormOptionsSchema, SchemaTpl} from '../../Schema'; import intersectionWith from 'lodash/intersectionWith'; -import {PopUp} from 'amis-ui'; +import type {TooltipWrapperSchema} from '../TooltipWrapper'; +import type {Option} from 'amis-core'; /** * Picker @@ -70,6 +72,31 @@ export interface PickerControlSchema extends FormOptionsSchema { * 内嵌模式,也就是说不弹框了。 */ embed?: boolean; + + /** + * 开启最大标签展示数量的相关配置 + */ + overflowConfig: { + /** + * 标签的最大展示数量,超出数量后以收纳浮层的方式展示,仅在多选模式开启后生效 + */ + maxTagCount?: number; + + /** + * 开启最大标签展示数量后,收纳标签生效的位置,未开启内嵌模式默认为选择器, 开启后默认为选择器 + 模态框,可选值为'select'(选择器)、'crud'(增删改查) + */ + displayPosition?: ('select' | 'crud')[]; + + /** + * 开启最大标签展示数量后,选择器内收纳标签的Popover配置 + */ + overflowTagPopover?: TooltipWrapperSchema; + + /** + * 开启最大标签展示数量后,CRUD顶部内收纳标签的Popover配置 + */ + overflowTagPopoverInCRUD?: TooltipWrapperSchema; + }; } export interface PickerProps extends OptionsControlProps { @@ -115,7 +142,24 @@ export default class PickerControl extends React.PureComponent< title: '${label|raw}' } }, - embed: false + embed: false, + overflowConfig: { + /** 默认值为-1,不开启 */ + maxTagCount: -1, + displayPosition: ['select', 'crud'], + overflowTagPopover: { + placement: 'top', + trigger: 'hover', + showArrow: false, + offset: [0, -10] + }, + overflowTagPopoverInCRUD: { + placement: 'bottom', + trigger: 'hover', + showArrow: false, + offset: [0, 10] + } + } }; state: PickerState = { @@ -398,10 +442,15 @@ export default class PickerControl extends React.PureComponent< onChange(resetValue !== void 0 ? resetValue : ''); } - renderValues() { + getOverflowConfig() { + const {overflowConfig} = this.props; + + return merge(PickerControl.defaultProps.overflowConfig, overflowConfig); + } + + renderTag(item: Option, index: number) { const { classPrefix: ns, - selectedOptions, labelField, labelTpl, translate: __, @@ -409,47 +458,119 @@ export default class PickerControl extends React.PureComponent< env } = this.props; + return ( +
+ { + e.stopPropagation(); + this.removeItem(index); + }} + > + × + + { + e.stopPropagation(); + this.handleItemClick(item); + }} + > + {labelTpl ? ( + + ) : ( + `${ + getVariable(item, labelField || 'label') || + getVariable(item, 'id') + }` + )} + +
+ ); + } + + renderValues() { + const { + classPrefix: ns, + selectedOptions, + translate: __, + disabled, + multiple, + popOverContainer + } = this.props; + const {maxTagCount, overflowTagPopover} = this.getOverflowConfig(); + const totalCount = selectedOptions.length; + let tags = selectedOptions; + const enableOverflow = + multiple !== false && + isIntegerInRange(maxTagCount, { + start: 0, + end: totalCount, + left: 'inclusive', + right: 'exclusive' + }); + + /** 多选且开启限制标签数量 */ + if (enableOverflow) { + tags = [ + ...selectedOptions.slice(0, maxTagCount), + {label: `+ ${totalCount - maxTagCount} ...`, value: '__overflow_tag__'} + ]; + } + return (
- {selectedOptions.map((item, index) => ( -
- { - e.stopPropagation(); - this.removeItem(index); - }} - > - × - - { - e.stopPropagation(); - this.handleItemClick(item); - }} - > - {labelTpl ? ( - - ) : ( - `${ - getVariable(item, labelField || 'label') || - getVariable(item, 'id') - }` - )} - -
- ))} + {tags.map((item, index) => { + if (enableOverflow && index === maxTagCount) { + return ( + { + return ( +
+ {selectedOptions + .slice(maxTagCount, totalCount) + .map((overflowItem, rawIndex) => { + const key = rawIndex + maxTagCount; + + return this.renderTag(overflowItem, key); + })} +
+ ); + } + }} + > +
+ {item.label} +
+
+ ); + } + + return this.renderTag(item, index); + })}
); } @@ -466,6 +587,8 @@ export default class PickerControl extends React.PureComponent< source, strictMode } = this.props; + const {maxTagCount, overflowTagPopoverInCRUD, displayPosition} = + this.getOverflowConfig(); return render('modal-body', this.state.schema, { value: selectedOptions, @@ -512,7 +635,11 @@ export default class PickerControl extends React.PureComponent< } : undefined, ref: this.crudRef, - popOverContainer + popOverContainer, + ...(embed || + (Array.isArray(displayPosition) && displayPosition.includes('crud')) + ? {maxTagCount, overflowTagPopover: overflowTagPopoverInCRUD} + : {}) }) as JSX.Element; } render() {