@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.
- package/package.json +3 -3
- package/src/client.jsx +1 -0
- package/src/scripts/components/button/button.scss +14 -0
- package/src/scripts/components/page/page.scss +2 -70
- package/src/scripts/components/search-page/SearchResultItem.tsx +3 -1
- package/src/scripts/components/search-page/search-page.scss +1 -13
- package/src/scripts/config.ts +6 -0
- package/src/scripts/dataset/files/__tests__/__snapshots__/file.spec.jsx.snap +2 -2
- package/src/scripts/dataset/files/file.tsx +2 -2
- package/src/scripts/dataset/mutations/__tests__/update-file.spec.tsx +126 -0
- package/src/scripts/dataset/mutations/update-file.jsx +20 -5
- package/src/scripts/dataset/routes/snapshot.tsx +1 -1
- package/src/scripts/errors/errorRoute.tsx +2 -0
- package/src/scripts/pages/admin/__tests__/users.spec.tsx +51 -18
- package/src/scripts/pages/admin/admin.jsx +2 -2
- package/src/scripts/pages/admin/user-fragment.ts +10 -3
- package/src/scripts/pages/admin/user-summary.tsx +100 -0
- package/src/scripts/pages/admin/user-tools.tsx +81 -58
- package/src/scripts/pages/admin/users.module.scss +277 -0
- package/src/scripts/pages/admin/users.tsx +351 -152
- package/src/scripts/queries/user.ts +120 -3
- package/src/scripts/queries/users.ts +247 -0
- package/src/scripts/routes.tsx +7 -15
- package/src/scripts/types/user-types.ts +12 -13
- package/src/scripts/uploader/file-select.tsx +42 -57
- package/src/scripts/uploader/upload-select.jsx +1 -1
- package/src/scripts/users/__tests__/dataset-card.spec.tsx +127 -0
- package/src/scripts/users/__tests__/user-account-view.spec.tsx +150 -67
- package/src/scripts/users/__tests__/user-card.spec.tsx +6 -17
- package/src/scripts/users/__tests__/user-query.spec.tsx +133 -38
- package/src/scripts/users/__tests__/user-routes.spec.tsx +156 -27
- package/src/scripts/users/__tests__/user-tabs.spec.tsx +7 -7
- package/src/scripts/users/components/edit-list.tsx +26 -5
- package/src/scripts/users/components/edit-string.tsx +40 -13
- package/src/scripts/users/components/editable-content.tsx +10 -3
- package/src/scripts/users/components/user-dataset-filters.tsx +205 -121
- package/src/scripts/users/dataset-card.tsx +3 -2
- package/src/scripts/users/github-auth-button.tsx +98 -0
- package/src/scripts/users/scss/datasetcard.module.scss +65 -12
- package/src/scripts/users/scss/useraccountview.module.scss +1 -1
- package/src/scripts/users/user-account-view.tsx +43 -34
- package/src/scripts/users/user-card.tsx +23 -22
- package/src/scripts/users/user-container.tsx +9 -5
- package/src/scripts/users/user-datasets-view.tsx +350 -40
- package/src/scripts/users/user-menu.tsx +4 -9
- package/src/scripts/users/user-notifications-view.tsx +9 -7
- package/src/scripts/users/user-query.tsx +3 -6
- package/src/scripts/users/user-routes.tsx +11 -5
- package/src/scripts/users/user-tabs.tsx +4 -2
- package/src/scripts/users/__tests__/datasest-card.spec.tsx +0 -201
- 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.
|
|
3
|
+
"version": "4.36.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.
|
|
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": "
|
|
78
|
+
"gitHead": "ac7fa1782779cfdf1f46c9a5add8865155177b99"
|
|
79
79
|
}
|
package/src/client.jsx
CHANGED
|
@@ -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
|
+
}
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
// admin page
|
|
28
28
|
.admin.route-wrapper {
|
|
29
29
|
margin: 20px auto;
|
|
30
|
-
max-width:
|
|
30
|
+
max-width: 1500px;
|
|
31
31
|
width: 90%;
|
|
32
32
|
.nav-pills.tabs {
|
|
33
33
|
display: flex;
|
|
@@ -39,80 +39,12 @@
|
|
|
39
39
|
display: block;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
display: inline-block;
|
|
44
|
-
appearance: none;
|
|
45
|
-
box-sizing: border-box;
|
|
46
|
-
border-radius: 0;
|
|
47
|
-
border: 0.1rem solid #dfdfdf;
|
|
48
|
-
padding: 10px 12px;
|
|
49
|
-
max-width: 100%;
|
|
50
|
-
line-height: 12px;
|
|
51
|
-
}
|
|
42
|
+
|
|
52
43
|
.header-wrap {
|
|
53
44
|
display: flex;
|
|
54
45
|
justify-content: space-between;
|
|
55
46
|
align-items: center;
|
|
56
47
|
}
|
|
57
|
-
|
|
58
|
-
.filters {
|
|
59
|
-
button {
|
|
60
|
-
margin: 0 10px;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
.users-panel-wrap {
|
|
64
|
-
.user-panel {
|
|
65
|
-
display: flex;
|
|
66
|
-
justify-content: space-between;
|
|
67
|
-
margin: 20px 0;
|
|
68
|
-
border: 1px solid #ddd;
|
|
69
|
-
border-radius: 4px;
|
|
70
|
-
padding: 10px;
|
|
71
|
-
> .user-col {
|
|
72
|
-
flex: auto;
|
|
73
|
-
label {
|
|
74
|
-
display: block;
|
|
75
|
-
font-weight: 600;
|
|
76
|
-
font-size: 14px;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
.user-panel-inner {
|
|
80
|
-
display: flex;
|
|
81
|
-
justify-content: space-between;
|
|
82
|
-
> .user-col {
|
|
83
|
-
flex: 1;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
.uc-summary .summary-data,
|
|
87
|
-
.uc-provider {
|
|
88
|
-
font-size: 12px;
|
|
89
|
-
margin: 0 0 10px;
|
|
90
|
-
}
|
|
91
|
-
.uc-name {
|
|
92
|
-
font-size: 20px;
|
|
93
|
-
display: flex;
|
|
94
|
-
align-items: flex-start;
|
|
95
|
-
.badge {
|
|
96
|
-
font-size: 12px;
|
|
97
|
-
color: #fff;
|
|
98
|
-
background: red;
|
|
99
|
-
display: inline-block;
|
|
100
|
-
padding: 5px;
|
|
101
|
-
border-radius: 4px;
|
|
102
|
-
font-weight: bold;
|
|
103
|
-
}
|
|
104
|
-
.dataset-tools-wrap-admin .tools {
|
|
105
|
-
display: flex;
|
|
106
|
-
button {
|
|
107
|
-
background-color: transparent;
|
|
108
|
-
border: 0;
|
|
109
|
-
outline: 0;
|
|
110
|
-
font-size: 12px;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
48
|
}
|
|
117
49
|
|
|
118
50
|
//api page
|
|
@@ -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
|
-
<
|
|
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
|
-
|
|
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 {
|
package/src/scripts/config.ts
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
path
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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 />} />
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
import { fireEvent, render, screen } from "@testing-library/react"
|
|
3
3
|
import { MockedProvider } from "@apollo/client/testing"
|
|
4
|
-
import { GET_USERS, UsersQuery } from "../users"
|
|
5
4
|
import { vi } from "vitest"
|
|
5
|
+
import { UserQuery } from "../../../users/user-query"
|
|
6
|
+
import { GET_USERS } from "../../../queries/users"
|
|
7
|
+
import { MemoryRouter, Route, Routes } from "react-router-dom"
|
|
6
8
|
|
|
7
9
|
// Mock admin login
|
|
8
10
|
vi.mock("../../../authentication/profile", (_importOriginal) => {
|
|
@@ -16,6 +18,21 @@ vi.mock("../../../authentication/profile", (_importOriginal) => {
|
|
|
16
18
|
}
|
|
17
19
|
})
|
|
18
20
|
|
|
21
|
+
// MOCK THE USERQUERY COMPONENT
|
|
22
|
+
|
|
23
|
+
vi.mock("../../../users/user-query", () => {
|
|
24
|
+
return {
|
|
25
|
+
UserQuery: vi.fn(() => (
|
|
26
|
+
<div data-testid="mock-user-query-admin-view">
|
|
27
|
+
<h3>Current Users (Mocked UserQuery)</h3>
|
|
28
|
+
<input type="text" placeholder="Search Name or Email" />
|
|
29
|
+
<div data-testid="user-list-container">
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
)),
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
19
36
|
const users = [
|
|
20
37
|
{
|
|
21
38
|
__typename: "User",
|
|
@@ -31,6 +48,10 @@ const users = [
|
|
|
31
48
|
]
|
|
32
49
|
|
|
33
50
|
describe("Users", () => {
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
vi.clearAllMocks()
|
|
53
|
+
})
|
|
54
|
+
|
|
34
55
|
it("renders users", async () => {
|
|
35
56
|
const mocks = [
|
|
36
57
|
{
|
|
@@ -44,25 +65,33 @@ describe("Users", () => {
|
|
|
44
65
|
|
|
45
66
|
render(
|
|
46
67
|
<MockedProvider mocks={mocks}>
|
|
47
|
-
<
|
|
68
|
+
<MemoryRouter initialEntries={["/admin/users"]}>
|
|
69
|
+
<Routes>
|
|
70
|
+
<Route path="/admin/users" element={<UserQuery />} />
|
|
71
|
+
</Routes>
|
|
72
|
+
</MemoryRouter>
|
|
48
73
|
</MockedProvider>,
|
|
49
74
|
)
|
|
50
75
|
|
|
51
|
-
expect(await screen.findByText("Current Users"))
|
|
52
|
-
|
|
76
|
+
expect(await screen.findByText("Current Users (Mocked UserQuery)"))
|
|
77
|
+
.toBeInTheDocument()
|
|
53
78
|
})
|
|
79
|
+
|
|
54
80
|
it("handles filtering users with no email", async () => {
|
|
55
|
-
const emailLessUsers = [
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
const emailLessUsers = [
|
|
82
|
+
...users,
|
|
83
|
+
{
|
|
84
|
+
__typename: "User",
|
|
85
|
+
id: "db3e7a0b-950b-4951-9059-c003ca3c1669",
|
|
86
|
+
name: "New User",
|
|
87
|
+
admin: false,
|
|
88
|
+
email: null,
|
|
89
|
+
blocked: false,
|
|
90
|
+
provider: "orcid",
|
|
91
|
+
lastSeen: "2019-09-24T19:26:07.704Z",
|
|
92
|
+
created: "2013-09-24T19:26:07.704Z",
|
|
93
|
+
},
|
|
94
|
+
]
|
|
66
95
|
const mocks = [
|
|
67
96
|
{
|
|
68
97
|
delay: 30,
|
|
@@ -75,7 +104,11 @@ describe("Users", () => {
|
|
|
75
104
|
|
|
76
105
|
render(
|
|
77
106
|
<MockedProvider mocks={mocks}>
|
|
78
|
-
<
|
|
107
|
+
<MemoryRouter initialEntries={["/admin/users"]}>
|
|
108
|
+
<Routes>
|
|
109
|
+
<Route path="/admin/users" element={<UserQuery />} />
|
|
110
|
+
</Routes>
|
|
111
|
+
</MemoryRouter>
|
|
79
112
|
</MockedProvider>,
|
|
80
113
|
)
|
|
81
114
|
|
|
@@ -83,7 +116,7 @@ describe("Users", () => {
|
|
|
83
116
|
fireEvent.change(input, { target: { value: "test" } })
|
|
84
117
|
fireEvent.keyDown(input, { key: "a" })
|
|
85
118
|
|
|
86
|
-
expect(await screen.findByText("Current Users"))
|
|
87
|
-
|
|
119
|
+
expect(await screen.findByText("Current Users (Mocked UserQuery)"))
|
|
120
|
+
.toBeInTheDocument()
|
|
88
121
|
})
|
|
89
122
|
})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React from "react"
|
|
4
4
|
import { Navigate, NavLink, Route, Routes } from "react-router-dom"
|
|
5
|
-
import
|
|
5
|
+
import { UsersPage } from "./users"
|
|
6
6
|
import FlaggedFiles from "./flagged-files.jsx"
|
|
7
7
|
import AdminUser from "../../authentication/admin-user.jsx"
|
|
8
8
|
|
|
@@ -28,7 +28,7 @@ class Dashboard extends React.Component {
|
|
|
28
28
|
</li>
|
|
29
29
|
</ul>
|
|
30
30
|
<Routes>
|
|
31
|
-
<Route path="/users" element={<
|
|
31
|
+
<Route path="/users" element={<UsersPage />} />
|
|
32
32
|
<Route path="/flagged-files" element={<FlaggedFiles />} />
|
|
33
33
|
<Route
|
|
34
34
|
path="/"
|
|
@@ -4,11 +4,18 @@ export const USER_FRAGMENT = gql`
|
|
|
4
4
|
fragment userFields on User {
|
|
5
5
|
id
|
|
6
6
|
name
|
|
7
|
+
admin
|
|
8
|
+
blocked
|
|
7
9
|
email
|
|
8
10
|
provider
|
|
9
|
-
admin
|
|
10
|
-
created
|
|
11
11
|
lastSeen
|
|
12
|
-
|
|
12
|
+
created
|
|
13
|
+
avatar
|
|
14
|
+
github
|
|
15
|
+
institution
|
|
16
|
+
location
|
|
17
|
+
modified
|
|
18
|
+
orcid
|
|
19
|
+
|
|
13
20
|
}
|
|
14
21
|
`
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import parseISO from "date-fns/parseISO"
|
|
3
|
+
import formatDistanceToNow from "date-fns/formatDistanceToNow"
|
|
4
|
+
import { formatDate } from "../../utils/date.js"
|
|
5
|
+
import type { User } from "../../types/user-types"
|
|
6
|
+
import styles from "./users.module.scss"
|
|
7
|
+
import { Tooltip } from "../../components/tooltip/Tooltip"
|
|
8
|
+
import { UserTools } from "./user-tools.js"
|
|
9
|
+
|
|
10
|
+
interface UserSummaryProps {
|
|
11
|
+
user: User
|
|
12
|
+
refetchCurrentPage: () => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const UserSummary = ({ user, refetchCurrentPage }: UserSummaryProps) => {
|
|
16
|
+
const adminBadge = user.admin
|
|
17
|
+
? (
|
|
18
|
+
<Tooltip tooltip="Admin">
|
|
19
|
+
<span className={`${styles.badge} ${styles.admin}`}>
|
|
20
|
+
<i className="fa fa-star"></i>
|
|
21
|
+
</span>
|
|
22
|
+
</Tooltip>
|
|
23
|
+
)
|
|
24
|
+
: null
|
|
25
|
+
const blockedBadge = user.blocked
|
|
26
|
+
? (
|
|
27
|
+
<Tooltip tooltip="Blocked">
|
|
28
|
+
<span className={`${styles.badge} ${styles.blocked}`}>
|
|
29
|
+
<i className="fa fa-lock"></i>
|
|
30
|
+
</span>
|
|
31
|
+
</Tooltip>
|
|
32
|
+
)
|
|
33
|
+
: (
|
|
34
|
+
<Tooltip tooltip="Active">
|
|
35
|
+
<span className={`${styles.badge}`}>
|
|
36
|
+
<i className="fa fa-lock-open"></i>
|
|
37
|
+
</span>
|
|
38
|
+
</Tooltip>
|
|
39
|
+
)
|
|
40
|
+
const userEmail = <a href={`mailto:${user.email}`}>{user.email}</a>
|
|
41
|
+
const userOrcid = <a href={`/user/${user.orcid}`}>{user.orcid}</a>
|
|
42
|
+
return (
|
|
43
|
+
<div className={styles.gridRow}>
|
|
44
|
+
<div className={`${styles.gtCell} ${styles.colLarge}`}>
|
|
45
|
+
<h3>
|
|
46
|
+
{user.name}
|
|
47
|
+
{adminBadge && adminBadge}
|
|
48
|
+
{blockedBadge}
|
|
49
|
+
<br />
|
|
50
|
+
</h3>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div className={`${styles.gtCell} ${styles.colXLarge}`}>
|
|
54
|
+
<div>{user.email && userEmail}</div>
|
|
55
|
+
{user.orcid && userOrcid}
|
|
56
|
+
<span>
|
|
57
|
+
Provider:{" "}
|
|
58
|
+
<b style={{ textTransform: "uppercase" }}>{user.provider}</b>
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
<div className={`${styles.gtCell} ${styles.colSmall}`}>
|
|
62
|
+
<>
|
|
63
|
+
<div>
|
|
64
|
+
<b>{formatDate(user.created)}</b>
|
|
65
|
+
</div>
|
|
66
|
+
Created
|
|
67
|
+
</>
|
|
68
|
+
</div>
|
|
69
|
+
<div className={`${styles.gtCell} ${styles.colSmall}`}>
|
|
70
|
+
{user.lastSeen !== null && (
|
|
71
|
+
<>
|
|
72
|
+
<div>
|
|
73
|
+
<b>{formatDistanceToNow(parseISO(user.lastSeen))} ago</b>
|
|
74
|
+
</div>
|
|
75
|
+
Last Login
|
|
76
|
+
</>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
<div className={`${styles.gtCell} ${styles.colSmall}`}>
|
|
80
|
+
{user.modified !== null && (
|
|
81
|
+
<>
|
|
82
|
+
<div>
|
|
83
|
+
<b>{formatDistanceToNow(parseISO(user.modified))} ago</b>
|
|
84
|
+
</div>
|
|
85
|
+
Modified
|
|
86
|
+
</>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div className={`${styles.gtCell} ${styles.colFlex}`}>
|
|
91
|
+
<UserTools
|
|
92
|
+
user={user}
|
|
93
|
+
refetchCurrentPage={refetchCurrentPage}
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default UserSummary
|