Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
e8df50a
feat: add unread notifications count to Header, getTranslatedNotifica…
ekraffmiller Aug 24, 2025
12ba0f0
added Notification translation functions and tests
ekraffmiller Aug 27, 2025
000634f
add call to js-dataverse use case, refactor NotificationsHelper.tsx
ekraffmiller Sep 9, 2025
9e2ccbe
resolve merge conflicts
ekraffmiller Sep 9, 2025
ceff9f6
Merge branch 'develop' into 775-account-page-notifications
ekraffmiller Sep 10, 2025
eb5f8d7
feat: add NotificationsSection.tsx to Account.tsx
ekraffmiller Sep 10, 2025
f4d1757
fix: welcome notification
ekraffmiller Sep 12, 2025
a17b0cf
feat: update NavBarDropdown to accept JSX title
ekraffmiller Sep 17, 2025
0830d43
add selection logic for delete/read/unread
ekraffmiller Sep 18, 2025
3a171cd
add Buttons from design system
ekraffmiller Sep 18, 2025
f4eba03
use NotificationsContext to share state between Header and Notificati…
ekraffmiller Sep 19, 2025
8015723
add useEffect() to fix timing of fetchNotifications
ekraffmiller Sep 19, 2025
de20c3b
remove select checboxes, replace with X icon for marking as read
ekraffmiller Sep 23, 2025
e63d60f
show only unread notifications, add useMemo to avoid rerenders.
ekraffmiller Sep 23, 2025
330f3e0
add polling, use lodash.isEqual() to avoid rerenders
ekraffmiller Sep 23, 2025
6493436
fix ingest message, add translations
ekraffmiller Sep 23, 2025
73b9628
update NotificationsSection.spec.tsx
ekraffmiller Sep 23, 2025
1dba4ff
update Header.spec.tsx
ekraffmiller Sep 23, 2025
9c825c6
update LoggedInHeaderActions.spec.tsx
ekraffmiller Sep 23, 2025
3877599
fix tests in NotificationsHelper.spec.ts
ekraffmiller Sep 23, 2025
e001685
fix tests in Account.spec.ts
ekraffmiller Sep 24, 2025
bb5062c
add timestamp, fix welcome message
ekraffmiller Sep 24, 2025
6ff1949
add NotificationProvider to Account.spec.tsx
ekraffmiller Sep 24, 2025
8f4971c
Account.spec.tsx: notifications tab is enabled
ekraffmiller Sep 24, 2025
41401f9
attempt to fix flaky test
ekraffmiller Sep 24, 2025
7be7804
increase code coverage in NotificationsHelper.spec.ts
ekraffmiller Sep 24, 2025
aebb448
resolve merge conflicts
ekraffmiller Sep 24, 2025
4b43ae1
feat: improve badge and add pill prop to design system
g-saracca Sep 29, 2025
0345215
feat: more styles improved
g-saracca Sep 30, 2025
bc6e614
resolve merge conflicts
ekraffmiller Oct 1, 2025
4820100
fix: add lodash dependency
ekraffmiller Oct 1, 2025
59c3b49
fix: add lodash-es dependency, add NotificationContext to stories
ekraffmiller Oct 1, 2025
a8db8f5
fix: Header.stories.tsx
ekraffmiller Oct 1, 2025
56fcc68
increase test coverage
ekraffmiller Oct 2, 2025
0f4ee30
increase test coverage
ekraffmiller Oct 2, 2025
5d9a468
fix requestFileAccess notification
ekraffmiller Oct 3, 2025
8cad171
display DatasetMentioned notification type with additionalInfo
ekraffmiller Oct 8, 2025
9f7f892
resolve merge conflicts
ekraffmiller Oct 8, 2025
8ab7cd0
call getAllNotificationsByUser() with unreadOnly = true
ekraffmiller Oct 9, 2025
5343c84
display unescaped text in notification translation
ekraffmiller Oct 9, 2025
4b3e4cc
fix: unit test
ekraffmiller Oct 9, 2025
e1d5824
fix: display of additionalInfo
ekraffmiller Oct 9, 2025
1609637
at timeout for marking as read
ekraffmiller Oct 15, 2025
20c8fed
remove context, replace with custom hooks and externalStore
ekraffmiller Oct 16, 2025
7ae2c4d
fix NotificationsSection.spec.tsx unit test
ekraffmiller Oct 17, 2025
3f63ae4
fix unit tests
ekraffmiller Oct 17, 2025
afdf51c
resolve merge conflicts
ekraffmiller Oct 17, 2025
62b7e3e
fix: increase wait for download in e2e test
ekraffmiller Oct 20, 2025
60c61f0
fix: lint errors, remove debug statements
ekraffmiller Oct 20, 2025
7493817
fix: set user to null after logout
ekraffmiller Oct 20, 2025
9899302
updates from review
ekraffmiller Oct 30, 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
23 changes: 19 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"@dnd-kit/sortable": "8.0.0",
"@dnd-kit/utilities": "3.2.2",
"@faker-js/faker": "7.6.0",
"@iqss/dataverse-client-javascript": "2.0.0-alpha.76",
"@iqss/dataverse-client-javascript": "2.1.0-pr387.40189c8",
"@iqss/dataverse-design-system": "*",
"@istanbuljs/nyc-config-typescript": "1.0.2",
"@tanstack/react-table": "8.9.2",
Expand All @@ -39,6 +39,7 @@
"i18next-browser-languagedetector": "7.0.1",
"i18next-http-backend": "2.1.1",
"js-md5": "0.8.3",
"lodash-es": "4.17.21",
"moment-timezone": "0.5.43",
"react-bootstrap": "2.7.2",
"react-bootstrap-icons": "1.11.4",
Expand Down Expand Up @@ -131,6 +132,7 @@
"@storybook/test-runner": "0.23.0",
"@testing-library/cypress": "10.1.0",
"@types/chai-as-promised": "7.1.5",
"@types/lodash": "4.17.20",
"@types/node-sass": "4.11.3",
"@types/sinon": "10.0.13",
"@typescript-eslint/eslint-plugin": "5.51.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/design-system/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
- Fix word wrapping in options list to prevent overflow and ensure long text is displayed correctly.
- **ButtonGroup:**
- Fix styles for vertical button groups when using tooltips.
- **Badge:**:
- Add `pill` prop to allow pill-shaped badges.
- Add `dataTestId` prop to facilitate testing.
- Add `className` prop to allow custom styling.

