@openneuro/app 4.34.2 → 4.35.0-alpha.1

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 (50) hide show
  1. package/package.json +4 -4
  2. package/src/scripts/authentication/profile.ts +3 -5
  3. package/src/scripts/common/containers/header.tsx +1 -2
  4. package/src/scripts/common/partials/freshdesk-widget.jsx +5 -1
  5. package/src/scripts/components/header/Header.tsx +4 -10
  6. package/src/scripts/components/header/LandingExpandedHeader.tsx +11 -9
  7. package/src/scripts/components/header/header.scss +4 -2
  8. package/src/scripts/components/logo/Logo.tsx +1 -1
  9. package/src/scripts/components/modal/UserLoginModal.tsx +12 -11
  10. package/src/scripts/components/modal/__tests__/UserLoginModal.spec.tsx +1 -1
  11. package/src/scripts/components/page/Page.tsx +1 -2
  12. package/src/scripts/components/scss/upload-modal.scss +6 -0
  13. package/src/scripts/components/search-page/SearchResultItem.tsx +14 -13
  14. package/src/scripts/components/search-page/SearchResultsList.tsx +0 -19
  15. package/src/scripts/dataset/draft-container.tsx +0 -1
  16. package/src/scripts/dataset/fragments/__tests__/dataset-history.spec.tsx +155 -0
  17. package/src/scripts/dataset/fragments/dataset-history.jsx +6 -7
  18. package/src/scripts/dataset/snapshot-container.tsx +13 -11
  19. package/src/scripts/errors/errorRoute.tsx +2 -0
  20. package/src/scripts/errors/freshdesk-widget.jsx +5 -1
  21. package/src/scripts/errors/orcid/email-warning.tsx +21 -0
  22. package/src/scripts/index.tsx +15 -2
  23. package/src/scripts/pages/__tests__/orcid-link.spec.tsx +13 -0
  24. package/src/scripts/pages/orcid-link.tsx +60 -0
  25. package/src/scripts/queries/user.ts +71 -0
  26. package/src/scripts/routes.tsx +2 -0
  27. package/src/scripts/scss/variables.scss +13 -9
  28. package/src/scripts/search/search-container.tsx +2 -12
  29. package/src/scripts/search/search-params-ctx.tsx +6 -3
  30. package/src/scripts/search/use-search-results.tsx +1 -1
  31. package/src/scripts/types/user-types.ts +2 -0
  32. package/src/scripts/uploader/file-select.tsx +3 -1
  33. package/src/scripts/uploader/upload-select.jsx +40 -24
  34. package/src/scripts/users/__tests__/user-account-view.spec.tsx +36 -22
  35. package/src/scripts/users/__tests__/user-query.spec.tsx +3 -3
  36. package/src/scripts/users/__tests__/user-routes.spec.tsx +28 -11
  37. package/src/scripts/users/__tests__/user-tabs.spec.tsx +12 -9
  38. package/src/scripts/users/scss/user-menu.scss +133 -0
  39. package/src/scripts/users/scss/usernotifications.module.scss +1 -1
  40. package/src/scripts/users/user-account-view.tsx +35 -21
  41. package/src/scripts/users/user-container.tsx +2 -1
  42. package/src/scripts/users/user-menu.tsx +114 -0
  43. package/src/scripts/users/user-notification-accordion.tsx +2 -2
  44. package/src/scripts/users/user-query.tsx +6 -36
  45. package/src/scripts/users/user-routes.tsx +7 -5
  46. package/src/scripts/users/user-tabs.tsx +4 -3
  47. package/src/scripts/validation/__tests__/__snapshots__/validation-issues.spec.tsx.snap +10 -1
  48. package/src/scripts/validation/validation-issues.tsx +7 -3
  49. package/src/scripts/components/user/UserMenu.tsx +0 -72
  50. package/src/scripts/components/user/user-menu.scss +0 -88
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openneuro/app",
3
- "version": "4.34.2",
3
+ "version": "4.35.0-alpha.1",
4
4
  "description": "React JS web frontend for the OpenNeuro platform.",
5
5
  "license": "MIT",
6
6
  "main": "public/client.js",
