[web] open project notification settings from URL (#32689)

GitOrigin-RevId: 8d6e1c2d7cb63c1597d952d546121f030b79d126
This commit is contained in:
Kristina
2026-04-15 11:04:10 +02:00
committed by Copybot
parent be2e875bc5
commit 21ea6ab989
4 changed files with 306 additions and 1 deletions
@@ -11,6 +11,7 @@ import {
useSettingsModalContext,
} from '../context/settings-modal-context'
import useFocusOnSetting from '../hooks/use-focus-on-setting'
import useOpenSettingsViaQueryParam from '../hooks/use-open-settings-via-query-param'
const SettingsModalWrapper = () => {
return (
@@ -26,6 +27,7 @@ const SettingsModal = () => {
useSettingsModalContext()
useFocusOnSetting()
useOpenSettingsViaQueryParam()
return (
<OLModal
@@ -0,0 +1,26 @@
import { useEffect } from 'react'
import { useSettingsModalContext } from '../context/settings-modal-context'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
export default function useOpenSettingsViaQueryParam() {
const { setShow, setActiveTab } = useSettingsModalContext()
useEffect(() => {
const inNotificationsSplitTest = isSplitTestEnabled('email-notifications')
if (!inNotificationsSplitTest) {
return
}
const params = new URLSearchParams(window.location.search)
if (params.get('open') !== 'project-notifications') {
return
}
setShow(true)
setActiveTab('project_notifications')
const url = new URL(window.location.href)
url.searchParams.delete('open')
window.history.replaceState(window.history.state, '', url.toString())
}, [setShow, setActiveTab])
}
@@ -1,4 +1,4 @@
import { screen, render } from '@testing-library/react'
import { screen, render, waitFor } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import { EditorProviders } from '../../helpers/editor-providers'
@@ -131,4 +131,60 @@ describe('<SettingsModal />', function () {
settings.forEach(setting => assertSettingIsVisible(setting))
})
})
describe('when open=project-notifications query param is present', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-splitTestVariants', {
'email-notifications': 'enabled',
})
fetchMock.get(/\/notifications\/preferences\/project\//, {
trackedChangesOnOwnProject: false,
trackedChangesOnInvitedProject: false,
commentOnOwnProject: false,
commentOnInvitedProject: false,
repliesOnOwnProject: false,
repliesOnInvitedProject: false,
repliesOnAuthoredThread: false,
repliesOnParticipatingThread: false,
})
window.history.pushState({}, '', '?open=project-notifications')
})
afterEach(function () {
window.history.pushState({}, '', window.location.pathname)
window.metaAttributesCache.delete('ol-splitTestVariants')
})
it('opens the modal and selects the Project notifications tab', async function () {
render(
<EditorProviders rootFolder={[rootFolder as any]}>
<SettingsModal />
</EditorProviders>
)
await waitFor(
() =>
expect(
screen.getByRole('tab', {
name: /Project notifications/,
selected: true,
})
).to.exist
)
})
it('removes the open param from the URL', async function () {
render(
<EditorProviders rootFolder={[rootFolder as any]}>
<SettingsModal />
</EditorProviders>
)
await waitFor(() => {
expect(window.location.search).to.not.include(
'open=project-notifications'
)
})
})
})
})
@@ -0,0 +1,221 @@
import { screen, render, waitFor } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import userEvent from '@testing-library/user-event'
import { EditorProviders, PROJECT_ID } from '../../../helpers/editor-providers'
import { SettingsModalProvider } from '@/features/settings/context/settings-modal-context'
import ProjectNotificationsSetting from '@/features/settings/components/editor-settings/project-notifications-setting'
const preferencesUrl = `/notifications/preferences/project/${PROJECT_ID}`
const allNotificationsOn = {
trackedChangesOnOwnProject: true,
trackedChangesOnInvitedProject: true,
commentOnOwnProject: true,
commentOnInvitedProject: true,
repliesOnOwnProject: true,
repliesOnInvitedProject: true,
repliesOnAuthoredThread: true,
repliesOnParticipatingThread: true,
}
const repliesOnlyPreferences = {
trackedChangesOnOwnProject: false,
trackedChangesOnInvitedProject: false,
commentOnOwnProject: false,
commentOnInvitedProject: false,
repliesOnOwnProject: false,
repliesOnInvitedProject: false,
repliesOnAuthoredThread: true,
repliesOnParticipatingThread: true,
}
const allNotificationsOff = {
trackedChangesOnOwnProject: false,
trackedChangesOnInvitedProject: false,
commentOnOwnProject: false,
commentOnInvitedProject: false,
repliesOnOwnProject: false,
repliesOnInvitedProject: false,
repliesOnAuthoredThread: false,
repliesOnParticipatingThread: false,
}
function renderComponent() {
return render(
<EditorProviders>
<SettingsModalProvider>
<ProjectNotificationsSetting />
</SettingsModalProvider>
</EditorProviders>
)
}
describe('<ProjectNotificationsSetting />', function () {
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('selects "All project activity" when all notifications are on', async function () {
fetchMock.get(preferencesUrl, allNotificationsOn)
renderComponent()
await waitFor(
() =>
expect(
(
screen.getByLabelText('All project activity', {
exact: false,
}) as HTMLInputElement
).checked
).to.be.true
)
expect(
(
screen.getByLabelText('Replies to your activity only', {
exact: false,
}) as HTMLInputElement
).checked
).to.be.false
expect(
(screen.getByLabelText('Off', { exact: false }) as HTMLInputElement)
.checked
).to.be.false
})
it('selects "Replies to your activity only" when only reply notifications are on', async function () {
fetchMock.get(preferencesUrl, repliesOnlyPreferences)
renderComponent()
await waitFor(
() =>
expect(
(
screen.getByLabelText('Replies to your activity only', {
exact: false,
}) as HTMLInputElement
).checked
).to.be.true
)
expect(
(
screen.getByLabelText('All project activity', {
exact: false,
}) as HTMLInputElement
).checked
).to.be.false
expect(
(screen.getByLabelText('Off', { exact: false }) as HTMLInputElement)
.checked
).to.be.false
})
it('selects "Off" when all notifications are off', async function () {
fetchMock.get(preferencesUrl, allNotificationsOff)
renderComponent()
await waitFor(
() =>
expect(
(screen.getByLabelText('Off', { exact: false }) as HTMLInputElement)
.checked
).to.be.true
)
expect(
(
screen.getByLabelText('All project activity', {
exact: false,
}) as HTMLInputElement
).checked
).to.be.false
expect(
(
screen.getByLabelText('Replies to your activity only', {
exact: false,
}) as HTMLInputElement
).checked
).to.be.false
})
it('POSTs "replies" preferences when "Replies to your activity only" is selected', async function () {
fetchMock.get(preferencesUrl, allNotificationsOn)
const saveMock = fetchMock.post(preferencesUrl, { status: 200 })
renderComponent()
await waitFor(
() =>
expect(
(
screen.getByLabelText('All project activity', {
exact: false,
}) as HTMLInputElement
).checked
).to.be.true
)
await userEvent.click(
screen.getByLabelText('Replies to your activity only', { exact: false })
)
expect(
saveMock.callHistory.called(preferencesUrl, {
body: repliesOnlyPreferences,
})
).to.be.true
})
it('POSTs "off" preferences when "Off" is selected', async function () {
fetchMock.get(preferencesUrl, allNotificationsOn)
const saveMock = fetchMock.post(preferencesUrl, { status: 200 })
renderComponent()
await waitFor(
() =>
expect(
(
screen.getByLabelText('All project activity', {
exact: false,
}) as HTMLInputElement
).checked
).to.be.true
)
await userEvent.click(screen.getByLabelText('Off', { exact: false }))
expect(
saveMock.callHistory.called(preferencesUrl, {
body: allNotificationsOff,
})
).to.be.true
})
it('POSTs "all" preferences when "All project activity" is selected', async function () {
fetchMock.get(preferencesUrl, allNotificationsOff)
const saveMock = fetchMock.post(preferencesUrl, { status: 200 })
renderComponent()
await waitFor(
() =>
expect(
(screen.getByLabelText('Off', { exact: false }) as HTMLInputElement)
.checked
).to.be.true
)
await userEvent.click(
screen.getByLabelText('All project activity', { exact: false })
)
expect(
saveMock.callHistory.called(preferencesUrl, {
body: allNotificationsOn,
})
).to.be.true
})
})