@7shifts/sous-chef 4.11.0 → 4.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.
@@ -2,14 +2,15 @@ import React from 'react';
2
2
  import type { Props as SelectProps } from '../SelectField/SelectField';
3
3
  import { AsyncSelectOptions } from './types';
4
4
  type Props<T> = {
5
- /** It is a function that takes the input search as parameter and returns a Promise with all the options and if there are more options to load. If it brings all the options on the first load, it won't call the `loadOptions` to perform the search. */
6
- loadOptions: (inputValue: string) => Promise<AsyncSelectOptions<T>>;
5
+ /** It is a function that takes the input search and an optional cursor as parameters and returns a Promise with all the options, if there are more options to load, and optionally the next cursor. If it brings all the options on the first load, it won't call `loadOptions` to perform the search. */
6
+ loadOptions: (inputValue: string, cursor?: string | null) => Promise<AsyncSelectOptions<T>>;
7
7
  /** It fires `loadOptions` again if this key value changes. Used when it depends on other fields to load its options */
8
8
  key?: string | number;
9
9
  } & Omit<SelectProps<T>, 'options'>;
10
10
  /**
11
11
  * A variation of select field that loads a dynamic list of options from a source rather than a fixed set of options. Use when you need to select from a dynamic list that can change such as roles, employees, locations, etc.
12
- * Instead of passing a `options` props, this component requires a `loadOptions` prop.
12
+ * Instead of passing an `options` prop, this component requires a `loadOptions` prop.
13
+ * Supports scroll-to-load-more: when the user scrolls to the bottom of the menu and a `nextCursor` is returned, the next page is automatically fetched and appended.
13
14
  * */
14
15
  declare const AsyncSelectField: <T extends unknown>({ loadOptions, ...props }: Props<T>) => React.JSX.Element;
15
16
  export default AsyncSelectField;
@@ -3,6 +3,7 @@ import { components } from 'react-select';
3
3
  type Props = ComponentProps<typeof components.MenuList> & {
4
4
  hasMoreOptions: boolean;
5
5
  hasMoreOptionsFirstLoad: boolean | null | undefined;
6
+ isLoadingMore: boolean;
6
7
  };
7
- declare const CustomList: ({ children, hasMoreOptions, hasMoreOptionsFirstLoad, ...props }: Props) => React.JSX.Element;
8
+ declare const CustomList: ({ children, hasMoreOptions, hasMoreOptionsFirstLoad, isLoadingMore, ...props }: Props) => React.JSX.Element;
8
9
  export default CustomList;
@@ -2,4 +2,5 @@ import { SelectOptions } from '../SelectField/types';
2
2
  export type AsyncSelectOptions<T> = {
3
3
  options: SelectOptions<T>;
4
4
  hasMore: boolean;
5
+ nextCursor?: string | null;
5
6
  };
package/dist/index.js CHANGED
@@ -15,7 +15,6 @@ var originalStylers = require('react-day-picker/dist/style.module.css');
15
15
  require('react-toastify/dist/ReactToastify.css');
16
16
  var libphonenumberJs = require('libphonenumber-js');
17
17
  var Select = require('react-select');
18
- var AsyncSelect = require('react-select/async');
19
18
  var dateFns = require('date-fns');
20
19
  var getDay = require('date-fns/getDay');
21
20
  var getDate = require('date-fns/getDate');
@@ -54,7 +53,6 @@ var dateFnsParse__default = /*#__PURE__*/_interopDefaultLegacy(dateFnsParse);
54
53
  var startOfDay__default = /*#__PURE__*/_interopDefaultLegacy(startOfDay);
55
54
  var originalStylers__default = /*#__PURE__*/_interopDefaultLegacy(originalStylers);
56
55
  var Select__default = /*#__PURE__*/_interopDefaultLegacy(Select);
57
- var AsyncSelect__default = /*#__PURE__*/_interopDefaultLegacy(AsyncSelect);
58
56
  var getDay__default = /*#__PURE__*/_interopDefaultLegacy(getDay);
59
57
  var getDate__default = /*#__PURE__*/_interopDefaultLegacy(getDate);
60
58
  var getDaysInMonth__default = /*#__PURE__*/_interopDefaultLegacy(getDaysInMonth);
