@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.
@@ -16,7 +16,6 @@ import originalStylers from 'react-day-picker/dist/style.module.css';
16
16
  import 'react-toastify/dist/ReactToastify.css';
17
17
  import { isSupportedCountry, parsePhoneNumber, AsYouType, getCountryCallingCode, isValidPhoneNumber as isValidPhoneNumber$1 } from 'libphonenumber-js';
18
18
  import Select, { components } from 'react-select';
19
- import AsyncSelect from 'react-select/async';
20
19
  import { startOfDay as startOfDay$1, isDate } from 'date-fns';
21
20
  import getDay from 'date-fns/getDay';
22
21
  import getDate from 'date-fns/getDate';
@@ -17622,12 +17621,13 @@ const MultiSelectField = ({
17622
17621
 
17623
17622
  var styles$r = {"custom-list":"_cSkvD"};
17624
17623
 
17625
- const _excluded$b4 = ["children", "hasMoreOptions", "hasMoreOptionsFirstLoad"];
17624
+ const _excluded$b4 = ["children", "hasMoreOptions", "hasMoreOptionsFirstLoad", "isLoadingMore"];
17626
17625
  const CustomList = _ref => {
17627
17626
  let {
17628
17627
  children,
17629
17628
  hasMoreOptions,
17630
- hasMoreOptionsFirstLoad
17629
+ hasMoreOptionsFirstLoad,
17630
+ isLoadingMore
17631
17631
  } = _ref,
17632
17632
  props = _objectWithoutPropertiesLoose(_ref, _excluded$b4);
17633
17633
  const __ = useTranslation('AsyncSelectField');
@@ -17635,7 +17635,11 @@ const CustomList = _ref => {
17635
17635
  if (props.selectProps.inputValue === '' && typeof hasMoreOptionsFirstLoad === 'boolean') {
17636
17636
  showFooter = hasMoreOptionsFirstLoad;
17637
17637
  }
17638
- return React__default.createElement(components.MenuList, _extends({}, props), React__default.createElement(Fragment, null, children, showFooter && props.options.length > 0 && React__default.createElement(Inline, {
17638
+ return React__default.createElement(components.MenuList, _extends({}, props), React__default.createElement(Fragment, null, children, isLoadingMore ? React__default.createElement(Inline, {
17639
+ justifyContent: "center"
17640
+ }, React__default.createElement(Spinner, {
17641
+ size: 20
17642
+ })) : showFooter && props.options.length > 0 && React__default.createElement(Inline, {
17639
17643
  justifyContent: "center"
17640
17644
  }, React__default.createElement("div", {
17641
17645
  className: styles$r['custom-list']
@@ -17643,54 +17647,105 @@ const CustomList = _ref => {
17643
17647
  };
17644
17648
 
17645
17649
  const _excluded$b3 = ["loadOptions"];
17650
+ const MenuListContext = React__default.createContext({
17651
+ hasMoreOptions: false,
17652
+ hasMoreOptionsFirstLoad: null,
17653
+ isLoadingMore: false
17654
+ });
17655
+ // Defined outside the component so the reference is stable — prevents MenuList from
17656
+ // unmounting/remounting (and losing scroll position) on every state change.
17657
+ const AsyncMenuList = menuListProps => {
17658
+ const ctx = React__default.useContext(MenuListContext);
17659
+ return React__default.createElement(CustomList, _extends({}, menuListProps, ctx));
17660
+ };
17646
17661
  /**
17647
17662
  * 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.
17648
- * Instead of passing a `options` props, this component requires a `loadOptions` prop.
17663
+ * Instead of passing an `options` prop, this component requires a `loadOptions` prop.
17664
+ * 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.
17649
17665
  * */
17650
17666
  const AsyncSelectField = _ref => {
17651
17667
  let {
17652
17668
  loadOptions
17653
17669
  } = _ref,
17654
17670
  props = _objectWithoutPropertiesLoose(_ref, _excluded$b3);
17655
- const [hasMoreOptions, setHasMoreOptions] = useState(false);
17656
- const [hasMoreOptionsFirstLoad, setHasMoreOptionsFirstLoad] = useState();
17657
- const [hasFirstLoadPerformed, setHasFirstLoadPerformed] = useState(false);
17671
+ const [displayedOptions, setDisplayedOptions] = useState([]);
17672
+ const [isLoading, setIsLoading] = useState(false);
17673
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
17674
+ const [inputValue, setInputValue] = useState('');
17675
+ const [hasMore, setHasMore] = useState(false);
17676
+ const [nextCursor, setNextCursor] = useState(null);
17658
17677
  const [localOptions, setLocalOptions] = useState(null);
17659
- const ref = useRef(null);
17660
- const handleInternalSearch = (inputValue, callback) => {
17661
- if (!localOptions) {
17662
- return;
17663
- }
17664
- const filteredOptions = localOptions.filter(option => option.label.toLowerCase().includes(inputValue.toLocaleLowerCase()));
17665
- callback(filteredOptions);
17666
- };
17667
- const handleLoadOptions = (inputValue, callback) => {
17668
- if (hasMoreOptions) {
17669
- setHasMoreOptions(false);
17670
- }
17678
+ const [hasMoreOptionsFirstLoad, setHasMoreOptionsFirstLoad] = useState(null);
17679
+ const loadOptionsRef = useRef(loadOptions);
17680
+ useEffect(() => {
17681
+ loadOptionsRef.current = loadOptions;
17682
+ }, [loadOptions]);
17683
+ const fetchIdRef = useRef(0);
17684
+ const firstLoadDoneRef = useRef(false);
17685
+ // All dependencies are refs and stable state setters — safe with empty dep array.
17686
+ const fetchOptions = useCallback(input => {
17687
+ const id = ++fetchIdRef.current;
17688
+ setIsLoading(true);
17689
+ loadOptionsRef.current(input, null).then(({
17690
+ options,
17691
+ hasMore: more,
17692
+ nextCursor: resultCursor
17693
+ }) => {
17694
+ if (id !== fetchIdRef.current) return;
17695
+ if (!firstLoadDoneRef.current) {
17696
+ firstLoadDoneRef.current = true;
17697
+ setHasMoreOptionsFirstLoad(more && resultCursor == null);
17698
+ if (!more) setLocalOptions(options);
17699
+ }
17700
+ setDisplayedOptions(options);
17701
+ setHasMore(more);
17702
+ setNextCursor(resultCursor != null ? resultCursor : null);
17703
+ setIsLoading(false);
17704
+ }).catch(() => {
17705
+ if (id !== fetchIdRef.current) return;
17706
+ setIsLoading(false);
17707
+ });
17708
+ },
17709
+ // eslint-disable-next-line react-hooks/exhaustive-deps
17710
+ []);
17711
+ const debouncedFetchOptions = useMemo(() => debounce(fetchOptions, 500, {
17712
+ leading: true
17713
+ }), [fetchOptions]);
17714
+ useEffect(() => {
17715
+ fetchOptions('');
17716
+ }, [fetchOptions]);
17717
+ const handleSearch = value => {
17718
+ ++fetchIdRef.current;
17719
+ setIsLoadingMore(false);
17671
17720
  if (localOptions) {
17672
- handleInternalSearch(inputValue, callback);
17721
+ const filtered = localOptions.filter(option => option.label.toLowerCase().includes(value.toLowerCase()));
17722
+ setDisplayedOptions(filtered);
17673
17723
  return;
17674
17724
  }
17675
- loadOptions(inputValue).then(({
17676
- hasMore,
17677
- options
17725
+ setDisplayedOptions([]);
17726
+ setHasMore(false);
17727
+ setNextCursor(null);
17728
+ setIsLoading(true);
17729
+ debouncedFetchOptions(value);
17730
+ };
17731
+ const handleScrollToBottom = () => {
17732
+ if (isLoadingMore || !hasMore || !nextCursor) return;
17733
+ // Capture the search generation so a concurrent query change can invalidate this response.
17734
+ const id = fetchIdRef.current;
17735
+ setIsLoadingMore(true);
17736
+ loadOptionsRef.current(inputValue, nextCursor).then(({
17737
+ options,
17738
+ hasMore: more,
17739
+ nextCursor: resultCursor
17678
17740
  }) => {
17679
- var _ref$current;
17680
- if (ref.current && inputValue !== (ref == null || (_ref$current = ref.current) == null ? void 0 : _ref$current['inputRef']['value'])) {
17681
- return;
17682
- }
17683
- if (!hasFirstLoadPerformed) {
17684
- setHasFirstLoadPerformed(true);
17685
- if (!hasMore) {
17686
- setLocalOptions(options);
17687
- }
17688
- }
17689
- setHasMoreOptions(hasMore);
17690
- if (hasMoreOptionsFirstLoad === undefined) {
17691
- setHasMoreOptionsFirstLoad(hasMore);
17692
- }
17693
- callback(options);
17741
+ if (id !== fetchIdRef.current) return;
17742
+ setDisplayedOptions(prev => [...prev, ...options]);
17743
+ setHasMore(more);
17744
+ setNextCursor(resultCursor != null ? resultCursor : null);
17745
+ setIsLoadingMore(false);
17746
+ }).catch(() => {
17747
+ if (id !== fetchIdRef.current) return;
17748
+ setIsLoadingMore(false);
17694
17749
  });
17695
17750
  };
17696
17751
  const {
@@ -17699,22 +17754,37 @@ const AsyncSelectField = _ref => {
17699
17754
  } = useSelectField(_extends({}, props, {
17700
17755
  options: []
17701
17756
  }));
17702
- return React__default.createElement(Field, _extends({}, fieldProps), React__default.createElement(AffixContainer, {
17757
+ const menuListComponents = useMemo(() => _extends({}, selectProps.components, {
17758
+ MenuList: AsyncMenuList
17759
+ }),
17760
+ // eslint-disable-next-line react-hooks/exhaustive-deps
17761
+ [selectProps.components]);
17762
+ return React__default.createElement(MenuListContext.Provider, {
17763
+ value: {
17764
+ hasMoreOptions: hasMore && nextCursor === null,
17765
+ hasMoreOptionsFirstLoad,
17766
+ isLoadingMore
17767
+ }
17768
+ }, React__default.createElement(Field, _extends({}, fieldProps), React__default.createElement(AffixContainer, {
17703
17769
  prefix: props.prefix
17704
- }, React__default.createElement(AsyncSelect, _extends({}, selectProps, {
17705
- components: _extends({}, selectProps.components, {
17706
- MenuList: props => React__default.createElement(CustomList, _extends({}, props, {
17707
- hasMoreOptions: hasMoreOptions,
17708
- hasMoreOptionsFirstLoad: hasMoreOptionsFirstLoad
17709
- }))
17710
- }),
17711
- cacheOptions: false,
17712
- defaultOptions: true,
17713
- loadOptions: debounce(handleLoadOptions, 500, {
17714
- leading: true
17715
- }),
17716
- ref: ref
17717
- }))));
17770
+ }, React__default.createElement(Select, _extends({}, selectProps, {
17771
+ components: menuListComponents,
17772
+ options: displayedOptions,
17773
+ isLoading: isLoading,
17774
+ inputValue: inputValue,
17775
+ onInputChange: (value, {
17776
+ action
17777
+ }) => {
17778
+ if (action !== 'input-blur' && action !== 'menu-close') {
17779
+ setInputValue(value);
17780
+ }
17781
+ if (action === 'input-change') {
17782
+ handleSearch(value);
17783
+ }
17784
+ },
17785
+ onMenuScrollToBottom: handleScrollToBottom,
17786
+ filterOption: null
17787
+ })))));
17718
17788
  };
17719
17789
 
17720
17790
  const useDateFieldControllers = ({