@openneuro/app 4.35.0 → 4.36.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/package.json +3 -3
  2. package/src/client.jsx +1 -0
  3. package/src/scripts/components/button/button.scss +14 -0
  4. package/src/scripts/components/search-page/SearchResultItem.tsx +3 -1
  5. package/src/scripts/components/search-page/search-page.scss +1 -13
  6. package/src/scripts/config.ts +6 -0
  7. package/src/scripts/dataset/files/__tests__/__snapshots__/file.spec.jsx.snap +2 -2
  8. package/src/scripts/dataset/files/file.tsx +2 -2
  9. package/src/scripts/dataset/mutations/__tests__/update-file.spec.tsx +126 -0
  10. package/src/scripts/dataset/mutations/update-file.jsx +20 -5
  11. package/src/scripts/dataset/routes/snapshot.tsx +1 -1
  12. package/src/scripts/errors/errorRoute.tsx +2 -0
  13. package/src/scripts/queries/user.ts +120 -3
  14. package/src/scripts/types/user-types.ts +11 -13
  15. package/src/scripts/uploader/file-select.tsx +42 -57
  16. package/src/scripts/uploader/upload-select.jsx +1 -1
  17. package/src/scripts/users/__tests__/dataset-card.spec.tsx +127 -0
  18. package/src/scripts/users/__tests__/user-account-view.spec.tsx +150 -67
  19. package/src/scripts/users/__tests__/user-card.spec.tsx +6 -17
  20. package/src/scripts/users/__tests__/user-query.spec.tsx +133 -38
  21. package/src/scripts/users/__tests__/user-routes.spec.tsx +156 -27
  22. package/src/scripts/users/__tests__/user-tabs.spec.tsx +7 -7
  23. package/src/scripts/users/components/edit-list.tsx +26 -5
  24. package/src/scripts/users/components/edit-string.tsx +40 -13
  25. package/src/scripts/users/components/editable-content.tsx +10 -3
  26. package/src/scripts/users/components/user-dataset-filters.tsx +205 -121
  27. package/src/scripts/users/dataset-card.tsx +3 -2
  28. package/src/scripts/users/github-auth-button.tsx +98 -0
  29. package/src/scripts/users/scss/datasetcard.module.scss +65 -12
  30. package/src/scripts/users/scss/useraccountview.module.scss +1 -1
  31. package/src/scripts/users/user-account-view.tsx +43 -34
  32. package/src/scripts/users/user-card.tsx +12 -17
  33. package/src/scripts/users/user-container.tsx +9 -5
  34. package/src/scripts/users/user-datasets-view.tsx +350 -40
  35. package/src/scripts/users/user-menu.tsx +4 -9
  36. package/src/scripts/users/user-notifications-view.tsx +9 -7
  37. package/src/scripts/users/user-query.tsx +3 -3
  38. package/src/scripts/users/user-routes.tsx +11 -5
  39. package/src/scripts/users/user-tabs.tsx +4 -2
  40. package/src/scripts/users/__tests__/datasest-card.spec.tsx +0 -201
  41. package/src/scripts/users/fragments/query.js +0 -42
@@ -2,16 +2,20 @@ import React, { useState } from "react"
2
2
  import * as Sentry from "@sentry/react"
3
3
  import { useMutation } from "@apollo/client"
4
4
  import { EditableContent } from "./components/editable-content"
5
- import { GET_USER, UPDATE_USER, useUser } from "../queries/user"
5
+ import { GET_USER, UPDATE_USER } from "../queries/user"
6
6
  import styles from "./scss/useraccountview.module.scss"
7
+ import { GitHubAuthButton } from "./github-auth-button"
8
+ import type { UserAccountViewProps } from "../types/user-types"
7
9
 
