useListItemCheckboxState.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  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 {listQueryKey} = useFeedbackQueryKeys();
  51. const [state, setState] = useState<State>({ids: new Set()});
  52. useEffect(() => {
  53. // Reset the state when the list changes
  54. setState({ids: new Set()});
  55. }, [listQueryKey]);
  56. const selectAll = useCallback(() => {
  57. // Record that the virtual "all" list is enabled.
  58. setState({all: true});
  59. }, []);
  60. const deselectAll = useCallback(() => {
  61. setState({ids: new Set()});
  62. }, []);
  63. const toggleSelected = useCallback(
  64. (id: string) => {
  65. setState(prev => {
  66. if ('all' in prev && hits !== knownIds.length) {
  67. // Unable to toggle individual items when "all" are selected, but not
  68. // all items are loaded. We can't omit one item from this virtual list.
  69. }
  70. // If all is selected, then we're toggling this one off
  71. if ('all' in prev) {
  72. const ids = new Set(knownIds);
  73. ids.delete(id);
  74. return {ids};
  75. }
  76. // We have a list of ids, so we enable/disable as needed
  77. const ids = prev.ids;
  78. if (ids.has(id)) {
  79. ids.delete(id);
  80. } else {
  81. ids.add(id);
  82. }
  83. return {ids};
  84. });
  85. },
  86. [hits, knownIds]
  87. );
  88. const isSelected = useCallback(
  89. (id: string) => {
  90. // If we are using the virtual "all", and we don't have everything loaded,
  91. // return the sentinal value 'all-selected'
  92. if ('all' in state && hits !== knownIds.length) {
  93. return 'all-selected';
  94. }
  95. // If "all" is selected
  96. if ('all' in state) {
  97. return true;
  98. }
  99. // Otherwise true/value is fine
  100. return state.ids.has(id);
  101. },
  102. [state, hits, knownIds]
  103. );
  104. const isAllSelected = useMemo(() => {
  105. if ('all' in state) {
  106. return true;
  107. }
  108. if (state.ids.size === 0) {
  109. return false;
  110. }
  111. if (state.ids.size === hits) {
  112. return true;
  113. }
  114. return 'indeterminate';
  115. }, [state, hits]);
  116. const isAnySelected = useMemo(() => 'all' in state || state.ids.size > 0, [state]);
  117. const selectedIds = useMemo(() => {
  118. return 'all' in state ? 'all' : Array.from(state.ids);
  119. }, [state]);
  120. return {
  121. countSelected: 'all' in state ? hits : selectedIds.length,
  122. deselectAll,
  123. isAllSelected,
  124. isAnySelected,
  125. isSelected,
  126. selectAll,
  127. selectedIds,
  128. toggleSelected,
  129. };
  130. }