@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,170 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
Callout,
|
|
6
|
+
Dialog,
|
|
7
|
+
Flex,
|
|
8
|
+
Select,
|
|
9
|
+
Skeleton,
|
|
10
|
+
Text,
|
|
11
|
+
VisuallyHidden,
|
|
12
|
+
} from "@radix-ui/themes";
|
|
13
|
+
import { type ReactNode, useState } from "react";
|
|
14
|
+
import { useUpdateUserRole } from "./api/user";
|
|
15
|
+
import type { User } from "./api/user";
|
|
16
|
+
import { PrimaryButton, SecondaryButton } from "./elements";
|
|
17
|
+
import { getBestName } from "./utils";
|
|
18
|
+
import { useRoles } from "./api/role";
|
|
19
|
+
import { useElement } from "./widgets-context";
|
|
20
|
+
|
|
21
|
+
interface EditUserDialogProps extends Dialog.RootProps {
|
|
22
|
+
open: boolean;
|
|
23
|
+
onOpenChange: (open: boolean) => void;
|
|
24
|
+
user: User;
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const EditUserDetailsDialog = ({
|
|
29
|
+
children,
|
|
30
|
+
user,
|
|
31
|
+
...props
|
|
32
|
+
}: EditUserDialogProps) => {
|
|
33
|
+
const displayName = getBestName(user) || user.email;
|
|
34
|
+
const rolesQuery = useRoles();
|
|
35
|
+
const { data: roles } = rolesQuery;
|
|
36
|
+
const updateUser = useUpdateUserRole();
|
|
37
|
+
const dropdownProps = useElement("dropdown");
|
|
38
|
+
const [selectedRole, setSelectedRole] = useState(
|
|
39
|
+
user.roles[0]?.slug || "Unknown",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const onSubmitForm = ({ id, roles }: { id: string; roles: string[] }) => {
|
|
43
|
+
updateUser.mutate(
|
|
44
|
+
{ id, data: { roles } },
|
|
45
|
+
{
|
|
46
|
+
onSuccess: () => {
|
|
47
|
+
props.onOpenChange(false);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const rootId = React.useId();
|
|
54
|
+
const formId = `edit-user-form-${rootId}`;
|
|
55
|
+
const selectId = `role-select-${rootId}`;
|
|
56
|
+
const selectLabelId = `${selectId}-label`;
|
|
57
|
+
const infoId = `${selectId}-info`;
|
|
58
|
+
const errorId = `${selectId}-error`;
|
|
59
|
+
const showErrorMessage = !!rolesQuery.error;
|
|
60
|
+
const showSingleRoleInfo = rolesQuery.isSuccess && roles.length === 1;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Dialog.Root {...props}>
|
|
64
|
+
{children && <Dialog.Trigger>{children}</Dialog.Trigger>}
|
|
65
|
+
<Dialog.Content maxWidth="480px">
|
|
66
|
+
<Dialog.Title>Edit role</Dialog.Title>
|
|
67
|
+
<Dialog.Description>
|
|
68
|
+
Select the role to assign to <Text weight="bold">{displayName}</Text>
|
|
69
|
+
</Dialog.Description>
|
|
70
|
+
<Flex mt="2" direction="column" gap="1" asChild>
|
|
71
|
+
<form
|
|
72
|
+
id={formId}
|
|
73
|
+
onSubmit={async (event) => {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
onSubmitForm({ id: user.id, roles: [selectedRole] });
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<Select.Root
|
|
79
|
+
name="roles"
|
|
80
|
+
value={selectedRole ?? "Unknown"}
|
|
81
|
+
onValueChange={setSelectedRole}
|
|
82
|
+
disabled={rolesQuery.isLoading || showSingleRoleInfo}
|
|
83
|
+
>
|
|
84
|
+
<Skeleton loading={rolesQuery.isLoading}>
|
|
85
|
+
<Select.Trigger
|
|
86
|
+
id={selectId}
|
|
87
|
+
placeholder="Assign a role"
|
|
88
|
+
aria-labelledby={selectLabelId}
|
|
89
|
+
aria-invalid={showErrorMessage || undefined}
|
|
90
|
+
aria-describedby={
|
|
91
|
+
[showErrorMessage && errorId, showSingleRoleInfo && infoId]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join(" ") || undefined
|
|
94
|
+
}
|
|
95
|
+
/>
|
|
96
|
+
</Skeleton>
|
|
97
|
+
|
|
98
|
+
<Select.Content {...dropdownProps}>
|
|
99
|
+
{roles.map((role) => (
|
|
100
|
+
<Select.Item key={role.slug} value={role.slug}>
|
|
101
|
+
{role.name}
|
|
102
|
+
</Select.Item>
|
|
103
|
+
))}
|
|
104
|
+
</Select.Content>
|
|
105
|
+
</Select.Root>
|
|
106
|
+
|
|
107
|
+
{showErrorMessage ? (
|
|
108
|
+
<Text color="red" size="2" id={errorId}>
|
|
109
|
+
{getRoleSelectErrorMessage(rolesQuery.error)}
|
|
110
|
+
</Text>
|
|
111
|
+
) : null}
|
|
112
|
+
|
|
113
|
+
{showSingleRoleInfo ? (
|
|
114
|
+
<Text color="gray" size="2" id={infoId} mt="1">
|
|
115
|
+
You cannot update the role for this user as there is only one
|
|
116
|
+
role available.
|
|
117
|
+
</Text>
|
|
118
|
+
) : null}
|
|
119
|
+
</form>
|
|
120
|
+
</Flex>
|
|
121
|
+
|
|
122
|
+
{updateUser.error ? (
|
|
123
|
+
<Callout.Root color="red" mt="4" mb="-2">
|
|
124
|
+
<Callout.Text>
|
|
125
|
+
{getMutationErrorMessage(updateUser.error)}
|
|
126
|
+
</Callout.Text>
|
|
127
|
+
</Callout.Root>
|
|
128
|
+
) : null}
|
|
129
|
+
|
|
130
|
+
<Flex mt="5" gap="3" justify="end">
|
|
131
|
+
<Dialog.Close>
|
|
132
|
+
<SecondaryButton disabled={updateUser.isPending}>
|
|
133
|
+
Cancel
|
|
134
|
+
</SecondaryButton>
|
|
135
|
+
</Dialog.Close>
|
|
136
|
+
|
|
137
|
+
<PrimaryButton
|
|
138
|
+
form={formId}
|
|
139
|
+
loading={updateUser.isPending}
|
|
140
|
+
disabled={rolesQuery.isLoading || showSingleRoleInfo || undefined}
|
|
141
|
+
>
|
|
142
|
+
Save
|
|
143
|
+
</PrimaryButton>
|
|
144
|
+
</Flex>
|
|
145
|
+
{/* mirror errors in a live region */}
|
|
146
|
+
<VisuallyHidden asChild>
|
|
147
|
+
<section aria-live="polite">
|
|
148
|
+
{getMutationErrorMessage(updateUser.error)}
|
|
149
|
+
</section>
|
|
150
|
+
</VisuallyHidden>
|
|
151
|
+
</Dialog.Content>
|
|
152
|
+
</Dialog.Root>
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
function getRoleSelectErrorMessage(error: unknown) {
|
|
157
|
+
if (!error) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
// TODO Handle server errors
|
|
161
|
+
return "There was an error fetching roles. Please try again.";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getMutationErrorMessage(error: unknown) {
|
|
165
|
+
if (!error) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
// TODO Handle server errors
|
|
169
|
+
return "There was an error updating the user role. Please try again.";
|
|
170
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
Avatar as RadixAvatar,
|
|
7
|
+
AvatarProps as RadixAvatarProps,
|
|
8
|
+
Badge as RadixBadge,
|
|
9
|
+
IconButton as RadixIconButton,
|
|
10
|
+
TextField as RadixTextField,
|
|
11
|
+
} from "@radix-ui/themes";
|
|
12
|
+
import type {
|
|
13
|
+
GetPropDefTypes,
|
|
14
|
+
avatarPropDefs,
|
|
15
|
+
badgePropDefs,
|
|
16
|
+
buttonPropDefs,
|
|
17
|
+
dropdownMenuContentPropDefs,
|
|
18
|
+
dropdownMenuItemPropDefs,
|
|
19
|
+
iconButtonPropDefs,
|
|
20
|
+
textFieldRootPropDefs,
|
|
21
|
+
} from "@radix-ui/themes/props";
|
|
22
|
+
import { type ComponentPropsWithoutRef, forwardRef } from "react";
|
|
23
|
+
import { useElement } from "./widgets-context";
|
|
24
|
+
|
|
25
|
+
export const PrimaryButton = forwardRef<
|
|
26
|
+
HTMLButtonElement,
|
|
27
|
+
ComponentPropsWithoutRef<typeof Button>
|
|
28
|
+
>((props, ref) => {
|
|
29
|
+
const element = useElement("primaryButton");
|
|
30
|
+
|
|
31
|
+
return <Button ref={ref} variant="solid" {...props} {...element} />;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
PrimaryButton.displayName = "PrimaryButton";
|
|
35
|
+
|
|
36
|
+
export const SecondaryButton = forwardRef<
|
|
37
|
+
HTMLButtonElement,
|
|
38
|
+
ComponentPropsWithoutRef<typeof Button>
|
|
39
|
+
>((props, ref) => {
|
|
40
|
+
const element = useElement("secondaryButton");
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Button ref={ref} variant="surface" color="gray" {...props} {...element} />
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
SecondaryButton.displayName = "SecondaryButton";
|
|
48
|
+
|
|
49
|
+
export const DestructiveButton = forwardRef<
|
|
50
|
+
HTMLButtonElement,
|
|
51
|
+
ComponentPropsWithoutRef<typeof Button>
|
|
52
|
+
>((props, ref) => {
|
|
53
|
+
const element = useElement("destructiveButton");
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Button ref={ref} variant="solid" color="red" {...props} {...element} />
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
DestructiveButton.displayName = "DestructiveButton";
|
|
61
|
+
|
|
62
|
+
export const IconButton = forwardRef<
|
|
63
|
+
HTMLButtonElement,
|
|
64
|
+
ComponentPropsWithoutRef<typeof RadixIconButton>
|
|
65
|
+
>((props, ref) => {
|
|
66
|
+
const element = useElement("iconButton");
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<RadixIconButton
|
|
70
|
+
ref={ref}
|
|
71
|
+
variant="ghost"
|
|
72
|
+
color="gray"
|
|
73
|
+
{...props}
|
|
74
|
+
{...element}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
IconButton.displayName = "IconButton";
|
|
80
|
+
|
|
81
|
+
export const TextField = forwardRef<
|
|
82
|
+
HTMLInputElement,
|
|
83
|
+
ComponentPropsWithoutRef<typeof RadixTextField.Root>
|
|
84
|
+
>((props, ref) => {
|
|
85
|
+
const element = useElement("textfield");
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<RadixTextField.Root
|
|
89
|
+
data-1p-ignore
|
|
90
|
+
ref={ref}
|
|
91
|
+
variant="surface"
|
|
92
|
+
{...props}
|
|
93
|
+
{...element}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
TextField.displayName = "TextField";
|
|
99
|
+
|
|
100
|
+
export const TextFieldSlot = RadixTextField.Slot;
|
|
101
|
+
|
|
102
|
+
export const Badge = forwardRef<
|
|
103
|
+
HTMLSpanElement,
|
|
104
|
+
ComponentPropsWithoutRef<typeof RadixBadge>
|
|
105
|
+
>((props, ref) => {
|
|
106
|
+
const element = useElement("badge");
|
|
107
|
+
|
|
108
|
+
return <RadixBadge ref={ref} {...element} {...props} />;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
Badge.displayName = "Badge";
|
|
112
|
+
|
|
113
|
+
export const PrimaryMenuItem = forwardRef<
|
|
114
|
+
HTMLDivElement,
|
|
115
|
+
ComponentPropsWithoutRef<typeof DropdownMenu.Item>
|
|
116
|
+
>((props, ref) => {
|
|
117
|
+
const element = useElement("primaryMenuItem");
|
|
118
|
+
|
|
119
|
+
return <DropdownMenu.Item ref={ref} {...props} {...element} />;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
PrimaryMenuItem.displayName = "PrimaryMenuItem";
|
|
123
|
+
|
|
124
|
+
export const DestructiveMenuItem = forwardRef<
|
|
125
|
+
HTMLDivElement,
|
|
126
|
+
ComponentPropsWithoutRef<typeof DropdownMenu.Item>
|
|
127
|
+
>((props, ref) => {
|
|
128
|
+
const element = useElement("destructiveMenuItem");
|
|
129
|
+
|
|
130
|
+
return <DropdownMenu.Item ref={ref} color="red" {...props} {...element} />;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
DestructiveMenuItem.displayName = "DestructiveMenuItem";
|
|
134
|
+
|
|
135
|
+
interface AvatarProps extends RadixAvatarProps {
|
|
136
|
+
dim?: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const Avatar = forwardRef<HTMLImageElement, AvatarProps>(
|
|
140
|
+
({ dim, ...props }, ref) => {
|
|
141
|
+
const element = useElement("avatar");
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<RadixAvatar
|
|
145
|
+
ref={ref}
|
|
146
|
+
color="gray"
|
|
147
|
+
{...props}
|
|
148
|
+
{...element}
|
|
149
|
+
// TODO: use CSS var instead of hard-coded value for opacity
|
|
150
|
+
style={dim ? { opacity: 0.6, ...props.style } : props.style}
|
|
151
|
+
/>
|
|
152
|
+
);
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
Avatar.displayName = "Avatar";
|
|
157
|
+
|
|
158
|
+
type OmitAsChild<T> = {
|
|
159
|
+
[K in keyof T]: T[K] extends undefined
|
|
160
|
+
? undefined
|
|
161
|
+
: Omit<NonNullable<T[K]>, "asChild">;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export type Elements = OmitAsChild<{
|
|
165
|
+
primaryButton?: GetPropDefTypes<typeof buttonPropDefs>;
|
|
166
|
+
secondaryButton?: GetPropDefTypes<typeof buttonPropDefs>;
|
|
167
|
+
destructiveButton?: GetPropDefTypes<typeof buttonPropDefs>;
|
|
168
|
+
iconButton?: GetPropDefTypes<typeof iconButtonPropDefs>;
|
|
169
|
+
textfield?: GetPropDefTypes<typeof textFieldRootPropDefs>;
|
|
170
|
+
badge?: GetPropDefTypes<typeof badgePropDefs>;
|
|
171
|
+
dropdown?: GetPropDefTypes<typeof dropdownMenuContentPropDefs>;
|
|
172
|
+
primaryMenuItem?: GetPropDefTypes<typeof dropdownMenuItemPropDefs>;
|
|
173
|
+
destructiveMenuItem?: GetPropDefTypes<typeof dropdownMenuItemPropDefs>;
|
|
174
|
+
avatar?: Omit<GetPropDefTypes<typeof avatarPropDefs>, "fallback">;
|
|
175
|
+
}>;
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Callout,
|
|
5
|
+
Dialog,
|
|
6
|
+
Flex,
|
|
7
|
+
Select,
|
|
8
|
+
Text,
|
|
9
|
+
VisuallyHidden,
|
|
10
|
+
} from "@radix-ui/themes";
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import { type Role, useRoles } from "./api/role";
|
|
13
|
+
import { type InviteUserPayload, useInviteUser } from "./api/user";
|
|
14
|
+
import { PrimaryButton, SecondaryButton, TextField } from "./elements";
|
|
15
|
+
import { Label } from "./label";
|
|
16
|
+
import { isErrorLike } from "./utils";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Used to stub a fake value for the role select. It will be selected by default
|
|
20
|
+
* before the role query resolves, or if the query results in an error. We do
|
|
21
|
+
* this because we need to provide _any_ value to the select to avoid
|
|
22
|
+
* controlled/uncontrolled bugs.
|
|
23
|
+
*/
|
|
24
|
+
const PLACEHOLDER_ROLE = "_rolePlaceholder";
|
|
25
|
+
|
|
26
|
+
interface InviteUserDialogProps {
|
|
27
|
+
children?: React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface DialogFormContextValue {
|
|
31
|
+
dialogId: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DialogFormContext = React.createContext<DialogFormContextValue>(null!);
|
|
35
|
+
DialogFormContext.displayName = "DialogFormContext";
|
|
36
|
+
|
|
37
|
+
export const InviteUserDialog = ({ children }: InviteUserDialogProps) => {
|
|
38
|
+
const [open, setOpen] = React.useState(false);
|
|
39
|
+
const dialogId = toId("invite-user", React.useId());
|
|
40
|
+
const formId = toId(dialogId, "form");
|
|
41
|
+
|
|
42
|
+
const inviteUser = useInviteUser();
|
|
43
|
+
const rolesQuery = useRoles();
|
|
44
|
+
const roles = rolesQuery.data;
|
|
45
|
+
const [selectedRole, setSelectedRole] = React.useState(
|
|
46
|
+
() => getDefaultRole(roles)?.slug || PLACEHOLDER_ROLE,
|
|
47
|
+
);
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
// Update the selected role if it's not in the list (eg if the list was
|
|
50
|
+
// previously empty and the query resolved)
|
|
51
|
+
setSelectedRole((selectedRole) => {
|
|
52
|
+
if (roles.find((role) => role.slug === selectedRole)) {
|
|
53
|
+
// if current selected role is in the new list, don't change it
|
|
54
|
+
return selectedRole;
|
|
55
|
+
}
|
|
56
|
+
return getDefaultRole(roles)?.slug || PLACEHOLDER_ROLE;
|
|
57
|
+
});
|
|
58
|
+
}, [roles]);
|
|
59
|
+
|
|
60
|
+
const onSubmitForm = (data: InviteUserPayload) => {
|
|
61
|
+
if (inviteUser.isPending || rolesQuery.status !== "success") {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
inviteUser.mutate(data, {
|
|
65
|
+
onSuccess: () => {
|
|
66
|
+
setOpen(false);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const formErrors = getFormErrors(inviteUser.error);
|
|
72
|
+
useFormFieldFocusOnError(dialogId, inviteUser.error);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Dialog.Root open={open} onOpenChange={setOpen}>
|
|
76
|
+
{children && <Dialog.Trigger>{children}</Dialog.Trigger>}
|
|
77
|
+
<Dialog.Content maxWidth="480px" key={String(open)}>
|
|
78
|
+
<Dialog.Title>Invite user</Dialog.Title>
|
|
79
|
+
<Dialog.Description>
|
|
80
|
+
An invitation will be sent to this email address with a link to
|
|
81
|
+
complete their account.
|
|
82
|
+
</Dialog.Description>
|
|
83
|
+
<DialogFormContext.Provider value={{ dialogId }}>
|
|
84
|
+
<Flex direction="column" gap="4" mt="5" asChild>
|
|
85
|
+
<form
|
|
86
|
+
id={formId}
|
|
87
|
+
onSubmit={async (event) => {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
onSubmitForm({
|
|
90
|
+
email: event.currentTarget.email.value,
|
|
91
|
+
role: selectedRole,
|
|
92
|
+
});
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<FormField
|
|
96
|
+
name="email"
|
|
97
|
+
label="Email address"
|
|
98
|
+
error={formErrors.fields.email}
|
|
99
|
+
required
|
|
100
|
+
control={(props) => (
|
|
101
|
+
<TextField
|
|
102
|
+
{...props}
|
|
103
|
+
data-1p-ignore
|
|
104
|
+
type="email"
|
|
105
|
+
autoComplete="email"
|
|
106
|
+
placeholder="Enter an email address"
|
|
107
|
+
/>
|
|
108
|
+
)}
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
<FormField
|
|
112
|
+
name="role"
|
|
113
|
+
label="Role"
|
|
114
|
+
error={formErrors.fields.role}
|
|
115
|
+
disabled={rolesQuery.isPending || roles.length <= 1}
|
|
116
|
+
info={
|
|
117
|
+
roles.length === 1 ? (
|
|
118
|
+
<>
|
|
119
|
+
New users will be invited with the{" "}
|
|
120
|
+
<Text weight="bold">{roles[0].name}</Text> role, as it is
|
|
121
|
+
the only one available.
|
|
122
|
+
</>
|
|
123
|
+
) : undefined
|
|
124
|
+
}
|
|
125
|
+
control={({
|
|
126
|
+
id,
|
|
127
|
+
"aria-invalid": ariaInvalid,
|
|
128
|
+
"aria-describedby": ariaDescribedBy,
|
|
129
|
+
...props
|
|
130
|
+
}) => (
|
|
131
|
+
<Select.Root
|
|
132
|
+
{...props}
|
|
133
|
+
value={selectedRole}
|
|
134
|
+
onValueChange={setSelectedRole}
|
|
135
|
+
>
|
|
136
|
+
<Select.Trigger
|
|
137
|
+
id={id}
|
|
138
|
+
aria-invalid={ariaInvalid}
|
|
139
|
+
aria-describedby={ariaDescribedBy}
|
|
140
|
+
/>
|
|
141
|
+
<Select.Content>
|
|
142
|
+
<Select.Item value={PLACEHOLDER_ROLE} disabled>
|
|
143
|
+
Select a role
|
|
144
|
+
</Select.Item>
|
|
145
|
+
{roles.map((role) => (
|
|
146
|
+
<Select.Item key={role.slug} value={role.slug}>
|
|
147
|
+
{role.name}
|
|
148
|
+
</Select.Item>
|
|
149
|
+
))}
|
|
150
|
+
</Select.Content>
|
|
151
|
+
</Select.Root>
|
|
152
|
+
)}
|
|
153
|
+
/>
|
|
154
|
+
</form>
|
|
155
|
+
</Flex>
|
|
156
|
+
</DialogFormContext.Provider>
|
|
157
|
+
|
|
158
|
+
{formErrors.form ? (
|
|
159
|
+
<Callout.Root color="red" mt="4" mb="-2">
|
|
160
|
+
<Callout.Text>{formErrors.form}</Callout.Text>
|
|
161
|
+
</Callout.Root>
|
|
162
|
+
) : null}
|
|
163
|
+
|
|
164
|
+
<Flex mt="5" gap="3" justify="end">
|
|
165
|
+
<Dialog.Close>
|
|
166
|
+
<SecondaryButton disabled={inviteUser.isPending}>
|
|
167
|
+
Cancel
|
|
168
|
+
</SecondaryButton>
|
|
169
|
+
</Dialog.Close>
|
|
170
|
+
<PrimaryButton
|
|
171
|
+
form={formId}
|
|
172
|
+
loading={inviteUser.isPending}
|
|
173
|
+
disabled={rolesQuery.isPending || undefined}
|
|
174
|
+
>
|
|
175
|
+
Invite
|
|
176
|
+
</PrimaryButton>
|
|
177
|
+
</Flex>
|
|
178
|
+
{/* mirror errors in a live region */}
|
|
179
|
+
<VisuallyHidden asChild>
|
|
180
|
+
<section aria-live="polite">{formErrors.form}</section>
|
|
181
|
+
</VisuallyHidden>
|
|
182
|
+
</Dialog.Content>
|
|
183
|
+
</Dialog.Root>
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
interface FormControlRenderProps {
|
|
188
|
+
id: string;
|
|
189
|
+
name: string;
|
|
190
|
+
"aria-describedby": string | undefined;
|
|
191
|
+
"aria-invalid"?: boolean;
|
|
192
|
+
required: boolean | undefined;
|
|
193
|
+
disabled: boolean | undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function FormField({
|
|
197
|
+
name,
|
|
198
|
+
label,
|
|
199
|
+
error,
|
|
200
|
+
info,
|
|
201
|
+
control,
|
|
202
|
+
required,
|
|
203
|
+
disabled,
|
|
204
|
+
}: {
|
|
205
|
+
name: string;
|
|
206
|
+
label: string;
|
|
207
|
+
error?: React.ReactNode;
|
|
208
|
+
info?: React.ReactNode;
|
|
209
|
+
control: (props: FormControlRenderProps) => React.ReactNode;
|
|
210
|
+
required?: boolean;
|
|
211
|
+
disabled?: boolean;
|
|
212
|
+
}) {
|
|
213
|
+
const { dialogId } = React.useContext(DialogFormContext);
|
|
214
|
+
const fieldId = toId(dialogId, name);
|
|
215
|
+
const errorId = toId(dialogId, name, "error");
|
|
216
|
+
const infoId = toId(dialogId, name, "info");
|
|
217
|
+
return (
|
|
218
|
+
<Flex direction="column" gap="1">
|
|
219
|
+
<Label htmlFor={fieldId}>{label}</Label>
|
|
220
|
+
{control({
|
|
221
|
+
id: fieldId,
|
|
222
|
+
name,
|
|
223
|
+
"aria-describedby": (() => {
|
|
224
|
+
const tags: string[] = [];
|
|
225
|
+
if (error) {
|
|
226
|
+
tags.push(errorId);
|
|
227
|
+
}
|
|
228
|
+
if (info) {
|
|
229
|
+
tags.push(infoId);
|
|
230
|
+
}
|
|
231
|
+
if (tags.length === 0) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
return tags.join(" ");
|
|
235
|
+
})(),
|
|
236
|
+
"aria-invalid": !!error || undefined,
|
|
237
|
+
required: required || undefined,
|
|
238
|
+
disabled: disabled || undefined,
|
|
239
|
+
})}
|
|
240
|
+
|
|
241
|
+
{error ? (
|
|
242
|
+
<Text color="red" size="2" id={errorId}>
|
|
243
|
+
{error}
|
|
244
|
+
</Text>
|
|
245
|
+
) : null}
|
|
246
|
+
{info ? (
|
|
247
|
+
<Text color="gray" size="2" id={infoId} mt="1">
|
|
248
|
+
{info}
|
|
249
|
+
</Text>
|
|
250
|
+
) : null}
|
|
251
|
+
</Flex>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function toId(...parts: string[]) {
|
|
256
|
+
return parts.join("-");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function getFormErrors(queryError: unknown) {
|
|
260
|
+
const formErrors = {
|
|
261
|
+
form: null as string | null,
|
|
262
|
+
fields: {
|
|
263
|
+
email: null as string | null,
|
|
264
|
+
role: null as string | null,
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
if (queryError) {
|
|
269
|
+
if (!isErrorLike(queryError)) {
|
|
270
|
+
return {
|
|
271
|
+
...formErrors,
|
|
272
|
+
form: "An unexpected error occurred. Please try again.",
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
switch (queryError.message.toLowerCase()) {
|
|
277
|
+
case "user already exists":
|
|
278
|
+
case "user already invited":
|
|
279
|
+
case "invalid email":
|
|
280
|
+
formErrors.fields.email = queryError.message;
|
|
281
|
+
break;
|
|
282
|
+
case "invalid role":
|
|
283
|
+
formErrors.fields.role = queryError.message;
|
|
284
|
+
break;
|
|
285
|
+
default:
|
|
286
|
+
// TODO handle more cases for various server errors
|
|
287
|
+
formErrors.form =
|
|
288
|
+
"There was an error inviting this user. Please refresh the page and try again.";
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return formErrors;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function useFormFieldFocusOnError(dialogId: string, queryError: unknown) {
|
|
297
|
+
React.useEffect(() => {
|
|
298
|
+
const fieldErrors = getFormErrors(queryError).fields;
|
|
299
|
+
for (const [name, error] of Object.entries(fieldErrors)) {
|
|
300
|
+
if (error) {
|
|
301
|
+
const fieldElement = document.getElementById(toId(dialogId, name)) as
|
|
302
|
+
| HTMLInputElement
|
|
303
|
+
| HTMLButtonElement
|
|
304
|
+
| null;
|
|
305
|
+
if (fieldElement) {
|
|
306
|
+
fieldElement?.focus();
|
|
307
|
+
if ("select" in fieldElement) {
|
|
308
|
+
fieldElement.select();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}, [dialogId, queryError]);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function getDefaultRole(roles: Role[]) {
|
|
318
|
+
return roles.find((role) => role.default) || roles[0];
|
|
319
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Text, type TextProps } from "@radix-ui/themes";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
|
|
4
|
+
type LabelProps = Omit<Extract<TextProps, { as: "label" }>, "as">;
|
|
5
|
+
|
|
6
|
+
export const Label = forwardRef<HTMLLabelElement, LabelProps>(
|
|
7
|
+
({ children, ...props }, ref) => (
|
|
8
|
+
<Text as="label" ref={ref} weight="bold" size="2" {...props}>
|
|
9
|
+
{children}
|
|
10
|
+
</Text>
|
|
11
|
+
),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
Label.displayName = "Label";
|