@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.
Files changed (51) 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/page/page.scss +2 -70
  5. package/src/scripts/components/search-page/SearchResultItem.tsx +3 -1
  6. package/src/scripts/components/search-page/search-page.scss +1 -13
  7. package/src/scripts/config.ts +6 -0
  8. package/src/scripts/dataset/files/__tests__/__snapshots__/file.spec.jsx.snap +2 -2
  9. package/src/scripts/dataset/files/file.tsx +2 -2
  10. package/src/scripts/dataset/mutations/__tests__/update-file.spec.tsx +126 -0
  11. package/src/scripts/dataset/mutations/update-file.jsx +20 -5
  12. package/src/scripts/dataset/routes/snapshot.tsx +1 -1
  13. package/src/scripts/errors/errorRoute.tsx +2 -0
  14. package/src/scripts/pages/admin/__tests__/users.spec.tsx +51 -18
  15. package/src/scripts/pages/admin/admin.jsx +2 -2
  16. package/src/scripts/pages/admin/user-fragment.ts +10 -3
  17. package/src/scripts/pages/admin/user-summary.tsx +100 -0
  18. package/src/scripts/pages/admin/user-tools.tsx +81 -58
  19. package/src/scripts/pages/admin/users.module.scss +277 -0
  20. package/src/scripts/pages/admin/users.tsx +351 -152
  21. package/src/scripts/queries/user.ts +120 -3
  22. package/src/scripts/queries/users.ts +247 -0
  23. package/src/scripts/routes.tsx +7 -15
  24. package/src/scripts/types/user-types.ts +12 -13
  25. package/src/scripts/uploader/file-select.tsx +42 -57
  26. package/src/scripts/uploader/upload-select.jsx +1 -1
  27. package/src/scripts/users/__tests__/dataset-card.spec.tsx +127 -0
  28. package/src/scripts/users/__tests__/user-account-view.spec.tsx +150 -67
  29. package/src/scripts/users/__tests__/user-card.spec.tsx +6 -17
  30. package/src/scripts/users/__tests__/user-query.spec.tsx +133 -38
  31. package/src/scripts/users/__tests__/user-routes.spec.tsx +156 -27
  32. package/src/scripts/users/__tests__/user-tabs.spec.tsx +7 -7
  33. package/src/scripts/users/components/edit-list.tsx +26 -5
  34. package/src/scripts/users/components/edit-string.tsx +40 -13
  35. package/src/scripts/users/components/editable-content.tsx +10 -3
  36. package/src/scripts/users/components/user-dataset-filters.tsx +205 -121
  37. package/src/scripts/users/dataset-card.tsx +3 -2
  38. package/src/scripts/users/github-auth-button.tsx +98 -0
  39. package/src/scripts/users/scss/datasetcard.module.scss +65 -12
  40. package/src/scripts/users/scss/useraccountview.module.scss +1 -1
  41. package/src/scripts/users/user-account-view.tsx +43 -34
  42. package/src/scripts/users/user-card.tsx +23 -22
  43. package/src/scripts/users/user-container.tsx +9 -5
  44. package/src/scripts/users/user-datasets-view.tsx +350 -40
  45. package/src/scripts/users/user-menu.tsx +4 -9
  46. package/src/scripts/users/user-notifications-view.tsx +9 -7
  47. package/src/scripts/users/user-query.tsx +3 -6
  48. package/src/scripts/users/user-routes.tsx +11 -5
  49. package/src/scripts/users/user-tabs.tsx +4 -2
  50. package/src/scripts/users/__tests__/datasest-card.spec.tsx +0 -201
  51. package/src/scripts/users/fragments/query.js +0 -42
@@ -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>
@@ -11,7 +11,7 @@ import iconUnread from "../../assets/icon-unread.png"
11
11
  import iconSaved from "../../assets/icon-saved.png"
12
12
  import iconArchived from "../../assets/icon-archived.png"
13
13
 
