Merge pull request #32244 from overleaf/ab-labs-in-feature-flags

[web] Merge Labs programme into the Feature flags system

GitOrigin-RevId: db75e07bf3272becc11ef1eeda3850098b3daa9d
This commit is contained in:
Alexandre Bourdin
2026-03-26 09:06:51 +00:00
committed by Copybot
parent 6f3e0963b8
commit 671df33da3
25 changed files with 883 additions and 360 deletions
@@ -18,25 +18,21 @@ type LinkProps = {
onMouseDown?: MouseEventHandler<HTMLAnchorElement>
}
const defaultHref = '/beta/participate'
const BetaBadge: FC<{
tooltip?: TooltipProps
link?: LinkProps
description?: ReactNode
phase?: string
}> = ({
tooltip,
link = { href: defaultHref },
description,
phase = 'beta',
}) => {
}> = ({ tooltip, link = {}, description, phase = 'beta' }) => {
const { href, ...linkProps } = link
const linkedBadge = (
<a
target="_blank"
rel="noopener noreferrer"
href={href || defaultHref}
href={
href || (phase === 'labs' ? '/labs/participate' : '/beta/participate')
}
{...linkProps}
>
<span className="visually-hidden">{description || tooltip?.text}</span>
@@ -0,0 +1,38 @@
import { FC, useMemo } from 'react'
import { micromark } from 'micromark'
import DOMPurify from 'dompurify'
const PURIFY_CONFIG = {
ALLOWED_TAGS: ['#text', 'p', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href'],
}
const LINK_REL = 'noreferrer noopener'
const LINK_TARGET = '_BLANK'
function sanitizeDescription(description: string) {
DOMPurify.addHook('afterSanitizeAttributes', node => {
if (node.nodeName === 'A') {
node.setAttribute('rel', LINK_REL)
node.setAttribute('target', LINK_TARGET)
}
})
try {
return DOMPurify.sanitize(micromark(description), PURIFY_CONFIG)
} finally {
DOMPurify.removeHook('afterSanitizeAttributes')
}
}
/**
* Renders a labs experiment description from markdown to sanitized HTML.
* Only bold, italic, and links are supported.
*/
export const LabsDescription: FC<{ description: string }> = ({
description,
}) => {
const html = useMemo(() => sanitizeDescription(description), [description])
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
@@ -0,0 +1,21 @@
import React from 'react'
import MaterialIcon from '@/shared/components/material-icon'
type LabsExperimentIconProps = {
icon: string
}
const LabsExperimentIcon: React.FC<LabsExperimentIconProps> = ({ icon }) => {
if (!icon) {
return null
}
return (
<MaterialIcon
type={icon}
className="rounded bg-primary-subtle labs-experiment-icon"
/>
)
}
export default LabsExperimentIcon
@@ -73,14 +73,14 @@ export function LabsExperimentWidget({
<h3 className="h4">{title}</h3>
{optedIn && <OLBadge bg="info">{t('enabled')}</OLBadge>}
</div>
<p className="small">
<div className="small">
{optedIn && optedInDescription ? optedInDescription : description}{' '}
{helpPath && (
<a href={helpPath} target="_blank" rel="noreferrer">
{t('learn_more')}
</a>
)}
</p>
</div>
</div>
<div>
{optedIn && feedbackLink && (