@workos-inc/widgets 0.0.0-pre.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 (230) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/dist/cjs/index.d.ts +3 -0
  4. package/dist/cjs/index.d.ts.map +1 -0
  5. package/dist/cjs/index.js +8 -0
  6. package/dist/cjs/index.js.map +1 -0
  7. package/dist/cjs/lib/api/config.d.ts +9 -0
  8. package/dist/cjs/lib/api/config.d.ts.map +1 -0
  9. package/dist/cjs/lib/api/config.js +12 -0
  10. package/dist/cjs/lib/api/config.js.map +1 -0
  11. package/dist/cjs/lib/api/role.d.ts +9 -0
  12. package/dist/cjs/lib/api/role.d.ts.map +1 -0
  13. package/dist/cjs/lib/api/role.js +94 -0
  14. package/dist/cjs/lib/api/role.js.map +1 -0
  15. package/dist/cjs/lib/api/user.d.ts +61 -0
  16. package/dist/cjs/lib/api/user.d.ts.map +1 -0
  17. package/dist/cjs/lib/api/user.js +312 -0
  18. package/dist/cjs/lib/api/user.js.map +1 -0
  19. package/dist/cjs/lib/constants.d.ts +3 -0
  20. package/dist/cjs/lib/constants.d.ts.map +1 -0
  21. package/dist/cjs/lib/constants.js +6 -0
  22. package/dist/cjs/lib/constants.js.map +1 -0
  23. package/dist/cjs/lib/delete-user-dialog.d.ts +12 -0
  24. package/dist/cjs/lib/delete-user-dialog.d.ts.map +1 -0
  25. package/dist/cjs/lib/delete-user-dialog.js +37 -0
  26. package/dist/cjs/lib/delete-user-dialog.js.map +1 -0
  27. package/dist/cjs/lib/edit-user-details-dialog.d.ts +12 -0
  28. package/dist/cjs/lib/edit-user-details-dialog.d.ts.map +1 -0
  29. package/dist/cjs/lib/edit-user-details-dialog.js +81 -0
  30. package/dist/cjs/lib/edit-user-details-dialog.js.map +1 -0
  31. package/dist/cjs/lib/elements.d.ts +32 -0
  32. package/dist/cjs/lib/elements.d.ts.map +1 -0
  33. package/dist/cjs/lib/elements.js +57 -0
  34. package/dist/cjs/lib/elements.js.map +1 -0
  35. package/dist/cjs/lib/invite-user-dialog.d.ts +7 -0
  36. package/dist/cjs/lib/invite-user-dialog.d.ts.map +1 -0
  37. package/dist/cjs/lib/invite-user-dialog.js +167 -0
  38. package/dist/cjs/lib/invite-user-dialog.js.map +1 -0
  39. package/dist/cjs/lib/label.d.ts +7 -0
  40. package/dist/cjs/lib/label.d.ts.map +1 -0
  41. package/dist/cjs/lib/label.js +9 -0
  42. package/dist/cjs/lib/label.js.map +1 -0
  43. package/dist/cjs/lib/pagination.d.ts +8 -0
  44. package/dist/cjs/lib/pagination.d.ts.map +1 -0
  45. package/dist/cjs/lib/pagination.js +67 -0
  46. package/dist/cjs/lib/pagination.js.map +1 -0
  47. package/dist/cjs/lib/resend-invite-dialog.d.ts +10 -0
  48. package/dist/cjs/lib/resend-invite-dialog.d.ts.map +1 -0
  49. package/dist/cjs/lib/resend-invite-dialog.js +71 -0
  50. package/dist/cjs/lib/resend-invite-dialog.js.map +1 -0
  51. package/dist/cjs/lib/revoke-invite-dialog.d.ts +10 -0
  52. package/dist/cjs/lib/revoke-invite-dialog.d.ts.map +1 -0
  53. package/dist/cjs/lib/revoke-invite-dialog.js +37 -0
  54. package/dist/cjs/lib/revoke-invite-dialog.js.map +1 -0
  55. package/dist/cjs/lib/search-provider.d.ts +11 -0
  56. package/dist/cjs/lib/search-provider.d.ts.map +1 -0
  57. package/dist/cjs/lib/search-provider.js +55 -0
  58. package/dist/cjs/lib/search-provider.js.map +1 -0
  59. package/dist/cjs/lib/use-is-hydrated.d.ts +2 -0
  60. package/dist/cjs/lib/use-is-hydrated.d.ts.map +1 -0
  61. package/dist/cjs/lib/use-is-hydrated.js +34 -0
  62. package/dist/cjs/lib/use-is-hydrated.js.map +1 -0
  63. package/dist/cjs/lib/user-actions-dropdown.d.ts +9 -0
  64. package/dist/cjs/lib/user-actions-dropdown.d.ts.map +1 -0
  65. package/dist/cjs/lib/user-actions-dropdown.js +83 -0
  66. package/dist/cjs/lib/user-actions-dropdown.js.map +1 -0
  67. package/dist/cjs/lib/users-filter.d.ts +9 -0
  68. package/dist/cjs/lib/users-filter.d.ts.map +1 -0
  69. package/dist/cjs/lib/users-filter.js +63 -0
  70. package/dist/cjs/lib/users-filter.js.map +1 -0
  71. package/dist/cjs/lib/users-management-context.d.ts +23 -0
  72. package/dist/cjs/lib/users-management-context.d.ts.map +1 -0
  73. package/dist/cjs/lib/users-management-context.js +83 -0
  74. package/dist/cjs/lib/users-management-context.js.map +1 -0
  75. package/dist/cjs/lib/users-management-state.d.ts +22 -0
  76. package/dist/cjs/lib/users-management-state.d.ts.map +1 -0
  77. package/dist/cjs/lib/users-management-state.js +143 -0
  78. package/dist/cjs/lib/users-management-state.js.map +1 -0
  79. package/dist/cjs/lib/users-management.d.ts +12 -0
  80. package/dist/cjs/lib/users-management.d.ts.map +1 -0
  81. package/dist/cjs/lib/users-management.js +141 -0
  82. package/dist/cjs/lib/users-management.js.map +1 -0
  83. package/dist/cjs/lib/users-search.d.ts +3 -0
  84. package/dist/cjs/lib/users-search.d.ts.map +1 -0
  85. package/dist/cjs/lib/users-search.js +65 -0
  86. package/dist/cjs/lib/users-search.js.map +1 -0
  87. package/dist/cjs/lib/utils.d.ts +15 -0
  88. package/dist/cjs/lib/utils.d.ts.map +1 -0
  89. package/dist/cjs/lib/utils.js +78 -0
  90. package/dist/cjs/lib/utils.js.map +1 -0
  91. package/dist/cjs/lib/widgets-context.d.ts +11 -0
  92. package/dist/cjs/lib/widgets-context.d.ts.map +1 -0
  93. package/dist/cjs/lib/widgets-context.js +45 -0
  94. package/dist/cjs/lib/widgets-context.js.map +1 -0
  95. package/dist/cjs/users-management.client.d.ts +6 -0
  96. package/dist/cjs/users-management.client.d.ts.map +1 -0
  97. package/dist/cjs/users-management.client.js +57 -0
  98. package/dist/cjs/users-management.client.js.map +1 -0
  99. package/dist/cjs/workos-widgets.client.d.ts +17 -0
  100. package/dist/cjs/workos-widgets.client.d.ts.map +1 -0
  101. package/dist/cjs/workos-widgets.client.js +55 -0
  102. package/dist/cjs/workos-widgets.client.js.map +1 -0
  103. package/dist/esm/index.d.ts +3 -0
  104. package/dist/esm/index.d.ts.map +1 -0
  105. package/dist/esm/index.js +3 -0
  106. package/dist/esm/index.js.map +1 -0
  107. package/dist/esm/lib/api/config.d.ts +9 -0
  108. package/dist/esm/lib/api/config.d.ts.map +1 -0
  109. package/dist/esm/lib/api/config.js +9 -0
  110. package/dist/esm/lib/api/config.js.map +1 -0
  111. package/dist/esm/lib/api/role.d.ts +9 -0
  112. package/dist/esm/lib/api/role.d.ts.map +1 -0
  113. package/dist/esm/lib/api/role.js +89 -0
  114. package/dist/esm/lib/api/role.js.map +1 -0
  115. package/dist/esm/lib/api/user.d.ts +61 -0
  116. package/dist/esm/lib/api/user.d.ts.map +1 -0
  117. package/dist/esm/lib/api/user.js +302 -0
  118. package/dist/esm/lib/api/user.js.map +1 -0
  119. package/dist/esm/lib/constants.d.ts +3 -0
  120. package/dist/esm/lib/constants.d.ts.map +1 -0
  121. package/dist/esm/lib/constants.js +3 -0
  122. package/dist/esm/lib/constants.js.map +1 -0
  123. package/dist/esm/lib/delete-user-dialog.d.ts +12 -0
  124. package/dist/esm/lib/delete-user-dialog.d.ts.map +1 -0
  125. package/dist/esm/lib/delete-user-dialog.js +33 -0
  126. package/dist/esm/lib/delete-user-dialog.js.map +1 -0
  127. package/dist/esm/lib/edit-user-details-dialog.d.ts +12 -0
  128. package/dist/esm/lib/edit-user-details-dialog.d.ts.map +1 -0
  129. package/dist/esm/lib/edit-user-details-dialog.js +54 -0
  130. package/dist/esm/lib/edit-user-details-dialog.js.map +1 -0
  131. package/dist/esm/lib/elements.d.ts +32 -0
  132. package/dist/esm/lib/elements.d.ts.map +1 -0
  133. package/dist/esm/lib/elements.js +54 -0
  134. package/dist/esm/lib/elements.js.map +1 -0
  135. package/dist/esm/lib/invite-user-dialog.d.ts +7 -0
  136. package/dist/esm/lib/invite-user-dialog.d.ts.map +1 -0
  137. package/dist/esm/lib/invite-user-dialog.js +140 -0
  138. package/dist/esm/lib/invite-user-dialog.js.map +1 -0
  139. package/dist/esm/lib/label.d.ts +7 -0
  140. package/dist/esm/lib/label.d.ts.map +1 -0
  141. package/dist/esm/lib/label.js +6 -0
  142. package/dist/esm/lib/label.js.map +1 -0
  143. package/dist/esm/lib/pagination.d.ts +8 -0
  144. package/dist/esm/lib/pagination.d.ts.map +1 -0
  145. package/dist/esm/lib/pagination.js +40 -0
  146. package/dist/esm/lib/pagination.js.map +1 -0
  147. package/dist/esm/lib/resend-invite-dialog.d.ts +10 -0
  148. package/dist/esm/lib/resend-invite-dialog.d.ts.map +1 -0
  149. package/dist/esm/lib/resend-invite-dialog.js +44 -0
  150. package/dist/esm/lib/resend-invite-dialog.js.map +1 -0
  151. package/dist/esm/lib/revoke-invite-dialog.d.ts +10 -0
  152. package/dist/esm/lib/revoke-invite-dialog.d.ts.map +1 -0
  153. package/dist/esm/lib/revoke-invite-dialog.js +33 -0
  154. package/dist/esm/lib/revoke-invite-dialog.js.map +1 -0
  155. package/dist/esm/lib/search-provider.d.ts +11 -0
  156. package/dist/esm/lib/search-provider.d.ts.map +1 -0
  157. package/dist/esm/lib/search-provider.js +27 -0
  158. package/dist/esm/lib/search-provider.js.map +1 -0
  159. package/dist/esm/lib/use-is-hydrated.d.ts +2 -0
  160. package/dist/esm/lib/use-is-hydrated.d.ts.map +1 -0
  161. package/dist/esm/lib/use-is-hydrated.js +8 -0
  162. package/dist/esm/lib/use-is-hydrated.js.map +1 -0
  163. package/dist/esm/lib/user-actions-dropdown.d.ts +9 -0
  164. package/dist/esm/lib/user-actions-dropdown.d.ts.map +1 -0
  165. package/dist/esm/lib/user-actions-dropdown.js +56 -0
  166. package/dist/esm/lib/user-actions-dropdown.js.map +1 -0
  167. package/dist/esm/lib/users-filter.d.ts +9 -0
  168. package/dist/esm/lib/users-filter.d.ts.map +1 -0
  169. package/dist/esm/lib/users-filter.js +36 -0
  170. package/dist/esm/lib/users-filter.js.map +1 -0
  171. package/dist/esm/lib/users-management-context.d.ts +23 -0
  172. package/dist/esm/lib/users-management-context.d.ts.map +1 -0
  173. package/dist/esm/lib/users-management-context.js +54 -0
  174. package/dist/esm/lib/users-management-context.js.map +1 -0
  175. package/dist/esm/lib/users-management-state.d.ts +22 -0
  176. package/dist/esm/lib/users-management-state.d.ts.map +1 -0
  177. package/dist/esm/lib/users-management-state.js +117 -0
  178. package/dist/esm/lib/users-management-state.js.map +1 -0
  179. package/dist/esm/lib/users-management.d.ts +12 -0
  180. package/dist/esm/lib/users-management.d.ts.map +1 -0
  181. package/dist/esm/lib/users-management.js +114 -0
  182. package/dist/esm/lib/users-management.js.map +1 -0
  183. package/dist/esm/lib/users-search.d.ts +3 -0
  184. package/dist/esm/lib/users-search.d.ts.map +1 -0
  185. package/dist/esm/lib/users-search.js +39 -0
  186. package/dist/esm/lib/users-search.js.map +1 -0
  187. package/dist/esm/lib/utils.d.ts +15 -0
  188. package/dist/esm/lib/utils.d.ts.map +1 -0
  189. package/dist/esm/lib/utils.js +70 -0
  190. package/dist/esm/lib/utils.js.map +1 -0
  191. package/dist/esm/lib/widgets-context.d.ts +11 -0
  192. package/dist/esm/lib/widgets-context.d.ts.map +1 -0
  193. package/dist/esm/lib/widgets-context.js +17 -0
  194. package/dist/esm/lib/widgets-context.js.map +1 -0
  195. package/dist/esm/users-management.client.d.ts +6 -0
  196. package/dist/esm/users-management.client.d.ts.map +1 -0
  197. package/dist/esm/users-management.client.js +30 -0
  198. package/dist/esm/users-management.client.js.map +1 -0
  199. package/dist/esm/workos-widgets.client.d.ts +17 -0
  200. package/dist/esm/workos-widgets.client.d.ts.map +1 -0
  201. package/dist/esm/workos-widgets.client.js +28 -0
  202. package/dist/esm/workos-widgets.client.js.map +1 -0
  203. package/dist/tsconfig.cjs.tsbuildinfo +1 -0
  204. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  205. package/package.json +69 -0
  206. package/src/index.ts +5 -0
  207. package/src/lib/api/config.ts +9 -0
  208. package/src/lib/api/role.ts +124 -0
  209. package/src/lib/api/user.ts +458 -0
  210. package/src/lib/constants.ts +2 -0
  211. package/src/lib/delete-user-dialog.tsx +103 -0
  212. package/src/lib/edit-user-details-dialog.tsx +170 -0
  213. package/src/lib/elements.tsx +175 -0
  214. package/src/lib/invite-user-dialog.tsx +319 -0
  215. package/src/lib/label.tsx +14 -0
  216. package/src/lib/pagination.tsx +69 -0
  217. package/src/lib/resend-invite-dialog.tsx +136 -0
  218. package/src/lib/revoke-invite-dialog.tsx +104 -0
  219. package/src/lib/search-provider.tsx +51 -0
  220. package/src/lib/use-is-hydrated.ts +13 -0
  221. package/src/lib/user-actions-dropdown.tsx +161 -0
  222. package/src/lib/users-filter.tsx +122 -0
  223. package/src/lib/users-management-context.tsx +89 -0
  224. package/src/lib/users-management-state.ts +165 -0
  225. package/src/lib/users-management.tsx +461 -0
  226. package/src/lib/users-search.tsx +130 -0
  227. package/src/lib/utils.ts +94 -0
  228. package/src/lib/widgets-context.ts +29 -0
  229. package/src/users-management.client.tsx +59 -0
  230. package/src/workos-widgets.client.tsx +73 -0
