@snapdragonsnursery/react-components 1.17.2 → 1.17.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snapdragonsnursery/react-components",
3
- "version": "1.17.2",
3
+ "version": "1.17.4",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -34,7 +34,7 @@
34
34
  // - maxHeight?: string // max height for dropdown (default: '160px')
35
35
 
36
36
  import React from "react";
37
- import { Users } from "lucide-react";
37
+ import { Users, Search } from "lucide-react";
38
38
  import { cn } from "../lib/utils";
39
39
 
40
40
  import {
@@ -61,7 +61,28 @@ export const EmployeeSelect = ({
61
61
  allowAll = false,
62
62
  allLabel = "All employees",
63
63
  maxHeight = "160px",
64
+ // New props for server-side search
65
+ enableServerSearch = false,
66
+ onSearchChange = null,
67
+ searchResults = [],
68
+ isSearching = false,
64
69
  }) => {
70
+ const [searchTerm, setSearchTerm] = React.useState("");
71
+ const [isOpen, setIsOpen] = React.useState(false);
72
+ const [debouncedSearchTerm, setDebouncedSearchTerm] = React.useState("");
73
+
74
+ // Debounce search term for server-side search
75
+ React.useEffect(() => {
76
+ const timer = setTimeout(() => {
77
+ setDebouncedSearchTerm(searchTerm);
78
+ if (enableServerSearch && onSearchChange) {
79
+ onSearchChange(searchTerm);
80
+ }
81
+ }, 300); // 300ms debounce
82
+
83
+ return () => clearTimeout(timer);
84
+ }, [searchTerm, enableServerSearch, onSearchChange]);
85
+
65
86
  // Helper function to generate initials from full name
66
87
  const getInitials = (name) => {
67
88
  if (!name) return "??";
@@ -86,7 +107,9 @@ export const EmployeeSelect = ({
86
107
  };
87
108
 
88
109
  const processedItems = React.useMemo(() => {
89
- const list = Array.isArray(items) ? [...items] : [];
110
+ // Use server search results if enabled, otherwise use items
111
+ const sourceList = enableServerSearch ? searchResults : items;
112
+ const list = Array.isArray(sourceList) ? [...sourceList] : [];
90
113
 
91
114
  // Filter by employee status
92
115
  const filtered = list.filter((e) => {
@@ -108,8 +131,23 @@ export const EmployeeSelect = ({
108
131
  }))
109
132
  .filter((e) => e.entraId); // Remove items without entra_id
110
133
 
134
+ // Apply client-side search filter only if not using server search
135
+ const searchFiltered = enableServerSearch
136
+ ? mapped // Server search already filtered
137
+ : mapped.filter((e) => {
138
+ if (!searchTerm) return true;
139
+ const searchLower = searchTerm.toLowerCase();
140
+ return (
141
+ e.fullName.toLowerCase().includes(searchLower) ||
142
+ e.siteName.toLowerCase().includes(searchLower) ||
143
+ e.roleName.toLowerCase().includes(searchLower) ||
144
+ e.employeeId.toLowerCase().includes(searchLower) ||
145
+ e.email.toLowerCase().includes(searchLower)
146
+ );
147
+ });
148
+
111
149
  // Sort based on grouping
112
- mapped.sort((a, b) => {
150
+ searchFiltered.sort((a, b) => {
113
151
  if (groupBy === "site") {
114
152
  const siteCompare = a.siteName.localeCompare(b.siteName);
115
153
  if (siteCompare !== 0) return siteCompare;
@@ -123,8 +161,8 @@ export const EmployeeSelect = ({
123
161
  return a.fullName.localeCompare(b.fullName);
124
162
  });
125
163
 
126
- return mapped;
127
- }, [items, filter, groupBy]);
164
+ return searchFiltered;
165
+ }, [items, filter, groupBy, searchTerm, enableServerSearch, searchResults]);
128
166
 
129
167
  const selectedEmployee = processedItems.find((e) => e.entraId === value);
130
168
 
@@ -170,8 +208,12 @@ export const EmployeeSelect = ({
170
208
  } else {
171
209
  onChange?.(val);
172
210
  }
211
+ setSearchTerm(""); // Clear search when selection is made
212
+ setIsOpen(false);
173
213
  }}
174
214
  disabled={disabled}
215
+ open={isOpen}
216
+ onOpenChange={setIsOpen}
175
217
  >
176
218
  <SelectTrigger className={cn("w-full", className)}>
177
219
  <div className="flex items-center gap-2">
@@ -192,6 +234,25 @@ export const EmployeeSelect = ({
192
234
  className="[&_[data-radix-select-viewport]]:max-h-[var(--select-max-height)] [&_[data-radix-select-viewport]]:overflow-y-auto [&>button[data-radix-select-scroll-up-button]]:hidden [&>button[data-radix-select-scroll-down-button]]:hidden"
193
235
  style={{ "--select-max-height": maxHeight }}
194
236
  >
237
+ {/* Search input */}
238
+ <div className="p-2 border-b">
239
+ <div className="relative">
240
+ <Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
241
+ <input
242
+ type="text"
243
+ placeholder={enableServerSearch ? "Search employees (server-side)..." : "Search employees..."}
244
+ value={searchTerm}
245
+ onChange={(e) => setSearchTerm(e.target.value)}
246
+ className="w-full pl-8 pr-3 py-2 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 bg-background"
247
+ onClick={(e) => e.stopPropagation()}
248
+ />
249
+ {enableServerSearch && isSearching && (
250
+ <div className="absolute right-2 top-1/2 transform -translate-y-1/2">
251
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary"></div>
252
+ </div>
253
+ )}
254
+ </div>
255
+ </div>
195
256
  {allowAll && <SelectItem value="__all__">{allLabel}</SelectItem>}
196
257
  {groupBy === "site"
197
258
  ? // Group by site