@openneuro/app 4.35.0-alpha.1 → 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 (42) 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/pages/orcid-link.tsx +9 -0
  14. package/src/scripts/queries/user.ts +120 -3
  15. package/src/scripts/types/user-types.ts +11 -13
  16. package/src/scripts/uploader/file-select.tsx +42 -57
  17. package/src/scripts/uploader/upload-select.jsx +1 -1
  18. package/src/scripts/users/__tests__/dataset-card.spec.tsx +127 -0
  19. package/src/scripts/users/__tests__/user-account-view.spec.tsx +150 -67
  20. package/src/scripts/users/__tests__/user-card.spec.tsx +6 -17
  21. package/src/scripts/users/__tests__/user-query.spec.tsx +133 -38
  22. package/src/scripts/users/__tests__/user-routes.spec.tsx +156 -27
  23. package/src/scripts/users/__tests__/user-tabs.spec.tsx +7 -7
  24. package/src/scripts/users/components/edit-list.tsx +26 -5
  25. package/src/scripts/users/components/edit-string.tsx +40 -13
  26. package/src/scripts/users/components/editable-content.tsx +10 -3
  27. package/src/scripts/users/components/user-dataset-filters.tsx +205 -121
  28. package/src/scripts/users/dataset-card.tsx +3 -2
  29. package/src/scripts/users/github-auth-button.tsx +98 -0
  30. package/src/scripts/users/scss/datasetcard.module.scss +65 -12
  31. package/src/scripts/users/scss/useraccountview.module.scss +1 -1
  32. package/src/scripts/users/user-account-view.tsx +43 -34
  33. package/src/scripts/users/user-card.tsx +12 -17
  34. package/src/scripts/users/user-container.tsx +9 -5
  35. package/src/scripts/users/user-datasets-view.tsx +350 -40
  36. package/src/scripts/users/user-menu.tsx +4 -9
  37. package/src/scripts/users/user-notifications-view.tsx +9 -7
  38. package/src/scripts/users/user-query.tsx +3 -3
  39. package/src/scripts/users/user-routes.tsx +11 -5
  40. package/src/scripts/users/user-tabs.tsx +4 -2
  41. package/src/scripts/users/__tests__/datasest-card.spec.tsx +0 -201
  42. package/src/scripts/users/fragments/query.js +0 -42
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openneuro/app",
3
- "version": "4.35.0-alpha.1",
3
+ "version": "4.36.0-alpha.0",
4
4
  "description": "React JS web frontend for the OpenNeuro platform.",
5
5
  "license": "MIT",
6
6
  "main": "public/client.js",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "author": "Squishymedia",
16
16
  "dependencies": {
17
- "@apollo/client": "3.11.8",
17
+ "@apollo/client": "3.13.8",
18
18
  "@artsy/fresnel": "^1.3.1",
19
19
  "@bids/validator": "npm:@jsr/bids__validator@^2.0.3",
20
20
  "@emotion/react": "11.11.1",
@@ -75,5 +75,5 @@
75
75
  "publishConfig": {
76
76
  "access": "public"
77
77
  },
78
- "gitHead": "ac9c013877e5d9c1fa9615a177f79e0da0747f84"
78
+ "gitHead": "0d754964177331113f4279c48d6c0fae0f59adc2"
79
79
  }
package/src/client.jsx CHANGED
@@ -30,6 +30,7 @@ const client = new ApolloClient({
30
30
  },
31
31
  },
32
32
  }),
33
+ connectToDevTools: config.sentry.environment !== "production",
33
34
  })
34
35
 
35
36
  container.render(
@@ -138,3 +138,17 @@
138
138
  }
139
139
  }
140
140
  }
