@openneuro/app 4.30.2 → 4.31.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 (58) hide show
  1. package/package.json +5 -5
  2. package/src/assets/activity-icon.png +0 -0
  3. package/src/assets/icon-archived.png +0 -0
  4. package/src/assets/icon-saved.png +0 -0
  5. package/src/assets/icon-unread.png +0 -0
  6. package/src/client.jsx +1 -1
  7. package/src/scripts/datalad/dataset/dataset-query-fragments.js +4 -14
  8. package/src/scripts/dataset/__tests__/__snapshots__/snapshot-container.spec.tsx.snap +4 -304
  9. package/src/scripts/dataset/components/ValidationBlock.tsx +13 -15
  10. package/src/scripts/dataset/components/__tests__/ValidationBlock.spec.tsx +2 -0
  11. package/src/scripts/dataset/draft-container.tsx +2 -1
  12. package/src/scripts/dataset/files/__tests__/__snapshots__/file-tree.spec.jsx.snap +1 -9
  13. package/src/scripts/dataset/fragments/__tests__/{dataset-alert-draft.spec.tsx → dataset-alert.spec.tsx} +33 -1
  14. package/src/scripts/dataset/fragments/{dataset-alert-draft.tsx → dataset-alert.tsx} +30 -18
  15. package/src/scripts/dataset/routes/delete-page.tsx +72 -39
  16. package/src/scripts/dataset/routes/snapshot.tsx +23 -17
  17. package/src/scripts/dataset/routes/tab-routes-draft.tsx +5 -2
  18. package/src/scripts/dataset/snapshot-container.tsx +11 -0
  19. package/src/scripts/search/__tests__/search-params-ctx.spec.tsx +3 -0
  20. package/src/scripts/search/initial-search-params.tsx +2 -0
  21. package/src/scripts/search/inputs/__tests__/nihselect.spec.tsx +36 -0
  22. package/src/scripts/search/inputs/index.ts +2 -0
  23. package/src/scripts/search/inputs/nih-select.tsx +63 -0
  24. package/src/scripts/search/search-container.tsx +20 -12
  25. package/src/scripts/search/search-params-ctx.tsx +2 -0
  26. package/src/scripts/search/search-routes.tsx +14 -6
  27. package/src/scripts/search/use-search-results.tsx +15 -0
  28. package/src/scripts/types/user-types.ts +72 -0
  29. package/src/scripts/uploader/upload-issues.tsx +2 -2
  30. package/src/scripts/users/__tests__/datasest-card.spec.tsx +201 -0
  31. package/src/scripts/users/__tests__/user-card.spec.tsx +30 -3
  32. package/src/scripts/users/__tests__/user-query.spec.tsx +6 -0
  33. package/src/scripts/users/__tests__/user-routes.spec.tsx +42 -18
  34. package/src/scripts/users/components/user-dataset-filters.tsx +157 -0
  35. package/src/scripts/users/dataset-card.tsx +121 -0
  36. package/src/scripts/users/fragments/query.js +42 -0
  37. package/src/scripts/users/scss/datasetcard.module.scss +153 -0
  38. package/src/scripts/users/scss/usernotifications.module.scss +159 -0
  39. package/src/scripts/users/user-account-view.tsx +1 -12
  40. package/src/scripts/users/user-card.tsx +1 -14
  41. package/src/scripts/users/user-container.tsx +1 -17
  42. package/src/scripts/users/user-datasets-view.tsx +58 -43
  43. package/src/scripts/users/user-notification-accordion.tsx +160 -0
  44. package/src/scripts/users/user-notification-list.tsx +27 -0
  45. package/src/scripts/users/user-notifications-tab-content.tsx +85 -0
  46. package/src/scripts/users/user-notifications-view.tsx +102 -4
  47. package/src/scripts/users/user-query.tsx +6 -14
  48. package/src/scripts/users/user-routes.tsx +18 -19
  49. package/src/scripts/utils/__tests__/user-datasets.spec.tsx +86 -0
  50. package/src/scripts/utils/gtag.js +3 -2
  51. package/src/scripts/utils/user-datasets.tsx +60 -0
  52. package/src/scripts/validation/__tests__/__snapshots__/validation-issues.spec.tsx.snap +1 -122
  53. package/src/scripts/validation/validation-results-query.ts +44 -0
  54. package/src/scripts/validation/validation-results.tsx +31 -7
  55. package/src/scripts/validation/validation.tsx +58 -49
  56. package/src/scripts/workers/schema.worker.ts +2 -7
  57. package/tsconfig.json +1 -2
  58. package/vite.config.js +1 -0
