From ab0ae4aba873f3655cd6c4892b56fb024c23f334 Mon Sep 17 00:00:00 2001 From: Jimmy Domagala-Tang Date: Thu, 7 Dec 2023 09:29:45 -0500 Subject: [PATCH] Merge pull request #16007 from overleaf/jdt-writeful-user-settings Add Writeful to user settings GitOrigin-RevId: 15b3dd47b96cdc8bf8002afe3ddc570b03a6065f --- .../src/Features/Project/ProjectController.js | 8 +- .../src/Features/User/UserPagesController.js | 10 ++ services/web/app/src/models/User.js | 3 + services/web/config/settings.defaults.js | 1 + .../web/frontend/extracted-translations.json | 6 + .../settings/components/linking-section.tsx | 42 +++++- .../components/linking/enable-widget.tsx | 125 ++++++++++++++++++ .../js/features/settings/components/root.tsx | 9 +- .../js/shared/context/user-context.jsx | 3 + .../js/shared/svgs/writefull-logo.jsx | 39 ++++++ services/web/locales/en.json | 7 + .../components/linking-section.test.tsx | 10 +- services/web/types/window.ts | 1 + 13 files changed, 256 insertions(+), 8 deletions(-) create mode 100644 services/web/frontend/js/features/settings/components/linking/enable-widget.tsx create mode 100644 services/web/frontend/js/shared/svgs/writefull-logo.jsx diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index 51e898d2c8..49b12c98cd 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -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 = [ diff --git a/services/web/app/src/Features/User/UserPagesController.js b/services/web/app/src/Features/User/UserPagesController.js index a38c4061d2..0f67d632d8 100644 --- a/services/web/app/src/Features/User/UserPagesController.js +++ b/services/web/app/src/Features/User/UserPagesController.js @@ -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, diff --git a/services/web/app/src/models/User.js b/services/web/app/src/models/User.js index 312e4c107c..2f532e51f4 100644 --- a/services/web/app/src/models/User.js +++ b/services/web/app/src/models/User.js @@ -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 }, diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index 15a3b3623e..da08ac3c4f 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -851,6 +851,7 @@ module.exports = { sourceEditorComponents: [], sourceEditorCompletionSources: [], sourceEditorSymbolPalette: [], + langFeedbackLinkingWidgets: [], integrationLinkingWidgets: [], referenceLinkingWidgets: [], importProjectFromGithubModalWrapper: [], diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 2e1156fa39..52250c47af 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -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": "", diff --git a/services/web/frontend/js/features/settings/components/linking-section.tsx b/services/web/frontend/js/features/settings/components/linking-section.tsx index 0d02a5c250..e5d81901ad 100644 --- a/services/web/frontend/js/features/settings/components/linking-section.tsx +++ b/services/web/frontend/js/features/settings/components/linking-section.tsx @@ -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( + () => + 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 } = + 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() { <>

{t('integrations')}

{t('linked_accounts_explained')}

+ {haslangFeedbackLinkingWidgets ? ( + <> +

+ {t('language_feedback')} +

+
+ {langFeedbackLinkingWidgets.map( + ({ import: { default: widget }, path }, widgetIndex) => ( + + ) + )} +
+ + ) : null} {hasIntegrationLinkingSection ? ( <>

@@ -140,7 +179,8 @@ function LinkingSection() { ) : null} - {hasIntegrationLinkingSection || + {haslangFeedbackLinkingWidgets || + hasIntegrationLinkingSection || hasReferencesLinkingSection || hasSSOLinkingSection ? (
diff --git a/services/web/frontend/js/features/settings/components/linking/enable-widget.tsx b/services/web/frontend/js/features/settings/components/linking/enable-widget.tsx new file mode 100644 index 0000000000..998d7feda0 --- /dev/null +++ b/services/web/frontend/js/features/settings/components/linking/enable-widget.tsx @@ -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 ( +
+
{logo}
+
+
+

{title}

+ {!hasFeature && isPremiumFeature && ( + {t('premium_feature')} + )} +
+

+ {description}{' '} + + {t('learn_more')} + +

+ {children} + {hasFeature && statusIndicator} +
+
+ +
+
+ ) +} + +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 ( + + ) + } else if (linked) { + return ( + + ) + } else { + return ( + + ) + } +} + +export default EnableWidget diff --git a/services/web/frontend/js/features/settings/components/root.tsx b/services/web/frontend/js/features/settings/components/root.tsx index 46f8943365..ccad776df7 100644 --- a/services/web/frontend/js/features/settings/components/root.tsx +++ b/services/web/frontend/js/features/settings/components/root.tsx @@ -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() {
- - - + + + + + {isOverleaf ? ( <> diff --git a/services/web/frontend/js/shared/context/user-context.jsx b/services/web/frontend/js/shared/context/user-context.jsx index 8c2ba0c256..186bccdbc8 100644 --- a/services/web/frontend/js/shared/context/user-context.jsx +++ b/services/web/frontend/js/shared/context/user-context.jsx @@ -30,6 +30,9 @@ UserContext.Provider.propTypes = { mendeley: PropTypes.boolean, zotero: PropTypes.boolean, }), + writefull: PropTypes.shape({ + enabled: PropTypes.boolean, + }), }), }), } diff --git a/services/web/frontend/js/shared/svgs/writefull-logo.jsx b/services/web/frontend/js/shared/svgs/writefull-logo.jsx new file mode 100644 index 0000000000..ad0ec12d88 --- /dev/null +++ b/services/web/frontend/js/shared/svgs/writefull-logo.jsx @@ -0,0 +1,39 @@ +function WritefullLogo() { + return ( + + + + + + + + + + + ) +} + +export default WritefullLogo diff --git a/services/web/locales/en.json b/services/web/locales/en.json index eb0eadfc3f..c85c0e5f8e 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -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.", "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 doesn’t help, follow our <0>troubleshooting guide.", "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 you’ll be able to invite your non-LaTeX colleagues to contribute directly to your LaTeX documents. They’ll 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", diff --git a/services/web/test/frontend/features/settings/components/linking-section.test.tsx b/services/web/test/frontend/features/settings/components/linking-section.test.tsx index ca01a64188..317bace8d0 100644 --- a/services/web/test/frontend/features/settings/components/linking-section.test.tsx +++ b/services/web/test/frontend/features/settings/components/linking-section.test.tsx @@ -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(, { wrapper: ({ children }) => ( - - {children} - + + + {children} + + ), }) } @@ -47,6 +50,7 @@ describe('', function () { // all environments window.metaAttributesCache.set('integrationLinkingWidgets', []) window.metaAttributesCache.set('referenceLinkingWidgets', []) + window.metaAttributesCache.set('integrationLinkingWidgets', []) window.metaAttributesCache.set('ol-thirdPartyIds', { google: 'google-id', diff --git a/services/web/types/window.ts b/services/web/types/window.ts index f787553e6e..79b514ddf8 100644 --- a/services/web/types/window.ts +++ b/services/web/types/window.ts @@ -42,5 +42,6 @@ declare global { useRecaptchaNet?: boolean } expectingLinkedFileRefreshedSocketFor?: string | null + writefull?: Map } }