@dhis2-ui/transfer 10.10.2 → 10.12.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/build/cjs/__e2e__/notify_at_end_of_list.e2e.stories.js +46 -3
- package/build/cjs/end-intersection-detector.js +5 -4
- package/build/cjs/features/notify_at_end_of_list/index.js +35 -0
- package/build/cjs/features/notify_at_end_of_list.feature +35 -0
- package/build/cjs/options-container.js +14 -8
- package/build/cjs/transfer/index.js +11 -0
- package/build/cjs/transfer/use-options-key-monitor.js +42 -0
- package/build/cjs/transfer.js +41 -14
- package/build/cjs/transfer.prod.stories.js +25 -37
- package/build/es/__e2e__/notify_at_end_of_list.e2e.stories.js +42 -1
- package/build/es/end-intersection-detector.js +4 -3
- package/build/es/features/notify_at_end_of_list/index.js +35 -0
- package/build/es/features/notify_at_end_of_list.feature +35 -0
- package/build/es/options-container.js +14 -8
- package/build/es/transfer/index.js +2 -1
- package/build/es/transfer/use-options-key-monitor.js +35 -0
- package/build/es/transfer.js +39 -14
- package/build/es/transfer.prod.stories.js +26 -38
- package/package.json +7 -7
- package/types/index.d.ts +6 -0
- package/build/cjs/picked-options.js +0 -44
- package/build/cjs/source-options.js +0 -44
- package/build/cjs/use-resize-counter.js +0 -36
- package/build/es/picked-options.js +0 -36
- package/build/es/source-options.js +0 -36
- package/build/es/use-resize-counter.js +0 -30
|
@@ -11,6 +11,10 @@ Given('the Transfer does not have enough items to fill the source list completel
|
|
|
11
11
|
cy.visitStory('Transfer End Of List', 'Partial Source List');
|
|
12
12
|
cy.wrap('source').as('listType');
|
|
13
13
|
});
|
|
14
|
+
Given('the Transfer source options list does not fill the list completely', () => {
|
|
15
|
+
cy.visitStory('Transfer End Of List', 'Option Changes For Short List');
|
|
16
|
+
cy.wrap('source').as('listType');
|
|
17
|
+
});
|
|
14
18
|
Given('the Transfer does not have enough items to fill the picked list completely', () => {
|
|
15
19
|
cy.visitStory('Transfer End Of List', 'Partial Picked List');
|
|
16
20
|
cy.wrap('picked').as('listType');
|
|
@@ -21,6 +25,12 @@ When('the user scroll to the end of the list', () => {
|
|
|
21
25
|
cy.get(`{${listSelector}-endintersectiondetector}`).scrollIntoView();
|
|
22
26
|
});
|
|
23
27
|
});
|
|
28
|
+
When('the user adds an item by clicking the button', () => {
|
|
29
|
+
cy.contains('Increment options lists').click();
|
|
30
|
+
});
|
|
31
|
+
Then('the last list item should be fully visible', () => {
|
|
32
|
+
cy.contains('ARI treated with antibiotics (pneumonia) new').should('be.visible');
|
|
33
|
+
});
|
|
24
34
|
Then('the callback for reaching the end should not be called', () => {
|
|
25
35
|
cy.all(() => cy.window(), () => cy.get('@listType')).should(_ref => {
|
|
26
36
|
let [win, listType] = _ref;
|
|
@@ -34,4 +44,29 @@ Then('the callback for reaching the end should be called', () => {
|
|
|
34
44
|
const callback = listType === 'source' ? win.onEndReached : win.onEndReachedPicked;
|
|
35
45
|
expect(callback).to.be.calledOnce;
|
|
36
46
|
});
|
|
47
|
+
});
|
|
48
|
+
When('the user scrolls down to the source list end', () => {
|
|
49
|
+
cy.get('[data-test="dhis2-uicore-transfer-sourceoptions"]').find('[data-test="dhis2-uicore-intersectiondetector"]').scrollIntoView();
|
|
50
|
+
});
|
|
51
|
+
Then('the list end indicator of the source list should be visible', () => {
|
|
52
|
+
cy.get('[data-test="dhis2-uicore-transfer-sourceoptions"]').find('[data-test="dhis2-uicore-intersectiondetector"]').should('be.visible');
|
|
53
|
+
});
|
|
54
|
+
Then('the list end indicator of the source list should not be visible', () => {
|
|
55
|
+
cy.get('[data-test="dhis2-uicore-transfer-sourceoptions"]').find('[data-test="dhis2-uicore-intersectiondetector"]').should('not.be.visible');
|
|
56
|
+
});
|
|
57
|
+
Then('the callback for reaching the end of the source list should be called {int} times', function (int) {
|
|
58
|
+
cy.window().should(win => {
|
|
59
|
+
expect(win.onEndReached).to.have.callCount(int);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
Then('the selected item is being displayed in the picked list', () => {
|
|
63
|
+
cy.get('[data-test="dhis2-uicore-transfer-pickedoptions"]').contains('Option nr. 9').should('be.visible');
|
|
64
|
+
});
|
|
65
|
+
When('the user selects option nr. {}', function (int) {
|
|
66
|
+
cy.contains(`Option nr. ${int}`).dblclick();
|
|
67
|
+
});
|
|
68
|
+
Then('the callback for reaching the end of the picked list should be called {int} times', function (int) {
|
|
69
|
+
cy.window().should(win => {
|
|
70
|
+
expect(win.onEndReachedPicked).to.have.callCount(int);
|
|
71
|
+
});
|
|
37
72
|
});
|
|
@@ -27,3 +27,38 @@ Feature: The source and picked option lists notify the consumer when the end has
|
|
|
27
27
|
| type |
|
|
28
28
|
| source |
|
|
29
29
|
| picked |
|
|
30
|
+
|
|
31
|
+
Scenario: The list is short and items are added within the list container
|
|
32
|
+
Given the Transfer source options list does not fill the list completely
|
|
33
|
+
# Initial state: the list has 7 items and the list ends well above the container bottom
|
|
34
|
+
Then the list end indicator of the source list should be visible
|
|
35
|
+
Then the callback for reaching the end of the source list should be called 1 times
|
|
36
|
+
Then the callback for reaching the end of the picked list should be called 1 times
|
|
37
|
+
# Selected item is not in the options array but is present in the `selectedOptionsLookup`
|
|
38
|
+
Then the selected item is being displayed in the picked list
|
|
39
|
+
When the user adds an item by clicking the button
|
|
40
|
+
# The indicator is still just in view
|
|
41
|
+
Then the list end indicator of the source list should be visible
|
|
42
|
+
Then the callback for reaching the end of the source list should be called 2 times
|
|
43
|
+
When the user adds an item by clicking the button
|
|
44
|
+
# This adds val-9, which is a selected item so it gets added to picked options
|
|
45
|
+
# not source options, but the callback is still called
|
|
46
|
+
Then the list end indicator of the source list should be visible
|
|
47
|
+
Then the callback for reaching the end of the source list should be called 3 times
|
|
48
|
+
# The picked list callback does not get called because the picked item was already present
|
|
49
|
+
Then the callback for reaching the end of the picked list should be called 1 times
|
|
50
|
+
When the user adds an item by clicking the button
|
|
51
|
+
# The indicator is still in view but only just
|
|
52
|
+
Then the list end indicator of the source list should be visible
|
|
53
|
+
Then the callback for reaching the end of the source list should be called 4 times
|
|
54
|
+
When the user adds an item by clicking the button
|
|
55
|
+
# The indicator now is out of view, no more calls
|
|
56
|
+
Then the list end indicator of the source list should not be visible
|
|
57
|
+
Then the callback for reaching the end of the source list should be called 4 times
|
|
58
|
+
# But scrolling down does trigger a call
|
|
59
|
+
When the user scrolls down to the source list end
|
|
60
|
+
Then the list end indicator of the source list should be visible
|
|
61
|
+
Then the callback for reaching the end of the source list should be called 5 times
|
|
62
|
+
# And selecting an item triggers a call in the picked list
|
|
63
|
+
When the user selects option nr. 11
|
|
64
|
+
Then the callback for reaching the end of the picked list should be called 2 times
|
|
@@ -3,9 +3,10 @@ import { CircularLoader } from '@dhis2-ui/loader';
|
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
import React, { Fragment, useRef } from 'react';
|
|
5
5
|
import { EndIntersectionDetector } from './end-intersection-detector.js';
|
|
6
|
-
import {
|
|
6
|
+
import { useOptionsKeyMonitor } from './transfer/use-options-key-monitor.js';
|
|
7
7
|
export const OptionsContainer = _ref => {
|
|
8
8
|
let {
|
|
9
|
+
allOptionsKey,
|
|
9
10
|
dataTest,
|
|
10
11
|
emptyComponent,
|
|
11
12
|
onEndReached,
|
|
@@ -18,19 +19,24 @@ export const OptionsContainer = _ref => {
|
|
|
18
19
|
selectionHandler,
|
|
19
20
|
toggleHighlightedOption
|
|
20
21
|
} = _ref;
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
22
|
+
const scrollBoxRef = useRef(null);
|
|
23
|
+
const listRef = useRef(null);
|
|
24
|
+
useOptionsKeyMonitor({
|
|
25
|
+
scrollBoxRef,
|
|
26
|
+
listRef,
|
|
27
|
+
allOptionsKey,
|
|
28
|
+
onEndReached
|
|
29
|
+
});
|
|
24
30
|
return /*#__PURE__*/React.createElement("div", {
|
|
25
31
|
className: "jsx-1882699425" + " " + "optionsContainer"
|
|
26
32
|
}, loading && /*#__PURE__*/React.createElement("div", {
|
|
27
33
|
className: "jsx-1882699425" + " " + "loading"
|
|
28
34
|
}, /*#__PURE__*/React.createElement(CircularLoader, null)), /*#__PURE__*/React.createElement("div", {
|
|
29
35
|
"data-test": dataTest,
|
|
30
|
-
ref:
|
|
36
|
+
ref: scrollBoxRef,
|
|
31
37
|
className: "jsx-1882699425" + " " + "container"
|
|
32
38
|
}, /*#__PURE__*/React.createElement("div", {
|
|
33
|
-
ref:
|
|
39
|
+
ref: listRef,
|
|
34
40
|
className: "jsx-1882699425" + " " + "content-container"
|
|
35
41
|
}, !options.length && emptyComponent, options.map(option => {
|
|
36
42
|
const highlighted = !!highlightedOptions.find(highlightedSourceOption => highlightedSourceOption === option.value);
|
|
@@ -44,14 +50,14 @@ export const OptionsContainer = _ref => {
|
|
|
44
50
|
}));
|
|
45
51
|
}), onEndReached && /*#__PURE__*/React.createElement(EndIntersectionDetector, {
|
|
46
52
|
dataTest: `${dataTest}-endintersectiondetector`,
|
|
47
|
-
|
|
48
|
-
rootRef: optionsRef,
|
|
53
|
+
rootRef: scrollBoxRef,
|
|
49
54
|
onEndReached: onEndReached
|
|
50
55
|
}))), /*#__PURE__*/React.createElement(_JSXStyle, {
|
|
51
56
|
id: "1882699425"
|
|
52
57
|
}, [".optionsContainer.jsx-1882699425{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;position:relative;overflow:hidden;}", ".container.jsx-1882699425{overflow-y:auto;height:100%;}", ".loading.jsx-1882699425{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;height:100%;width:100%;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;position:absolute;z-index:2;top:0;inset-inline-start:0;}", ".content-container.jsx-1882699425{z-index:1;position:relative;}", ".loading.jsx-1882699425+.container.jsx-1882699425 .content-container.jsx-1882699425{-webkit-filter:blur(2px);filter:blur(2px);}"]));
|
|
53
58
|
};
|
|
54
59
|
OptionsContainer.propTypes = {
|
|
60
|
+
allOptionsKey: PropTypes.string.isRequired,
|
|
55
61
|
dataTest: PropTypes.string.isRequired,
|
|
56
62
|
getOptionClickHandlers: PropTypes.func.isRequired,
|
|
57
63
|
emptyComponent: PropTypes.node,
|
|
@@ -10,4 +10,5 @@ export * from './move-highlighted-picked-option-up.js';
|
|
|
10
10
|
export * from './remove-all-picked-options.js';
|
|
11
11
|
export * from './remove-individual-picked-options.js';
|
|
12
12
|
export * from './use-filter.js';
|
|
13
|
-
export * from './use-highlighted-options.js';
|
|
13
|
+
export * from './use-highlighted-options.js';
|
|
14
|
+
export * from './use-options-key-monitor.js';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { INTERSECTION_DETECTOR_HEIGHT } from '../end-intersection-detector.js';
|
|
3
|
+
const isEndIntersectionDetectorWithinScrollBox = (scrollBoxRef, listRef) => {
|
|
4
|
+
if (!scrollBoxRef.current || !listRef.current) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
const scrollBoxRect = scrollBoxRef.current.getBoundingClientRect();
|
|
8
|
+
const listRect = listRef.current.getBoundingClientRect();
|
|
9
|
+
return listRect.bottom - scrollBoxRect.bottom < INTERSECTION_DETECTOR_HEIGHT;
|
|
10
|
+
};
|
|
11
|
+
export const useOptionsKeyMonitor = _ref => {
|
|
12
|
+
let {
|
|
13
|
+
scrollBoxRef,
|
|
14
|
+
listRef,
|
|
15
|
+
allOptionsKey,
|
|
16
|
+
onEndReached
|
|
17
|
+
} = _ref;
|
|
18
|
+
/* Store in ref so this works even if a consumer does not pass a stable
|
|
19
|
+
* function reference */
|
|
20
|
+
const onEndReachedRef = useRef(onEndReached);
|
|
21
|
+
const prevAllOptionsKey = useRef(allOptionsKey);
|
|
22
|
+
onEndReachedRef.current = onEndReached;
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
/* When new options are loaded and the list end is (still) in
|
|
25
|
+
* view we need to call onEndReached, because the end of the list
|
|
26
|
+
* has indeed been reached but the interception detector will not pick
|
|
27
|
+
* up on this */
|
|
28
|
+
if (onEndReachedRef.current && prevAllOptionsKey.current !== allOptionsKey && isEndIntersectionDetectorWithinScrollBox(scrollBoxRef, listRef)) {
|
|
29
|
+
onEndReachedRef.current();
|
|
30
|
+
}
|
|
31
|
+
prevAllOptionsKey.current = allOptionsKey;
|
|
32
|
+
/* This effect will only run on mount and when allOptionsKey
|
|
33
|
+
* changes because scrollBoxRef, listRef are stable references */
|
|
34
|
+
}, [scrollBoxRef, listRef, allOptionsKey]);
|
|
35
|
+
};
|
package/build/es/transfer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import PropTypes from 'prop-types';
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { useMemo } from 'react';
|
|
3
3
|
import { Actions } from './actions.js';
|
|
4
4
|
import { AddAll } from './add-all.js';
|
|
5
5
|
import { AddIndividual } from './add-individual.js';
|
|
@@ -19,6 +19,7 @@ import { addAllSelectableSourceOptions, addIndividualSourceOptions, createDouble
|
|
|
19
19
|
import { TransferOption } from './transfer-option.js';
|
|
20
20
|
const identity = value => value;
|
|
21
21
|
const defaultSelected = [];
|
|
22
|
+
const defaultSelectedOptionsLookup = {};
|
|
22
23
|
export const Transfer = _ref => {
|
|
23
24
|
let {
|
|
24
25
|
options,
|
|
@@ -42,6 +43,7 @@ export const Transfer = _ref => {
|
|
|
42
43
|
hideFilterInputPicked,
|
|
43
44
|
initialSearchTerm = '',
|
|
44
45
|
initialSearchTermPicked = '',
|
|
46
|
+
selectedOptionsLookup = defaultSelectedOptionsLookup,
|
|
45
47
|
leftFooter,
|
|
46
48
|
leftHeader,
|
|
47
49
|
loadingPicked,
|
|
@@ -124,16 +126,15 @@ export const Transfer = _ref => {
|
|
|
124
126
|
* Actual picked options:
|
|
125
127
|
* Extract the selected options. Can't use `options.filter`
|
|
126
128
|
* because we need to keep the order of `selected`
|
|
129
|
+
* Note: Only map if selected is an array
|
|
127
130
|
*/
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
.filter(identity), actualFilterPicked);
|
|
136
|
-
}
|
|
131
|
+
const pickedOptions = useMemo(() => Array.isArray(selected) ? actualFilterPickedCallback(selected.map(value => {
|
|
132
|
+
var _selectedOptionsLooku;
|
|
133
|
+
return (_selectedOptionsLooku = selectedOptionsLookup[value]) !== null && _selectedOptionsLooku !== void 0 ? _selectedOptionsLooku : options.find(option => value === option.value);
|
|
134
|
+
})
|
|
135
|
+
// filter -> in case a selected value has been provided
|
|
136
|
+
// that does not exist as option
|
|
137
|
+
.filter(identity), actualFilterPicked) : [], [selected, options, actualFilterPicked, actualFilterPickedCallback, selectedOptionsLookup]);
|
|
137
138
|
|
|
138
139
|
/*
|
|
139
140
|
* Source options highlighting:
|
|
@@ -176,6 +177,18 @@ export const Transfer = _ref => {
|
|
|
176
177
|
const isAddIndividualDisabled = disabled || !highlightedSourceOptions.length;
|
|
177
178
|
const isRemoveAllDisabled = disabled || !selected.length;
|
|
178
179
|
const isRemoveIndividualDisabled = disabled || !highlightedPickedOptions.length;
|
|
180
|
+
const allOptionsKey = useMemo(() => options.map(_ref4 => {
|
|
181
|
+
let {
|
|
182
|
+
value
|
|
183
|
+
} = _ref4;
|
|
184
|
+
return value;
|
|
185
|
+
}).join('|'), [options]);
|
|
186
|
+
const pickedOptionsKey = useMemo(() => pickedOptions.map(_ref5 => {
|
|
187
|
+
let {
|
|
188
|
+
value
|
|
189
|
+
} = _ref5;
|
|
190
|
+
return value;
|
|
191
|
+
}).join('|'), [pickedOptions]);
|
|
179
192
|
return /*#__PURE__*/React.createElement(Container, {
|
|
180
193
|
dataTest: dataTest,
|
|
181
194
|
className: className,
|
|
@@ -190,13 +203,14 @@ export const Transfer = _ref => {
|
|
|
190
203
|
placeholder: filterPlaceholder,
|
|
191
204
|
dataTest: `${dataTest}-filter`,
|
|
192
205
|
filter: actualFilter,
|
|
193
|
-
onChange: onFilterChange ? onFilterChange :
|
|
206
|
+
onChange: onFilterChange ? onFilterChange : _ref6 => {
|
|
194
207
|
let {
|
|
195
208
|
value
|
|
196
|
-
} =
|
|
209
|
+
} = _ref6;
|
|
197
210
|
return setInternalFilter(value);
|
|
198
211
|
}
|
|
199
212
|
})), /*#__PURE__*/React.createElement(OptionsContainer, {
|
|
213
|
+
allOptionsKey: allOptionsKey,
|
|
200
214
|
dataTest: `${dataTest}-sourceoptions`,
|
|
201
215
|
emptyComponent: sourceEmptyPlaceholder,
|
|
202
216
|
getOptionClickHandlers: getOptionClickHandlers,
|
|
@@ -264,14 +278,15 @@ export const Transfer = _ref => {
|
|
|
264
278
|
placeholder: filterPlaceholderPicked,
|
|
265
279
|
dataTest: `${dataTest}-filter`,
|
|
266
280
|
filter: actualFilterPicked,
|
|
267
|
-
onChange: onFilterChangePicked ? onFilterChangePicked :
|
|
281
|
+
onChange: onFilterChangePicked ? onFilterChangePicked : _ref7 => {
|
|
268
282
|
let {
|
|
269
283
|
value
|
|
270
|
-
} =
|
|
284
|
+
} = _ref7;
|
|
271
285
|
return setInternalFilterPicked(value);
|
|
272
286
|
}
|
|
273
287
|
})), /*#__PURE__*/React.createElement(OptionsContainer, {
|
|
274
288
|
selected: true,
|
|
289
|
+
allOptionsKey: pickedOptionsKey,
|
|
275
290
|
dataTest: `${dataTest}-pickedoptions`,
|
|
276
291
|
emptyComponent: selectedEmptyComponent,
|
|
277
292
|
getOptionClickHandlers: getOptionClickHandlers,
|
|
@@ -350,6 +365,16 @@ Transfer.propTypes = {
|
|
|
350
365
|
searchTermPicked: PropTypes.string,
|
|
351
366
|
selected: PropTypes.arrayOf(PropTypes.string),
|
|
352
367
|
selectedEmptyComponent: PropTypes.node,
|
|
368
|
+
/**
|
|
369
|
+
* To be used in scenarios where selected options may not be present
|
|
370
|
+
* in the options array. Like when having options that lazy load or can
|
|
371
|
+
* be filtered async.
|
|
372
|
+
*/
|
|
373
|
+
selectedOptionsLookup: PropTypes.objectOf(PropTypes.shape({
|
|
374
|
+
label: PropTypes.string.isRequired,
|
|
375
|
+
value: PropTypes.string.isRequired,
|
|
376
|
+
disabled: PropTypes.bool
|
|
377
|
+
})),
|
|
353
378
|
selectedWidth: PropTypes.string,
|
|
354
379
|
sourceEmptyPlaceholder: PropTypes.node,
|
|
355
380
|
onEndReached: PropTypes.func,
|
|
@@ -2,7 +2,7 @@ function _extends() { return _extends = Object.assign ? Object.assign.bind() : f
|
|
|
2
2
|
import { SingleSelectField, SingleSelectOption } from '@dhis2-ui/select';
|
|
3
3
|
import { Tab, TabBar } from '@dhis2-ui/tab';
|
|
4
4
|
import PropTypes from 'prop-types';
|
|
5
|
-
import React, { useEffect, useState } from 'react';
|
|
5
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
6
|
import { TransferOption } from './transfer-option.js';
|
|
7
7
|
import { Transfer } from './transfer.js';
|
|
8
8
|
const subtitle = 'Allows users to select options from a list';
|
|
@@ -444,6 +444,8 @@ const pageSize = 5;
|
|
|
444
444
|
* To keep the code as small as possible, handling selecting items is not
|
|
445
445
|
included
|
|
446
446
|
*/
|
|
447
|
+
const preSelectedOptions = optionsPool.slice(optionsPool.length - 4);
|
|
448
|
+
const waitMs = async ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
447
449
|
export const InfiniteLoading = args => {
|
|
448
450
|
useEffect(() => {
|
|
449
451
|
console.clear();
|
|
@@ -451,59 +453,45 @@ export const InfiniteLoading = args => {
|
|
|
451
453
|
|
|
452
454
|
// state for whether the next page's options are being loaded
|
|
453
455
|
const [loading, setLoading] = useState(false);
|
|
454
|
-
//
|
|
455
|
-
const [
|
|
456
|
-
// all options (incl. available AND selected options)
|
|
456
|
+
// prevent fetches after list is complete
|
|
457
|
+
const [isOptionsListComplete, setIsOptionsListComplete] = useState(false);
|
|
457
458
|
const [options, setOptions] = useState([]);
|
|
458
459
|
// selected options
|
|
459
460
|
const [selected] = useState(
|
|
460
|
-
//
|
|
461
|
-
|
|
461
|
+
// Last 4 options preselected
|
|
462
|
+
preSelectedOptions.map(_ref13 => {
|
|
462
463
|
let {
|
|
463
464
|
value
|
|
464
465
|
} = _ref13;
|
|
465
466
|
return value;
|
|
466
467
|
}));
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
setPage(page + 1);
|
|
473
|
-
};
|
|
474
|
-
|
|
475
|
-
// fake fetch request
|
|
476
|
-
const fetchOptions = nextPage => new Promise(resolve => setTimeout(() => {
|
|
477
|
-
const nextOptions = optionsPool.slice(options.length, nextPage * pageSize);
|
|
478
|
-
resolve(nextOptions);
|
|
479
|
-
}, 2000));
|
|
480
|
-
const loadNextOptions = async () => {
|
|
468
|
+
const selectedOptionsLookup = useMemo(() => preSelectedOptions.reduce((lookup, option) => {
|
|
469
|
+
lookup[option.value] = option;
|
|
470
|
+
return lookup;
|
|
471
|
+
}, {}), []);
|
|
472
|
+
const loadMoreOptions = useCallback(async () => {
|
|
481
473
|
setLoading(true);
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
} = nextOption;
|
|
489
|
-
return selected.includes(value);
|
|
490
|
-
});
|
|
491
|
-
if (allAlreadySelected) {
|
|
492
|
-
onEndReached();
|
|
474
|
+
await waitMs(2000);
|
|
475
|
+
const newOptions = optionsPool.slice(options.length, Math.min(options.length + pageSize, optionsPool.length));
|
|
476
|
+
const combinedOptions = [...options, ...newOptions];
|
|
477
|
+
setOptions(combinedOptions);
|
|
478
|
+
if (combinedOptions.length === optionsPool.length) {
|
|
479
|
+
setIsOptionsListComplete(true);
|
|
493
480
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (
|
|
498
|
-
|
|
481
|
+
setLoading(false);
|
|
482
|
+
}, [options]);
|
|
483
|
+
const onEndReached = useCallback(() => {
|
|
484
|
+
if (!isOptionsListComplete) {
|
|
485
|
+
loadMoreOptions();
|
|
499
486
|
}
|
|
500
|
-
}, [
|
|
487
|
+
}, [loadMoreOptions, isOptionsListComplete]);
|
|
501
488
|
return /*#__PURE__*/React.createElement(Transfer, _extends({}, args, {
|
|
502
489
|
loading: loading,
|
|
503
490
|
options: options,
|
|
504
491
|
selected: selected,
|
|
505
492
|
onChange: () => null /* noop */,
|
|
506
|
-
onEndReached: onEndReached
|
|
493
|
+
onEndReached: onEndReached,
|
|
494
|
+
selectedOptionsLookup: selectedOptionsLookup
|
|
507
495
|
}));
|
|
508
496
|
};
|
|
509
497
|
export const LoadingSource = StatefulTemplate.bind({});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dhis2-ui/transfer",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.12.0",
|
|
4
4
|
"description": "UI Transfer",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@dhis2/prop-types": "^3.1.2",
|
|
36
|
-
"@dhis2-ui/button": "10.
|
|
37
|
-
"@dhis2-ui/field": "10.
|
|
38
|
-
"@dhis2-ui/input": "10.
|
|
39
|
-
"@dhis2-ui/intersection-detector": "10.
|
|
40
|
-
"@dhis2-ui/loader": "10.
|
|
41
|
-
"@dhis2/ui-constants": "10.
|
|
36
|
+
"@dhis2-ui/button": "10.12.0",
|
|
37
|
+
"@dhis2-ui/field": "10.12.0",
|
|
38
|
+
"@dhis2-ui/input": "10.12.0",
|
|
39
|
+
"@dhis2-ui/intersection-detector": "10.12.0",
|
|
40
|
+
"@dhis2-ui/loader": "10.12.0",
|
|
41
|
+
"@dhis2/ui-constants": "10.12.0",
|
|
42
42
|
"classnames": "^2.3.1",
|
|
43
43
|
"prop-types": "^15.7.2"
|
|
44
44
|
},
|
package/types/index.d.ts
CHANGED
|
@@ -44,6 +44,12 @@ export interface TransferProps {
|
|
|
44
44
|
searchTermPicked?: string
|
|
45
45
|
selected?: string[]
|
|
46
46
|
selectedEmptyComponent?: React.ReactNode
|
|
47
|
+
/**
|
|
48
|
+
* To be used in scenarios where selected options may not be present
|
|
49
|
+
* in the options array. Like when having options that lazy load or can
|
|
50
|
+
* be filtered async.
|
|
51
|
+
*/
|
|
52
|
+
selectedOptionsLookup?: Record<string, TransferOption>
|
|
47
53
|
selectedWidth?: string
|
|
48
54
|
sourceEmptyPlaceholder?: React.ReactNode
|
|
49
55
|
onEndReached?: () => void
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.PickedOptions = void 0;
|
|
7
|
-
var _style = _interopRequireDefault(require("styled-jsx/style"));
|
|
8
|
-
var _uiConstants = require("@dhis2/ui-constants");
|
|
9
|
-
var _propTypes = _interopRequireDefault(require("prop-types"));
|
|
10
|
-
var _react = _interopRequireDefault(require("react"));
|
|
11
|
-
var _endIntersectionDetector = require("./end-intersection-detector.js");
|
|
12
|
-
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
|
-
const PickedOptions = _ref => {
|
|
14
|
-
let {
|
|
15
|
-
children,
|
|
16
|
-
dataTest,
|
|
17
|
-
selectedEmptyComponent,
|
|
18
|
-
pickedOptionsRef,
|
|
19
|
-
onPickedEndReached
|
|
20
|
-
} = _ref;
|
|
21
|
-
return /*#__PURE__*/_react.default.createElement("div", {
|
|
22
|
-
"data-test": dataTest,
|
|
23
|
-
ref: pickedOptionsRef,
|
|
24
|
-
className: _style.default.dynamic([["392419471", [_uiConstants.spacers.dp4]]]) + " " + "container"
|
|
25
|
-
}, /*#__PURE__*/_react.default.createElement("div", {
|
|
26
|
-
className: _style.default.dynamic([["392419471", [_uiConstants.spacers.dp4]]]) + " " + "content-container"
|
|
27
|
-
}, !_react.default.Children.count(children) && selectedEmptyComponent, children, onPickedEndReached && /*#__PURE__*/_react.default.createElement(_endIntersectionDetector.EndIntersectionDetector, {
|
|
28
|
-
rootRef: pickedOptionsRef,
|
|
29
|
-
onEndReached: onPickedEndReached
|
|
30
|
-
})), /*#__PURE__*/_react.default.createElement(_style.default, {
|
|
31
|
-
id: "392419471",
|
|
32
|
-
dynamic: [_uiConstants.spacers.dp4]
|
|
33
|
-
}, [`.container.__jsx-style-dynamic-selector{padding:${_uiConstants.spacers.dp4} 0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;overflow-y:auto;}`, ".content-container.__jsx-style-dynamic-selector{position:relative;}"]));
|
|
34
|
-
};
|
|
35
|
-
exports.PickedOptions = PickedOptions;
|
|
36
|
-
PickedOptions.propTypes = {
|
|
37
|
-
children: _propTypes.default.node.isRequired,
|
|
38
|
-
dataTest: _propTypes.default.string.isRequired,
|
|
39
|
-
pickedOptionsRef: _propTypes.default.shape({
|
|
40
|
-
current: _propTypes.default.instanceOf(HTMLElement)
|
|
41
|
-
}),
|
|
42
|
-
selectedEmptyComponent: _propTypes.default.node,
|
|
43
|
-
onPickedEndReached: _propTypes.default.func
|
|
44
|
-
};
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.SourceOptions = void 0;
|
|
7
|
-
var _style = _interopRequireDefault(require("styled-jsx/style"));
|
|
8
|
-
var _uiConstants = require("@dhis2/ui-constants");
|
|
9
|
-
var _propTypes = _interopRequireDefault(require("prop-types"));
|
|
10
|
-
var _react = _interopRequireDefault(require("react"));
|
|
11
|
-
var _endIntersectionDetector = require("./end-intersection-detector.js");
|
|
12
|
-
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
|
-
const SourceOptions = _ref => {
|
|
14
|
-
let {
|
|
15
|
-
children,
|
|
16
|
-
dataTest,
|
|
17
|
-
sourceEmptyPlaceholder,
|
|
18
|
-
sourceOptionsRef,
|
|
19
|
-
onSourceEndReached
|
|
20
|
-
} = _ref;
|
|
21
|
-
return /*#__PURE__*/_react.default.createElement("div", {
|
|
22
|
-
"data-test": dataTest,
|
|
23
|
-
ref: sourceOptionsRef,
|
|
24
|
-
className: _style.default.dynamic([["392419471", [_uiConstants.spacers.dp4]]]) + " " + "container"
|
|
25
|
-
}, /*#__PURE__*/_react.default.createElement("div", {
|
|
26
|
-
className: _style.default.dynamic([["392419471", [_uiConstants.spacers.dp4]]]) + " " + "content-container"
|
|
27
|
-
}, children, !_react.default.Children.count(children) && sourceEmptyPlaceholder, onSourceEndReached && /*#__PURE__*/_react.default.createElement(_endIntersectionDetector.EndIntersectionDetector, {
|
|
28
|
-
rootRef: sourceOptionsRef,
|
|
29
|
-
onEndReached: onSourceEndReached
|
|
30
|
-
})), /*#__PURE__*/_react.default.createElement(_style.default, {
|
|
31
|
-
id: "392419471",
|
|
32
|
-
dynamic: [_uiConstants.spacers.dp4]
|
|
33
|
-
}, [`.container.__jsx-style-dynamic-selector{padding:${_uiConstants.spacers.dp4} 0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;overflow-y:auto;}`, ".content-container.__jsx-style-dynamic-selector{position:relative;}"]));
|
|
34
|
-
};
|
|
35
|
-
exports.SourceOptions = SourceOptions;
|
|
36
|
-
SourceOptions.propTypes = {
|
|
37
|
-
dataTest: _propTypes.default.string.isRequired,
|
|
38
|
-
children: _propTypes.default.node,
|
|
39
|
-
sourceEmptyPlaceholder: _propTypes.default.node,
|
|
40
|
-
sourceOptionsRef: _propTypes.default.shape({
|
|
41
|
-
current: _propTypes.default.instanceOf(HTMLElement)
|
|
42
|
-
}),
|
|
43
|
-
onSourceEndReached: _propTypes.default.func
|
|
44
|
-
};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.useResizeCounter = void 0;
|
|
7
|
-
var _react = require("react");
|
|
8
|
-
/*
|
|
9
|
-
* The initial call is irrelevant as there has been
|
|
10
|
-
* no resize yet that we want to react to
|
|
11
|
-
* So we start with -1 with will returned as 0 by this hook
|
|
12
|
-
*
|
|
13
|
-
* @param {Element} element
|
|
14
|
-
* @returns {number}
|
|
15
|
-
*/
|
|
16
|
-
const useResizeCounter = element => {
|
|
17
|
-
const [counter, setCounter] = (0, _react.useState)(-1);
|
|
18
|
-
(0, _react.useEffect)(() => {
|
|
19
|
-
// using an internal counter as using the one from `useState`
|
|
20
|
-
// would cause an infinite loop as this `useEffect` would
|
|
21
|
-
// both depend on that value as well as change it every time
|
|
22
|
-
// it's executed as the callback passed to `ResizeObserver`
|
|
23
|
-
// will be executed on construction
|
|
24
|
-
let internalCounter = counter;
|
|
25
|
-
if (element) {
|
|
26
|
-
const observer = new ResizeObserver(() => {
|
|
27
|
-
++internalCounter;
|
|
28
|
-
setCounter(internalCounter);
|
|
29
|
-
});
|
|
30
|
-
observer.observe(element);
|
|
31
|
-
return () => observer.disconnect();
|
|
32
|
-
}
|
|
33
|
-
}, [element, setCounter]);
|
|
34
|
-
return counter < 1 ? 0 : counter;
|
|
35
|
-
};
|
|
36
|
-
exports.useResizeCounter = useResizeCounter;
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import _JSXStyle from "styled-jsx/style";
|
|
2
|
-
import { spacers } from '@dhis2/ui-constants';
|
|
3
|
-
import PropTypes from 'prop-types';
|
|
4
|
-
import React from 'react';
|
|
5
|
-
import { EndIntersectionDetector } from './end-intersection-detector.js';
|
|
6
|
-
export const PickedOptions = _ref => {
|
|
7
|
-
let {
|
|
8
|
-
children,
|
|
9
|
-
dataTest,
|
|
10
|
-
selectedEmptyComponent,
|
|
11
|
-
pickedOptionsRef,
|
|
12
|
-
onPickedEndReached
|
|
13
|
-
} = _ref;
|
|
14
|
-
return /*#__PURE__*/React.createElement("div", {
|
|
15
|
-
"data-test": dataTest,
|
|
16
|
-
ref: pickedOptionsRef,
|
|
17
|
-
className: _JSXStyle.dynamic([["392419471", [spacers.dp4]]]) + " " + "container"
|
|
18
|
-
}, /*#__PURE__*/React.createElement("div", {
|
|
19
|
-
className: _JSXStyle.dynamic([["392419471", [spacers.dp4]]]) + " " + "content-container"
|
|
20
|
-
}, !React.Children.count(children) && selectedEmptyComponent, children, onPickedEndReached && /*#__PURE__*/React.createElement(EndIntersectionDetector, {
|
|
21
|
-
rootRef: pickedOptionsRef,
|
|
22
|
-
onEndReached: onPickedEndReached
|
|
23
|
-
})), /*#__PURE__*/React.createElement(_JSXStyle, {
|
|
24
|
-
id: "392419471",
|
|
25
|
-
dynamic: [spacers.dp4]
|
|
26
|
-
}, [`.container.__jsx-style-dynamic-selector{padding:${spacers.dp4} 0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;overflow-y:auto;}`, ".content-container.__jsx-style-dynamic-selector{position:relative;}"]));
|
|
27
|
-
};
|
|
28
|
-
PickedOptions.propTypes = {
|
|
29
|
-
children: PropTypes.node.isRequired,
|
|
30
|
-
dataTest: PropTypes.string.isRequired,
|
|
31
|
-
pickedOptionsRef: PropTypes.shape({
|
|
32
|
-
current: PropTypes.instanceOf(HTMLElement)
|
|
33
|
-
}),
|
|
34
|
-
selectedEmptyComponent: PropTypes.node,
|
|
35
|
-
onPickedEndReached: PropTypes.func
|
|
36
|
-
};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import _JSXStyle from "styled-jsx/style";
|
|
2
|
-
import { spacers } from '@dhis2/ui-constants';
|
|
3
|
-
import PropTypes from 'prop-types';
|
|
4
|
-
import React from 'react';
|
|
5
|
-
import { EndIntersectionDetector } from './end-intersection-detector.js';
|
|
6
|
-
export const SourceOptions = _ref => {
|
|
7
|
-
let {
|
|
8
|
-
children,
|
|
9
|
-
dataTest,
|
|
10
|
-
sourceEmptyPlaceholder,
|
|
11
|
-
sourceOptionsRef,
|
|
12
|
-
onSourceEndReached
|
|
13
|
-
} = _ref;
|
|
14
|
-
return /*#__PURE__*/React.createElement("div", {
|
|
15
|
-
"data-test": dataTest,
|
|
16
|
-
ref: sourceOptionsRef,
|
|
17
|
-
className: _JSXStyle.dynamic([["392419471", [spacers.dp4]]]) + " " + "container"
|
|
18
|
-
}, /*#__PURE__*/React.createElement("div", {
|
|
19
|
-
className: _JSXStyle.dynamic([["392419471", [spacers.dp4]]]) + " " + "content-container"
|
|
20
|
-
}, children, !React.Children.count(children) && sourceEmptyPlaceholder, onSourceEndReached && /*#__PURE__*/React.createElement(EndIntersectionDetector, {
|
|
21
|
-
rootRef: sourceOptionsRef,
|
|
22
|
-
onEndReached: onSourceEndReached
|
|
23
|
-
})), /*#__PURE__*/React.createElement(_JSXStyle, {
|
|
24
|
-
id: "392419471",
|
|
25
|
-
dynamic: [spacers.dp4]
|
|
26
|
-
}, [`.container.__jsx-style-dynamic-selector{padding:${spacers.dp4} 0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;overflow-y:auto;}`, ".content-container.__jsx-style-dynamic-selector{position:relative;}"]));
|
|
27
|
-
};
|
|
28
|
-
SourceOptions.propTypes = {
|
|
29
|
-
dataTest: PropTypes.string.isRequired,
|
|
30
|
-
children: PropTypes.node,
|
|
31
|
-
sourceEmptyPlaceholder: PropTypes.node,
|
|
32
|
-
sourceOptionsRef: PropTypes.shape({
|
|
33
|
-
current: PropTypes.instanceOf(HTMLElement)
|
|
34
|
-
}),
|
|
35
|
-
onSourceEndReached: PropTypes.func
|
|
36
|
-
};
|