@@ -0,0 +1,69 @@
1
+ import * as React from "react";
2
+ import { Flex, Skeleton } from "@radix-ui/themes";
3
+ import { SecondaryButton } from "./elements";
4
+ import type { PaginationData } from "./api/user";
5
+ import { useUsersManagementContext } from "./users-management-context";
6
+
7
+ interface PaginationProps {
8
+ isPending?: boolean;
9
+ pagination?: PaginationData;
10
+ }
11
+
12
+ export const Pagination = ({ isPending, pagination }: PaginationProps) => {
13
+ const { dispatch } = useUsersManagementContext();
14
+
15
+ // we only want to show the loading indicator if the request is still pending
16
+ // after 500ms. If the request is fast enough the indicator is a bit jarring.
17
+ const [deferredLoading, setDeferredLoading] = React.useState(false);
18
+ React.useEffect(() => {
19
+ if (isPending) {
20
+ const timeoutId = window.setTimeout(() => {
21
+ setDeferredLoading(true);
22
+ }, 500);
23
+ return () => {
24
+ window.clearTimeout(timeoutId);
25
+ };
26
+ } else {
27
+ setDeferredLoading(false);
28
+ }
29
+ }, [isPending]);
30
+
31
+ return (
32
+ <Flex gap="2" justify="end">
33
+ <Skeleton loading={!pagination}>
34
+ <SecondaryButton
35
+ size="1"
36
+ disabled={!pagination?.after || isPending || undefined}
37
+ loading={deferredLoading}
38
+ onClick={() => {
39
+ if (pagination?.after) {
40
+ dispatch({
41
+ type: "SET_PAGINATION",
42
+ pagination: { after: pagination.after },
43
+ });
44
+ }
45
+ }}
46
+ >
47
+ Previous
48
+ </SecondaryButton>
49
+ </Skeleton>
50
+ <Skeleton loading={!pagination}>
51
+ <SecondaryButton
52
+ size="1"
53
+ disabled={!pagination?.before || isPending || undefined}
54
+ loading={deferredLoading}
55
+ onClick={() => {
56
+ if (pagination?.before) {
57
+ dispatch({
58
+ type: "SET_PAGINATION",
59
+ pagination: { before: pagination.before },
60
+ });
61
+ }
62
+ }}
63
+ >
64
+ Next
65
+ </SecondaryButton>
66
+ </Skeleton>
67
+ </Flex>
68
+ );
69
+ };
@@ -0,0 +1,136 @@
1
+ "use client";
2
+
3
+ import {
4
+ AlertDialog,
5
+ Callout,
6
+ Dialog,
7
+ Flex,
8
+ Text,
9
+ VisuallyHidden,
10
+ } from "@radix-ui/themes";
11
+ import * as React from "react";
12
+ import { useResendUserInvite } from "./api/user";
13
+ import type { User } from "./api/user";
14
+ import { DestructiveButton, PrimaryButton, SecondaryButton } from "./elements";
15
+
16
+ interface ResendInviteDialogProps extends AlertDialog.RootProps {
17
+ user: User;
18
+ children?: React.ReactNode;
19
+ }
20
+
21
+ export const ResendInviteDialog = ({
22
+ children,
23
+ user,
24
+ ...props
25
+ }: ResendInviteDialogProps) => {
26
+ const resendInvite = useResendUserInvite();
27
+ const cancelButtonRef = React.useRef<HTMLButtonElement>(null);
28
+ const successButtonRef = React.useRef<HTMLButtonElement>(null);
29
+ const [successDialogIsOpen, setSuccessDialogIsOpen] = React.useState(false);
30
+
31
+ const onSubmitForm = () => {
32
+ resendInvite.mutate(user.id, {
33
+ onSuccess: () => {
34
+ setSuccessDialogIsOpen(true);
35
+ },
36
+ });
37
+ };
38
+
39
+ return (
40
+ <>
41
+ <AlertDialog.Root {...props}>
42
+ {children && <AlertDialog.Trigger>{children}</AlertDialog.Trigger>}
43
+
44
+ <AlertDialog.Content
45
+ maxWidth="480px"
46
+ onOpenAutoFocus={() => {
47
+ requestAnimationFrame(() => {
48
+ cancelButtonRef.current?.focus();
49
+ });
50
+ }}
51
+ >
52
+ <AlertDialog.Title>Resend invite?</AlertDialog.Title>
53
+ <Flex mb="4" direction="column" gap="3">
54
+ <AlertDialog.Description>
55
+ Are you sure you want to resend the invite to{" "}
56
+ <Text weight="bold">{user.email}</Text>?
57
+ </AlertDialog.Description>
58
+ </Flex>
59
+
60
+ {resendInvite.error ? (
61
+ <Callout.Root color="red" mt="4" mb="-2">
62
+ <Callout.Text>
63
+ {getMutationErrorMessage(resendInvite.error)}
64
+ </Callout.Text>
65
+ </Callout.Root>
66
+ ) : null}
67
+
68
+ <Flex gap="3" justify="end" mt="5" asChild>
69
+ <form
70
+ onSubmit={(event) => {
71
+ event.preventDefault();
72
+ onSubmitForm();
73
+ }}
74
+ >
75
+ <AlertDialog.Cancel>
76
+ <SecondaryButton
77
+ ref={cancelButtonRef}
78
+ disabled={resendInvite.isPending}
79
+ >
80
+ Cancel
81
+ </SecondaryButton>
82
+ </AlertDialog.Cancel>
83
+ <DestructiveButton type="submit" loading={resendInvite.isPending}>
84
+ Resend
85
+ </DestructiveButton>
86
+ </form>
87
+ </Flex>
88
+ </AlertDialog.Content>
89
+
90
+ {/* mirror errors in a live region */}
91
+ <VisuallyHidden asChild>
92
+ <section aria-live="polite">
93
+ {getMutationErrorMessage(resendInvite.error)}
94
+ </section>
95
+ </VisuallyHidden>
96
+ </AlertDialog.Root>
97
+ <Dialog.Root
98
+ open={successDialogIsOpen}
99
+ onOpenChange={(isOpen) => {
100
+ if (!isOpen) {
101
+ props.onOpenChange?.(false);
102
+ }
103
+ setSuccessDialogIsOpen(isOpen);
104
+ }}
105
+ >
106
+ <Dialog.Content
107
+ maxWidth="360px"
108
+ onOpenAutoFocus={() => {
109
+ requestAnimationFrame(() => {
110
+ successButtonRef.current?.focus();
111
+ });
112
+ }}
113
+ >
114
+ <Dialog.Title>Invite sent</Dialog.Title>
115
+ <Dialog.Description>
116
+ The invite email has been resent to{" "}
117
+ <Text weight="bold">{user.email}</Text>
118
+ </Dialog.Description>
119
+ <Flex gap="3" justify="end" mt="5">
120
+ <Dialog.Close>
121
+ <PrimaryButton ref={successButtonRef}>Close</PrimaryButton>
122
+ </Dialog.Close>
123
+ </Flex>
124
+ </Dialog.Content>
125
+ </Dialog.Root>
126
+ </>
127
+ );
128
+ };
129
+
130
+ function getMutationErrorMessage(error: unknown) {
131
+ if (!error) {
132
+ return "";
133
+ }
134
+ // TODO Handle server errors
135
+ return "There was an error sending the invite. Please try again.";
136
+ }
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import {
4
+ AlertDialog,
5
+ Callout,
6
+ Flex,
7
+ Text,
8
+ VisuallyHidden,
9
+ } from "@radix-ui/themes";
10
+ import { type ReactNode, useRef } from "react";
11
+ import { useRevokeUserInvite } from "./api/user";
12
+ import type { User } from "./api/user";
13
+ import { DestructiveButton, SecondaryButton } from "./elements";
14
+
15
+ interface RevokeInviteDialogProps extends AlertDialog.RootProps {
16
+ user: User;
17
+ children?: ReactNode;
18
+ }
19
+
20
+ export const RevokeInviteDialog = ({
21
+ children,
22
+ user,
23
+ ...props
24
+ }: RevokeInviteDialogProps) => {
25
+ const revokeInvite = useRevokeUserInvite();
26
+ const cancelButtonRef = useRef<HTMLButtonElement>(null);
27
+
28
+ const onSubmitForm = () => {
29
+ revokeInvite.mutate(user.id, {
30
+ onSuccess: () => {
31
+ props.onOpenChange?.(false);
32
+ },
33
+ });
34
+ };
35
+
36
+ return (
37
+ <AlertDialog.Root {...props}>
38
+ {children && <AlertDialog.Trigger>{children}</AlertDialog.Trigger>}
39
+
40
+ <AlertDialog.Content
41
+ maxWidth="480px"
42
+ onOpenAutoFocus={() => {
43
+ requestAnimationFrame(() => {
44
+ cancelButtonRef.current?.focus();
45
+ });
46
+ }}
47
+ >
48
+ <AlertDialog.Title>Revoke invite?</AlertDialog.Title>
49
+ <Flex mb="4" direction="column" gap="3">
50
+ <AlertDialog.Description>
51
+ Are you sure you want to revoke the invite to{" "}
52
+ <Text weight="bold">{user.email}</Text>? This action is immediate
53
+ and cannot be undone.
54
+ </AlertDialog.Description>
55
+ </Flex>
56
+
57
+ {revokeInvite.error ? (
58
+ <Callout.Root color="red" mt="4" mb="-2">
59
+ <Callout.Text>
60
+ {getMutationErrorMessage(revokeInvite.error)}
61
+ </Callout.Text>
62
+ </Callout.Root>
63
+ ) : null}
64
+
65
+ <Flex gap="3" justify="end" mt="5" asChild>
66
+ <form
67
+ onSubmit={(event) => {
68
+ event.preventDefault();
69
+ onSubmitForm();
70
+ }}
71
+ >
72
+ <AlertDialog.Cancel>
73
+ <SecondaryButton
74
+ ref={cancelButtonRef}
75
+ disabled={revokeInvite.isPending}
76
+ >
77
+ Cancel
78
+ </SecondaryButton>
79
+ </AlertDialog.Cancel>
80
+
81
+ <DestructiveButton type="submit" loading={revokeInvite.isPending}>
82
+ Revoke
83
+ </DestructiveButton>
84
+ </form>
85
+ </Flex>
86
+ </AlertDialog.Content>
87
+
88
+ {/* mirror errors in a live region */}
89
+ <VisuallyHidden asChild>
90
+ <section aria-live="polite">
91
+ {getMutationErrorMessage(revokeInvite.error)}
92
+ </section>
93
+ </VisuallyHidden>
94
+ </AlertDialog.Root>
95
+ );
96
+ };
97
+
98
+ function getMutationErrorMessage(error: unknown) {
99
+ if (!error) {
100
+ return null;
101
+ }
102
+ // TODO Handle server errors
103
+ return "There was an error revoking the invite. Please try again.";
104
+ }
@@ -0,0 +1,51 @@
1
+ import * as React from "react";
2
+ import { flushSync } from "react-dom";
3
+ import { useUsersManagementContext } from "./users-management-context";
4
+
5
+ interface SearchContextType {
6
+ inputRef: React.RefObject<HTMLInputElement>;
7
+ clearSearch: () => void;
8
+ searchValue: string;
9
+ setSearchValue: React.Dispatch<React.SetStateAction<string>>;
10
+ }
11
+
12
+ const SearchContext = React.createContext<SearchContextType | undefined>(
13
+ undefined,
14
+ );
15
+ SearchContext.displayName = "SearchContext";
16
+
17
+ export const SearchProvider: React.FC<React.PropsWithChildren> = ({
18
+ children,
19
+ }) => {
20
+ const inputRef = React.useRef<HTMLInputElement>(null);
21
+ const {
22
+ state: { searchQuery },
23
+ dispatch,
24
+ } = useUsersManagementContext();
25
+
26
+ const [searchValue, setSearchValue] = React.useState(searchQuery || "");
27
+
28
+ const clearSearch = React.useCallback(() => {
29
+ flushSync(() => {
30
+ setSearchValue("");
31
+ dispatch({ type: "FILTER_BY_SEARCH", searchQuery: "" });
32
+ });
33
+ inputRef.current?.focus();
34
+ }, [dispatch]);
35
+
36
+ return (
37
+ <SearchContext.Provider
38
+ value={{ inputRef, clearSearch, searchValue, setSearchValue }}
39
+ >
40
+ {children}
41
+ </SearchContext.Provider>
42
+ );
43
+ };
44
+
45
+ export const useSearchContext = () => {
46
+ const context = React.useContext(SearchContext);
47
+ if (!context) {
48
+ throw new Error("useSearchContext must be used within a SearchProvider");
49
+ }
50
+ return context;
51
+ };
@@ -0,0 +1,13 @@
1
+ import * as React from "react";
2
+
3
+ function subscribe() {
4
+ return () => {};
5
+ }
6
+
7
+ export function useIsHydrated() {
8
+ return React.useSyncExternalStore(
9
+ subscribe,
10
+ () => true,
11
+ () => false,
12
+ );
13
+ }
@@ -0,0 +1,161 @@
1
+ "use client";
2
+
3
+ import { DropdownMenu } from "@radix-ui/themes";
4
+ import * as React from "react";
5
+ import type { User } from "./api/user";
6
+ import { DeleteUserDialog } from "./delete-user-dialog";
7
+ import { EditUserDetailsDialog } from "./edit-user-details-dialog";
8
+ import { DestructiveMenuItem, PrimaryMenuItem } from "./elements";
9
+ import { ResendInviteDialog } from "./resend-invite-dialog";
10
+ import { RevokeInviteDialog } from "./revoke-invite-dialog";
11
+ import { useElement } from "./widgets-context";
12
+ import { useRoles } from "./api/role";
13
+
14
+ interface UserActionsDropdownProps {
15
+ user: User;
16
+ children: React.ReactNode;
17
+ }
18
+
19
+ type UserActionDialog =
20
+ | "revoke-membership"
21
+ | "revoke-invite"
22
+ | "resend-invite"
23
+ | "edit-role";
24
+
25
+ export const UserActionsDropdown = ({
26
+ user,
27
+ children,
28
+ }: UserActionsDropdownProps) => {
29
+ const rolesQuery = useRoles();
30
+ const [openDialog, setOpenDialog] = React.useState<UserActionDialog | null>(
31
+ null,
32
+ );
33
+ const dropdownProps = useElement("dropdown");
34
+
35
+ /**
36
+ * Assigns a key for each dialog based on its open state to ensure its
37
+ * internal state is cleared when it is closed.
38
+ */
39
+ function getDialogKey(dialog: UserActionDialog) {
40
+ return `${dialog}-${openDialog === dialog}-${user.id}`;
41
+ }
42
+
43
+ const { actions, items } = React.useMemo(() => {
44
+ const actions = new Set(user.actions);
45
+ const items: React.ReactElement[] = [];
46
+ if (actions.has("edit-role")) {
47
+ items.push(
48
+ <PrimaryMenuItem
49
+ key="edit-role"
50
+ onSelect={() => setOpenDialog("edit-role")}
51
+ disabled={
52
+ rolesQuery.isLoading ||
53
+ (rolesQuery.isSuccess && rolesQuery.data.length <= 1)
54
+ }
55
+ title={
56
+ rolesQuery.isSuccess && rolesQuery.data.length <= 1
57
+ ? "You cannot update the role for this user as there is only one role available."
58
+ : undefined
59
+ }
60
+ >
61
+ Edit role
62
+ </PrimaryMenuItem>,
63
+ );
64
+ }
65
+ if (actions.has("resend-invite")) {
66
+ items.push(
67
+ <PrimaryMenuItem
68
+ key="resend-invite"
69
+ onSelect={() => setOpenDialog("resend-invite")}
70
+ >
71
+ Resend invitation
72
+ </PrimaryMenuItem>,
73
+ );
74
+ }
75
+ if (actions.has("revoke-invite")) {
76
+ items.push(
77
+ <DestructiveMenuItem
78
+ key="revoke-invite"
79
+ onSelect={() => setOpenDialog("revoke-invite")}
80
+ >
81
+ Revoke invitation
82
+ </DestructiveMenuItem>,
83
+ );
84
+ }
85
+ if (actions.has("revoke-membership")) {
86
+ items.push(
87
+ <DestructiveMenuItem
88
+ key="revoke-membership"
89
+ onSelect={() => setOpenDialog("revoke-membership")}
90
+ >
91
+ Remove user
92
+ </DestructiveMenuItem>,
93
+ );
94
+ }
95
+ return {
96
+ actions,
97
+ items: items.map((item, index, array) => {
98
+ if (index !== array.length - 1) {
99
+ return (
100
+ <React.Fragment key={item.key}>
101
+ {item}
102
+ <DropdownMenu.Separator />
103
+ </React.Fragment>
104
+ );
105
+ }
106
+ return item;
107
+ }),
108
+ };
109
+ }, [rolesQuery, user.actions]);
110
+
111
+ if (user.isLoggedInUser || items.length === 0) {
112
+ return null;
113
+ }
114
+
115
+ return (
116
+ <>
117
+ <DropdownMenu.Root>
118
+ <DropdownMenu.Trigger>{children}</DropdownMenu.Trigger>
119
+ <DropdownMenu.Content size="2" align="end" {...dropdownProps}>
120
+ {items}
121
+ </DropdownMenu.Content>
122
+ </DropdownMenu.Root>
123
+
124
+ {actions.has("edit-role") && (
125
+ <EditUserDetailsDialog
126
+ key={getDialogKey("edit-role")}
127
+ user={user}
128
+ open={openDialog === "edit-role"}
129
+ onOpenChange={(open) => !open && setOpenDialog(null)}
130
+ />
131
+ )}
132
+
133
+ {actions.has("revoke-membership") && (
134
+ <DeleteUserDialog
135
+ key={getDialogKey("revoke-membership")}
136
+ user={user}
137
+ open={openDialog === "revoke-membership"}
138
+ onOpenChange={(open) => !open && setOpenDialog(null)}
139
+ />
140
+ )}
141
+
142
+ {actions.has("revoke-invite") && (
143
+ <RevokeInviteDialog
144
+ key={getDialogKey("revoke-invite")}
145
+ user={user}
146
+ open={openDialog === "revoke-invite"}
147
+ onOpenChange={(open) => !open && setOpenDialog(null)}
148
+ />
149
+ )}
150
+
151
+ {actions.has("resend-invite") && (
152
+ <ResendInviteDialog
153
+ key={getDialogKey("resend-invite")}
154
+ user={user}
155
+ open={openDialog === "resend-invite"}
156
+ onOpenChange={(open) => !open && setOpenDialog(null)}
157
+ />
158
+ )}
159
+ </>
160
+ );
161
+ };
@@ -0,0 +1,122 @@
1
+ "use client";
2
+
3
+ import {
4
+ Checkbox,
5
+ DropdownMenu,
6
+ Flex,
7
+ IconButton,
8
+ Select,
9
+ } from "@radix-ui/themes";
10
+ import * as React from "react";
11
+ import type { Role } from "./api/role";
12
+ import { PrimaryMenuItem } from "./elements";
13
+ import { useUsersManagementContext } from "./users-management-context";
14
+
15
+ const ALL_ROLES_NAME = "All";
16
+ const ALL_ROLES_VALUE = "null";
17
+
18
+ type UsersFilterProps = React.ComponentPropsWithoutRef<typeof Select.Root> & {
19
+ roles: Role[] | undefined;
20
+ };
21
+
22
+ export const UsersFilter: React.FC<UsersFilterProps> = ({ roles }) => {
23
+ const {
24
+ dispatch,
25
+ state: { role },
26
+ } = useUsersManagementContext();
27
+
28
+ const setFilterParams = (value: string | null) => {
29
+ dispatch({ type: "FILTER_BY_ROLE", role: value });
30
+ };
31
+ const isValidRole = (value: unknown): value is string =>
32
+ roles ? roles.findIndex((role) => role.slug === value) !== -1 : false;
33
+ const filteredRole = isValidRole(role) ? role : ALL_ROLES_VALUE;
34
+ const roleName =
35
+ roles?.find((role) => role.slug === filteredRole)?.name || ALL_ROLES_NAME;
36
+
37
+ const onValueChange = (value: string) => {
38
+ if (value === ALL_ROLES_VALUE) {
39
+ setFilterParams(null);
40
+ } else {
41
+ if (isValidRole(value)) {
42
+ setFilterParams(value);
43
+ }
44
+ }
45
+ };
46
+
47
+ return (
48
+ <Select.Root
49
+ value={roles ? filteredRole : "null"}
50
+ onValueChange={onValueChange}
51
+ >
52
+ <Select.Trigger>{roles ? roleName : ALL_ROLES_NAME}</Select.Trigger>
53
+ <Select.Content>
54
+ <Select.Item value={ALL_ROLES_VALUE}>{ALL_ROLES_NAME}</Select.Item>
55
+ {roles?.map((role) => (
56
+ <React.Fragment key={role.slug}>
57
+ <Select.Item value={role.slug}>{role.name}</Select.Item>
58
+ </React.Fragment>
59
+ ))}
60
+ </Select.Content>
61
+ </Select.Root>
62
+ );
63
+ };
64
+
65
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
66
+ function FilterMenu() {
67
+ return (
68
+ <DropdownMenu.Root>
69
+ <DropdownMenu.Trigger>
70
+ <IconButton
71
+ size="1"
72
+ color="gray"
73
+ variant="ghost"
74
+ radius="full"
75
+ aria-label="Filter users"
76
+ title="Filter users"
77
+ >
78
+ <FilterIcon aria-hidden="true" />
79
+ </IconButton>
80
+ </DropdownMenu.Trigger>
81
+ <DropdownMenu.Content size="2" align="end">
82
+ <PrimaryMenuItem>
83
+ <Flex gap="2" align="center">
84
+ <Checkbox variant="surface" />
85
+ One
86
+ </Flex>
87
+ </PrimaryMenuItem>
88
+ <PrimaryMenuItem>
89
+ <Flex gap="2" align="center">
90
+ <Checkbox />
91
+ Two
92
+ </Flex>
93
+ </PrimaryMenuItem>
94
+ </DropdownMenu.Content>
95
+ </DropdownMenu.Root>
96
+ );
97
+ }
98
+
99
+ const FilterIcon = React.forwardRef<
100
+ SVGSVGElement,
101
+ React.ComponentPropsWithRef<"svg">
102
+ >(function FilterIcon({ children, ...props }, forwardedRef) {
103
+ return (
104
+ <svg
105
+ width="15"
106
+ height="15"
107
+ viewBox="0 0 15 15"
108
+ fill="currentColor"
109
+ xmlns="http://www.w3.org/2000/svg"
110
+ ref={forwardedRef}
111
+ {...props}
112
+ >
113
+ {children}
114
+ <path
115
+ fillRule="evenodd"
116
+ clipRule="evenodd"
117
+ d="M1.5 1C1.22386 1 1 1.22386 1 1.5V4.5C1 4.66316 1.07961 4.81605 1.21327 4.90962L6 8.26033V13.5C6 13.6733 6.08973 13.8342 6.23713 13.9253C6.38454 14.0164 6.56861 14.0247 6.72361 13.9472L8.72361 12.9472C8.893 12.8625 9 12.6894 9 12.5V8.26033L13.7867 4.90962C13.9204 4.81605 14 4.66316 14 4.5V1.5C14 1.22386 13.7761 1 13.5 1H1.5ZM2 4.23967V2H13V4.23967L8.21327 7.59038C8.07961 7.68395 8 7.83684 8 8V12.191L7 12.691V8C7 7.83684 6.92039 7.68395 6.78673 7.59038L2 4.23967ZM12 3H3V4H12V3Z"
118
+ fill="black"
119
+ />
120
+ </svg>
121
+ );
122
+ });