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(
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) => {
// Handle case of deleted user
if (user == null) {
@ -845,6 +845,9 @@ const ProjectController = {
featureSwitches: user.featureSwitches,
features: user.features,
refProviders: _.mapValues(user.refProviders, Boolean),
writefull: {
enabled: Boolean(user.writefull?.enabled),
},
alphaProgram: user.alphaProgram,
betaProgram: user.betaProgram,
labsProgram: user.labsProgram,
@ -1146,6 +1149,9 @@ const defaultSettingsForAnonymousUser = userId => ({
},
alphaProgram: false,
betaProgram: false,
writefull: {
enabled: false,
},
})
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
if (showPersonalAccessToken || optionalPersonalAccessToken) {
try {
@ -133,6 +140,9 @@ async function settingsPage(req, res) {
mendeley: Boolean(user.refProviders?.mendeley),
zotero: Boolean(user.refProviders?.zotero),
},
writefull: {
enabled: Boolean(user.writefull?.enabled),
},
},
hasPassword: !!user.hashedPassword,
shouldAllowEditingDetails,

View file

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

View file

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

View file

@ -602,6 +602,7 @@
"labs_program_already_participating": "",
"labs_program_benefits": "",
"labs_program_not_participating": "",
"language_feedback": "",
"large_or_high-resolution_images_taking_too_long": "",
"last_active": "",
"last_active_description": "",
@ -1335,7 +1336,9 @@
"try_recompile_project_or_troubleshoot": "",
"try_relinking_provider": "",
"try_to_compile_despite_errors": "",
"turn_off": "",
"turn_off_link_sharing": "",
"turn_on": "",
"turn_on_link_sharing": "",
"unarchive": "",
"uncategorized": "",
@ -1444,6 +1447,9 @@
"work_offline": "",
"work_with_non_overleaf_users": "",
"would_you_like_to_receive_newsletter": "",
"writefull": "",
"writefull_how_to": "",
"writefull_settings_description": "",
"x_changes_in": "",
"x_changes_in_plural": "",
"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 getMeta from '../../../utils/meta'
import { useBroadcastUser } from '@/shared/hooks/user-channel/use-broadcast-user'
import { useSplitTestContext } from '@/shared/context/split-test-context'
function LinkingSection() {
useBroadcastUser()
@ -25,6 +26,11 @@ function LinkingSection() {
getMeta('referenceLinkingWidgets') ||
importOverleafModules('referenceLinkingWidgets')
)
const [langFeedbackLinkingWidgets] = useState<any[]>(
() =>
getMeta('langFeedbackLinkingWidgets') ||
importOverleafModules('langFeedbackLinkingWidgets')
)
const oauth2ServerComponents = importOverleafModules('oauth2Server') as {
import: { default: ElementType }
@ -39,6 +45,20 @@ function LinkingSection() {
? integrationLinkingWidgets.concat(oauth2ServerComponents)
: 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 hasReferencesLinkingSection = referenceLinkingWidgets.length
@ -63,6 +83,7 @@ function LinkingSection() {
const hasSSOLinkingSection = Object.keys(subscriptions).length > 0
if (
!haslangFeedbackLinkingWidgets &&
!hasIntegrationLinkingSection &&
!hasReferencesLinkingSection &&
!hasSSOLinkingSection
@ -74,6 +95,24 @@ function LinkingSection() {
<>
<h3>{t('integrations')}</h3>
<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 ? (
<>
<h3 id="project-sync" className="text-capitalize">
@ -140,7 +179,8 @@ function LinkingSection() {
</div>
</>
) : null}
{hasIntegrationLinkingSection ||
{haslangFeedbackLinkingWidgets ||
hasIntegrationLinkingSection ||
hasReferencesLinkingSection ||
hasSSOLinkingSection ? (
<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 { UserProvider } from '../../../shared/context/user-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 useScrollToIdOnLoad from '../../../shared/hooks/use-scroll-to-id-on-load'
import { ExposedSettings } from '../../../../../types/exposed-settings'
@ -63,9 +64,11 @@ function SettingsPageContent() {
</div>
</div>
<hr />
<SSOProvider>
<LinkingSection />
</SSOProvider>
<SplitTestProvider>
<SSOProvider>
<LinkingSection />
</SSOProvider>
</SplitTestProvider>
{isOverleaf ? (
<>
<BetaProgramSection />

View file

@ -30,6 +30,9 @@ UserContext.Provider.propTypes = {
mendeley: 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",
"activating": "Activating",
"activation_token_expired": "Your activation token has expired, you will need to get another one sent to you.",
"active": "Active",
"add": "Add",
"add_additional_certificate": "Add another certificate",
"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_not_participating": "You are not enrolled in Labs",
"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>.",
"last_active": "Last Active",
"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_relinking_provider": "It looks like you need to re-link your __provider__ account.",
"try_to_compile_despite_errors": "Try to compile despite errors",
"turn_off": "Turn off",
"turn_off_link_sharing": "Turn off link sharing",
"turn_on": "Turn on",
"turn_on_link_sharing": "Turn on link sharing",
"tutorials": "Tutorials",
"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.",
"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?",
"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_plural": "__count__ changes in",
"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 { UserProvider } from '../../../../../frontend/js/shared/context/user-context'
import { SSOProvider } from '../../../../../frontend/js/features/settings/context/sso-context'
import { SplitTestProvider } from '@/shared/context/split-test-context'
function renderSectionWithProviders() {
render(<LinkingSection />, {
wrapper: ({ children }) => (
<UserProvider>
<SSOProvider>{children}</SSOProvider>
</UserProvider>
<SplitTestProvider>
<UserProvider>
<SSOProvider>{children}</SSOProvider>
</UserProvider>
</SplitTestProvider>
),
})
}
@ -47,6 +50,7 @@ describe('<LinkingSection />', function () {
// all environments
window.metaAttributesCache.set('integrationLinkingWidgets', [])
window.metaAttributesCache.set('referenceLinkingWidgets', [])
window.metaAttributesCache.set('integrationLinkingWidgets', [])
window.metaAttributesCache.set('ol-thirdPartyIds', {
google: 'google-id',

View file

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