@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.
Files changed (97) hide show
  1. package/dist/blocks/AutoComplete/AutoComplete.js +1 -0
  2. package/dist/blocks/AutoComplete/meta.js +2 -1
  3. package/dist/blocks/ButtonSelector/ButtonSelector.js +74 -27
  4. package/dist/blocks/ButtonSelector/meta.js +18 -4
  5. package/dist/blocks/Carousel/meta.js +16 -0
  6. package/dist/blocks/CheckboxSelector/CheckboxSelector.js +46 -14
  7. package/dist/blocks/CheckboxSelector/meta.js +7 -1
  8. package/dist/blocks/CheckboxSwitch/CheckboxSwitch.js +1 -0
  9. package/dist/blocks/CheckboxSwitch/meta.js +4 -1
  10. package/dist/blocks/ColorSelector/ColorSelector.js +1 -0
  11. package/dist/blocks/ColorSelector/meta.js +2 -1
  12. package/dist/blocks/ConfigProvider/ConfigProvider.js +1 -0
  13. package/dist/blocks/ConfigProvider/meta.js +7 -0
  14. package/dist/blocks/ConfirmModal/ConfirmModal.js +2 -2
  15. package/dist/blocks/ConfirmModal/meta.js +2 -4
  16. package/dist/blocks/DateRangeSelector/DateRangeSelector.js +4 -9
  17. package/dist/blocks/DateRangeSelector/meta.js +4 -8
  18. package/dist/blocks/DateSelector/DateSelector.js +4 -3
  19. package/dist/blocks/DateSelector/meta.js +4 -5
  20. package/dist/blocks/DateTimeSelector/DateTimeSelector.js +4 -3
  21. package/dist/blocks/DateTimeSelector/meta.js +4 -5
  22. package/dist/blocks/DropdownMenu/meta.js +46 -6
  23. package/dist/blocks/Label/Label.js +30 -5
  24. package/dist/blocks/Label/meta.js +8 -2
  25. package/dist/blocks/ListSelector/ListSelector.js +384 -0
  26. package/dist/blocks/ListSelector/e2e.js +40 -0
  27. package/dist/blocks/ListSelector/meta.js +215 -0
  28. package/dist/blocks/Menu/Menu.js +26 -80
  29. package/dist/blocks/Menu/meta.js +160 -64
  30. package/dist/blocks/MobileMenu/meta.js +50 -50
  31. package/dist/blocks/Modal/Modal.js +2 -2
  32. package/dist/blocks/Modal/meta.js +2 -4
  33. package/dist/blocks/MonthSelector/MonthSelector.js +4 -3
  34. package/dist/blocks/MonthSelector/meta.js +4 -5
  35. package/dist/blocks/MultipleSelector/MultipleSelector.js +41 -9
  36. package/dist/blocks/MultipleSelector/meta.js +24 -5
  37. package/dist/blocks/NumberInput/NumberInput.js +3 -1
  38. package/dist/blocks/NumberInput/meta.js +3 -3
  39. package/dist/blocks/PageHeaderMenu/PageHeaderMenu.js +10 -2
  40. package/dist/blocks/PageHeaderMenu/meta.js +8 -1
  41. package/dist/blocks/PageSidebarLayout/PageSidebarLayout.js +2 -1
  42. package/dist/blocks/PageSidebarLayout/meta.js +8 -1
  43. package/dist/blocks/PageSiderMenu/PageSiderMenu.js +2 -1
  44. package/dist/blocks/PageSiderMenu/meta.js +8 -1
  45. package/dist/blocks/PasswordInput/PasswordInput.js +1 -0
  46. package/dist/blocks/PasswordInput/meta.js +2 -1
  47. package/dist/blocks/PhoneNumberInput/PhoneNumberInput.js +1 -0
  48. package/dist/blocks/PhoneNumberInput/meta.js +2 -1
  49. package/dist/blocks/RadioSelector/RadioSelector.js +44 -14
  50. package/dist/blocks/RadioSelector/meta.js +7 -1
  51. package/dist/blocks/RatingSlider/meta.js +2 -1
  52. package/dist/blocks/SegmentedSelector/SegmentedSelector.js +10 -4
  53. package/dist/blocks/SegmentedSelector/meta.js +7 -4
  54. package/dist/blocks/Selector/Selector.js +55 -9
  55. package/dist/blocks/Selector/meta.js +24 -5
  56. package/dist/blocks/Slider/Slider.js +1 -0
  57. package/dist/blocks/Slider/meta.js +2 -1
  58. package/dist/blocks/Switch/Switch.js +1 -0
  59. package/dist/blocks/Switch/meta.js +2 -1
  60. package/dist/blocks/Tabs/Tabs.js +30 -43
  61. package/dist/blocks/Tabs/meta.js +8 -10
  62. package/dist/blocks/TextArea/TextArea.js +1 -0
  63. package/dist/blocks/TextArea/meta.js +2 -1
  64. package/dist/blocks/TextInput/TextInput.js +1 -0
  65. package/dist/blocks/TextInput/meta.js +2 -1
  66. package/dist/blocks/TreeInput/TreeInput.js +91 -0
  67. package/dist/blocks/TreeInput/e2e.js +33 -0
  68. package/dist/blocks/TreeInput/meta.js +68 -0
  69. package/dist/blocks/TreeMultipleSelector/TreeMultipleSelector.js +161 -0
  70. package/dist/blocks/TreeMultipleSelector/e2e.js +46 -0
  71. package/dist/blocks/TreeMultipleSelector/meta.js +128 -0
  72. package/dist/blocks/TreeSelector/TreeSelector.js +127 -88
  73. package/dist/blocks/TreeSelector/e2e.js +20 -9
  74. package/dist/blocks/TreeSelector/meta.js +70 -254
  75. package/dist/blocks/WeekSelector/WeekSelector.js +2 -1
  76. package/dist/blocks/WeekSelector/meta.js +3 -3
  77. package/dist/blocks/buildMenuItems.js +89 -26
  78. package/dist/blocks/headerActions.js +87 -3
  79. package/dist/blocks/normalizeItemClassAndStyle.js +77 -0
  80. package/dist/blocks.js +3 -0
  81. package/dist/e2e.js +3 -0
  82. package/dist/getContrastTextColor.js +45 -0
  83. package/dist/getOptionColorStyle.js +36 -0
  84. package/dist/getSelectedIndex.js +42 -0
  85. package/dist/getSelectorOptions.js +67 -0
  86. package/dist/getTreeData.js +94 -0
  87. package/dist/metas.js +3 -0
  88. package/dist/schemas/dataOptions.js +36 -0
  89. package/dist/schemas/index.js +1 -0
  90. package/dist/schemas/label.js +3 -1
  91. package/dist/schemas/labelTooltip.js +44 -0
  92. package/dist/schemas/options.js +7 -3
  93. package/dist/schemas/treeSelectTheme.js +125 -0
  94. package/dist/serializeSelectorValue.js +38 -0
  95. package/dist/useSelectorOptions.js +38 -0
  96. package/dist/useSetData.js +27 -0
  97. package/package.json +9 -7
@@ -94,12 +94,24 @@
94
94
  description: 'Open link in new tab.'
95
95
  },
96
96
  style: {
97
- type: 'object',
98
- description: 'Css style applied to the link.',
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 to select this menu item. Renders a shortcut badge next to the label. Use "mod" for Cmd/Ctrl.'
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: 'object',
179
- description: 'Css style applied to the link.',
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 shortcut badge next to the label. Use "mod" for Cmd/Ctrl.'
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 { Col, Row } from 'antd';
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
- }))), /*#__PURE__*/ React.createElement(Col, {
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
- */ export default {
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
+ });