Merge pull request #16007 from overleaf/jdt-writeful-user-settings

Add Writeful to user settings

GitOrigin-RevId: 15b3dd47b96cdc8bf8002afe3ddc570b03a6065f
This commit is contained in:
Jimmy Domagala-Tang 2023-12-07 09:29:45 -05:00 committed by Copybot
parent 785b35241e
commit 24261ac617
13 changed files with 256 additions and 8 deletions

View file

@ -442,7 +442,7 @@ const ProjectController = {
) )
User.findById( User.findById(
userId, userId,
'email first_name last_name referal_id signUpDate featureSwitches features featuresEpoch refProviders alphaProgram betaProgram isAdmin ace labsProgram completedTutorials', 'email first_name last_name referal_id signUpDate featureSwitches features featuresEpoch refProviders alphaProgram betaProgram isAdmin ace labsProgram completedTutorials writefull',
(err, user) => { (err, user) => {
// Handle case of deleted user // Handle case of deleted user
if (user == null) { if (user == null) {
@ -845,6 +845,9 @@ const ProjectController = {
featureSwitches: user.featureSwitches, featureSwitches: user.featureSwitches,
features: user.features, features: user.features,
refProviders: _.mapValues(user.refProviders, Boolean), refProviders: _.mapValues(user.refProviders, Boolean),
writefull: {
enabled: Boolean(user.writefull?.enabled),
},
alphaProgram: user.alphaProgram, alphaProgram: user.alphaProgram,
betaProgram: user.betaProgram, betaProgram: user.betaProgram,
labsProgram: user.labsProgram, labsProgram: user.labsProgram,
@ -1146,6 +1149,9 @@ const defaultSettingsForAnonymousUser = userId => ({
}, },
alphaProgram: false, alphaProgram: false,
betaProgram: false, betaProgram: false,
writefull: {
enabled: false,
},
}) })
const THEME_LIST = [ const THEME_LIST = [

View file

@ -89,6 +89,13 @@ async function settingsPage(req, res) {
} }
} }
// eslint-disable-next-line no-unused-vars -- getAssignment sets res.locals, which will pass to the splitTest context
const writefullSplitTest = await SplitTestHandler.promises.getAssignment(
req,
res,
'writefull-integration'
)
let personalAccessTokens let personalAccessTokens
if (showPersonalAccessToken || optionalPersonalAccessToken) { if (showPersonalAccessToken || optionalPersonalAccessToken) {
try { try {
@ -133,6 +140,9 @@ async function settingsPage(req, res) {
mendeley: Boolean(user.refProviders?.mendeley), mendeley: Boolean(user.refProviders?.mendeley),
zotero: Boolean(user.refProviders?.zotero), zotero: Boolean(user.refProviders?.zotero),
}, },
writefull: {
enabled: Boolean(user.writefull?.enabled),
},
}, },
hasPassword: !!user.hashedPassword, hasPassword: !!user.hashedPassword,
shouldAllowEditingDetails, shouldAllowEditingDetails,

View file

@ -172,6 +172,9 @@ const UserSchema = new Schema(
mendeley: Schema.Types.Mixed, mendeley: Schema.Types.Mixed,
zotero: Schema.Types.Mixed, zotero: Schema.Types.Mixed,
}, },
writefull: {
enabled: { type: Boolean, default: false },
},
alphaProgram: { type: Boolean, default: false }, // experimental features alphaProgram: { type: Boolean, default: false }, // experimental features
betaProgram: { type: Boolean, default: false }, betaProgram: { type: Boolean, default: false },
labsProgram: { type: Boolean, default: false }, labsProgram: { type: Boolean, default: false },

View file

@ -851,6 +851,7 @@ module.exports = {
sourceEditorComponents: [], sourceEditorComponents: [],
sourceEditorCompletionSources: [], sourceEditorCompletionSources: [],
sourceEditorSymbolPalette: [], sourceEditorSymbolPalette: [],
langFeedbackLinkingWidgets: [],
integrationLinkingWidgets: [], integrationLinkingWidgets: [],
referenceLinkingWidgets: [], referenceLinkingWidgets: [],
importProjectFromGithubModalWrapper: [], importProjectFromGithubModalWrapper: [],

View file

@ -602,6 +602,7 @@
"labs_program_already_participating": "", "labs_program_already_participating": "",
"labs_program_benefits": "", "labs_program_benefits": "",
"labs_program_not_participating": "", "labs_program_not_participating": "",
"language_feedback": "",
"large_or_high-resolution_images_taking_too_long": "", "large_or_high-resolution_images_taking_too_long": "",
"last_active": "", "last_active": "",
"last_active_description": "", "last_active_description": "",
@ -1335,7 +1336,9 @@
"try_recompile_project_or_troubleshoot": "", "try_recompile_project_or_troubleshoot": "",
"try_relinking_provider": "", "try_relinking_provider": "",
"try_to_compile_despite_errors": "", "try_to_compile_despite_errors": "",
"turn_off": "",
"turn_off_link_sharing": "", "turn_off_link_sharing": "",
"turn_on": "",
"turn_on_link_sharing": "", "turn_on_link_sharing": "",
"unarchive": "", "unarchive": "",
"uncategorized": "", "uncategorized": "",
@ -1444,6 +1447,9 @@
"work_offline": "", "work_offline": "",
"work_with_non_overleaf_users": "", "work_with_non_overleaf_users": "",
"would_you_like_to_receive_newsletter": "", "would_you_like_to_receive_newsletter": "",
"writefull": "",
"writefull_how_to": "",
"writefull_settings_description": "",
"x_changes_in": "", "x_changes_in": "",
"x_changes_in_plural": "", "x_changes_in_plural": "",
"x_price_for_first_month": "", "x_price_for_first_month": "",

View file

@ -6,6 +6,7 @@ import { useSSOContext, SSOSubscription } from '../context/sso-context'
import { SSOLinkingWidget } from './linking/sso-widget' import { SSOLinkingWidget } from './linking/sso-widget'
import getMeta from '../../../utils/meta' import getMeta from '../../../utils/meta'
import { useBroadcastUser } from '@/shared/hooks/user-channel/use-broadcast-user' import { useBroadcastUser } from '@/shared/hooks/user-channel/use-broadcast-user'
import { useSplitTestContext } from '@/shared/context/split-test-context'
function LinkingSection() { function LinkingSection() {
useBroadcastUser() useBroadcastUser()
@ -25,6 +26,11 @@ function LinkingSection() {
getMeta('referenceLinkingWidgets') || getMeta('referenceLinkingWidgets') ||
importOverleafModules('referenceLinkingWidgets') importOverleafModules('referenceLinkingWidgets')
) )
const [langFeedbackLinkingWidgets] = useState<any[]>(
() =>
getMeta('langFeedbackLinkingWidgets') ||
importOverleafModules('langFeedbackLinkingWidgets')
)
const oauth2ServerComponents = importOverleafModules('oauth2Server') as { const oauth2ServerComponents = importOverleafModules('oauth2Server') as {
import: { default: ElementType } import: { default: ElementType }
@ -39,6 +45,20 @@ function LinkingSection() {
? integrationLinkingWidgets.concat(oauth2ServerComponents) ? integrationLinkingWidgets.concat(oauth2ServerComponents)
: integrationLinkingWidgets : integrationLinkingWidgets
// currently the only thing that is in the langFeedback section is writefull,
// which is behind a split test. we should hide this section if the user is not in the split test
// todo: remove split test check, and split test context after gradual rollout is complete
const {
splitTestVariants,
}: { splitTestVariants: Record<string, string | undefined> } =
useSplitTestContext()
const shouldLoadWritefull =
splitTestVariants['writefull-integration'] === 'enabled' &&
!window.writefull // check if the writefull extension is installed, in which case we dont handle the integration
const haslangFeedbackLinkingWidgets =
langFeedbackLinkingWidgets.length && shouldLoadWritefull
const hasIntegrationLinkingSection = allIntegrationLinkingWidgets.length const hasIntegrationLinkingSection = allIntegrationLinkingWidgets.length
const hasReferencesLinkingSection = referenceLinkingWidgets.length const hasReferencesLinkingSection = referenceLinkingWidgets.length
@ -63,6 +83,7 @@ function LinkingSection() {
const hasSSOLinkingSection = Object.keys(subscriptions).length > 0 const hasSSOLinkingSection = Object.keys(subscriptions).length > 0
if ( if (
!haslangFeedbackLinkingWidgets &&
!hasIntegrationLinkingSection && !hasIntegrationLinkingSection &&
!hasReferencesLinkingSection && !hasReferencesLinkingSection &&
!hasSSOLinkingSection !hasSSOLinkingSection
@ -74,6 +95,24 @@ function LinkingSection() {
<> <>
<h3>{t('integrations')}</h3> <h3>{t('integrations')}</h3>
<p className="small">{t('linked_accounts_explained')}</p> <p className="small">{t('linked_accounts_explained')}</p>
{haslangFeedbackLinkingWidgets ? (
<>
<h3 id="project-sync" className="text-capitalize">
{t('language_feedback')}
</h3>
<div className="settings-widgets-container">
{langFeedbackLinkingWidgets.map(
({ import: { default: widget }, path }, widgetIndex) => (
<ModuleLinkingWidget
key={path}
ModuleComponent={widget}
isLast={widgetIndex === langFeedbackLinkingWidgets.length - 1}
/>
)
)}
</div>
</>
) : null}
{hasIntegrationLinkingSection ? ( {hasIntegrationLinkingSection ? (
<> <>
<h3 id="project-sync" className="text-capitalize"> <h3 id="project-sync" className="text-capitalize">
@ -140,7 +179,8 @@ function LinkingSection() {
</div> </div>
</> </>
) : null} ) : null}
{hasIntegrationLinkingSection || {haslangFeedbackLinkingWidgets ||
hasIntegrationLinkingSection ||
hasReferencesLinkingSection || hasReferencesLinkingSection ||
hasSSOLinkingSection ? ( hasSSOLinkingSection ? (
<hr /> <hr />

View file

@ -0,0 +1,125 @@
import { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from 'react-bootstrap'
import { sendMB } from '@/infrastructure/event-tracking'
function trackUpgradeClick() {
sendMB('settings-upgrade-click')
}
type EnableWidgetProps = {
logo: ReactNode
title: string
description: string
helpPath: string
hasFeature?: boolean
isPremiumFeature?: boolean
statusIndicator?: ReactNode
children?: ReactNode
linked?: boolean
handleLinkClick: () => void
handleUnlinkClick: () => void
disabled?: boolean
}
export function EnableWidget({
logo,
title,
description,
helpPath,
hasFeature,
isPremiumFeature,
statusIndicator,
linked,
handleLinkClick,
handleUnlinkClick,
children,
disabled,
}: EnableWidgetProps) {
const { t } = useTranslation()
return (
<div className="settings-widget-container">
<div>{logo}</div>
<div className="description-container">
<div className="title-row">
<h4>{title}</h4>
{!hasFeature && isPremiumFeature && (
<span className="label label-info">{t('premium_feature')}</span>
)}
</div>
<p className="small">
{description}{' '}
<a href={helpPath} target="_blank" rel="noreferrer">
{t('learn_more')}
</a>
</p>
{children}
{hasFeature && statusIndicator}
</div>
<div>
<ActionButton
hasFeature={hasFeature}
linked={linked}
handleUnlinkClick={handleUnlinkClick}
handleLinkClick={handleLinkClick}
disabled={disabled}
/>
</div>
</div>
)
}
type ActionButtonProps = {
hasFeature?: boolean
linked?: boolean
handleUnlinkClick: () => void
handleLinkClick: () => void
disabled?: boolean
}
function ActionButton({
linked,
handleUnlinkClick,
handleLinkClick,
hasFeature,
disabled,
}: ActionButtonProps) {
const { t } = useTranslation()
if (!hasFeature) {
return (
<Button
bsStyle={null}
className="btn-primary"
href="/user/subscription/plans"
onClick={trackUpgradeClick}
>
<span className="text-capitalize">{t('upgrade')}</span>
</Button>
)
} else if (linked) {
return (
<Button
className="btn-danger-ghost"
onClick={handleUnlinkClick}
bsStyle={null}
disabled={disabled}
>
{t('turn_off')}
</Button>
)
} else {
return (
<Button
disabled={disabled}
bsStyle={null}
onClick={handleLinkClick}
className="btn btn-secondary-info btn-secondary text-capitalize"
>
{t('turn_on')}
</Button>
)
}
}
export default EnableWidget

View file

@ -14,6 +14,7 @@ import LeaveSection from './leave-section'
import * as eventTracking from '../../../infrastructure/event-tracking' import * as eventTracking from '../../../infrastructure/event-tracking'
import { UserProvider } from '../../../shared/context/user-context' import { UserProvider } from '../../../shared/context/user-context'
import { SSOProvider } from '../context/sso-context' import { SSOProvider } from '../context/sso-context'
import { SplitTestProvider } from '@/shared/context/split-test-context'
import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n' import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n'
import useScrollToIdOnLoad from '../../../shared/hooks/use-scroll-to-id-on-load' import useScrollToIdOnLoad from '../../../shared/hooks/use-scroll-to-id-on-load'
import { ExposedSettings } from '../../../../../types/exposed-settings' import { ExposedSettings } from '../../../../../types/exposed-settings'
@ -63,9 +64,11 @@ function SettingsPageContent() {
</div> </div>
</div> </div>
<hr /> <hr />
<SSOProvider> <SplitTestProvider>
<LinkingSection /> <SSOProvider>
</SSOProvider> <LinkingSection />
</SSOProvider>
</SplitTestProvider>
{isOverleaf ? ( {isOverleaf ? (
<> <>
<BetaProgramSection /> <BetaProgramSection />

View file

@ -30,6 +30,9 @@ UserContext.Provider.propTypes = {
mendeley: PropTypes.boolean, mendeley: PropTypes.boolean,
zotero: PropTypes.boolean, zotero: PropTypes.boolean,
}), }),
writefull: PropTypes.shape({
enabled: PropTypes.boolean,
}),
}), }),
}), }),
} }

File diff suppressed because one or more lines are too long

View file

@ -50,6 +50,7 @@
"activate_account": "Activate your account", "activate_account": "Activate your account",
"activating": "Activating", "activating": "Activating",
"activation_token_expired": "Your activation token has expired, you will need to get another one sent to you.", "activation_token_expired": "Your activation token has expired, you will need to get another one sent to you.",
"active": "Active",
"add": "Add", "add": "Add",
"add_additional_certificate": "Add another certificate", "add_additional_certificate": "Add another certificate",
"add_affiliation": "Add Affiliation", "add_affiliation": "Add Affiliation",
@ -921,6 +922,7 @@
"labs_program_benefits": "__appName__ is always looking for new ways to help users work more quickly and effectively. By joining Overleaf Labs, you can participate in experiments that explore innovative ideas in the space of collaborative writing and publishing.", "labs_program_benefits": "__appName__ is always looking for new ways to help users work more quickly and effectively. By joining Overleaf Labs, you can participate in experiments that explore innovative ideas in the space of collaborative writing and publishing.",
"labs_program_not_participating": "You are not enrolled in Labs", "labs_program_not_participating": "You are not enrolled in Labs",
"language": "Language", "language": "Language",
"language_feedback": "Language Feedback",
"large_or_high-resolution_images_taking_too_long": "Large or high-resolution images taking too long to process. You may be able to <0>optimize them</0>.", "large_or_high-resolution_images_taking_too_long": "Large or high-resolution images taking too long to process. You may be able to <0>optimize them</0>.",
"last_active": "Last Active", "last_active": "Last Active",
"last_active_description": "Last time a project was opened.", "last_active_description": "Last time a project was opened.",
@ -1950,7 +1952,9 @@
"try_recompile_project_or_troubleshoot": "Please try recompiling the project from scratch, and if that doesnt help, follow our <0>troubleshooting guide</0>.", "try_recompile_project_or_troubleshoot": "Please try recompiling the project from scratch, and if that doesnt help, follow our <0>troubleshooting guide</0>.",
"try_relinking_provider": "It looks like you need to re-link your __provider__ account.", "try_relinking_provider": "It looks like you need to re-link your __provider__ account.",
"try_to_compile_despite_errors": "Try to compile despite errors", "try_to_compile_despite_errors": "Try to compile despite errors",
"turn_off": "Turn off",
"turn_off_link_sharing": "Turn off link sharing", "turn_off_link_sharing": "Turn off link sharing",
"turn_on": "Turn on",
"turn_on_link_sharing": "Turn on link sharing", "turn_on_link_sharing": "Turn on link sharing",
"tutorials": "Tutorials", "tutorials": "Tutorials",
"two_users": "2 users", "two_users": "2 users",
@ -2099,6 +2103,9 @@
"work_with_word_users_blurb": "__appName__ is so easy to get started with that youll be able to invite your non-LaTeX colleagues to contribute directly to your LaTeX documents. Theyll be productive from day one and be able to pick up small amounts of LaTeX as they go.", "work_with_word_users_blurb": "__appName__ is so easy to get started with that youll be able to invite your non-LaTeX colleagues to contribute directly to your LaTeX documents. Theyll be productive from day one and be able to pick up small amounts of LaTeX as they go.",
"would_you_like_to_receive_newsletter": "Would you like to receive tailored content from Overleaf?", "would_you_like_to_receive_newsletter": "Would you like to receive tailored content from Overleaf?",
"would_you_like_to_see_a_university_subscription": "Would you like to see a university-wide __appName__ subscription at your university?", "would_you_like_to_see_a_university_subscription": "Would you like to see a university-wide __appName__ subscription at your university?",
"writefull": "Writefull",
"writefull_how_to": "[COPY PLACEHOLDER] to use Writefull, select some text and press the shortcut key. a toolbar will appear with the results.",
"writefull_settings_description": "Writefull is a new feature that helps you improve your writing by highlighting common errors and suggesting improvements.",
"x_changes_in": "__count__ change in", "x_changes_in": "__count__ change in",
"x_changes_in_plural": "__count__ changes in", "x_changes_in_plural": "__count__ changes in",
"x_collaborators_per_project": "__collaboratorsCount__ collaborators per project", "x_collaborators_per_project": "__collaboratorsCount__ collaborators per project",

View file

@ -4,13 +4,16 @@ import fetchMock from 'fetch-mock'
import LinkingSection from '../../../../../frontend/js/features/settings/components/linking-section' import LinkingSection from '../../../../../frontend/js/features/settings/components/linking-section'
import { UserProvider } from '../../../../../frontend/js/shared/context/user-context' import { UserProvider } from '../../../../../frontend/js/shared/context/user-context'
import { SSOProvider } from '../../../../../frontend/js/features/settings/context/sso-context' import { SSOProvider } from '../../../../../frontend/js/features/settings/context/sso-context'
import { SplitTestProvider } from '@/shared/context/split-test-context'
function renderSectionWithProviders() { function renderSectionWithProviders() {
render(<LinkingSection />, { render(<LinkingSection />, {
wrapper: ({ children }) => ( wrapper: ({ children }) => (
<UserProvider> <SplitTestProvider>
<SSOProvider>{children}</SSOProvider> <UserProvider>
</UserProvider> <SSOProvider>{children}</SSOProvider>
</UserProvider>
</SplitTestProvider>
), ),
}) })
} }
@ -47,6 +50,7 @@ describe('<LinkingSection />', function () {
// all environments // all environments
window.metaAttributesCache.set('integrationLinkingWidgets', []) window.metaAttributesCache.set('integrationLinkingWidgets', [])
window.metaAttributesCache.set('referenceLinkingWidgets', []) window.metaAttributesCache.set('referenceLinkingWidgets', [])
window.metaAttributesCache.set('integrationLinkingWidgets', [])
window.metaAttributesCache.set('ol-thirdPartyIds', { window.metaAttributesCache.set('ol-thirdPartyIds', {
google: 'google-id', google: 'google-id',

View file

@ -42,5 +42,6 @@ declare global {
useRecaptchaNet?: boolean useRecaptchaNet?: boolean
} }
expectingLinkedFileRefreshedSocketFor?: string | null expectingLinkedFileRefreshedSocketFor?: string | null
writefull?: Map<string, any>
} }
} }