@capillarytech/blaze-ui 0.1.4-alpha.6 → 0.1.5

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,11 +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
5
 
6
- // Attach subcomponents to the main component
6
+ CapInput.Number = Number;
7
7
  CapInput.Search = Search;
8
8
  CapInput.TextArea = TextArea;
9
- CapInput.Number = Number;
10
9
 
11
- export default CapInput;
10
+ export default CapInput;
@@ -4,6 +4,6 @@
4
4
 
5
5
  import { loadable } from '@capillarytech/cap-ui-utils';
6
6
 
7
- const CapInputLoadable = loadable(() => import('./index'));
7
+ const CapInputLoadable = loadable(() => import('./CapInput'));
8
8
 
9
9
  export default CapInputLoadable;
@@ -1,44 +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, useRef, useCallback } from 'react';
8
+ import React, { useEffect, useCallback, useState, useRef } from 'react';
5
9
  import PropTypes from 'prop-types';
6
- import { throttle } from 'lodash';
7
- import { Table } from 'antd';
8
- import styled from 'styled-components';
10
+ import { debounce } from 'lodash';
9
11
  import classNames from 'classnames';
10
- import { CAP_G07 } from '../styled/variables';
12
+ import { withStyles } from '@capillarytech/vulcan-react-sdk/utils';
13
+ import styles, { StyledTable } from './styles';
11
14
  import LocaleHoc from '../LocaleHoc';
12
15
 
13
- const StyledTable = styled(Table)`
14
- &.cap-table-v2 {
15
- .ant-table {
16
- border: 1px solid ${CAP_G07};
17
- }
18
- }
19
-
20
- &.show-loader {
21
- .ant-table-body > table > tbody::after {
22
- content: '${(props) => props.loadMoreData}';
23
- display: flex;
24
- justify-content: center;
25
- position: absolute;
26
- width: 100%;
27
- align-items: center;
28
- height: 60px;
29
- text-align: center;
30
- font-size: 16px;
31
- color: gray;
32
- border-top: 1px solid ${CAP_G07};
33
- }
34
- }
35
- `;
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;
36
20
 
37
21
  const CapTable = ({
38
22
  id,
39
23
  className,
40
24
  children,
41
- infinteScroll,
25
+ infiniteScroll,
42
26
  pagination,
43
27
  dataSource,
44
28
  offset_limit,
@@ -47,91 +31,103 @@ const CapTable = ({
47
31
  showLoader,
48
32
  ...rest
49
33
  }) => {
50
- const setPaginationCalled = useRef(false);
51
- const onScrollThrottle = useRef(
52
- throttle((event) => {
53
- const maxScroll = event.target.scrollHeight - event.target.clientHeight;
54
- const currentScroll = event.target.scrollTop;
55
-
56
- if (currentScroll >= maxScroll - 10 && !showLoader) {
57
- const offsetLimit = { ...offset_limit };
58
- offsetLimit.offset += offsetLimit.limit;
59
- setPagination?.(offsetLimit);
60
- }
61
- }, 50),
62
- ).current;
34
+ const scrollRef = useRef(null);
35
+ const [hasMore, setHasMore] = useState(true);
36
+ const currentOffsetRef = useRef(0);
63
37
 
64
- const addScrollEventListener = useCallback(() => {
65
- const listTable = document.querySelector(`#${id} div.ant-table-body`);
66
- if (listTable) {
67
- listTable.addEventListener('scroll', onScrollThrottle);
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);
68
48
  }
69
- }, [id, onScrollThrottle]);
49
+ }, [showLoader, hasMore, setPagination, offset_limit]);
70
50
 
71
- const callSetPaginationIfNotOverflow = useCallback(() => {
72
- const yscrollheight = scroll?.y || 0;
73
- const listTable = document.querySelector(`#${id} div.ant-table-body`);
74
- let isOverflow = false;
51
+ const handleScroll = useCallback(
52
+ debounce((event) => {
53
+ const target = event.target;
54
+ if (!target || !infiniteScroll || !hasMore || showLoader) return;
75
55
 
76
- if (yscrollheight && yscrollheight < listTable?.scrollHeight) {
77
- isOverflow = true;
78
- }
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;
79
61
 
80
- if (!isOverflow && setPagination && dataSource?.length > 0) {
81
- setPaginationCalled.current = true;
82
- const offsetLimit = { ...offset_limit };
83
- offsetLimit.offset += offsetLimit.limit;
84
- setPagination(offsetLimit);
85
- }
86
- }, [id, scroll?.y, setPagination, dataSource, offset_limit]);
62
+ if (scrolledPercentage >= SCROLL_THRESHOLD) {
63
+ loadMore();
64
+ }
65
+ }, DEBOUNCE_DELAY),
66
+ [infiniteScroll, showLoader, hasMore, loadMore]
67
+ );
87
68
 
