@capillarytech/blaze-ui 0.1.4-alpha.16 → 0.1.4-alpha.18

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/CapInput/index.js CHANGED
@@ -1,13 +1,10 @@
1
1
  import CapInput from './CapInput';
2
+ import Number from './Number';
2
3
  import Search from './Search';
3
4
  import TextArea from './TextArea';
4
- import Number from './Number';
5
-
6
- import CapInputLoadable from './loadable';
7
5
 
8
- // Attach subcomponents to the main component
6
+ CapInput.Number = Number;
9
7
  CapInput.Search = Search;
10
8
  CapInput.TextArea = TextArea;
11
- CapInput.Number = Number;
12
9
 
13
- export default CapInputLoadable;
10
+ export default CapInput;
@@ -1,21 +1,28 @@
1
1
  /**
2
2
  * CapTable - Migrated to Ant Design v5
3
+ * A table component that supports:
4
+ * - Infinite scrolling with virtualization
5
+ * - Sequential data loading
6
+ * - Optimized scroll performance
3
7
  */
4
- import React, { useEffect, useCallback, useState } from 'react';
8
+ import React, { useEffect, useCallback, useState, useRef } from 'react';
5
9
  import PropTypes from 'prop-types';
6
- import { throttle } from 'lodash';
10
+ import { debounce } from 'lodash';
7
11
  import classNames from 'classnames';
8
-
9
12
  import { withStyles } from '@capillarytech/vulcan-react-sdk/utils';
10
-
11
13
  import styles, { StyledTable } from './styles';
12
14
  import LocaleHoc from '../LocaleHoc';
13
15
 
