@workos-inc/widgets 1.1.5 → 1.2.0

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.
Files changed (82) hide show
  1. package/dist/cjs/lib/organization-switcher.d.ts +10 -1
  2. package/dist/cjs/lib/organization-switcher.d.ts.map +1 -1
  3. package/dist/cjs/lib/organization-switcher.js +31 -3
  4. package/dist/cjs/lib/organization-switcher.js.map +1 -1
  5. package/dist/cjs/workos-widgets.client.d.ts +6 -0
  6. package/dist/cjs/workos-widgets.client.d.ts.map +1 -1
  7. package/dist/cjs/workos-widgets.client.js +23 -3
  8. package/dist/cjs/workos-widgets.client.js.map +1 -1
  9. package/dist/esm/lib/organization-switcher.d.ts +10 -1
  10. package/dist/esm/lib/organization-switcher.d.ts.map +1 -1
  11. package/dist/esm/lib/organization-switcher.js +31 -3
  12. package/dist/esm/lib/organization-switcher.js.map +1 -1
  13. package/dist/esm/workos-widgets.client.d.ts +6 -0
  14. package/dist/esm/workos-widgets.client.d.ts.map +1 -1
  15. package/dist/esm/workos-widgets.client.js +25 -5
  16. package/dist/esm/workos-widgets.client.js.map +1 -1
  17. package/package.json +8 -9
  18. package/src/api/api-provider.tsx +0 -158
  19. package/src/api/constants.ts +0 -1
  20. package/src/api/endpoint.ts +0 -3097
  21. package/src/api/errors.ts +0 -48
  22. package/src/api/index.ts +0 -2
  23. package/src/api/utils.ts +0 -42
  24. package/src/api/widgets-api-client.ts +0 -87
  25. package/src/card-list.tsx +0 -26
  26. package/src/index.ts +0 -9
  27. package/src/lib/add-mfa-dialog.tsx +0 -379
  28. package/src/lib/api/config.ts +0 -9
  29. package/src/lib/api/user.ts +0 -98
  30. package/src/lib/change-password-dialog.tsx +0 -290
  31. package/src/lib/constants.ts +0 -3
  32. package/src/lib/copy-button.tsx +0 -53
  33. package/src/lib/delete-user-dialog.tsx +0 -110
  34. package/src/lib/edit-user-profile-dialog.tsx +0 -181
  35. package/src/lib/edit-user-role-dialog.tsx +0 -178
  36. package/src/lib/elements.tsx +0 -428
  37. package/src/lib/elevated-access.tsx +0 -261
  38. package/src/lib/error-boundary.tsx +0 -166
  39. package/src/lib/errors.ts +0 -49
  40. package/src/lib/generic-error.tsx +0 -70
  41. package/src/lib/icon-panel.tsx +0 -26
  42. package/src/lib/icons.tsx +0 -21
  43. package/src/lib/invite-user-dialog.tsx +0 -327
  44. package/src/lib/logout-all-sessions-dialog.tsx +0 -82
  45. package/src/lib/logout-dialog.tsx +0 -85
  46. package/src/lib/marker.tsx +0 -39
  47. package/src/lib/oauth-icons.tsx +0 -138
  48. package/src/lib/organization-switcher.tsx +0 -156
  49. package/src/lib/otp-input.tsx +0 -276
  50. package/src/lib/resend-invite-dialog.tsx +0 -145
  51. package/src/lib/reset-mfa-dialog.tsx +0 -104
  52. package/src/lib/revoke-invite-dialog.tsx +0 -111
  53. package/src/lib/save-button.tsx +0 -113
  54. package/src/lib/search-provider.tsx +0 -51
  55. package/src/lib/set-password-dialog.tsx +0 -204
  56. package/src/lib/use-dialog-close.tsx +0 -19
  57. package/src/lib/use-is-hydrated.ts +0 -13
  58. package/src/lib/use-layout-effect.ts +0 -6
  59. package/src/lib/use-security-settings.tsx +0 -49
  60. package/src/lib/user-actions-dropdown.tsx +0 -157
  61. package/src/lib/user-profile.tsx +0 -227
  62. package/src/lib/user-security.tsx +0 -187
  63. package/src/lib/user-sessions.tsx +0 -204
  64. package/src/lib/users-filter.tsx +0 -62
  65. package/src/lib/users-management-context.tsx +0 -74
  66. package/src/lib/users-management-state.ts +0 -165
  67. package/src/lib/users-management.tsx +0 -594
  68. package/src/lib/users-search.tsx +0 -73
  69. package/src/lib/utils.ts +0 -131
  70. package/src/lib/widgets-context.ts +0 -29
  71. package/src/organization-switcher.client.tsx +0 -81
  72. package/src/user-profile.client.tsx +0 -55
  73. package/src/user-security.client.tsx +0 -55
  74. package/src/user-sessions.client.tsx +0 -100
  75. package/src/users-management.client.tsx +0 -73
  76. package/src/workos-widgets.client.tsx +0 -75
  77. /package/{src → dist/css}/base.css +0 -0
  78. /package/{src → dist/css}/lib/card-list.css +0 -0
  79. /package/{src → dist/css}/lib/marker.css +0 -0
  80. /package/{src → dist/css}/lib/save-button.css +0 -0
  81. /package/{src → dist/css}/styles.css +0 -0
  82. /package/{src → dist/css}/users-management.css +0 -0