69
+ // Setup scroll listener and handle initial load
88
70
  useEffect(() => {
89
- const limit = offset_limit?.limit || 10;
90
- if (infinteScroll) {
91
- addScrollEventListener();
92
- if (dataSource?.length >= limit) {
93
- callSetPaginationIfNotOverflow();
94
- }
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;
81
+
82
+ if (shouldLoadInitially) {
83
+ currentOffsetRef.current = 0;
84
+ loadMore();
95
85
  }
96
86
 
87
+ // Cleanup
97
88
  return () => {
98
- const listTable = document.querySelector(`#${id} div.ant-table-body`);
99
- if (listTable) {
100
- listTable.removeEventListener('scroll', onScrollThrottle);
101
- }
89
+ scrollRef.current?.removeEventListener('scroll', handleScroll);
90
+ handleScroll.cancel();
102
91
  };
103
- }, [
104
- infinteScroll,
105
- dataSource,
106
- offset_limit,
107
- addScrollEventListener,
108
- callSetPaginationIfNotOverflow,
109
- id,
110
- onScrollThrottle,
111
- ]);
92
+ }, [id, handleScroll, showLoader, hasMore, loadMore]);
112
93
 
94
+ // Handle data changes
113
95
  useEffect(() => {
114
- const limit = offset_limit?.limit || 10;
115
- if (
116
- !setPaginationCalled.current &&
117
- infinteScroll &&
118
- dataSource?.length >= limit
119
- ) {
120
- callSetPaginationIfNotOverflow();
96
+ if (!dataSource?.length) {
97
+ currentOffsetRef.current = 0;
98
+ setHasMore(true);
99
+ } else {
100
+ setHasMore(true);
101
+ }
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
121
111
  }
122
- }, [infinteScroll, dataSource, offset_limit, callSetPaginationIfNotOverflow]);
112
+ );
123
113
 
124
114
  return (
125
115
  <StyledTable
126
- {...rest}
127
116
  id={id}
117
+ className={tableClassName}
128
118
  dataSource={dataSource}
129
- scroll={scroll}
130
- pagination={infinteScroll ? false : pagination}
131
- className={classNames('cap-table-v2', className, {
132
- 'show-loader': showLoader,
133
- })}
134
- />
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}
127
+ {...rest}
128
+ >
129
+ {children}
130
+ </StyledTable>
135
131
  );
136
132
  };
137
133
 
@@ -139,25 +135,13 @@ CapTable.propTypes = {
139
135
  id: PropTypes.string.isRequired,
140
136
  className: PropTypes.string,
141
137
  children: PropTypes.node,
142
- infinteScroll: PropTypes.bool,
143
- pagination: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
138
+ infiniteScroll: PropTypes.bool,
139
+ pagination: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
144
140
  dataSource: PropTypes.array,
145
- offset_limit: PropTypes.shape({
146
- offset: PropTypes.number,
147
- limit: PropTypes.number,
148
- }),
141
+ offset_limit: PropTypes.object,
149
142
  setPagination: PropTypes.func,
150
143
  scroll: PropTypes.object,
151
144
  showLoader: PropTypes.bool,
152
145
  };
153
146
 
154
- CapTable.defaultProps = {
155
- infinteScroll: false,
156
- showLoader: false,
157
- offset_limit: {
158
- offset: 0,
159
- limit: 10,
160
- },
161
- };
162
-
163
- export default LocaleHoc(CapTable, { key: 'CapTable' });
147
+ export default LocaleHoc(withStyles(CapTable, styles), { key: 'CapTable' });
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;
@@ -1,25 +1,61 @@
1
- import { css } from 'styled-components';
1
+ import { Table } from 'antd';
2
+ import styled, { css } from 'styled-components';
3
+
2
4
  import * as styledVars from '../styled/variables';
3
5
 
