@capillarytech/blaze-ui 0.1.6-alpha.15 → 0.1.6-alpha.16

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/.DS_Store CHANGED
Binary file
@@ -1,10 +1,11 @@
1
- // CapUnifiedSelect component using Ant Design v5 Select and TreeSelect directly
2
- import React from 'react';
1
+ // Enhanced CapUnifiedSelect supporting 4 scenarios with advanced features
2
+ import React, { useState, useEffect } from 'react';
3
3
  import PropTypes from 'prop-types';
4
- import { Select, TreeSelect } from 'antd';
5
- import { SelectWrapper, HeaderWrapper, StyledInfoIcon } from './styles';
6
- import CapLabel from '../CapLabel';
7
- import CapTooltip from '../CapTooltip';
4
+ import classnames from 'classnames';
5
+ import { Select, TreeSelect, Tooltip, Input, Button } from 'antd';
6
+ import { InfoCircleOutlined, SearchOutlined, WarningFilled } from '@ant-design/icons';
7
+ import { SelectWrapper, HeaderWrapper, selectStyles } from './styles';
8
+ import withStyles from './withStyles';
8
9
 
9
10
  const CapUnifiedSelect = ({
10
11
  type,
@@ -16,11 +17,23 @@ const CapUnifiedSelect = ({
16
17
  className,
17
18
  style,
18
19
  allowClear = false,
19
- showSearch = false,
20
20
  label,
21
21
  tooltip,
22
22
  disabled = false,
23
+ treeCheckable = false,
24
+ customPopupRender = false,
25
+ onConfirm,
26
+ onCancel
23
27
  }) => {
28
+ const [searchText, setSearchText] = useState('');
29
+ const [tempValue, setTempValue] = useState(value);
30
+ const [allSelected, setAllSelected] = useState(false);
31
+
32
+ // Update tempValue when value changes
33
+ useEffect(() => {
34
+ setTempValue(value);
35
+ }, [value]);
36
+
24
37
  const selectVirtualizationProps = {
25
38
  listHeight: 256,
26
39
  };
@@ -30,66 +43,233 @@ const CapUnifiedSelect = ({
30
43
  listItemHeight: 32,
31
44
  };
32
45
 
46
+ // No results component
47
+ const NoResult = () => (
48
+ <div style={{
49
+ display: 'flex',
50
+ flexDirection: 'column',
51
+ alignItems: 'center',
52
+ justifyContent: 'center',
53
+ height: 200,
54
+ color: '#8c8c8c',
55
+ fontSize: 14
56
+ }}>
57
+ <WarningFilled style={{ fontSize: 36, marginBottom: 8, color: '#bfbfbf' }} />
58
+ <div style={{ fontWeight: 500 }}>No results found</div>
59
+ </div>
60
+ );
61
+
62
+ // Filter tree data based on search text
63
+ const getFilteredTreeData = (data, search) => {
64
+ if (!search) return data;
65
+
66
+ const filterNode = node => {
67
+ const titleText = node.title?.toLowerCase() || '';
68
+ const labelText = node.label?.toLowerCase() || '';
69
+ return titleText.includes(search.toLowerCase()) || labelText.includes(search.toLowerCase());
70
+ };
71
+
72
+ const loop = data =>
73
+ data.map(item => {
74
+ const children = item.children ? loop(item.children) : [];
75
+ if (filterNode(item) || children.length) {
76
+ return { ...item, children };
77
+ }
78
+ return null;
79
+ }).filter(Boolean);
80
+
81
+ return loop(data);
82
+ };
83
+
84
+ // Get all leaf node values from tree data
85
+ const flattenedLeafValues = (nodes) =>
86
+ nodes?.flatMap((node) =>
87
+ node.children ? flattenedLeafValues(node.children) : [node.value]
88
+ ) || [];
89
+
90
+ const filteredTree = getFilteredTreeData(treeData || options, searchText);
91
+ const filteredOptions = !searchText ? options : options.filter(opt =>
92
+ opt.label?.toLowerCase().includes(searchText.toLowerCase())
93
+ );
94
+
95
+ // All available leaf values for the current filtered tree data
96
+ const leafValues = flattenedLeafValues(filteredTree);
97
+
98
+ // Handles select all checkbox click
99
+ const handleSelectAll = (isTree = false) => {
100
+ const availableValues = isTree ? leafValues : filteredOptions.map(opt => opt.value);
101
+
102
+ if (allSelected) {
103
+ setTempValue([]);
104
+ } else {
105
+ setTempValue(availableValues);
106
+ }
107
+
108
+ setAllSelected(!allSelected);
109
+ };
110
+
111
+ // Update allSelected state when value changes
112
+ useEffect(() => {
113
+ const isMultipleMode = type === 'multiSelect' || type === 'multiTreeSelect';
114
+ if (isMultipleMode && Array.isArray(tempValue)) {
115
+ const availableValues = type.includes('tree') ? leafValues : filteredOptions.map(opt => opt.value);
116
+ setAllSelected(tempValue.length > 0 && tempValue.length === availableValues.length);
117
+ }
118
+ }, [tempValue, filteredOptions, leafValues, type]);
119
+
120
+ // Handle confirmation
121
+ const handleConfirm = () => {
122
+ if (onConfirm) {
123
+ onConfirm(tempValue);
124
+ } else {
125
+ onChange(tempValue);
126
+ }
127
+ setSearchText('');
128
+ };
129
+
130
+ // Handle cancellation
131
+ const handleCancel = () => {
132
+ if (onCancel) {
133
+ onCancel();
134
+ }
135
+ setTempValue(value);
136
+ setSearchText('');
137
+ };
138
+
139
+ // Handle temporary value change
140
+ const handleTempChange = (newValue) => {
141
+ setTempValue(newValue);
142
+ };
143
+
33
144
  const renderHeader = () => {
34
145
  if (!label && !tooltip) return null;
35
-
36
146
  return (
37
147
  <HeaderWrapper className={disabled ? 'disabled' : ''}>
38
- {label && (
39
- <CapLabel type="label16" className={disabled ? 'disabled' : ''}>
40
- {label}
41
- </CapLabel>
42
- )}
148
+ {label && <label type="label16" className={disabled ? 'disabled' : ''}>{label}</label>}
43
149
  {tooltip && (
44
- <CapTooltip title={tooltip}>
45
- <StyledInfoIcon className={disabled ? 'disabled' : ''} />
46
- </CapTooltip>
150
+ <Tooltip title={tooltip} style={{ color: '#B3BAC5' }}>
151
+ <InfoCircleOutlined className={disabled ? 'disabled' : ''} />
152
+ </Tooltip>
47
153
  )}
48
154
  </HeaderWrapper>
49
155
  );
50
156
  };
51
157
 
52
158
  const renderDropdown = () => {
159
+ const isMultipleSelect = type === 'multiSelect' || type === 'multiTreeSelect';
160
+ const isTreeMode = type === 'treeSelect' || type === 'multiTreeSelect';
161
+ const currentItems = isTreeMode ? filteredTree : filteredOptions;
162
+ const selectedCount = Array.isArray(tempValue) ? tempValue.length : (tempValue ? 1 : 0);
163
+
164
+ // Custom dropdown render for both Select and TreeSelect
165
+ const renderCustomDropdown = (menu) => {
166
+ if (!customPopupRender) return menu;
167
+
168
+ return (
169
+ <>
170
+ <div style={{ borderBottom: '1px solid #f0f0f0', padding: '8px' }}>
171
+ <Input
172
+ prefix={<SearchOutlined style={{ color: "#B3BAC5" }} />}
173
+ placeholder="Search"
174
+ variant="borderless"
175
+ value={searchText}
176
+ onChange={(e) => setSearchText(e.target.value)}
177
+ onKeyDown={(e) => e.stopPropagation()}
178
+ />
179
+ </div>
180
+
181
+ {isMultipleSelect && currentItems.length > 0 && (
182
+ <div style={{
183
+ padding: '8px 12px',
184
+ cursor: 'pointer',
185
+ display: 'flex',
186
+ alignItems: 'center',
187
+ borderBottom: '1px solid #f0f0f0'
188
+ }}
189
+ onClick={() => handleSelectAll(isTreeMode)}>
190
+ <input
191
+ type="checkbox"
192
+ checked={allSelected}
193
+ readOnly
194
+ style={{ cursor: 'pointer' }}
195
+ />
196
+ <label style={{ marginLeft: 8, cursor: 'pointer' }}>Select all</label>
197
+ </div>
198
+ )}
199
+
200
+ {currentItems.length === 0 ? <NoResult /> : menu}
201
+
202
+ {currentItems.length > 0 && isMultipleSelect && (
203
+ <div style={{
204
+ display: 'flex',
205
+ justifyContent: 'space-between',
206
+ alignItems: 'center',
207
+ padding: '8px 12px',
208
+ borderTop: '1px solid #f0f0f0'
209
+ }}>
210
+ <div>
211
+ <Button type="primary" size="small" style={{ marginRight: 8 }} onClick={handleConfirm}>
212
+ Confirm
213
+ </Button>
214
+ <Button type="text" size="small" onClick={handleCancel}>
215
+ Cancel
216
+ </Button>
217
+ </div>
218
+ {selectedCount > 0 && (
219
+ <span style={{ color: '#8c8c8c', fontSize: '14px' }}>
220
+ {selectedCount} selected
221
+ </span>
222
+ )}
223
+ </div>
224
+ )}
225
+ </>
226
+ );
227
+ };
228
+
53
229
  if (type === 'treeSelect' || type === 'multiTreeSelect') {
54
230
  return (
55
231
  <TreeSelect
56
- treeData={treeData || options}
57
- value={value}
58
- onChange={onChange}
232
+ treeData={filteredTree}
233
+ value={customPopupRender ? tempValue : value}
234
+ onChange={customPopupRender ? handleTempChange : onChange}
59
235
  placeholder={placeholder}
60
- className={className}
236
+ className={classnames(`cap-unified-tree-select ${className || ''}`)}
61
237
  style={style}
62
238
  allowClear={allowClear}
63
- showSearch={showSearch}
64
- multiple={type === 'multiTreeSelect' ? true : false}
239
+ showSearch={false}
240
+ multiple={type === 'multiTreeSelect'}
241
+ treeCheckable={treeCheckable}
242
+ showCheckedStrategy={TreeSelect.SHOW_PARENT}
65
243
  virtual
66
- treeDefaultExpandAll
67
244
  disabled={disabled}
245
+ filterTreeNode={false}
68
246
  {...treeSelectVirtualizationProps}
247
+ popupRender={renderCustomDropdown}
69
248
  />
70
249
  );
71
250
  }
72
251
 
73
252
  return (
74
253
  <Select
75
- value={value}
76
- onChange={onChange}
254
+ options={filteredOptions}
255
+ value={customPopupRender ? tempValue : value}
256
+ onChange={customPopupRender ? handleTempChange : onChange}
77
257
  placeholder={placeholder}
78
- className={className}
258
+ className={classnames(`cap-unified-select ${className || ''}`)}
79
259
  style={style}
80
260
  allowClear={allowClear}
81
- showSearch={showSearch}
82
- options={options}
261
+ showSearch={false}
83
262
  mode={type === 'multiSelect' ? 'multiple' : undefined}
84
263
  virtual
85
264
  disabled={disabled}
86
265
  {...selectVirtualizationProps}
266
+ dropdownRender={renderCustomDropdown}
87
267
  />
88
268
  );
89
269
  };
90
270
 
91
271
  return (
92
- <SelectWrapper>
272
+ <SelectWrapper className={classnames(`cap-unified-select-container ${className || ''}`)}>
93
273
  {renderHeader()}
94
274
  {renderDropdown()}
95
275
  </SelectWrapper>
@@ -106,16 +286,20 @@ CapUnifiedSelect.propTypes = {
106
286
  className: PropTypes.string,
107
287
  style: PropTypes.object,
108
288
  allowClear: PropTypes.bool,
109
- showSearch: PropTypes.bool,
110
289
  label: PropTypes.string,
111
290
  tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
112
291
  disabled: PropTypes.bool,
292
+ treeCheckable: PropTypes.bool,
293
+ customPopupRender: PropTypes.bool,
294
+ onConfirm: PropTypes.func,
295
+ onCancel: PropTypes.func,
113
296
  };
114
297
 
115
298
  CapUnifiedSelect.defaultProps = {
116
299
  type: 'select',
117
300
  allowClear: false,
118
- showSearch: false,
301
+ customPopupRender: false,
302
+ treeCheckable: false,
119
303
  };
120
304
 
121
- export default CapUnifiedSelect;
305
+ export default withStyles(CapUnifiedSelect, selectStyles);
@@ -23,21 +23,72 @@ export const HeaderWrapper = styled.div`
23
23
  }
24
24
  `;
25
25
 
26
+ export const DropdownFooter = styled.div`
27
+ display: flex;
28
+ justify-content: space-between;
29
+ align-items: center;
30
+ padding: 8px;
31
+ border-top: 1px solid #f0f0f0;
32
+
33
+ .footer-buttons {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 8px;
37
+ }
38
+
39
+ .total-count {
40
+ color: #8c8c8c;
41
+ font-size: 12px;
42
+ }
43
+ `;
44
+
45
+ export const SearchInputWrapper = styled.div`
46
+ padding: 8px;
47
+ border-bottom: 1px solid #f0f0f0;
48
+ `;
49
+
50
+ export const SelectAllCheckbox = styled.div`
51
+ display: flex;
52
+ align-items: center;
53
+ padding: 8px 12px;
54
+ cursor: pointer;
55
+ font-weight: 500;
56
+ border-bottom: 1px solid #f0f0f0;
57
+ user-select: none;
58
+
59
+ input[type="checkbox"] {
60
+ cursor: pointer;
61
+ }
62
+ `;
63
+
64
+ export const NoResultWrapper = styled.div`
65
+ display: flex;
66
+ flex-direction: column;
67
+ align-items: center;
68
+ justify-content: center;
69
+ height: 200px;
70
+ color: #8c8c8c;
71
+ font-size: 14px;
72
+ `;
73
+
26
74
 
27
75
 
28
76
  export const StyledInfoIcon = styled.span`
29
- color: ${styledVars.CAP_G2};
77
+ color: ${styledVars.CAP_G05};
30
78
  font-size: 16px;
31
79
  cursor: help;
80
+ margin-left: 4px;
81
+ display: flex;
82
+ align-items: center;
32
83
 
33
84
  &:hover {
34
- color: ${styledVars.CAP_G1};
85
+ color: ${styledVars.CAP_G03};
35
86
  }
36
87
 
37
88
  &.disabled {
38
89
  cursor: not-allowed;
39
90
  &:hover {
40
- color: ${styledVars.CAP_G2};
91
+ color: ${styledVars.CAP_G05};
41
92
  }
42
93
  }
43
94
  `;
@@ -91,30 +142,47 @@ export const selectStyles = css`
91
142
  }
92
143
 
93
144
  /* Dropdown styles */
94
- .ant-select-dropdown {
95
- padding: 4px;
96
- border-radius: ${styledVars.RADIUS_04};
97
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
98
-
99
- .ant-select-item {
100
- border-radius: ${styledVars.RADIUS_04};
101
- padding: 8px 12px;
102
- transition: ${styledVars.TRANSITION_ALL};
145
+ .ant-select-dropdown,
146
+ &.ant-select-dropdown,
147
+ &.ant-select-dropdown-placement-bottomLeft {
148
+ padding: 0;
149
+ border-radius: 12px !important;
150
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
151
+ overflow: hidden;
152
+
153
+ /* Option base style - no background */
154
+ .ant-select-item-option {
155
+ padding: 10px 16px;
156
+ font-size: 14px;
157
+ color: #1c2530;
158
+ font-weight: 500;
159
+ background-color: transparent !important;
160
+ transition: background 0.3s;
161
+
162
+ /* Hover state only */
163
+ &:not(.ant-select-item-option-disabled):hover {
164
+ background-color: #fffbe6 !important;
165
+ border-radius: 4px;
166
+ color: #1c2530 !important;
167
+ }
103
168
 
104
- &-option-selected {
105
- background-color: ${styledVars.CAP_PRIMARY.light};
169
+ /* Selected state */
170
+ &-selected {
106
171
  color: ${styledVars.CAP_PRIMARY.base};
107
172
  font-weight: 500;
108
173
  }
109
174
 
110
- &-option-active {
111
- background-color: ${styledVars.CAP_G08};
175
+ /* Remove active state background unless hovered */
176
+ &-active:not(:hover):not(.ant-select-item-option-disabled) {
177
+ background-color: transparent !important;
112
178
  }
113
179
  }
114
180
 
115
181
  /* Search input styles */
182
+ .ant-select-dropdown-search,
116
183
  .ant-select-search {
117
- padding: 8px;
184
+ padding: 8px 12px;
185
+ border-bottom: 1px solid #f0f0f0;
118
186
 
119
187
  input {
120
188
  border-radius: ${styledVars.RADIUS_04};
@@ -126,6 +194,64 @@ export const selectStyles = css`
126
194
  }
127
195
  }
128
196
  }
197
+
198
+ /* Scrollbar */
199
+ .rc-virtual-list-scrollbar-thumb {
200
+ background-color: #dcdcdc;
201
+ border-radius: 4px;
202
+ }
203
+
204
+ /* Divider */
205
+ .ant-divider-horizontal {
206
+ margin: 0;
207
+ }
208
+
209
+ /* No result UI */
210
+ .no-result {
211
+ display: flex;
212
+ flex-direction: column;
213
+ align-items: center;
214
+ justify-content: center;
215
+ height: 200px;
216
+ color: #8c8c8c;
217
+ font-size: 14px;
218
+ }
219
+
220
+ /* Dropdown search inside custom popup */
221
+ .dropdown-search {
222
+ padding: 8px;
223
+ border-bottom: 1px solid #f0f0f0;
224
+ }
225
+
226
+ /* Select all checkbox */
227
+ .select-all-checkbox {
228
+ display: flex;
229
+ align-items: center;
230
+ padding: 8px 12px;
231
+ cursor: pointer;
232
+ font-weight: 500;
233
+ border-bottom: 1px solid #f0f0f0;
234
+ user-select: none;
235
+
236
+ input[type="checkbox"] {
237
+ cursor: pointer;
238
+ }
239
+ }
240
+
241
+ /* Footer buttons */
242
+ .dropdown-footer {
243
+ display: flex;
244
+ justify-content: space-between;
245
+ align-items: center;
246
+ padding: 8px;
247
+ border-top: 1px solid #f0f0f0;
248
+ }
249
+
250
+ /* Selected counter */
251
+ .selected-count {
252
+ color: #8c8c8c;
253
+ font-size: 12px;
254
+ }
129
255
  }
130
256
 
131
257
  /* Multiple selection styles */
@@ -162,14 +288,16 @@ export const selectStyles = css`
162
288
  .ant-select-tree-node-content-wrapper {
163
289
  padding: 4px 8px;
164
290
  border-radius: ${styledVars.RADIUS_04};
165
- transition: ${styledVars.TRANSITION_ALL};
291
+ transition: background 0.3s;
292
+ background-color: transparent !important;
166
293
 
167
294
  &:hover {
168
- background-color: ${styledVars.CAP_G08};
295
+ background-color: #fffbe6 !important;
296
+ color: #1c2530 !important;
297
+ border-radius: 4px;
169
298
  }
170
299
 
171
300
  &.ant-select-tree-node-selected {
172
- background-color: ${styledVars.CAP_PRIMARY.light};
173
301
  color: ${styledVars.CAP_PRIMARY.base};
174
302
  font-weight: 500;
175
303
  }
@@ -184,6 +312,19 @@ export const selectStyles = css`
184
312
  .ant-select-tree-checkbox {
185
313
  margin: 4px 8px 4px 0;
186
314
  }
315
+
316
+ .ant-select-tree-treenode {
317
+ padding: 2px 0;
318
+
319
+ &:hover {
320
+ background-color: transparent;
321
+ }
322
+ }
323
+
324
+ .ant-select-tree-checkbox-checked .ant-select-tree-checkbox-inner {
325
+ background-color: ${styledVars.CAP_PRIMARY.base};
326
+ border-color: ${styledVars.CAP_PRIMARY.base};
327
+ }
187
328
  }
188
329
 
189
330
  /* Size variations */