toolbar.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import { useTranslation } from 'react-i18next';
  2. import { Row } from 'app/design/view';
  3. import { NeoButton, NeoButtonLink } from 'app/design/controls';
  4. import { Input } from 'app/design/controls';
  5. import DropdownMenu from 'app/ui/atoms/dropdown-menu';
  6. import { hasSearchFilter, mapFilterDropdownItems } from './filters';
  7. import { useGrid } from './context';
  8. /** Shared chrome for independent toolbar actions (add / menu / link) — same row, same size. */
  9. const INDEPENDENT_BUTTON = {
  10. style: 'borderedProminent',
  11. controlSize: 'regular',
  12. };
  13. const BULK_BUTTON = {
  14. style: 'bordered',
  15. controlSize: 'regular',
  16. };
  17. /**
  18. * Independent "add" action that opens a dropdown of create callbacks
  19. * (each loads a form into the bottom sheet).
  20. */
  21. function MultiAdd({ data }) {
  22. const openGridModal = useGrid((state) => state.openGridModal);
  23. return (
  24. <DropdownMenu
  25. items={data.values}
  26. onSelect={(item) => openGridModal({ callback: item.callback, title: ' ' })}
  27. >
  28. <NeoButton {...INDEPENDENT_BUTTON} image="Plus" label={data.title} interactive />
  29. </DropdownMenu>
  30. );
  31. }
  32. /**
  33. * UNA `actions.bulk` is a list of `{ name, … }`. Only calculate / delete /
  34. * Stripe / credits are rendered; anything else is skipped.
  35. * Handlers read the selection from the store, so nothing is passed down.
  36. */
  37. function BulkGridButton({ item }) {
  38. const { t } = useTranslation();
  39. const deleteSelected = useGrid((state) => state.deleteSelected);
  40. const calculateSelected = useGrid((state) => state.calculateSelected);
  41. const checkoutSelected = useGrid((state) => state.checkoutSelected);
  42. switch (item.name) {
  43. case 'calculate':
  44. return (
  45. <NeoButton {...BULK_BUTTON} label={t('Calculate')} onPress={calculateSelected} />
  46. );
  47. case 'delete':
  48. return (
  49. <NeoButton {...BULK_BUTTON} image="Trash" label={t('Delete')} onPress={deleteSelected} />
  50. );
  51. case 'stripe_v3':
  52. return (
  53. <NeoButton
  54. {...BULK_BUTTON}
  55. label={t('Checkout with Stripe')}
  56. onPress={() => checkoutSelected('stripe_v3', item.payment_type)}
  57. />
  58. );
  59. case 'credits':
  60. return (
  61. <NeoButton
  62. {...BULK_BUTTON}
  63. label={t('Checkout with Credits')}
  64. onPress={() => checkoutSelected('credits')}
  65. />
  66. );
  67. default:
  68. return null;
  69. }
  70. }
  71. /**
  72. * Independent toolbar controls (`modal` button, `menu` dropdown, `link`).
  73. * Type switch — not a props map like bulk buttons.
  74. */
  75. function IndependentGridButton({ item }) {
  76. const { t } = useTranslation();
  77. const openGridModal = useGrid((state) => state.openGridModal);
  78. switch (item.type) {
  79. case 'modal':
  80. return (
  81. <NeoButton
  82. {...INDEPENDENT_BUTTON}
  83. image="Plus"
  84. label={t(item?.title || 'Add new')}
  85. onPress={() => openGridModal(item)}
  86. />
  87. );
  88. case 'menu':
  89. return (
  90. <MultiAdd data={item} />
  91. );
  92. case 'link':
  93. return (
  94. <NeoButtonLink
  95. href={item.link || item.url}
  96. {...INDEPENDENT_BUTTON}
  97. label={item.title}
  98. />
  99. );
  100. default:
  101. return null;
  102. }
  103. }
  104. /**
  105. * Grid chrome: bulk actions (only when rows are selected), filters/search,
  106. * and independent add/link actions.
  107. */
  108. export default function GridToolbar({
  109. actionsBulk,
  110. actionsIndependent,
  111. dropdownFilterKeys,
  112. settings,
  113. selectedFilters,
  114. handleFilter,
  115. handleSearch,
  116. }) {
  117. const { t } = useTranslation();
  118. /** Boolean selector — the toolbar only re-renders when the selection empties or fills. */
  119. const hasSelection = useGrid((state) => state.selected.size > 0);
  120. return (
  121. <Row className="w-full flex-wrap items-center justify-between gap-3 mt-2 mb-4">
  122. {/* Bulk actions operate on the current selection, so they lead the toolbar
  123. and stay hidden until something is selected rather than sitting disabled. */}
  124. {hasSelection ? (
  125. <Row className="flex-wrap gap-2 items-center shrink-0">
  126. {actionsBulk.map((item) => (
  127. <BulkGridButton key={item.name} item={item} />
  128. ))}
  129. </Row>
  130. ) : null}
  131. {(dropdownFilterKeys.length > 0 || hasSearchFilter(settings.filters)) ? (
  132. <Row className="flex-1 min-w-48 flex-wrap gap-2 items-center">
  133. {dropdownFilterKeys.map((filterKey) => {
  134. const items = mapFilterDropdownItems(settings.filters[filterKey]);
  135. const selectedFilter = selectedFilters[filterKey];
  136. return (
  137. <DropdownMenu
  138. key={filterKey}
  139. items={items}
  140. onSelect={(oItem) => handleFilter(filterKey, oItem)}
  141. >
  142. <NeoButton
  143. style="bordered"
  144. controlSize="small"
  145. label={selectedFilter?.title ?? items[0]?.title}
  146. interactive
  147. />
  148. </DropdownMenu>
  149. );
  150. })}
  151. {hasSearchFilter(settings.filters) ? (
  152. <Input size="small" placeholder={t('Search')} name="search" onChangeText={handleSearch} className="min-w-40 flex-1" />
  153. ) : null}
  154. </Row>
  155. ) : null}
  156. <Row className="flex-wrap gap-2 items-center shrink-0">
  157. {actionsIndependent.map((item) => (
  158. <IndependentGridButton key={`btn-${item.name}`} item={item} />
  159. ))}
  160. </Row>
  161. </Row>
  162. );
  163. }