@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
@@ -1,13 +1,19 @@
1
- import React from "react"
1
+ import React, { useEffect, useRef, useState } from "react"
2
+ import { useParams } from "react-router-dom"
2
3
  import styles from "../scss/datasetcard.module.scss"
4
+ import { isValidOrcid } from "../../utils/validationUtils"
5
+ import { useCookies } from "react-cookie"
6
+ import { getProfile } from "../../authentication/profile"
7
+ import { useUser } from "../../queries/user"
3
8
 
4
9
  interface UserDatasetFiltersProps {
5
10
  publicFilter: string
6
11
  setPublicFilter: React.Dispatch<React.SetStateAction<string>>
7
12
  sortOrder: string
8
13
  setSortOrder: React.Dispatch<React.SetStateAction<string>>
9
- searchQuery: string
10
- setSearchQuery: React.Dispatch<React.SetStateAction<string>>
14
+ onSearch: (query: string, publicFilter: string) => void
15
+ currentSearchTerm: string
16
+ hasEdit: boolean
11
17
  }
12
18
 
13
19
  export const UserDatasetFilters: React.FC<UserDatasetFiltersProps> = ({
@@ -15,11 +21,15 @@ export const UserDatasetFilters: React.FC<UserDatasetFiltersProps> = ({
15
21
  setPublicFilter,
16
22
  sortOrder,
17
23
  setSortOrder,
18
- searchQuery,
19
- setSearchQuery,
24
+ onSearch,
25
+ currentSearchTerm,
26
+ hasEdit,
20
27
  }) => {
21
- const [isFilterOpen, setIsFilterOpen] = React.useState(false)
22
- const [isSortOpen, setIsSortOpen] = React.useState(false)
28
+ const [isFilterOpen, setIsFilterOpen] = useState(false)
29
+ const [isSortOpen, setIsSortOpen] = useState(false)
30
+ const [localSearchQuery, setLocalSearchQuery] = useState(currentSearchTerm)
31
+ const searchInputRef = useRef<HTMLInputElement>(null)
32
+ const searchButtonRef = useRef<HTMLButtonElement>(null)
23
33
 
24
34
  const currentSortBy = sortOrder === "name-asc"
25
35
  ? "Name (A-Z)"
@@ -31,127 +41,201 @@ export const UserDatasetFilters: React.FC<UserDatasetFiltersProps> = ({
31
41
  ? "Oldest"
32
42
  : "Updated"
33
43
 
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
+ const handleSearchInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
45
+ setLocalSearchQuery(e.target.value)
46
+ }
47
+
48
+ const triggerSearch = () => {
49
+ onSearch(localSearchQuery, publicFilter)
50
+ }
51
+
52
+ const handleSearchInputKeyDown = (
53
+ e: React.KeyboardEvent<HTMLInputElement>,
54
+ ) => {
55
+ if (e.key === "Enter") {
56
+ triggerSearch()
57
+ }
58
+ }
59
+
60
+ const handleSearchButtonMouseOver = () => {
61
+ if (document.activeElement === searchButtonRef.current) {
62
+ triggerSearch()
63
+ }
64
+ }
65
+
66
+ const handleClearSearch = () => {
67
+ setLocalSearchQuery("")
68
+ onSearch("", publicFilter)
69
+ }
44
70
 
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>
71
+ useEffect(() => {
72
+ setLocalSearchQuery(currentSearchTerm)
73
+ }, [currentSearchTerm])
74
+ const datasetQuery = {
75
+ datasetType_selected: "My Datasets",
76
+ }
77
+
78
+ // construct query for "my datasets" search link
79
+ const jsonString = JSON.stringify(datasetQuery)
80
+ const encodedJsonString = encodeURIComponent(jsonString)
81
+ const searchPageUrl = `/search?query=${encodedJsonString}`
82
+
83
+ //check if user is current user to provide query to search "My Datasets"
84
+ const { orcid } = useParams()
85
+ const isOrcidValid = orcid && isValidOrcid(orcid)
86
+ const { user } = useUser(orcid)
87
+ const [cookies] = useCookies()
88
+ const profile = getProfile(cookies)
89
+ const isUser = (user?.id === profile?.sub) ? true : false
90
+
91
+ return (
92
+ <>
93
+ <div className={styles.searchInputContainer}>
94
+ {/* Search Input */}
95
+ <div className={styles.searchInputWrap}>
96
+ <input
97
+ ref={searchInputRef}
98
+ type="text"
99
+ placeholder="Keyword Search (Name, Authors, README, or ID)"
100
+ value={localSearchQuery}
101
+ onChange={handleSearchInputChange}
102
+ onKeyDown={handleSearchInputKeyDown}
103
+ className={styles.searchInput}
104
+ />
105
+ {localSearchQuery && (
106
+ <button
107
+ onClick={handleClearSearch}
108
+ className={styles.clearSearchButton}
109
+ >
110
+ <span aria-label="Clear Search">X</span>
111
+ </button>
90
112
  )}
91
113
  </div>
114
+ {/* Search Button */}
115
+ <button
116
+ ref={searchButtonRef}
117
+ onClick={triggerSearch}
118
+ onMouseOver={handleSearchButtonMouseOver}
119
+ className={styles.searchSubmitButton}
120
+ >
121
+ Search
122
+ </button>
92
123
  </div>
93
124
 
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>
125
+ <div className={styles.userDSfilters}>
126
+ {/* Filter by Visibility */}
127
+ {isOrcidValid && hasEdit && isUser && (
128
+ <a className={styles.searchLink} href={searchPageUrl}>
129
+ View your datasets on the search page
130
+ </a>
131
+ )}
132
+ {hasEdit &&
133
+ (
134
+ <div
135
+ data-testid="public-filter"
136
+ className={`${styles.filterDiv} ${
137
+ isFilterOpen ? styles.open : ""
138
+ }`}
139
+ onClick={() => setIsFilterOpen(!isFilterOpen)}
140
+ >
141
+ <span>
142
+ Filter by:{" "}
143
+ <b>
144
+ {publicFilter === "all"
145
+ ? "All"
146
+ : publicFilter.charAt(0).toUpperCase() +
147
+ publicFilter.slice(1)}
148
+ </b>
149
+ </span>
150
+ <div className={styles.filterDropdown}>
151
+ {isFilterOpen && (
152
+ <ul>
153
+ <li
154
+ onClick={() => {
155
+ setPublicFilter("all")
156
+ setIsFilterOpen(false)
157
+ }}
158
+ className={publicFilter === "all" ? styles.active : ""}
159
+ >
160
+ All
161
+ </li>
162
+ <li
163
+ onClick={() => {
164
+ setPublicFilter("public")
165
+ setIsFilterOpen(false)
166
+ }}
167
+ className={publicFilter === "public" ? styles.active : ""}
168
+ >
169
+ Public
170
+ </li>
171
+ </ul>
172
+ )}
173
+ </div>
174
+ </div>
152
175
  )}
176
+
177
+ {/* Sort Options */}
178
+ <div
179
+ data-testid="sort-order"
180
+ className={`${styles.sortDiv} ${isSortOpen ? styles.open : ""}`}
181
+ onClick={() => setIsSortOpen(!isSortOpen)}
182
+ >
183
+ <span>
184
+ Sort by: <b>{currentSortBy}</b>
185
+ </span>
186
+ <div className={styles.sortDropdown}>
187
+ {isSortOpen && (
188
+ <ul>
189
+ <li
190
+ onClick={() => {
191
+ setSortOrder("name-asc")
192
+ setIsSortOpen(false)
193
+ }}
194
+ className={sortOrder === "name-asc" ? styles.active : ""}
195
+ >
196
+ Name (A-Z)
197
+ </li>
198
+ <li
199
+ onClick={() => {
200
+ setSortOrder("name-desc")
201
+ setIsSortOpen(false)
202
+ }}
203
+ className={sortOrder === "name-desc" ? styles.active : ""}
204
+ >
205
+ Name (Z-A)
206
+ </li>
207
+ <li
208
+ onClick={() => {
209
+ setSortOrder("date-newest")
210
+ setIsSortOpen(false)
211
+ }}
212
+ className={sortOrder === "date-newest" ? styles.active : ""}
213
+ >
214
+ Added
215
+ </li>
216
+ <li
217
+ onClick={() => {
218
+ setSortOrder("date-oldest")
219
+ setIsSortOpen(false)
220
+ }}
221
+ className={sortOrder === "date-oldest" ? styles.active : ""}
222
+ >
223
+ Oldest
224
+ </li>
225
+ <li
226
+ onClick={() => {
227
+ setSortOrder("date-updated")
228
+ setIsSortOpen(false)
229
+ }}
230
+ className={sortOrder === "date-updated" ? styles.active : ""}
231
+ >
232
+ Updated
233
+ </li>
234
+ </ul>
235
+ )}
236
+ </div>
153
237
  </div>
154
238
  </div>
155
- </div>
239
+ </>
156
240
  )
157
241
  }
@@ -87,7 +87,6 @@ export const DatasetCard: React.FC<DatasetCardProps> = (
87
87
  datasetSize = `${sizeInBytes} bytes`
88
88
  }
89
89
  }
90
-
91
90
  return (
92
91
  <div
93
92
  className={styles.userDsCard}
@@ -95,7 +94,9 @@ export const DatasetCard: React.FC<DatasetCardProps> = (
95
94
  data-testid={`user-ds-${dataset.id}`}
96
95
  >
97
96
  <h4>
98
- <a href={`/datasets/${dataset.id}`}>{dataset.name}</a>
97
+ <a href={`/datasets/${dataset.id}`}>
98
+ {dataset.name ? dataset.name : dataset.id}
99
+ </a>
99
100
  </h4>
100
101
  <div className={styles.userDsFooter}>
101
102
  <div className={styles.userMetawrap}>
@@ -0,0 +1,98 @@
1
+ import React, { useEffect } from "react"
2
+ import { AccordionTab } from "@openneuro/components/accordion"
3
+ import { AccordionWrap } from "@openneuro/components/accordion/"
4
+ import styled from "@emotion/styled"
5
+ import { toast } from "react-toastify"
6
+ import { useSearchParams } from "react-router-dom" // React Router for URL parsing
7
+
8
+ interface GitHubAuthButtonProps {
9
+ sync: Date | null
10
+ }
11
+
12
+ const GithubSyncDiv = styled.div`
13
+ .synced-btn {
14
+ border-radius: var(--border-radius-default);
15
+ padding: 2px 10px;
16
+ transition: background-color 0.3s;
17
+ border: 1px solid var(--current-theme-secondary);
18
+ margin: 10px;
19
+
20
+ &:hover {
21
+ background-color: var(--current-theme-primary-light);
22
+ color: var(--current-theme-primary);
23
+ }
24
+ &.active {
25
+ background-color: transparent;
26
+ color: var(--current-theme-primary);
27
+ border-color: var(--current-theme-primary-hover);
28
+ }
29
+ }
30
+
31
+ img.inline-icon {
32
+ height: 1.1em;
33
+ vertical-align: middle;
34
+ }
35
+
36
+ .on-accordion-wrapper {
37
+ margin-top: 5px;
38
+ .keyword-accordion {
39
+ .accordion-title {
40
+ position: absolute;
41
+ top: -25px;
42
+ left: 270px;
43
+ }
44
+ &.synced .accordion-title {
45
+ left: 430px;
46
+ }
47
+ }
48
+ }
49
+ `
50
+
51
+ export const GitHubAuthButton: React.FC<GitHubAuthButtonProps> = ({ sync }) => {
52
+ const lastSyncedText = sync ? `Last synced: ${sync.toLocaleString()}` : null
53
+ const [searchParams] = useSearchParams()
54
+ useEffect(() => {
55
+ const error = searchParams.get("error")
56
+
57
+ if (error === "orcid_required") {
58
+ toast.error("Please login with your ORCID account")
59
+ } else if (error) {
60
+ toast.error(error.replace(/_/g, " ")) // Replace underscores with spaces for readability
61
+ }
62
+
63
+ const success = searchParams.get("success")
64
+ if (success === "github_auth_success") {
65
+ toast.success("GitHub linked!")
66
+ }
67
+ }, [searchParams])
68
+
69
+ return (
70
+ <GithubSyncDiv>
71
+ <a
72
+ href="/crn/auth/github"
73
+ className="synced-btn"
74
+ data-testid="github-sync-button" // Added data-testid here
75
+ >
76
+ Link user data from <i className="fab fa-github"></i> GitHub
77
+ </a>
78
+ <span>
79
+ {lastSyncedText && (
80
+ <small className="synced-text">{lastSyncedText}</small>
81
+ )}
82
+ </span>
83
+ <AccordionWrap>
84
+ <AccordionTab
85
+ accordionStyle="plain"
86
+ label="?"
87
+ className={lastSyncedText
88
+ ? "keyword-accordion synced"
89
+ : "keyword-accordion"}
90
+ >
91
+ <span>
92
+ Link profile data from GitHub (avatar, institution, or location).
93
+ </span>
94
+ </AccordionTab>
95
+ </AccordionWrap>
96
+ </GithubSyncDiv>
97
+ )
98
+ }
@@ -58,23 +58,27 @@
58
58
  }
59
59
  }
60
60
  }
61
+
62
+ .resultsSummary{
63
+ width: 100%;
64
+ font-size: 13px;
65
+ margin: 10px 0 0;
66
+ }
61
67
 
62
68
  .userDSfilters {
63
69
  display: flex;
64
70
  justify-content: flex-end;
65
71
  flex-wrap: wrap;
66
- margin-bottom: 20px;
72
+ margin: 20px 0;
67
73
  padding-bottom:20px;
68
74
  align-items: center;
69
75
  z-index: 10000;
70
76
  position: relative;
71
77
  border-bottom: 2px solid #eee;
72
-
73
- input {
74
- min-width: 50%;
75
- margin-right: auto;
76
- }
77
-
78
+
79
+ .searchLink{
80
+ margin-right: auto;
81
+ }
78
82
  .filterDiv,
79
83
  .sortDiv {
80
84
  cursor: pointer;
@@ -144,10 +148,59 @@
144
148
  margin-left: 20px;
145
149
  }
146
150
  }
147
-
148
151
 
149
- .resultsSummary{
152
+ .userDatasetsWrapper{
153
+ margin-bottom: 50px;
154
+ }
155
+ .searchInputContainer {
156
+ position: relative;
157
+ display: flex;
158
+
159
+ .searchInputWrap{
160
+ position: relative;
150
161
  width: 100%;
151
- font-size: 13px;
152
- margin: 10px 0 0;
153
- }
162
+ .searchInput {
163
+ flex-grow: 1;
164
+ padding-right: 30px;
165
+ margin-right: auto;
166
+ width: 100%;
167
+ }
168
+
169
+
170
+ .clearSearchButton {
171
+ position: absolute;
172
+ top: 50%;
173
+ right: 5px; /* Adjust position as needed */
174
+ transform: translateY(-50%);
175
+ background: none;
176
+ border: none;
177
+ cursor: pointer;
178
+ font-size: 0.8em;
179
+ color: #888;
180
+ padding: 0;
181
+ width: 20px;
182
+ height: 20px;
183
+ display: flex;
184
+ align-items: center;
185
+ justify-content: center;
186
+
187
+ &:hover {
188
+ color: #333;
189
+ }
190
+ }}
191
+ .searchSubmitButton {
192
+ background-color: var(--current-theme-secondary);
193
+ color: #fff;
194
+ border-radius: var(--border-radius-default);
195
+ font-weight: bold;
196
+ margin: 0 0 0 5px;
197
+ font-size: 14px;
198
+ padding: 10px 15px;
199
+ border: 0;
200
+ &:hover {
201
+ background-color: var(--current-theme-primary);
202
+ }
203
+ }
204
+
205
+
206
+ }
@@ -13,7 +13,7 @@
13
13
  }
14
14
  span{
15
15
  font-weight: 700;
16
- margin-right: 10px;
16
+ margin: 0 10px;
17
17
  }
18
18
  }
19
19
  }