@@ -1,7 +1,7 @@
1
1
  import React from "react"
2
2
  import pluralize from "pluralize"
3
3
  import { Loading } from "@openneuro/components/loading"
4
- import { ValidationResults } from "../validation/validation-results"
4
+ import { ValidationResultsDisplay } from "../validation/validation-results"
5
5
  import UploaderContext from "./uploader-context.js"
6
6
  import { validation } from "../workers/schema.js"
7
7
  import type { ValidationResult } from "@bids/validator/main"
@@ -106,7 +106,7 @@ class UploadValidator
106
106
  next={this.props.next}
107
107
  reset={this.props.reset}
108
108
  />
109
- <ValidationResults
109
+ <ValidationResultsDisplay
110
110
  issues={this.state.issues}
111
111
  />
112
112
  <span className="bids-link">
@@ -0,0 +1,201 @@
1
+ import React from "react"
2
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react"
3
+ import { DATASETS_QUERY, UserDatasetsView } from "../user-datasets-view"
4
+ import { MockedProvider } from "@apollo/client/testing"
5
+ import DatasetCard from "../dataset-card"
6
+
7
+ // Mocked datasets
8
+ const mockDatasets = [
9
+ {
10
+ node: {
11
+ id: "ds000001",
12
+ name: "The DBS-fMRI dataset",
13
+ created: "2025-01-22T19:55:49.997Z",
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: 6000,
24
+ created: "2025-01-22T19:55:49.997Z",
25
+ issues: [{ severity: "low" }],
26
+ description: {
27
+ Name: "DBS-FMRI",
28
+ Authors: ["John Doe"],
29
+ SeniorAuthor: "Dr. Smith",
30
+ DatasetType: "fMRI",
31
+ },
32
+ },
33
+ },
34
+ },
35
+ {
36
+ node: {
37
+ id: "ds000002",
38
+ name: "The DBS-fMRI dataset 2",
39
+ created: "2025-01-22T19:55:49.997Z",
40
+ followers: [
41
+ { userId: "user1", datasetId: "ds000002" },
42
+ { userId: "user2", datasetId: "ds000002" },
43
+ ],
44
+ stars: [
45
+ { userId: "user1", datasetId: "ds000002" },
46
+ ],
47
+ latestSnapshot: {
48
+ id: "ds000002:1.0.0",
49
+ size: 6000,
50
+ created: "2025-01-22T19:55:49.997Z",
51
+ issues: [{ severity: "medium" }],
52
+ description: {
53
+ Name: "DBS-FMRI 2",
54
+ Authors: ["Jane Doe"],
55
+ SeniorAuthor: "Dr. Johnson",
56
+ DatasetType: "fMRI",
57
+ },
58
+ },
59
+ },
60
+ },
61
+ ]
62
+
63
+ describe("<UserDatasetsView />", () => {
64
+ const mockUser = {
65
+ id: "user1",
66
+ name: "John Doe",
67
+ location: "Somewhere",
68
+ institution: "Some University",
69
+ email: "john.doe@example.com",
70
+ }
71
+ const mockHasEdit = true
72
+
73
+ it("renders loading state", () => {
74
+ const mockLoadingQuery = {
75
+ request: {
76
+ query: DATASETS_QUERY,
77
+ variables: { first: 25 },
78
+ },
79
+ result: { data: { datasets: { edges: [] } } },
80
+ }
81
+
82
+ render(
83
+ <MockedProvider mocks={[mockLoadingQuery]} addTypename={false}>
84
+ <UserDatasetsView user={mockUser} hasEdit={mockHasEdit} />
85
+ </MockedProvider>,
86
+ )
87
+
88
+ expect(screen.getByText("Loading datasets...")).toBeInTheDocument()
89
+ })
90
+
91
+ it("renders error state", async () => {
92
+ const mockErrorQuery = {
93
+ request: {
94
+ query: DATASETS_QUERY,
95
+ variables: { first: 25 },
96
+ },
97
+ error: new Error("Failed to fetch datasets"),
98
+ }
99
+
100
+ render(
101
+ <MockedProvider mocks={[mockErrorQuery]} addTypename={false}>
102
+ <UserDatasetsView user={mockUser} hasEdit={mockHasEdit} />
103
+ </MockedProvider>,
104
+ )
105
+
106
+ await waitFor(() => {
107
+ expect(
108
+ screen.getByText("Failed to fetch datasets: Failed to fetch datasets"),
109
+ ).toBeInTheDocument()
110
+ })
111
+ })
112
+
113
+ it("filters datasets by public filter", async () => {
114
+ const mockDatasetQuery = {
115
+ request: {
116
+ query: DATASETS_QUERY,
117
+ variables: { first: 25 },
118
+ },
119
+ result: { data: { datasets: { edges: mockDatasets } } },
120
+ }
121
+
122
+ render(
123
+ <MockedProvider mocks={[mockDatasetQuery]} addTypename={false}>
124
+ <UserDatasetsView user={mockUser} hasEdit={mockHasEdit} />
125
+ </MockedProvider>,
126
+ )
127
+
128
+ await waitFor(() => screen.getByTestId("public-filter"))
129
+ fireEvent.click(screen.getByTestId("public-filter"))
130
+ await waitFor(() => screen.getByText("Public"))
131
+ fireEvent.click(screen.getByText("Public"))
132
+
133
+ expect(screen.getByTestId("public-filter")).toHaveTextContent(
134
+ "Filter by: Public",
135
+ )
136
+ })
137
+
138
+ it("handles sorting datasets", async () => {
139
+ const mockDatasetQuery = {
140
+ request: {
141
+ query: DATASETS_QUERY,
142
+ variables: { first: 25 },
143
+ },
144
+ result: { data: { datasets: { edges: mockDatasets } } },
145
+ }
146
+
147
+ render(
148
+ <MockedProvider mocks={[mockDatasetQuery]} addTypename={false}>
149
+ <UserDatasetsView user={mockUser} hasEdit={mockHasEdit} />
150
+ </MockedProvider>,
151
+ )
152
+
153
+ await waitFor(() => screen.queryByText(mockDatasets[0].node.name))
154
+
155
+ fireEvent.click(screen.getByTestId("sort-order"))
156
+ await waitFor(() => screen.getByText("Name (A-Z)"))
157
+ fireEvent.click(screen.getByText("Name (Z-A)"))
158
+
159
+ expect(screen.getByTestId("sort-order")).toHaveTextContent("Name (Z-A)")
160
+ })
161
+ })
162
+
163
+ const mockDataset = {
164
+ id: "ds000001",
165
+ name: "Test Dataset",
166
+ created: "2025-01-01T00:00:00Z",
167
+ date: "2025-01-01T00:00:00Z",
168
+ public: true,
169
+ analytics: {
170
+ downloads: 12345,
171
+ views: 67890,
172
+ },
173
+ followers: [
174
+ { userId: "user1", datasetId: "ds000001" },
175
+ { userId: "user2", datasetId: "ds000001" },
176
+ ],
177
+ stars: [
178
+ { userId: "user1", datasetId: "ds000001" },
179
+ ],
180
+ latestSnapshot: {
181
+ id: "ds000001:1.0.0",
182
+ size: 1024 ** 3,
183
+ issues: [{ severity: "low" }],
184
+ created: "2025-01-01T00:00:00Z",
185
+ description: {
186
+ Authors: ["John Doe"],
187
+ SeniorAuthor: "Dr. Smith",
188
+ DatasetType: "fMRI",
189
+ },
190
+ },
191
+ }
192
+
193
+ describe("DatasetCard", () => {
194
+ it("should render dataset information correctly", () => {
195
+ render(<DatasetCard dataset={mockDataset} hasEdit={false} />)
196
+
197
+ expect(screen.getByText("Test Dataset")).toBeInTheDocument()
198
+ expect(screen.getByText("ds000001")).toBeInTheDocument()
199
+ expect(screen.getByText("1.00 GB")).toBeInTheDocument()
200
+ })
201
+ })
@@ -1,10 +1,22 @@
1
1
  import React from "react"
