Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel
- Show toast notification when API token is copied to clipboard.
- Dataset versions: (1) file changes should be `Access: Restricted` instead of `isResticted: true/false`; (2) logic of View Detail button. (#879)
- File versions: (1) logic of linking to a file version; (2)If file not included, show text information "File not included in this version.". (#879)
- Notifications unread count now refreshes immediately (without waiting for polling) after notifications are read and after dataset/collection create or dataset publish actions that generate notifications.

### Removed

Expand Down
27 changes: 1 addition & 26 deletions src/notifications/domain/hooks/useNeedsUpdate.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,5 @@
// 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()
import { needsUpdateStore } from './needsUpdateStore'

export function useNeedsUpdate() {
return useSyncExternalStore(needsUpdateStore.subscribe, needsUpdateStore.getSnapshot)
Expand Down
3 changes: 3 additions & 0 deletions src/notifications/domain/hooks/useNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Notification } from '@/notifications/domain/models/Notification'
import { NotificationRepository } from '@/notifications/domain/repositories/NotificationRepository'
import { NotificationsPaginationInfo } from '@/notifications/domain/models/NotificationsPaginationInfo'
import { getAllNotificationsByUser } from '@/notifications/domain/useCases/getAllNotificationsByUser'
import { needsUpdateStore } from './needsUpdateStore'

const POLLING_NOTIFICATIONS_INTERVAL_TIME = 30_000

Expand Down Expand Up @@ -67,6 +68,7 @@ export function useNotifications(
try {
await Promise.all(ids.map((id) => repository.markNotificationAsRead(id)))
setError(null)
needsUpdateStore.setNeedsUpdate(true)
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to mark as read'
setError(message)
Expand All @@ -78,6 +80,7 @@ export function useNotifications(
try {
await Promise.all(ids.map((id) => repository.deleteNotification(id)))
setError(null)
needsUpdateStore.setNeedsUpdate(true)
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to delete notifications'
setError(message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert, Button, CloseButton, Stack } from '@iqss/dataverse-design-system'
import { getTranslatedNotification } from '@/sections/account/notifications-section/NotificationsHelper'
import { needsUpdateStore } from '@/notifications/domain/hooks/needsUpdateStore'
import { useNotifications } from '@/notifications/domain/hooks/useNotifications'
import { NotificationRepository } from '@/notifications/domain/repositories/NotificationRepository'
import { NotificationsPaginationInfo } from '@/notifications/domain/models/NotificationsPaginationInfo'
Expand Down Expand Up @@ -39,7 +38,6 @@ export const NotificationsSection = ({ notificationRepository }: NotificationsSe
await markAsRead(unreadIds)
setReadIds((prev) => [...prev, ...unreadIds])
await refetch()
needsUpdateStore.setNeedsUpdate(true)
})()
}, 2000)
return () => clearTimeout(timer)
Expand Down Expand Up @@ -86,7 +84,9 @@ export const NotificationsSection = ({ notificationRepository }: NotificationsSe
direction="horizontal"
gap={2}
style={{ width: '100%', justifyContent: 'space-between', alignItems: 'center' }}>
<div>{t('notifications.displayingNotifications', { start, end, total })}</div>
{notifications.length > 0 && (
<div>{t('notifications.displayingNotifications', { start, end, total })}</div>
)}

{notifications.length > 0 && (
<Button
Expand Down
2 changes: 2 additions & 0 deletions src/sections/dataset/publish-dataset/usePublishDataset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { SubmissionStatus } from '../../shared/form/DatasetMetadataForm/useSubmi
import { CollectionRepository } from '../../../collection/domain/repositories/CollectionRepository'
import { UpwardHierarchyNode } from '../../../shared/hierarchy/domain/models/UpwardHierarchyNode'
import { publishCollection } from '../../../collection/domain/useCases/publishCollection'
import { needsUpdateStore } from '@/notifications/domain/hooks/needsUpdateStore'

type UsePublishDatasetReturnType =
| {
Expand Down Expand Up @@ -43,6 +44,7 @@ export function usePublishDataset(
.then(() => {
setPublishError(null)
setSubmissionStatus(SubmissionStatus.SubmitComplete)
needsUpdateStore.setNeedsUpdate(true)
onPublishSucceed()
})
.catch((err) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { MetadataFieldsHelper, type DatasetMetadataFormValues } from './Metadata
import { type DatasetMetadataFormMode } from '.'
import { QueryParamKey, Route } from '../../../Route.enum'
import { DatasetNonNumericVersionSearchParam } from '../../../../dataset/domain/models/Dataset'
import { needsUpdateStore } from '@/notifications/domain/hooks/needsUpdateStore'

export enum SubmissionStatus {
NotSubmitted = 'NotSubmitted',
Expand Down Expand Up @@ -64,6 +65,7 @@ export function useSubmitDataset(
.then(({ persistentId }) => {
setSubmitError(null)
setSubmissionStatus(SubmissionStatus.SubmitComplete)
needsUpdateStore.setNeedsUpdate(true)
toast.success(tDataset('alerts.datasetCreated.alertText'))
navigate(
`${Route.DATASETS}?${QueryParamKey.PERSISTENT_ID}=${persistentId}&${QueryParamKey.VERSION}=${DatasetNonNumericVersionSearchParam.DRAFT}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { editCollection } from '@/collection/domain/useCases/editCollection'
import { RouteWithParams } from '@/sections/Route.enum'
import { JSDataverseWriteErrorHandler } from '@/shared/helpers/JSDataverseWriteErrorHandler'
import { CollectionFormHelper } from '../CollectionFormHelper'
import { needsUpdateStore } from '@/notifications/domain/hooks/needsUpdateStore'

export enum SubmissionStatus {
NotSubmitted = 'NotSubmitted',
Expand Down Expand Up @@ -109,6 +110,7 @@ export function useSubmitCollection(
.then(() => {
setSubmitError(null)
setSubmissionStatus(SubmissionStatus.SubmitComplete)
needsUpdateStore.setNeedsUpdate(true)

navigate(RouteWithParams.COLLECTIONS(newOrUpdatedCollection.alias), {
state: { created: true }
Expand Down
20 changes: 20 additions & 0 deletions tests/component/sections/account/NotificationsSection.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ const mockErrorRepository = {
getAllNotificationsByUser: () => Promise.reject(new Error('Failed to fetch')),
getUnreadNotificationsCount: () => Promise.resolve(1)
}

const emptyNotificationRepository = {
getAllNotificationsByUser: () =>
Promise.resolve({
totalItemCount: 0,
items: []
}),
markNotificationAsRead: (_id: number) => Promise.resolve(),
deleteNotification: (_id: number) => Promise.resolve(),
getUnreadNotificationsCount: () => Promise.resolve(0)
}
describe('multiple page notifications', () => {
before(() => {
notificationsRepository.getAllNotificationsByUser = cy
Expand Down Expand Up @@ -123,4 +134,13 @@ describe('NotificationsSection', () => {

cy.get('@markAsRead').should('have.been.calledOnceWith', Cypress.sinon.match.number)
})

it('does not render notifications display count when there are no notifications', () => {
cy.mountAuthenticated(
<NotificationsSection notificationRepository={emptyNotificationRepository} />
)

cy.findByText(/Displaying .* Notifications/).should('not.exist')
cy.findByText('No notifications available.').should('exist')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CollectionRepository } from '../../../../../src/collection/domain/repos
import { UpwardHierarchyNodeMother } from '../../../shared/hierarchy/domain/models/UpwardHierarchyNodeMother'
import { CustomTermsMother } from '@tests/component/dataset/domain/models/TermsOfUseMother'
import { LicenseMother } from '@tests/component/dataset/domain/models/LicenseMother'
import { needsUpdateStore } from '@/notifications/domain/hooks/needsUpdateStore'

describe('PublishDatasetModal', () => {
it('display modal for never released dataset', () => {
Expand Down Expand Up @@ -42,6 +43,33 @@ describe('PublishDatasetModal', () => {
)
})

it('refreshes unread notification count after successful publish', () => {
const handleClose = cy.stub()
const repository = {} as DatasetRepository
repository.publish = cy.stub().as('repositoryPublish').resolves()
const collectionRepository = {} as CollectionRepository
collectionRepository.publish = cy.stub().as('collectionRepositoryPublish').resolves()
const parentCollection = UpwardHierarchyNodeMother.createCollection()
cy.spy(needsUpdateStore, 'setNeedsUpdate').as('setNeedsUpdate')

cy.mountAuthenticated(
<PublishDatasetModal
show={true}
repository={repository}
license={LicenseMother.create()}
collectionRepository={collectionRepository}
parentCollection={parentCollection}
persistentId="testPersistentId"
releasedVersionExists={false}
handleClose={handleClose}
/>
)

cy.findByText('Continue').click()
cy.get('@repositoryPublish').should('have.been.calledOnce')
cy.get('@setNeedsUpdate').should('have.been.calledWith', true)
})

it('displays an error message when publishDataset fails', () => {
const handleClose = cy.stub()
const repository = {} as DatasetRepository // Mock the repository as needed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CollectionRepository } from '../../../../../src/collection/domain/repos
import { CollectionMother } from '../../../collection/domain/models/CollectionMother'
import { AuthContext } from 'react-oauth2-code-pkce'
import { NotificationRepository } from '@/notifications/domain/repositories/NotificationRepository'
import { needsUpdateStore } from '@/notifications/domain/hooks/needsUpdateStore'

const testUser = UserMother.create()
const collectionRepository: CollectionRepository = {} as CollectionRepository
Expand Down Expand Up @@ -140,6 +141,28 @@ describe('LoggedInHeaderActions', () => {
cy.get('[data-testid="unread-notifications-badge"]').should('have.length', 2)
})

it('refreshes unread notification badge when notifications update', () => {
const unreadCountStub = cy.stub()
unreadCountStub.onFirstCall().resolves(3)
unreadCountStub.onSecondCall().resolves(0)
notificationRepository.getUnreadNotificationsCount = unreadCountStub

cy.mountAuthenticated(
<LoggedInHeaderActions
user={testUser}
collectionRepository={collectionRepository}
notificationRepository={notificationRepository}
/>
)

cy.get('[data-testid="unread-notifications-badge"]').should('exist').and('contain', '3')
cy.then(() => {
needsUpdateStore.setNeedsUpdate(true)
})
cy.wrap(unreadCountStub).should('have.been.calledTwice')
cy.get('[data-testid="unread-notifications-badge"]').should('not.exist')
})

it('calls the logout function when clicking the logout button', () => {
collectionRepository.getUserPermissions = cy.stub().resolves(userPermissionsMock)
collectionRepository.getById = cy.stub().resolves(CollectionMother.create())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DatasetMother } from '../../../dataset/domain/models/DatasetMother'
import { MetadataBlockInfoMother } from '../../../metadata-block-info/domain/models/MetadataBlockInfoMother'
import { UserMother } from '../../../users/domain/models/UserMother'
import { DatasetTemplateMother } from '@tests/component/dataset/domain/models/DatasetTemplateMother'
import { needsUpdateStore } from '@/notifications/domain/hooks/needsUpdateStore'

const datasetRepository: DatasetRepository = {} as DatasetRepository
const metadataBlockInfoRepository: MetadataBlockInfoRepository = {} as MetadataBlockInfoRepository
Expand Down Expand Up @@ -1276,6 +1277,7 @@ describe('DatasetMetadataForm', () => {
})
describe('should not display required errors when submitting the form with required fields filled', () => {
it('on create mode', () => {
cy.spy(needsUpdateStore, 'setNeedsUpdate').as('setNeedsUpdate')
cy.customMount(
<DatasetMetadataForm
mode="create"
Expand All @@ -1295,6 +1297,7 @@ describe('DatasetMetadataForm', () => {

cy.findByText('Error').should('not.exist')
cy.findByText('Success!').should('exist')
cy.get('@setNeedsUpdate').should('have.been.calledWith', true)
})

it('on edit mode', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CollectionMother } from '@tests/component/collection/domain/models/Coll
import { MetadataBlockInfoMother } from '@tests/component/metadata-block-info/domain/models/MetadataBlockInfoMother'
import { UpwardHierarchyNodeMother } from '@tests/component/shared/hierarchy/domain/models/UpwardHierarchyNodeMother'
import { UserMother } from '@tests/component/users/domain/models/UserMother'
import { needsUpdateStore } from '@/notifications/domain/hooks/needsUpdateStore'

const collectionRepository: CollectionRepository = {} as CollectionRepository
const metadataBlockInfoRepository = {} as MetadataBlockInfoRepository
Expand Down Expand Up @@ -251,6 +252,7 @@ describe('EditCreateCollectionForm', () => {
})

it('submits a valid form and succeed', () => {
cy.spy(needsUpdateStore, 'setNeedsUpdate').as('setNeedsUpdate')
cy.customMount(
<EditCreateCollectionForm
mode="create"
Expand All @@ -269,6 +271,7 @@ describe('EditCreateCollectionForm', () => {

cy.findByText('Error').should('not.exist')
cy.findByText('Success!').should('exist')
cy.get('@setNeedsUpdate').should('have.been.calledWith', true)
})

it('submits a valid form and fails', () => {
Expand Down
Loading