141
+
142
+ .load-more {
143
+ .on-button {
144
+ display: block;
145
+ border: 1px solid;
146
+ width: 100%;
147
+ background-color: #fff;
148
+ transition: background-color 0.3s;
149
+ &:hover {
150
+ text-decoration: none;
151
+ background-color: $newspaper;
152
+ }
153
+ }
154
+ }
@@ -127,6 +127,8 @@ export const SearchResultItem = ({
127
127
  const hasEdit = hasEditPermissions(node.permissions, profileSub) || isAdmin
128
128
 
129
129
  const heading = node.latestSnapshot.description?.Name
130
+ ? node.latestSnapshot.description?.Name
131
+ : node.id
130
132
  const summary = node.latestSnapshot?.summary
131
133
  const datasetId = node.id
132
134
  const numSessions = summary?.sessions.length > 0 ? summary.sessions.length : 1
@@ -134,7 +136,7 @@ export const SearchResultItem = ({
134
136
  const accessionNumber = (
135
137
  <span className="result-summary-meta">
136
138
  <strong>Openneuro Accession Number:</strong>
137
- <span>{node.id}</span>
139
+ <Link to={"/datasets/" + datasetId}>{node.id}</Link>
138
140
  </span>
139
141
  )
140
142
  const sessions = (
@@ -302,19 +302,7 @@
302
302
  border-bottom: 1px solid $newspaper;
303
303
  }
304
304
  }
305
- .load-more {
306
- .on-button {
307
- display: block;
308
- border: 1px solid;
309
- width: 100%;
310
- background-color: #fff;
311
- transition: background-color 0.3s;
312
- &:hover {
313
- text-decoration: none;
314
- background-color: $newspaper;
315
- }
316
- }
317
- }
305
+
318
306
  }
319
307
 
320
308
  .search-sort {
@@ -15,6 +15,9 @@ export interface OpenNeuroConfig {
15
15
  clientID: string
16
16
  ORCID_API_ENDPOINT: string
17
17
  }
18
+ github?: {
19
+ clientID: string
20
+ }
18
21
  globus?: {
19
22
  clientID: string
20
23
  }
@@ -48,6 +51,9 @@ export const config: OpenNeuroConfig = {
48
51
  clientID: globalThis.OpenNeuroConfig.ORCID_CLIENT_ID,
49
52
  ORCID_API_ENDPOINT: globalThis.OpenNeuroConfig.ORCID_API_ENDPOINT,
50
53
  },
54
+ github: {
55
+ clientID: globalThis.OpenNeuroConfig.GITHUB_CLIENT_ID,
56
+ },
51
57
  },
52
58
  analytics: {
53
59
  trackingIds: globalThis.OpenNeuroConfig.GOOGLE_TRACKING_IDS.split(",").map(
@@ -29,7 +29,7 @@ exports[`File component > renders for dataset snapshots 1`] = `
29
29
  >
30
30
  <a
31
31
  aria-label="download file"
32
- download=""
32
+ download="README"
33
33
  href="/crn/datasets/ds001/snapshots/1.0.0/files/README"
34
34
  >
35
35
  <i
@@ -90,7 +90,7 @@ exports[`File component > renders with common props 1`] = `
90
90
  >
91
91
  <a
92
92
  aria-label="download file"
93
- download=""
93
+ download="README"
94
94
  href="/crn/datasets/ds001/files/README"
95
95
  >
96
96
  <i
@@ -149,7 +149,7 @@ const File = ({
149
149
  <a
150
150
  href={urls?.[0] ||
151
151
  apiPath(datasetId, snapshotTag, filePath(path, filename))}
152
- download
152
+ download={filename}
153
153
  aria-label="download file"
154
154
  >
155
155
  <i className="fa fa-download" />
@@ -170,7 +170,7 @@ const File = ({
170
170
  {editMode && (
171
171
  <Media greaterThanOrEqual="medium">
172
172
  <Tooltip tooltip="Update">
173
- <UpdateFile datasetId={datasetId} path={path}>
173
+ <UpdateFile datasetId={datasetId} path={path} filename={filename}>
174
174
  <i className="fa fa-cloud-upload" />
175
175
  </UpdateFile>
176
176
  </Tooltip>
@@ -0,0 +1,126 @@
1
+ import React from "react"
2
+ import { fireEvent, render, screen } from "@testing-library/react"
3
+ import { vi } from "vitest"
4
+ import UpdateFile from "../update-file"
5
+ import UploaderContext from "../../../uploader/uploader-context.js"
6
+
7
+ describe("UpdateFile Component", () => {
8
+ const mockResumeDataset = vi.fn()
9
+ const mockUploader = {
10
+ resumeDataset: vi.fn(() => mockResumeDataset),
11
+ }
12
+
13
+ const datasetId = "ds000001"
14
+ const path = "sub-01/anat"
15
+
16
+ const renderComponent = (props = {}, uploader = mockUploader) => {
17
+ return render(
18
+ <UploaderContext.Provider value={uploader}>
19
+ <UpdateFile datasetId={datasetId} path={path} {...props}>
20
+ <button>Upload Button</button>
21
+ </UpdateFile>
22
+ </UploaderContext.Provider>,
23
+ )
24
+ }
25
+
26
+ it("renders children correctly", () => {
27
+ renderComponent()
28
+ expect(screen.getByText("Upload Button")).toBeInTheDocument()
29
+ })
30
+
31
+ it("sets webkitdirectory attribute when directory prop is true", () => {
32
+ renderComponent({ directory: true })
33
+ const inputElement = screen.getByRole("button", { name: "Upload Button" })
34
+ .previousSibling // The input is before the children
35
+ expect(inputElement).toHaveAttribute("webkitdirectory", "true")
36
+ })
37
+
38
+ it("does not set webkitdirectory attribute when directory prop is false", () => {
39
+ renderComponent({ directory: false })
40
+ const inputElement = screen.getByRole("button", { name: "Upload Button" })
41
+ .previousSibling
42
+ expect(inputElement).not.toHaveAttribute("webkitdirectory")
43
+ })
44
+
45
+ it("sets multiple attribute when multiple prop is true", () => {
46
+ renderComponent({ multiple: true })
47
+ const inputElement = screen.getByRole("button", { name: "Upload Button" })
48
+ .previousSibling
49
+ expect(inputElement).toHaveAttribute("multiple")
50
+ })
51
+
52
+ it("does not set multiple attribute when multiple prop is false", () => {
53
+ renderComponent({ multiple: false })
54
+ const inputElement = screen.getByRole("button", { name: "Upload Button" })
55
+ .previousSibling
56
+ expect(inputElement).not.toHaveAttribute("multiple")
57
+ })
58
+
59
+ describe("onChange event", () => {
60
+ const file1 = new File(["content1"], "original1.txt", {
61
+ type: "text/plain",
62
+ })
63
+ const file2 = new File(["content2"], "original2.txt", {
64
+ type: "text/plain",
65
+ })
66
+
67
+ it("calls uploader.resumeDataset with renamed file when filename is provided and one file is selected", () => {
68
+ const customFilename = "new_filename.txt"
69
+ renderComponent({ filename: customFilename })
70
+ const inputElement = screen.getByRole("button", { name: "Upload Button" })
71
+ .previousSibling
72
+
73
+ fireEvent.change(inputElement, {
74
+ target: { files: [file1] },
75
+ })
76
+
77
+ expect(mockUploader.resumeDataset).toHaveBeenCalledWith(
78
+ datasetId,
79
+ path,
80
+ false,
81
+ )
82
+ expect(mockResumeDataset).toHaveBeenCalledTimes(1)
83
+ const calledWithArgs = mockResumeDataset.mock.calls[0][0]
84
+ expect(calledWithArgs.files).toHaveLength(1)
85
+ expect(calledWithArgs.files[0].name).toBe(customFilename)
86
+ expect(calledWithArgs.files[0].type).toBe(file1.type)
87
+ })
88
+
89
+ it("calls uploader.resumeDataset with original files when filename is not provided", () => {
90
+ renderComponent()
91
+ const inputElement = screen.getByRole("button", { name: "Upload Button" })
92
+ .previousSibling
93
+
94
+ fireEvent.change(inputElement, {
95
+ target: { files: [file1, file2] },
96
+ })
97
+
98
+ expect(mockUploader.resumeDataset).toHaveBeenCalledWith(
99
+ datasetId,
100
+ path,
101
+ false,
102
+ )
103
+ expect(mockResumeDataset).toHaveBeenCalledTimes(1)
104
+ expect(mockResumeDataset).toHaveBeenCalledWith({ files: [file1, file2] })
105
+ })
106
+
107
+ it("calls uploader.resumeDataset with original files when multiple files are selected, even if filename is provided", () => {
108
+ const customFilename = "new_filename.txt"
109
+ renderComponent({ filename: customFilename, multiple: true })
110
+ const inputElement = screen.getByRole("button", { name: "Upload Button" })
111
+ .previousSibling
112
+
113
+ fireEvent.change(inputElement, {
114
+ target: { files: [file1, file2] },
115
+ })
116
+
117
+ expect(mockUploader.resumeDataset).toHaveBeenCalledWith(
118
+ datasetId,
119
+ path,
120
+ false,
121
+ )
122
+ expect(mockResumeDataset).toHaveBeenCalledTimes(1)
123
+ expect(mockResumeDataset).toHaveBeenCalledWith({ files: [file1, file2] })
124
+ })
125
+ })
126
+ })
@@ -7,6 +7,7 @@ const UpdateFile = ({
7
7
  directory = false,
8
8
  multiple = false,
9
9
  path = null,
10
+ filename = null,
10
11
  children,
11
12
  }) => {
12
13
  return (
@@ -18,11 +19,25 @@ const UpdateFile = ({
18
19
  className="update-file"
19
20
  onChange={(e) => {
20
21
  e.preventDefault()
21
- uploader.resumeDataset(
22
- datasetId,
23
- path,
24
- false,
25
- )({ files: e.target.files })
22
+ if (filename && e.target.files.length === 1) {
23
+ // In the case that a single file was selected,
24
+ // name that file based on the original path and not the client side name.
25
+ const target = e.target.files[0]
26
+ const files = [
27
+ new File([target], filename, { type: target.type }),
28
+ ]
29
+ uploader.resumeDataset(
30
+ datasetId,
31
+ path,
32
+ false,
33
+ )({ files })
34
+ } else {
35
+ uploader.resumeDataset(
36
+ datasetId,
37
+ path,
38
+ false,
39
+ )({ files: e.target.files })
40
+ }
26
41
  }}
27
42
  webkitdirectory={directory ? "true" : undefined}
28
43
  multiple={multiple && true}
@@ -18,7 +18,7 @@ const FormRow = styled.div`
18
18
  export const NoErrors = ({ validation, authors, children }) => {
19
19
  const noErrors = validation?.errors === 0
20
20
  // zero authors will cause DOI minting to fail
21
- const hasAuthor = authors.length > 0
21
+ const hasAuthor = authors?.length > 0
22
22
  if (noErrors && hasAuthor) {
23
23
  return children
24
24
  } else {
@@ -6,12 +6,14 @@ import { Route, Routes } from "react-router-dom"
6
6
  import OrcidGeneral from "./orcid/general.jsx"
7
7
  import { OrcidEmailWarning } from "./orcid/email-warning.js"
8
8
  import FourOFourPage from "./404page.js"
9
+ import FourOThreePage from "./403page.js"
9
10
 
10
11
  function ErrorRoute() {
11
12
  return (
12
13
  <div className="container errors">
13
14
  <div className="panel">
14
15
  <Routes>
16
+ <Route path="github" element={<FourOThreePage />} />
15
17
  <Route path="orcid" element={<OrcidGeneral />} />
16
18
  <Route path="email-warning" element={<OrcidEmailWarning />} />
17
19
  <Route path="*" element={<FourOFourPage />} />
@@ -36,6 +36,15 @@ export function OrcidLinkPage() {
36
36
  have will be merged into the combined OpenNeuro account linked to your
37
37
  ORCID iD.
38
38
  </p>
39
+ <p>
40
+ </p>
41
+ <p>
42
+ Please see{" "}
43
+ <a href="https://docs.openneuro.org/orcid.html">our documentation</a>
44
+ {" "}
45
+ for additional details on how we use ORCID data and how to link your
46
+ account.
47
+ </p>
39
48
  <h3>Why are we making this change?</h3>
40
49
  <p>
41
50
  ORCID allows richer researcher metadata for contributions and
@@ -20,6 +20,8 @@ export const GET_USER = gql`
20
20
  created
21
21
  lastSeen
22
22
  blocked
23
+ githubSynced
24
+ github
23
25
  }
24
26
  }
25
27
  `
@@ -45,17 +47,132 @@ export const UPDATE_USER = gql`
45
47
  }
46
48
  `
47
49
 
50
+ export const ADVANCED_SEARCH_DATASETS_QUERY = gql`
51
+ query advancedSearchDatasets(
52
+ $query: JSON!
53
+ $cursor: String
54
+ $allDatasets: Boolean
55
+ $datasetStatus: String
56
+ $sortBy: JSON
57
+ $first: Int!
58
+ ) {
59
+ datasets: advancedSearch(
60
+ query: $query
61
+ allDatasets: $allDatasets
62
+ datasetStatus: $datasetStatus
63
+ sortBy: $sortBy
64
+ first: $first
65
+ after: $cursor
66
+ ) {
67
+ edges {
68
+ id
69
+ node {
70
+ id
71
+ created
72
+ name
73
+ uploader {
74
+ id
75
+ name
76
+ orcid
77
+ }
78
+ public
79
+ permissions {
80
+ id
81
+ userPermissions {
82
+ userId
83
+ level
84
+ access: level
85
+ user {
86
+ id
87
+ name
88
+ email
89
+ provider
90
+ }
91
+ }
92
+ }
93
+ metadata {
94
+ ages
95
+ }
96
+ latestSnapshot {
97
+ size
98
+ summary {
99
+ modalities
100
+ secondaryModalities
101
+ sessions
102
+ subjects
103
+ subjectMetadata {
104
+ participantId
105
+ age
106
+ sex
107
+ group
108
+ }
109
+ tasks
110
+ size
111
+ totalFiles
112
+ dataProcessed
113
+ pet {
114
+ BodyPart
115
+ ScannerManufacturer
116
+ ScannerManufacturersModelName
117
+ TracerName
118
+ TracerRadionuclide
119
+ }
120
+ }
121
+ issues {
122
+ severity
123
+ }
124
+ validation {
125
+ errors
126
+ warnings
127
+ }
128
+ description {
129
+ Name
130
+ Authors
131
+ }
132
+ }
133
+ analytics {
134
+ views
135
+ downloads
136
+ }
137
+ stars {
138
+ userId
139
+ datasetId
140
+ }
141
+ followers {
142
+ userId
143
+ datasetId
144
+ }
145
+ snapshots {
146
+ id
147
+ created
148
+ tag
149
+ }
150
+ }
151
+ }
152
+ pageInfo {
153
+ startCursor
154
+ endCursor
155
+ hasPreviousPage
156
+ hasNextPage
157
+ count
158
+ }
159
+ }
160
+ }
161
+ `
162
+
48
163
  // Reusable hook to fetch user data
49
- export const useUser = () => {
164
+ export const useUser = (userId?: string) => {
50
165
  const [cookies] = useCookies()
51
166
  const profile = getProfile(cookies)
52
167
  const profileSub = profile?.sub
53
168
 
169
+ const finalUserId = userId || profileSub
170
+
54
171
  const { data: userData, loading: userLoading, error: userError } = useQuery(
55
172
  GET_USER,
56
173
  {
57
- variables: { userId: profileSub },
58
- skip: !profileSub,
174
+ variables: { userId: finalUserId },
175
+ skip: !finalUserId,
59
176
  },
60
177
  )
61
178
 
@@ -8,27 +8,25 @@ export interface User {
8
8
  avatar?: string
9
9
  orcid?: string
10
10
  links?: string[]
11
+ githubSynced?: Date
12
+ admin?: boolean
13
+ provider?: string
14
+ created?: Date
15
+ lastSeen?: Date
16
+ blocked?: boolean
11
17
  }
12
18
 
13
19
  export interface UserRoutesProps {
14
- user: User
20
+ orcidUser: User
15
21
  hasEdit: boolean
16
22
  isUser: boolean
17
23
  }
18
24
  export interface UserCardProps {
19
- user: User
25
+ orcidUser: User
20
26
  }
21
27
 
22
28
  export interface UserAccountViewProps {
23
- user: {
24
- name: string
25
- email: string
26
- orcid?: string
27
- links?: string[]
28
- location?: string
29
- institution?: string
30
- github?: string
31
- }
29
+ orcidUser: User
32
30
  }
33
31
 
34
32
  export interface Dataset {
@@ -63,12 +61,12 @@ export interface DatasetCardProps {
63
61
  }
64
62
 
65
63
  export interface UserDatasetsViewProps {
66
- user: User
64
+ orcidUser: User
67
65
  hasEdit: boolean
68
66
  }
69
67
 
70
68
  export interface AccountContainerProps {
71
- user: User
69
+ orcidUser: User
72
70
  hasEdit: boolean
73
71
  isUser: boolean
74
72
  }
@@ -1,73 +1,58 @@
1
- // dependencies -------------------------------------------------------
2
-
3
1
  import React from "react"
4
- import PropTypes from "prop-types"
5
2
 
6
- type UploadProps = {
3
+ type UploadFileSelectProps = {
7
4
  resume?: boolean
8
5
  onClick?: React.MouseEventHandler<HTMLButtonElement>
9
- onChange?: (e?: { files: File[] }) => void
6
+ onChange: (e?: { files: File[] }) => void
10
7
  disabled?: boolean
11
8
  }
12
9
 
13
- class Upload extends React.Component<UploadProps> {
14
- public static propTypes = {}
15
- // life cycle events --------------------------------------------------
16
- render(): React.ReactNode {
17
- const resumeIcon = (
18
- <span>
19
- <i className="fa fa-repeat" />
20
- &nbsp;
21
- </span>
22
- )
23
- const icon = this.props.resume ? resumeIcon : null
24
- const text = this.props.resume ? "Resume" : "Select folder"
25
-
26
- return (
27
- <div
28
- className={"fileupload-btn" + (this.props.disabled ? " disabled" : "")}
29
- >
30
- <span>
31
- {icon}
32
- {text}
33
- </span>
34
- <input
35
- type="file"
36
- id="multifile-select"
37
- className="multifile-select-btn"
38
- onClick={this._click.bind(this)}
39
- onChange={this._onFileSelect.bind(this)}
40
- webkitdirectory="true"
41
- directory="true"
42
- disabled={this.props.disabled}
43
- />
44
- </div>
45
- )
46
- }
47
-
48
- // custom methods -----------------------------------------------------
49
-
50
- _click(e): void {
10
+ export const UploadFileSelect: React.FC<UploadFileSelectProps> = (
11
+ { resume, onClick, onChange, disabled },
12
+ ) => {
13
+ const handleClick = (e: React.MouseEvent<HTMLInputElement>): void => {
51
14
  e.stopPropagation()
52
- e.target.value = null
53
- if (this.props.onClick) {
54
- this.props.onClick(e)
15
+ // Reset the input value to allow selecting the same file/folder again
16
+ if (e.target instanceof HTMLInputElement) {
17
+ e.target.value = ""
55
18
  }
19
+ if (onClick) onClick(e as React.MouseEvent<HTMLButtonElement>)
56
20
  }
57
21
 
58
- _onFileSelect(e): void {
59
- if (e.target && e.target.files.length > 0) {
60
- const files = e.target.files
61
- this.props.onChange({ files })
22
+ const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>): void => {
23
+ if (e.target && e.target.files && e.target.files.length > 0) {
24
+ const files = Array.from(e.target.files)
25
+ onChange({ files })
62
26
  }
63
27
  }
64
- }
65
28
 
66
- Upload.propTypes = {
67
- resume: PropTypes.bool,
68
- onClick: PropTypes.func,
69
- onChange: PropTypes.func,
70
- disabled: PropTypes.bool,
29
+ const resumeIcon = (
30
+ <span>
31
+ <i className="fa fa-repeat" />
32
+ &nbsp;
33
+ </span>
34
+ )
35
+ const icon = resume ? resumeIcon : null
36
+ const text = resume ? "Resume" : "Select folder"
37
+
38
+ return (
39
+ <div className={`fileupload-btn${disabled ? " disabled" : ""}`}>
40
+ <span>
41
+ {icon}
42
+ {text}
43
+ </span>
44
+ <input
45
+ type="file"
46
+ id="multifile-select"
47
+ className="multifile-select-btn"
48
+ onClick={handleClick}
49
+ onChange={handleFileSelect}
50
+ webkitdirectory="true"
51
+ directory="true"
52
+ disabled={disabled}
53
+ />
54
+ </div>
55
+ )
71
56
  }
72
57
 
73
- export default Upload
58
+ export default UploadFileSelect
@@ -6,7 +6,7 @@ import { useUser } from "../queries/user"
6
6
  const UploadSelect = () => {
7
7
  const { user, loading, error } = useUser()
8
8
  const disabled = loading || Boolean(error) || !user || !user.email
9
- const noEmail = !user.email
9
+ const noEmail = !(user?.email)
10
10
  return (
11
11
  <div>
12
12
  <UploaderContext.Consumer>