2
2
  import { render, screen } from "@testing-library/react"
3
- import type { User } from "../user-card"
4
3
  import { UserCard } from "../user-card"
5
4
 
5
+ interface User {
6
+ id: string
7
+ name: string
8
+ location: string
9
+ github?: string
10
+ institution: string
11
+ email: string
12
+ avatar: string
13
+ orcid: string
14
+ links: string[]
15
+ }
16
+
6
17
  describe("UserCard Component", () => {
7
18
  const baseUser: User = {
19
+ id: "123",
8
20
  name: "John Doe",
9
21
  email: "johndoe@example.com",
10
22
  orcid: "0000-0001-2345-6789",
@@ -12,6 +24,7 @@ describe("UserCard Component", () => {
12
24
  institution: "University of California",
13
25
  links: ["https://example.com", "https://example.org"],
14
26
  github: "johndoe",
27
+ avatar: "https://example.com/avatar.jpg",
15
28
  }
16
29
 
17
30
  it("renders all user details when all data is provided", () => {
@@ -44,10 +57,14 @@ describe("UserCard Component", () => {
44
57
 
45
58
  it("renders without optional fields", () => {
46
59
  const minimalUser: User = {
60
+ id: "124",
47
61
  name: "Jane Doe",
48
62
  email: "janedoe@example.com",
49
63
  orcid: "0000-0002-3456-7890",
50
64
  links: [],
65
+ avatar: "https://example.com/avatar.jpg",
66
+ location: "",
67
+ institution: "",
51
68
  }
52
69
 
53
70
  render(<UserCard user={minimalUser} />)
@@ -70,8 +87,14 @@ describe("UserCard Component", () => {
70
87
 
71
88
  it("renders correctly when links are empty", () => {
72
89
  const userWithEmptyLinks: User = {
73
- ...baseUser,
74
- links: [],
90
+ id: "125",
91
+ name: "John Smith",
92
+ email: "johnsmith@example.com",
93
+ orcid: "0000-0003-4567-8901",
94
+ links: [], // Empty links
95
+ avatar: "https://example.com/avatar.jpg",
96
+ location: "New York, NY",
97
+ institution: "NYU",
75
98
  }
76
99
 
77
100
  render(<UserCard user={userWithEmptyLinks} />)
@@ -84,10 +107,14 @@ describe("UserCard Component", () => {
84
107
 
85
108
  it("renders correctly when location and institution are missing", () => {
86
109
  const userWithoutLocationAndInstitution: User = {
110
+ id: "126",
87
111
  name: "Emily Doe",
88
112
  email: "emilydoe@example.com",
89
113
  orcid: "0000-0003-4567-8901",
90
114
  links: ["https://example.com"],
115
+ avatar: "https://example.com/avatar.jpg",
116
+ location: "",
117
+ institution: "",
91
118
  }
92
119
 
93
120
  render(<UserCard user={userWithoutLocationAndInstitution} />)
@@ -4,6 +4,12 @@ import { MockedProvider } from "@apollo/client/testing"
4
4
  import { MemoryRouter, Route, Routes } from "react-router-dom"
5
5
  import { UserQuery } from "../user-query"
6
6
  import { GET_USER_BY_ORCID } from "../user-query"
7
+ import * as ProfileUtils from "../../authentication/profile"
8
+
9
+ Object.defineProperty(ProfileUtils, "getProfile", {
10
+ value: () => ({ sub: "1" }),
11
+ writable: true,
12
+ })
7
13
 
8
14
  const validOrcid = "0009-0001-9689-7232"
9
15
 
@@ -3,8 +3,8 @@ import { cleanup, render, screen } from "@testing-library/react"
3
3
  import { MemoryRouter } from "react-router-dom"
4
4
  import { MockedProvider } from "@apollo/client/testing"
5
5
  import { UserRoutes } from "../user-routes"
6
- import type { User } from "../user-routes"
7
- import { UPDATE_USER } from "../user-query"
6
+ import type { User } from "../../types/user-types"
7
+ import { DATASETS_QUERY } from "../user-datasets-view"
8
8
 
9
9
  const defaultUser: User = {
10
10
  id: "1",
@@ -21,24 +21,49 @@ const defaultUser: User = {
21
21
  const mocks = [
22
22
  {
23
23
  request: {
24
- query: UPDATE_USER,
25
- variables: {
26
- id: "1",
27
- name: "John Doe",
28
- location: "Unknown",
29
- github: "",
30
- institution: "Unknown Institution",
31
- email: "john.doe@example.com",
32
- avatar: "https://dummyimage.com/200x200/000/fff",
33
- orcid: "0000-0000-0000-0000",
34
- links: [],
35
- },
24
+ query: DATASETS_QUERY,
25
+ variables: { first: 25 },
36
26
  },
37
27
  result: {
38
28
  data: {
39
- updateUser: {
40
- id: "1",
41
- name: "John Doe",
29
+ datasets: {
30
+ edges: [
31
+ {
32
+ node: {
33
+ id: "ds001012",
34
+ created: "2025-01-22T19:55:49.997Z",
35
+ name: "The DBS-fMRI dataset",
36
+ public: null,
37
+ analytics: {
38
+ views: 9,
39
+ downloads: 0,
40
+ },
41
+ stars: [],
42
+ followers: [
43
+ {
44
+ userId: "47e6a401-5edf-4022-801f-c05fffbf1d10",
45
+ datasetId: "ds001012",
46
+ },
47
+ ],
48
+ latestSnapshot: {
49
+ id: "ds001012:1.0.0",
50
+ size: 635,
51
+ created: "2025-01-22T19:55:49.997Z",
52
+ description: {
53
+ Name: "The DBS-fMRI dataset",
54
+ Authors: [
55
+ "Jianxun Ren",
56
+ " Changqing Jiang",
57
+ "Wei Zhang",
58
+ "Louisa Dahmani",
59
+ "Lunhao Shen",
60
+ "Feng Zhang",
61
+ ],
62
+ },
63
+ },
64
+ },
65
+ },
66
+ ],
42
67
  },
43
68
  },
44
69
  },
@@ -60,7 +85,6 @@ describe("UserRoutes Component", () => {
60
85
 
61
86
  it("renders UserDatasetsView for the default route", async () => {
62
87
  renderWithRouter(user, "/", true)
63
- expect(screen.getByText(`${user.name}'s Datasets`)).toBeInTheDocument()
64
88
  const datasetsView = await screen.findByTestId("user-datasets-view")
65
89
  expect(datasetsView).toBeInTheDocument()
66
90
  })
@@ -0,0 +1,157 @@
1
+ import React from "react"
2
+ import styles from "../scss/datasetcard.module.scss"
3
+
4
+ interface UserDatasetFiltersProps {
5
+ publicFilter: string
6
+ setPublicFilter: React.Dispatch<React.SetStateAction<string>>
7
+ sortOrder: string
8
+ setSortOrder: React.Dispatch<React.SetStateAction<string>>
9
+ searchQuery: string
10
+ setSearchQuery: React.Dispatch<React.SetStateAction<string>>
11
+ }
12
+
13
+ export const UserDatasetFilters: React.FC<UserDatasetFiltersProps> = ({
14
+ publicFilter,
15
+ setPublicFilter,
16
+ sortOrder,
17
+ setSortOrder,
18
+ searchQuery,
19
+ setSearchQuery,
20
+ }) => {
21
+ const [isFilterOpen, setIsFilterOpen] = React.useState(false)
22
+ const [isSortOpen, setIsSortOpen] = React.useState(false)
23
+
24
+ const currentSortBy = sortOrder === "name-asc"
25
+ ? "Name (A-Z)"
26
+ : sortOrder === "name-desc"
27
+ ? "Name (Z-A)"
28
+ : sortOrder === "date-newest"
29
+ ? "Added (Newest)"
30
+ : sortOrder === "date-oldest"
31
+ ? "Oldest"
32
+ : "Updated"
33
+
34
+ return (
35
+ <div className={styles.userDSfilters}>
36
+ {/* Search Input */}
37
+ <input
38
+ type="text"
39
+ placeholder="Keyword Search (Name or ID)"
40
+ value={searchQuery}
41
+ onChange={(e) => setSearchQuery(e.target.value)}
42
+ className={styles.searchInput}
43
+ />
44
+
45
+ {/* Filter by Visibility */}
46
+ <div
47
+ data-testid="public-filter"
48
+ className={`${styles.filterDiv} ${isFilterOpen ? styles.open : ""}`}
49
+ onClick={() => setIsFilterOpen(!isFilterOpen)}
50
+ >
51
+ <span>
52
+ Filter by:{" "}
53
+ <b>
54
+ {publicFilter === "all"
55
+ ? "All"
56
+ : publicFilter.charAt(0).toUpperCase() + publicFilter.slice(1)}
57
+ </b>
58
+ </span>
59
+ <div className={styles.filterDropdown}>
60
+ {isFilterOpen && (
61
+ <ul>
62
+ <li
63
+ onClick={() => {
64
+ setPublicFilter("all")
65
+ setIsFilterOpen(false)
66
+ }}
67
+ className={publicFilter === "all" ? styles.active : ""}
68
+ >
69
+ All
70
+ </li>
71
+ <li
72
+ onClick={() => {
73
+ setPublicFilter("public")
74
+ setIsFilterOpen(false)
75
+ }}
76
+ className={publicFilter === "public" ? styles.active : ""}
77
+ >
78
+ Public
79
+ </li>
80
+ <li
81
+ onClick={() => {
82
+ setPublicFilter("private")
83
+ setIsFilterOpen(false)
84
+ }}
85
+ className={publicFilter === "private" ? styles.active : ""}
86
+ >
87
+ Private
88
+ </li>
89
+ </ul>
90
+ )}
91
+ </div>
92
+ </div>
93
+
94
+ {/* Sort Options */}
95
+ <div
96
+ data-testid="sort-order"
97
+ className={`${styles.sortDiv} ${isSortOpen ? styles.open : ""}`}
98
+ onClick={() => setIsSortOpen(!isSortOpen)}
99
+ >
100
+ <span>
101
+ Sort by: <b>{currentSortBy}</b>
102
+ </span>
103
+ <div className={styles.sortDropdown}>
104
+ {isSortOpen && (
105
+ <ul>
106
+ <li
107
+ onClick={() => {
108
+ setSortOrder("name-asc")
109
+ setIsSortOpen(false)
110
+ }}
111
+ className={sortOrder === "name-asc" ? styles.active : ""}
112
+ >
113
+ Name (A-Z)
114
+ </li>
115
+ <li
116
+ onClick={() => {
117
+ setSortOrder("name-desc")
118
+ setIsSortOpen(false)
119
+ }}
120
+ className={sortOrder === "name-desc" ? styles.active : ""}
121
+ >
122
+ Name (Z-A)
123
+ </li>
124
+ <li
125
+ onClick={() => {
126
+ setSortOrder("date-newest")
127
+ setIsSortOpen(false)
128
+ }}
129
+ className={sortOrder === "date-newest" ? styles.active : ""}
130
+ >
131
+ Added
132
+ </li>
133
+ <li
134
+ onClick={() => {
135
+ setSortOrder("date-oldest")
136
+ setIsSortOpen(false)
137
+ }}
138
+ className={sortOrder === "date-oldest" ? styles.active : ""}
139
+ >
140
+ Oldest
141
+ </li>
142
+ <li
143
+ onClick={() => {
144
+ setSortOrder("date-updated")
145
+ setIsSortOpen(false)
146
+ }}
147
+ className={sortOrder === "date-updated" ? styles.active : ""}
148
+ >
149
+ Updated
150
+ </li>
151
+ </ul>
152
+ )}
153
+ </div>
154
+ </div>
155
+ </div>
156
+ )
157
+ }
@@ -0,0 +1,121 @@
1
+ import React from "react"
2
+ import { formatDistanceToNow, parseISO } from "date-fns"
3
+ import activityPulseIcon from "../../assets/activity-icon.png"
4
+ import { Tooltip } from "@openneuro/components/tooltip"
5
+ import { Icon } from "@openneuro/components/icon"
6
+ import styles from "./scss/datasetcard.module.scss"
7
+ import type { DatasetCardProps } from "../types/user-types"
8
+
9
+ export const DatasetCard: React.FC<DatasetCardProps> = (
10
+ { dataset, hasEdit },
11
+ ) => {
12
+ // Check visibility conditions
13
+ if (!dataset.public && !hasEdit) {
14
+ return null
15
+ }
16
+
17
+ const dateAdded = new Date(dataset.created).toLocaleDateString()
18
+
19
+ // Check if the created date is valid before formatting
20
+ const parsedCreatedDate = parseISO(dataset.created)
21
+ const dateAddedDifference = isNaN(parsedCreatedDate.getTime())
22
+ ? "Invalid date"
23
+ : formatDistanceToNow(parsedCreatedDate)
24
+
25
+ // Check if dataset.analytics exists before accessing its properties
26
+ const downloads = dataset.analytics?.downloads
27
+ ? `${dataset.analytics.downloads.toLocaleString()} Downloads \n`
28
+ : ""
29
+ const views = dataset.analytics?.views
30
+ ? `${dataset.analytics.views.toLocaleString()} Views \n`
31
+ : ""
32
+
33
+ // Check if dataset.followers is an array and has length
34
+ const following = Array.isArray(dataset.followers) && dataset.followers.length
35
+ ? `${dataset.followers.length.toLocaleString()} Follower \n`
36
+ : ""
37
+
38
+ // Check if dataset.stars is an array and has length
39
+ const stars = Array.isArray(dataset.stars) && dataset.stars.length
40
+ ? `${dataset.stars.length.toLocaleString()} Bookmarked`
41
+ : ""
42
+
43
+ const activityTooltip = downloads + views + following + stars
44
+
45
+ const activityIcon = (
46
+ <Tooltip
47
+ tooltip={activityTooltip}
48
+ flow="up"
49
+ className="result-icon result-activity-icon"
50
+ >
51
+ <Icon
52
+ imgSrc={activityPulseIcon}
53
+ iconSize="22px"
54
+ label="activity"
55
+ iconOnly={true}
56
+ />
57
+ </Tooltip>
58
+ )
59
+
60
+ const publicIcon = dataset.public && (
61
+ <Tooltip
62
+ tooltip="Visible to all viewers"
63
+ flow="up"
64
+ className="result-icon result-public-icon"
65
+ >
66
+ <Icon
67
+ icon="fas fa-globe"
68
+ color="rgb(116,181,105)"
69
+ iconSize="16px"
70
+ label="Public"
71
+ iconOnly={true}
72
+ />
73
+ </Tooltip>
74
+ )
75
+
76
+ const sizeInBytes = dataset.latestSnapshot?.size
77
+ let datasetSize = "Unknown size"
78
+
79
+ if (sizeInBytes) {
80
+ if (sizeInBytes >= 1024 ** 3) {
81
+ datasetSize = `${(sizeInBytes / (1024 ** 3)).toFixed(2)} GB`
82
+ } else if (sizeInBytes >= 1024 ** 2) {
83
+ datasetSize = `${(sizeInBytes / (1024 ** 2)).toFixed(2)} MB`
84
+ } else if (sizeInBytes >= 1024) {
85
+ datasetSize = `${(sizeInBytes / 1024).toFixed(2)} KB`
86
+ } else {
87
+ datasetSize = `${sizeInBytes} bytes`
88
+ }
89
+ }
90
+
91
+ return (
92
+ <div
93
+ className={styles.userDsCard}
94
+ key={dataset.id}
95
+ data-testid={`user-ds-${dataset.id}`}
96
+ >
97
+ <h4>
98
+ <a href={`/datasets/${dataset.id}`}>{dataset.name}</a>
99
+ </h4>
100
+ <div className={styles.userDsFooter}>
101
+ <div className={styles.userMetawrap}>
102
+ <span>
103
+ Added: <b>{dateAdded}</b> ({dateAddedDifference} ago)
104
+ </span>
105
+ <span>
106
+ OpenNeuro Accession Number: <b>{dataset.id}</b>
107
+ </span>
108
+ <span>
109
+ Dataset Size: <b>{datasetSize}</b>
110
+ </span>
111
+ </div>
112
+ <div className={styles.userIconwrap}>
113
+ {activityIcon}
114
+ {publicIcon && <div className="owner-icon-wrap">{publicIcon}</div>}
115
+ </div>
116
+ </div>
117
+ </div>
118
+ )
119
+ }
120
+
121
+ export default DatasetCard