Просмотр исходного кода

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

GitOrigin-RevId: 8d6e1c2d7cb63c1597d952d546121f030b79d126
Kristina 6 месяцев назад
Родитель
Сommit
21ea6ab989

+ 2 - 0
services/web/frontend/js/features/settings/components/settings-modal.tsx

@@ -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

+ 26 - 0
services/web/frontend/js/features/settings/hooks/use-open-settings-via-query-param.tsx

@@ -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])
+}

+ 57 - 1
services/web/test/frontend/features/settings-modal/settings-modal.test.tsx

@@ -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'
+        )
+      })
+    })
+  })
 })

+ 221 - 0
services/web/test/frontend/features/settings-modal/settings/project-notifications-setting.test.tsx

@@ -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
+  })
+})