@openneuro/app 4.35.0 → 4.36.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/package.json +3 -3
- package/src/client.jsx +1 -0
- package/src/scripts/components/button/button.scss +14 -0
- package/src/scripts/components/page/page.scss +2 -70
- package/src/scripts/components/search-page/SearchResultItem.tsx +3 -1
- package/src/scripts/components/search-page/search-page.scss +1 -13
- package/src/scripts/config.ts +6 -0
- package/src/scripts/dataset/files/__tests__/__snapshots__/file.spec.jsx.snap +2 -2
- package/src/scripts/dataset/files/file.tsx +2 -2
- package/src/scripts/dataset/mutations/__tests__/update-file.spec.tsx +126 -0
- package/src/scripts/dataset/mutations/update-file.jsx +20 -5
- package/src/scripts/dataset/routes/snapshot.tsx +1 -1
- package/src/scripts/errors/errorRoute.tsx +2 -0
- package/src/scripts/pages/admin/__tests__/users.spec.tsx +51 -18
- package/src/scripts/pages/admin/admin.jsx +2 -2
- package/src/scripts/pages/admin/user-fragment.ts +10 -3
- package/src/scripts/pages/admin/user-summary.tsx +100 -0
- package/src/scripts/pages/admin/user-tools.tsx +81 -58
- package/src/scripts/pages/admin/users.module.scss +277 -0
- package/src/scripts/pages/admin/users.tsx +351 -152
- package/src/scripts/queries/user.ts +120 -3
- package/src/scripts/queries/users.ts +247 -0
- package/src/scripts/routes.tsx +7 -15
- package/src/scripts/types/user-types.ts +12 -13
- package/src/scripts/uploader/file-select.tsx +42 -57
- package/src/scripts/uploader/upload-select.jsx +1 -1
- package/src/scripts/users/__tests__/dataset-card.spec.tsx +127 -0
- package/src/scripts/users/__tests__/user-account-view.spec.tsx +150 -67
- package/src/scripts/users/__tests__/user-card.spec.tsx +6 -17
- package/src/scripts/users/__tests__/user-query.spec.tsx +133 -38
- package/src/scripts/users/__tests__/user-routes.spec.tsx +156 -27
- package/src/scripts/users/__tests__/user-tabs.spec.tsx +7 -7
- package/src/scripts/users/components/edit-list.tsx +26 -5
- package/src/scripts/users/components/edit-string.tsx +40 -13
- package/src/scripts/users/components/editable-content.tsx +10 -3
- package/src/scripts/users/components/user-dataset-filters.tsx +205 -121
- package/src/scripts/users/dataset-card.tsx +3 -2
- package/src/scripts/users/github-auth-button.tsx +98 -0
- package/src/scripts/users/scss/datasetcard.module.scss +65 -12
- package/src/scripts/users/scss/useraccountview.module.scss +1 -1
- package/src/scripts/users/user-account-view.tsx +43 -34
- package/src/scripts/users/user-card.tsx +23 -22
- package/src/scripts/users/user-container.tsx +9 -5
- package/src/scripts/users/user-datasets-view.tsx +350 -40
- package/src/scripts/users/user-menu.tsx +4 -9
- package/src/scripts/users/user-notifications-view.tsx +9 -7
- package/src/scripts/users/user-query.tsx +3 -6
- package/src/scripts/users/user-routes.tsx +11 -5
- package/src/scripts/users/user-tabs.tsx +4 -2
- package/src/scripts/users/__tests__/datasest-card.spec.tsx +0 -201
- package/src/scripts/users/fragments/query.js +0 -42
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react"
|
|
2
|
+
import { gql, useQuery } from "@apollo/client"
|
|
3
|
+
import * as Sentry from "@sentry/react"
|
|
4
|
+
import type { User } from "../types/user-types"
|
|
5
|
+
|
|
6
|
+
// --- Fragments ---
|
|
7
|
+
export const USER_FRAGMENT = gql`
|
|
8
|
+
fragment userFields on User {
|
|
9
|
+
id
|
|
10
|
+
name
|
|
11
|
+
admin
|
|
12
|
+
blocked
|
|
13
|
+
email
|
|
14
|
+
provider
|
|
15
|
+
lastSeen
|
|
16
|
+
created
|
|
17
|
+
avatar
|
|
18
|
+
github
|
|
19
|
+
institution
|
|
20
|
+
location
|
|
21
|
+
modified
|
|
22
|
+
orcid
|
|
23
|
+
}
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
// --- GET_USERS QUERY ---
|
|
27
|
+
export const GET_USERS = gql`
|
|
28
|
+
query GetUsers(
|
|
29
|
+
$orderBy: [UserSortInput!]
|
|
30
|
+
$isAdmin: Boolean
|
|
31
|
+
$isBlocked: Boolean
|
|
32
|
+
$search: String
|
|
33
|
+
$limit: Int
|
|
34
|
+
$offset: Int
|
|
35
|
+
) {
|
|
36
|
+
users(
|
|
37
|
+
orderBy: $orderBy
|
|
38
|
+
isAdmin: $isAdmin
|
|
39
|
+
isBlocked: $isBlocked
|
|
40
|
+
search: $search
|
|
41
|
+
limit: $limit
|
|
42
|
+
offset: $offset
|
|
43
|
+
) {
|
|
44
|
+
users {
|
|
45
|
+
...userFields
|
|
46
|
+
}
|
|
47
|
+
totalCount
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
${USER_FRAGMENT}
|
|
51
|
+
`
|
|
52
|
+
|
|
53
|
+
export const SET_ADMIN_MUTATION = gql`
|
|
54
|
+
mutation SetAdmin($id: ID!, $admin: Boolean!) {
|
|
55
|
+
setAdmin(id: $id, admin: $admin) {
|
|
56
|
+
...userFields
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
${USER_FRAGMENT}
|
|
60
|
+
`
|
|
61
|
+
|
|
62
|
+
export const SET_BLOCKED_MUTATION = gql`
|
|
63
|
+
mutation SetBlocked($id: ID!, $blocked: Boolean!) {
|
|
64
|
+
setBlocked(id: $id, blocked: $blocked) {
|
|
65
|
+
...userFields
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
${USER_FRAGMENT}
|
|
69
|
+
`
|
|
70
|
+
|
|
71
|
+
// --- INTERFACES ---
|
|
72
|
+
export interface GetUsersQueryResult {
|
|
73
|
+
users?: {
|
|
74
|
+
users: User[]
|
|
75
|
+
totalCount: number
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface GetUsersQueryVariables {
|
|
80
|
+
orderBy?: { field: string; order: "ascending" | "descending" }[]
|
|
81
|
+
isAdmin?: boolean
|
|
82
|
+
isBlocked?: boolean
|
|
83
|
+
search?: string
|
|
84
|
+
limit?: number
|
|
85
|
+
offset?: number
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface UseUsersOptions {
|
|
89
|
+
orderBy?: { field: string | null; order: "ascending" | "descending" }
|
|
90
|
+
isAdmin?: boolean | null
|
|
91
|
+
isBlocked?: boolean | null
|
|
92
|
+
search?: string | undefined
|
|
93
|
+
initialLimit?: number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- useUsers HOOK ---
|
|
97
|
+
export const useUsers = (options: UseUsersOptions = {}) => {
|
|
98
|
+
const [currentLimit] = useState(options.initialLimit || 100)
|
|
99
|
+
const [offset, setOffset] = useState(0)
|
|
100
|
+
|
|
101
|
+
const filterSortSearchVariables = useRef({
|
|
102
|
+
orderBy: options.orderBy?.field
|
|
103
|
+
? [{ field: options.orderBy.field, order: options.orderBy.order }]
|
|
104
|
+
: undefined,
|
|
105
|
+
search: options.search,
|
|
106
|
+
isAdmin: typeof options.isAdmin === "boolean" ? options.isAdmin : undefined,
|
|
107
|
+
isBlocked: typeof options.isBlocked === "boolean"
|
|
108
|
+
? options.isBlocked
|
|
109
|
+
: undefined,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Effect to update the ref when options change and reset offset
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
filterSortSearchVariables.current = {
|
|
115
|
+
orderBy: options.orderBy?.field
|
|
116
|
+
? [{ field: options.orderBy.field, order: options.orderBy.order }]
|
|
117
|
+
: undefined,
|
|
118
|
+
search: options.search,
|
|
119
|
+
isAdmin: typeof options.isAdmin === "boolean"
|
|
120
|
+
? options.isAdmin
|
|
121
|
+
: undefined,
|
|
122
|
+
isBlocked: typeof options.isBlocked === "boolean"
|
|
123
|
+
? options.isBlocked
|
|
124
|
+
: undefined,
|
|
125
|
+
}
|
|
126
|
+
// When filters/sort/search change, reset the offset to 0 to start a new list
|
|
127
|
+
setOffset(0)
|
|
128
|
+
}, [options.orderBy, options.search, options.isAdmin, options.isBlocked])
|
|
129
|
+
|
|
130
|
+
const { data, loading, error, fetchMore, refetch } = useQuery<
|
|
131
|
+
GetUsersQueryResult
|
|
132
|
+
>(GET_USERS, {
|
|
133
|
+
variables: {
|
|
134
|
+
...filterSortSearchVariables.current,
|
|
135
|
+
limit: currentLimit,
|
|
136
|
+
offset: offset,
|
|
137
|
+
},
|
|
138
|
+
notifyOnNetworkStatusChange: true,
|
|
139
|
+
fetchPolicy: "cache-and-network",
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const [allUsers, setAllUsers] = useState<User[]>([])
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (data?.users?.users) {
|
|
146
|
+
if (offset === 0) {
|
|
147
|
+
setAllUsers(data.users.users)
|
|
148
|
+
} else {
|
|
149
|
+
setAllUsers((prevUsers) => {
|
|
150
|
+
const newUsers = data.users!.users
|
|
151
|
+
const uniqueNewUsers = newUsers.filter(
|
|
152
|
+
(newUser) =>
|
|
153
|
+
!prevUsers.some((existingUser) => existingUser.id === newUser.id),
|
|
154
|
+
)
|
|
155
|
+
return [...prevUsers, ...uniqueNewUsers]
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}, [data?.users?.users, offset])
|
|
160
|
+
|
|
161
|
+
const totalCount = data?.users?.totalCount || 0
|
|
162
|
+
const hasMore = totalCount > allUsers.length || loading
|
|
163
|
+
|
|
164
|
+
const loadMore = useCallback(() => {
|
|
165
|
+
if (hasMore && !loading) {
|
|
166
|
+
const newOffset = allUsers.length
|
|
167
|
+
fetchMore({
|
|
168
|
+
variables: {
|
|
169
|
+
...filterSortSearchVariables.current,
|
|
170
|
+
offset: newOffset,
|
|
171
|
+
limit: currentLimit,
|
|
172
|
+
},
|
|
173
|
+
updateQuery: (prev, { fetchMoreResult }) => {
|
|
174
|
+
if (!fetchMoreResult) return prev
|
|
175
|
+
|
|
176
|
+
const incomingUsers = fetchMoreResult.users.users
|
|
177
|
+
const currentUsers = prev.users?.users || []
|
|
178
|
+
|
|
179
|
+
const combinedUsers = currentUsers.concat(
|
|
180
|
+
incomingUsers.filter(
|
|
181
|
+
(newUser) =>
|
|
182
|
+
!currentUsers.some(
|
|
183
|
+
(existingUser) => existingUser.id === newUser.id,
|
|
184
|
+
),
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
users: {
|
|
190
|
+
...prev.users,
|
|
191
|
+
users: combinedUsers,
|
|
192
|
+
totalCount: fetchMoreResult.users.totalCount,
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
}, [
|
|
199
|
+
allUsers.length,
|
|
200
|
+
hasMore,
|
|
201
|
+
loading,
|
|
202
|
+
fetchMore,
|
|
203
|
+
currentLimit,
|
|
204
|
+
filterSortSearchVariables,
|
|
205
|
+
])
|
|
206
|
+
|
|
207
|
+
const refetchFullList = useCallback(
|
|
208
|
+
async (variablesToRefetch?: GetUsersQueryVariables) => {
|
|
209
|
+
setOffset(0)
|
|
210
|
+
await refetch({
|
|
211
|
+
...filterSortSearchVariables.current,
|
|
212
|
+
...variablesToRefetch,
|
|
213
|
+
offset: 0,
|
|
214
|
+
limit: currentLimit,
|
|
215
|
+
})
|
|
216
|
+
},
|
|
217
|
+
[refetch, filterSortSearchVariables, currentLimit],
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
const refetchCurrentPage = useCallback(async () => {
|
|
221
|
+
await refetch({
|
|
222
|
+
...filterSortSearchVariables.current,
|
|
223
|
+
offset: 0,
|
|
224
|
+
limit: allUsers.length > 0 ? allUsers.length : currentLimit,
|
|
225
|
+
})
|
|
226
|
+
}, [refetch, filterSortSearchVariables, allUsers.length, currentLimit])
|
|
227
|
+
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
if (error) {
|
|
230
|
+
Sentry.captureException(error)
|
|
231
|
+
}
|
|
232
|
+
}, [error])
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
users: allUsers,
|
|
236
|
+
loading,
|
|
237
|
+
error,
|
|
238
|
+
refetchFullList,
|
|
239
|
+
refetchCurrentPage,
|
|
240
|
+
loadMore,
|
|
241
|
+
hasMore,
|
|
242
|
+
totalCount,
|
|
243
|
+
currentLimit,
|
|
244
|
+
currentOffset: allUsers.length,
|
|
245
|
+
filterSortSearchVariables: filterSortSearchVariables.current,
|
|
246
|
+
}
|
|
247
|
+
}
|
package/src/scripts/routes.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { Navigate, Route, Routes } from "react-router-dom"
|
|
|
4
4
|
// TODO - Re-enable code splitting these when we can
|
|
5
5
|
import DatasetQuery from "./dataset/dataset-query"
|
|
6
6
|
//import PreRefactorDatasetProps from './dataset/dataset-pre-refactor-container'
|
|
7
|
-
|
|
7
|
+
import { isAdmin } from "./authentication/admin-user.jsx"
|
|
8
8
|
import FaqPage from "./pages/faq/faq"
|
|
9
9
|
import FrontPageContainer from "./pages/front-page/front-page"
|
|
10
10
|
import Admin from "./pages/admin/admin"
|
|
@@ -19,10 +19,8 @@ import { DatasetMetadata } from "./pages/metadata/dataset-metadata"
|
|
|
19
19
|
import { TermsPage } from "./pages/terms"
|
|
20
20
|
import { ImageAttribution } from "./pages/image-attribution"
|
|
21
21
|
import { UserQuery } from "./users/user-query"
|
|
22
|
-
import LoggedIn from "../scripts/authentication/logged-in"
|
|
23
|
-
import LoggedOut from "../scripts/authentication/logged-out"
|
|
24
|
-
import FourOThreePage from "./errors/403page"
|
|
25
22
|
import { OrcidLinkPage } from "./pages/orcid-link"
|
|
23
|
+
import FourOThreePage from "./errors/403page"
|
|
26
24
|
|
|
27
25
|
const AppRoutes: React.VoidFunctionComponent = () => (
|
|
28
26
|
<Routes>
|
|
@@ -31,7 +29,10 @@ const AppRoutes: React.VoidFunctionComponent = () => (
|
|
|
31
29
|
<Route path="/keygen" element={<APIKey />} />
|
|
32
30
|
<Route path="/datasets/:datasetId/*" element={<DatasetQuery />} />
|
|
33
31
|
<Route path="/search/*" element={<SearchRoutes />} />
|
|
34
|
-
<Route
|
|
32
|
+
<Route
|
|
33
|
+
path="/admin/*"
|
|
34
|
+
element={isAdmin() ? <Admin /> : <FourOThreePage />}
|
|
35
|
+
/>
|
|
35
36
|
<Route path="/error/*" element={<ErrorRoute />} />
|
|
36
37
|
<Route path="/pet" element={<PETRedirect />} />
|
|
37
38
|
<Route path="/cite" element={<Citation />} />
|
|
@@ -43,16 +44,7 @@ const AppRoutes: React.VoidFunctionComponent = () => (
|
|
|
43
44
|
<Route path="/orcid-link" element={<OrcidLinkPage />} />
|
|
44
45
|
<Route
|
|
45
46
|
path="/user/:orcid/*"
|
|
46
|
-
element={
|
|
47
|
-
<>
|
|
48
|
-
<LoggedIn>
|
|
49
|
-
<UserQuery />
|
|
50
|
-
</LoggedIn>
|
|
51
|
-
<LoggedOut>
|
|
52
|
-
<FourOThreePage />
|
|
53
|
-
</LoggedOut>
|
|
54
|
-
</>
|
|
55
|
-
}
|
|
47
|
+
element={<UserQuery />}
|
|
56
48
|
/>
|
|
57
49
|
<Route
|
|
58
50
|
path="/saved"
|
|
@@ -8,27 +8,26 @@ export interface User {
|
|
|
8
8
|
avatar?: string
|
|
9
9
|
orcid?: string
|
|
10
10
|
links?: string[]
|
|
11
|
+
admin?: boolean
|
|
12
|
+
blocked?: boolean
|
|
13
|
+
lastSeen?: string
|
|
14
|
+
created?: string
|
|
15
|
+
provider?: string
|
|
16
|
+
modified?: string
|
|
17
|
+
githubSynced?: Date
|
|
11
18
|
}
|
|
12
19
|
|
|
13
20
|
export interface UserRoutesProps {
|
|
14
|
-
|
|
21
|
+
orcidUser: User
|
|
15
22
|
hasEdit: boolean
|
|
16
23
|
isUser: boolean
|
|
17
24
|
}
|
|
18
25
|
export interface UserCardProps {
|
|
19
|
-
|
|
26
|
+
orcidUser: User
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
export interface UserAccountViewProps {
|
|
23
|
-
|
|
24
|
-
name: string
|
|
25
|
-
email: string
|
|
26
|
-
orcid?: string
|
|
27
|
-
links?: string[]
|
|
28
|
-
location?: string
|
|
29
|
-
institution?: string
|
|
30
|
-
github?: string
|
|
31
|
-
}
|
|
30
|
+
orcidUser: User
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
export interface Dataset {
|
|
@@ -63,12 +62,12 @@ export interface DatasetCardProps {
|
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
export interface UserDatasetsViewProps {
|
|
66
|
-
|
|
65
|
+
orcidUser: User
|
|
67
66
|
hasEdit: boolean
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
export interface AccountContainerProps {
|
|
71
|
-
|
|
70
|
+
orcidUser: User
|
|
72
71
|
hasEdit: boolean
|
|
73
72
|
isUser: boolean
|
|
74
73
|
}
|
|
@@ -1,73 +1,58 @@
|
|
|
1
|
-
// dependencies -------------------------------------------------------
|
|
2
|
-
|
|
3
1
|
import React from "react"
|
|
4
|
-
import PropTypes from "prop-types"
|
|
5
2
|
|
|
6
|
-
type
|
|
3
|
+
type UploadFileSelectProps = {
|
|
7
4
|
resume?: boolean
|
|
8
5
|
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
|
9
|
-
onChange
|
|
6
|
+
onChange: (e?: { files: File[] }) => void
|
|
10
7
|
disabled?: boolean
|
|
11
8
|
}
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const resumeIcon = (
|
|
18
|
-
<span>
|
|
19
|
-
<i className="fa fa-repeat" />
|
|
20
|
-
|
|
21
|
-
</span>
|
|
22
|
-
)
|
|
23
|
-
const icon = this.props.resume ? resumeIcon : null
|
|
24
|
-
const text = this.props.resume ? "Resume" : "Select folder"
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<div
|
|
28
|
-
className={"fileupload-btn" + (this.props.disabled ? " disabled" : "")}
|
|
29
|
-
>
|
|
30
|
-
<span>
|
|
31
|
-
{icon}
|
|
32
|
-
{text}
|
|
33
|
-
</span>
|
|
34
|
-
<input
|
|
35
|
-
type="file"
|
|
36
|
-
id="multifile-select"
|
|
37
|
-
className="multifile-select-btn"
|
|
38
|
-
onClick={this._click.bind(this)}
|
|
39
|
-
onChange={this._onFileSelect.bind(this)}
|
|
40
|
-
webkitdirectory="true"
|
|
41
|
-
directory="true"
|
|
42
|
-
disabled={this.props.disabled}
|
|
43
|
-
/>
|
|
44
|
-
</div>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// custom methods -----------------------------------------------------
|
|
49
|
-
|
|
50
|
-
_click(e): void {
|
|
10
|
+
export const UploadFileSelect: React.FC<UploadFileSelectProps> = (
|
|
11
|
+
{ resume, onClick, onChange, disabled },
|
|
12
|
+
) => {
|
|
13
|
+
const handleClick = (e: React.MouseEvent<HTMLInputElement>): void => {
|
|
51
14
|
e.stopPropagation()
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
15
|
+
// Reset the input value to allow selecting the same file/folder again
|
|
16
|
+
if (e.target instanceof HTMLInputElement) {
|
|
17
|
+
e.target.value = ""
|
|
55
18
|
}
|
|
19
|
+
if (onClick) onClick(e as React.MouseEvent<HTMLButtonElement>)
|
|
56
20
|
}
|
|
57
21
|
|
|
58
|
-
|
|
59
|
-
if (e.target && e.target.files.length > 0) {
|
|
60
|
-
const files = e.target.files
|
|
61
|
-
|
|
22
|
+
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
|
23
|
+
if (e.target && e.target.files && e.target.files.length > 0) {
|
|
24
|
+
const files = Array.from(e.target.files)
|
|
25
|
+
onChange({ files })
|
|
62
26
|
}
|
|
63
27
|
}
|
|
64
|
-
}
|
|
65
28
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
29
|
+
const resumeIcon = (
|
|
30
|
+
<span>
|
|
31
|
+
<i className="fa fa-repeat" />
|
|
32
|
+
|
|
33
|
+
</span>
|
|
34
|
+
)
|
|
35
|
+
const icon = resume ? resumeIcon : null
|
|
36
|
+
const text = resume ? "Resume" : "Select folder"
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={`fileupload-btn${disabled ? " disabled" : ""}`}>
|
|
40
|
+
<span>
|
|
41
|
+
{icon}
|
|
42
|
+
{text}
|
|
43
|
+
</span>
|
|
44
|
+
<input
|
|
45
|
+
type="file"
|
|
46
|
+
id="multifile-select"
|
|
47
|
+
className="multifile-select-btn"
|
|
48
|
+
onClick={handleClick}
|
|
49
|
+
onChange={handleFileSelect}
|
|
50
|
+
webkitdirectory="true"
|
|
51
|
+
directory="true"
|
|
52
|
+
disabled={disabled}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
71
56
|
}
|
|
72
57
|
|
|
73
|
-
export default
|
|
58
|
+
export default UploadFileSelect
|
|
@@ -6,7 +6,7 @@ import { useUser } from "../queries/user"
|
|
|
6
6
|
const UploadSelect = () => {
|
|
7
7
|
const { user, loading, error } = useUser()
|
|
8
8
|
const disabled = loading || Boolean(error) || !user || !user.email
|
|
9
|
-
const noEmail = !user
|
|
9
|
+
const noEmail = !(user?.email)
|
|
10
10
|
return (
|
|
11
11
|
<div>
|
|
12
12
|
<UploaderContext.Consumer>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { render, screen } from "@testing-library/react"
|
|
3
|
+
import DatasetCard from "../dataset-card"
|
|
4
|
+
|
|
5
|
+
const mockDataset = {
|
|
6
|
+
id: "ds000001",
|
|
7
|
+
name: "Test Dataset",
|
|
8
|
+
created: "2025-01-01T00:00:00Z",
|
|
9
|
+
public: true,
|
|
10
|
+
analytics: {
|
|
11
|
+
downloads: 12345,
|
|
12
|
+
views: 67890,
|
|
13
|
+
},
|
|
14
|
+
followers: [
|
|
15
|
+
{ userId: "user1", datasetId: "ds000001" },
|
|
16
|
+
{ userId: "user2", datasetId: "ds000001" },
|
|
17
|
+
],
|
|
18
|
+
stars: [
|
|
19
|
+
{ userId: "user1", datasetId: "ds000001" },
|
|
20
|
+
],
|
|
21
|
+
latestSnapshot: {
|
|
22
|
+
id: "ds000001:1.0.0",
|
|
23
|
+
size: 1024 ** 3,
|
|
24
|
+
issues: [{ severity: "low" }],
|
|
25
|
+
created: "2025-01-01T00:00:00Z",
|
|
26
|
+
description: {
|
|
27
|
+
Name: "Test Dataset Description",
|
|
28
|
+
Authors: ["John Doe"],
|
|
29
|
+
SeniorAuthor: "Dr. Smith",
|
|
30
|
+
DatasetType: "fMRI",
|
|
31
|
+
},
|
|
32
|
+
summary: {
|
|
33
|
+
modalities: ["fMRI"],
|
|
34
|
+
secondaryModalities: [],
|
|
35
|
+
sessions: 1,
|
|
36
|
+
subjects: 1,
|
|
37
|
+
subjectMetadata: [],
|
|
38
|
+
tasks: ["rest"],
|
|
39
|
+
size: 1024 ** 3,
|
|
40
|
+
totalFiles: 10,
|
|
41
|
+
dataProcessed: true,
|
|
42
|
+
pet: null,
|
|
43
|
+
},
|
|
44
|
+
validation: {
|
|
45
|
+
errors: [],
|
|
46
|
+
warnings: [],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
uploader: {
|
|
50
|
+
id: "uploaderId123",
|
|
51
|
+
name: "Uploader Name",
|
|
52
|
+
orcid: "1234-5678-9012-3456",
|
|
53
|
+
},
|
|
54
|
+
permissions: {
|
|
55
|
+
id: "somePermId",
|
|
56
|
+
userPermissions: [
|
|
57
|
+
{
|
|
58
|
+
userId: "someUserId",
|
|
59
|
+
level: "admin",
|
|
60
|
+
access: "admin",
|
|
61
|
+
user: {
|
|
62
|
+
id: "someUser",
|
|
63
|
+
name: "Some User",
|
|
64
|
+
email: "some@user.com",
|
|
65
|
+
provider: "github",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
metadata: { ages: [20] },
|
|
71
|
+
snapshots: [
|
|
72
|
+
{
|
|
73
|
+
id: "ds000001:1.0.0",
|
|
74
|
+
created: "2025-01-01T00:00:00Z",
|
|
75
|
+
tag: "1.0.0",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe("DatasetCard", () => {
|
|
81
|
+
it("should render dataset information correctly", () => {
|
|
82
|
+
render(<DatasetCard dataset={mockDataset} hasEdit={false} />)
|
|
83
|
+
expect(screen.getByText("Test Dataset")).toBeInTheDocument()
|
|
84
|
+
expect(screen.getByText("ds000001")).toBeInTheDocument()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("should hide the dataset if not public and hasEdit is false", () => {
|
|
88
|
+
const privateDataset = { ...mockDataset, public: false }
|
|
89
|
+
const { container } = render(
|
|
90
|
+
<DatasetCard dataset={privateDataset} hasEdit={false} />,
|
|
91
|
+
)
|
|
92
|
+
expect(container).toBeEmptyDOMElement()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("should show the dataset if not public but hasEdit is true", () => {
|
|
96
|
+
const privateDataset = { ...mockDataset, public: false }
|
|
97
|
+
render(<DatasetCard dataset={privateDataset} hasEdit={true} />)
|
|
98
|
+
expect(screen.getByText("Test Dataset")).toBeInTheDocument()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("should render activity details correctly", () => {
|
|
102
|
+
render(<DatasetCard dataset={mockDataset} hasEdit={false} />)
|
|
103
|
+
expect(screen.getByRole("img", { name: /activity/i })).toBeInTheDocument()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it("should render public icon if dataset is public", () => {
|
|
107
|
+
render(<DatasetCard dataset={mockDataset} hasEdit={false} />)
|
|
108
|
+
expect(screen.getByLabelText("Public")).toBeInTheDocument()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it("should not render public icon if dataset is not public", () => {
|
|
112
|
+
const privateDataset = { ...mockDataset, public: false }
|
|
113
|
+
render(<DatasetCard dataset={privateDataset} hasEdit={true} />)
|
|
114
|
+
expect(screen.queryByLabelText("Public")).not.toBeInTheDocument()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it("should display 'Unknown size' if latestSnapshot or size is missing", () => {
|
|
118
|
+
const datasetWithoutSize = {
|
|
119
|
+
...mockDataset,
|
|
120
|
+
latestSnapshot: { ...mockDataset.latestSnapshot, size: undefined },
|
|
121
|
+
}
|
|
122
|
+
render(<DatasetCard dataset={datasetWithoutSize} hasEdit={false} />)
|
|
123
|
+
expect(screen.getByText("Dataset Size:")).toHaveTextContent(
|
|
124
|
+
"Dataset Size: Unknown size",
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
})
|