@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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|