Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
fca641a
adding new events for contributorRequest and contributorResponse
thinknoack Jul 24, 2025
fe7aca2
update to user notifications tab. TODO - filter by user specific status
thinknoack Jul 28, 2025
de8a768
updates to user notification tab/view. various resolver changes
thinknoack Jul 30, 2025
09dd158
adding comments I removed
thinknoack Jul 30, 2025
20c27b2
adding comments I removed
thinknoack Jul 30, 2025
0718006
adding comments I removed
thinknoack Jul 30, 2025
9a7d549
refactor into components
thinknoack Jul 30, 2025
32534f1
Merge branch 'master' into feature/request-contrib-role
thinknoack Jul 30, 2025
1b289d7
lint and test updates
thinknoack Jul 31, 2025
a3ed21e
adding button styling for contrib request button
thinknoack Jul 31, 2025
c5a57a9
minor test update
thinknoack Jul 31, 2025
d07aa13
remove comment
thinknoack Jul 31, 2025
5393faa
remove unused var
thinknoack Jul 31, 2025
ce92a03
Update packages/openneuro-app/src/scripts/dataset/mutations/dataset-e…
thinknoack Aug 11, 2025
2dee014
remove setTimeout from processContributorRequest
thinknoack Aug 11, 2025
03434de
consolidated event types into file
thinknoack Aug 11, 2025
ff8884a
move processedEvent enrichment to backend
thinknoack Aug 11, 2025
f2d2728
remove general perm check to check for orcid id in contributors array…
thinknoack Aug 11, 2025
f5293fe
lint type fixes
thinknoack Aug 11, 2025
6abbb23
Merge branch 'master' into feature/request-contrib-role
thinknoack Aug 19, 2025
8d9e564
feat: Add a user-specific virtual notification status to dataset even…
thinknoack Aug 19, 2025
d45affe
update comment
thinknoack Aug 19, 2025
3467259
text update
thinknoack Aug 19, 2025
dab42f4
updates to the notification tabs/queries/components that show counts …
thinknoack Aug 20, 2025
842ce8b
refactor after working on app
thinknoack Aug 20, 2025
2ddf73d
refactor after working on app
thinknoack Aug 20, 2025
ae715ce
lint updates
thinknoack Aug 21, 2025
da10192
updates to fix errors
thinknoack Aug 21, 2025
12760eb
lint updates
thinknoack Aug 21, 2025
4c1c45e
updates to fix errors
thinknoack Aug 21, 2025
40b4702
add null check for username
thinknoack Aug 21, 2025
83bd039
update dataset admin lookup and mongo pipeline
thinknoack Aug 25, 2025
11a33b0
update dataset admin lookup and mongo pipeline - saved
thinknoack Aug 25, 2025
e128193
Merge branch 'feat/api-profile-event-status' into feat/app-profile-ev…
thinknoack Aug 25, 2025
1346f4d
Revert "Merge branch 'feat/api-profile-event-status' into feat/app-pr…
thinknoack Aug 25, 2025
6dbecb8
update the pipline for notifications
thinknoack Aug 25, 2025
6ff6846
update user notifications to show events for dataset admins
thinknoack Aug 25, 2025
4f7fb79
Merge branch 'feat/api-profile-event-status' into feat/app-profile-ev…
thinknoack Aug 25, 2025
be9bd2f
remove admin from label it is confusing
thinknoack Aug 25, 2025
89b9fc8
Merge branch 'master' into feature/request-contrib-role
thinknoack Aug 27, 2025
263e54b
Merge branch 'feature/request-contrib-role' into feat/api-profile-eve…
thinknoack Aug 27, 2025
ca0263b
Merge branch 'feat/api-profile-event-status' into feat/app-profile-ev…
thinknoack Aug 27, 2025
6387e46
refactor: remove unnecessary dataset lookup
thinknoack Aug 29, 2025
f79d051
Merge branch 'feat/api-profile-event-status' into feat/app-profile-ev…
thinknoack Aug 29, 2025
9396eb9
Merge branch 'master' into feature/request-contrib-role
thinknoack Sep 2, 2025
efd130c
Merge branch 'feature/request-contrib-role' into feat/api-profile-eve…
thinknoack Sep 2, 2025
7ad9635
Merge branch 'feat/api-profile-event-status' into feat/app-profile-ev…
thinknoack Sep 2, 2025
f6e383d
snapshot update
thinknoack Sep 8, 2025
89894dd
adding mutation to update datacite and better datacite resolver
thinknoack Sep 15, 2025
fb5c753
adding datacite ui for updates to file
thinknoack Sep 15, 2025
9f47a47
ui/query updates for apollo cache
thinknoack Sep 16, 2025
51899be
api updates to manage apollo cache
thinknoack Sep 16, 2025
5f08cd9
Merge branch 'feature/datacite-mutation' into feature/datacite-mutati…
thinknoack Sep 16, 2025
bd404a1
update is admin hook to run outside of jsx, move contributor files an…
thinknoack Sep 16, 2025
8c6b4ed
update lints
thinknoack Sep 16, 2025
61a07f5
update lints
thinknoack Sep 16, 2025
626d3d9
Merge branch 'feature/datacite-mutation' into feature/datacite-mutati…
thinknoack Sep 16, 2025
0948621
test updates
thinknoack Sep 17, 2025
dbeff55
update tests
thinknoack Sep 17, 2025
03378b7
Merge branch 'feature/datacite-mutation' into feature/datacite-mutati…
thinknoack Sep 17, 2025
5146c15
update test to use contributor vs author
thinknoack Sep 17, 2025
d862772
remove comments
thinknoack Sep 17, 2025
485b049
remove comments
thinknoack Sep 17, 2025
d68cbdc
updates after review
thinknoack Sep 18, 2025
ea19cdd
Merge branch 'feature/datacite-mutation' into feature/datacite-mutati…
thinknoack Sep 18, 2025
57252e7
Merge pull request #3575 from OpenNeuroOrg/feature/datacite-mutation
nellh Sep 19, 2025
fb93188
adding additional ui test
thinknoack Sep 19, 2025
c46cbac
adding save test for contrib form row
thinknoack Sep 19, 2025
042f5dd
addign mutation for createContributorCitationEvent
thinknoack Sep 25, 2025
a6808ce
mutations for creating contributor-datacite events and processing the…
thinknoack Sep 30, 2025
42103f3
unused var
thinknoack Sep 30, 2025
2c37d57
lint/test updates for api
thinknoack Oct 1, 2025
8b1475c
update comments
thinknoack Oct 1, 2025
fb5f5d8
Merge pull request #3589 from OpenNeuroOrg/feature/user-event-flow-co…
nellh Oct 1, 2025
6aa33cd
Merge pull request #3578 from OpenNeuroOrg/app-contributor-tests
nellh Oct 1, 2025
d05bd93
Merge pull request #3576 from OpenNeuroOrg/feature/datacite-mutation-ui
nellh Oct 1, 2025
17d4e15
Merge branch 'feature/request-contrib-role' into feat/api-profile-eve…
nellh Oct 1, 2025
d04f236
Merge pull request #3548 from OpenNeuroOrg/feat/api-profile-event-status
nellh Oct 1, 2025
1ea7a13
Merge branch 'master' into feature/request-contrib-role
nellh Oct 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/openneuro-app/src/scripts/components/button/button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
line-height: 1.4em;
}