@@ -19,7 +19,7 @@
19
19
  "@bids/validator": "npm:@jsr/bids__validator@^2.0.3",
20
20
  "@emotion/react": "11.11.1",
21
21
  "@emotion/styled": "11.11.0",
22
- "@niivue/niivue": "0.45.1",
22
+ "@niivue/niivue": "0.57.0",
23
23
  "@openneuro/components": "^4.33.4",
24
24
  "@sentry/react": "^8.25.0",
25
25
  "@tanstack/react-table": "^8.9.3",
@@ -69,11 +69,11 @@
69
69
  "sass": "^1.32.8",
70
70
  "stream-browserify": "^3.0.0",
71
71
  "typescript": "5.6.3",
72
- "vite": "5.4.18",
72
+ "vite": "5.4.19",
73
73
  "vitest": "2.1.2"
74
74
  },
75
75
  "publishConfig": {
76
76
  "access": "public"
77
77
  },
78
- "gitHead": "6420eabb1a1b5068990523eb57991597faffb73e"
78
+ "gitHead": "ac9c013877e5d9c1fa9615a177f79e0da0747f84"
79
79
  }
@@ -2,12 +2,10 @@ import jwtDecode from "jwt-decode"
2
2
 
3
3
  export interface OpenNeuroTokenProfile {
4
4
  sub: string
5
- email: string
6
- provider: string
7
- name: string
8
5
  admin: boolean
9
6
  iat: number
10
7
  exp: number
8
+ scopes?: string[]
11
9
  }
12
10
 
13
11
  /**
@@ -21,9 +19,9 @@ export const parseJwt = jwtDecode
21
19
  */
22
20
  export function getProfile(cookies): OpenNeuroTokenProfile {
23
21
  const accessToken = cookies["accessToken"]
24
- return accessToken ? parseJwt(accessToken) : null
22
+ if (!accessToken) return null
23
+ return parseJwt(accessToken) as OpenNeuroTokenProfile
25
24
  }
