@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,461 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
Box,
|
|
6
|
+
Flex,
|
|
7
|
+
Grid,
|
|
8
|
+
Skeleton,
|
|
9
|
+
Table,
|
|
10
|
+
Text,
|
|
11
|
+
type TextProps,
|
|
12
|
+
VisuallyHidden,
|
|
13
|
+
} from "@radix-ui/themes";
|
|
14
|
+
import type { Role } from "./api/role";
|
|
15
|
+
import type { Paginated, User } from "./api/user";
|
|
16
|
+
import {
|
|
17
|
+
Avatar,
|
|
18
|
+
Badge,
|
|
19
|
+
IconButton,
|
|
20
|
+
PrimaryButton,
|
|
21
|
+
SecondaryButton,
|
|
22
|
+
} from "./elements";
|
|
23
|
+
import { InviteUserDialog } from "./invite-user-dialog";
|
|
24
|
+
import { Pagination } from "./pagination";
|
|
25
|
+
import { SearchProvider, useSearchContext } from "./search-provider";
|
|
26
|
+
import { useIsHydrated } from "./use-is-hydrated";
|
|
27
|
+
import { UserActionsDropdown } from "./user-actions-dropdown";
|
|
28
|
+
import { UsersFilter } from "./users-filter";
|
|
29
|
+
import { UsersSearch } from "./users-search";
|
|
30
|
+
import { getBestName, getComparativeReadableDate } from "./utils";
|
|
31
|
+
import { USER_ROW_LIMIT } from "./constants";
|
|
32
|
+
import { useUsersManagementContext } from "./users-management-context";
|
|
33
|
+
|
|
34
|
+
interface UsersManagementProps {
|
|
35
|
+
rolesData: Role[] | undefined;
|
|
36
|
+
userData: Paginated<User[]> | undefined;
|
|
37
|
+
|
|
38
|
+
// Render the rows with an alternate background color
|
|
39
|
+
alternate?: boolean;
|
|
40
|
+
// When the users list is loading new users
|
|
41
|
+
isPending?: boolean;
|
|
42
|
+
// Show the skeleton UI
|
|
43
|
+
isInitialLoading?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const UsersManagement = ({
|
|
47
|
+
userData,
|
|
48
|
+
rolesData,
|
|
49
|
+
alternate,
|
|
50
|
+
isPending,
|
|
51
|
+
isInitialLoading,
|
|
52
|
+
}: UsersManagementProps) => {
|
|
53
|
+
const users = userData?.data;
|
|
54
|
+
const usersCount = users?.length ?? 0;
|
|
55
|
+
const isHydrated = useIsHydrated();
|
|
56
|
+
return (
|
|
57
|
+
<SearchProvider>
|
|
58
|
+
<Flex direction="column" gap="3">
|
|
59
|
+
<Grid columns="1fr auto" gap="2">
|
|
60
|
+
<Flex gap="2" align="center">
|
|
61
|
+
<Skeleton loading={isInitialLoading}>
|
|
62
|
+
<Box flexBasis="380px" flexGrow="0" flexShrink="1">
|
|
63
|
+
<UsersSearch />
|
|
64
|
+
</Box>
|
|
65
|
+
</Skeleton>
|
|
66
|
+
<Skeleton loading={isInitialLoading}>
|
|
67
|
+
<Box flexGrow="0" flexShrink="0">
|
|
68
|
+
<UsersFilter roles={rolesData} />
|
|
69
|
+
</Box>
|
|
70
|
+
</Skeleton>
|
|
71
|
+
</Flex>
|
|
72
|
+
|
|
73
|
+
<Skeleton loading={isInitialLoading}>
|
|
74
|
+
<Box flexGrow="0" flexShrink="0" style={{ placeSelf: "flex-end" }}>
|
|
75
|
+
<InviteUserDialog>
|
|
76
|
+
<PrimaryButton>Invite user</PrimaryButton>
|
|
77
|
+
</InviteUserDialog>
|
|
78
|
+
</Box>
|
|
79
|
+
</Skeleton>
|
|
80
|
+
</Grid>
|
|
81
|
+
<Table.Root variant="ghost" size="1">
|
|
82
|
+
<Table.Header>
|
|
83
|
+
<Table.Row>
|
|
84
|
+
<Table.ColumnHeaderCell width="260px">
|
|
85
|
+
<Skeleton loading={isInitialLoading}>User</Skeleton>
|
|
86
|
+
</Table.ColumnHeaderCell>
|
|
87
|
+
<Table.ColumnHeaderCell width="100px">
|
|
88
|
+
<Skeleton loading={isInitialLoading}>Role</Skeleton>
|
|
89
|
+
</Table.ColumnHeaderCell>
|
|
90
|
+
<Table.ColumnHeaderCell width="140px">
|
|
91
|
+
<Skeleton loading={isInitialLoading}>Last active</Skeleton>
|
|
92
|
+
</Table.ColumnHeaderCell>
|
|
93
|
+
<Table.ColumnHeaderCell width="28px" />
|
|
94
|
+
</Table.Row>
|
|
95
|
+
</Table.Header>
|
|
96
|
+
|
|
97
|
+
<Table.Body
|
|
98
|
+
style={{
|
|
99
|
+
transition: `opacity 0.2s ease-out ${isPending ? "0.2s" : "0s"}`,
|
|
100
|
+
opacity: isPending && usersCount > 0 ? 0.5 : 1,
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
{isInitialLoading && (
|
|
104
|
+
<SkeletonRows length={USER_ROW_LIMIT} alternate={alternate} />
|
|
105
|
+
)}
|
|
106
|
+
{users?.map((user, i) => {
|
|
107
|
+
// TODO only support one role for now
|
|
108
|
+
const userRole = user.roles[0]?.name;
|
|
109
|
+
const userDisplayName = getBestName(user);
|
|
110
|
+
const dimText =
|
|
111
|
+
user.status === "InviteRevoked" ||
|
|
112
|
+
user.status === "InviteExpired";
|
|
113
|
+
return (
|
|
114
|
+
<Table.Row
|
|
115
|
+
key={user.id}
|
|
116
|
+
align="center"
|
|
117
|
+
style={{
|
|
118
|
+
background:
|
|
119
|
+
alternate && i % 2 === 1 ? "var(--gray-a1)" : undefined,
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
<Table.RowHeaderCell>
|
|
123
|
+
<Flex
|
|
124
|
+
align="center"
|
|
125
|
+
gap="3"
|
|
126
|
+
overflow="hidden"
|
|
127
|
+
height="var(--space-7)"
|
|
128
|
+
>
|
|
129
|
+
<Avatar
|
|
130
|
+
size="2"
|
|
131
|
+
fallback={<FallbackUserIcon />}
|
|
132
|
+
src={user.profilePictureUrl ?? undefined}
|
|
133
|
+
dim={dimText}
|
|
134
|
+
/>
|
|
135
|
+
|
|
136
|
+
{userDisplayName ? (
|
|
137
|
+
<Flex
|
|
138
|
+
direction="column"
|
|
139
|
+
align="start"
|
|
140
|
+
height="var(--space-7)"
|
|
141
|
+
justify="center"
|
|
142
|
+
overflow="hidden"
|
|
143
|
+
>
|
|
144
|
+
<Flex gap="2" align="center" minWidth="0">
|
|
145
|
+
<TableCellText dim={dimText}>
|
|
146
|
+
{userDisplayName}
|
|
147
|
+
</TableCellText>
|
|
148
|
+
<UserBadge user={user} />
|
|
149
|
+
</Flex>
|
|
150
|
+
<TableCellText
|
|
151
|
+
level="secondary"
|
|
152
|
+
title={user.email}
|
|
153
|
+
dim={dimText}
|
|
154
|
+
>
|
|
155
|
+
{user.email}
|
|
156
|
+
</TableCellText>
|
|
157
|
+
</Flex>
|
|
158
|
+
) : (
|
|
159
|
+
<Flex gap="2" align="center" minWidth="0">
|
|
160
|
+
<TableCellText dim={dimText} title={user.email}>
|
|
161
|
+
{user.email}
|
|
162
|
+
</TableCellText>
|
|
163
|
+
<UserBadge user={user} />
|
|
164
|
+
</Flex>
|
|
165
|
+
)}
|
|
166
|
+
</Flex>
|
|
167
|
+
</Table.RowHeaderCell>
|
|
168
|
+
<Table.Cell>
|
|
169
|
+
<TableCellText dim={dimText}>
|
|
170
|
+
{userRole || (
|
|
171
|
+
<>
|
|
172
|
+
<VisuallyHidden>No roles assigned</VisuallyHidden>
|
|
173
|
+
<span aria-hidden style={{ userSelect: "none" }}>
|
|
174
|
+
–
|
|
175
|
+
</span>
|
|
176
|
+
</>
|
|
177
|
+
)}
|
|
178
|
+
</TableCellText>
|
|
179
|
+
</Table.Cell>
|
|
180
|
+
<Table.Cell>
|
|
181
|
+
<LastActive
|
|
182
|
+
user={user}
|
|
183
|
+
isHydrated={isHydrated}
|
|
184
|
+
dim={dimText}
|
|
185
|
+
/>
|
|
186
|
+
</Table.Cell>
|
|
187
|
+
<Table.Cell justify="end">
|
|
188
|
+
<UserActionsDropdown user={user}>
|
|
189
|
+
<IconButton title="User actions">
|
|
190
|
+
<VisuallyHidden>User actions</VisuallyHidden>
|
|
191
|
+
<svg
|
|
192
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
193
|
+
fill="none"
|
|
194
|
+
viewBox="0 0 24 24"
|
|
195
|
+
width="16"
|
|
196
|
+
height="16"
|
|
197
|
+
strokeWidth={1.5}
|
|
198
|
+
stroke="currentColor"
|
|
199
|
+
aria-hidden
|
|
200
|
+
>
|
|
201
|
+
<path
|
|
202
|
+
strokeLinecap="round"
|
|
203
|
+
strokeLinejoin="round"
|
|
204
|
+
d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"
|
|
205
|
+
/>
|
|
206
|
+
</svg>
|
|
207
|
+
</IconButton>
|
|
208
|
+
</UserActionsDropdown>
|
|
209
|
+
</Table.Cell>
|
|
210
|
+
</Table.Row>
|
|
211
|
+
);
|
|
212
|
+
})}
|
|
213
|
+
|
|
214
|
+
{users?.length === 0 && (
|
|
215
|
+
<Table.Row align="center">
|
|
216
|
+
<Table.Cell colSpan={4}>
|
|
217
|
+
<UsersManagementEmptyState />
|
|
218
|
+
</Table.Cell>
|
|
219
|
+
</Table.Row>
|
|
220
|
+
)}
|
|
221
|
+
</Table.Body>
|
|
222
|
+
</Table.Root>
|
|
223
|
+
|
|
224
|
+
<Pagination isPending={isPending} pagination={userData?.pagination} />
|
|
225
|
+
</Flex>
|
|
226
|
+
</SearchProvider>
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
function UserBadge({ user }: { user: User }) {
|
|
231
|
+
// TODO: This is not yet available in the data. Update here after API is updated.
|
|
232
|
+
if (user.isLoggedInUser) {
|
|
233
|
+
return (
|
|
234
|
+
<Badge color="gray" style={{ userSelect: "none" }}>
|
|
235
|
+
You
|
|
236
|
+
</Badge>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (user.status === "Invited") {
|
|
240
|
+
return (
|
|
241
|
+
<Badge color="amber" style={{ userSelect: "none" }}>
|
|
242
|
+
<VisuallyHidden>Status: </VisuallyHidden>Invited
|
|
243
|
+
</Badge>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (user.status === "InviteExpired") {
|
|
247
|
+
return (
|
|
248
|
+
<Badge color="red" style={{ userSelect: "none" }}>
|
|
249
|
+
<VisuallyHidden>Status: Invite </VisuallyHidden>
|
|
250
|
+
Expired
|
|
251
|
+
</Badge>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (user.status === "InviteRevoked") {
|
|
255
|
+
return (
|
|
256
|
+
<Badge color="red" style={{ userSelect: "none" }}>
|
|
257
|
+
<VisuallyHidden>Status: Invite </VisuallyHidden>
|
|
258
|
+
Revoked
|
|
259
|
+
</Badge>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
interface LastActiveProps {
|
|
266
|
+
user: User;
|
|
267
|
+
isHydrated: boolean;
|
|
268
|
+
dim?: boolean;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function LastActive(props: LastActiveProps) {
|
|
272
|
+
if (!props.user.lastActivityAt) {
|
|
273
|
+
return (
|
|
274
|
+
<>
|
|
275
|
+
<VisuallyHidden>
|
|
276
|
+
{props.user.status === "Active" ? "Never" : "Not active"}
|
|
277
|
+
</VisuallyHidden>
|
|
278
|
+
<TableCellText
|
|
279
|
+
dim={props.dim}
|
|
280
|
+
aria-hidden
|
|
281
|
+
style={{ userSelect: "none" }}
|
|
282
|
+
>
|
|
283
|
+
–
|
|
284
|
+
</TableCellText>
|
|
285
|
+
</>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
return <LastActiveImpl {...props} date={props.user.lastActivityAt} />;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function LastActiveImpl({
|
|
292
|
+
date,
|
|
293
|
+
isHydrated,
|
|
294
|
+
dim,
|
|
295
|
+
}: LastActiveProps & { date: string }) {
|
|
296
|
+
const { lastActiveDateTime, lastActiveDisplay } = React.useMemo(() => {
|
|
297
|
+
const defaultTimeZone = "America/Los_Angeles";
|
|
298
|
+
const lastActiveDate = new Date(date);
|
|
299
|
+
const lastActiveDateTime = lastActiveDate.toLocaleTimeString("en-US", {
|
|
300
|
+
// hard-coded timezone before hydration to prevent server/client mismatch
|
|
301
|
+
timeZone: isHydrated ? undefined : defaultTimeZone,
|
|
302
|
+
month: "long",
|
|
303
|
+
day: "numeric",
|
|
304
|
+
year: "numeric",
|
|
305
|
+
hour: "numeric",
|
|
306
|
+
minute: "numeric",
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Server and client may produce a different 'now' date, so only
|
|
310
|
+
// show comparative date if the component is hydrated to prevent a
|
|
311
|
+
// server/client mismatch
|
|
312
|
+
const lastActiveDisplay = isHydrated
|
|
313
|
+
? getComparativeReadableDate(new Date(), lastActiveDate)
|
|
314
|
+
: lastActiveDate.toLocaleDateString("en-US", {
|
|
315
|
+
// hard-coded timezone to prevent server/client mismatch
|
|
316
|
+
timeZone: defaultTimeZone,
|
|
317
|
+
month: "long",
|
|
318
|
+
day: "numeric",
|
|
319
|
+
year: "numeric",
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return { lastActiveDateTime, lastActiveDisplay };
|
|
323
|
+
}, [isHydrated, date]);
|
|
324
|
+
|
|
325
|
+
// handle cases where the DB might return an invalid date string
|
|
326
|
+
if (lastActiveDisplay === "Invalid Date") {
|
|
327
|
+
return (
|
|
328
|
+
<>
|
|
329
|
+
<VisuallyHidden>Unknown</VisuallyHidden>
|
|
330
|
+
<TableCellText dim={dim} aria-hidden style={{ userSelect: "none" }}>
|
|
331
|
+
–
|
|
332
|
+
</TableCellText>
|
|
333
|
+
</>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<TableCellText asChild dim={dim}>
|
|
339
|
+
<time dateTime={date} title={lastActiveDateTime}>
|
|
340
|
+
{lastActiveDisplay}
|
|
341
|
+
</time>
|
|
342
|
+
</TableCellText>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const SkeletonRows = ({
|
|
347
|
+
length,
|
|
348
|
+
alternate = false,
|
|
349
|
+
}: {
|
|
350
|
+
length: number;
|
|
351
|
+
alternate?: boolean;
|
|
352
|
+
}) => {
|
|
353
|
+
return Array.from({ length }, (_, index) => {
|
|
354
|
+
return (
|
|
355
|
+
<Table.Row
|
|
356
|
+
key={index}
|
|
357
|
+
align="center"
|
|
358
|
+
style={{
|
|
359
|
+
background:
|
|
360
|
+
alternate && index % 2 === 1 ? "var(--gray-a1)" : undefined,
|
|
361
|
+
}}
|
|
362
|
+
>
|
|
363
|
+
<Table.RowHeaderCell>
|
|
364
|
+
<Flex align="center" gap="3">
|
|
365
|
+
<Skeleton>
|
|
366
|
+
<Avatar size="2" fallback="F" />
|
|
367
|
+
</Skeleton>
|
|
368
|
+
|
|
369
|
+
<Flex direction="column" height="var(--space-7)" justify="center">
|
|
370
|
+
<Skeleton width="180px" height="var(--space-4)" />
|
|
371
|
+
<Skeleton width="90px" height="var(--space-3)" mt="1" />
|
|
372
|
+
</Flex>
|
|
373
|
+
</Flex>
|
|
374
|
+
</Table.RowHeaderCell>
|
|
375
|
+
<Table.Cell>
|
|
376
|
+
<Flex wrap="wrap" gap="1">
|
|
377
|
+
<Skeleton width="75px" height="var(--space-4)" />
|
|
378
|
+
</Flex>
|
|
379
|
+
</Table.Cell>
|
|
380
|
+
<Table.Cell>
|
|
381
|
+
<Skeleton width="120px" height="var(--space-4)" />
|
|
382
|
+
</Table.Cell>
|
|
383
|
+
<Table.Cell justify="end" />
|
|
384
|
+
</Table.Row>
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const TableCellText = React.forwardRef<HTMLSpanElement, TableCellTextProps>(
|
|
390
|
+
function TableCellText(
|
|
391
|
+
{ children, dim, level = "primary", ...props },
|
|
392
|
+
forwardedRef,
|
|
393
|
+
) {
|
|
394
|
+
return (
|
|
395
|
+
<Text
|
|
396
|
+
ref={forwardedRef}
|
|
397
|
+
color={level === "secondary" ? "gray" : undefined}
|
|
398
|
+
weight={level === "secondary" ? "regular" : "medium"}
|
|
399
|
+
size={level === "secondary" ? "1" : "2"}
|
|
400
|
+
truncate
|
|
401
|
+
{...props}
|
|
402
|
+
style={
|
|
403
|
+
dim
|
|
404
|
+
? {
|
|
405
|
+
// TODO: use CSS var instead of hard-coded value for opacity
|
|
406
|
+
opacity: 0.6,
|
|
407
|
+
...props.style,
|
|
408
|
+
}
|
|
409
|
+
: props.style
|
|
410
|
+
}
|
|
411
|
+
>
|
|
412
|
+
{children}
|
|
413
|
+
</Text>
|
|
414
|
+
);
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
type TableCellTextProps = TextProps & {
|
|
419
|
+
level?: "primary" | "secondary";
|
|
420
|
+
dim?: boolean;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const FallbackUserIcon = () => (
|
|
424
|
+
<svg
|
|
425
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
426
|
+
width="20"
|
|
427
|
+
height="20"
|
|
428
|
+
fill="currentColor"
|
|
429
|
+
viewBox="0 0 256 256"
|
|
430
|
+
>
|
|
431
|
+
<title>User icon</title>
|
|
432
|
+
<path d="M229.19,213c-15.81-27.32-40.63-46.49-69.47-54.62a70,70,0,1,0-63.44,0C67.44,166.5,42.62,185.67,26.81,213a6,6,0,1,0,10.38,6C56.4,185.81,90.34,166,128,166s71.6,19.81,90.81,53a6,6,0,1,0,10.38-6ZM70,96a58,58,0,1,1,58,58A58.07,58.07,0,0,1,70,96Z" />
|
|
433
|
+
</svg>
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
const UsersManagementEmptyState = () => {
|
|
437
|
+
const { clearSearch } = useSearchContext();
|
|
438
|
+
const {
|
|
439
|
+
state: { searchQuery },
|
|
440
|
+
} = useUsersManagementContext();
|
|
441
|
+
|
|
442
|
+
if (searchQuery) {
|
|
443
|
+
return (
|
|
444
|
+
<Flex align="center" justify="center" py="8" direction="column" gap="2">
|
|
445
|
+
<Text size="2">
|
|
446
|
+
No users found for query <Text weight="medium">“{searchQuery}”</Text>
|
|
447
|
+
</Text>
|
|
448
|
+
|
|
449
|
+
<SecondaryButton size="1" onClick={clearSearch}>
|
|
450
|
+
Clear search
|
|
451
|
+
</SecondaryButton>
|
|
452
|
+
</Flex>
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<Flex align="center" justify="center" py="8" gap="2">
|
|
458
|
+
<Text size="2">No users found</Text>
|
|
459
|
+
</Flex>
|
|
460
|
+
);
|
|
461
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useComposedRefs } from "@radix-ui/react-compose-refs";
|
|
4
|
+
import { Cross2Icon, MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
|
5
|
+
import { Checkbox, DropdownMenu, Flex, IconButton } from "@radix-ui/themes";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { useDebouncedCallback } from "use-debounce";
|
|
8
|
+
import { PrimaryMenuItem, TextField, TextFieldSlot } from "./elements";
|
|
9
|
+
import { useSearchContext } from "./search-provider";
|
|
10
|
+
import { useUsersManagementContext } from "./users-management-context";
|
|
11
|
+
|
|
12
|
+
type UsersSearchProps = React.ComponentPropsWithoutRef<typeof TextField>;
|
|
13
|
+
|
|
14
|
+
export const UsersSearch = React.forwardRef<HTMLInputElement, UsersSearchProps>(
|
|
15
|
+
(props, ref) => {
|
|
16
|
+
const { inputRef, clearSearch, searchValue, setSearchValue } =
|
|
17
|
+
useSearchContext();
|
|
18
|
+
const { dispatch } = useUsersManagementContext();
|
|
19
|
+
|
|
20
|
+
const filter = useDebouncedCallback((value) => {
|
|
21
|
+
dispatch({ type: "FILTER_BY_SEARCH", searchQuery: value });
|
|
22
|
+
}, 200);
|
|
23
|
+
|
|
24
|
+
const resetSearch = () => {
|
|
25
|
+
clearSearch();
|
|
26
|
+
filter.cancel();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<TextField
|
|
31
|
+
ref={useComposedRefs(inputRef, ref)}
|
|
32
|
+
autoComplete="off"
|
|
33
|
+
placeholder="Search by name or e-mail"
|
|
34
|
+
value={searchValue}
|
|
35
|
+
onChange={(event) => {
|
|
36
|
+
const value = event.target.value;
|
|
37
|
+
setSearchValue(value);
|
|
38
|
+
filter(value);
|
|
39
|
+
}}
|
|
40
|
+
onKeyDown={(event) => {
|
|
41
|
+
if (event.key === "Escape") {
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
resetSearch();
|
|
44
|
+
}
|
|
45
|
+
}}
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
48
|
+
<TextFieldSlot side="left">
|
|
49
|
+
<MagnifyingGlassIcon aria-hidden="true" height="16" width="16" />
|
|
50
|
+
</TextFieldSlot>
|
|
51
|
+
|
|
52
|
+
<TextFieldSlot side="right">
|
|
53
|
+
{searchValue && (
|
|
54
|
+
<IconButton
|
|
55
|
+
size="1"
|
|
56
|
+
color="gray"
|
|
57
|
+
variant="ghost"
|
|
58
|
+
radius="full"
|
|
59
|
+
onClick={resetSearch}
|
|
60
|
+
aria-label="Clear search"
|
|
61
|
+
title="Clear search"
|
|
62
|
+
>
|
|
63
|
+
<Cross2Icon aria-hidden="true" />
|
|
64
|
+
</IconButton>
|
|
65
|
+
)}
|
|
66
|
+
{/* <FilterMenu /> */}
|
|
67
|
+
</TextFieldSlot>
|
|
68
|
+
</TextField>
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
74
|
+
function FilterMenu() {
|
|
75
|
+
return (
|
|
76
|
+
<DropdownMenu.Root>
|
|
77
|
+
<DropdownMenu.Trigger>
|
|
78
|
+
<IconButton
|
|
79
|
+
size="1"
|
|
80
|
+
color="gray"
|
|
81
|
+
variant="ghost"
|
|
82
|
+
radius="full"
|
|
83
|
+
aria-label="Filter users"
|
|
84
|
+
title="Filter users"
|
|
85
|
+
>
|
|
86
|
+
<FilterIcon aria-hidden="true" />
|
|
87
|
+
</IconButton>
|
|
88
|
+
</DropdownMenu.Trigger>
|
|
89
|
+
<DropdownMenu.Content size="2" align="end">
|
|
90
|
+
<PrimaryMenuItem>
|
|
91
|
+
<Flex gap="2" align="center">
|
|
92
|
+
<Checkbox variant="surface" />
|
|
93
|
+
One
|
|
94
|
+
</Flex>
|
|
95
|
+
</PrimaryMenuItem>
|
|
96
|
+
<PrimaryMenuItem>
|
|
97
|
+
<Flex gap="2" align="center">
|
|
98
|
+
<Checkbox />
|
|
99
|
+
Two
|
|
100
|
+
</Flex>
|
|
101
|
+
</PrimaryMenuItem>
|
|
102
|
+
</DropdownMenu.Content>
|
|
103
|
+
</DropdownMenu.Root>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const FilterIcon = React.forwardRef<
|
|
108
|
+
SVGSVGElement,
|
|
109
|
+
React.ComponentPropsWithRef<"svg">
|
|
110
|
+
>(function FilterIcon({ children, ...props }, forwardedRef) {
|
|
111
|
+
return (
|
|
112
|
+
<svg
|
|
113
|
+
width="15"
|
|
114
|
+
height="15"
|
|
115
|
+
viewBox="0 0 15 15"
|
|
116
|
+
fill="currentColor"
|
|
117
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
118
|
+
ref={forwardedRef}
|
|
119
|
+
{...props}
|
|
120
|
+
>
|
|
121
|
+
{children}
|
|
122
|
+
<path
|
|
123
|
+
fillRule="evenodd"
|
|
124
|
+
clipRule="evenodd"
|
|
125
|
+
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"
|
|
126
|
+
fill="black"
|
|
127
|
+
/>
|
|
128
|
+
</svg>
|
|
129
|
+
);
|
|
130
|
+
});
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { User } from "./api/user";
|
|
2
|
+
|
|
3
|
+
export const canUseDOM = !!(
|
|
4
|
+
typeof window !== "undefined" &&
|
|
5
|
+
window.document &&
|
|
6
|
+
window.document.createElement
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
export function getBestName({
|
|
10
|
+
firstName,
|
|
11
|
+
lastName,
|
|
12
|
+
}: Pick<User, "firstName" | "lastName">) {
|
|
13
|
+
return [firstName, lastName].filter(Boolean).join(" ") || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getComparativeReadableDate(
|
|
17
|
+
now: Date,
|
|
18
|
+
then: Date,
|
|
19
|
+
options?: { timeZone?: string },
|
|
20
|
+
): string {
|
|
21
|
+
const timeSince = now.getTime() - then.getTime();
|
|
22
|
+
|
|
23
|
+
// Has it been less than a minute?
|
|
24
|
+
if (timeSince < 60_000) {
|
|
25
|
+
return "Just now";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Has it been less than an hour?
|
|
29
|
+
if (timeSince < 3_600_000) {
|
|
30
|
+
const timePassed = Math.floor(timeSince / 60_000);
|
|
31
|
+
return timePassed === 1 ? "1 minute ago" : `${timePassed} minutes ago`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Has it been less than a day?
|
|
35
|
+
if (timeSince < 86_400_000) {
|
|
36
|
+
const timePassed = Math.floor(timeSince / 3_600_000);
|
|
37
|
+
return timePassed === 1 ? "1 hour ago" : `${timePassed} hours ago`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Has it been less than a week?
|
|
41
|
+
if (timeSince < 604_800_000) {
|
|
42
|
+
const timePassed = Math.floor(timeSince / 86_400_000);
|
|
43
|
+
return timePassed === 1 ? "1 day ago" : `${timePassed} days ago`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Has it been less than a month?
|
|
47
|
+
if (timeSince < 2_592_000_000) {
|
|
48
|
+
const timePassed = Math.floor(timeSince / 604_800_000);
|
|
49
|
+
return timePassed === 1 ? "1 week ago" : `${timePassed} weeks ago`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Any later?
|
|
53
|
+
return then.toLocaleDateString("en-US", {
|
|
54
|
+
timeZone: options?.timeZone,
|
|
55
|
+
month: "long",
|
|
56
|
+
day: "numeric",
|
|
57
|
+
// omit year if it's the same as the current year
|
|
58
|
+
year: now.getFullYear() !== then.getFullYear() ? "numeric" : undefined,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isObjectLike(value: unknown): value is Record<string, unknown> {
|
|
63
|
+
return typeof value === "object" && value !== null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isErrorLike(
|
|
67
|
+
value: unknown,
|
|
68
|
+
): value is Record<string, unknown> & { message: string } {
|
|
69
|
+
return isObjectLike(value) && typeof value.message === "string";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function parseErrorResponse(
|
|
73
|
+
response: Response,
|
|
74
|
+
): Promise<{ message: string; status: number }> {
|
|
75
|
+
try {
|
|
76
|
+
const json = await response.json();
|
|
77
|
+
if (!isObjectLike(json) || typeof json.message !== "string") {
|
|
78
|
+
return {
|
|
79
|
+
status: response.status,
|
|
80
|
+
message: response.statusText,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
...json,
|
|
85
|
+
status: response.status,
|
|
86
|
+
message: json.message || response.statusText,
|
|
87
|
+
};
|
|
88
|
+
} catch {
|
|
89
|
+
return {
|
|
90
|
+
status: response.status,
|
|
91
|
+
message: response.statusText,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|