@lowdefy/blocks-antd 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blocks/AutoComplete/AutoComplete.js +1 -0
- package/dist/blocks/AutoComplete/meta.js +2 -1
- package/dist/blocks/ButtonSelector/ButtonSelector.js +74 -27
- package/dist/blocks/ButtonSelector/meta.js +18 -4
- package/dist/blocks/Carousel/meta.js +16 -0
- package/dist/blocks/CheckboxSelector/CheckboxSelector.js +46 -14
- package/dist/blocks/CheckboxSelector/meta.js +7 -1
- package/dist/blocks/CheckboxSwitch/CheckboxSwitch.js +1 -0
- package/dist/blocks/CheckboxSwitch/meta.js +4 -1
- package/dist/blocks/ColorSelector/ColorSelector.js +1 -0
- package/dist/blocks/ColorSelector/meta.js +2 -1
- package/dist/blocks/ConfigProvider/ConfigProvider.js +1 -0
- package/dist/blocks/ConfigProvider/meta.js +7 -0
- package/dist/blocks/ConfirmModal/ConfirmModal.js +2 -2
- package/dist/blocks/ConfirmModal/meta.js +2 -4
- package/dist/blocks/DateRangeSelector/DateRangeSelector.js +4 -9
- package/dist/blocks/DateRangeSelector/meta.js +4 -8
- package/dist/blocks/DateSelector/DateSelector.js +4 -3
- package/dist/blocks/DateSelector/meta.js +4 -5
- package/dist/blocks/DateTimeSelector/DateTimeSelector.js +4 -3
- package/dist/blocks/DateTimeSelector/meta.js +4 -5
- package/dist/blocks/DropdownMenu/meta.js +46 -6
- package/dist/blocks/Label/Label.js +30 -5
- package/dist/blocks/Label/meta.js +8 -2
- package/dist/blocks/ListSelector/ListSelector.js +384 -0
- package/dist/blocks/ListSelector/e2e.js +40 -0
- package/dist/blocks/ListSelector/meta.js +215 -0
- package/dist/blocks/Menu/Menu.js +26 -80
- package/dist/blocks/Menu/meta.js +160 -64
- package/dist/blocks/MobileMenu/meta.js +50 -50
- package/dist/blocks/Modal/Modal.js +2 -2
- package/dist/blocks/Modal/meta.js +2 -4
- package/dist/blocks/MonthSelector/MonthSelector.js +4 -3
- package/dist/blocks/MonthSelector/meta.js +4 -5
- package/dist/blocks/MultipleSelector/MultipleSelector.js +41 -9
- package/dist/blocks/MultipleSelector/meta.js +24 -5
- package/dist/blocks/NumberInput/NumberInput.js +3 -1
- package/dist/blocks/NumberInput/meta.js +3 -3
- package/dist/blocks/PageHeaderMenu/PageHeaderMenu.js +10 -2
- package/dist/blocks/PageHeaderMenu/meta.js +8 -1
- package/dist/blocks/PageSidebarLayout/PageSidebarLayout.js +2 -1
- package/dist/blocks/PageSidebarLayout/meta.js +8 -1
- package/dist/blocks/PageSiderMenu/PageSiderMenu.js +2 -1
- package/dist/blocks/PageSiderMenu/meta.js +8 -1
- package/dist/blocks/PasswordInput/PasswordInput.js +1 -0
- package/dist/blocks/PasswordInput/meta.js +2 -1
- package/dist/blocks/PhoneNumberInput/PhoneNumberInput.js +1 -0
- package/dist/blocks/PhoneNumberInput/meta.js +2 -1
- package/dist/blocks/RadioSelector/RadioSelector.js +44 -14
- package/dist/blocks/RadioSelector/meta.js +7 -1
- package/dist/blocks/RatingSlider/meta.js +2 -1
- package/dist/blocks/SegmentedSelector/SegmentedSelector.js +10 -4
- package/dist/blocks/SegmentedSelector/meta.js +7 -4
- package/dist/blocks/Selector/Selector.js +55 -9
- package/dist/blocks/Selector/meta.js +24 -5
- package/dist/blocks/Slider/Slider.js +1 -0
- package/dist/blocks/Slider/meta.js +2 -1
- package/dist/blocks/Switch/Switch.js +1 -0
- package/dist/blocks/Switch/meta.js +2 -1
- package/dist/blocks/Tabs/Tabs.js +30 -43
- package/dist/blocks/Tabs/meta.js +8 -10
- package/dist/blocks/TextArea/TextArea.js +1 -0
- package/dist/blocks/TextArea/meta.js +2 -1
- package/dist/blocks/TextInput/TextInput.js +1 -0
- package/dist/blocks/TextInput/meta.js +2 -1
- package/dist/blocks/TreeInput/TreeInput.js +91 -0
- package/dist/blocks/TreeInput/e2e.js +33 -0
- package/dist/blocks/TreeInput/meta.js +68 -0
- package/dist/blocks/TreeMultipleSelector/TreeMultipleSelector.js +161 -0
- package/dist/blocks/TreeMultipleSelector/e2e.js +46 -0
- package/dist/blocks/TreeMultipleSelector/meta.js +128 -0
- package/dist/blocks/TreeSelector/TreeSelector.js +127 -88
- package/dist/blocks/TreeSelector/e2e.js +20 -9
- package/dist/blocks/TreeSelector/meta.js +70 -254
- package/dist/blocks/WeekSelector/WeekSelector.js +2 -1
- package/dist/blocks/WeekSelector/meta.js +3 -3
- package/dist/blocks/buildMenuItems.js +89 -26
- package/dist/blocks/headerActions.js +87 -3
- package/dist/blocks/normalizeItemClassAndStyle.js +77 -0
- package/dist/blocks.js +3 -0
- package/dist/e2e.js +3 -0
- package/dist/getContrastTextColor.js +45 -0
- package/dist/getOptionColorStyle.js +36 -0
- package/dist/getSelectedIndex.js +42 -0
- package/dist/getSelectorOptions.js +67 -0
- package/dist/getTreeData.js +94 -0
- package/dist/metas.js +3 -0
- package/dist/schemas/dataOptions.js +36 -0
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/label.js +3 -1
- package/dist/schemas/labelTooltip.js +44 -0
- package/dist/schemas/options.js +7 -3
- package/dist/schemas/treeSelectTheme.js +125 -0
- package/dist/serializeSelectorValue.js +38 -0
- package/dist/useSelectorOptions.js +38 -0
- package/dist/useSetData.js +27 -0
- package/package.json +9 -7
|
@@ -94,12 +94,24 @@
|
|
|
94
94
|
description: 'Open link in new tab.'
|
|
95
95
|
},
|
|
96
96
|
style: {
|
|
97
|
-
type:
|
|
98
|
-
|
|
97
|
+
type: [
|
|
98
|
+
'object',
|
|
99
|
+
'string',
|
|
100
|
+
'array'
|
|
101
|
+
],
|
|
102
|
+
description: 'CSS styles for the menu item. Use a flat object for the item wrapper, or dot-prefixed slot keys (`.element`, `.icon`, `.label`).',
|
|
99
103
|
docs: {
|
|
100
104
|
displayType: 'yaml'
|
|
101
105
|
}
|
|
102
106
|
},
|
|
107
|
+
class: {
|
|
108
|
+
type: [
|
|
109
|
+
'string',
|
|
110
|
+
'array',
|
|
111
|
+
'object'
|
|
112
|
+
],
|
|
113
|
+
description: 'CSS classes for the menu item. Flat applies to the item wrapper; use dot-prefixed slot keys to target parts.'
|
|
114
|
+
},
|
|
103
115
|
properties: {
|
|
104
116
|
type: 'object',
|
|
105
117
|
description: 'Properties for the menu item.',
|
|
@@ -128,6 +140,14 @@
|
|
|
128
140
|
default: false,
|
|
129
141
|
description: 'Disable the menu item.'
|
|
130
142
|
},
|
|
143
|
+
tooltip: {
|
|
144
|
+
type: 'string',
|
|
145
|
+
description: 'Tooltip text shown when the menu is collapsed.'
|
|
146
|
+
},
|
|
147
|
+
extra: {
|
|
148
|
+
type: 'string',
|
|
149
|
+
description: 'Free-form right-aligned label on a MenuLink. For real keybindings use `shortcut`; when both are set, `shortcut` sits to the right of `extra`.'
|
|
150
|
+
},
|
|
131
151
|
dashed: {
|
|
132
152
|
type: 'boolean',
|
|
133
153
|
default: false,
|
|
@@ -135,7 +155,7 @@
|
|
|
135
155
|
},
|
|
136
156
|
shortcut: {
|
|
137
157
|
type: 'string',
|
|
138
|
-
description: 'Keyboard shortcut
|
|
158
|
+
description: 'Keyboard shortcut for this menu item. Renders a kbd badge floated to the far right of the item AND wires the key handler. Use "mod" for Cmd/Ctrl.'
|
|
139
159
|
}
|
|
140
160
|
}
|
|
141
161
|
},
|
|
@@ -175,12 +195,24 @@
|
|
|
175
195
|
description: 'Open link in new tab.'
|
|
176
196
|
},
|
|
177
197
|
style: {
|
|
178
|
-
type:
|
|
179
|
-
|
|
198
|
+
type: [
|
|
199
|
+
'object',
|
|
200
|
+
'string',
|
|
201
|
+
'array'
|
|
202
|
+
],
|
|
203
|
+
description: 'CSS styles for the menu item. Flat or dot-prefixed slot keys (`.element`, `.icon`, `.label`).',
|
|
180
204
|
docs: {
|
|
181
205
|
displayType: 'yaml'
|
|
182
206
|
}
|
|
183
207
|
},
|
|
208
|
+
class: {
|
|
209
|
+
type: [
|
|
210
|
+
'string',
|
|
211
|
+
'array',
|
|
212
|
+
'object'
|
|
213
|
+
],
|
|
214
|
+
description: 'CSS classes for the menu item.'
|
|
215
|
+
},
|
|
184
216
|
properties: {
|
|
185
217
|
type: 'object',
|
|
186
218
|
description: 'Properties for the menu item.',
|
|
@@ -209,6 +241,14 @@
|
|
|
209
241
|
default: false,
|
|
210
242
|
description: 'Disable the item.'
|
|
211
243
|
},
|
|
244
|
+
tooltip: {
|
|
245
|
+
type: 'string',
|
|
246
|
+
description: 'Tooltip text shown when the menu is collapsed.'
|
|
247
|
+
},
|
|
248
|
+
extra: {
|
|
249
|
+
type: 'string',
|
|
250
|
+
description: 'Free-form right-aligned label on a MenuLink. For real keybindings use `shortcut`.'
|
|
251
|
+
},
|
|
212
252
|
dashed: {
|
|
213
253
|
type: 'boolean',
|
|
214
254
|
default: false,
|
|
@@ -216,7 +256,7 @@
|
|
|
216
256
|
},
|
|
217
257
|
shortcut: {
|
|
218
258
|
type: 'string',
|
|
219
|
-
description: 'Keyboard shortcut. Renders a
|
|
259
|
+
description: 'Keyboard shortcut. Renders a kbd badge floated to the far right and wires the key handler. Use "mod" for Cmd/Ctrl.'
|
|
220
260
|
}
|
|
221
261
|
}
|
|
222
262
|
}
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
// MIT Copyright (c) 2015-present Ant UED, https://xtech.antfin.com/ - 2020-09-08
|
|
17
17
|
import React from 'react';
|
|
18
18
|
import { renderHtml, withBlockDefaults } from '@lowdefy/block-utils';
|
|
19
|
-
import {
|
|
19
|
+
import { type } from '@lowdefy/helpers';
|
|
20
|
+
import { Col, Row, Tooltip } from 'antd';
|
|
20
21
|
import classNames from 'classnames';
|
|
21
22
|
import CSSMotion from '@rc-component/motion';
|
|
22
23
|
import labelLogic from './labelLogic.js';
|
|
@@ -26,7 +27,7 @@ const validationKeyMap = {
|
|
|
26
27
|
warning: 'warnings'
|
|
27
28
|
};
|
|
28
29
|
let iconMap;
|
|
29
|
-
const Label = ({ blockId, classNames: blockClassNames = {}, components: { Icon }, content, properties, required, styles = {}, validation })=>{
|
|
30
|
+
const Label = ({ blockId, classNames: blockClassNames = {}, components: { Icon }, content, methods, properties, required, styles = {}, validation })=>{
|
|
30
31
|
const { extraClassName, extraStyle, feedbackClassName, feedbackStyle, iconClassName, label, labelClassName, labelCol, labelColClassName, labelColStyle, labelStyle, rowClassName, rowStyle, showExtra, showFeedback, wrapperCol } = labelLogic({
|
|
31
32
|
blockId,
|
|
32
33
|
blockClassNames,
|
|
@@ -56,6 +57,12 @@ const Label = ({ blockId, classNames: blockClassNames = {}, components: { Icon }
|
|
|
56
57
|
const icon = validation.status && IconNode ? /*#__PURE__*/ React.createElement("span", {
|
|
57
58
|
className: iconClassName
|
|
58
59
|
}, /*#__PURE__*/ React.createElement(IconNode, null)) : null;
|
|
60
|
+
// tooltip is either a string (the tooltip text) or an object that also
|
|
61
|
+
// customizes the icon and color. The onClick is exposed as the block's
|
|
62
|
+
// onTooltipClick event, not a property.
|
|
63
|
+
const tooltip = type.isString(properties.tooltip) ? {
|
|
64
|
+
title: properties.tooltip
|
|
65
|
+
} : properties.tooltip;
|
|
59
66
|
return /*#__PURE__*/ React.createElement(Row, {
|
|
60
67
|
id: blockId,
|
|
61
68
|
className: rowClassName,
|
|
@@ -67,11 +74,29 @@ const Label = ({ blockId, classNames: blockClassNames = {}, components: { Icon }
|
|
|
67
74
|
}, /*#__PURE__*/ React.createElement("label", {
|
|
68
75
|
htmlFor: `${blockId}_input`,
|
|
69
76
|
className: labelClassName,
|
|
70
|
-
style: labelStyle
|
|
71
|
-
title: label
|
|
77
|
+
style: labelStyle
|
|
72
78
|
}, renderHtml({
|
|
73
79
|
html: label
|
|
74
|
-
})
|
|
80
|
+
}), tooltip && /*#__PURE__*/ React.createElement(Tooltip, {
|
|
81
|
+
title: tooltip.title ? renderHtml({
|
|
82
|
+
html: tooltip.title
|
|
83
|
+
}) : undefined
|
|
84
|
+
}, /*#__PURE__*/ React.createElement("span", {
|
|
85
|
+
className: "ldf-label-tooltip",
|
|
86
|
+
style: {
|
|
87
|
+
marginInlineStart: 4,
|
|
88
|
+
cursor: 'pointer',
|
|
89
|
+
color: tooltip.color
|
|
90
|
+
},
|
|
91
|
+
onClick: (event)=>{
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
methods?.triggerEvent({
|
|
94
|
+
name: 'onTooltipClick'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}, /*#__PURE__*/ React.createElement(Icon, {
|
|
98
|
+
properties: tooltip.icon ?? 'AiOutlineQuestionCircle'
|
|
99
|
+
}))))), /*#__PURE__*/ React.createElement(Col, {
|
|
75
100
|
...wrapperCol,
|
|
76
101
|
className: "ant-form-item-control"
|
|
77
102
|
}, /*#__PURE__*/ React.createElement("div", {
|
|
@@ -12,13 +12,18 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/
|
|
15
|
+
*/ import tooltip from '../../schemas/labelTooltip.js';
|
|
16
|
+
export default {
|
|
16
17
|
category: 'container',
|
|
18
|
+
events: {
|
|
19
|
+
onTooltipClick: 'Trigger actions when the tooltip icon is clicked.'
|
|
20
|
+
},
|
|
17
21
|
icons: [
|
|
18
22
|
'AiFillCloseCircle',
|
|
19
23
|
'AiFillCheckCircle',
|
|
20
24
|
'AiOutlineLoading',
|
|
21
|
-
'AiFillExclamationCircle'
|
|
25
|
+
'AiFillExclamationCircle',
|
|
26
|
+
'AiOutlineQuestionCircle'
|
|
22
27
|
],
|
|
23
28
|
valueType: null,
|
|
24
29
|
slots: {
|
|
@@ -71,6 +76,7 @@
|
|
|
71
76
|
type: 'string',
|
|
72
77
|
description: 'Label title - supports html.'
|
|
73
78
|
},
|
|
79
|
+
tooltip,
|
|
74
80
|
span: {
|
|
75
81
|
type: 'number',
|
|
76
82
|
description: 'Label inline span.'
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
16
|
+
import { Card, Input, Skeleton, theme } from 'antd';
|
|
17
|
+
import { Virtuoso } from 'react-virtuoso';
|
|
18
|
+
import { renderHtml, withBlockDefaults } from '@lowdefy/block-utils';
|
|
19
|
+
import { nunjucksFunction } from '@lowdefy/nunjucks';
|
|
20
|
+
import { get, serializer, type } from '@lowdefy/helpers';
|
|
21
|
+
import withTheme from '../withTheme.js';
|
|
22
|
+
import useSetData from '../../useSetData.js';
|
|
23
|
+
const FIELD_SEPARATOR = '';
|
|
24
|
+
const SKELETON_COUNT = 4;
|
|
25
|
+
// The value stored on select: `valueKey` names the field, otherwise the whole item.
|
|
26
|
+
function valueOf(item, valueKey) {
|
|
27
|
+
return type.isString(valueKey) && type.isObject(item) ? get(item, valueKey) : item;
|
|
28
|
+
}
|
|
29
|
+
// The identity used to match the current value back to a row: `primaryKey` (falling back to
|
|
30
|
+
// `valueKey`) names the field; with neither, the whole value is the identity.
|
|
31
|
+
function identityOf(x, effKey) {
|
|
32
|
+
return type.isString(effKey) && type.isObject(x) ? get(x, effKey) : x;
|
|
33
|
+
}
|
|
34
|
+
const ListSelectorRow = /*#__PURE__*/ React.memo(function ListSelectorRow({ blockId, index, item, template, bordered, hoverable, size, gap, cardClassName, bodyClassName, cardStyle, bodyStyle, clickable, selectable, selectedKey, effKey, selectedClassName, selectedStyle, onRowClick, methodsRef }) {
|
|
35
|
+
const html = useMemo(()=>template ? template({
|
|
36
|
+
item,
|
|
37
|
+
index
|
|
38
|
+
}) : null, [
|
|
39
|
+
template,
|
|
40
|
+
item,
|
|
41
|
+
index
|
|
42
|
+
]);
|
|
43
|
+
const selected = useMemo(()=>selectedKey != null && serializer.serializeToString(identityOf(item, effKey), {
|
|
44
|
+
stable: true
|
|
45
|
+
}) === selectedKey, [
|
|
46
|
+
item,
|
|
47
|
+
selectedKey,
|
|
48
|
+
effKey
|
|
49
|
+
]);
|
|
50
|
+
const handleClick = useCallback(()=>onRowClick(index, item), [
|
|
51
|
+
onRowClick,
|
|
52
|
+
index,
|
|
53
|
+
item
|
|
54
|
+
]);
|
|
55
|
+
const className = [
|
|
56
|
+
cardClassName,
|
|
57
|
+
selected ? selectedClassName : null
|
|
58
|
+
].filter(Boolean).join(' ') || undefined;
|
|
59
|
+
return /*#__PURE__*/ React.createElement("div", {
|
|
60
|
+
style: {
|
|
61
|
+
paddingBottom: gap
|
|
62
|
+
}
|
|
63
|
+
}, /*#__PURE__*/ React.createElement(Card, {
|
|
64
|
+
id: `${blockId}_${index}`,
|
|
65
|
+
variant: bordered === false ? 'borderless' : 'outlined',
|
|
66
|
+
hoverable: hoverable,
|
|
67
|
+
size: size,
|
|
68
|
+
onClick: clickable ? handleClick : undefined,
|
|
69
|
+
"aria-selected": selectable ? selected : undefined,
|
|
70
|
+
className: className,
|
|
71
|
+
classNames: {
|
|
72
|
+
body: bodyClassName
|
|
73
|
+
},
|
|
74
|
+
style: {
|
|
75
|
+
outline: 'none',
|
|
76
|
+
cursor: clickable ? 'pointer' : undefined,
|
|
77
|
+
...cardStyle,
|
|
78
|
+
...selected ? selectedStyle : null
|
|
79
|
+
},
|
|
80
|
+
styles: {
|
|
81
|
+
body: bodyStyle
|
|
82
|
+
}
|
|
83
|
+
}, html != null && renderHtml({
|
|
84
|
+
html,
|
|
85
|
+
methods: methodsRef.current
|
|
86
|
+
})));
|
|
87
|
+
});
|
|
88
|
+
function useSearchBlobs(data, fields, caseSensitive) {
|
|
89
|
+
return useMemo(()=>{
|
|
90
|
+
if (!data || data.length === 0) return null;
|
|
91
|
+
const normalize = caseSensitive ? (s)=>s : (s)=>s.toLowerCase();
|
|
92
|
+
if (!type.isArray(fields) || fields.length === 0) {
|
|
93
|
+
return data.map((item)=>normalize(JSON.stringify(item) ?? ''));
|
|
94
|
+
}
|
|
95
|
+
return data.map((item)=>normalize(fields.map((f)=>{
|
|
96
|
+
const v = get(item, f);
|
|
97
|
+
return type.isNone(v) ? '' : String(v);
|
|
98
|
+
}).join(FIELD_SEPARATOR)));
|
|
99
|
+
}, [
|
|
100
|
+
data,
|
|
101
|
+
fields,
|
|
102
|
+
caseSensitive
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
const ListSelector = ({ blockId, classNames = {}, events, loading, methods, properties, styles = {}, value })=>{
|
|
106
|
+
const data = useSetData({
|
|
107
|
+
properties,
|
|
108
|
+
methods
|
|
109
|
+
}) ?? [];
|
|
110
|
+
const template = useMemo(()=>type.isString(properties.html) ? nunjucksFunction(properties.html) : null, [
|
|
111
|
+
properties.html
|
|
112
|
+
]);
|
|
113
|
+
const selectable = properties.selectable !== false;
|
|
114
|
+
const allowDeselect = properties.allowDeselect !== false;
|
|
115
|
+
// `valueKey` names the field stored on select (otherwise the whole item). `primaryKey` (falling
|
|
116
|
+
// back to `valueKey`) is the identity matched when the value is controlled via state.
|
|
117
|
+
const valueKey = properties.valueKey;
|
|
118
|
+
const effKey = type.isString(properties.primaryKey) ? properties.primaryKey : valueKey;
|
|
119
|
+
// Selection lives in the block value (app state), so a single serialized key identifies the
|
|
120
|
+
// selected row. When selection is off the block stores no value and renders like a plain list.
|
|
121
|
+
const selectedKey = useMemo(()=>!selectable || type.isNone(value) ? null : serializer.serializeToString(identityOf(value, effKey), {
|
|
122
|
+
stable: true
|
|
123
|
+
}), [
|
|
124
|
+
selectable,
|
|
125
|
+
value,
|
|
126
|
+
effKey
|
|
127
|
+
]);
|
|
128
|
+
const methodsRef = useRef(methods);
|
|
129
|
+
methodsRef.current = methods;
|
|
130
|
+
const selectableRef = useRef(selectable);
|
|
131
|
+
selectableRef.current = selectable;
|
|
132
|
+
const allowDeselectRef = useRef(allowDeselect);
|
|
133
|
+
allowDeselectRef.current = allowDeselect;
|
|
134
|
+
const selectedKeyRef = useRef(selectedKey);
|
|
135
|
+
selectedKeyRef.current = selectedKey;
|
|
136
|
+
const valueKeyRef = useRef(valueKey);
|
|
137
|
+
valueKeyRef.current = valueKey;
|
|
138
|
+
const effKeyRef = useRef(effKey);
|
|
139
|
+
effKeyRef.current = effKey;
|
|
140
|
+
const clickable = selectable || Boolean(events.onClick);
|
|
141
|
+
const onRowClick = useCallback((index, item)=>{
|
|
142
|
+
if (selectableRef.current) {
|
|
143
|
+
const itemKey = serializer.serializeToString(identityOf(item, effKeyRef.current), {
|
|
144
|
+
stable: true
|
|
145
|
+
});
|
|
146
|
+
const deselect = allowDeselectRef.current && itemKey === selectedKeyRef.current;
|
|
147
|
+
const newValue = deselect ? null : valueOf(item, valueKeyRef.current);
|
|
148
|
+
methodsRef.current.setValue(newValue);
|
|
149
|
+
methodsRef.current.triggerEvent({
|
|
150
|
+
name: 'onChange',
|
|
151
|
+
event: {
|
|
152
|
+
value: newValue,
|
|
153
|
+
index,
|
|
154
|
+
item
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
methodsRef.current.triggerEvent({
|
|
159
|
+
name: 'onClick',
|
|
160
|
+
event: {
|
|
161
|
+
index,
|
|
162
|
+
item
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}, []);
|
|
166
|
+
const { token } = theme.useToken();
|
|
167
|
+
const selectedStyle = useMemo(()=>({
|
|
168
|
+
borderColor: token.colorPrimary,
|
|
169
|
+
boxShadow: `0 0 0 1px ${token.colorPrimary}`,
|
|
170
|
+
...styles.selected
|
|
171
|
+
}), [
|
|
172
|
+
token.colorPrimary,
|
|
173
|
+
styles.selected
|
|
174
|
+
]);
|
|
175
|
+
const gap = properties.gap ?? 8;
|
|
176
|
+
const useWindowScroll = type.isNone(properties.height);
|
|
177
|
+
const overscan = properties.overscan ?? 400;
|
|
178
|
+
const search = properties.search;
|
|
179
|
+
const searchEnabled = type.isObject(search);
|
|
180
|
+
const searchCaseSensitive = searchEnabled ? !!search.caseSensitive : false;
|
|
181
|
+
const searchFields = searchEnabled ? search.fields : null;
|
|
182
|
+
const searchMinLength = searchEnabled ? search.minLength ?? 0 : 0;
|
|
183
|
+
const searchDebounce = searchEnabled ? search.debounce ?? 150 : 150;
|
|
184
|
+
const searchSticky = searchEnabled ? search.sticky !== false : false;
|
|
185
|
+
const searchAllowClear = searchEnabled ? search.allowClear !== false : true;
|
|
186
|
+
const searchPlaceholder = searchEnabled ? search.placeholder ?? methods.translate('blocks.listSelector.search.placeholder') : '';
|
|
187
|
+
const noResultsText = searchEnabled ? search.noResultsText ?? methods.translate('blocks.listSelector.search.noResults') : '';
|
|
188
|
+
const noDataText = properties.noData ?? methods.translate('blocks.listSelector.noData');
|
|
189
|
+
const [rawQuery, setRawQuery] = useState('');
|
|
190
|
+
const [appliedQuery, setAppliedQuery] = useState('');
|
|
191
|
+
const debounceRef = useRef(null);
|
|
192
|
+
useEffect(()=>()=>{
|
|
193
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
194
|
+
}, []);
|
|
195
|
+
const onSearchChange = useCallback((e)=>{
|
|
196
|
+
const v = e.target.value;
|
|
197
|
+
setRawQuery(v);
|
|
198
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
199
|
+
debounceRef.current = setTimeout(()=>setAppliedQuery(v), searchDebounce);
|
|
200
|
+
}, [
|
|
201
|
+
searchDebounce
|
|
202
|
+
]);
|
|
203
|
+
const blobs = useSearchBlobs(searchEnabled ? data : null, searchEnabled ? searchFields : null, searchCaseSensitive);
|
|
204
|
+
const filterActive = searchEnabled && appliedQuery && appliedQuery.length >= searchMinLength;
|
|
205
|
+
const filteredEntries = useMemo(()=>{
|
|
206
|
+
if (!filterActive || !blobs) return null;
|
|
207
|
+
const needle = searchCaseSensitive ? appliedQuery : appliedQuery.toLowerCase();
|
|
208
|
+
const out = [];
|
|
209
|
+
for(let i = 0; i < blobs.length; i++){
|
|
210
|
+
if (blobs[i].includes(needle)) out.push({
|
|
211
|
+
originalIndex: i,
|
|
212
|
+
item: data[i]
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}, [
|
|
217
|
+
filterActive,
|
|
218
|
+
blobs,
|
|
219
|
+
data,
|
|
220
|
+
appliedQuery,
|
|
221
|
+
searchCaseSensitive
|
|
222
|
+
]);
|
|
223
|
+
const lastFiredQueryRef = useRef('');
|
|
224
|
+
useEffect(()=>{
|
|
225
|
+
if (!searchEnabled) return;
|
|
226
|
+
if (lastFiredQueryRef.current === appliedQuery) return;
|
|
227
|
+
lastFiredQueryRef.current = appliedQuery;
|
|
228
|
+
const resultCount = filterActive ? filteredEntries ? filteredEntries.length : 0 : data.length;
|
|
229
|
+
methodsRef.current.triggerEvent({
|
|
230
|
+
name: 'onSearch',
|
|
231
|
+
event: {
|
|
232
|
+
value: appliedQuery,
|
|
233
|
+
resultCount
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}, [
|
|
237
|
+
searchEnabled,
|
|
238
|
+
appliedQuery,
|
|
239
|
+
filterActive,
|
|
240
|
+
filteredEntries,
|
|
241
|
+
data
|
|
242
|
+
]);
|
|
243
|
+
const itemContent = useCallback((_virtualIndex, payload)=>{
|
|
244
|
+
const isEntry = filterActive && payload && type.isObject(payload) && 'originalIndex' in payload;
|
|
245
|
+
const index = isEntry ? payload.originalIndex : _virtualIndex;
|
|
246
|
+
const item = isEntry ? payload.item : payload;
|
|
247
|
+
return /*#__PURE__*/ React.createElement(ListSelectorRow, {
|
|
248
|
+
blockId: blockId,
|
|
249
|
+
index: index,
|
|
250
|
+
item: item,
|
|
251
|
+
template: template,
|
|
252
|
+
bordered: properties.bordered,
|
|
253
|
+
hoverable: properties.hoverable,
|
|
254
|
+
size: properties.size,
|
|
255
|
+
gap: gap,
|
|
256
|
+
cardClassName: classNames.card,
|
|
257
|
+
bodyClassName: classNames.body,
|
|
258
|
+
cardStyle: styles.card,
|
|
259
|
+
bodyStyle: styles.body,
|
|
260
|
+
clickable: clickable,
|
|
261
|
+
selectable: selectable,
|
|
262
|
+
selectedKey: selectedKey,
|
|
263
|
+
effKey: effKey,
|
|
264
|
+
selectedClassName: classNames.selected,
|
|
265
|
+
selectedStyle: selectedStyle,
|
|
266
|
+
onRowClick: onRowClick,
|
|
267
|
+
methodsRef: methodsRef
|
|
268
|
+
});
|
|
269
|
+
}, [
|
|
270
|
+
filterActive,
|
|
271
|
+
blockId,
|
|
272
|
+
template,
|
|
273
|
+
properties.bordered,
|
|
274
|
+
properties.hoverable,
|
|
275
|
+
properties.size,
|
|
276
|
+
gap,
|
|
277
|
+
classNames.card,
|
|
278
|
+
classNames.body,
|
|
279
|
+
classNames.selected,
|
|
280
|
+
styles.card,
|
|
281
|
+
styles.body,
|
|
282
|
+
clickable,
|
|
283
|
+
selectable,
|
|
284
|
+
selectedKey,
|
|
285
|
+
effKey,
|
|
286
|
+
selectedStyle,
|
|
287
|
+
onRowClick
|
|
288
|
+
]);
|
|
289
|
+
const computeItemKey = useCallback((_virtualIndex, payload)=>{
|
|
290
|
+
if (filterActive && payload && type.isObject(payload) && 'originalIndex' in payload) {
|
|
291
|
+
return `${blockId}_${payload.originalIndex}`;
|
|
292
|
+
}
|
|
293
|
+
return `${blockId}_${_virtualIndex}`;
|
|
294
|
+
}, [
|
|
295
|
+
blockId,
|
|
296
|
+
filterActive
|
|
297
|
+
]);
|
|
298
|
+
const containerStyle = useWindowScroll ? styles.element : {
|
|
299
|
+
display: 'flex',
|
|
300
|
+
flexDirection: 'column',
|
|
301
|
+
height: properties.height,
|
|
302
|
+
...styles.element
|
|
303
|
+
};
|
|
304
|
+
const headerStyle = {
|
|
305
|
+
position: searchSticky ? 'sticky' : undefined,
|
|
306
|
+
top: 0,
|
|
307
|
+
zIndex: 1,
|
|
308
|
+
paddingBottom: gap
|
|
309
|
+
};
|
|
310
|
+
const placeholderStyle = {
|
|
311
|
+
padding: token.paddingLG,
|
|
312
|
+
textAlign: 'center',
|
|
313
|
+
color: token.colorTextSecondary
|
|
314
|
+
};
|
|
315
|
+
const renderSearch = ()=>searchEnabled ? /*#__PURE__*/ React.createElement("div", {
|
|
316
|
+
style: headerStyle,
|
|
317
|
+
className: classNames.search
|
|
318
|
+
}, /*#__PURE__*/ React.createElement(Input.Search, {
|
|
319
|
+
id: `${blockId}_search`,
|
|
320
|
+
placeholder: searchPlaceholder,
|
|
321
|
+
allowClear: searchAllowClear,
|
|
322
|
+
value: rawQuery,
|
|
323
|
+
onChange: onSearchChange
|
|
324
|
+
})) : null;
|
|
325
|
+
if (loading) {
|
|
326
|
+
return /*#__PURE__*/ React.createElement("div", {
|
|
327
|
+
id: blockId,
|
|
328
|
+
className: classNames.element,
|
|
329
|
+
style: containerStyle
|
|
330
|
+
}, renderSearch(), Array.from({
|
|
331
|
+
length: SKELETON_COUNT
|
|
332
|
+
}).map((_, i)=>/*#__PURE__*/ React.createElement("div", {
|
|
333
|
+
key: `${blockId}_skeleton_${i}`,
|
|
334
|
+
style: {
|
|
335
|
+
paddingBottom: gap
|
|
336
|
+
}
|
|
337
|
+
}, /*#__PURE__*/ React.createElement(Card, {
|
|
338
|
+
variant: properties.bordered === false ? 'borderless' : 'outlined',
|
|
339
|
+
size: properties.size,
|
|
340
|
+
className: classNames.card,
|
|
341
|
+
styles: {
|
|
342
|
+
body: styles.body
|
|
343
|
+
}
|
|
344
|
+
}, /*#__PURE__*/ React.createElement(Skeleton, {
|
|
345
|
+
active: true,
|
|
346
|
+
title: true,
|
|
347
|
+
paragraph: {
|
|
348
|
+
rows: 2
|
|
349
|
+
}
|
|
350
|
+
})))));
|
|
351
|
+
}
|
|
352
|
+
if (data.length === 0) {
|
|
353
|
+
return /*#__PURE__*/ React.createElement("div", {
|
|
354
|
+
id: blockId,
|
|
355
|
+
className: classNames.element,
|
|
356
|
+
style: containerStyle
|
|
357
|
+
}, /*#__PURE__*/ React.createElement("div", {
|
|
358
|
+
className: classNames.noData,
|
|
359
|
+
style: placeholderStyle
|
|
360
|
+
}, noDataText));
|
|
361
|
+
}
|
|
362
|
+
const virtuosoData = filterActive ? filteredEntries : data;
|
|
363
|
+
const virtuosoStyle = useWindowScroll ? undefined : {
|
|
364
|
+
flex: '1 1 auto',
|
|
365
|
+
minHeight: 0
|
|
366
|
+
};
|
|
367
|
+
return /*#__PURE__*/ React.createElement("div", {
|
|
368
|
+
id: blockId,
|
|
369
|
+
className: classNames.element,
|
|
370
|
+
style: containerStyle
|
|
371
|
+
}, renderSearch(), filterActive && filteredEntries.length === 0 ? /*#__PURE__*/ React.createElement("div", {
|
|
372
|
+
className: classNames.noResults,
|
|
373
|
+
style: placeholderStyle
|
|
374
|
+
}, noResultsText) : /*#__PURE__*/ React.createElement(Virtuoso, {
|
|
375
|
+
data: virtuosoData,
|
|
376
|
+
style: virtuosoStyle,
|
|
377
|
+
useWindowScroll: useWindowScroll,
|
|
378
|
+
overscan: overscan,
|
|
379
|
+
increaseViewportBy: overscan,
|
|
380
|
+
itemContent: itemContent,
|
|
381
|
+
computeItemKey: computeItemKey
|
|
382
|
+
}));
|
|
383
|
+
};
|
|
384
|
+
export default withTheme('Card', withBlockDefaults(ListSelector));
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { createBlockHelper, escapeId } from '@lowdefy/e2e-utils';
|
|
16
|
+
import { expect } from '@playwright/test';
|
|
17
|
+
const locator = (page, blockId)=>page.locator(`#${escapeId(blockId)}`);
|
|
18
|
+
export default createBlockHelper({
|
|
19
|
+
locator,
|
|
20
|
+
do: {
|
|
21
|
+
clickItem: async (page, blockId, index)=>{
|
|
22
|
+
const card = page.locator(`#${escapeId(`${blockId}_${index}`)}`);
|
|
23
|
+
await card.scrollIntoViewIfNeeded();
|
|
24
|
+
await card.click();
|
|
25
|
+
},
|
|
26
|
+
select: async (page, blockId, index)=>{
|
|
27
|
+
const card = page.locator(`#${escapeId(`${blockId}_${index}`)}`);
|
|
28
|
+
await card.scrollIntoViewIfNeeded();
|
|
29
|
+
await card.click();
|
|
30
|
+
},
|
|
31
|
+
scrollToIndex: (page, blockId, index)=>page.locator(`#${escapeId(`${blockId}_${index}`)}`).scrollIntoViewIfNeeded(),
|
|
32
|
+
search: (page, blockId, text)=>page.locator(`#${escapeId(`${blockId}_search`)} input`).fill(text),
|
|
33
|
+
clearSearch: (page, blockId)=>page.locator(`#${escapeId(`${blockId}_search`)} input`).fill('')
|
|
34
|
+
},
|
|
35
|
+
expect: {
|
|
36
|
+
renderedCount: (page, blockId, count)=>expect(locator(page, blockId).locator('.ant-card')).toHaveCount(count),
|
|
37
|
+
noResults: (page, blockId, text = 'No results')=>expect(locator(page, blockId)).toContainText(text),
|
|
38
|
+
selected: (page, blockId, index)=>expect(page.locator(`#${escapeId(`${blockId}_${index}`)}`)).toHaveAttribute('aria-selected', 'true')
|
|
39
|
+
}
|
|
40
|
+
});
|