26
-
27
25
  /**
28
26
  * Return profile if token is not expired.
29
27
  * @param {*} cookies
@@ -105,9 +105,8 @@ export const HeaderContainer: FC = () => {
105
105
  </UploaderContext.Consumer>
106
106
  )}
107
107
  renderOnFreshDeskWidget={() => <FreshdeskWidget />}
108
- renderOnExpanded={(profile) => (
108
+ renderOnExpanded={() => (
109
109
  <LandingExpandedHeader
110
- user={profile}
111
110
  loginUrls={loginUrls}
112
111
  renderAggregateCounts={(modality: string) => (
113
112
  <AggregateCountsContainer modality={modality} />
@@ -3,6 +3,7 @@ import PropTypes from "prop-types"
3
3
  import { useCookies } from "react-cookie"
4
4
  import { getProfile } from "../../authentication/profile"
5
5
  import { config } from "../../config"
6
+ import { useUser } from "../../queries/user"
6
7
 
7
8
  const buildCustomQuery = (customText, prepopulatedFields) => {
8
9
  const customizerQueries = [
@@ -31,11 +32,14 @@ function FreshdeskWidget({ subject, error, sentryId, description }) {
31
32
  screenshot: "No",
32
33
  captcha: "yes",
33
34
  }
35
+ const { user } = useUser()
36
+
34
37
  const prepopulatedFields = {
35
- requester: profile && profile.email,
38
+ requester: profile && user?.email,
36
39
  subject,
37
40
  description: joinedDescription,
38
41
  }
42
+
39
43
  return (
40
44
  <>
41
45
  <script
@@ -3,16 +3,12 @@ import { NavLink } from "react-router-dom"
3
3
  import { Button } from "../button/Button"
4
4
  import { Logo } from "../logo/Logo"
5
5
  import { Modal } from "../modal/Modal"
6
- import { UserMenu } from "../user/UserMenu"
6
+ import { UserMenu } from "../../users/user-menu"
7
+ import type { OpenNeuroTokenProfile } from "../../authentication/profile"
7
8
  import "./header.scss"
8
9
 
9
10
  export interface HeaderProps {
10
- profile?: {
11
- name: string
12
- admin: boolean
13
- email: string
14
- provider: string
15
- }
11
+ profile: OpenNeuroTokenProfile
16
12
  expanded?: boolean
17
13
  isOpenSupport: boolean
18
14
  toggleLoginModal: (
@@ -39,7 +35,6 @@ export const Header = ({
39
35
  renderUploader,
40
36
  }: HeaderProps) => {
41
37
  const [isOpen, setOpen] = React.useState(false)
42
-
43
38
  return (
44
39
  <>
45
40
  <header>
@@ -109,7 +104,6 @@ export const Header = ({
109
104
  ? (
110
105
  <div className="header-account-btn">
111
106
  <UserMenu
112
- profile={profile}
113
107
  signOutAndRedirect={signOutAndRedirect}
114
108
  />
115
109
  </div>
@@ -120,7 +114,7 @@ export const Header = ({
120
114
  navbar
121
115
  onClick={toggleLoginModal}
122
116
  label="Sign in"
123
- size="large"
117
+ size="small"
124
118
  />
125
119
  </>
126
120
  )}
@@ -6,11 +6,12 @@ import { ModalityCube } from "../modality-cube/ModalityCube"
6
6
  import { frontPage } from "../../common/content/front-page-content"
7
7
  import { cubeData } from "../../common/content/modality-cube-content"
8
8
  import orcidIcon from "../../../assets/orcid_24x24.png"
9
+ import { loginCheck } from "../../authentication/loginCheck"
10
+ import { useCookies } from "react-cookie"
9
11
 
10
12
  import "./header.scss"
11
13
 
12
14
  export interface LandingExpandedHeaderProps {
13
- user?: object
14
15
  loginUrls?: Record<string, string>
15
16
  renderAggregateCounts?: (modality?: string) => React.ReactNode
16
17
  renderFacetSelect: () => React.ReactNode
@@ -19,7 +20,6 @@ export interface LandingExpandedHeaderProps {
19
20
  }
20
21
 
21
22
  export const LandingExpandedHeader: React.FC<LandingExpandedHeaderProps> = ({
22
- user,
23
23
  loginUrls,
24
24
  renderAggregateCounts,
25
25
  renderFacetSelect,
@@ -29,6 +29,7 @@ export const LandingExpandedHeader: React.FC<LandingExpandedHeaderProps> = ({
29
29
  const aggregateCounts = (modality: string): React.ReactNode =>
30
30
  renderAggregateCounts ? renderAggregateCounts(modality) : null
31
31
  const navigate = useNavigate()
32
+ const [cookies] = useCookies()
32
33
  const hexGrid = (
33
34
  <ul id="hexGrid">
34
35
  {cubeData.map((item, index) => (
@@ -47,6 +48,7 @@ export const LandingExpandedHeader: React.FC<LandingExpandedHeaderProps> = ({
47
48
  ))}
48
49
  </ul>
49
50
  )
51
+ const isLoggedIn = loginCheck(cookies)
50
52
 
51
53
  return (
52
54
  <div className="expaned-header" style={{ minHeight: "720px" }}>
@@ -72,28 +74,28 @@ export const LandingExpandedHeader: React.FC<LandingExpandedHeaderProps> = ({
72
74
  </div>
73
75
  </div>
74
76
 
75
- {!user
77
+ {!isLoggedIn
76
78
  ? (
77
79
  <div className="grid grid-start hero-signin">
78
80
  <div className=" hero-sigin-label">
79
81
  <h3>SIGN IN</h3>
80
82
  </div>
81
83
  <div>
82
- <a href={loginUrls.google}>
84
+ <a href={loginUrls.orcid}>
83
85
  <Button
84
- label="Google"
86
+ label="ORCID"
85
87
  color="#fff"
86
- icon="fab fa-google"
88
+ imgSrc={orcidIcon}
87
89
  iconSize="23px"
88
90
  />
89
91
  </a>
90
92
  </div>
91
93
  <div>
92
- <a href={loginUrls.orcid}>
94
+ <a href={loginUrls.google}>
93
95
  <Button
94
- label="ORCID"
96
+ label="Google"
95
97
  color="#fff"
96
- imgSrc={orcidIcon}
98
+ icon="fab fa-google"
97
99
  iconSize="23px"
98
100
  />
99
101
  </a>
@@ -34,6 +34,8 @@ header {
34
34
  }
35
35
 
36
36
  .navbar-navigation {
37
+ margin-left: auto;
38
+ margin-right: 20px;
37
39
  @media (max-width: 800px) {
38
40
  margin-top: 20px;
39
41
  margin-left: auto;
@@ -133,6 +135,7 @@ header {
133
135
  right: 8px;
134
136
  top: 8px;
135
137
  }
138
+
136
139
  &.nav-open {
137
140
  .navbar-navigation ul {
138
141
  transform: translateY(0);
@@ -200,8 +203,7 @@ header {
200
203
  align-items: center;
201
204
  }
202
205
  > div {
203
- flex-basis: 50%;
204
- max-width: 50%;
206
+ flex-basis: 100%;
205
207
  margin: 0 10px 0 0;
206
208
  &:last-child {
207
209
  margin: 0 0 0 10px;
@@ -13,7 +13,7 @@ export interface LogoProps {
13
13
 
14
14
  export const Logo: React.FC<LogoProps> = ({
15
15
  dark = true,
16
- width = "300px",
16
+ width = "225px",
17
17
  horizontal = true,
18
18
  ...props
19
19
  }) => {
@@ -29,17 +29,6 @@ export const UserLoginModal = ({
29
29
  <h2>Sign in</h2>
30
30
  </div>
31
31
  <div className="sign-in-modal-content">
32
- <div>
33
- <a href={loginUrls.google + `?redirectPath=${btoa(redirectPath)}`}>
34
- <Button
35
- className="login-button"
36
- primary
37
- label="Google"
38
- icon="fab fa-google"
39
- iconSize="23px"
40
- />
41
- </a>
42
- </div>
43
32
  <div>
44
33
  <a href={loginUrls.orcid + `?redirectPath=${btoa(redirectPath)}`}>
45
34
  <Button
@@ -66,6 +55,18 @@ export const UserLoginModal = ({
66
55
  }
67
56
  />
68
57
  </AccordionWrap>
58
+ <div>
59
+ <a
60
+ href={loginUrls.google + `?redirectPath=${btoa(redirectPath)}`}
61
+ >
62
+ <Button
63
+ className="login-button"
64
+ label="Migrate Google to ORCID"
65
+ icon="fab fa-google"
66
+ iconSize="23px"
67
+ />
68
+ </a>
69
+ </div>
69
70
  </div>
70
71
  </div>
71
72
  </Modal>
@@ -31,7 +31,7 @@ describe("UserLoginModal component", () => {
31
31
  </MemoryRouter>,
32
32
  )
33
33
  expect(
34
- screen.getByRole("link", { name: /orcid/i }).getAttribute("href"),
34
+ screen.getByRole("link", { name: "ORCID" }).getAttribute("href"),
35
35
  ).toBe("https://openneuro.org/crn/auth/orcid?redirectPath=L2ltcG9ydA==")
36
36
  })
37
37
  })
@@ -32,9 +32,8 @@ export const Page = ({ children, headerArgs, className }: PageProps) => {
32
32
  navigateToNewSearch={() => console.log("go to /search")}
33
33
  renderUploader={() => <li>Upload</li>}
34
34
  renderOnFreshDeskWidget={() => <>This is a freshdesk widget</>}
35
- renderOnExpanded={(profile) => (
35
+ renderOnExpanded={() => (
36
36
  <LandingExpandedHeader
37
- user={profile}
38
37
  renderFacetSelect={() => <>front facet example</>}
39
38
  renderSearchInput={() => (
40
39
  <Input
@@ -146,6 +146,12 @@
146
146
  &:hover {
147
147
  background-color: var(--current-theme-primary-hover);
148
148
  }
149
+ &.disabled {
150
+ background-color: $rock;
151
+ &:hover {
152
+ background-color: $rock;
153
+ }
154
+ }
149
155
  }
150
156
  .fileupload-btn .multifile-select-btn {
151
157
  opacity: 0;
@@ -3,14 +3,15 @@ import bytes from "bytes"
3
3
  import parseISO from "date-fns/parseISO"
4
4
  import formatDistanceToNow from "date-fns/formatDistanceToNow"
5
5
  import { Link } from "react-router-dom"
6
-
7
6
  import { Tooltip } from "../../components/tooltip/Tooltip"
8
7
  import { Icon } from "../../components/icon/Icon"
9
-
8
+ import { useCookies } from "react-cookie"
9
+ import { getProfile } from "../../authentication/profile"
10
+ import { useUser } from "../../queries/user"
10
11
  import "./search-result.scss"
11
12
  import activityPulseIcon from "../../../assets/activity-icon.png"
12
13
  import { ModalityLabel } from "../../components/formatting/modality-label"
13
-
14
+ import { hasEditPermissions } from "../../authentication/profile"
14
15
  /**
15
16
  * Return an equivalent to moment(date).format('L') without moment
16
17
  * @param {*} dateObject
@@ -110,20 +111,20 @@ export interface SearchResultItemProps {
110
111
  },
111
112
  ]
112
113
  }
113
- /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
114
- profile: Record<string, any> // TODO - Use the actual user type here
115
114
  datasetTypeSelected?: string
116
- hasEditPermissions: (permissions: object, userId: string) => boolean
117
115
  }
118
116
 
119
117
  export const SearchResultItem = ({
120
118
  node,
121
- profile,
122
119
  datasetTypeSelected,
123
- hasEditPermissions,
124
120
  }: SearchResultItemProps) => {
125
- const isAdmin = profile?.admin
126
- const hasEdit = hasEditPermissions(node.permissions, profile?.sub) || isAdmin
121
+ const { user } = useUser()
122
+ const [cookies] = useCookies()
123
+ const profile = getProfile(cookies)
124
+ const profileSub = profile?.sub
125
+
126
+ const isAdmin = user?.admin
127
+ const hasEdit = hasEditPermissions(node.permissions, profileSub) || isAdmin
127
128
 
128
129
  const heading = node.latestSnapshot.description?.Name
129
130
  const summary = node.latestSnapshot?.summary
@@ -209,7 +210,7 @@ export const SearchResultItem = ({
209
210
  const uploader = (
210
211
  <div className="uploader">
211
212
  <span>Uploaded by:</span>
212
- {node.uploader.name} on {dateAdded} - {dateAddedDifference} ago
213
+ {node.uploader?.name} on {dateAdded} - {dateAddedDifference} ago
213
214
  </div>
214
215
  )
215
216
  const downloads = node.analytics.downloads
@@ -318,11 +319,11 @@ export const SearchResultItem = ({
318
319
  // Test if there's any schema validator errors
319
320
  invalid = node.latestSnapshot.validation?.errors > 0
320
321
  }
321
- const shared = !node.public && node.uploader.id !== profile.sub
322
+ const shared = !node.public && node.uploader?.id !== profileSub
322
323
 
323
324
  const MyDatasetsPage = datasetTypeSelected === "My Datasets"
324
325
  const datasetPerms = node.permissions.userPermissions.map((item) => {
325
- if (item.user.id === profile?.sub && item.access !== null) {
326
+ if (item.user.id === profileSub && item.access !== null) {
326
327
  if (item.access === "ro") {
327
328
  return "Read Only"
328
329
  } else if (item.access === "rw") {
@@ -1,32 +1,15 @@
1
1
  import React from "react"
2
-
3
2
  import { SearchResultItem } from "./SearchResultItem"
4
-
5
3
  import "./search-page.scss"
6
4
 
7
- // TODO - unify this type with the one in the app package
8
- export interface OpenNeuroTokenProfile {
9
- sub: string
10
- email: string
11
- provider: string
12
- name: string
13
- admin: boolean
14
- iat: number
15
- exp: number
16
- }
17
-
18
5
  export interface SearchResultsListProps {
19
6
  items
20
- profile?: OpenNeuroTokenProfile
21
7
  datasetTypeSelected: string
22
- hasEditPermissions: (permissions: object, userId: string) => boolean
23
8
  }
24
9
 
25
10
  export const SearchResultsList = ({
26
11
  items,
27
- profile,
28
12
  datasetTypeSelected,
29
- hasEditPermissions,
30
13
  }: SearchResultsListProps) => {
31
14
  return (
32
15
  <div className="search-results">
@@ -36,8 +19,6 @@ export const SearchResultsList = ({
36
19
  <SearchResultItem
37
20
  node={data.node}
38
21
  key={data.node.id}
39
- profile={profile}
40
- hasEditPermissions={hasEditPermissions}
41
22
  datasetTypeSelected={datasetTypeSelected}
42
23
  />
43
24
  )
@@ -50,7 +50,6 @@ const DraftContainer: React.FC<DraftContainerProps> = ({ dataset }) => {
50
50
  const activeDataset = snapshotVersion(location) || "draft"
51
51
 
52
52
  const [selectedVersion, setSelectedVersion] = React.useState(activeDataset)
53
-
54
53
  const summary = dataset.draft.summary
55
54
  const description = dataset.draft.description
56
55
  const datasetId = dataset.id
@@ -0,0 +1,155 @@
1
+ import React from "react"
2
+ import { render, screen } from "@testing-library/react"
3
+ import { MockedProvider } from "@apollo/client/testing"
4
+ import { DatasetHistory, GET_HISTORY } from "../dataset-history"
5
+
6
+ describe("DatasetHistory", () => {
7
+ it("renders an example history correctly", async () => {
8
+ const dataset = {
9
+ id: "ds000001",
10
+ name: "Test Dataset",
11
+ created: "2023-01-01T00:00:00.000Z",
12
+ downloads: 0,
13
+ views: 0,
14
+ stars: 0,
15
+ size: 0,
16
+ history: [
17
+ {
18
+ id: "adbafb8cc26558e1fe2be02fa782bf9f1a6c2556",
19
+ date: "2023-01-01T00:00:00.000Z",
20
+ authorName: "Test Author",
21
+ authorEmail: "test@example.com",
22
+ references: "",
23
+ message: "Initial commit",
24
+ },
25
+ {
26
+ id: "adbafb8cc26558e1fe2be02fa782bf9f1a6c0313",
27
+ date: "2023-01-02T00:00:00.000Z",
28
+ authorName: "Test Author",
29
+ authorEmail: "test@example.com",
30
+ references: "1.0.0",
31
+ message: "Test snapshot",
32
+ },
33
+ ],
34
+ authors: [],
35
+ editors: [],
36
+ public: true,
37
+ uploader: null,
38
+ latestSnapshot: null,
39
+ relatedDatasets: [],
40
+ mriqcResults: null,
41
+ tasks: [],
42
+ modalities: [],
43
+ datasetSummary: null,
44
+ datasetDescription: null,
45
+ readme: null,
46
+ license: null,
47
+ funding: null,
48
+ acknowledgements: null,
49
+ howToAcknowledge: null,
50
+ ethicsApprovals: [],
51
+ publications: [],
52
+ datasetMetadata: [],
53
+ changelog: null,
54
+ issues: [],
55
+ starred: false,
56
+ followers: [],
57
+ hasOpenIssues: false,
58
+ createdAt: "2023-01-01T00:00:00.000Z",
59
+ updatedAt: "2023-01-01T00:00:00.000Z",
60
+ uploaderId: null,
61
+ orcid: null,
62
+ userId: null,
63
+ user: null,
64
+ isPrivate: false,
65
+ onboarded: false,
66
+ worker: 0,
67
+ __typename: "Dataset",
68
+ }
69
+ const mocks = [
70
+ {
71
+ request: {
72
+ query: GET_HISTORY,
73
+ variables: { datasetId: dataset.id },
74
+ },
75
+ result: {
76
+ data: {
77
+ dataset,
78
+ },
79
+ },
80
+ },
81
+ ]
82
+ await render(
83
+ <MockedProvider mocks={mocks}>
84
+ <DatasetHistory datasetId={dataset.id} />
85
+ </MockedProvider>,
86
+ )
87
+ expect(await screen.findByText("Initial commit")).toBeInTheDocument()
88
+ expect(await screen.findByText("Test snapshot")).toBeInTheDocument()
89
+ })
90
+ it("renders correctly when dataset.history is null", async () => {
91
+ const dataset = {
92
+ id: "ds000001",
93
+ name: "Test Dataset",
94
+ created: "2023-01-01T00:00:00.000Z",
95
+ downloads: 0,
96
+ views: 0,
97
+ stars: 0,
98
+ size: 0,
99
+ history: null,
100
+ authors: [],
101
+ editors: [],
102
+ public: true,
103
+ uploader: null,
104
+ latestSnapshot: null,
105
+ relatedDatasets: [],
106
+ mriqcResults: null,
107
+ tasks: [],
108
+ modalities: [],
109
+ datasetSummary: null,
110
+ datasetDescription: null,
111
+ readme: null,
112
+ license: null,
113
+ funding: null,
114
+ acknowledgements: null,
115
+ howToAcknowledge: null,
116
+ ethicsApprovals: [],
117
+ publications: [],
118
+ datasetMetadata: [],
119
+ changelog: null,
120
+ issues: [],
121
+ starred: false,
122
+ followers: [],
123
+ hasOpenIssues: false,
124
+ createdAt: "2023-01-01T00:00:00.000Z",
125
+ updatedAt: "2023-01-01T00:00:00.000Z",
126
+ uploaderId: null,
127
+ orcid: null,
128
+ userId: null,
129
+ user: null,
130
+ isPrivate: false,
131
+ onboarded: false,
132
+ worker: 0,
133
+ __typename: "Dataset",
134
+ }
135
+ const mocks = [
136
+ {
137
+ request: {
138
+ query: GET_HISTORY,
139
+ variables: { datasetId: dataset.id },
140
+ },
141
+ result: {
142
+ data: {
143
+ dataset,
144
+ },
145
+ },
146
+ },
147
+ ]
148
+ await render(
149
+ <MockedProvider mocks={mocks}>
150
+ <DatasetHistory datasetId={dataset.id} />
151
+ </MockedProvider>,
152
+ )
153
+ expect(await screen.findByText("No history available")).toBeInTheDocument()
154
+ })
155
+ })
@@ -2,10 +2,9 @@ import React from "react"
2
2
  import PropTypes from "prop-types"
3
3
  import styled from "@emotion/styled"
4
4
  import { gql, useQuery } from "@apollo/client"
5
-
6
5
  import Revalidate from "../mutations/revalidate.jsx"
7
6
 
8
- const GET_HISTORY = gql`
7
+ export const GET_HISTORY = gql`
9
8
  query getHistory($datasetId: ID!) {
10
9
  dataset(id: $datasetId) {
11
10
  id
@@ -29,16 +28,16 @@ const DatasetHistoryTable = styled.div`
29
28
  .row:nth-of-type(2n) {
30
29
  padding-top: 1em;
31
30
  }
32
- .row:nth-of-type(2n + 1) {
31
+ .row:nth-of-type(2n+1) {
33
32
  padding-bottom: 1em;
34
33
  }
35
34
  .row:nth-of-type(4n),
36
- .row:nth-of-type(4n + 1) {
35
+ .row:nth-of-type(4n+1) {
37
36
  background: #f4f4f4;
38
37
  }
39
38
  `
40
39
 
41
- const DatasetHistory = ({ datasetId }) => {
40
+ export const DatasetHistory = ({ datasetId }) => {
42
41
  const { loading, data } = useQuery(GET_HISTORY, {
43
42
  variables: { datasetId },
44
43
  errorPolicy: "all",
@@ -58,7 +57,7 @@ const DatasetHistory = ({ datasetId }) => {
58
57
  <h4 className="col-lg col col-2">References</h4>
59
58
  <h4 className="col-lg col col-2 text--right">Action</h4>
60
59
  </div>
61
- {data.dataset.history.map((commit) => (
60
+ {data.dataset?.history?.map((commit) => (
62
61
  <React.Fragment key={commit.id}>
63
62
  <div className="grid faux-table">
64
63
  <div className="commit col-lg col col-2">
@@ -83,7 +82,7 @@ const DatasetHistory = ({ datasetId }) => {
83
82
  <div className="col-lg col col-12">{commit.message}</div>
84
83
  </div>
85
84
  </React.Fragment>
86
- ))}
85
+ )) || "No history available"}
87
86
  </DatasetHistoryTable>
88
87
  </div>
89
88
  )