16
+ const SCROLL_THRESHOLD = 80; // Percentage of scroll to trigger load
17
+ const DEBOUNCE_DELAY = 250; // ms to wait between scroll events
18
+ const DEFAULT_ROW_HEIGHT = 54;
19
+ const DEFAULT_SCROLL_HEIGHT = 400;
20
+
14
21
  const CapTable = ({
15
22
  id,
16
23
  className,
17
24
  children,
18
- infinteScroll,
25
+ infiniteScroll,
19
26
  pagination,
20
27
  dataSource,
21
28
  offset_limit,
@@ -24,58 +31,99 @@ const CapTable = ({
24
31
  showLoader,
25
32
  ...rest
26
33
  }) => {
27
- const [hasCheckedInitialLoad, setHasCheckedInitialLoad] = useState(false);
34
+ const scrollRef = useRef(null);
35
+ const [hasMore, setHasMore] = useState(true);
36
+ const currentOffsetRef = useRef(0);
37
+
38
+ const loadMore = useCallback(() => {
39
+ if (!showLoader && hasMore) {
40
+ const nextOffset = currentOffsetRef.current + 1;
41
+ const newOffsetLimit = {
42
+ ...offset_limit,
43
+ offset: nextOffset
44
+ };
45
+
46
+ currentOffsetRef.current = nextOffset;
47
+ setPagination(newOffsetLimit);
48
+ }
49
+ }, [showLoader, hasMore, setPagination, offset_limit]);
28
50
 
29
- // Combine scroll check and load more logic
30
- const checkAndLoadMore = useCallback(
31
- throttle((target) => {
32
- if (!infinteScroll || showLoader) return;
51
+ const handleScroll = useCallback(
52
+ debounce((event) => {
53
+ const target = event.target;
54
+ if (!target || !infiniteScroll || !hasMore || showLoader) return;
33
55
 
34
- const { scrollTop, scrollHeight, clientHeight } = target;
35
- const maxScroll = scrollHeight - clientHeight;
36
- const threshold = maxScroll * 0.8; // Load more when 80% scrolled
56
+ const scrollTop = Math.ceil(target.scrollTop);
57
+ const visibleHeight = target.clientHeight;
58
+ const totalHeight = target.scrollHeight;
59
+
60
+ const scrolledPercentage = (scrollTop + visibleHeight) / totalHeight * 100;
37
61
 
38
- if (scrollTop >= threshold) {
39
- setPagination(offset_limit);
62
+ if (scrolledPercentage >= SCROLL_THRESHOLD) {
63
+ loadMore();
40
64
  }
41
- }, 300),
42
- [infinteScroll, showLoader, offset_limit, setPagination],
65
+ }, DEBOUNCE_DELAY),
66
+ [infiniteScroll, showLoader, hasMore, loadMore]
43
67
  );
44
68
 
45
- // Handle scroll event
46
- const onScroll = useCallback(
47
- ({ target }) => {
48
- checkAndLoadMore(target);
49
- },
50
- [checkAndLoadMore],
51
- );
69
+ // Setup scroll listener and handle initial load
70
+ useEffect(() => {
71
+ const tableBody = document.querySelector(`#${id} .ant-table-body`);
72
+ if (!tableBody) return;
73
+
74
+ scrollRef.current = tableBody;
75
+ tableBody.addEventListener('scroll', handleScroll, { passive: true });
76
+
77
+ // Check if initial load needed
78
+ const shouldLoadInitially = tableBody.scrollHeight <= tableBody.clientHeight
79
+ && !showLoader
80
+ && hasMore;
52
81
 
53
- // Combined effect for initial load check and scroll setup
82
+ if (shouldLoadInitially) {
83
+ currentOffsetRef.current = 0;
84
+ loadMore();
85
+ }
86
+
87
+ // Cleanup
88
+ return () => {
89
+ scrollRef.current?.removeEventListener('scroll', handleScroll);
90
+ handleScroll.cancel();
91
+ };
92
+ }, [id, handleScroll, showLoader, hasMore, loadMore]);
93
+
94
+ // Handle data changes
54
95
  useEffect(() => {
55
- if (!hasCheckedInitialLoad && infinteScroll) {
56
- const tableBody = document.querySelector(`#${id} .ant-table-body`);
57
- if (tableBody) {
58
- const { scrollHeight, clientHeight } = tableBody;
59
- if (scrollHeight === clientHeight) {
60
- setPagination(offset_limit);
61
- }
62
- setHasCheckedInitialLoad(true);
63
- }
96
+ if (!dataSource?.length) {
97
+ currentOffsetRef.current = 0;
98
+ setHasMore(true);
99
+ } else {
100
+ setHasMore(true);
64
101
  }
65
- }, [id, infinteScroll, hasCheckedInitialLoad, offset_limit, setPagination]);
102
+ }, [dataSource]);
103
+
104
+ const tableClassName = classNames(
105
+ 'cap-table-v2',
106
+ className,
107
+ {
108
+ 'show-loader': showLoader,
109
+ 'infinite-scroll': infiniteScroll,
110
+ 'has-more': hasMore
111
+ }
112
+ );
66
113
 
67
114
  return (
68
115
  <StyledTable
69
116
  id={id}
70
- className={classNames('cap-table-v2', className, {
71
- 'show-loader': showLoader,
72
- })}
117
+ className={tableClassName}
73
118
  dataSource={dataSource}
74
- pagination={pagination}
75
- scroll={scroll}
76
- onScroll={onScroll}
77
- virtual={infinteScroll} // Enable virtual scrolling
78
- rowHeight={54} // Fixed row height for virtual scrolling
119
+ pagination={false}
120
+ scroll={{
121
+ x: scroll?.x,
122
+ y: scroll?.y || DEFAULT_SCROLL_HEIGHT,
123
+ scrollToFirstRowOnChange: false
124
+ }}
125
+ virtual={infiniteScroll}
126
+ rowHeight={DEFAULT_ROW_HEIGHT}
79
127
  {...rest}
80
128
  >
81
129
  {children}
@@ -84,10 +132,10 @@ const CapTable = ({
84
132
  };
85
133
 
86
134
  CapTable.propTypes = {
87
- id: PropTypes.string,
135
+ id: PropTypes.string.isRequired,
88
136
  className: PropTypes.string,
89
137
  children: PropTypes.node,
90
- infinteScroll: PropTypes.bool,
138
+ infiniteScroll: PropTypes.bool,
91
139
  pagination: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
92
140
  dataSource: PropTypes.array,
93
141
  offset_limit: PropTypes.object,
package/CapTable/index.js CHANGED
@@ -1 +1,3 @@
1
- export { default } from './CapTable';
1
+ import CapTableLoadable from './loadable';
2
+
3
+ export default CapTableLoadable;
@@ -0,0 +1,13 @@
1
+ import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
2
+ import { loadable } from '@capillarytech/cap-ui-utils';
3
+ import React, { Suspense } from 'react';
4
+
5
+ const LoadableComponent = loadable(() => import('./CapTable'));
6
+
7
+ const CapTableLoadable = () => (
8
+ <Suspense fallback={<CapSpin />}>
9
+ <LoadableComponent />
10
+ </Suspense>
11
+ );
12
+
13
+ export default CapTableLoadable;
@@ -0,0 +1,159 @@
1
+ import React, { useMemo, useCallback } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Select, TreeSelect } from 'antd';
4
+ import styled from 'styled-components';
5
+ import { selectStyles } from './styles';
6
+ import { useIntl } from 'react-intl';
7
+ import messages from './messages';
8
+
9
+ const StyledSelect = styled(Select)`${selectStyles}`;
10
+ const StyledTreeSelect = styled(TreeSelect)`${selectStyles}`;
11
+
12
+ const CapUnifiedSelect = ({
13
+ type,
14
+ options,
15
+ treeData,
16
+ value,
17
+ onChange,
18
+ onSearch,
19
+ placeholder,
20
+ disabled,
21
+ loading,
22
+ allowClear,
23
+ enableSearch,
24
+ maxTagCount,
25
+ size,
26
+ status,
27
+ className,
28
+ style,
29
+ rightSlotContent,
30
+ virtualized,
31
+ ...restProps
32
+ }) => {
33
+ const intl = useIntl();
34
+
35
+ const handleChange = useCallback((newValue, option) => {
36
+ if (onChange) {
37
+ onChange(newValue, option);
38
+ }
39
+ }, [onChange]);
40
+
41
+ const handleSearch = useCallback((searchText) => {
42
+ if (onSearch) {
43
+ onSearch(searchText);
44
+ }
45
+ }, [onSearch]);
46
+
47
+ const mergedClassName = useMemo(() => {
48
+ const classes = ['cap-unified-select'];
49
+ if (className) classes.push(className);
50
+ return classes.join(' ');
51
+ }, [className]);
52
+
53
+ // Common props for both Select and TreeSelect
54
+ const commonProps = {
55
+ value,
56
+ onChange: handleChange,
57
+ onSearch: enableSearch ? handleSearch : undefined,
58
+ placeholder: placeholder || intl.formatMessage(messages.selectPlaceholder),
59
+ disabled,
60
+ loading,
61
+ allowClear,
62
+ showSearch: enableSearch,
63
+ maxTagCount,
64
+ size,
65
+ status,
66
+ className: mergedClassName,
67
+ style,
68
+ virtual: virtualized,
69
+ notFoundContent: loading
70
+ ? intl.formatMessage(messages.loading)
71
+ : intl.formatMessage(messages.noData),
72
+ ...restProps
73
+ };
74
+
75
+ // Render right slot content if provided
76
+ const suffixIcon = rightSlotContent && (
77
+ <div className="cap-unified-select-right-slot">
78
+ {rightSlotContent}
79
+ </div>
80
+ );
81
+
82
+ // For tree-based selects
83
+ if (type === 'treeSelect' || type === 'multiTreeSelect') {
84
+ return (
85
+ <StyledTreeSelect
86
+ {...commonProps}
87
+ treeData={treeData}
88
+ multiple={type === 'multiTreeSelect'}
89
+ suffixIcon={suffixIcon}
90
+ />
91
+ );
92
+ }
93
+
94
+ // For regular selects
95
+ return (
96
+ <StyledSelect
97
+ {...commonProps}
98
+ mode={type === 'multiSelect' ? 'multiple' : undefined}
99
+ options={options}
100
+ suffixIcon={suffixIcon}
101
+ />
102
+ );
103
+ };
104
+
105
+ CapUnifiedSelect.propTypes = {
106
+ type: PropTypes.oneOf(['select', 'multiSelect', 'treeSelect', 'multiTreeSelect']).isRequired,
107
+ options: PropTypes.arrayOf(
108
+ PropTypes.shape({
109
+ label: PropTypes.node,
110
+ value: PropTypes.any,
111
+ disabled: PropTypes.bool,
112
+ })
113
+ ),
114
+ treeData: PropTypes.arrayOf(
115
+ PropTypes.shape({
116
+ title: PropTypes.node,
117
+ value: PropTypes.any,
118
+ children: PropTypes.array,
119
+ disabled: PropTypes.bool,
120
+ })
121
+ ),
122
+ value: PropTypes.any,
123
+ onChange: PropTypes.func,
124
+ onSearch: PropTypes.func,
125
+ placeholder: PropTypes.string,
126
+ disabled: PropTypes.bool,
127
+ loading: PropTypes.bool,
128
+ allowClear: PropTypes.bool,
129
+ enableSearch: PropTypes.bool,
130
+ maxTagCount: PropTypes.number,
131
+ size: PropTypes.oneOf(['small', 'middle', 'large']),
132
+ status: PropTypes.oneOf(['error', 'warning']),
133
+ className: PropTypes.string,
134
+ style: PropTypes.object,
135
+ rightSlotContent: PropTypes.node,
136
+ virtualized: PropTypes.bool,
137
+ };
138
+
139
+ CapUnifiedSelect.defaultProps = {
140
+ options: [],
141
+ treeData: [],
142
+ value: undefined,
143
+ onChange: undefined,
144
+ onSearch: undefined,
145
+ placeholder: '',
146
+ disabled: false,
147
+ loading: false,
148
+ allowClear: true,
149
+ enableSearch: false,
150
+ maxTagCount: undefined,
151
+ size: 'middle',
152
+ status: undefined,
153
+ className: '',
154
+ style: {},
155
+ rightSlotContent: null,
156
+ virtualized: true,
157
+ };
158
+
159
+ export default CapUnifiedSelect;
@@ -0,0 +1,4 @@
1
+ import CapUnifiedSelect from './CapUnifiedSelect';
2
+ import CapUnifiedSelectLoadable from './loadable';
3
+
4
+ export default CapUnifiedSelectLoadable;
@@ -0,0 +1,3 @@
1
+ import loadable from '@loadable/component';
2
+
3
+ export default loadable(() => import('./CapUnifiedSelect'));
@@ -0,0 +1,24 @@
1
+ import { defineMessages } from 'react-intl';
2
+
3
+ export default defineMessages({
4
+ selectPlaceholder: {
5
+ id: 'cap.unified.select.placeholder',
6
+ defaultMessage: 'Select an option',
7
+ },
8
+ searchPlaceholder: {
9
+ id: 'cap.unified.select.search.placeholder',
10
+ defaultMessage: 'Search...',
11
+ },
12
+ noData: {
13
+ id: 'cap.unified.select.no.data',
14
+ defaultMessage: 'No data',
15
+ },
16
+ loading: {
17
+ id: 'cap.unified.select.loading',
18
+ defaultMessage: 'Loading...',
19
+ },
20
+ selected: {
21
+ id: 'cap.unified.select.selected',
22
+ defaultMessage: '{count} items selected',
23
+ },
24
+ });
@@ -0,0 +1,182 @@
1
+ import { css } from 'styled-components';
2
+ import * as styledVars from '../styled/variables';
3
+
4
+ export const selectStyles = css`
5
+ &.cap-unified-select {
6
+ width: 100%;
7
+ font-family: ${styledVars.FONT_FAMILY};
8
+
9
+ .ant-select-selector {
10
+ border-radius: ${styledVars.RADIUS_04};
11
+ transition: ${styledVars.TRANSITION_ALL};
12
+ padding: 0 12px;
13
+ min-height: 32px;
14
+ display: flex;
15
+ align-items: center;
16
+
17
+ &:hover {
18
+ border-color: ${styledVars.CAP_G11};
19
+ }
20
+ }
21
+
22
+ /* Right slot content styles */
23
+ .cap-unified-select-right-slot {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 8px;
27
+ margin-left: 8px;
28
+ }
29
+
30
+ &.ant-select-focused {
31
+ .ant-select-selector {
32
+ border-color: ${styledVars.CAP_G01} !important;
33
+ box-shadow: none !important;
34
+ }
35
+ }
36
+
37
+ /* Error state */
38
+ &.ant-select-status-error {
39
+ .ant-select-selector {
40
+ border-color: ${styledVars.CAP_RED};
41
+ }
42
+ }
43
+
44
+ /* Disabled state */
45
+ &.ant-select-disabled {
46
+ .ant-select-selector {
47
+ background-color: ${styledVars.CAP_G08};
48
+ cursor: not-allowed;
49
+ }
50
+ }
51
+
52
+ /* Dropdown styles */
53
+ .ant-select-dropdown {
54
+ padding: 4px;
55
+ border-radius: ${styledVars.RADIUS_04};
56
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
57
+
58
+ .ant-select-item {
59
+ border-radius: ${styledVars.RADIUS_04};
60
+ padding: 8px 12px;
61
+ transition: ${styledVars.TRANSITION_ALL};
62
+
63
+ &-option-selected {
64
+ background-color: ${styledVars.CAP_PRIMARY.light};
65
+ color: ${styledVars.CAP_PRIMARY.base};
66
+ font-weight: 500;
67
+ }
68
+
69
+ &-option-active {
70
+ background-color: ${styledVars.CAP_G08};
71
+ }
72
+ }
73
+
74
+ /* Search input styles */
75
+ .ant-select-search {
76
+ padding: 8px;
77
+
78
+ input {
79
+ border-radius: ${styledVars.RADIUS_04};
80
+ transition: ${styledVars.TRANSITION_ALL};
81
+
82
+ &:focus {
83
+ border-color: ${styledVars.CAP_G01};
84
+ box-shadow: none;
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ /* Multiple selection styles */
91
+ &.ant-select-multiple {
92
+ .ant-select-selection-item {
93
+ background-color: ${styledVars.CAP_PRIMARY.light};
94
+ border-color: ${styledVars.CAP_PRIMARY.light};
95
+ border-radius: ${styledVars.RADIUS_04};
96
+ color: ${styledVars.CAP_PRIMARY.base};
97
+ margin: 2px 4px 2px 0;
98
+ padding: 0 8px;
99
+ height: 24px;
100
+ line-height: 22px;
101
+
102
+ &-remove {
103
+ color: ${styledVars.CAP_PRIMARY.base};
104
+
105
+ &:hover {
106
+ color: ${styledVars.CAP_PRIMARY.hover};
107
+ }
108
+ }
109
+ }
110
+
111
+ .ant-select-selection-overflow {
112
+ flex-wrap: wrap;
113
+ gap: 4px;
114
+ }
115
+ }
116
+
117
+ /* Tree select styles */
118
+ .ant-select-tree {
119
+ padding: 4px 0;
120
+
121
+ .ant-select-tree-node-content-wrapper {
122
+ padding: 4px 8px;
123
+ border-radius: ${styledVars.RADIUS_04};
124
+ transition: ${styledVars.TRANSITION_ALL};
125
+
126
+ &:hover {
127
+ background-color: ${styledVars.CAP_G08};
128
+ }
129
+
130
+ &.ant-select-tree-node-selected {
131
+ background-color: ${styledVars.CAP_PRIMARY.light};
132
+ color: ${styledVars.CAP_PRIMARY.base};
133
+ font-weight: 500;
134
+ }
135
+ }
136
+
137
+ .ant-select-tree-switcher {
138
+ width: 24px;
139
+ height: 24px;
140
+ line-height: 24px;
141
+ }
142
+
143
+ .ant-select-tree-checkbox {
144
+ margin: 4px 8px 4px 0;
145
+ }
146
+ }
147
+
148
+ /* Size variations */
149
+ &.ant-select-lg {
150
+ .ant-select-selector {
151
+ height: 40px;
152
+ padding: 0 16px;
153
+ }
154
+
155
+ .ant-select-selection-item {
156
+ height: 28px;
157
+ line-height: 26px;
158
+ }
159
+ }
160
+
161
+ &.ant-select-sm {
162
+ .ant-select-selector {
163
+ height: 24px;
164
+ padding: 0 8px;
165
+ }
166
+
167
+ .ant-select-selection-item {
168
+ height: 20px;
169
+ line-height: 18px;
170
+ }
171
+ }
172
+
173
+ /* Loading state */
174
+ &.ant-select-loading {
175
+ .ant-select-arrow {
176
+ .anticon-loading {
177
+ color: ${styledVars.CAP_PRIMARY.base};
178
+ }
179
+ }
180
+ }
181
+ }
182
+ `;