Merge pull request #26683 from overleaf/ab-update-survey-form

Update survey form and preview + support custom button CTA

GitOrigin-RevId: 2b519ab1da1c8e7897b3135956f80619f4e901b4
This commit is contained in:
Alexandre Bourdin
2025-07-02 11:33:01 +02:00
committed by Copybot
parent 75d443934f
commit 36c4c65609
9 changed files with 71 additions and 124 deletions

View File

@@ -14,7 +14,7 @@ import UserGetter from '../User/UserGetter.js'
* determines if there is a survey to show, given current surveys and rollout percentages
* uses userId in computation, to ensure that rollout groups always contain same users
* @param {string} userId
* @returns {Promise<Survey | undefined>}
* @returns {Promise<Pick<Survey, 'name' | 'title' | 'text' | 'cta' | 'url'> | undefined>}
*/
async function getSurvey(userId) {
const survey = await SurveyCache.get(true)
@@ -27,7 +27,7 @@ async function getSurvey(userId) {
}
}
const { name, preText, linkText, url, options } = survey?.toObject() || {}
const { name, title, text, cta, url, options } = survey?.toObject() || {}
// default to full rollout for backwards compatibility
const rolloutPercentage = options?.rolloutPercentage || 100
if (!_userInRolloutPercentile(userId, name, rolloutPercentage)) {
@@ -53,7 +53,7 @@ async function getSurvey(userId) {
}
}
return { name, preText, linkText, url }
return { name, title, text, cta, url }
}
}

View File

