@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.
- package/package.json +5 -5
- package/src/assets/activity-icon.png +0 -0
- package/src/assets/icon-archived.png +0 -0
- package/src/assets/icon-saved.png +0 -0
- package/src/assets/icon-unread.png +0 -0
- package/src/client.jsx +1 -1
- package/src/scripts/datalad/dataset/dataset-query-fragments.js +4 -14
- package/src/scripts/dataset/__tests__/__snapshots__/snapshot-container.spec.tsx.snap +4 -304
- package/src/scripts/dataset/components/ValidationBlock.tsx +13 -15
- package/src/scripts/dataset/components/__tests__/ValidationBlock.spec.tsx +2 -0
- package/src/scripts/dataset/draft-container.tsx +2 -1
- package/src/scripts/dataset/files/__tests__/__snapshots__/file-tree.spec.jsx.snap +1 -9
- package/src/scripts/dataset/fragments/__tests__/{dataset-alert-draft.spec.tsx → dataset-alert.spec.tsx} +33 -1
- package/src/scripts/dataset/fragments/{dataset-alert-draft.tsx → dataset-alert.tsx} +30 -18
- package/src/scripts/dataset/routes/delete-page.tsx +72 -39
- package/src/scripts/dataset/routes/snapshot.tsx +23 -17
- package/src/scripts/dataset/routes/tab-routes-draft.tsx +5 -2
- package/src/scripts/dataset/snapshot-container.tsx +11 -0
- package/src/scripts/search/__tests__/search-params-ctx.spec.tsx +3 -0
- package/src/scripts/search/initial-search-params.tsx +2 -0
- package/src/scripts/search/inputs/__tests__/nihselect.spec.tsx +36 -0
- package/src/scripts/search/inputs/index.ts +2 -0
- package/src/scripts/search/inputs/nih-select.tsx +63 -0
- package/src/scripts/search/search-container.tsx +20 -12
- package/src/scripts/search/search-params-ctx.tsx +2 -0
- package/src/scripts/search/search-routes.tsx +14 -6
- package/src/scripts/search/use-search-results.tsx +15 -0
- package/src/scripts/types/user-types.ts +72 -0
- package/src/scripts/uploader/upload-issues.tsx +2 -2
- package/src/scripts/users/__tests__/datasest-card.spec.tsx +201 -0
- package/src/scripts/users/__tests__/user-card.spec.tsx +30 -3
- package/src/scripts/users/__tests__/user-query.spec.tsx +6 -0
- package/src/scripts/users/__tests__/user-routes.spec.tsx +42 -18
- package/src/scripts/users/components/user-dataset-filters.tsx +157 -0
- package/src/scripts/users/dataset-card.tsx +121 -0
- package/src/scripts/users/fragments/query.js +42 -0
- package/src/scripts/users/scss/datasetcard.module.scss +153 -0
- package/src/scripts/users/scss/usernotifications.module.scss +159 -0
- package/src/scripts/users/user-account-view.tsx +1 -12
- package/src/scripts/users/user-card.tsx +1 -14
- package/src/scripts/users/user-container.tsx +1 -17
- package/src/scripts/users/user-datasets-view.tsx +58 -43
- package/src/scripts/users/user-notification-accordion.tsx +160 -0
- package/src/scripts/users/user-notification-list.tsx +27 -0
- package/src/scripts/users/user-notifications-tab-content.tsx +85 -0
- package/src/scripts/users/user-notifications-view.tsx +102 -4
- package/src/scripts/users/user-query.tsx +6 -14
- package/src/scripts/users/user-routes.tsx +18 -19
- package/src/scripts/utils/__tests__/user-datasets.spec.tsx +86 -0
- package/src/scripts/utils/gtag.js +3 -2
- package/src/scripts/utils/user-datasets.tsx +60 -0
- package/src/scripts/validation/__tests__/__snapshots__/validation-issues.spec.tsx.snap +1 -122
- package/src/scripts/validation/validation-results-query.ts +44 -0
- package/src/scripts/validation/validation-results.tsx +31 -7
- package/src/scripts/validation/validation.tsx +58 -49
- package/src/scripts/workers/schema.worker.ts +2 -7
- package/tsconfig.json +1 -2
- 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 {
|
|
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
|
-
<
|
|
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
|
-
|
|
74
|
-
|
|
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 "
|
|
7
|
-
import {
|
|
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:
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|