# [2.0.2](https://github.com/IQSS/dataverse-frontend/compare/@iqss/[email protected]...@iqss/[email protected]) (2024-06-23)

Expand Down
17 changes: 15 additions & 2 deletions packages/design-system/src/lib/components/badge/Badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,25 @@ import styles from './Badge.module.scss'

interface BadgeProps {
variant?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info'
pill?: boolean
dataTestId?: string
className?: string
children: ReactNode
}

export function Badge({ variant = 'secondary', children }: BadgeProps) {
export function Badge({
variant = 'secondary',
pill = false,
className,
dataTestId,
children
}: BadgeProps) {
return (
<BadgeBS bg={variant} className={styles[variant]}>
<BadgeBS
bg={variant}
pill={pill}
className={`${styles[variant]} ${className || ''}`}
data-testid={dataTestId}>
{children}
</BadgeBS>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { NavDropdown as NavDropdownBS } from 'react-bootstrap'
import { PropsWithChildren } from 'react'
import { PropsWithChildren, ReactNode } from 'react'
import { NavbarDropdownItem } from './NavbarDropdownItem'

interface DropdownProps {
title: string
title: ReactNode
id: string
}

Expand Down
25 changes: 25 additions & 0 deletions packages/design-system/src/lib/stories/badge/Badge.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,28 @@ export const AllVariantsAtAGlance: Story = {
</>
)
}

export const WithPillShape: Story = {
render: () => (
<>
<Badge variant="primary" pill>
Primary
</Badge>
<Badge variant="secondary" pill>
Secondary
</Badge>
<Badge variant="success" pill>
Success
</Badge>
<Badge variant="danger" pill>
Danger
</Badge>
<Badge variant="warning" pill>
Warning
</Badge>
<Badge variant="info" pill>
Info
</Badge>
</>
)
}
43 changes: 43 additions & 0 deletions public/locales/en/account.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,48 @@
"datasetAndFile": "datasets or files"
}
}
},
"notifications": {
"title": "Notifications",
"userGuideLinkText": "User Guide",
"demoServerLinkText": "Demo Site",
"clearAll": "Clear All",
"dismiss": "Dismiss",
"noNotifications": "No notifications available.",
"notification": {
"createAcc": "Welcome to {{installationBrandName}}! Get started by adding or finding data. Have questions? Check out the <userGuideLink>{{userGuideLinkText}}</userGuideLink>. Want to test out Dataverse features? Use our <demoLink>{{demoServerLinkText}}</demoLink>. Also, check for your welcome email to verify your address.",
"ingestCompleted": "Dataset <datasetLink>{{- datasetDisplayName}}</datasetLink> has one or more tabular files that completed the <userGuideLink>tabular ingest process</userGuideLink> and are available in archival formats.",
"ingestCompletedWithErrors": "Dataset <datasetLink>{{- datasetDisplayName}}</datasetLink> has one or more tabular files that are available but are not supported for <b>tabular ingest</b>.",
"genericObjectDeleted": "The dataverse, dataset, or file for this notification has been deleted.",
"assignRole": "You have been granted the {{roleName}} role for <objectLink>{{- objectName}}</objectLink>.",
"assignRoleFileDownloader": "You now have access to all published restricted and unrestricted files in <objectLink>{{- objectName}}</objectLink>.",
"revokeRole": "You have been removed from a role in <objectLink>{{- objectName}}</objectLink>.",
"createCollection": "<collectionLink>{{- collectionDisplayName}}</collectionLink> was created in <ownerLink>{{- ownerDisplayName}}</ownerLink> . To learn more about what you can do with your collection, check out the <userGuideLink>{{userGuideLinkText}}</userGuideLink>.",
"createDataset": "<datasetLink>{{- datasetDisplayName}}</datasetLink> was created in <collectionLink>{{- collectionDisplayName}}</collectionLink>. To learn more about what you can do with your dataset, check out the <userGuideLink>{{userGuideLinkText}}</userGuideLink>.",
"requestFileAccess": "File access requested for dataset: <datasetLink>{{- datasetDisplayName}}</datasetLink> was made by {{requestorFirstName}} {{requestorLastName}} ({{requestorEmail}}).",
"requestedFileAccess": "You have requested access to files in dataset: <datasetLink>{{- datasetDisplayName}}</datasetLink>.",
"grantFileAccess": "Access granted for files in dataset: <datasetLink>{{- datasetDisplayName}}</datasetLink>.",
"rejectFileAccess": "Access rejected for requested files in dataset: <datasetLink>{{- datasetDisplayName}}</datasetLink>.",
"datasetCreated": "<datasetLink>{{- datasetDisplayName}}</datasetLink> was created in <ownerLink>{{- ownerDisplayName}}</ownerLink> by {{requestorFirstName}} {{requestorLastName}}.",
"submittedDataset": "<datasetLink>{{- datasetDisplayName}}</datasetLink> was submitted for review to be published in <ownerLink>{{- ownerDisplayName}}</ownerLink>. Don''t forget to publish it or send it back to the contributor, {{requestorFirstName}} {{requestorLastName}} ({{requestorEmail}})!",
"returnedDataset": "<datasetLink>{{- datasetDisplayName}}</datasetLink> was returned by the curator of <ownerLink>{{- ownerDisplayName}}</ownerLink>.",
"publishedDataset": "<datasetLink>{{- datasetDisplayName}}</datasetLink> was published in <ownerLink>{{- ownerDisplayName}}</ownerLink>.",
"publishFailedPidReg": "<datasetLink>{{- datasetDisplayName}}</datasetLink> in <ownerLink>{{- ownerDisplayName}}</ownerLink> could not be published due to a failure to register, or update the Global Identifier for the dataset or one of the files in it. Contact support if this continues to happen.",
"workflowFailure": "An external workflow run on <datasetLink>{{- datasetDisplayName}}</datasetLink> in <ownerLink>{{- ownerDisplayName}}</ownerLink> has failed. Check your email and/or view the Dataset page which may have additional details. Contact support if this continues to happen.",
"workflowSuccess": "An external workflow run on <datasetLink>{{- datasetDisplayName}}</datasetLink> in <ownerLink>{{- ownerDisplayName}}</ownerLink> has succeeded. Check your email and/or view the Dataset page which may have additional details.",
"statusUpdated": "The status of dataset <datasetLink>{{- datasetDisplayName}}</datasetLink> has been updated to {{currentCurationStatus}}.",
"pidreconciled": "The persistent identifier of dataset <datasetLink>{{- datasetDisplayName}}</datasetLink> has been updated to {{datasetPersistentId}}.",
"datasetMentioned": "Announcement Received: Newly released {{type}} <relatedLink>{{- name}}</relatedLink> {{relationship}} dataset <datasetLink>{{- datasetDisplayName}}</datasetLink>.",
"datasetMentionedGeneric": "Announcement received for dataset <datasetLink>{{- datasetDisplayName}}</datasetLink>, additional info: {{- additionalInfo}}.",
"checksumFail": "One or more files in your upload failed checksum validation for dataset <datasetLink>{{- datasetDisplayName}}</datasetLink>. Please re-run the upload script. If the problem persists, please contact support.",
"importFilesystem": "Dataset <datasetLink>{{- datasetDisplayName}}</datasetLink> has been successfully uploaded and verified.",
"globusUploadCompleted": "Globus transfer to Dataset <datasetLink>{{- datasetDisplayName}}</datasetLink> was successful. File(s) have been uploaded and verified.",
"globusDownloadCompleted": "Globus transfer from the dataset <datasetLink>{{- datasetDisplayName}}</datasetLink> was successful.",
"globusUploadCompletedWithErrors": "Globus transfer to Dataset <datasetLink>{{- datasetDisplayName}}</datasetLink> is complete with errors.",
"globusUploadFailedRemotely": "Remote data transfer between Globus collections for Dataset <datasetLink>{{- datasetDisplayName}}</datasetLink> failed, reported via Globus API.",
"globusUploadFailedLocally": "Dataverse received a confirmation of a successful Globus data transfer for Dataset <datasetLink>{{- datasetDisplayName}}</datasetLink>, but failed to add the files to the dataset locally.",
"globusDownloadCompletedWithErrors": "Globus transfer from the dataset <datasetLink>{{- datasetDisplayName}}</datasetLink> is complete with errors.",
"importChecksum": "<datasetLink>{{datasetDisplayName}}</datasetLink>, dataset had file checksums added via a batch job."
}
}
}
1 change: 1 addition & 0 deletions public/locales/en/header.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"brandLogoImage": "Dataverse brand logo",
"navigation": {
"addData": "Add Data",
"notifications": "Notifications",
"newCollection": "New Collection",
"newDataset": "New Dataset",
"accountInfo": "Account Information",
Expand Down
27 changes: 27 additions & 0 deletions src/notifications/domain/hooks/needsUpdateStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// store/updateFlagStore.ts
type Listener = () => void

class NeedsUpdateStore {
private needsUpdate = false
private listeners = new Set<Listener>()

getSnapshot = () => this.needsUpdate

subscribe = (callback: Listener) => {
this.listeners.add(callback)
return () => this.listeners.delete(callback)
}

setNeedsUpdate(value: boolean) {
if (this.needsUpdate !== value) {
this.needsUpdate = value
this.emit()
}
}

private emit() {
this.listeners.forEach((listener) => listener())
}
}

export const needsUpdateStore = new NeedsUpdateStore()
31 changes: 31 additions & 0 deletions src/notifications/domain/hooks/useNeedsUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// hooks/useNeedsUpdate.ts
import { useSyncExternalStore } from 'react'
type Listener = () => void

class NeedsUpdateStore {
private needsUpdate = false
private listeners = new Set<Listener>()

getSnapshot = () => this.needsUpdate

subscribe = (callback: Listener) => {
this.listeners.add(callback)
return () => this.listeners.delete(callback)
}

setNeedsUpdate(value: boolean) {
if (this.needsUpdate !== value) {
this.needsUpdate = value
this.emit()
}
}

private emit() {
this.listeners.forEach((listener) => listener())
}
}
const needsUpdateStore = new NeedsUpdateStore()

export function useNeedsUpdate() {
return useSyncExternalStore(needsUpdateStore.subscribe, needsUpdateStore.getSnapshot)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about including the needsUpdateStore.ts logic in this same file?
Just saying to avoid one more extra file..

72 changes: 72 additions & 0 deletions src/notifications/domain/hooks/useNotifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { useCallback, useEffect, useState } from 'react'
import { useSession } from '@/sections/session/SessionContext'
import { Notification } from '@/notifications/domain/models/Notification'
import { NotificationRepository } from '@/notifications/domain/repositories/NotificationRepository'
import { getAllNotificationsByUser } from '@/notifications/domain/useCases/getAllNotificationsByUser'

const POLLING_NOTIFICATIONS_INTERVAL_TIME = 30_000

export function useNotifications(repository: NotificationRepository) {
const [notifications, setNotifications] = useState<Notification[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const { user } = useSession()

const fetchNotifications = useCallback(async () => {
try {
const fetched = await getAllNotificationsByUser(repository)
setError(null)
setNotifications(fetched)
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to fetch notifications'
setError(message)
} finally {
setIsLoading(false)
}
}, [repository])

useEffect(() => {
if (!user) return

void fetchNotifications()

const interval = setInterval(() => {
void fetchNotifications()
}, POLLING_NOTIFICATIONS_INTERVAL_TIME)

return () => clearInterval(interval)
}, [fetchNotifications, user])

const markAsRead = async (ids: number[]) => {
setNotifications((prev) =>
prev.map((n) => (ids.includes(n.id) ? { ...n, displayAsRead: true } : n))
)
try {
await Promise.all(ids.map((id) => repository.markNotificationAsRead(id)))
setError(null)
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to mark as read'
setError(message)
}
}

const deleteMany = async (ids: number[]) => {
setNotifications((prev) => prev.filter((n) => !ids.includes(n.id)))
try {
await Promise.all(ids.map((id) => repository.deleteNotification(id)))
setError(null)
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to delete notifications'
setError(message)
}
}

return {
notifications,
isLoading,
error,
refetch: fetchNotifications,
markAsRead,
deleteMany
}
}
39 changes: 39 additions & 0 deletions src/notifications/domain/hooks/useUnreadCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useCallback, useEffect, useState } from 'react'
import { needsUpdateStore } from './needsUpdateStore'
import { NotificationRepository } from '@/notifications/domain/repositories/NotificationRepository'
import { User } from '@/users/domain/models/User'
import { useNeedsUpdate } from '@/notifications/domain/hooks/useNeedsUpdate'
import { getUnreadNotificationsCount } from '@/notifications/domain/useCases/getUnreadNotificationsCount'

const POLLING_INTERVAL = 30000 // 30 seconds
export function useUnreadCount(user: User, notificationRepository: NotificationRepository) {
const [unreadCount, setUnreadCount] = useState(0)

const needsUpdate = useNeedsUpdate()
const fetchUnread = useCallback(async () => {
if (user) {
const count = await getUnreadNotificationsCount(notificationRepository)
setUnreadCount(count)
}
needsUpdateStore.setNeedsUpdate(false)
}, [user, notificationRepository])
useEffect(() => {
if (needsUpdate) {
void fetchUnread()
}
}, [needsUpdate, fetchUnread, notificationRepository])
// Polling trigger
useEffect(() => {
const interval = setInterval(() => {
void fetchUnread()
}, POLLING_INTERVAL)

return () => clearInterval(interval)
}, [fetchUnread, notificationRepository])

useEffect(() => {
void fetchUnread() // run once when the component mounts
}, [fetchUnread])

return unreadCount
}
Loading
Loading