4
- export const tableStyles = css`
6
+ const {
7
+ CAP_G09,
8
+ CAP_G01,
9
+ CAP_G06,
10
+ CAP_G07,
11
+ CAP_G10,
12
+ SPACING_16,
13
+ SPACING_24,
14
+ FONT_SIZE_S,
15
+ } = styledVars;
16
+
17
+ export const StyledTable = styled(Table)`
18
+ &.cap-table-v2 {
19
+ .ant-table {
20
+ border: 1px solid ${CAP_G07};
21
+ }
22
+ }
23
+
24
+ &.show-loader {
25
+ .ant-table-body > table > tbody::after {
26
+ content: '${(props) => props.loadMoreData}';
27
+ display: flex;
28
+ justify-content: center;
29
+ position: absolute;
30
+ width: 100%;
31
+ align-items: center;
32
+ height: 60px;
33
+ text-align: center;
34
+ font-size: 16px;
35
+ color: gray;
36
+ border-top: 1px solid ${CAP_G07};
37
+ }
38
+ }
39
+ `;
40
+ export default css`
5
41
  .ant-table {
6
- border: 1px solid ${styledVars.CAP_G07};
42
+ border: 1px solid ${CAP_G07};
7
43
 
8
44
  .ant-table-thead > tr > th {
9
- color: ${styledVars.CAP_G01};
10
- font-size: ${styledVars.FONT_SIZE_S};
11
- line-height: ${styledVars.SPACING_16};
12
- background-color: ${styledVars.CAP_G10};
45
+ color: ${CAP_G01};
46
+ font-size: ${FONT_SIZE_S};
47
+ line-height: ${SPACING_16};
48
+ background-color: ${CAP_G10};
13
49
  text-align: left;
14
50
  }
15
51
 
16
52
  .ant-table-thead > tr > th,
17
53
  .ant-table-tbody > tr > td {
18
- padding: ${styledVars.SPACING_16} ${styledVars.SPACING_24};
54
+ padding: ${SPACING_16} ${SPACING_24};
19
55
  }
20
56
 
21
57
  .ant-table-tbody > tr > td {
22
- border-bottom: 1px solid ${styledVars.CAP_G07};
58
+ border-bottom: 1px solid ${CAP_G07};
23
59
  }
24
60
 
25
61
  .ant-table-tbody > tr:last-child > td {
@@ -30,37 +66,37 @@ export const tableStyles = css`
30
66
  .ant-table-tbody > tr.ant-table-row-hover:not(.ant-table-expanded-row) > td,
31
67
  .ant-table-thead > tr:hover:not(.ant-table-expanded-row) > td,
32
68
  .ant-table-tbody > tr:hover:not(.ant-table-expanded-row) > td {
33
- background-color: ${styledVars.CAP_G09};
69
+ background-color: ${CAP_G09};
34
70
  }
35
71
 
36
72
  .ant-table-thead > tr > th .ant-table-column-sorter-up,
37
73
  .ant-table-thead > tr > th .ant-table-column-sorter-down {
38
74
  &.active {
39
- color: ${styledVars.CAP_G01};
75
+ color: ${CAP_G01};
40
76
  }
41
77
  &:not(.active) {
42
- color: ${styledVars.CAP_G06};
78
+ color: ${CAP_G06};
43
79
  }
44
80
  }
45
81
 
46
82
  .ant-table-thead {
47
83
  .table-children {
48
- padding: 6px ${styledVars.SPACING_24} 16px;
84
+ padding: 6px ${SPACING_24} 16px;
49
85
  position: relative;
50
86
  }
51
87
 
52
88
  .table-parent {
53
89
  padding-bottom: 0;
54
- padding-left: ${styledVars.SPACING_24};
90
+ padding-left: ${SPACING_24};
55
91
  }
56
92
 
57
93
  .table-children.show-separator:not(:last-child)::after {
58
- content: "";
94
+ content: '';
59
95
  height: 8px;
60
96
  width: 1px;
61
97
  right: 0;
62
98
  top: 30%;
63
- background-color: ${styledVars.CAP_G07};
99
+ background-color: ${CAP_G07};
64
100
  position: absolute;
65
101
  }
66
102
  }
@@ -85,8 +121,12 @@ export const tableStyles = css`
85
121
 
86
122
  &.hide-hover {
87
123
  .ant-table {
88
- .ant-table-thead > tr.ant-table-row-hover:not(.ant-table-expanded-row) > td,
89
- .ant-table-tbody > tr.ant-table-row-hover:not(.ant-table-expanded-row) > td,
124
+ .ant-table-thead
125
+ > tr.ant-table-row-hover:not(.ant-table-expanded-row)
126
+ > td,
127
+ .ant-table-tbody
128
+ > tr.ant-table-row-hover:not(.ant-table-expanded-row)
129
+ > td,
90
130
  .ant-table-thead > tr:hover:not(.ant-table-expanded-row) > td,
91
131
  .ant-table-tbody > tr:hover:not(.ant-table-expanded-row) > td {
92
132
  background: none;
@@ -95,6 +135,6 @@ export const tableStyles = css`
95
135
  }
96
136
 
97
137
  a {
98
- color: ${styledVars.CAP_G01};
138
+ color: ${CAP_G01};
99
139
  }
100
- `;
140
+ `;
@@ -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
+ });