diff --git a/services/web/app/src/infrastructure/ExpressLocals.js b/services/web/app/src/infrastructure/ExpressLocals.js index a9d5b0947a..a749bd64f5 100644 --- a/services/web/app/src/infrastructure/ExpressLocals.js +++ b/services/web/app/src/infrastructure/ExpressLocals.js @@ -369,7 +369,8 @@ module.exports = function(webRouter, privateApiRouter, publicApiRouter) { sentryAllowedOriginRegex: Settings.sentry.allowedOriginRegex, sentryDsn: Settings.sentry.publicDSN, sentryEnvironment: Settings.sentry.environment, - sentryRelease: Settings.sentry.release + sentryRelease: Settings.sentry.release, + enableSubscriptions: Settings.enableSubscriptions } next() }) diff --git a/services/web/frontend/extracted-translation-keys.json b/services/web/frontend/extracted-translation-keys.json index c2ea31aa17..2e255fae4c 100644 --- a/services/web/frontend/extracted-translation-keys.json +++ b/services/web/frontend/extracted-translation-keys.json @@ -1,55 +1,82 @@ [ + "ask_proj_owner_to_upgrade_for_longer_compiles", "auto_compile", "autocompile_disabled_reason", "autocompile_disabled", - "autocomplete", "autocomplete_references", + "autocomplete", + "blocked_filename", + "cancel", "clear_cached_files", "clsi_maintenance", "clsi_unavailable", + "collabs_per_proj", "collapse", "common", "compile_error_description", "compile_error_entry_description", + "compile_larger_projects", "compile_mode", "compile_terminated_by_user", "compiling", "conflicting_paths_found", + "copy_project", + "copy", + "copying", + "create", + "creating", + "delete", + "deleting", "dismiss_error_popup", + "done", "download_file", "download_pdf", + "duplicate_file", "duplicate_paths_found", "editing", + "error", "expand", "fast", - "file_outline", - "file_already_exists", "file_already_exists_in_this_location", - "blocked_filename", + "file_already_exists", + "file_outline", + "file_tree_badge_tooltip", "files_cannot_include_invalid_characters", "find_out_more_about_the_file_outline", "first_error_popup_label", "following_paths_conflict", + "free_accounts_have_timeout_upgrade_to_increase", + "full_doc_history", "full_screen", + "generic_something_went_wrong", "go_to_error_location", + "headers", "hide_outline", "hotkeys", "ignore_validation_errors", + "invalid_file_name", "latex_error", "learn_how_to_make_documents_compile_quickly", "loading", "log_entry_description", "log_hint_extra_info", "main_file_not_found", - "navigation", + "math_display", + "math_inline", "n_errors_plural", "n_errors", + "n_items", "n_warnings_plural", "n_warnings", "navigate_log_source", + "navigation", + "new_file", + "new_folder", + "new_name", "no_messages", "normal", "off", + "ok", "on", "other_logs_and_files", "other_output_files", @@ -58,7 +85,9 @@ "pdf_compile_try_again", "pdf_rendering_error", "please_compile_pdf_before_download", + "please_refresh", "please_set_main_file", + "plus_upgraded_accounts_receive", "proj_timed_out_reason", "project_flagged_too_many_compiles", "project_too_large_please_reduce", @@ -67,6 +96,8 @@ "raw_logs", "recompile_from_scratch", "recompile", + "refresh", + "rename", "review", "run_syntax_check_now", "send_first_message", @@ -75,13 +106,21 @@ "something_went_wrong_rendering_pdf", "somthing_went_wrong_compiling", "split_screen", + "start_free_trial", "stop_compile", "stop_on_validation_error", + "sure_you_want_to_delete", + "sync_to_dropbox", + "sync_to_github", "terminated", "timedout", "toggle_compile_options_menu", "toggle_output_files_list", "too_recently_compiled", + "total_words", + "unlimited_projects", + "upgrade_for_longer_compiles", + "upload", "validation_issue_description", "validation_issue_entry_description", "view_all_errors", @@ -89,39 +128,7 @@ "view_pdf", "view_warnings", "we_cant_find_any_sections_or_subsections_in_this_file", - "your_message", - "your_project_has_errors", - "copy_project", - "copying", - "copy", - "new_name", - "recompile_from_scratch", - "run_syntax_check_now", - "toggle_compile_options_menu", - "sure_you_want_to_delete", - "delete", - "deleting", - "cancel", - "new_file", - "new_folder", - "create", - "creating", - "upload", - "rename", - "n_items", - "please_refresh", - "generic_something_went_wrong", - "refresh", - "duplicate_file", - "error", - "invalid_file_name", - "ok", - "refresh", "word_count", - "total_words", - "headers", - "math_inline", - "math_display", - "done", - "file_tree_badge_tooltip" -] + "your_message", + "your_project_has_errors" +] \ No newline at end of file diff --git a/services/web/frontend/js/features/preview/components/preview-error.js b/services/web/frontend/js/features/preview/components/preview-error.js index d815a76726..365650e0ac 100644 --- a/services/web/frontend/js/features/preview/components/preview-error.js +++ b/services/web/frontend/js/features/preview/components/preview-error.js @@ -2,8 +2,17 @@ import React from 'react' import PropTypes from 'prop-types' import { useTranslation } from 'react-i18next' import PreviewLogsPaneEntry from './preview-logs-pane-entry' +import Icon from '../../../shared/components/icon' +import { useApplicationContext } from '../../../shared/context/application-context' +import { useEditorContext } from '../../../shared/context/editor-context' +import { startFreeTrial } from '../../../main/account-upgrade' function PreviewError({ name }) { + const { isProjectOwner } = useEditorContext() + const { + exposedSettings: { enableSubscriptions } + } = useApplicationContext() + const { t } = useTranslation() let errorTitle let errorContent @@ -53,17 +62,101 @@ function PreviewError({ name }) { } return errorTitle ? ( - + <> + + {name === 'timedout' && enableSubscriptions ? ( + + ) : null} + ) : null } +function TimeoutUpgradePrompt({ isProjectOwner }) { + const { t } = useTranslation() + + function handleStartFreeTrialClick() { + startFreeTrial('compile-timeout') + } + + const timeoutUpgradePromptContent = ( + <> +

{t('free_accounts_have_timeout_upgrade_to_increase')}

+

{t('plus_upgraded_accounts_receive')}:

+
+
    +
  • + +   + {t('unlimited_projects')} +
  • +
  • + +   + {t('collabs_per_proj', { collabcount: 'Multiple' })} +
  • +
  • + +   + {t('full_doc_history')} +
  • +
  • + +   + {t('sync_to_dropbox')} +
  • +
  • + +   + {t('sync_to_github')} +
  • +
  • + +   + {t('compile_larger_projects')} +
  • +
+
+ {isProjectOwner ? ( +

+ +

+ ) : null} + + ) + return ( + + ) +} + PreviewError.propTypes = { name: PropTypes.string.isRequired } +TimeoutUpgradePrompt.propTypes = { + isProjectOwner: PropTypes.bool.isRequired +} + export default PreviewError diff --git a/services/web/frontend/js/features/preview/components/preview-logs-pane-entry.js b/services/web/frontend/js/features/preview/components/preview-logs-pane-entry.js index ecf7249e76..ed5610e768 100644 --- a/services/web/frontend/js/features/preview/components/preview-logs-pane-entry.js +++ b/services/web/frontend/js/features/preview/components/preview-logs-pane-entry.js @@ -78,13 +78,15 @@ function PreviewLogEntryHeader({ 'log-entry-header-error': level === 'error', 'log-entry-header-warning': level === 'warning', 'log-entry-header-typesetting': level === 'typesetting', - 'log-entry-header-raw': level === 'raw' + 'log-entry-header-raw': level === 'raw', + 'log-entry-header-success': level === 'success' }) const logEntryLocationBtnClasses = classNames('log-entry-header-link', { 'log-entry-header-link-error': level === 'error', 'log-entry-header-link-warning': level === 'warning', 'log-entry-header-link-typesetting': level === 'typesetting', - 'log-entry-header-link-raw': level === 'raw' + 'log-entry-header-link-raw': level === 'raw', + 'log-entry-header-link-success': level === 'success' }) const headerLogLocationTitle = t('navigate_log_source', { location: file + (line ? `, ${line}` : '') @@ -255,7 +257,8 @@ PreviewLogsPaneEntry.propTypes = { logType: PropTypes.string, formattedContent: PropTypes.node, extraInfoURL: PropTypes.string, - level: PropTypes.oneOf(['error', 'warning', 'typesetting', 'raw']).isRequired, + level: PropTypes.oneOf(['error', 'warning', 'typesetting', 'raw', 'success']) + .isRequired, customClass: PropTypes.string, showSourceLocationLink: PropTypes.bool, showCloseButton: PropTypes.bool, diff --git a/services/web/frontend/js/ide.js b/services/web/frontend/js/ide.js index fc7da903f8..0728abe840 100644 --- a/services/web/frontend/js/ide.js +++ b/services/web/frontend/js/ide.js @@ -57,8 +57,8 @@ import './services/validateCaptchaV3' import './services/wait-for' import './filters/formatDate' import './main/event' -import './main/account-upgrade' -import './main/exposed-settings' +import './main/account-upgrade-angular' +import './main/exposed-settings-angular' import './main/system-messages' import '../../modules/modules-ide.js' diff --git a/services/web/frontend/js/ide/pdf/controllers/PdfController.js b/services/web/frontend/js/ide/pdf/controllers/PdfController.js index 67d52718fd..9f2f55b32b 100644 --- a/services/web/frontend/js/ide/pdf/controllers/PdfController.js +++ b/services/web/frontend/js/ide/pdf/controllers/PdfController.js @@ -3,6 +3,7 @@ import HumanReadableLogs from '../../human-readable-logs/HumanReadableLogs' import BibLogParser from 'libs/bib-log-parser' import PreviewPane from '../../../features/preview/components/preview-pane' import { react2angular } from 'react2angular' +import { rootContext } from '../../../shared/context/root-context' import 'ace/ace' const AUTO_COMPILE_MAX_WAIT = 5000 // We add a 1 second debounce to sending user changes to server if they aren't @@ -1152,6 +1153,11 @@ App.controller('ClearCacheModalController', function($scope, $modalInstance) { $scope.cancel = () => $modalInstance.dismiss('cancel') }) - // Wrap React component as Angular component. Only needed for "top-level" component -App.component('previewPane', react2angular(PreviewPane)) +App.component( + 'previewPane', + react2angular( + rootContext.use(PreviewPane), + Object.keys(PreviewPane.propTypes) + ) +) diff --git a/services/web/frontend/js/main.js b/services/web/frontend/js/main.js index 1a41832644..cd87c2aeef 100644 --- a/services/web/frontend/js/main.js +++ b/services/web/frontend/js/main.js @@ -12,7 +12,7 @@ import './main/token-access' import './main/project-list/index' import './main/account-settings' import './main/clear-sessions' -import './main/account-upgrade' +import './main/account-upgrade-angular' import './main/plans' import './main/post-gateway' import './main/user-membership' @@ -28,7 +28,7 @@ import './main/register-users' import './main/subscription/team-invite-controller' import './main/subscription/upgrade-subscription' import './main/learn' -import './main/exposed-settings' +import './main/exposed-settings-angular' import './main/affiliations/components/affiliationForm' import './main/affiliations/components/inputSuggestions' import './main/affiliations/controllers/UserAffiliationsController' diff --git a/services/web/frontend/js/main/account-upgrade-angular.js b/services/web/frontend/js/main/account-upgrade-angular.js new file mode 100644 index 0000000000..0af5fba4c1 --- /dev/null +++ b/services/web/frontend/js/main/account-upgrade-angular.js @@ -0,0 +1,13 @@ +import App from '../base' +import { startFreeTrial, upgradePlan } from './account-upgrade' + +App.controller('FreeTrialModalController', function($scope, eventTracking) { + $scope.buttonClass = 'btn-primary' + $scope.startFreeTrial = (source, version) => + startFreeTrial(source, version, $scope, eventTracking) +}) + +App.controller('UpgradeModalController', function($scope, eventTracking) { + $scope.buttonClass = 'btn-primary' + $scope.upgradePlan = source => upgradePlan(source, $scope) +}) diff --git a/services/web/frontend/js/main/account-upgrade.js b/services/web/frontend/js/main/account-upgrade.js index 4a7d81e87b..5f8bb4e37c 100644 --- a/services/web/frontend/js/main/account-upgrade.js +++ b/services/web/frontend/js/main/account-upgrade.js @@ -1,59 +1,49 @@ -import App from '../base' +function startFreeTrial(source, version, $scope, eventTracking) { + const plan = 'collaborator_free_trial_7_days' -export default App.controller('FreeTrialModalController', function( - $scope, - eventTracking -) { - $scope.buttonClass = 'btn-primary' - - $scope.startFreeTrial = function(source, version) { - const plan = 'collaborator_free_trial_7_days' - - const w = window.open() - const go = function() { - let url - if (typeof ga === 'function') { - ga( - 'send', - 'event', - 'subscription-funnel', - 'upgraded-free-trial', - source - ) - } - url = `/user/subscription/new?planCode=${plan}&ssp=true` - url = `${url}&itm_campaign=${source}` - if (version) { - url = `${url}&itm_content=${version}` - } + const w = window.open() + const go = function() { + let url + if (typeof ga === 'function') { + ga('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source) + } + url = `/user/subscription/new?planCode=${plan}&ssp=true` + url = `${url}&itm_campaign=${source}` + if (version) { + url = `${url}&itm_content=${version}` + } + if ($scope) { $scope.startedFreeTrial = true + } + if (eventTracking) { eventTracking.sendMB('subscription-start-trial', { source, plan }) - - w.location = url } - go() + w.location = url } -}) -App.controller('UpgradeModalController', function($scope, eventTracking) { - $scope.buttonClass = 'btn-primary' + go() +} - $scope.upgradePlan = function(source) { - const w = window.open() - const go = function() { - let url - if (typeof ga === 'function') { - ga('send', 'event', 'subscription-funnel', 'upgraded-plan', source) - } - url = '/user/subscription' +function upgradePlan(source, $scope) { + const w = window.open() + const go = function() { + let url + if (typeof ga === 'function') { + ga('send', 'event', 'subscription-funnel', 'upgraded-plan', source) + } + url = '/user/subscription' + + if ($scope) { $scope.startedFreeTrial = true - - w.location = url } - go() + w.location = url } -}) + + go() +} + +export { startFreeTrial, upgradePlan } diff --git a/services/web/frontend/js/main/exposed-settings-angular.js b/services/web/frontend/js/main/exposed-settings-angular.js new file mode 100644 index 0000000000..4717110712 --- /dev/null +++ b/services/web/frontend/js/main/exposed-settings-angular.js @@ -0,0 +1,4 @@ +import App from '../base' +import ExposedSettings from './exposed-settings' + +App.constant('ExposedSettings', ExposedSettings) diff --git a/services/web/frontend/js/main/exposed-settings.js b/services/web/frontend/js/main/exposed-settings.js index 8fbfb2644c..0e5368c44d 100644 --- a/services/web/frontend/js/main/exposed-settings.js +++ b/services/web/frontend/js/main/exposed-settings.js @@ -1,7 +1,3 @@ -import App from '../base' - const ExposedSettings = window.ExposedSettings -App.constant('ExposedSettings', ExposedSettings) - export default ExposedSettings diff --git a/services/web/frontend/js/shared/context/application-context.js b/services/web/frontend/js/shared/context/application-context.js index 3668ac1037..016474bf32 100644 --- a/services/web/frontend/js/shared/context/application-context.js +++ b/services/web/frontend/js/shared/context/application-context.js @@ -1,15 +1,16 @@ import React, { createContext, useContext } from 'react' import PropTypes from 'prop-types' +import ExposedSettings from '../../main/exposed-settings' export const ApplicationContext = createContext() export function ApplicationProvider({ children }) { + const applicationContextValue = { + user: window.user, + exposedSettings: ExposedSettings + } return ( - + {children} ) @@ -20,8 +21,6 @@ ApplicationProvider.propTypes = { } export function useApplicationContext() { - const { user } = useContext(ApplicationContext) - return { - user - } + const applicationContext = useContext(ApplicationContext) + return applicationContext } diff --git a/services/web/frontend/js/shared/context/editor-context.js b/services/web/frontend/js/shared/context/editor-context.js index 49e272cd99..41ca0091b4 100644 --- a/services/web/frontend/js/shared/context/editor-context.js +++ b/services/web/frontend/js/shared/context/editor-context.js @@ -4,12 +4,18 @@ import PropTypes from 'prop-types' export const EditorContext = createContext() export function EditorProvider({ children }) { + const ownerId = + window._ide.$scope.project && window._ide.$scope.project.owner + ? window._ide.$scope.project.owner._id + : null + + const editorContextValue = { + projectId: window.project_id, + isProjectOwner: ownerId === window.user.id + } + return ( - + {children} ) @@ -20,8 +26,6 @@ EditorProvider.propTypes = { } export function useEditorContext() { - const { projectId } = useContext(EditorContext) - return { - projectId - } + const editorContext = useContext(EditorContext) + return editorContext } diff --git a/services/web/frontend/stylesheets/app/editor/logs.less b/services/web/frontend/stylesheets/app/editor/logs.less index 092883fc1c..eba5eb04fb 100644 --- a/services/web/frontend/stylesheets/app/editor/logs.less +++ b/services/web/frontend/stylesheets/app/editor/logs.less @@ -76,6 +76,14 @@ .btn-alert-variant(@ol-blue-gray-4); } +.log-entry-header-success { + background-color: @green; +} + +.log-entry-header-link-success { + .btn-alert-variant(@green); +} + .log-entry-header-title, .log-entry-header-link { font-family: @font-family-sans-serif; diff --git a/services/web/test/frontend/features/chat/components/stubs.js b/services/web/test/frontend/features/chat/components/stubs.js index 71174dc717..baa0b4d084 100644 --- a/services/web/test/frontend/features/chat/components/stubs.js +++ b/services/web/test/frontend/features/chat/components/stubs.js @@ -27,11 +27,9 @@ export function tearDownMathJaxStubs() { } export function stubChatStore({ user }) { - window._ide = { socket: { on: sinon.stub(), removeListener: sinon.stub() } } window.user = user } export function tearDownChatStore() { - delete window._ide delete window.user } diff --git a/services/web/test/frontend/features/preview/components/preview-logs-pane.test.js b/services/web/test/frontend/features/preview/components/preview-logs-pane.test.js index a0c331f64a..e8af5d4edf 100644 --- a/services/web/test/frontend/features/preview/components/preview-logs-pane.test.js +++ b/services/web/test/frontend/features/preview/components/preview-logs-pane.test.js @@ -1,7 +1,8 @@ import React from 'react' -import { screen, render, fireEvent } from '@testing-library/react' +import { screen, fireEvent } from '@testing-library/react' import PreviewLogsPane from '../../../../../frontend/js/features/preview/components/preview-logs-pane' import sinon from 'sinon' +import { renderWithEditorContext } from '../../../helpers/render-with-context' const { expect } = require('chai') @@ -60,7 +61,7 @@ entering extended mode const noOp = () => describe('with logs', function() { beforeEach(function() { - render( + renderWithEditorContext( {children}