@@ -9,15 +9,16 @@ async function getSurvey() {
}
}
async function updateSurvey({ name, preText, linkText, url, options }) {
async function updateSurvey({ name, title, text, cta, url, options }) {
validateOptions(options)
let survey = await getSurvey()
if (!survey) {
survey = new Survey()
}
survey.name = name
survey.preText = preText
survey.linkText = linkText
survey.title = title
survey.text = text
survey.cta = cta
survey.url = url
survey.options = options
await survey.save()

View File

@@ -19,14 +19,18 @@ const SurveySchema = new Schema(
message: `invalid, must match: ${NAME_REGEX}`,
},
},
preText: {
title: {
type: String,
required: true,
},
linkText: {
text: {
type: String,
required: true,
},
cta: {
type: String,
required: false,
},
url: {
type: String,
required: true,

View File

@@ -1,49 +0,0 @@
import NewProjectButton from '../new-project-button'
import SidebarFilters from './sidebar-filters'
import AddAffiliation, { useAddAffiliation } from '../add-affiliation'
import SurveyWidget from '../survey-widget'
import { usePersistedResize } from '../../../../shared/hooks/use-resize'
function Sidebar() {
const { show: showAddAffiliationWidget } = useAddAffiliation()
const { mousePos, getHandleProps, getTargetProps } = usePersistedResize({
name: 'project-sidebar',
})
return (
<div
className="project-list-sidebar-wrapper-react d-none d-md-block"
{...getTargetProps({
style: {
...(mousePos?.x && { flexBasis: `${mousePos.x}px` }),
},
})}
>
<div className="project-list-sidebar-subwrapper">
<aside className="project-list-sidebar-react">
<NewProjectButton id="new-project-button-sidebar" />
<SidebarFilters />
{showAddAffiliationWidget && <hr />}
<AddAffiliation />
</aside>
<div className="project-list-sidebar-survey-wrapper">
<SurveyWidget />
</div>
</div>
<div
{...getHandleProps({
style: {
position: 'absolute',
zIndex: 1,
top: 0,
right: '-2px',
height: '100%',
width: '4px',
},
})}
/>
</div>
)
}
export default Sidebar

View File

@@ -26,8 +26,8 @@ export function SurveyWidgetDsNav() {
<div className="notification-entry">
<div role="alert" className="survey-notification">
<div className="notification-body">
<p className="fw-bold fs-6 pe-4">{survey.preText}</p>
<p>{survey.linkText}</p>
<p className="fw-bold fs-6 pe-4">{survey.title}</p>
<p>{survey.text}</p>
<OLButton
variant="secondary"
size="sm"
@@ -35,7 +35,7 @@ export function SurveyWidgetDsNav() {
target="_blank"
rel="noreferrer noopener"
>
{t('take_survey')}
{survey.cta || t('take_survey')}
</OLButton>
</div>
<OLButton

View File

@@ -1,43 +0,0 @@
import usePersistedState from '../../../shared/hooks/use-persisted-state'
import getMeta from '../../../utils/meta'
import { useCallback } from 'react'
import Close from '@/shared/components/close'
export default function SurveyWidget() {
const survey = getMeta('ol-survey')
const [dismissedSurvey, setDismissedSurvey] = usePersistedState(
`dismissed-${survey?.name}`,
false
)
const dismissSurvey = useCallback(() => {
setDismissedSurvey(true)
}, [setDismissedSurvey])
if (!survey?.name || dismissedSurvey) {
return null
}
return (
<div className="user-notifications">
<div className="notification-entry">
<div role="alert" className="survey-notification">
<div className="notification-body">
{survey.preText}&nbsp;
<a
className="project-list-sidebar-survey-link"
href={survey.url}
target="_blank"
rel="noreferrer noopener"
>
{survey.linkText}
</a>
</div>
<div className="notification-close notification-close-button-style">
<Close variant="dark" onDismiss={() => dismissSurvey()} />
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,31 +1,32 @@
import SurveyWidget from '../../js/features/project-list/components/survey-widget'
import { SurveyWidgetDsNav } from '@/features/project-list/components/survey-widget-ds-nav'
export const Survey = (args: any) => {
localStorage.clear()
window.metaAttributesCache.set('ol-survey', {
name: 'my-survey',
preText: 'To help shape the future of Overleaf',
linkText: 'Click here!',
title: 'To help shape the future of Overleaf',
text: 'Click here!',
cta: 'Lets go!',
url: 'https://example.com/my-survey',
})
return <SurveyWidget {...args} />
return <SurveyWidgetDsNav {...args} />
}
export const UndefinedSurvey = (args: any) => {
localStorage.clear()
return <SurveyWidget {...args} />
return <SurveyWidgetDsNav {...args} />
}
export const EmptySurvey = (args: any) => {
localStorage.clear()
window.metaAttributesCache.set('ol-survey', {})
return <SurveyWidget {...args} />
return <SurveyWidgetDsNav {...args} />
}
export default {
title: 'Project List / Survey Widget',
component: SurveyWidget,
component: SurveyWidgetDsNav,
}

View File

@@ -1,13 +1,14 @@
import { expect } from 'chai'
import { fireEvent, render, screen } from '@testing-library/react'
import { SurveyWidgetDsNav } from '../../../../../frontend/js/features/project-list/components/survey-widget-ds-nav'
import { SurveyWidgetDsNav } from '@/features/project-list/components/survey-widget-ds-nav'
import { SplitTestProvider } from '@/shared/context/split-test-context'
describe('<SurveyWidgetDsNav />', function () {
beforeEach(function () {
this.name = 'my-survey'
this.preText = 'To help shape the future of Overleaf'
this.linkText = 'Click here!'
this.title = 'To help shape the future of Overleaf'
this.text = 'Click here!'
this.cta = 'Lets go!'
this.url = 'https://example.com/my-survey'
localStorage.clear()
@@ -17,8 +18,8 @@ describe('<SurveyWidgetDsNav />', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-survey', {
name: this.name,
preText: this.preText,
linkText: this.linkText,
title: this.title,
text: this.text,
url: this.url,
})
@@ -33,8 +34,8 @@ describe('<SurveyWidgetDsNav />', function () {
const dismissed = localStorage.getItem('dismissed-my-survey')
expect(dismissed).to.equal(null)
screen.getByText(this.preText)
screen.getByText(this.linkText)
screen.getByText(this.title)
screen.getByText(this.text)
const link = screen.getByRole('link', {
name: 'Take survey',
@@ -48,7 +49,7 @@ describe('<SurveyWidgetDsNav />', function () {
})
fireEvent.click(dismissButton)
const text = screen.queryByText(this.preText)
const text = screen.queryByText(this.title)
expect(text).to.be.null
const link = screen.queryByRole('button')
@@ -59,12 +60,43 @@ describe('<SurveyWidgetDsNav />', function () {
})
})
describe('survey widget is visible with custom CTA', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-survey', {
name: this.name,
title: this.title,
text: this.text,
cta: this.cta,
url: this.url,
})
render(
<SplitTestProvider>
<SurveyWidgetDsNav />
</SplitTestProvider>
)
})
it('shows text and link with custom CTA', function () {
const dismissed = localStorage.getItem('dismissed-my-survey')
expect(dismissed).to.equal(null)
screen.getByText(this.title)
screen.getByText(this.text)
const link = screen.getByRole('link', {
name: this.cta,
}) as HTMLAnchorElement
expect(link.href).to.equal(this.url)
})
})
describe('survey widget is not shown when already dismissed', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-survey', {
name: this.name,
preText: this.preText,
linkText: this.linkText,
title: this.title,
text: this.text,
url: this.url,
})
localStorage.setItem('dismissed-my-survey', 'true')
@@ -77,7 +109,7 @@ describe('<SurveyWidgetDsNav />', function () {
})
it('nothing is displayed', function () {
const text = screen.queryByText(this.preText)
const text = screen.queryByText(this.title)
expect(text).to.be.null
const link = screen.queryByRole('button')
@@ -95,7 +127,7 @@ describe('<SurveyWidgetDsNav />', function () {
})
it('nothing is displayed', function () {
const text = screen.queryByText(this.preText)
const text = screen.queryByText(this.title)
expect(text).to.be.null
const link = screen.queryByRole('button')

View File

@@ -1,6 +1,7 @@
export type Survey = {
name: string
preText: string
linkText: string
title: string
text: string
cta?: string
url: string
}