@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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +8 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/lib/api/config.d.ts +9 -0
- package/dist/cjs/lib/api/config.d.ts.map +1 -0
- package/dist/cjs/lib/api/config.js +12 -0
- package/dist/cjs/lib/api/config.js.map +1 -0
- package/dist/cjs/lib/api/role.d.ts +9 -0
- package/dist/cjs/lib/api/role.d.ts.map +1 -0
- package/dist/cjs/lib/api/role.js +94 -0
- package/dist/cjs/lib/api/role.js.map +1 -0
- package/dist/cjs/lib/api/user.d.ts +61 -0
- package/dist/cjs/lib/api/user.d.ts.map +1 -0
- package/dist/cjs/lib/api/user.js +312 -0
- package/dist/cjs/lib/api/user.js.map +1 -0
- package/dist/cjs/lib/constants.d.ts +3 -0
- package/dist/cjs/lib/constants.d.ts.map +1 -0
- package/dist/cjs/lib/constants.js +6 -0
- package/dist/cjs/lib/constants.js.map +1 -0
- package/dist/cjs/lib/delete-user-dialog.d.ts +12 -0
- package/dist/cjs/lib/delete-user-dialog.d.ts.map +1 -0
- package/dist/cjs/lib/delete-user-dialog.js +37 -0
- package/dist/cjs/lib/delete-user-dialog.js.map +1 -0
- package/dist/cjs/lib/edit-user-details-dialog.d.ts +12 -0
- package/dist/cjs/lib/edit-user-details-dialog.d.ts.map +1 -0
- package/dist/cjs/lib/edit-user-details-dialog.js +81 -0
- package/dist/cjs/lib/edit-user-details-dialog.js.map +1 -0
- package/dist/cjs/lib/elements.d.ts +32 -0
- package/dist/cjs/lib/elements.d.ts.map +1 -0
- package/dist/cjs/lib/elements.js +57 -0
- package/dist/cjs/lib/elements.js.map +1 -0
- package/dist/cjs/lib/invite-user-dialog.d.ts +7 -0
- package/dist/cjs/lib/invite-user-dialog.d.ts.map +1 -0
- package/dist/cjs/lib/invite-user-dialog.js +167 -0
- package/dist/cjs/lib/invite-user-dialog.js.map +1 -0
- package/dist/cjs/lib/label.d.ts +7 -0
- package/dist/cjs/lib/label.d.ts.map +1 -0
- package/dist/cjs/lib/label.js +9 -0
- package/dist/cjs/lib/label.js.map +1 -0
- package/dist/cjs/lib/pagination.d.ts +8 -0
- package/dist/cjs/lib/pagination.d.ts.map +1 -0
- package/dist/cjs/lib/pagination.js +67 -0
- package/dist/cjs/lib/pagination.js.map +1 -0
- package/dist/cjs/lib/resend-invite-dialog.d.ts +10 -0
- package/dist/cjs/lib/resend-invite-dialog.d.ts.map +1 -0
- package/dist/cjs/lib/resend-invite-dialog.js +71 -0
- package/dist/cjs/lib/resend-invite-dialog.js.map +1 -0
- package/dist/cjs/lib/revoke-invite-dialog.d.ts +10 -0
- package/dist/cjs/lib/revoke-invite-dialog.d.ts.map +1 -0
- package/dist/cjs/lib/revoke-invite-dialog.js +37 -0
- package/dist/cjs/lib/revoke-invite-dialog.js.map +1 -0
- package/dist/cjs/lib/search-provider.d.ts +11 -0
- package/dist/cjs/lib/search-provider.d.ts.map +1 -0
- package/dist/cjs/lib/search-provider.js +55 -0
- package/dist/cjs/lib/search-provider.js.map +1 -0
- package/dist/cjs/lib/use-is-hydrated.d.ts +2 -0
- package/dist/cjs/lib/use-is-hydrated.d.ts.map +1 -0
- package/dist/cjs/lib/use-is-hydrated.js +34 -0
- package/dist/cjs/lib/use-is-hydrated.js.map +1 -0
- package/dist/cjs/lib/user-actions-dropdown.d.ts +9 -0
- package/dist/cjs/lib/user-actions-dropdown.d.ts.map +1 -0
- package/dist/cjs/lib/user-actions-dropdown.js +83 -0
- package/dist/cjs/lib/user-actions-dropdown.js.map +1 -0
- package/dist/cjs/lib/users-filter.d.ts +9 -0
- package/dist/cjs/lib/users-filter.d.ts.map +1 -0
- package/dist/cjs/lib/users-filter.js +63 -0
- package/dist/cjs/lib/users-filter.js.map +1 -0
- package/dist/cjs/lib/users-management-context.d.ts +23 -0
- package/dist/cjs/lib/users-management-context.d.ts.map +1 -0
- package/dist/cjs/lib/users-management-context.js +83 -0
- package/dist/cjs/lib/users-management-context.js.map +1 -0
- package/dist/cjs/lib/users-management-state.d.ts +22 -0
- package/dist/cjs/lib/users-management-state.d.ts.map +1 -0
- package/dist/cjs/lib/users-management-state.js +143 -0
- package/dist/cjs/lib/users-management-state.js.map +1 -0
- package/dist/cjs/lib/users-management.d.ts +12 -0
- package/dist/cjs/lib/users-management.d.ts.map +1 -0
- package/dist/cjs/lib/users-management.js +141 -0
- package/dist/cjs/lib/users-management.js.map +1 -0
- package/dist/cjs/lib/users-search.d.ts +3 -0
- package/dist/cjs/lib/users-search.d.ts.map +1 -0
- package/dist/cjs/lib/users-search.js +65 -0
- package/dist/cjs/lib/users-search.js.map +1 -0
- package/dist/cjs/lib/utils.d.ts +15 -0
- package/dist/cjs/lib/utils.d.ts.map +1 -0
- package/dist/cjs/lib/utils.js +78 -0
- package/dist/cjs/lib/utils.js.map +1 -0
- package/dist/cjs/lib/widgets-context.d.ts +11 -0
- package/dist/cjs/lib/widgets-context.d.ts.map +1 -0
- package/dist/cjs/lib/widgets-context.js +45 -0
- package/dist/cjs/lib/widgets-context.js.map +1 -0
- package/dist/cjs/users-management.client.d.ts +6 -0
- package/dist/cjs/users-management.client.d.ts.map +1 -0
- package/dist/cjs/users-management.client.js +57 -0
- package/dist/cjs/users-management.client.js.map +1 -0
- package/dist/cjs/workos-widgets.client.d.ts +17 -0
- package/dist/cjs/workos-widgets.client.d.ts.map +1 -0
- package/dist/cjs/workos-widgets.client.js +55 -0
- package/dist/cjs/workos-widgets.client.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/lib/api/config.d.ts +9 -0
- package/dist/esm/lib/api/config.d.ts.map +1 -0
- package/dist/esm/lib/api/config.js +9 -0
- package/dist/esm/lib/api/config.js.map +1 -0
- package/dist/esm/lib/api/role.d.ts +9 -0
- package/dist/esm/lib/api/role.d.ts.map +1 -0
- package/dist/esm/lib/api/role.js +89 -0
- package/dist/esm/lib/api/role.js.map +1 -0
- package/dist/esm/lib/api/user.d.ts +61 -0
- package/dist/esm/lib/api/user.d.ts.map +1 -0
- package/dist/esm/lib/api/user.js +302 -0
- package/dist/esm/lib/api/user.js.map +1 -0
- package/dist/esm/lib/constants.d.ts +3 -0
- package/dist/esm/lib/constants.d.ts.map +1 -0
- package/dist/esm/lib/constants.js +3 -0
- package/dist/esm/lib/constants.js.map +1 -0
- package/dist/esm/lib/delete-user-dialog.d.ts +12 -0
- package/dist/esm/lib/delete-user-dialog.d.ts.map +1 -0
- package/dist/esm/lib/delete-user-dialog.js +33 -0
- package/dist/esm/lib/delete-user-dialog.js.map +1 -0
- package/dist/esm/lib/edit-user-details-dialog.d.ts +12 -0
- package/dist/esm/lib/edit-user-details-dialog.d.ts.map +1 -0
- package/dist/esm/lib/edit-user-details-dialog.js +54 -0
- package/dist/esm/lib/edit-user-details-dialog.js.map +1 -0
- package/dist/esm/lib/elements.d.ts +32 -0
- package/dist/esm/lib/elements.d.ts.map +1 -0
- package/dist/esm/lib/elements.js +54 -0
- package/dist/esm/lib/elements.js.map +1 -0
- package/dist/esm/lib/invite-user-dialog.d.ts +7 -0
- package/dist/esm/lib/invite-user-dialog.d.ts.map +1 -0
- package/dist/esm/lib/invite-user-dialog.js +140 -0
- package/dist/esm/lib/invite-user-dialog.js.map +1 -0
- package/dist/esm/lib/label.d.ts +7 -0
- package/dist/esm/lib/label.d.ts.map +1 -0
- package/dist/esm/lib/label.js +6 -0
- package/dist/esm/lib/label.js.map +1 -0
- package/dist/esm/lib/pagination.d.ts +8 -0
- package/dist/esm/lib/pagination.d.ts.map +1 -0
- package/dist/esm/lib/pagination.js +40 -0
- package/dist/esm/lib/pagination.js.map +1 -0
- package/dist/esm/lib/resend-invite-dialog.d.ts +10 -0
- package/dist/esm/lib/resend-invite-dialog.d.ts.map +1 -0
- package/dist/esm/lib/resend-invite-dialog.js +44 -0
- package/dist/esm/lib/resend-invite-dialog.js.map +1 -0
- package/dist/esm/lib/revoke-invite-dialog.d.ts +10 -0
- package/dist/esm/lib/revoke-invite-dialog.d.ts.map +1 -0
- package/dist/esm/lib/revoke-invite-dialog.js +33 -0
- package/dist/esm/lib/revoke-invite-dialog.js.map +1 -0
- package/dist/esm/lib/search-provider.d.ts +11 -0
- package/dist/esm/lib/search-provider.d.ts.map +1 -0
- package/dist/esm/lib/search-provider.js +27 -0
- package/dist/esm/lib/search-provider.js.map +1 -0
- package/dist/esm/lib/use-is-hydrated.d.ts +2 -0
- package/dist/esm/lib/use-is-hydrated.d.ts.map +1 -0
- package/dist/esm/lib/use-is-hydrated.js +8 -0
- package/dist/esm/lib/use-is-hydrated.js.map +1 -0
- package/dist/esm/lib/user-actions-dropdown.d.ts +9 -0
- package/dist/esm/lib/user-actions-dropdown.d.ts.map +1 -0
- package/dist/esm/lib/user-actions-dropdown.js +56 -0
- package/dist/esm/lib/user-actions-dropdown.js.map +1 -0
- package/dist/esm/lib/users-filter.d.ts +9 -0
- package/dist/esm/lib/users-filter.d.ts.map +1 -0
- package/dist/esm/lib/users-filter.js +36 -0
- package/dist/esm/lib/users-filter.js.map +1 -0
- package/dist/esm/lib/users-management-context.d.ts +23 -0
- package/dist/esm/lib/users-management-context.d.ts.map +1 -0
- package/dist/esm/lib/users-management-context.js +54 -0
- package/dist/esm/lib/users-management-context.js.map +1 -0
- package/dist/esm/lib/users-management-state.d.ts +22 -0
- package/dist/esm/lib/users-management-state.d.ts.map +1 -0
- package/dist/esm/lib/users-management-state.js +117 -0
- package/dist/esm/lib/users-management-state.js.map +1 -0
- package/dist/esm/lib/users-management.d.ts +12 -0
- package/dist/esm/lib/users-management.d.ts.map +1 -0
- package/dist/esm/lib/users-management.js +114 -0
- package/dist/esm/lib/users-management.js.map +1 -0
- package/dist/esm/lib/users-search.d.ts +3 -0
- package/dist/esm/lib/users-search.d.ts.map +1 -0
- package/dist/esm/lib/users-search.js +39 -0
- package/dist/esm/lib/users-search.js.map +1 -0
- package/dist/esm/lib/utils.d.ts +15 -0
- package/dist/esm/lib/utils.d.ts.map +1 -0
- package/dist/esm/lib/utils.js +70 -0
- package/dist/esm/lib/utils.js.map +1 -0
- package/dist/esm/lib/widgets-context.d.ts +11 -0
- package/dist/esm/lib/widgets-context.d.ts.map +1 -0
- package/dist/esm/lib/widgets-context.js +17 -0
- package/dist/esm/lib/widgets-context.js.map +1 -0
- package/dist/esm/users-management.client.d.ts +6 -0
- package/dist/esm/users-management.client.d.ts.map +1 -0
- package/dist/esm/users-management.client.js +30 -0
- package/dist/esm/users-management.client.js.map +1 -0
- package/dist/esm/workos-widgets.client.d.ts +17 -0
- package/dist/esm/workos-widgets.client.d.ts.map +1 -0
- package/dist/esm/workos-widgets.client.js +28 -0
- package/dist/esm/workos-widgets.client.js.map +1 -0
- package/dist/tsconfig.cjs.tsbuildinfo +1 -0
- package/dist/tsconfig.esm.tsbuildinfo +1 -0
- package/package.json +69 -0
- package/src/index.ts +5 -0
- package/src/lib/api/config.ts +9 -0
- package/src/lib/api/role.ts +124 -0
- package/src/lib/api/user.ts +458 -0
- package/src/lib/constants.ts +2 -0
- package/src/lib/delete-user-dialog.tsx +103 -0
- package/src/lib/edit-user-details-dialog.tsx +170 -0
- package/src/lib/elements.tsx +175 -0
- package/src/lib/invite-user-dialog.tsx +319 -0
- package/src/lib/label.tsx +14 -0
- package/src/lib/pagination.tsx +69 -0
- package/src/lib/resend-invite-dialog.tsx +136 -0
- package/src/lib/revoke-invite-dialog.tsx +104 -0
- package/src/lib/search-provider.tsx +51 -0
- package/src/lib/use-is-hydrated.ts +13 -0
- package/src/lib/user-actions-dropdown.tsx +161 -0
- package/src/lib/users-filter.tsx +122 -0
- package/src/lib/users-management-context.tsx +89 -0
- package/src/lib/users-management-state.ts +165 -0
- package/src/lib/users-management.tsx +461 -0
- package/src/lib/users-search.tsx +130 -0
- package/src/lib/utils.ts +94 -0
- package/src/lib/widgets-context.ts +29 -0
- package/src/users-management.client.tsx +59 -0
- 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,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
|
+
});
|