8
- export const UserAccountView: React.FC = () => {
9
- const { user, loading, error } = useUser()
10
-
11
- const [userLinks, setLinks] = useState<string[]>(user?.links || [])
12
- const [userLocation, setLocation] = useState<string>(user?.location || "")
10
+ export const UserAccountView: React.FC<UserAccountViewProps> = ({
11
+ orcidUser,
12
+ }) => {
13
+ const [userLinks, setLinks] = useState<string[]>(orcidUser?.links || [])
14
+ const [userLocation, setLocation] = useState<string>(
15
+ orcidUser?.location || "",
16
+ )
13
17
  const [userInstitution, setInstitution] = useState<string>(
14
- user?.institution || "",
18
+ orcidUser?.institution || "",
15
19
  )
16
20
  const [updateUser] = useMutation(UPDATE_USER)
17
21
 
@@ -20,13 +24,13 @@ export const UserAccountView: React.FC = () => {
20
24
  try {
21
25
  await updateUser({
22
26
  variables: {
23
- id: user?.orcid,
27
+ id: orcidUser?.orcid,
24
28
  links: newLinks,
25
29
  },
26
30
  refetchQueries: [
27
31
  {
28
32
  query: GET_USER,
29
- variables: { id: user?.orcid },
33
+ variables: { id: orcidUser?.orcid },
30
34
  },
31
35
  ],
32
36
  })
@@ -41,13 +45,13 @@ export const UserAccountView: React.FC = () => {
41
45
  try {
42
46
  await updateUser({
43
47
  variables: {
44
- id: user?.orcid,
48
+ id: orcidUser?.orcid,
45
49
  location: newLocation,
46
50
  },
47
51
  refetchQueries: [
48
52
  {
49
53
  query: GET_USER,
50
- variables: { id: user?.orcid },
54
+ variables: { id: orcidUser?.orcid },
51
55
  },
52
56
  ],
53
57
  })
@@ -62,13 +66,13 @@ export const UserAccountView: React.FC = () => {
62
66
  try {
63
67
  await updateUser({
64
68
  variables: {
65
- id: user?.orcid,
69
+ id: orcidUser?.orcid,
66
70
  institution: newInstitution,
67
71
  },
68
72
  refetchQueries: [
69
73
  {
70
74
  query: GET_USER,
71
- variables: { id: user?.orcid },
75
+ variables: { id: orcidUser?.orcid },
72
76
  },
73
77
  ],
74
78
  })
@@ -77,16 +81,20 @@ export const UserAccountView: React.FC = () => {
77
81
  }
78
82
  }
79
83
 
80
- if (loading) {
81
- return <div>Loading Account Information...</div>
82
- }
83
-
84
- if (error) {
85
- return <div>Error loading account information. Please try again.</div>
86
- }
87
-
88
- if (!user) {
89
- return <div>Could not load account information.</div>
84
+ // --- URL VALIDATION FUNCTION ---
85
+ const validateHttpHttpsUrl = (url: string): boolean => {
86
+ if (!url) {
87
+ return false // Empty string is not a valid URL
88
+ }
89
+ try {
90
+ const parsedUrl = new URL(url)
91
+ // Check if the protocol is either http: or https:
92
+ return parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:"
93
+ } catch (error) {
94
+ // If new URL() throws an error, the string is not a valid URL
95
+ Sentry.captureException(error)
96
+ return false
97
+ }
90
98
  }
91
99
 
92
100
  return (
@@ -95,24 +103,26 @@ export const UserAccountView: React.FC = () => {
95
103
  <ul className={styles.accountDetail}>
96
104
  <li>
97
105
  <span>Name:</span>
98
- {user.name}
106
+ {orcidUser.name}
99
107
  </li>
100
108
  <li>
101
109
  <span>Email:</span>
102
- {user.email}
110
+ {orcidUser.email}
103
111
  </li>
104
112
  <li>
105
113
  <span>ORCID:</span>
106
- {user.orcid}
114
+ {orcidUser.orcid}
107
115
  </li>
108
- {user.github
109
- ? (
116
+ {orcidUser?.github &&
117
+ (
110
118
  <li>
111
119
  <span>GitHub:</span>
112
- {user.github}
120
+ {orcidUser.github}
113
121
  </li>
114
- )
115
- : <li>Connect your GitHub</li>}
122
+ )}
123
+ <li>
124
+ <GitHubAuthButton sync={orcidUser.githubSynced} />
125
+ </li>
116
126
  </ul>
117
127
 
118
128
  <EditableContent
@@ -120,9 +130,8 @@ export const UserAccountView: React.FC = () => {
120
130
  setRows={handleLinksChange}
121
131
  className="custom-class"
122
132
  heading="Links"
123
- // eslint-disable-next-line no-useless-escape
124
- validation={/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/} // URL validation regex
125
- validationMessage="Invalid URL format. Please use a valid link."
133
+ validation={validateHttpHttpsUrl}
134
+ validationMessage="Invalid URL format. Please start with http:// or https://"
126
135
  data-testid="links-section"
127
136
  />
128
137
 
@@ -2,8 +2,9 @@ import React from "react"
2
2
  import styles from "./scss/usercard.module.scss"
3
3
  import type { UserCardProps } from "../types/user-types"
4
4
 
5
- export const UserCard: React.FC<UserCardProps> = ({ user }) => {
6
- const { location, institution, email, orcid, links = [], github, name } = user
5
+ export const UserCard: React.FC<UserCardProps> = ({ orcidUser }) => {
6
+ const { location, institution, email, orcid, links = [], github, name } =
7
+ orcidUser
7
8
 
8
9
  return (
9
10
  <div className={styles.userCard}>
@@ -22,11 +23,7 @@ export const UserCard: React.FC<UserCardProps> = ({ user }) => {
22
23
  )}
23
24
  <li>
24
25
  <i className="fas fa-envelope"></i>
25
- <a
26
- href={"mailto:" + email}
27
- target="_blank"
28
- rel="noopener noreferrer"
29
- >
26
+ <a href={"mailto:" + email} target="_blank" rel="noopener noreferrer">
30
27
  {email}
31
28
  </a>
32
29
  </li>
@@ -57,16 +54,14 @@ export const UserCard: React.FC<UserCardProps> = ({ user }) => {
57
54
  </li>
58
55
  )}
59
56
  {links.length > 0 &&
60
- links
61
- .filter(Boolean)
62
- .map((link, index) => (
63
- <li key={index}>
64
- <i className="fa fa-link"></i>
65
- <a href={link} target="_blank" rel="noopener noreferrer">
66
- {link}
67
- </a>
68
- </li>
69
- ))}
57
+ links.filter(Boolean).map((link, index) => (
58
+ <li key={index}>
59
+ <i className="fa fa-link"></i>
60
+ <a href={link} target="_blank" rel="noopener noreferrer">
61
+ {link}
62
+ </a>
63
+ </li>
64
+ ))}
70
65
  </ul>
71
66
  </div>
72
67
  )
@@ -6,7 +6,7 @@ import styles from "./scss/usercontainer.module.scss"
6
6
  import type { AccountContainerProps } from "../types/user-types"
7
7
 
8
8
  export const UserAccountContainer: React.FC<AccountContainerProps> = ({
9
- user,
9
+ orcidUser,
10
10
  hasEdit,
11
11
  isUser,
12
12
  }) => {
@@ -14,15 +14,19 @@ export const UserAccountContainer: React.FC<AccountContainerProps> = ({
14
14
  <>
15
15
  <div className="container">
16
16
  <header className={styles.userHeader}>
17
- {user.avatar && (
18
- <img className={styles.avatar} src={user.avatar} alt={user.name} />
17
+ {orcidUser.avatar && (
18
+ <img
19
+ className={styles.avatar}
20
+ src={orcidUser.avatar}
21
+ alt={orcidUser.name}
22
+ />
19
23
  )}
20
- <h2 className={styles.username}>{user.name}</h2>
24
+ <h2 className={styles.username}>{orcidUser.name}</h2>
21
25
  </header>
22
26
  </div>
23
27
  <div className={styles.usercontainer + " container"}>
24
28
  <section className={styles.userSidebar}>
25
- <UserCard user={user} />
29
+ <UserCard orcidUser={orcidUser} />
26
30
  <UserAccountTabs hasEdit={hasEdit} isUser={isUser} />
27
31
  </section>
28
32
  <section className={styles.userViews}>
@@ -1,68 +1,378 @@
1
- import React, { useState } from "react"
1
+ import React, { useCallback, useEffect, useState } from "react"
2
+ import { useQuery } from "@apollo/client"
3
+ import * as Sentry from "@sentry/react"
2
4
  import { DatasetCard } from "./dataset-card"
3
5
  import { UserDatasetFilters } from "./components/user-dataset-filters"
4
- import { gql, useQuery } from "@apollo/client"
5
- import styles from "./scss/datasetcard.module.scss"
6
+ import { ADVANCED_SEARCH_DATASETS_QUERY } from "../queries/user"
7
+ import { Button } from "../components/button/Button"
8
+ import { Loading } from "../components/loading/Loading"
6
9
  import type { Dataset, UserDatasetsViewProps } from "../types/user-types"
7
- import { INDEX_DATASET_FRAGMENT } from "./fragments/query"
8
- import { filterAndSortDatasets } from "../utils/user-datasets"
9
-
10
- export const DATASETS_QUERY = gql`
11
- query Datasets($first: Int) {
12
- datasets(first: $first) {
13
- edges {
14
- node {
15
- ...DatasetIndex
10
+ import styles from "./scss/datasetcard.module.scss"
11
+
12
+ type SortByType = {
13
+ [key: string]: "asc" | "desc"
14
+ } | null
15
+
16
+ interface ElasticsearchQuery {
17
+ bool: {
18
+ filter: (
19
+ | { terms: { "permissions.userPermissions.user.id": string[] } }
20
+ | { term: { public: boolean | null } }
21
+ )[]
22
+ must: (
23
+ | {
24
+ bool: {
25
+ should: (
26
+ | {
27
+ multi_match: {
28
+ query: string
29
+ fields: string[]
30
+ fuzziness: number
31
+ }
32
+ }
33
+ | { prefix: { name: string } }
34
+ | { prefix: { "description.Name": string } }
35
+ )[]
36
+ minimum_should_match: number
16
37
  }
17
38
  }
18
- }
39
+ | { match_all: Record<string, never> }
40
+ )[]
19
41
  }
20
- ${INDEX_DATASET_FRAGMENT}
21
- `
42
+ }
22
43
 
23
- export const UserDatasetsView: React.FC<UserDatasetsViewProps> = (
24
- { user, hasEdit },
25
- ) => {
44
+ export const UserDatasetsView: React.FC<UserDatasetsViewProps> = ({
45
+ orcidUser,
46
+ hasEdit,
47
+ }) => {
26
48
  const [searchQuery, setSearchQuery] = useState("")
27
49
  const [publicFilter, setPublicFilter] = useState<string>("all")
28
50
  const [sortOrder, setSortOrder] = useState<string>("date-updated")
51
+ const [visibleDatasets, setVisibleDatasets] = useState<Dataset[]>([])
52
+ const [loadMoreLoading, setLoadMoreLoading] = useState(false)
53
+ const [cursor, setCursor] = useState<string | null>(null)
54
+ const [hasNextPage, setHasNextPage] = useState(false)
55
+ const [sortBy, setSortBy] = useState<SortByType>(null)
56
+ const loadAmount = 26
57
+
58
+ const generateElasticsearchQuery = useCallback(
59
+ (
60
+ currentSearchQuery: string,
61
+ currentPublicFilter: string,
62
+ ): ElasticsearchQuery => {
63
+ const baseQuery: ElasticsearchQuery = {
64
+ bool: {
65
+ filter: [],
66
+ must: [],
67
+ },
68
+ }
69
+
70
+ if (orcidUser?.id) {
71
+ baseQuery.bool.filter.push({
72
+ terms: {
73
+ "permissions.userPermissions.user.id": [orcidUser.id],
74
+ },
75
+ })
76
+ }
77
+
78
+ if (hasEdit) {
79
+ if (currentPublicFilter === "public") {
80
+ baseQuery.bool.filter.push({ term: { public: true } })
81
+ }
82
+ } else {
83
+ baseQuery.bool.filter.push({ term: { public: true } })
84
+ }
85
+
86
+ if (currentSearchQuery) {
87
+ baseQuery.bool.must.push({
88
+ bool: {
89
+ should: [
90
+ {
91
+ multi_match: {
92
+ query: currentSearchQuery,
93
+ fields: [
94
+ "id^3",
95
+ "name^3",
96
+ "description.Name^3",
97
+ "description.Authors^3",
98
+ "latestSnapshot.description.Name",
99
+ "latestSnapshot.description.Authors",
100
+ "latestSnapshot.readme",
101
+ ],
102
+ fuzziness: 1,
103
+ },
104
+ },
105
+ {
106
+ prefix: {
107
+ name: currentSearchQuery.toLowerCase(),
108
+ },
109
+ },
110
+ {
111
+ prefix: {
112
+ "description.Name": currentSearchQuery.toLowerCase(),
113
+ },
114
+ },
115
+ ],
116
+ minimum_should_match: 1,
117
+ },
118
+ })
119
+ } else {
120
+ baseQuery.bool.must.push({ match_all: {} })
121
+ }
122
+
123
+ return baseQuery
124
+ },
125
+ [orcidUser?.id, hasEdit],
126
+ )
127
+
128
+ const [elasticsearchQuery, setElasticsearchQuery] = useState(() =>
129
+ generateElasticsearchQuery("", "all")
130
+ )
131
+
132
+ const { data, loading, error, fetchMore, refetch } = useQuery(
133
+ ADVANCED_SEARCH_DATASETS_QUERY,
134
+ {
135
+ variables: {
136
+ first: loadAmount,
137
+ query: elasticsearchQuery,
138
+ sortBy: sortBy,
139
+ cursor: null,
140
+ allDatasets: true,
141
+ datasetStatus: undefined,
142
+ },
143
+ onCompleted: (initialData) => {
144
+ const initialEdges = initialData?.datasets?.edges || []
145
+ const initialDatasets = initialEdges.map((edge) => edge.node)
146
+ setVisibleDatasets(initialDatasets)
147
+ setCursor(initialData?.datasets?.pageInfo?.endCursor || null)
148
+ setHasNextPage(initialData?.datasets?.pageInfo?.hasNextPage || false)
149
+ },
150
+ onError: (err) => {
151
+ Sentry.captureException(err)
152
+ },
153
+ errorPolicy: "ignore",
154
+ fetchPolicy: "cache-and-network",
155
+ nextFetchPolicy: "cache-first",
156
+ notifyOnNetworkStatusChange: true,
157
+ },
158
+ )
159
+
160
+ const handleSearch = useCallback(
161
+ (newSearchQuery: string, currentPublicFilter: string) => {
162
+ setSearchQuery(newSearchQuery)
163
+ setPublicFilter(currentPublicFilter)
164
+ const newElasticsearchQuery = generateElasticsearchQuery(
165
+ newSearchQuery,
166
+ currentPublicFilter,
167
+ )
168
+ setElasticsearchQuery(newElasticsearchQuery)
169
+ refetch({
170
+ query: newElasticsearchQuery,
171
+ cursor: null,
172
+ sortBy: sortBy,
173
+ datasetStatus: undefined,
174
+ })
175
+ },
176
+ [
177
+ generateElasticsearchQuery,
178
+ refetch,
179
+ setSearchQuery,
180
+ setPublicFilter,
181
+ setElasticsearchQuery,
182
+ sortBy,
183
+ hasEdit,
184
+ orcidUser?.id,
185
+ ],
186
+ )
187
+
188
+ const handlePublicFilterChange = useCallback(
189
+ (newPublicFilter) => {
190
+ setPublicFilter(newPublicFilter)
191
+ const newElasticsearchQuery = generateElasticsearchQuery(
192
+ searchQuery,
193
+ newPublicFilter,
194
+ )
195
+ setElasticsearchQuery(newElasticsearchQuery)
196
+ refetch({
197
+ query: newElasticsearchQuery,
198
+ cursor: null,
199
+ sortBy: sortBy,
200
+ datasetStatus: undefined,
201
+ })
202
+ },
203
+ [
204
+ setPublicFilter,
205
+ generateElasticsearchQuery,
206
+ refetch,
207
+ searchQuery,
208
+ sortBy,
209
+ hasEdit,
210
+ orcidUser?.id,
211
+ ],
212
+ )
213
+
214
+ const handleSortOrderChange = useCallback(
215
+ (newSortOrder) => {
216
+ setSortOrder(newSortOrder)
217
+ let newSortBy = null
218
+ switch (newSortOrder) {
219
+ case "name-asc":
220
+ newSortBy = { "metadata.datasetName": "asc" }
221
+ break
222
+ case "name-desc":
223
+ newSortBy = { "metadata.datasetName": "desc" }
224
+ break
225
+ case "date-newest":
226
+ newSortBy = { created: "desc" }
227
+ break
228
+ case "date-oldest":
229
+ newSortBy = { created: "asc" }
230
+ break
231
+ case "date-updated":
232
+ newSortBy = { "metadata.latestSnapshotCreatedAt": "desc" }
233
+ break
234
+ default:
235
+ newSortBy = null
236
+ }
237
+ setSortBy(newSortBy)
238
+ refetch({
239
+ sortBy: newSortBy,
240
+ first: loadAmount,
241
+ cursor: null,
242
+ query: elasticsearchQuery,
243
+ datasetStatus: undefined,
244
+ })
245
+ },
246
+ [setSortOrder, refetch, setSortBy, elasticsearchQuery, hasEdit],
247
+ )
248
+
249
+ const handleLoadMore = useCallback(() => {
250
+ if (!hasNextPage || loadMoreLoading || !cursor) {
251
+ return
252
+ }
29
253
 
30
- const { data, loading, error } = useQuery(DATASETS_QUERY, {
31
- variables: { first: 25 },
32
- })
254
+ setLoadMoreLoading(true)
255
+ fetchMore({
256
+ variables: {
257
+ first: loadAmount,
258
+ cursor: cursor,
259
+ query: elasticsearchQuery,
260
+ sortBy: sortBy,
261
+ allDatasets: true,
262
+ datasetStatus: undefined,
263
+ },
264
+ updateQuery: (previousResult, { fetchMoreResult }) => {
265
+ if (!fetchMoreResult?.datasets?.edges) {
266
+ return previousResult
267
+ }
268
+
269
+ const newEdges = fetchMoreResult.datasets.edges
270
+ const newCursor = fetchMoreResult.datasets.pageInfo.endCursor
271
+ const newHasNextPage = fetchMoreResult.datasets.pageInfo.hasNextPage
272
+
273
+ setCursor(newCursor)
274
+ setHasNextPage(newHasNextPage)
275
+
276
+ return {
277
+ datasets: {
278
+ __typename: previousResult.datasets.__typename,
279
+ edges: [...previousResult.datasets.edges, ...newEdges],
280
+ pageInfo: {
281
+ ...previousResult.datasets.pageInfo,
282
+ endCursor: newCursor,
283
+ hasNextPage: newHasNextPage,
284
+ },
285
+ },
286
+ }
287
+ },
288
+ }).finally(() => {
289
+ setLoadMoreLoading(false)
290
+ })
291
+ }, [
292
+ fetchMore,
293
+ hasNextPage,
294
+ loadMoreLoading,
295
+ cursor,
296
+ elasticsearchQuery,
297
+ sortBy,
298
+ hasEdit,
299
+ ])
33
300
 
34
- if (loading) return <p>Loading datasets...</p>
35
- if (error) return <p>Failed to fetch datasets: {error.message}</p>
301
+ useEffect(() => {
302
+ if (data?.datasets?.edges) {
303
+ const fetched = data.datasets.edges.map((edge) => edge.node)
304
+ setVisibleDatasets(fetched)
305
+ setHasNextPage(data?.datasets?.pageInfo?.hasNextPage || false)
306
+ }
307
+ }, [data, hasEdit, loadAmount])
36
308
 
37
- const datasets: Dataset[] =
38
- data?.datasets?.edges?.map((edge: { node: Dataset }) => edge.node) || []
309
+ useEffect(() => {
310
+ const newElasticsearchQuery = generateElasticsearchQuery(
311
+ searchQuery,
312
+ publicFilter,
313
+ )
314
+ setElasticsearchQuery(newElasticsearchQuery)
315
+ }, [searchQuery, publicFilter, generateElasticsearchQuery])
39
316
 
40
- const filteredAndSortedDatasets = filterAndSortDatasets(datasets, {
41
- searchQuery,
42
- publicFilter,
43
- sortOrder,
44
- })
317
+ useEffect(() => {
318
+ if (elasticsearchQuery) {
319
+ refetch({
320
+ query: elasticsearchQuery,
321
+ cursor: null,
322
+ sortBy: sortBy,
323
+ datasetStatus: undefined,
324
+ })
325
+ }
326
+ }, [elasticsearchQuery, refetch, sortBy, hasEdit])
45
327
 
328
+ if (loading) return <Loading />
329
+ if (error) {
330
+ return (
331
+ <p>
332
+ Failed to load datasets. Please contact an administrator if the error
333
+ persists.
334
+ </p>
335
+ )
336
+ }
337
+
338
+ const visibleToRender = visibleDatasets
46
339
  return (
47
- <div data-testid="user-datasets-view">
48
- <h3>{user.name}'s Datasets</h3>
340
+ <div
341
+ className={styles.userDatasetsWrapper}
342
+ data-testid="user-datasets-view"
343
+ >
344
+ <h3>{orcidUser.name}'s Datasets</h3>
49
345
 
50
346
  <UserDatasetFilters
51
347
  publicFilter={publicFilter}
52
- setPublicFilter={setPublicFilter}
348
+ setPublicFilter={handlePublicFilterChange}
53
349
  sortOrder={sortOrder}
54
- setSortOrder={setSortOrder}
55
- searchQuery={searchQuery}
56
- setSearchQuery={setSearchQuery}
350
+ setSortOrder={handleSortOrderChange}
351
+ onSearch={handleSearch}
352
+ currentSearchTerm={searchQuery}
353
+ hasEdit={hasEdit}
57
354
  />
58
355
 
59
356
  <div className={styles.userDsWrap}>
60
- {filteredAndSortedDatasets.length > 0
61
- ? filteredAndSortedDatasets.map((dataset) => (
62
- <DatasetCard key={dataset.id} dataset={dataset} hasEdit={hasEdit} />
63
- ))
357
+ {visibleToRender.length > 0
358
+ ? (
359
+ visibleToRender.map((dataset) => (
360
+ <DatasetCard
361
+ key={dataset.id}
362
+ dataset={dataset}
363
+ hasEdit={hasEdit}
364
+ />
365
+ ))
366
+ )
64
367
  : <p>No datasets found.</p>}
65
368
  </div>
369
+
370
+ {visibleToRender.length >= loadAmount && !loadMoreLoading &&
371
+ hasNextPage && (
372
+ <div className="load-more">
373
+ <Button label="Load More" onClick={handleLoadMore} />
374
+ </div>
375
+ )}
66
376
  </div>
67
377
  )
68
378
  }
@@ -64,23 +64,18 @@ export const UserMenu = (
64
64
  </p>
65
65
  </li>
66
66
  <li>
67
- {
68
- /* {user?.orcid
67
+ {user?.orcid
69
68
  ? <Link to={`/user/${user?.orcid}`}>My Datasets</Link>
70
- : <Link to="/search?mydatasets">My Datasets</Link>} */
71
- }
72
- <Link to="/search?mydatasets">My Datasets</Link>
69
+ : <Link to="/search?mydatasets">My Datasets</Link>}
73
70
  </li>
74
71
 
75
- {
76
- /* {user?.orcid && (
72
+ {user?.orcid && (
77
73
  <li>
78
74
  <Link to={`/user/${user?.orcid}/account`}>
79
75
  Account Info
80
76
  </Link>
81
77
  </li>
82
- )} */
83
- }
78
+ )}
84
79
 
85
80
  <li className="user-menu-link">
86
81
  <Link to="/keygen">Obtain an API Key</Link>