useListItemCheckboxState.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import {useCallback, useEffect, useMemo, useState} from 'react';
  2. import useFeedbackQueryKeys from 'sentry/components/feedback/useFeedbackQueryKeys';
  3. interface Props {
  4. hits: number;
  5. knownIds: string[];
  6. }
  7. /**
  8. * We can either have a list of ids, or have all selected.
  9. * When all is selected we may, or may not, have all ids loaded into the browser
  10. */
  11. type State = {ids: Set<string>} | {all: true};
  12. interface Return {
  13. /**
  14. * How many ids are selected
  15. */
  16. countSelected: number;
  17. /**
  18. * Ensure nothing is selected, no matter the state prior
  19. */
  20. deselectAll: () => void;
  21. /**
  22. * True if all are selected
  23. *
  24. * When some are selected returns 'indeterminate'
  25. */
  26. isAllSelected: 'indeterminate' | boolean;
  27. /**
  28. * True if one or more are selected
  29. */
  30. isAnySelected: boolean;
  31. /**
  32. * True if this specific id is selected
  33. */
  34. isSelected: (id: string) => 'all-selected' | boolean;
  35. /**
  36. * Record that all are selected, wether or not all feedback ids are loaded or not
  37. */
  38. selectAll: () => void;
  39. /**
  40. * The list of specifically selected ids, or 'all' to save space
  41. */
  42. selectedIds: 'all' | string[];
  43. /**
  44. * Toggle if a feedback is selected or not
  45. * It's not possible to toggle when all are selected, but not all are loaded
  46. */
  47. toggleSelected: (id: string) => void;
  48. }
  49. export default function useListItemCheckboxState({hits, knownIds}: Props): Return {
  50. const {getListQueryKey} = useFeedbackQueryKeys();
  51. const [state, setState] = useState<State>({ids: new Set()});
  52. const listQueryKey = getListQueryKey();
  53. useEffect(() => {
  54. // Reset the state when the list changes
  55. setState({ids: new Set()});
  56. }, [listQueryKey]);
  57. const selectAll = useCallback(() => {
  58. // Record that the virtual "all" list is enabled.
  59. setState({all: true});
  60. }, []);
  61. const deselectAll = useCallback(() => {
  62. setState({ids: new Set()});
  63. }, []);
  64. const toggleSelected = useCallback(
  65. (id: string) => {
  66. setState(prev => {
  67. if ('all' in prev && hits !== knownIds.length) {
  68. // Unable to toggle individual items when "all" are selected, but not
  69. // all items are loaded. We can't omit one item from this virtual list.
  70. }
  71. // If all is selected, then we're toggling this one off
  72. if ('all' in prev) {
  73. const ids = new Set(knownIds);
  74. ids.delete(id);
  75. return {ids};
  76. }
  77. // We have a list of ids, so we enable/disable as needed
  78. const ids = prev.ids;
  79. if (ids.has(id)) {
  80. ids.delete(id);
  81. } else {
  82. ids.add(id);
  83. }
  84. return {ids};
  85. });
  86. },
  87. [hits, knownIds]
  88. );
  89. const isSelected = useCallback(
  90. (id: string) => {
  91. // If we are using the virtual "all", and we don't have everything loaded,
  92. // return the sentinal value 'all-selected'
  93. if ('all' in state && hits !== knownIds.length) {
  94. return 'all-selected';
  95. }
  96. // If "all" is selected
  97. if ('all' in state) {
  98. return true;
  99. }
  100. // Otherwise true/value is fine
  101. return state.ids.has(id);
  102. },
  103. [state, hits, knownIds]
  104. );
  105. const isAllSelected = useMemo(() => {
  106. if ('all' in state) {
  107. return true;
  108. }
  109. if (state.ids.size === 0) {
  110. return false;
  111. }
  112. if (state.ids.size === hits) {
  113. return true;
  114. }
  115. return 'indeterminate';
  116. }, [state, hits]);
  117. const isAnySelected = useMemo(() => 'all' in state || state.ids.size > 0, [state]);
  118. const selectedIds = useMemo(() => {
  119. return 'all' in state ? 'all' : Array.from(state.ids);
  120. }, [state]);
  121. return {
  122. countSelected: 'all' in state ? hits : selectedIds.length,
  123. deselectAll,
  124. isAllSelected,
  125. isAnySelected,
  126. isSelected,
  127. selectAll,
  128. selectedIds,
  129. toggleSelected,
  130. };
  131. }