diff --git a/services/clsi/app/js/CompileController.js b/services/clsi/app/js/CompileController.js index dbcb928425..daa4e77f72 100644 --- a/services/clsi/app/js/CompileController.js +++ b/services/clsi/app/js/CompileController.js @@ -64,16 +64,17 @@ function compile(req, res, next) { 'error running compile' ) } else { - let file - status = 'failure' - for (file of outputFiles) { - if (file.path === 'output.pdf' && file.size > 0) { - status = 'success' - lastSuccessfulCompileTimestamp = Date.now() - } - } - - if (status === 'failure') { + if ( + outputFiles.some( + file => file.path === 'output.pdf' && file.size > 0 + ) + ) { + status = 'success' + lastSuccessfulCompileTimestamp = Date.now() + } else if (request.stopOnFirstError) { + status = 'stopped-on-first-error' + } else { + status = 'failure' logger.warn( { project_id: request.project_id, outputFiles }, 'project failed to compile successfully, no output.pdf generated' @@ -81,13 +82,11 @@ function compile(req, res, next) { } // log an error if any core files are found - for (file of outputFiles) { - if (file.path === 'core') { - logger.error( - { project_id: request.project_id, req, outputFiles }, - 'core file found in output' - ) - } + if (outputFiles.some(file => file.path === 'core')) { + logger.error( + { project_id: request.project_id, req, outputFiles }, + 'core file found in output' + ) } } diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index ecef5f0342..33ee8ee4a2 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -89,6 +89,7 @@ "demonstrating_git_integration": "", "department": "", "description": "", + "disable_stop_on_first_error": "", "dismiss": "", "dismiss_error_popup": "", "doesnt_match": "", @@ -252,7 +253,6 @@ "log_viewer_error": "", "login_with_service": "", "logs_and_output_files": "", - "logs_pane_info_message": "", "main_file_not_found": "", "make_email_primary_description": "", "make_primary": "", @@ -435,6 +435,8 @@ "start_free_trial": "", "stop_compile": "", "stop_on_first_error": "", + "stop_on_first_error_enabled_description": "", + "stop_on_first_error_enabled_title": "", "stop_on_validation_error": "", "store_your_work": "", "subject": "", diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-logs-button.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-logs-button.tsx index fae09a580a..325ecd8748 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-logs-button.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-logs-button.tsx @@ -6,7 +6,8 @@ import Icon from '../../../shared/components/icon' import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context' function PdfHybridLogsButton() { - const { error, logEntries, toggleLogs, showLogs } = useCompileContext() + const { error, logEntries, toggleLogs, showLogs, stoppedOnFirstError } = + useCompileContext() const { t } = useTranslation() @@ -26,7 +27,7 @@ function PdfHybridLogsButton() { > + + } + level="info" + /> + ) +} diff --git a/services/web/frontend/js/features/pdf-preview/util/compiler.js b/services/web/frontend/js/features/pdf-preview/util/compiler.js index 3d5fc2acce..07e3056b2d 100644 --- a/services/web/frontend/js/features/pdf-preview/util/compiler.js +++ b/services/web/frontend/js/features/pdf-preview/util/compiler.js @@ -41,7 +41,10 @@ export default class DocumentCompiler { this.currentDoc = null this.error = undefined this.timer = 0 - this.stopOnFirstError = false + this.defaultOptions = { + draft: false, + stopOnFirstError: false, + } this.debouncedAutoCompile = debounce( () => { @@ -57,9 +60,7 @@ export default class DocumentCompiler { // The main "compile" function. // Call this directly to run a compile now, otherwise call debouncedAutoCompile. async compile(options = {}) { - if (!options) { - options = {} - } + options = { ...this.defaultOptions, ...options } // set "compiling" to true (in the React component's state), and return if it was already true const wasCompiling = this.compilingRef.current @@ -85,14 +86,14 @@ export default class DocumentCompiler { const body = { rootDoc_id: this.getRootDocOverrideId(), - draft: this.draft, + draft: options.draft, check: 'silent', // NOTE: 'error' and 'validate' are possible, but unused // use incremental compile for all users but revert to a full compile // if there was previously a server error incrementalCompilesEnabled: !this.error, } if (getMeta('ol-showStopOnFirstError')) { - body.stopOnFirstError = this.stopOnFirstError + body.stopOnFirstError = options.stopOnFirstError } const data = await postJSON( `/project/${this.projectId}/compile?${params}`, @@ -202,4 +203,8 @@ export default class DocumentCompiler { this.setError('clear-cache') }) } + + setOption(option, value) { + this.defaultOptions[option] = value + } } diff --git a/services/web/frontend/js/features/preview/components/preview-log-entry-header.js b/services/web/frontend/js/features/preview/components/preview-log-entry-header.js index f24552ab3d..b8d286c1fd 100644 --- a/services/web/frontend/js/features/preview/components/preview-log-entry-header.js +++ b/services/web/frontend/js/features/preview/components/preview-log-entry-header.js @@ -32,6 +32,7 @@ function PreviewLogEntryHeader({ const logEntryHeaderClasses = classNames('log-entry-header', { 'log-entry-header-error': level === 'error', 'log-entry-header-warning': level === 'warning', + 'log-entry-header-info': level === 'info', 'log-entry-header-typesetting': level === 'typesetting', 'log-entry-header-raw': level === 'raw', 'log-entry-header-success': level === 'success', @@ -41,6 +42,7 @@ function PreviewLogEntryHeader({ '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-info': level === 'info', 'log-entry-header-link-success': level === 'success', }) const headerLogLocationTitle = t('navigate_log_source', { diff --git a/services/web/frontend/js/ide/log-parser/latex-log-parser.js b/services/web/frontend/js/ide/log-parser/latex-log-parser.js index 386bb8c2ec..bbddc2a006 100644 --- a/services/web/frontend/js/ide/log-parser/latex-log-parser.js +++ b/services/web/frontend/js/ide/log-parser/latex-log-parser.js @@ -79,7 +79,11 @@ export default class LatexParser { } currentLineIsError() { - return this.currentLine[0] === '!' + return ( + this.currentLine[0] === '!' && + this.currentLine !== + '! ==> Fatal error occurred, no output PDF file produced!' + ) } currentLineIsFileLineError() { diff --git a/services/web/frontend/js/shared/context/detach-compile-context.js b/services/web/frontend/js/shared/context/detach-compile-context.js index 941bf4456c..524fa5eb10 100644 --- a/services/web/frontend/js/shared/context/detach-compile-context.js +++ b/services/web/frontend/js/shared/context/detach-compile-context.js @@ -50,6 +50,7 @@ export function DetachCompileProvider({ children }) { showLogs: _showLogs, stopOnFirstError: _stopOnFirstError, stopOnValidationError: _stopOnValidationError, + stoppedOnFirstError: _stoppedOnFirstError, uncompiled: _uncompiled, validationIssues: _validationIssues, firstRenderDone: _firstRenderDone, @@ -172,6 +173,12 @@ export function DetachCompileProvider({ children }) { 'detacher', 'detached' ) + const [stoppedOnFirstError] = useDetachStateWatcher( + 'stoppedOnFirstError', + _stoppedOnFirstError, + 'detacher', + 'detached' + ) const [uncompiled] = useDetachStateWatcher( 'uncompiled', _uncompiled, @@ -239,14 +246,12 @@ export function DetachCompileProvider({ children }) { 'detached', 'detacher' ) - const setStopOnFirstError = useDetachAction( 'setStopOnFirstError', _setStopOnFirstError, 'detached', 'detacher' ) - const setStopOnValidationError = useDetachAction( 'setStopOnValidationError', _setStopOnValidationError, @@ -333,6 +338,7 @@ export function DetachCompileProvider({ children }) { stopCompile, stopOnFirstError, stopOnValidationError, + stoppedOnFirstError, uncompiled, validationIssues, firstRenderDone, @@ -375,6 +381,7 @@ export function DetachCompileProvider({ children }) { stopCompile, stopOnFirstError, stopOnValidationError, + stoppedOnFirstError, uncompiled, validationIssues, firstRenderDone, diff --git a/services/web/frontend/js/shared/context/local-compile-context.js b/services/web/frontend/js/shared/context/local-compile-context.js index a0d5683df1..f83d22ce27 100644 --- a/services/web/frontend/js/shared/context/local-compile-context.js +++ b/services/web/frontend/js/shared/context/local-compile-context.js @@ -62,6 +62,7 @@ export const CompileContextPropTypes = { showLogs: PropTypes.bool.isRequired, stopOnFirstError: PropTypes.bool.isRequired, stopOnValidationError: PropTypes.bool.isRequired, + stoppedOnFirstError: PropTypes.bool.isRequired, uncompiled: PropTypes.bool, validationIssues: PropTypes.object, firstRenderDone: PropTypes.func, @@ -164,6 +165,9 @@ export function LocalCompileProvider({ children }) { true ) + // whether the last compiles stopped on first error + const [stoppedOnFirstError, setStoppedOnFirstError] = useState(false) + // whether compiling should be prevented if there are linting errors const [stopOnValidationError, setStopOnValidationError] = usePersistedState( `stop_on_validation_error:${projectId}`, @@ -221,12 +225,12 @@ export function LocalCompileProvider({ children }) { // keep draft setting in sync with the compiler useEffect(() => { - compiler.draft = draft + compiler.setOption('draft', draft) }, [compiler, draft]) // keep stop on first error setting in sync with the compiler useEffect(() => { - compiler.stopOnFirstError = stopOnFirstError + compiler.setOption('stopOnFirstError', stopOnFirstError) }, [compiler, stopOnFirstError]) // pass the "uncompiled" value up into the scope for use outside this context provider @@ -311,6 +315,11 @@ export function LocalCompileProvider({ children }) { setShowLogs(false) break + case 'stopped-on-first-error': + setError(undefined) + setShowLogs(true) + break + case 'clsi-maintenance': case 'compile-in-progress': case 'exited': @@ -354,6 +363,8 @@ export function LocalCompileProvider({ children }) { setError('error') break } + + setStoppedOnFirstError(data.status === 'stopped-on-first-error') } return () => { @@ -484,6 +495,7 @@ export function LocalCompileProvider({ children }) { stopCompile, stopOnFirstError, stopOnValidationError, + stoppedOnFirstError, uncompiled, validationIssues, firstRenderDone, @@ -523,6 +535,7 @@ export function LocalCompileProvider({ children }) { stopCompile, stopOnFirstError, stopOnValidationError, + stoppedOnFirstError, uncompiled, validationIssues, firstRenderDone, diff --git a/services/web/frontend/stylesheets/app/editor/logs.less b/services/web/frontend/stylesheets/app/editor/logs.less index 86bcf15b0b..3b106c5975 100644 --- a/services/web/frontend/stylesheets/app/editor/logs.less +++ b/services/web/frontend/stylesheets/app/editor/logs.less @@ -68,11 +68,13 @@ .btn-alert-variant(@ol-blue); } -.log-entry-header-raw { +.log-entry-header-raw, +.log-entry-header-info { background-color: @ol-blue-gray-4; } -.log-entry-header-link-raw { +.log-entry-header-link-raw, +.log-entry-header-link-info { .btn-alert-variant(@ol-blue-gray-4); } diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 9d8b998c80..5bd4bf3264 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1752,5 +1752,8 @@ "email_or_password_wrong_try_again_or_reset": "Your email or password is incorrect. Please try again, or <0>set or reset your password.", "compile_error_handling": "Compile Error Handling", "stop_on_first_error": "Stop on first error", - "try_to_compile_despite_errors": "Try to compile despite errors" + "try_to_compile_despite_errors": "Try to compile despite errors", + "stop_on_first_error_enabled_title": "No PDF: Stop on first error enabled", + "stop_on_first_error_enabled_description": "<0>“Stop on first error” is enabled. Disabling it may allow the compiler to produce a PDF (but your project will still have errors).", + "disable_stop_on_first_error": "Disable “Stop on first error”" }