14
- export const UserNotificationsView = ({ user }) => {
14
+ export const UserNotificationsView = ({ orcidUser }) => {
15
15
  const tabsRef = useRef<HTMLUListElement | null>(null)
16
16
  const { tab = "unread" } = useParams()
17
17
  const navigate = useNavigate()
@@ -50,18 +50,20 @@ export const UserNotificationsView = ({ user }) => {
50
50
  // Redirect to default tab if no tab is specified
51
51
  useEffect(() => {
52
52
  if (!["unread", "saved", "archived"].includes(tab)) {
53
- navigate(`/user/${user.orcid}/notifications/unread`, { replace: true })
53
+ navigate(`/user/${orcidUser.orcid}/notifications/unread`, {
54
+ replace: true,
55
+ })
54
56
  }
55
- }, [tab, user.orcid, navigate])
57
+ }, [tab, orcidUser.orcid, navigate])
56
58
 
57
59
  return (
58
60
  <div data-testid="user-notifications-view">
59
- <h3>Notifications for {user.name}</h3>
61
+ <h3>Notifications for {orcidUser.name}</h3>
60
62
  <div className={styles.tabContainer}>
61
63
  <ul className={styles.tabs} ref={tabsRef}>
62
64
  <li>
63
65
  <NavLink
64
- to={`/user/${user.orcid}/notifications/unread`}
66
+ to={`/user/${orcidUser.orcid}/notifications/unread`}
65
67
  className={({ isActive }) =>
66
68
  isActive
67
69
  ? `${styles.active} ${styles.tabUnread}`
@@ -73,7 +75,7 @@ export const UserNotificationsView = ({ user }) => {
73
75
  </li>
74
76
  <li>
75
77
  <NavLink
76
- to={`/user/${user.orcid}/notifications/saved`}
78
+ to={`/user/${orcidUser.orcid}/notifications/saved`}
77
79
  className={({ isActive }) =>
78
80
  isActive
79
81
  ? `${styles.active} ${styles.tabSaved}`
@@ -85,7 +87,7 @@ export const UserNotificationsView = ({ user }) => {
85
87
  </li>
86
88
  <li>
87
89
  <NavLink
88
- to={`/user/${user.orcid}/notifications/archived`}
90
+ to={`/user/${orcidUser.orcid}/notifications/archived`}
89
91
  className={({ isActive }) =>
90
92
  isActive
91
93
  ? `${styles.active} ${styles.tabArchived}`
@@ -11,7 +11,7 @@ import { useUser } from "../queries/user"
11
11
  export const UserQuery: React.FC = () => {
12
12
  const { orcid } = useParams()
13
13
  const isOrcidValid = orcid && isValidOrcid(orcid)
14
- const { user, loading, error } = useUser()
14
+ const { user, loading, error } = useUser(orcid)
15
15
 
16
16
  const [cookies] = useCookies()
17
17
  const profile = getProfile(cookies)
@@ -23,16 +23,13 @@ export const UserQuery: React.FC = () => {
23
23
 
24
24
  if (loading) return <div>Loading...</div>
25
25
 
26
- if (error || !user || user?.orcid !== orcid) {
26
+ if (error || !user) {
27
27
  return <FourOFourPage />
28
28
  }
29
29
 
30
- if (!profile || !profile.sub) {
31
- return <FourOFourPage />
32
- }
33
30
  // is admin or profile matches id from the user data being returned
34
31
  const isUser = (user?.id === profile?.sub) ? true : false
35
32
  const hasEdit = isAdminUser || (user?.id === profile?.sub) ? true : false
36
33
  // Render user data with UserRoutes
37
- return <UserRoutes user={user} hasEdit={hasEdit} isUser={isUser} />
34
+ return <UserRoutes orcidUser={user} hasEdit={hasEdit} isUser={isUser} />
38
35
  }
@@ -15,7 +15,7 @@ import {
15
15
  import type { UserRoutesProps } from "../types/user-types"
16
16
 
17
17
  export const UserRoutes: React.FC<UserRoutesProps> = (
18
- { user, hasEdit, isUser },
18
+ { orcidUser, hasEdit, isUser },
19
19
  ) => {
20
20
  return (
21
21
  <Routes>
@@ -23,21 +23,27 @@ export const UserRoutes: React.FC<UserRoutesProps> = (
23
23
  <Route
24
24
  path="*"
25
25
  element={
26
- <UserAccountContainer user={user} hasEdit={hasEdit} isUser={isUser} />
26
+ <UserAccountContainer
27
+ orcidUser={orcidUser}
28
+ hasEdit={hasEdit}
29
+ isUser={isUser}
30
+ />
27
31
  }
28
32
  >
29
33
  <Route
30
34
  path=""
31
- element={<UserDatasetsView user={user} hasEdit={hasEdit} />}
35
+ element={<UserDatasetsView orcidUser={orcidUser} hasEdit={hasEdit} />}
32
36
  />
33
37
  <Route
34
38
  path="account"
35
- element={hasEdit ? <UserAccountView /> : <FourOThreePage />}
39
+ element={hasEdit
40
+ ? <UserAccountView orcidUser={orcidUser} />
41
+ : <FourOThreePage />}
36
42
  />
37
43
  <Route
38
44
  path="notifications/*"
39
45
  element={hasEdit
40
- ? <UserNotificationsView user={user} />
46
+ ? <UserNotificationsView orcidUser={orcidUser} />
41
47
  : <FourOThreePage />}
42
48
  >
43
49
  <Route index element={<UnreadNotifications />} />
@@ -50,7 +50,8 @@ export const UserAccountTabs: React.FC<UserAccountTabsProps> = (
50
50
  {isUser ? "My" : "User"} Datasets
51
51
  </NavLink>
52
52
  </li>
53
- <li>
53
+ {
54
+ /* <li>
54
55
  <NavLink
55
56
  data-testid="user-notifications-tab"
56
57
  to="notifications"
@@ -59,7 +60,8 @@ export const UserAccountTabs: React.FC<UserAccountTabsProps> = (
59
60
  >
60
61
  Notifications
61
62
  </NavLink>
62
- </li>
63
+ </li> */
64
+ }
63
65
  <li>
64
66
  <NavLink
65
67
  to="account"
@@ -1,201 +0,0 @@
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,42 +0,0 @@
1
- import { gql } from "@apollo/client"
2
-
3
- export const INDEX_DATASET_FRAGMENT = gql`
4
- fragment DatasetIndex on Dataset {
5
- id
6
- created
7
- name
8
- public
9
- analytics {
10
- views
11
- downloads
12
- }
13
- stars {
14
- userId
15
- datasetId
16
- }
17
- followers {
18
- userId
19
- datasetId
20
- }
21
- latestSnapshot {
22
- id
23
- size
24
- created
25
- issues {
26
- severity
27
- }
28
- description {
29
- Name
30
- Authors
31
- SeniorAuthor
32
- DatasetType
33
- }
34
- }
35
- draft {
36
- id
37
- }
38
- uploader {
39
- id
40
- }
41
- }
42
- `