SearchForm.jsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. "use client";
  2. import React from "react";
  3. import { SEARCH_SCOPE } from "@/lib/frontend/search/urlState";
  4. import SearchQueryBox from "@/components/search/form/SearchQueryBox";
  5. import SearchScopeSelect from "@/components/search/form/SearchScopeSelect";
  6. import SearchLimitSelect from "@/components/search/form/SearchLimitSelect";
  7. import SearchSingleBranchCombobox from "@/components/search/form/SearchSingleBranchCombobox";
  8. import SearchMultiBranchPicker from "@/components/search/form/SearchMultiBranchPicker";
  9. import SearchDateRangePicker from "@/components/search/form/SearchDateRangePicker";
  10. import SearchDateFilterChip from "@/components/search/form/SearchDateFilterChip";
  11. import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
  12. import { AlertCircleIcon } from "lucide-react";
  13. export default function SearchForm({
  14. branch,
  15. qDraft,
  16. onQDraftChange,
  17. onSubmit,
  18. currentQuery,
  19. isSubmitting,
  20. isAdminDev,
  21. scope,
  22. onScopeChange,
  23. onSingleBranchChange,
  24. availableBranches,
  25. branchesStatus,
  26. selectedBranches,
  27. onToggleBranch,
  28. onClearAllBranches,
  29. limit,
  30. onLimitChange,
  31. from,
  32. to,
  33. onDateRangeChange,
  34. validationError,
  35. }) {
  36. const canSearch = typeof qDraft === "string" && qDraft.trim().length > 0;
  37. // RHL-038 UI behavior:
  38. // - Single scope shows an extra combobox (branch picker).
  39. // - Multi/All does not.
  40. // - We animate the combobox container (max-width/max-height/opacity) so the
  41. // query block (input + search button) grows/shrinks smoothly.
  42. const showSingleBranchCombobox = Boolean(
  43. isAdminDev && scope === SEARCH_SCOPE.SINGLE
  44. );
  45. const hasDateFilter = Boolean(from || to);
  46. return (
  47. <div className="space-y-4">
  48. <form
  49. onSubmit={(e) => {
  50. e.preventDefault();
  51. if (!canSearch) return;
  52. onSubmit();
  53. }}
  54. className="space-y-4"
  55. >
  56. {/*
  57. Layout goal:
  58. - Desktop: everything on one line (scope + optional single combobox | query | date range | limit)
  59. - Mobile: stack to avoid horizontal overflow
  60. */}
  61. <div className="flex flex-col gap-3 lg:flex-row items-start lg:flex-nowrap">
  62. {/* Left block: scope + (animated) single-branch combobox */}
  63. {isAdminDev ? (
  64. <div className="flex items-end shrink-0">
  65. <SearchScopeSelect
  66. branch={branch}
  67. scope={scope}
  68. onScopeChange={onScopeChange}
  69. isSubmitting={isSubmitting}
  70. />
  71. {/*
  72. Animated presence (no mount/unmount):
  73. - When hidden: max-w-0/max-h-0 + opacity-0 so it takes no space.
  74. - When shown: expands to a reasonable max size.
  75. This animation drives the smooth resize of the query input block.
  76. */}
  77. <div
  78. className={[
  79. "overflow-hidden transition-all duration-200 ease-in-out",
  80. showSingleBranchCombobox
  81. ? "ml-2 max-w-44 max-h-24 opacity-100"
  82. : "ml-0 max-w-0 max-h-0 opacity-0 pointer-events-none",
  83. ].join(" ")}
  84. aria-hidden={showSingleBranchCombobox ? "false" : "true"}
  85. >
  86. <SearchSingleBranchCombobox
  87. branch={branch}
  88. branchesStatus={branchesStatus}
  89. availableBranches={availableBranches}
  90. onSingleBranchChange={onSingleBranchChange}
  91. // Disable when hidden so it can't be focused/clicked.
  92. isSubmitting={isSubmitting || !showSingleBranchCombobox}
  93. />
  94. </div>
  95. </div>
  96. ) : null}
  97. {/* Middle block: query must take the most space */}
  98. <div className="flex-1 min-w-0">
  99. <SearchQueryBox
  100. qDraft={qDraft}
  101. onQDraftChange={onQDraftChange}
  102. onSubmit={onSubmit}
  103. currentQuery={currentQuery}
  104. isSubmitting={isSubmitting}
  105. canSearch={canSearch}
  106. />
  107. </div>
  108. {/* Date range picker */}
  109. <div className="shrink-0">
  110. <SearchDateRangePicker
  111. from={from}
  112. to={to}
  113. onDateRangeChange={onDateRangeChange}
  114. isSubmitting={isSubmitting}
  115. />
  116. </div>
  117. {/* Right block: limit stays “content-sized” */}
  118. <div className="shrink-0">
  119. <SearchLimitSelect
  120. limit={limit}
  121. onLimitChange={onLimitChange}
  122. isSubmitting={isSubmitting}
  123. />
  124. </div>
  125. </div>
  126. </form>
  127. {/* Validation feedback belongs near the inputs (not in results). */}
  128. {validationError ? (
  129. <Alert variant="destructive">
  130. <AlertCircleIcon />
  131. <AlertTitle>{validationError.title}</AlertTitle>
  132. <AlertDescription>{validationError.description}</AlertDescription>
  133. </Alert>
  134. ) : null}
  135. {/* Active date filter chip (quick clear) */}
  136. {hasDateFilter ? (
  137. <div className="flex flex-wrap items-center gap-2">
  138. <span className="text-xs text-muted-foreground">Aktive Filter:</span>
  139. <SearchDateFilterChip
  140. from={from}
  141. to={to}
  142. isSubmitting={isSubmitting}
  143. onClear={() => {
  144. if (typeof onDateRangeChange !== "function") return;
  145. onDateRangeChange({ from: null, to: null });
  146. }}
  147. />
  148. </div>
  149. ) : null}
  150. {/* Multi scope branch picker remains below */}
  151. {isAdminDev && scope === SEARCH_SCOPE.MULTI ? (
  152. <SearchMultiBranchPicker
  153. branchesStatus={branchesStatus}
  154. availableBranches={availableBranches}
  155. selectedBranches={selectedBranches}
  156. onToggleBranch={onToggleBranch}
  157. onClearAllBranches={onClearAllBranches}
  158. isSubmitting={isSubmitting}
  159. />
  160. ) : null}
  161. </div>
  162. );
  163. }