.on-button--primary {
color: #fff;
background-color: var(--current-theme-primary);
Expand Down Expand Up @@ -40,6 +41,20 @@
border-color: $on-light-green;
}
}
.on-button--ghost {
color: $on-dark-aqua;
background-color: transparent;
text-transform: uppercase;
transition: background-color 0.3s;
box-shadow:none;
border: 2px solid transparent;
&:hover,
&.active {
background-color: transparent;
color: $on-dark-aqua;
border: 2px solid $on-dark-aqua;
}
}

.icon-text {
display: flex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface SelectGroupProps {
value: string
}[]
value: string
setValue: (string) => void
setValue: (value: string) => void
label?: string
id: string
layout: "inline" | "stacked"
Expand All @@ -19,6 +19,7 @@ export const SelectGroup = ({
label,
id,
layout,
value,
}: SelectGroupProps) => {
return (
<span
Expand All @@ -29,11 +30,14 @@ export const SelectGroup = ({
{label && <label htmlFor={id}>{label}</label>}
<select
className="on-select"
onChange={(e) => setValue(e.target.value)}
id={id}
value={value}
onChange={(e) => setValue(e.target.value)}
>
{options.map((item, index) => (
<option key={index} label={item.label} value={item.value} />
<option key={index} value={item.value}>
{item.label}
</option>
))}
</select>
</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@import "../scss/variables.scss";
@import "../../scss/variables.scss";

.on-select-wrapper {
label {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from "react"
import { vi } from "vitest"
import { fireEvent, render, screen } from "@testing-library/react"
import { ContributorFormRow } from "../contributor-form-row"
import type { Contributor } from "../../types/datacite"

describe("ContributorFormRow", () => {
const mockContributor: Contributor = {
name: "Jane Doe",
givenName: "Jane",
familyName: "Doe",
orcid: "",
contributorType: "Researcher",
order: 1,
}

const defaultProps = {
contributor: mockContributor,
index: 0,
errors: {},
onChange: vi.fn(),
onMove: vi.fn(),
onRemove: vi.fn(),
isFirst: true,
isLast: false,
}

beforeEach(() => {
vi.clearAllMocks()
})

it("renders contributor name input", () => {
render(<ContributorFormRow {...defaultProps} />)
expect(screen.getByPlaceholderText("Name")).toHaveValue("Jane Doe")
})

it("calls onChange when name is updated", () => {
render(<ContributorFormRow {...defaultProps} />)
const input = screen.getByPlaceholderText("Name")
fireEvent.change(input, { target: { value: "New Name" } })
expect(defaultProps.onChange).toHaveBeenCalledWith(0, "name", "New Name")
})

it("calls onMove when up or down buttons are clicked", () => {
render(<ContributorFormRow {...defaultProps} isFirst={false} />)
const upButton = screen.getByText("↑")
fireEvent.click(upButton)
expect(defaultProps.onMove).toHaveBeenCalledWith(0, "up")
})

it("calls onRemove when trash button is clicked", () => {
render(<ContributorFormRow {...defaultProps} />)
const trashButton = screen.getByRole("button", { name: "" })
fireEvent.click(trashButton)
expect(defaultProps.onRemove).toHaveBeenCalledWith(0)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from "react"
import { fireEvent, render, screen, waitFor } from "@testing-library/react"

Check failure on line 2 in packages/openneuro-app/src/scripts/contributors/__tests__/contributor-list.spec.tsx

View workflow job for this annotation

GitHub Actions / eslint

'waitFor' is defined but never used. Allowed unused vars must match /^_/u
import { MockedProvider } from "@apollo/client/testing"
import { ContributorsListDisplay } from "../contributors-list"
import type { Contributor } from "../../types/datacite"

describe("ContributorsListDisplay", () => {
const baseContributors: Contributor[] = [
{
name: "Jane Doe",
givenName: "Jane",
familyName: "Doe",
orcid: "",
contributorType: "Researcher",
order: 1,
},
{
name: "John Smith",
givenName: "John",
familyName: "Smith",
orcid: "0000-0000-0000-0000",
contributorType: "DataCollector",
order: 2,
},
]

const renderComponent = (props = {}) => {
return render(
<MockedProvider>
<ContributorsListDisplay
contributors={baseContributors}
datasetId="ds000001"
editable
{...props}
/>
</MockedProvider>,
)
}

it("renders contributors list", async () => {
renderComponent()
expect(await screen.findByText(/Jane Doe/)).toBeInTheDocument()
expect(screen.getByText(/John Smith/)).toBeInTheDocument()
})

it("shows edit button when editable", async () => {
renderComponent()
expect(await screen.findByText("Edit")).toBeInTheDocument()
})

it("enters edit mode when edit button clicked", async () => {
renderComponent()

const editButton = await screen.findByText("Edit")
fireEvent.click(editButton)

const nameInputs = await screen.findAllByPlaceholderText("Name")
expect(nameInputs.length).toBe(2)
expect(nameInputs[0]).toHaveValue("Jane Doe")
expect(nameInputs[1]).toHaveValue("John Smith")
})

it("adds a new contributor when 'Add Contributor' clicked", async () => {
renderComponent()
const editButton = await screen.findByText("Edit")
fireEvent.click(editButton)

const addButton = await screen.findByText("Add Contributor")
fireEvent.click(addButton)

expect(await screen.findAllByPlaceholderText("Name")).toHaveLength(3)
})

it("shows an error when trying to submit with empty name", async () => {
renderComponent()

const editButton = await screen.findByText("Edit")
fireEvent.click(editButton)

const addButton = await screen.findByText("Add Contributor")
fireEvent.click(addButton)

const saveButton = await screen.findByText("Save")
fireEvent.click(saveButton)

// Check that the new field has the browser validation pseudo-state
const nameInputs = await screen.findAllByPlaceholderText("Name")

expect(nameInputs[2]).toBeInvalid()
expect(nameInputs[2]).toBeRequired()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { cloneContributor, CONTRIBUTOR_TYPES } from "../contributor-utils"
import type { Contributor } from "../../types/datacite"

describe("contributor-utils", () => {
it("clones a contributor deeply", () => {
const contributor: Contributor = {
name: "Jane Doe",
givenName: "Jane",
familyName: "Doe",
orcid: "0000-0000-0000-0000",
contributorType: "Researcher",
order: 1,
}

const cloned = cloneContributor(contributor)
expect(cloned).toEqual(contributor)
expect(cloned).not.toBe(contributor)
})

it("includes Researcher as a valid contributor type", () => {
expect(CONTRIBUTOR_TYPES).toContain("Researcher")
})

it("contains at least one contributor type", () => {
expect(CONTRIBUTOR_TYPES.length).toBeGreaterThan(0)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,28 @@ describe("SingleContributorDisplay - Basic Loading", () => {
it("renders the component and displays the contributor name", async () => {
const contributor = { name: "Jane Doe", orcid: null }
renderComponent({ contributor })
expect(screen.getByText("Jane Doe")).toBeInTheDocument()

// wait for async rendering
expect(await screen.findByText("Jane Doe")).toBeInTheDocument()
})

it("renders 'Unknown Contributor' if the contributor name is missing", async () => {
const contributor = { name: undefined, orcid: null }
renderComponent({ contributor })
expect(screen.getByText("Unknown Contributor")).toBeInTheDocument()

expect(await screen.findByText("Unknown Contributor")).toBeInTheDocument()
})

it("renders the component and displays the ORCID link if an ID is provided", async () => {
it("renders the ORCID link if an ID is provided", async () => {
const testOrcid = "0000-0000-0000-0000"
const contributor = { name: "Author With ORCID", orcid: testOrcid }
renderComponent({ contributor })
const orcidLink = screen.getByLabelText(
`ORCID profile for ${contributor.name}`,
)

// check link by role
const orcidLink = await screen.findByRole("link", {
name: /ORCID profile for Author With ORCID/,
})

expect(orcidLink).toBeInTheDocument()
expect(orcidLink).toHaveAttribute(
"href",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from "react"
import type { Contributor } from "../types/datacite"
import { SelectGroup } from "../components/select/SelectGroup"
import { CONTRIBUTOR_TYPES } from "./contributor-utils"

interface ContributorFormRowProps {
contributor: Contributor
index: number
errors: Record<number, string>
onChange: (index: number, field: keyof Contributor, value: string) => void
onMove: (index: number, direction: "up" | "down") => void
onRemove: (index: number) => void
isFirst: boolean
isLast: boolean
}

export const ContributorFormRow: React.FC<ContributorFormRowProps> = ({
contributor,
index,
errors,
onChange,
onMove,
onRemove,
isFirst,
isLast,
}) => (
<div
style={{
display: "flex",
alignItems: "center",
marginBottom: 8,
gap: 8,
flexWrap: "wrap",
}}
>
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<button
type="button"
onClick={() => onMove(index, "up")}
disabled={isFirst}
>
</button>
<button
type="button"
onClick={() => onMove(index, "down")}
disabled={isLast}
>
</button>
</div>

<div style={{ display: "flex", flexDirection: "column" }}>
<input
type="text"
placeholder="Name"
value={contributor.name || ""}
onChange={(e) => onChange(index, "name", e.target.value)}
style={{
borderColor: errors[index] ? "red" : undefined,
borderWidth: errors[index] ? 2 : undefined,
}}
required
/>
{errors[index] && (
<span style={{ color: "red", fontSize: "0.8em" }}>{errors[index]}</span>
)}
</div>

<SelectGroup
id={`contributor-type-${index}`}
layout="inline"
options={CONTRIBUTOR_TYPES.map((t) => ({ label: t, value: t }))}
value={contributor.contributorType?.trim() ?? ""}
setValue={(v) => onChange(index, "contributorType", v)}
/>

<button
type="button"
onClick={() => onRemove(index)}
style={{ color: "#C82429", border: 0, background: "none" }}
>
<i className="fa fa-trash"></i>
</button>
</div>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Contributor } from "../types/datacite"

export const CONTRIBUTOR_TYPES = [
"ContactPerson",
"DataCollector",
"DataCurator",
"DataManager",
"Distributor",
"Editor",
"HostingInstitution",
"Producer",
"ProjectLeader",
"ProjectManager",
"ProjectMember",
"RegistrationAgency",
"RegistrationAuthority",
"RelatedPerson",
"Researcher",
"ResearchGroup",
"RightsHolder",
"Sponsor",
"Supervisor",
"WorkPackageLeader",
"Other",
]

// Utility to deep clone a contributor
export const cloneContributor = (c: Contributor): Contributor =>
structuredClone(c)
Loading
Loading