@@ -17649,18 +17647,23 @@ var MultiSelectField = function MultiSelectField(_ref) {
17649
17647
 
17650
17648
  var styles$r = {"custom-list":"_cSkvD"};
17651
17649
 
17652
- var _excluded$b4 = ["children", "hasMoreOptions", "hasMoreOptionsFirstLoad"];
17650
+ var _excluded$b4 = ["children", "hasMoreOptions", "hasMoreOptionsFirstLoad", "isLoadingMore"];
17653
17651
  var CustomList = function CustomList(_ref) {
17654
17652
  var children = _ref.children,
17655
17653
  hasMoreOptions = _ref.hasMoreOptions,
17656
17654
  hasMoreOptionsFirstLoad = _ref.hasMoreOptionsFirstLoad,
17655
+ isLoadingMore = _ref.isLoadingMore,
17657
17656
  props = _objectWithoutPropertiesLoose(_ref, _excluded$b4);
17658
17657
  var __ = useTranslation('AsyncSelectField');
17659
17658
  var showFooter = hasMoreOptions;
17660
17659
  if (props.selectProps.inputValue === '' && typeof hasMoreOptionsFirstLoad === 'boolean') {
17661
17660
  showFooter = hasMoreOptionsFirstLoad;
17662
17661
  }
17663
- return React__default["default"].createElement(Select.components.MenuList, _extends({}, props), React__default["default"].createElement(React.Fragment, null, children, showFooter && props.options.length > 0 && React__default["default"].createElement(Inline, {
17662
+ return React__default["default"].createElement(Select.components.MenuList, _extends({}, props), React__default["default"].createElement(React.Fragment, null, children, isLoadingMore ? React__default["default"].createElement(Inline, {
17663
+ justifyContent: "center"
17664
+ }, React__default["default"].createElement(Spinner, {
17665
+ size: 20
17666
+ })) : showFooter && props.options.length > 0 && React__default["default"].createElement(Inline, {
17664
17667
  justifyContent: "center"
17665
17668
  }, React__default["default"].createElement("div", {
17666
17669
  className: styles$r['custom-list']
@@ -17668,61 +17671,123 @@ var CustomList = function CustomList(_ref) {
17668
17671
  };
17669
17672
 
17670
17673
  var _excluded$b3 = ["loadOptions"];
17674
+ var MenuListContext = React__default["default"].createContext({
17675
+ hasMoreOptions: false,
17676
+ hasMoreOptionsFirstLoad: null,
17677
+ isLoadingMore: false
17678
+ });
17679
+ // Defined outside the component so the reference is stable — prevents MenuList from
17680
+ // unmounting/remounting (and losing scroll position) on every state change.
17681
+ var AsyncMenuList = function AsyncMenuList(menuListProps) {
17682
+ var ctx = React__default["default"].useContext(MenuListContext);
17683
+ return React__default["default"].createElement(CustomList, _extends({}, menuListProps, ctx));
17684
+ };
17671
17685
  /**
17672
17686
  * A variation of select field that loads a dynamic list of options from a source rather than a fixed set of options. Use when you need to select from a dynamic list that can change such as roles, employees, locations, etc.
17673
- * Instead of passing a `options` props, this component requires a `loadOptions` prop.
17687
+ * Instead of passing an `options` prop, this component requires a `loadOptions` prop.
17688
+ * Supports scroll-to-load-more: when the user scrolls to the bottom of the menu and a `nextCursor` is returned, the next page is automatically fetched and appended.
17674
17689
  * */
17675
17690
  var AsyncSelectField = function AsyncSelectField(_ref) {
17676
17691
  var loadOptions = _ref.loadOptions,
17677
17692
  props = _objectWithoutPropertiesLoose(_ref, _excluded$b3);
17678
- var _useState = React.useState(false),
17679
- hasMoreOptions = _useState[0],
17680
- setHasMoreOptions = _useState[1];
17681
- var _useState2 = React.useState(),
17682
- hasMoreOptionsFirstLoad = _useState2[0],
17683
- setHasMoreOptionsFirstLoad = _useState2[1];
17693
+ var _useState = React.useState([]),
17694
+ displayedOptions = _useState[0],
17695
+ setDisplayedOptions = _useState[1];
17696
+ var _useState2 = React.useState(false),
17697
+ isLoading = _useState2[0],
17698
+ setIsLoading = _useState2[1];
17684
17699
  var _useState3 = React.useState(false),
17685
- hasFirstLoadPerformed = _useState3[0],
17686
- setHasFirstLoadPerformed = _useState3[1];
17687
- var _useState4 = React.useState(null),
17688
- localOptions = _useState4[0],
17689
- setLocalOptions = _useState4[1];
17690
- var ref = React.useRef(null);
17691
- var handleInternalSearch = function handleInternalSearch(inputValue, callback) {
17692
- if (!localOptions) {
17693
- return;
17694
- }
17695
- var filteredOptions = localOptions.filter(function (option) {
17696
- return option.label.toLowerCase().includes(inputValue.toLocaleLowerCase());
17700
+ isLoadingMore = _useState3[0],
17701
+ setIsLoadingMore = _useState3[1];
17702
+ var _useState4 = React.useState(''),
17703
+ inputValue = _useState4[0],
17704
+ setInputValue = _useState4[1];
17705
+ var _useState5 = React.useState(false),
17706
+ hasMore = _useState5[0],
17707
+ setHasMore = _useState5[1];
17708
+ var _useState6 = React.useState(null),
17709
+ nextCursor = _useState6[0],
17710
+ setNextCursor = _useState6[1];
17711
+ var _useState7 = React.useState(null),
17712
+ localOptions = _useState7[0],
17713
+ setLocalOptions = _useState7[1];
17714
+ var _useState8 = React.useState(null),
17715
+ hasMoreOptionsFirstLoad = _useState8[0],
17716
+ setHasMoreOptionsFirstLoad = _useState8[1];
17717
+ var loadOptionsRef = React.useRef(loadOptions);
17718
+ React.useEffect(function () {
17719
+ loadOptionsRef.current = loadOptions;
17720
+ }, [loadOptions]);
17721
+ var fetchIdRef = React.useRef(0);
17722
+ var firstLoadDoneRef = React.useRef(false);
17723
+ // All dependencies are refs and stable state setters — safe with empty dep array.
17724
+ var fetchOptions = React.useCallback(function (input) {
17725
+ var id = ++fetchIdRef.current;
17726
+ setIsLoading(true);
17727
+ loadOptionsRef.current(input, null).then(function (_ref2) {
17728
+ var options = _ref2.options,
17729
+ more = _ref2.hasMore,
17730
+ resultCursor = _ref2.nextCursor;
17731
+ if (id !== fetchIdRef.current) return;
17732
+ if (!firstLoadDoneRef.current) {
17733
+ firstLoadDoneRef.current = true;
17734
+ setHasMoreOptionsFirstLoad(more && resultCursor == null);
17735
+ if (!more) setLocalOptions(options);
17736
+ }
17737
+ setDisplayedOptions(options);
17738
+ setHasMore(more);
17739
+ setNextCursor(resultCursor != null ? resultCursor : null);
17740
+ setIsLoading(false);
17741
+ })["catch"](function () {
17742
+ if (id !== fetchIdRef.current) return;
17743
+ setIsLoading(false);
17697
17744
  });
17698
- callback(filteredOptions);
17699
- };
17700
- var handleLoadOptions = function handleLoadOptions(inputValue, callback) {
17701
- if (hasMoreOptions) {
17702
- setHasMoreOptions(false);
17703
- }
17745
+ },
17746
+ // eslint-disable-next-line react-hooks/exhaustive-deps
17747
+ []);
17748
+ var debouncedFetchOptions = React.useMemo(function () {
17749
+ return lodashEs.debounce(fetchOptions, 500, {
17750
+ leading: true
17751
+ });
17752
+ }, [fetchOptions]);
17753
+ React.useEffect(function () {
17754
+ fetchOptions('');
17755
+ }, [fetchOptions]);
17756
+ var handleSearch = function handleSearch(value) {
17757
+ ++fetchIdRef.current;
17758
+ setIsLoadingMore(false);
17704
17759
  if (localOptions) {
17705
- handleInternalSearch(inputValue, callback);
17760
+ var filtered = localOptions.filter(function (option) {
17761
+ return option.label.toLowerCase().includes(value.toLowerCase());
17762
+ });
17763
+ setDisplayedOptions(filtered);
17706
17764
  return;
17707
17765
  }
17708
- loadOptions(inputValue).then(function (_ref2) {
17709
- var _ref$current;
17710
- var hasMore = _ref2.hasMore,
17711
- options = _ref2.options;
17712
- if (ref.current && inputValue !== (ref == null || (_ref$current = ref.current) == null ? void 0 : _ref$current['inputRef']['value'])) {
17713
- return;
17714
- }
17715
- if (!hasFirstLoadPerformed) {
17716
- setHasFirstLoadPerformed(true);
17717
- if (!hasMore) {
17718
- setLocalOptions(options);
17719
- }
17720
- }
17721
- setHasMoreOptions(hasMore);
17722
- if (hasMoreOptionsFirstLoad === undefined) {
17723
- setHasMoreOptionsFirstLoad(hasMore);
17724
- }
17725
- callback(options);
17766
+ setDisplayedOptions([]);
17767
+ setHasMore(false);
17768
+ setNextCursor(null);
17769
+ setIsLoading(true);
17770
+ debouncedFetchOptions(value);
17771
+ };
17772
+ var handleScrollToBottom = function handleScrollToBottom() {
17773
+ if (isLoadingMore || !hasMore || !nextCursor) return;
17774
+ // Capture the search generation so a concurrent query change can invalidate this response.
17775
+ var id = fetchIdRef.current;
17776
+ setIsLoadingMore(true);
17777
+ loadOptionsRef.current(inputValue, nextCursor).then(function (_ref3) {
17778
+ var options = _ref3.options,
17779
+ more = _ref3.hasMore,
17780
+ resultCursor = _ref3.nextCursor;
17781
+ if (id !== fetchIdRef.current) return;
17782
+ setDisplayedOptions(function (prev) {
17783
+ return [].concat(prev, options);
17784
+ });
17785
+ setHasMore(more);
17786
+ setNextCursor(resultCursor != null ? resultCursor : null);
17787
+ setIsLoadingMore(false);
17788
+ })["catch"](function () {
17789
+ if (id !== fetchIdRef.current) return;
17790
+ setIsLoadingMore(false);
17726
17791
  });
17727
17792
  };
17728
17793
  var _useSelectField = useSelectField(_extends({}, props, {
@@ -17730,24 +17795,38 @@ var AsyncSelectField = function AsyncSelectField(_ref) {
17730
17795
  })),
17731
17796
  fieldProps = _useSelectField.fieldProps,
17732
17797
  selectProps = _useSelectField.selectProps;
17733
- return React__default["default"].createElement(Field, _extends({}, fieldProps), React__default["default"].createElement(AffixContainer, {
17798
+ var menuListComponents = React.useMemo(function () {
17799
+ return _extends({}, selectProps.components, {
17800
+ MenuList: AsyncMenuList
17801
+ });
17802
+ },
17803
+ // eslint-disable-next-line react-hooks/exhaustive-deps
17804
+ [selectProps.components]);
17805
+ return React__default["default"].createElement(MenuListContext.Provider, {
17806
+ value: {
17807
+ hasMoreOptions: hasMore && nextCursor === null,
17808
+ hasMoreOptionsFirstLoad: hasMoreOptionsFirstLoad,
17809
+ isLoadingMore: isLoadingMore
17810
+ }
17811
+ }, React__default["default"].createElement(Field, _extends({}, fieldProps), React__default["default"].createElement(AffixContainer, {
17734
17812
  prefix: props.prefix
17735
- }, React__default["default"].createElement(AsyncSelect__default["default"], _extends({}, selectProps, {
17736
- components: _extends({}, selectProps.components, {
17737
- MenuList: function MenuList(props) {
17738
- return React__default["default"].createElement(CustomList, _extends({}, props, {
17739
- hasMoreOptions: hasMoreOptions,
17740
- hasMoreOptionsFirstLoad: hasMoreOptionsFirstLoad
17741
- }));
17813
+ }, React__default["default"].createElement(Select__default["default"], _extends({}, selectProps, {
17814
+ components: menuListComponents,
17815
+ options: displayedOptions,
17816
+ isLoading: isLoading,
17817
+ inputValue: inputValue,
17818
+ onInputChange: function onInputChange(value, _ref4) {
17819
+ var action = _ref4.action;
17820
+ if (action !== 'input-blur' && action !== 'menu-close') {
17821
+ setInputValue(value);
17742
17822
  }
17743
- }),
17744
- cacheOptions: false,
17745
- defaultOptions: true,
17746
- loadOptions: lodashEs.debounce(handleLoadOptions, 500, {
17747
- leading: true
17748
- }),
17749
- ref: ref
17750
- }))));
17823
+ if (action === 'input-change') {
17824
+ handleSearch(value);
17825
+ }
17826
+ },
17827
+ onMenuScrollToBottom: handleScrollToBottom,
17828
+ filterOption: null
17829
+ })))));
17751
17830
  };
17752
17831
 
17753
17832
  var useDateFieldControllers = function useDateFieldControllers(_ref) {