@@ -1,594 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import {
5
- Box,
6
- Flex,
7
- Grid,
8
- Select,
9
- Separator,
10
- Table,
11
- Text,
12
- type TextProps,
13
- VisuallyHidden,
14
- } from "@radix-ui/themes";
15
- import {
16
- Avatar,
17
- Badge,
18
- IconButton,
19
- PrimaryButton,
20
- SecondaryButton,
21
- SelectContent,
22
- SelectItem,
23
- SelectTrigger,
24
- Skeleton,
25
- TextField,
26
- } from "./elements";
27
- import { InviteUserDialog } from "./invite-user-dialog";
28
- import { SearchProvider, useSearchContext } from "./search-provider";
29
- import { useIsHydrated } from "./use-is-hydrated";
30
- import { UserActionsDropdown } from "./user-actions-dropdown";
31
- import { UsersFilter } from "./users-filter";
32
- import { UsersSearch } from "./users-search";
33
- import { getBestName, getComparativeReadableDate } from "./utils";
34
- import { USER_ROW_LIMIT } from "./constants";
35
- import { useUsersManagementContext } from "./users-management-context";
36
- import clsx from "clsx";
37
- import { Member, MemberRole, MembersQueryResult } from "../api";
38
- import { GenericError } from "./generic-error";
39
-
40
- interface UsersManagementProps {
41
- rolesData: MemberRole[];
42
- userData: MembersQueryResult;
43
- disableRolesFilter?: boolean;
44
- // When the users list is loading new users
45
- isPending: boolean;
46
- }
47
-
48
- export const UsersManagement = ({
49
- userData,
50
- rolesData,
51
- isPending,
52
- disableRolesFilter,
53
- }: UsersManagementProps) => {
54
- const users = userData?.data ?? [];
55
- const usersCount = users?.length ?? 0;
56
- const isHydrated = useIsHydrated();
57
- const { listMetadata: pagination = {} } = userData;
58
- const { dispatch } = useUsersManagementContext();
59
-
60
- // we only want to show the loading indicator for some buttons if the request
61
- // is still pending after 500ms. If the request is fast enough the indicator
62
- // is a bit jarring.
63
- const [deferredLoading, setDeferredLoading] = React.useState(false);
64
- React.useEffect(() => {
65
- if (isPending) {
66
- const timeoutId = window.setTimeout(() => {
67
- setDeferredLoading(true);
68
- }, 500);
69
- return () => {
70
- window.clearTimeout(timeoutId);
71
- };
72
- } else {
73
- setDeferredLoading(false);
74
- }
75
- }, [isPending]);
76
-
77
- const showPagination = !!(pagination.before || pagination.after);
78
-
79
- return (
80
- <SearchProvider>
81
- <UsersManagementRoot>
82
- <Grid columns="1fr auto" gap="2">
83
- <Flex gap="2" align="center">
84
- <Box flexBasis="380px" flexGrow="0" flexShrink="1">
85
- <UsersSearch />
86
- </Box>
87
- <Box flexGrow="0" flexShrink="0">
88
- <UsersFilter roles={rolesData} disabled={disableRolesFilter} />
89
- </Box>
90
- </Flex>
91
- <Box flexGrow="0" flexShrink="0" style={{ placeSelf: "flex-end" }}>
92
- <InviteUserDialog>
93
- <PrimaryButton>Invite user</PrimaryButton>
94
- </InviteUserDialog>
95
- </Box>
96
- </Grid>
97
- <Table.Root variant="ghost" size="1">
98
- <Table.Header>
99
- <Table.Row>
100
- <Table.ColumnHeaderCell width="260px">
101
- User
102
- </Table.ColumnHeaderCell>
103
- <Table.ColumnHeaderCell width="100px">
104
- Role
105
- </Table.ColumnHeaderCell>
106
- <Table.ColumnHeaderCell width="140px">
107
- Last active
108
- </Table.ColumnHeaderCell>
109
- <Table.ColumnHeaderCell width="28px" />
110
- </Table.Row>
111
- </Table.Header>
112
-
113
- <Table.Body
114
- style={{
115
- transition: `opacity 0.2s ease-out ${isPending ? "0.2s" : "0s"}`,
116
- opacity: isPending && usersCount > 0 ? 0.5 : 1,
117
- }}
118
- >
119
- {users.length > 0 ? (
120
- users.map((user) => {
121
- // TODO only support one role for now
122
- const userRole = user?.roles?.at(0)?.name;
123
- const userDisplayName = getBestName(user);
124
- const dimText =
125
- user.status === "InviteRevoked" ||
126
- user.status === "InviteExpired";
127
- return (
128
- <Table.Row key={user.id} align="center">
129
- <Table.RowHeaderCell>
130
- <Flex
131
- align="center"
132
- gap="3"
133
- overflow="hidden"
134
- height="var(--space-7)"
135
- >
136
- <Avatar
137
- size="2"
138
- fallback={<FallbackUserIcon />}
139
- src={user.profilePictureUrl ?? undefined}
140
- dim={dimText}
141
- />
142
-
143
- {userDisplayName ? (
144
- <Flex
145
- direction="column"
146
- align="start"
147
- height="var(--space-7)"
148
- justify="center"
149
- overflow="hidden"
150
- >
151
- <Flex gap="2" align="center" minWidth="0">
152
- <TableCellText dim={dimText}>
153
- {userDisplayName}
154
- </TableCellText>
155
- <UserBadge user={user} />
156
- </Flex>
157
- <TableCellText
158
- level="secondary"
159
- title={user.email}
160
- dim={dimText}
161
- >
162
- {user.email}
163
- </TableCellText>
164
- </Flex>
165
- ) : (
166
- <Flex gap="2" align="center" minWidth="0">
167
- <TableCellText dim={dimText} title={user.email}>
168
- {user.email}
169
- </TableCellText>
170
- <UserBadge user={user} />
171
- </Flex>
172
- )}
173
- </Flex>
174
- </Table.RowHeaderCell>
175
- <Table.Cell>
176
- <TableCellText dim={dimText}>
177
- {userRole || (
178
- <>
179
- <VisuallyHidden>No roles assigned</VisuallyHidden>
180
- <span aria-hidden style={{ userSelect: "none" }}>
181
-
182
- </span>
183
- </>
184
- )}
185
- </TableCellText>
186
- </Table.Cell>
187
- <Table.Cell>
188
- <LastActive user={user} isHydrated={isHydrated} />
189
- </Table.Cell>
190
- <Table.Cell justify="end">
191
- <UserActionsDropdown user={user}>
192
- <IconButton title="User actions">
193
- <VisuallyHidden>User actions</VisuallyHidden>
194
- <svg
195
- xmlns="http://www.w3.org/2000/svg"
196
- fill="none"
197
- viewBox="0 0 24 24"
198
- width="16"
199
- height="16"
200
- strokeWidth={1.5}
201
- stroke="currentColor"
202
- aria-hidden
203
- >
204
- <path
205
- strokeLinecap="round"
206
- strokeLinejoin="round"
207
- d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"
208
- />
209
- </svg>
210
- </IconButton>
211
- </UserActionsDropdown>
212
- </Table.Cell>
213
- </Table.Row>
214
- );
215
- })
216
- ) : (
217
- <Table.Row align="center">
218
- <Table.Cell colSpan={4}>
219
- <UsersManagementEmptyState isPending={isPending} />
220
- </Table.Cell>
221
- </Table.Row>
222
- )}
223
- </Table.Body>
224
- </Table.Root>
225
-
226
- {showPagination ? (
227
- <Flex gap="2" justify="end">
228
- <SecondaryButton
229
- size="1"
230
- disabled={!pagination.after || isPending || undefined}
231
- loading={deferredLoading}
232
- onClick={() => {
233
- if (pagination.after) {
234
- dispatch({
235
- type: "SET_PAGINATION",
236
- pagination: { after: pagination.after },
237
- });
238
- }
239
- }}
240
- >
241
- Previous
242
- </SecondaryButton>
243
- <SecondaryButton
244
- size="1"
245
- disabled={!pagination.before || isPending || undefined}
246
- loading={deferredLoading}
247
- onClick={() => {
248
- if (pagination.before) {
249
- dispatch({
250
- type: "SET_PAGINATION",
251
- pagination: { before: pagination.before },
252
- });
253
- }
254
- }}
255
- >
256
- Next
257
- </SecondaryButton>
258
- </Flex>
259
- ) : null}
260
- </UsersManagementRoot>
261
- </SearchProvider>
262
- );
263
- };
264
-
265
- export function UsersManagementLoading() {
266
- return (
267
- <UsersManagementRoot>
268
- <Grid columns="1fr auto" gap="2">
269
- <Flex gap="2" align="center">
270
- <Skeleton loading>
271
- <Box flexBasis="380px" flexGrow="0" flexShrink="1">
272
- <TextField />
273
- </Box>
274
- </Skeleton>
275
- <Skeleton loading>
276
- <Box flexGrow="0" flexShrink="0">
277
- <Select.Root value="all" onValueChange={() => void 0}>
278
- <SelectTrigger>All</SelectTrigger>
279
- <SelectContent>
280
- <SelectItem value="all">All</SelectItem>
281
- </SelectContent>
282
- </Select.Root>
283
- </Box>
284
- </Skeleton>
285
- </Flex>
286
- <Skeleton loading>
287
- <Box flexGrow="0" flexShrink="0" style={{ placeSelf: "flex-end" }}>
288
- <PrimaryButton>Invite user</PrimaryButton>
289
- </Box>
290
- </Skeleton>
291
- </Grid>
292
- <Table.Root variant="ghost" size="1">
293
- <Table.Header>
294
- <Table.Row>
295
- <Table.ColumnHeaderCell width="260px">
296
- <Skeleton loading>User</Skeleton>
297
- </Table.ColumnHeaderCell>
298
- <Table.ColumnHeaderCell width="100px">
299
- <Skeleton>Role</Skeleton>
300
- </Table.ColumnHeaderCell>
301
- <Table.ColumnHeaderCell width="140px">
302
- <Skeleton>Last active</Skeleton>
303
- </Table.ColumnHeaderCell>
304
- <Table.ColumnHeaderCell width="28px" />
305
- </Table.Row>
306
- </Table.Header>
307
-
308
- <Table.Body>
309
- {Array.from({ length: USER_ROW_LIMIT }, (_, index) => (
310
- <Table.Row key={index} align="center">
311
- <Table.RowHeaderCell>
312
- <Flex align="center" gap="3">
313
- <Skeleton>
314
- <Avatar size="2" fallback="F" />
315
- </Skeleton>
316
-
317
- <Flex
318
- direction="column"
319
- height="var(--space-7)"
320
- justify="center"
321
- >
322
- <Skeleton width="180px" height="var(--space-4)" />
323
- <Skeleton width="90px" height="var(--space-3)" mt="1" />
324
- </Flex>
325
- </Flex>
326
- </Table.RowHeaderCell>
327
- <Table.Cell>
328
- <Flex wrap="wrap" gap="1">
329
- <Skeleton width="75px" height="var(--space-4)" />
330
- </Flex>
331
- </Table.Cell>
332
- <Table.Cell>
333
- <Skeleton width="120px" height="var(--space-4)" />
334
- </Table.Cell>
335
- <Table.Cell justify="end" />
336
- </Table.Row>
337
- ))}
338
- </Table.Body>
339
- </Table.Root>
340
-
341
- <Flex gap="2" justify="end">
342
- <Skeleton loading>
343
- <SecondaryButton size="1">Previous</SecondaryButton>
344
- </Skeleton>
345
- <Skeleton loading>
346
- <SecondaryButton size="1">Next</SecondaryButton>
347
- </Skeleton>
348
- </Flex>
349
- </UsersManagementRoot>
350
- );
351
- }
352
-
353
- export function UsersManagementError({ error }: { error: unknown }) {
354
- return (
355
- <UsersManagementRoot
356
- direction="row"
357
- justify="center"
358
- align="center"
359
- minHeight="676px"
360
- >
361
- <GenericError error={error} />
362
- </UsersManagementRoot>
363
- );
364
- }
365
-
366
- function UsersManagementRoot({
367
- className,
368
- children,
369
- ...props
370
- }: React.ComponentProps<typeof Flex>) {
371
- return (
372
- <Flex
373
- className={clsx("woswidgets-widget", className)}
374
- data-woswidgets-widget-id="users-management"
375
- direction="column"
376
- gap="3"
377
- {...props}
378
- >
379
- {children}
380
- </Flex>
381
- );
382
- }
383
-
384
- function UserBadge({ user }: { user: Member }) {
385
- // TODO: This is not yet available in the data. Update here after API is updated.
386
- if (user.isLoggedInUser) {
387
- return (
388
- <Badge color="gray" style={{ userSelect: "none" }}>
389
- You
390
- </Badge>
391
- );
392
- }
393
- if (user.status === "Invited") {
394
- return (
395
- <Badge color="amber" style={{ userSelect: "none" }}>
396
- <VisuallyHidden>Status: </VisuallyHidden>Invited
397
- </Badge>
398
- );
399
- }
400
- if (user.status === "InviteExpired") {
401
- return (
402
- <Badge color="red" style={{ userSelect: "none" }}>
403
- <VisuallyHidden>Status: Invite </VisuallyHidden>
404
- Expired
405
- </Badge>
406
- );
407
- }
408
- if (user.status === "InviteRevoked") {
409
- return (
410
- <Badge color="red" style={{ userSelect: "none" }}>
411
- <VisuallyHidden>Status: Invite </VisuallyHidden>
412
- Revoked
413
- </Badge>
414
- );
415
- }
416
- return null;
417
- }
418
-
419
- interface LastActiveProps {
420
- user: Member;
421
- isHydrated: boolean;
422
- dim?: boolean;
423
- }
424
-
425
- function LastActive(props: LastActiveProps) {
426
- if (!props.user.lastActivityAt) {
427
- return (
428
- <>
429
- <VisuallyHidden>
430
- {props.user.status === "Active" ? "Never" : "Not active"}
431
- </VisuallyHidden>
432
- <Separator />
433
- </>
434
- );
435
- }
436
- return <LastActiveImpl {...props} date={props.user.lastActivityAt} />;
437
- }
438
-
439
- function LastActiveImpl({
440
- date,
441
- isHydrated,
442
- dim,
443
- }: LastActiveProps & { date: string }) {
444
- const { lastActiveDateTime, lastActiveDisplay } = React.useMemo(() => {
445
- const defaultTimeZone = "America/Los_Angeles";
446
- const lastActiveDate = new Date(date);
447
- const lastActiveDateTime = lastActiveDate.toLocaleTimeString("en-US", {
448
- // hard-coded timezone before hydration to prevent server/client mismatch
449
- timeZone: isHydrated ? undefined : defaultTimeZone,
450
- month: "long",
451
- day: "numeric",
452
- year: "numeric",
453
- hour: "numeric",
454
- minute: "numeric",
455
- });
456
-
457
- // Server and client may produce a different 'now' date, so only
458
- // show comparative date if the component is hydrated to prevent a
459
- // server/client mismatch
460
- const lastActiveDisplay = isHydrated
461
- ? getComparativeReadableDate(new Date(), lastActiveDate)
462
- : lastActiveDate.toLocaleDateString("en-US", {
463
- // hard-coded timezone to prevent server/client mismatch
464
- timeZone: defaultTimeZone,
465
- month: "long",
466
- day: "numeric",
467
- year: "numeric",
468
- });
469
-
470
- return { lastActiveDateTime, lastActiveDisplay };
471
- }, [isHydrated, date]);
472
-
473
- // handle cases where the DB might return an invalid date string
474
- if (lastActiveDisplay === "Invalid Date") {
475
- return (
476
- <>
477
- <VisuallyHidden>Unknown</VisuallyHidden>
478
- <Separator />
479
- </>
480
- );
481
- }
482
-
483
- return (
484
- <TableCellText asChild dim={dim}>
485
- <time dateTime={date} title={lastActiveDateTime}>
486
- {lastActiveDisplay}
487
- </time>
488
- </TableCellText>
489
- );
490
- }
491
-
492
- const TableCellText = React.forwardRef<HTMLSpanElement, TableCellTextProps>(
493
- function TableCellText(
494
- { children, dim, level = "primary", ...props },
495
- forwardedRef,
496
- ) {
497
- return (
498
- <Text
499
- ref={forwardedRef}
500
- color={level === "secondary" ? "gray" : undefined}
501
- weight={level === "secondary" ? "regular" : "medium"}
502
- size={level === "secondary" ? "1" : "2"}
503
- truncate
504
- {...props}
505
- style={
506
- dim
507
- ? {
508
- // TODO: use CSS var instead of hard-coded value for opacity
509
- opacity: 0.6,
510
- ...props.style,
511
- }
512
- : props.style
513
- }
514
- >
515
- {children}
516
- </Text>
517
- );
518
- },
519
- );
520
-
521
- type TableCellTextProps = TextProps & {
522
- level?: "primary" | "secondary";
523
- dim?: boolean;
524
- };
525
-
526
- const FallbackUserIcon = () => (
527
- <svg
528
- xmlns="http://www.w3.org/2000/svg"
529
- width="20"
530
- height="20"
531
- fill="currentColor"
532
- viewBox="0 0 256 256"
533
- >
534
- <title>User icon</title>
535
- <path d="M229.19,213c-15.81-27.32-40.63-46.49-69.47-54.62a70,70,0,1,0-63.44,0C67.44,166.5,42.62,185.67,26.81,213a6,6,0,1,0,10.38,6C56.4,185.81,90.34,166,128,166s71.6,19.81,90.81,53a6,6,0,1,0,10.38-6ZM70,96a58,58,0,1,1,58,58A58.07,58.07,0,0,1,70,96Z" />
536
- </svg>
537
- );
538
-
539
- const UsersManagementEmptyState = ({ isPending }: { isPending: boolean }) => {
540
- const { clearSearch } = useSearchContext();
541
- const {
542
- state: { searchQuery },
543
- } = useUsersManagementContext();
544
-
545
- // When the search query is cleared, the users query is re-fetched which sends
546
- // us into a pending state. When this happens we want to keep a snapshot of
547
- // the previous search query while the query is revalidated. We can use this
548
- // to keep the 'No users found for query' UI in place until re-fetching is
549
- // complete, otherwise the view flips to 'No users found' very quickly before
550
- // the full table is shown again.
551
- const [{ isClearing, lastSearchQuery }, setClearing] = React.useState({
552
- isClearing: false,
553
- lastSearchQuery: null as null | string,
554
- });
555
- const [wasPending, setWasPending] = React.useState(isPending);
556
- if (wasPending !== isPending) {
557
- setWasPending(isPending);
558
- if (!isPending) {
559
- setClearing((prev) =>
560
- prev.isClearing ? { isClearing: false, lastSearchQuery: null } : prev,
561
- );
562
- }
563
- }
564
-
565
- if (searchQuery || isClearing) {
566
- return (
567
- <Flex align="center" justify="center" py="8" direction="column" gap="2">
568
- <Text size="2">
569
- No users found for query{" "}
570
- <Text weight="medium">
571
- “{isClearing ? lastSearchQuery : searchQuery}”
572
- </Text>
573
- </Text>
574
-
575
- <SecondaryButton
576
- size="1"
577
- onClick={() => {
578
- setClearing({ isClearing: true, lastSearchQuery: searchQuery });
579
- clearSearch();
580
- }}
581
- loading={isPending}
582
- >
583
- Clear search
584
- </SecondaryButton>
585
- </Flex>
586
- );
587
- }
588
-
589
- return (
590
- <Flex align="center" justify="center" py="8" gap="2">
591
- <Text size="2">No users found</Text>
592
- </Flex>
593
- );
594
- };
@@ -1,73 +0,0 @@
1
- "use client";
2
-
3
- import { useComposedRefs } from "@radix-ui/react-compose-refs";
4
- import { Cross2Icon, MagnifyingGlassIcon } from "@radix-ui/react-icons";
5
- import { IconButton } from "@radix-ui/themes";
6
- import cx from "clsx";
7
- import * as React from "react";
8
- import { useDebouncedCallback } from "use-debounce";
9
- import { TextField, TextFieldSlot } from "./elements";
10
- import { useSearchContext } from "./search-provider";
11
- import { useUsersManagementContext } from "./users-management-context";
12
- import { namespaceClassNames } from "./utils";
13
-
14
- type UsersSearchProps = React.ComponentPropsWithoutRef<typeof TextField>;
15
-
16
- export const UsersSearch = React.forwardRef<HTMLInputElement, UsersSearchProps>(
17
- ({ className, ...props }, ref) => {
18
- const { inputRef, clearSearch, searchValue, setSearchValue } =
19
- useSearchContext();
20
- const { dispatch } = useUsersManagementContext();
21
-
22
- const filter = useDebouncedCallback((value) => {
23
- dispatch({ type: "FILTER_BY_SEARCH", searchQuery: value });
24
- }, 200);
25
-
26
- const resetSearch = () => {
27
- clearSearch();
28
- filter.cancel();
29
- };
30
-
31
- return (
32
- <TextField
33
- ref={useComposedRefs(inputRef, ref)}
34
- className={cx(namespaceClassNames("users-search"), className)}
35
- autoComplete="off"
36
- placeholder="Search by name or e-mail"
37
- value={searchValue}
38
- onChange={(event) => {
39
- const value = event.target.value;
40
- setSearchValue(value);
41
- filter(value);
42
- }}
43
- onKeyDown={(event) => {
44
- if (event.key === "Escape") {
45
- event.preventDefault();
46
- resetSearch();
47
- }
48
- }}
49
- {...props}
50
- >
51
- <TextFieldSlot side="left">
52
- <MagnifyingGlassIcon aria-hidden="true" height="16" width="16" />
53
- </TextFieldSlot>
54
-
55
- <TextFieldSlot side="right">
56
- {searchValue && (
57
- <IconButton
58
- size="1"
59
- color="gray"
60
- variant="ghost"
61
- radius="full"
62
- onClick={resetSearch}
63
- aria-label="Clear search"
64
- title="Clear search"
65
- >
66
- <Cross2Icon aria-hidden="true" />
67
- </IconButton>
68
- )}
69
- </TextFieldSlot>
70
- </TextField>
71
- );
72
- },
73
- );