From fc6df69e4129450bfd4b6041368f77a2ffbfc1e5 Mon Sep 17 00:00:00 2001 From: roo hutton Date: Thu, 3 Apr 2025 09:14:27 +0100 Subject: [PATCH 001/978] Merge pull request #24630 from overleaf/rh-null-editor-fix Coerce null reviewer_refs to empty array GitOrigin-RevId: dd1931b306e22fc4b7dbd3709dfac786a2475724 --- .../Collaborators/CollaboratorsHandler.js | 37 +++-- .../CollaboratorsHandlerTests.js | 154 +++++++----------- 2 files changed, 84 insertions(+), 107 deletions(-) diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js b/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js index ab16929bf9..05137a97f8 100644 --- a/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js +++ b/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js @@ -28,22 +28,34 @@ module.exports = { convertTrackChangesToExplicitFormat, }, } +// Forces null pendingReviewer_refs, readOnly_refs, and reviewer_refs to +// be empty arrays to avoid errors during $pull ops +// See https://github.com/overleaf/internal/issues/24610 +async function fixNullCollaboratorRefs(projectId) { + // Temporary cleanup for the case where pendingReviewer_refs is null + await Project.updateOne( + { _id: projectId, pendingReviewer_refs: { $type: 'null' } }, + { $set: { pendingReviewer_refs: [] } } + ).exec() + + // Temporary cleanup for the case where readOnly_refs is null + await Project.updateOne( + { _id: projectId, readOnly_refs: { $type: 'null' } }, + { $set: { readOnly_refs: [] } } + ).exec() + + // Temporary cleanup for the case where reviewer_refs is null + await Project.updateOne( + { _id: projectId, reviewer_refs: { $type: 'null' } }, + { $set: { reviewer_refs: [] } } + ).exec() +} async function removeUserFromProject(projectId, userId) { try { const project = await Project.findOne({ _id: projectId }).exec() - // Temporary workaround for the case where pendingReviewer_refs is null - await Project.updateOne( - { _id: projectId, pendingReviewer_refs: { $type: 'null' } }, - { $set: { pendingReviewer_refs: [] } } - ).exec() - - // Temporary workaround for the case where readOnly_refs is null - await Project.updateOne( - { _id: projectId, readOnly_refs: { $type: 'null' } }, - { $set: { readOnly_refs: [] } } - ).exec() + await fixNullCollaboratorRefs(projectId) // Deal with the old type of boolean value for archived // In order to clear it @@ -307,6 +319,9 @@ async function setCollaboratorPrivilegeLevel( ], } let update + + await fixNullCollaboratorRefs(projectId) + switch (privilegeLevel) { case PrivilegeLevels.READ_AND_WRITE: { update = { diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js b/services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js index 7cf9adbb8e..8542bd8355 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js +++ b/services/web/test/unit/src/Collaborators/CollaboratorsHandlerTests.js @@ -82,6 +82,46 @@ describe('CollaboratorsHandler', function () { './CollaboratorsGetter': this.CollaboratorsGetter, }, }) + + // Helper function to set up mock expectations for null reference cleanup + this.expectNullReferenceCleanup = projectId => { + this.ProjectMock.expects('updateOne') + .withArgs( + { + _id: projectId, + pendingReviewer_refs: { $type: 'null' }, + }, + { + $set: { pendingReviewer_refs: [] }, + } + ) + .chain('exec') + .resolves() + this.ProjectMock.expects('updateOne') + .withArgs( + { + _id: projectId, + readOnly_refs: { $type: 'null' }, + }, + { + $set: { readOnly_refs: [] }, + } + ) + .chain('exec') + .resolves() + this.ProjectMock.expects('updateOne') + .withArgs( + { + _id: projectId, + reviewer_refs: { $type: 'null' }, + }, + { + $set: { reviewer_refs: [] }, + } + ) + .chain('exec') + .resolves() + } }) afterEach(function () { @@ -100,30 +140,7 @@ describe('CollaboratorsHandler', function () { }) it('should remove the user from mongo', async function () { - this.ProjectMock.expects('updateOne') - .withArgs( - { - _id: this.project._id, - pendingReviewer_refs: { $type: 'null' }, - }, - { - $set: { pendingReviewer_refs: [] }, - } - ) - .chain('exec') - .resolves() - this.ProjectMock.expects('updateOne') - .withArgs( - { - _id: this.project._id, - readOnly_refs: { $type: 'null' }, - }, - { - $set: { readOnly_refs: [] }, - } - ) - .chain('exec') - .resolves() + this.expectNullReferenceCleanup(this.project._id) this.ProjectMock.expects('updateOne') .withArgs( { @@ -166,30 +183,7 @@ describe('CollaboratorsHandler', function () { }) it('should remove the user from mongo', async function () { - this.ProjectMock.expects('updateOne') - .withArgs( - { - _id: this.oldArchivedProject._id, - pendingReviewer_refs: { $type: 'null' }, - }, - { - $set: { pendingReviewer_refs: [] }, - } - ) - .chain('exec') - .resolves() - this.ProjectMock.expects('updateOne') - .withArgs( - { - _id: this.oldArchivedProject._id, - readOnly_refs: { $type: 'null' }, - }, - { - $set: { readOnly_refs: [] }, - } - ) - .chain('exec') - .resolves() + this.expectNullReferenceCleanup(this.oldArchivedProject._id) this.ProjectMock.expects('updateOne') .withArgs( { @@ -230,30 +224,7 @@ describe('CollaboratorsHandler', function () { }) it('should remove the user from mongo', async function () { - this.ProjectMock.expects('updateOne') - .withArgs( - { - _id: this.archivedProject._id, - pendingReviewer_refs: { $type: 'null' }, - }, - { - $set: { pendingReviewer_refs: [] }, - } - ) - .chain('exec') - .resolves() - this.ProjectMock.expects('updateOne') - .withArgs( - { - _id: this.archivedProject._id, - readOnly_refs: { $type: 'null' }, - }, - { - $set: { readOnly_refs: [] }, - } - ) - .chain('exec') - .resolves() + this.expectNullReferenceCleanup(this.archivedProject._id) this.ProjectMock.expects('updateOne') .withArgs( { @@ -541,30 +512,7 @@ describe('CollaboratorsHandler', function () { .chain('exec') .resolves({ _id: projectId }) - this.ProjectMock.expects('updateOne') - .withArgs( - { - _id: projectId, - pendingReviewer_refs: { $type: 'null' }, - }, - { - $set: { pendingReviewer_refs: [] }, - } - ) - .chain('exec') - .resolves() - this.ProjectMock.expects('updateOne') - .withArgs( - { - _id: projectId, - readOnly_refs: { $type: 'null' }, - }, - { - $set: { readOnly_refs: [] }, - } - ) - .chain('exec') - .resolves() + this.expectNullReferenceCleanup(projectId) this.ProjectMock.expects('updateOne') .withArgs( { @@ -727,6 +675,8 @@ describe('CollaboratorsHandler', function () { describe('setCollaboratorPrivilegeLevel', function () { it('sets a collaborator to read-only', async function () { + this.expectNullReferenceCleanup(this.project._id) + this.ProjectMock.expects('updateOne') .withArgs( { @@ -757,6 +707,8 @@ describe('CollaboratorsHandler', function () { }) it('sets a collaborator to read-write', async function () { + this.expectNullReferenceCleanup(this.project._id) + this.ProjectMock.expects('updateOne') .withArgs( { @@ -796,6 +748,8 @@ describe('CollaboratorsHandler', function () { }) }) it('should correctly update the project', async function () { + this.expectNullReferenceCleanup(this.project._id) + this.ProjectMock.expects('updateOne') .withArgs( { @@ -839,6 +793,8 @@ describe('CollaboratorsHandler', function () { }) }) it('should correctly update the project', async function () { + this.expectNullReferenceCleanup(this.project._id) + this.ProjectMock.expects('updateOne') .withArgs( { @@ -871,6 +827,8 @@ describe('CollaboratorsHandler', function () { }) it('sets a collaborator to read-only as a pendingEditor', async function () { + this.expectNullReferenceCleanup(this.project._id) + this.ProjectMock.expects('updateOne') .withArgs( { @@ -904,6 +862,8 @@ describe('CollaboratorsHandler', function () { }) it('sets a collaborator to read-only as a pendingReviewer', async function () { + this.expectNullReferenceCleanup(this.project._id) + this.ProjectMock.expects('updateOne') .withArgs( { @@ -937,6 +897,8 @@ describe('CollaboratorsHandler', function () { }) it('throws a NotFoundError if the project or collaborator does not exist', async function () { + this.expectNullReferenceCleanup(this.project._id) + this.ProjectMock.expects('updateOne') .chain('exec') .resolves({ matchedCount: 0 }) From 9f527f10e1736b526c0bd3a3e743e3e7dc125e6e Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 3 Apr 2025 11:24:59 +0100 Subject: [PATCH 002/978] Merge pull request #24104 from overleaf/bg-batched-update-concurrency-warning prevent concurrent execution of batchedUpdate GitOrigin-RevId: 90853ccd83943d8cd7b01fd11f152512d806e9a7 --- libraries/mongo-utils/batchedUpdate.js | 106 ++++++++++++++----------- 1 file changed, 58 insertions(+), 48 deletions(-) diff --git a/libraries/mongo-utils/batchedUpdate.js b/libraries/mongo-utils/batchedUpdate.js index 5e97f45aca..7e3ad677db 100644 --- a/libraries/mongo-utils/batchedUpdate.js +++ b/libraries/mongo-utils/batchedUpdate.js @@ -16,6 +16,7 @@ let VERBOSE_LOGGING let BATCH_RANGE_START let BATCH_RANGE_END let BATCH_MAX_TIME_SPAN_IN_MS +let BATCHED_UPDATE_RUNNING = false /** * @typedef {import("mongodb").Collection} Collection @@ -211,57 +212,66 @@ async function batchedUpdate( findOptions, batchedUpdateOptions ) { - ID_EDGE_PAST = await getIdEdgePast(collection) - if (!ID_EDGE_PAST) { - console.warn( - `The collection ${collection.collectionName} appears to be empty.` - ) - return 0 + // only a single batchedUpdate can run at a time due to global variables + if (BATCHED_UPDATE_RUNNING) { + throw new Error('batchedUpdate is already running') } - refreshGlobalOptionsForBatchedUpdate(batchedUpdateOptions) - - findOptions = findOptions || {} - findOptions.readPreference = READ_PREFERENCE_SECONDARY - - projection = projection || { _id: 1 } - let nextBatch - let updated = 0 - let start = BATCH_RANGE_START - - while (start !== BATCH_RANGE_END) { - let end = getNextEnd(start) - nextBatch = await getNextBatch( - collection, - query, - start, - end, - projection, - findOptions - ) - if (nextBatch.length > 0) { - end = nextBatch[nextBatch.length - 1]._id - updated += nextBatch.length - - if (VERBOSE_LOGGING) { - console.log( - `Running update on batch with ids ${JSON.stringify( - nextBatch.map(entry => entry._id) - )}` - ) - } else { - console.error(`Running update on batch ending ${renderObjectId(end)}`) - } - - if (typeof update === 'function') { - await update(nextBatch) - } else { - await performUpdate(collection, nextBatch, update) - } + try { + BATCHED_UPDATE_RUNNING = true + ID_EDGE_PAST = await getIdEdgePast(collection) + if (!ID_EDGE_PAST) { + console.warn( + `The collection ${collection.collectionName} appears to be empty.` + ) + return 0 } - console.error(`Completed batch ending ${renderObjectId(end)}`) - start = end + refreshGlobalOptionsForBatchedUpdate(batchedUpdateOptions) + + findOptions = findOptions || {} + findOptions.readPreference = READ_PREFERENCE_SECONDARY + + projection = projection || { _id: 1 } + let nextBatch + let updated = 0 + let start = BATCH_RANGE_START + + while (start !== BATCH_RANGE_END) { + let end = getNextEnd(start) + nextBatch = await getNextBatch( + collection, + query, + start, + end, + projection, + findOptions + ) + if (nextBatch.length > 0) { + end = nextBatch[nextBatch.length - 1]._id + updated += nextBatch.length + + if (VERBOSE_LOGGING) { + console.log( + `Running update on batch with ids ${JSON.stringify( + nextBatch.map(entry => entry._id) + )}` + ) + } else { + console.error(`Running update on batch ending ${renderObjectId(end)}`) + } + + if (typeof update === 'function') { + await update(nextBatch) + } else { + await performUpdate(collection, nextBatch, update) + } + } + console.error(`Completed batch ending ${renderObjectId(end)}`) + start = end + } + return updated + } finally { + BATCHED_UPDATE_RUNNING = false } - return updated } /** From 90fac6b2065ce14d01088985ca906bd02ff4be03 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 3 Apr 2025 11:25:20 +0100 Subject: [PATCH 003/978] Merge pull request #24587 from overleaf/bg-fix-web-routes-script fix bin/routes script GitOrigin-RevId: bc791cf01ce3321ec4badffe2cbc8c4ea93ba381 --- services/web/bin/routes | 49 ++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/services/web/bin/routes b/services/web/bin/routes index 2cf77e966c..707a5da161 100755 --- a/services/web/bin/routes +++ b/services/web/bin/routes @@ -39,9 +39,9 @@ const routerCall = callExpression => { (routerName === 'app' || routerName.match('^.*[rR]outer$')) ) { return { - routerName: routerName, + routerName, method: property.name, - args: args, + args, } } else { return null @@ -55,27 +55,30 @@ const formatMethodCall = expression => { const parseAndPrintRoutesSync = path => { const content = fs.readFileSync(path) // Walk the AST (Abstract Syntax Tree) - acornWalk.simple(acorn.parse(content), { - // We only care about call expression ( like `a.b()` ) - CallExpression(node) { - const call = routerCall(node) - if (call) { - const firstArg = _.first(call.args) - try { - print( - ` ${formatRouterName(call.routerName)}\t .${call.method} \t: ${ - firstArg.value - } => ${call.args.slice(1).map(formatMethodCall).join(' => ')}` - ) - } catch (e) { - print('>> Error') - print(e) - print(JSON.stringify(call)) - process.exit(1) + acornWalk.simple( + acorn.parse(content, { sourceType: 'module', ecmaVersion: 2020 }), + { + // We only care about call expression ( like `a.b()` ) + CallExpression(node) { + const call = routerCall(node) + if (call) { + const firstArg = _.first(call.args) + try { + print( + ` ${formatRouterName(call.routerName)}\t .${call.method} \t: ${ + firstArg.value + } => ${call.args.slice(1).map(formatMethodCall).join(' => ')}` + ) + } catch (e) { + print('>> Error') + print(e) + print(JSON.stringify(call)) + process.exit(1) + } } - } - }, - }) + }, + } + ) } const routerNameMapping = { @@ -101,7 +104,7 @@ const main = () => { } // Find all routers - glob('*[rR]outer.js', { matchBase: true }, (err, files) => { + glob('*[rR]outer.*js', { matchBase: true }, (err, files) => { if (err) { console.error(err) process.exit(1) From bbf85ae6d2cef624807737ce3fcf8f99688a6a4b Mon Sep 17 00:00:00 2001 From: Tim Down <158919+timdown@users.noreply.github.com> Date: Thu, 3 Apr 2025 11:50:32 +0100 Subject: [PATCH 004/978] Merge pull request #24460 from overleaf/ar-personal-access-token-add-type-to-index [web] personal access token add type to index GitOrigin-RevId: 28b0fb8d3764c977d667cd8a5ee543d1f2e2eed2 --- ...21160345_add_type_to_pat_user_id_index.mjs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 services/web/migrations/20250321160345_add_type_to_pat_user_id_index.mjs diff --git a/services/web/migrations/20250321160345_add_type_to_pat_user_id_index.mjs b/services/web/migrations/20250321160345_add_type_to_pat_user_id_index.mjs new file mode 100644 index 0000000000..6b546a430d --- /dev/null +++ b/services/web/migrations/20250321160345_add_type_to_pat_user_id_index.mjs @@ -0,0 +1,31 @@ +import Helpers from './lib/helpers.mjs' + +const tags = ['server-ce', 'server-pro', 'saas'] + +const oldIndex = { + key: { user_id: 1 }, + name: 'user_id_1', +} + +const newIndex = { + key: { user_id: 1, type: 1 }, + name: 'user_id_1_type_1', +} + +const migrate = async client => { + const { db } = client + await Helpers.addIndexesToCollection(db.oauthAccessTokens, [newIndex]) + await Helpers.dropIndexesFromCollection(db.oauthAccessTokens, [oldIndex]) +} + +const rollback = async client => { + const { db } = client + await Helpers.addIndexesToCollection(db.oauthAccessTokens, [oldIndex]) + await Helpers.dropIndexesFromCollection(db.oauthAccessTokens, [newIndex]) +} + +export default { + tags, + migrate, + rollback, +} From 73b4584575cbf628b541149649158199266d089c Mon Sep 17 00:00:00 2001 From: M Fahru Date: Thu, 3 Apr 2025 06:20:21 -0700 Subject: [PATCH 005/978] Merge pull request #24384 from overleaf/mf-teardown-checkout-redesign-split-test [web] Tear down `checkout-redesign` split test GitOrigin-RevId: b3038276c28aece85a47d7b0a8134fad75e8af2c --- .../web/frontend/extracted-translations.json | 2 - .../stylesheets/app/subscription.less | 187 ------------------ services/web/locales/en.json | 6 +- 3 files changed, 2 insertions(+), 193 deletions(-) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index b826f386fb..6d64ddd77c 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -66,7 +66,6 @@ "add_comment_error_message": "", "add_comment_error_title": "", "add_company_details": "", - "add_company_details_lowercase": "", "add_email_address": "", "add_email_to_claim_features": "", "add_error_assist_annual_to_your_projects": "", @@ -1193,7 +1192,6 @@ "plus_more": "", "plus_x_additional_licenses_for_a_total_of_y_licenses": "", "postal_code": "", - "postal_code_sentence_case": "", "premium": "", "premium_feature": "", "premium_features": "", diff --git a/services/web/frontend/stylesheets/app/subscription.less b/services/web/frontend/stylesheets/app/subscription.less index 8b1cc3530c..7ec9f8db8e 100644 --- a/services/web/frontend/stylesheets/app/subscription.less +++ b/services/web/frontend/stylesheets/app/subscription.less @@ -16,63 +16,6 @@ } } -.price-breakdown { - text-align: center; - margin-bottom: -10px; -} - -.input-feedback-message { - display: none; - font-size: 0.8em; - - .has-error & { - display: inline-block; - } -} - -.payment-submit { - padding-top: (@line-height-computed / 2); -} - -.payment-method-toggle { - margin-bottom: (@line-height-computed / 2); - - &-switch { - display: inline-block; - width: 50%; - text-align: center; - border: solid 1px @gray-lighter; - border-radius: @border-radius-large 0 0 @border-radius-large; - padding: (@line-height-computed / 2); - color: @btn-switch-color; - - &:hover, - &:focus { - color: @btn-switch-color; - text-decoration: none; - } - - &:hover { - color: @btn-switch-hover-color; - } - - & + & { - border-left-width: 0; - border-radius: 0 @border-radius-large @border-radius-large 0; - } - - &-selected { - color: @link-active-color; - box-shadow: inset 0 -2px 0 0; - - &:hover, - &:focus { - color: @link-active-color; - } - } - } -} - .team-invite .message { margin: 3em 0; } @@ -85,136 +28,6 @@ text-transform: capitalize; } -.three-d-secure-container { - > .three-d-secure-recurly-container { - height: 400px; - - > div[data-recurly='three-d-secure-container'] { - height: 100%; - } - } -} - -.price-switch-header { - margin-bottom: @line-height-computed; - h2 { - margin: 0; - } - - .student-disclaimer { - font-size: 14px; - color: @gray; - margin: 12.5px 0 0 0; - } -} - -.price-feature-description { - h3 { - margin-top: 0; - } - h4 { - margin-top: 5px; - margin-bottom: 15px; - } - ul { - padding-left: 10px; - } - li { - list-style-position: inside; - } - .number-of-collaborators { - margin-bottom: 5px; - } -} - -.price-summary { - padding-bottom: 7.5px; - - .price-summary-line { - display: flex; - justify-content: space-between; - - .price-summary-line-width { - width: 70%; - } - } - .price-summary-total-line { - margin-top: 5px; - font-size: 16px; - - .price-summary-due { - color: @green-50; - } - } - hr { - margin-top: 20px; - margin-bottom: 20px; - } -} - -.price-nowrap { - white-space: nowrap; -} - -.price-details-spacing { - height: @line-height-computed / 2; -} - -.price-cancel-anytime { - font-size: 12px; -} - -.trial-coupon-summary { - font-size: 12px; - padding-top: 7.5px; - padding-bottom: 7.5px; - - .trial-coupon-summary-list-container { - padding-left: 0; - - li { - display: flex; - align-items: center; - font-size: 14px; - justify-content: flex-start; - margin-bottom: 8px; - gap: 12px; - } - } -} - -.toggle-address-second-line { - margin-bottom: @line-height-computed / 2; -} - -.payment-method-toggle { - border-bottom: 1px solid @hr-border; -} - -.change-currency { - margin-top: 5px; -} - -.change-currency-toggle { - padding-left: 0px; - font-size: 12px; - text-decoration: underline; - color: @text-small-color; - &:hover, - &:focus { - color: @text-small-color; - } -} - -.change-currency-dropdown-selected-icon { - position: absolute; - left: 10px; -} - -.recurly-hosted-field-input { - &:extend(.form-control); -} - .back-btn { padding: 0 10px 2px 12px; border-radius: 50%; diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 5083303331..e6b66f4d91 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -79,8 +79,7 @@ "add_comment": "Add comment", "add_comment_error_message": "There was an error adding your comment. Please try again in a few moments.", "add_comment_error_title": "Add Comment Error", - "add_company_details": "Add Company Details", - "add_company_details_lowercase": "Add company details", + "add_company_details": "Add company details", "add_email": "Add Email", "add_email_address": "Add email address", "add_email_to_claim_features": "Add an institutional email address to claim your features.", @@ -1601,8 +1600,7 @@ "popular_tags": "Popular Tags", "portal_add_affiliation_to_join": "It looks like you are already logged in to __appName__. If you have a __portalTitle__ email you can add it now.", "position": "Position", - "postal_code": "Postal Code", - "postal_code_sentence_case": "Postal code", + "postal_code": "Postal code", "premium": "Premium", "premium_feature": "Premium feature", "premium_features": "Premium features", From 1d1bad23e3b5c5f1bdd343d725d337e60affcd0d Mon Sep 17 00:00:00 2001 From: M Fahru Date: Thu, 3 Apr 2025 06:20:38 -0700 Subject: [PATCH 006/978] Merge pull request #24625 from overleaf/mf-fix-error-auth-pages-bs5 [web] Fix form message errors aren't shown properly in bs5 auth pages GitOrigin-RevId: 9a94fe53647d224faf63bdd047bfa26463d385f1 --- .../web/app/views/user/passwordReset-bs5.pug | 2 +- .../web/app/views/user/primaryEmailCheck-bs5.pug | 2 +- services/web/app/views/user/reconfirm-bs5.pug | 2 +- services/web/app/views/user/setPassword-bs5.pug | 16 ++++++++-------- .../stylesheets/bootstrap-5/pages/auth.scss | 4 ++++ 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/services/web/app/views/user/passwordReset-bs5.pug b/services/web/app/views/user/passwordReset-bs5.pug index d92ec5ece6..7637a91062 100644 --- a/services/web/app/views/user/passwordReset-bs5.pug +++ b/services/web/app/views/user/passwordReset-bs5.pug @@ -40,7 +40,7 @@ block content p.mb-3.pb-3(data-ol-not-sent) #{translate("enter_your_email_address_below_and_we_will_send_you_a_link_to_reset_your_password")}. div(data-ol-not-sent) - +formMessages() + +formMessagesNewStyle() if error && error !== 'password_reset_token_expired' +notification({ariaLive: 'assertive', type: 'error', className: 'mb-3', content: translate(error)}) diff --git a/services/web/app/views/user/primaryEmailCheck-bs5.pug b/services/web/app/views/user/primaryEmailCheck-bs5.pug index b04511a48f..0828c06e4b 100644 --- a/services/web/app/views/user/primaryEmailCheck-bs5.pug +++ b/services/web/app/views/user/primaryEmailCheck-bs5.pug @@ -14,7 +14,7 @@ block content method="POST" ) input(name='_csrf', type='hidden', value=csrfToken) - +formMessages() + +formMessagesNewStyle() button.btn.btn-primary.w-100.mb-3( type='submit' diff --git a/services/web/app/views/user/reconfirm-bs5.pug b/services/web/app/views/user/reconfirm-bs5.pug index 959abf26c6..8d9d13955f 100644 --- a/services/web/app/views/user/reconfirm-bs5.pug +++ b/services/web/app/views/user/reconfirm-bs5.pug @@ -33,7 +33,7 @@ block content | . div(data-ol-not-sent) - +formMessages() + +formMessagesNewStyle() input(type="hidden" name="_csrf" value=csrfToken) .form-group.mb-3 diff --git a/services/web/app/views/user/setPassword-bs5.pug b/services/web/app/views/user/setPassword-bs5.pug index be0c902a26..007ae5e87c 100644 --- a/services/web/app/views/user/setPassword-bs5.pug +++ b/services/web/app/views/user/setPassword-bs5.pug @@ -27,17 +27,17 @@ block content div(data-ol-not-sent) h1.h3.mb-3.mt-0 #{translate("reset_your_password")} p(data-ol-hide-on-error-message="token-expired") #{translate("create_a_new_password_for_your_account")}. - +formMessages() + +formMessagesNewStyle() - +customFormMessage('password-contains-email', 'danger') + +customFormMessageNewStyle('password-contains-email', 'danger') | #{translate('invalid_password_contains_email')}. | #{translate('use_a_different_password')}. - +customFormMessage('password-too-similar', 'danger') + +customFormMessageNewStyle('password-too-similar', 'danger') | #{translate('invalid_password_too_similar')}. | #{translate('use_a_different_password')}. - +customFormMessage('token-expired', 'danger') + +customFormMessageNewStyle('token-expired', 'danger') | #{translate('password_reset_token_expired')} br a(href="/user/password/reset") @@ -48,7 +48,7 @@ block content .form-group.mb-3 label.form-label(for='passwordField', data-ol-hide-on-error-message="token-expired") #{translate("new_password")} - input.form-control#passwordField( + input.form-control.auth-aux-new-password#passwordField( type='password' name='password' autocomplete="new-password" @@ -57,13 +57,13 @@ block content minlength=settings.passwordStrengthOptions.length.min ) - +customValidationMessage('invalid-password') + +customValidationMessageNewStyle('invalid-password') | #{translate('invalid_password')}. - +customValidationMessage('password-must-be-different') + +customValidationMessageNewStyle('password-must-be-different') | #{translate('password_cant_be_the_same_as_current_one')}. - +customValidationMessage('password-must-be-strong') + +customValidationMessageNewStyle('password-must-be-strong') | !{translate('password_was_detected_on_a_public_list_of_known_compromised_passwords', {}, [{name: 'a', attrs: {href: 'https://haveibeenpwned.com/passwords', rel: 'noopener noreferrer', target: '_blank'}}])}. | #{translate('use_a_different_password')}. diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/auth.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/auth.scss index d408ba28a9..cd23cadcee 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/auth.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/auth.scss @@ -23,6 +23,10 @@ } } +.auth-aux-new-password ~ .notification { + margin-top: var(--spacing-04); +} + .login-overleaf-logo-container { display: block; padding: var(--spacing-06); From 767ac1632e9d89e50ad304feccb7518319367ab5 Mon Sep 17 00:00:00 2001 From: Mathias Jakobsen Date: Fri, 4 Apr 2025 09:19:26 +0100 Subject: [PATCH 007/978] Merge pull request #24427 from overleaf/mj-command-registry [web] Editor redesign: Add command registry GitOrigin-RevId: c3d78d052f7e6e067de3247da8fe04329d8822ff --- services/web/.eslintrc.js | 6 + .../web/frontend/extracted-translations.json | 2 + .../context/command-registry-context.tsx | 59 +++++++++ .../ide-react/context/react-context-root.tsx | 6 +- .../ide-react/hooks/use-command-provider.ts | 21 +++ .../components/file-tree-toolbar.tsx | 34 +++++ .../components/toolbar/command-dropdown.tsx | 124 ++++++++++++++++++ .../components/toolbar/download-project.tsx | 31 +++++ .../components/toolbar/menu-bar.tsx | 51 ++++--- .../components/menu-bar/menu-bar-option.tsx | 3 + services/web/locales/en.json | 2 + 11 files changed, 321 insertions(+), 18 deletions(-) create mode 100644 services/web/frontend/js/features/ide-react/context/command-registry-context.tsx create mode 100644 services/web/frontend/js/features/ide-react/hooks/use-command-provider.ts create mode 100644 services/web/frontend/js/features/ide-redesign/components/toolbar/command-dropdown.tsx diff --git a/services/web/.eslintrc.js b/services/web/.eslintrc.js index 47d15bca87..3c672de7e7 100644 --- a/services/web/.eslintrc.js +++ b/services/web/.eslintrc.js @@ -39,6 +39,12 @@ module.exports = { 'error', { functions: false, classes: false, variables: false }, ], + 'react-hooks/exhaustive-deps': [ + 'warn', + { + additionalHooks: '(useCommandProvider)', + }, + ], }, overrides: [ // NOTE: changing paths may require updating them in the Makefile too. diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 6d64ddd77c..85094e14c5 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -1495,6 +1495,7 @@ "show_local_file_contents": "", "show_more": "", "show_outline": "", + "show_version_history": "", "show_x_more_projects": "", "showing_1_result": "", "showing_1_result_of_total": "", @@ -1893,6 +1894,7 @@ "upgrade_to_unlock_more_time": "", "upgrade_your_subscription": "", "upload": "", + "upload_file": "", "upload_from_computer": "", "upload_project": "", "upload_zipped_project": "", diff --git a/services/web/frontend/js/features/ide-react/context/command-registry-context.tsx b/services/web/frontend/js/features/ide-react/context/command-registry-context.tsx new file mode 100644 index 0000000000..340d4e1b77 --- /dev/null +++ b/services/web/frontend/js/features/ide-react/context/command-registry-context.tsx @@ -0,0 +1,59 @@ +import { createContext, useCallback, useContext, useState } from 'react' + +type CommandInvocationContext = { + location?: string +} + +export type Command = { + label: string + id: string + handler?: (context: CommandInvocationContext) => void + href?: string + disabled?: boolean + // TODO: Keybinding? +} + +const CommandRegistryContext = createContext( + undefined +) + +type CommandRegistry = { + registry: Map + register: (...elements: Command[]) => void + unregister: (...id: string[]) => void +} + +export const CommandRegistryProvider: React.FC = ({ children }) => { + const [registry, setRegistry] = useState(new Map()) + const register = useCallback((...elements: Command[]) => { + setRegistry( + registry => + new Map([ + ...registry, + ...elements.map(element => [element.id, element] as const), + ]) + ) + }, []) + + const unregister = useCallback((...ids: string[]) => { + setRegistry( + registry => new Map([...registry].filter(([key]) => !ids.includes(key))) + ) + }, []) + + return ( + + {children} + + ) +} + +export const useCommandRegistry = (): CommandRegistry => { + const context = useContext(CommandRegistryContext) + if (!context) { + throw new Error( + 'useCommandRegistry must be used within a CommandRegistryProvider' + ) + } + return context +} diff --git a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx index 9551777813..72c25b88c9 100644 --- a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx +++ b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx @@ -25,6 +25,7 @@ import { SplitTestProvider } from '@/shared/context/split-test-context' import { UserProvider } from '@/shared/context/user-context' import { UserSettingsProvider } from '@/shared/context/user-settings-context' import { IdeRedesignSwitcherProvider } from './ide-redesign-switcher-context' +import { CommandRegistryProvider } from './command-registry-context' export const ReactContextRoot: FC<{ providers?: Record }> = ({ children, @@ -57,6 +58,7 @@ export const ReactContextRoot: FC<{ providers?: Record }> = ({ UserProvider, UserSettingsProvider, IdeRedesignSwitcherProvider, + CommandRegistryProvider, ...providers, } @@ -87,7 +89,9 @@ export const ReactContextRoot: FC<{ providers?: Record }> = ({ - {children} + + {children} + diff --git a/services/web/frontend/js/features/ide-react/hooks/use-command-provider.ts b/services/web/frontend/js/features/ide-react/hooks/use-command-provider.ts new file mode 100644 index 0000000000..088304b39d --- /dev/null +++ b/services/web/frontend/js/features/ide-react/hooks/use-command-provider.ts @@ -0,0 +1,21 @@ +import { DependencyList, useEffect } from 'react' +import { + Command, + useCommandRegistry, +} from '../context/command-registry-context' + +export const useCommandProvider = ( + generateElements: () => Command[] | undefined, + dependencies: DependencyList +) => { + const { register, unregister } = useCommandRegistry() + useEffect(() => { + const elements = generateElements() + if (!elements) return + register(...elements) + return () => { + unregister(...elements.map(element => element.id)) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, dependencies) +} diff --git a/services/web/frontend/js/features/ide-redesign/components/file-tree-toolbar.tsx b/services/web/frontend/js/features/ide-redesign/components/file-tree-toolbar.tsx index cdc28ef4b7..957226c414 100644 --- a/services/web/frontend/js/features/ide-redesign/components/file-tree-toolbar.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/file-tree-toolbar.tsx @@ -8,6 +8,7 @@ import MaterialIcon, { } from '@/shared/components/material-icon' import React from 'react' import useCollapsibleFileTree from '../hooks/use-collapsible-file-tree' +import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider' function FileTreeToolbar() { const { t } = useTranslation() @@ -44,6 +45,39 @@ function FileTreeActionButtons() { startCreatingDocOrFile, startUploadingDocOrFile, } = useFileTreeActionable() + useCommandProvider(() => { + if (!canCreate || fileTreeReadOnly) return + return [ + { + label: t('new_file'), + id: 'new_file', + handler: ({ location }) => { + eventTracking.sendMB('new-file-click', { location }) + startCreatingDocOrFile() + }, + }, + { + label: t('new_folder'), + id: 'new_folder', + handler: startCreatingFolder, + }, + { + label: t('upload_file'), + id: 'upload_file', + handler: ({ location }) => { + eventTracking.sendMB('upload-click', { location }) + startUploadingDocOrFile() + }, + }, + ] + }, [ + canCreate, + fileTreeReadOnly, + startCreatingDocOrFile, + t, + startCreatingFolder, + startUploadingDocOrFile, + ]) if (!canCreate || fileTreeReadOnly) return null diff --git a/services/web/frontend/js/features/ide-redesign/components/toolbar/command-dropdown.tsx b/services/web/frontend/js/features/ide-redesign/components/toolbar/command-dropdown.tsx new file mode 100644 index 0000000000..9d0deea0c5 --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/components/toolbar/command-dropdown.tsx @@ -0,0 +1,124 @@ +import { + Command, + useCommandRegistry, +} from '@/features/ide-react/context/command-registry-context' +import { + DropdownDivider, + DropdownHeader, +} from '@/features/ui/components/bootstrap-5/dropdown-menu' +import { + MenuBarDropdown, + NestedMenuBarDropdown, +} from '@/shared/components/menu-bar/menu-bar-dropdown' +import { MenuBarOption } from '@/shared/components/menu-bar/menu-bar-option' +import { Fragment, useCallback } from 'react' + +type CommandId = string +type TaggedCommand = Command & { type: 'command' } +type Entry = T | GroupStructure +type GroupStructure = { + id: string + title: string + children: Array> +} +type MenuSectionStructure = { + title?: string + id: string + children: Array> +} +export type MenuStructure = Array> + +const CommandDropdown = ({ + menu, + title, + id, +}: { + menu: MenuStructure + title: string + id: string +}) => { + const { registry } = useCommandRegistry() + const populatedSections = menu + .map(section => populateSectionOrGroup(section, registry)) + .filter(x => x.children.length > 0) + return ( + + {populatedSections.map((section, index) => { + return ( + + {index > 0 && } + {section.title && {section.title}} + {section.children.map(child => ( + + ))} + + ) + })} + + ) +} + +const CommandDropdownChild = ({ item }: { item: Entry }) => { + const onClickHandler = useCallback(() => { + if (isTaggedCommand(item)) { + item.handler?.({ location: 'menu-bar' }) + } + }, [item]) + + if (isTaggedCommand(item)) { + return ( + + ) + } else { + return ( + + {item.children.map(subChild => { + return + })} + + ) + } +} + +export default CommandDropdown + +function populateSectionOrGroup< + T extends { children: Array> }, +>( + section: T, + registry: Map +): Omit & { + children: Array> +} { + const { children, ...rest } = section + return { + ...rest, + children: children + .map(child => { + if (typeof child !== 'string') { + return populateSectionOrGroup(child, registry) + } + const command = registry.get(child) + if (command) { + return { ...command, type: 'command' as const } + } + return undefined + }) + .filter(x => x !== undefined), + } +} + +function isTaggedCommand(item: Entry): item is TaggedCommand { + return 'type' in item && item.type === 'command' +} diff --git a/services/web/frontend/js/features/ide-redesign/components/toolbar/download-project.tsx b/services/web/frontend/js/features/ide-redesign/components/toolbar/download-project.tsx index e40f385fa1..712c874309 100644 --- a/services/web/frontend/js/features/ide-redesign/components/toolbar/download-project.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/toolbar/download-project.tsx @@ -1,3 +1,4 @@ +import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider' import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item' import OLTooltip from '@/features/ui/components/ol/ol-tooltip' import { isSmallDevice, sendMB } from '@/infrastructure/event-tracking' @@ -17,6 +18,17 @@ export const DownloadProjectZip = () => { }) }, [projectId]) + useCommandProvider( + () => [ + { + id: 'download-as-source-zip', + href: `/project/${projectId}/download/zip`, + label: t('download_as_source_zip'), + }, + ], + [t, projectId] + ) + return ( { }) }, [projectId]) + useCommandProvider( + () => [ + { + id: 'download-pdf', + disabled: !pdfUrl, + href: pdfDownloadUrl || pdfUrl, + handler: ({ location }) => { + sendMB('download-pdf-button-click', { + projectId, + location, + isSmallDevice, + }) + }, + label: t('download_as_pdf'), + }, + ], + [t, pdfUrl, projectId, pdfDownloadUrl] + ) + const button = ( { const { t } = useTranslation() @@ -22,27 +25,41 @@ export const ToolbarMenuBar = () => { const openEditorRedesignSwitcherModal = useCallback(() => { setShowSwitcherModal(true) }, [setShowSwitcherModal]) + const { setView, view } = useLayoutContext() + + useCommandProvider( + () => [ + { + label: t('show_version_history'), + handler: () => { + setView(view === 'history' ? 'editor' : 'history') + }, + id: 'show_version_history', + }, + ], + [t, setView, view] + ) + const fileMenuStructure: MenuStructure = useMemo( + () => [ + { + id: 'file-file-tree', + children: ['new_file', 'new_folder', 'upload_file'], + }, + { id: 'file-history', children: ['show_version_history'] }, + { + id: 'file-download', + children: ['download-as-source-zip', 'download-pdf'], + }, + ], + [] + ) + return ( - - - - - - - - - - - - + { @@ -24,6 +26,7 @@ export const MenuBarOption = ({ onClick={onClick} disabled={disabled} trailingIcon={trailingIcon} + href={href} > {title} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index e6b66f4d91..d550e59d0a 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1968,6 +1968,7 @@ "show_local_file_contents": "Show Local File Contents", "show_more": "show more", "show_outline": "Show File outline", + "show_version_history": "Show version history", "show_x_more_projects": "Show __x__ more projects", "showing_1_result": "Showing 1 result", "showing_1_result_of_total": "Showing 1 result of __total__", @@ -2432,6 +2433,7 @@ "upgrade_your_subscription": "Upgrade your subscription", "upload": "Upload", "upload_failed": "Upload failed", + "upload_file": "Upload file", "upload_from_computer": "Upload from computer", "upload_project": "Upload Project", "upload_zipped_project": "Upload Zipped Project", From 0cc244c5161b40dc6b1a830dc11e28d335acd01c Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Fri, 4 Apr 2025 10:52:07 +0100 Subject: [PATCH 008/978] Merge pull request #20022 from overleaf/bg-check-file-tree add script to check for errors in project file tree GitOrigin-RevId: da115cbd79e7ca53a0222638a54bbea1b633f709 --- services/web/scripts/check_project_files.js | 261 ++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 services/web/scripts/check_project_files.js diff --git a/services/web/scripts/check_project_files.js b/services/web/scripts/check_project_files.js new file mode 100644 index 0000000000..41fe9ecda7 --- /dev/null +++ b/services/web/scripts/check_project_files.js @@ -0,0 +1,261 @@ +const Path = require('path') +const DocstoreManager = require('../app/src/Features/Docstore/DocstoreManager') +const DocumentUpdaterHandler = require('../app/src/Features/DocumentUpdater/DocumentUpdaterHandler') +const FileStoreHandler = require('../app/src/Features/FileStore/FileStoreHandler') +const ProjectGetter = require('../app/src/Features/Project/ProjectGetter') +const ProjectEntityMongoUpdateHandler = require('../app/src/Features/Project/ProjectEntityMongoUpdateHandler') +const { waitForDb, db, ObjectId } = require('../app/src/infrastructure/mongodb') +const logger = require('@overleaf/logger').logger + +const args = require('minimist')(process.argv.slice(2), { + boolean: ['verbose', 'fix'], +}) +const verbose = args.verbose + +if (!verbose) { + logger.level('error') +} + +// no remaining arguments, print usage +if (args._.length === 0) { + console.log( + 'Usage: node services/web/scripts/check_project_docs.js [--verbose] [--fix] ...' + ) + process.exit(1) +} + +function logDoc(projectId, path, doc, message = '') { + console.log( + 'projectId:', + projectId, + 'doc:', + JSON.stringify({ + _id: doc._id, + name: doc.name, + lines: doc.lines ? doc.lines.join('\n').length : 0, + rev: doc.rev, + version: doc.version, + ranges: typeof doc.ranges, + }), + path, + message + ) +} + +function logFile(projectId, path, file, message = '') { + console.log( + 'projectId:', + projectId, + 'file:', + JSON.stringify({ + _id: file._id, + name: file.name, + linkedFileData: file.linkedFileData, + hash: file.hash, + size: file.size, + }), + path, + message + ) +} + +function findPathCounts(projectId, docEntries, fileEntries) { + const pathCounts = new Map() + const docPaths = docEntries.map(({ path }) => path) + const filePaths = fileEntries.map(({ path }) => path) + const allPaths = docPaths.concat(filePaths) + for (const path of allPaths) { + pathCounts.set(path, (pathCounts.get(path) || 0) + 1) + } + return pathCounts +} + +// copied from services/web/app/src/Features/Project/ProjectDuplicator.js +function _getFolderEntries(folder, folderPath = '/') { + const docEntries = [] + const fileEntries = [] + const docs = folder.docs || [] + const files = folder.fileRefs || [] + const subfolders = folder.folders || [] + + for (const doc of docs) { + if (doc == null || doc._id == null) { + continue + } + const path = Path.join(folderPath, doc.name) + docEntries.push({ doc, path }) + } + + for (const file of files) { + if (file == null || file._id == null) { + continue + } + const path = Path.join(folderPath, file.name) + fileEntries.push({ file, path }) + } + + for (const subfolder of subfolders) { + if (subfolder == null || subfolder._id == null) { + continue + } + const subfolderPath = Path.join(folderPath, subfolder.name) + const subfolderEntries = _getFolderEntries(subfolder, subfolderPath) + for (const docEntry of subfolderEntries.docEntries) { + docEntries.push(docEntry) + } + for (const fileEntry of subfolderEntries.fileEntries) { + fileEntries.push(fileEntry) + } + } + return { docEntries, fileEntries } +} + +async function getDocsInMongo(projectId) { + return await db.docs + .find({ project_id: new ObjectId(projectId), deleted: { $ne: true } }) + .toArray() +} + +function getDocIdsInFileTree(docEntries) { + return docEntries.map(({ doc }) => doc._id.toString()) +} + +function findMissingDocs(docsInMongo, docIdsInFileTree) { + const missingDocs = [] + for (const doc of docsInMongo) { + const docId = doc._id.toString() + if (!docIdsInFileTree.includes(docId)) { + console.log(`Found doc in docstore not in project filetree:`, docId) + missingDocs.push(doc) + } + } + return missingDocs +} + +async function createRecoveryFolder(projectId) { + const recoveryFolder = `recovered-${Date.now()}` + const { folder } = await ProjectEntityMongoUpdateHandler.promises.mkdirp( + new ObjectId(projectId), + recoveryFolder + ) + console.log('Created recovery folder:', folder._id.toString()) + return folder +} + +async function restoreMissingDocs(projectId, folder, missingDocs) { + for (const doc of missingDocs) { + doc.name = doc.name || `unknown-file-${doc._id.toString()}` + try { + await ProjectEntityMongoUpdateHandler.promises.addDoc( + new ObjectId(projectId), + folder._id, + doc + ) + console.log('Restored doc to filetree:', doc._id.toString()) + } catch (err) { + console.log(`Error adding doc to filetree:`, err) + } + } +} + +async function checkProject(projectId) { + try { + await DocumentUpdaterHandler.promises.flushProjectToMongo(projectId) + } catch (err) { + console.log(`Error flushing project ${projectId} to mongo: ${err}`) + } + const project = await ProjectGetter.promises.getProject(projectId, { + rootFolder: true, + rootDoc_id: true, + }) + if (verbose) { + console.log(`project: ${JSON.stringify(project)}`) + } + const { docEntries, fileEntries } = _getFolderEntries(project.rootFolder[0]) + console.log( + `Found ${docEntries.length} docEntries and ${fileEntries.length} fileEntries` + ) + const pathCounts = findPathCounts(projectId, docEntries, fileEntries) + + for (const [path, count] of pathCounts) { + if (count > 1) { + console.log(`Found duplicate path: ${path}`) + } + } + + let errors = 0 + for (const { doc, path } of docEntries) { + try { + const { lines, rev, version, ranges } = + await DocstoreManager.promises.getDoc(projectId, doc._id) + if (!lines) { + throw new Error('no doclines') + } + if (pathCounts.get(path) > 1) { + logDoc( + projectId, + path, + { ...doc, lines, rev, version, ranges }, + 'duplicate path' + ) + errors++ + } else if (verbose) { + logDoc(projectId, path, { ...doc, lines, rev, version, ranges }) + } + } catch (err) { + logDoc(projectId, path, doc, err) + errors++ + } + } + for (const { file, path } of fileEntries) { + try { + const fileSize = await FileStoreHandler.promises.getFileSize( + projectId, + file._id + ) + if (pathCounts.get(path) > 1) { + logFile(projectId, path, { ...file, fileSize }, 'duplicate path') + errors++ + } else if (verbose) { + logFile(projectId, path, { ...file, fileSize }) + } + } catch (err) { + logFile(projectId, path, file, err) + errors++ + } + } + + // now look for docs in the docstore that are not in the project filetree + const docsInMongo = await getDocsInMongo(projectId) + const docIdsInFileTree = getDocIdsInFileTree(docEntries) + const missingDocs = findMissingDocs(docsInMongo, docIdsInFileTree) + + if (args.fix && missingDocs.length > 0) { + console.log('Restoring missing docs to filetree...') + const folder = await createRecoveryFolder(projectId) + await restoreMissingDocs(projectId, folder, missingDocs) + } + + if (errors > 0) { + console.log(`Errors found in project: ${projectId}`) + } else { + console.log(`No errors found in project: ${projectId}`) + } +} + +async function main() { + await waitForDb() + for (const projectId of args._) { + await checkProject(projectId) + } +} + +main() + .then(() => { + console.log('DONE') + process.exit(0) + }) + .catch(err => { + console.error(err) + process.exit(1) + }) From bfe5871e9ea9782651e175cd881567227b50ca32 Mon Sep 17 00:00:00 2001 From: Domagoj Kriskovic Date: Fri, 4 Apr 2025 12:29:56 +0200 Subject: [PATCH 009/978] Dropbox sync info message for read-only and reviewer collaborators (#24641) * Dropbox sync info message for read-only and reviewer collaborators * fix translation text GitOrigin-RevId: 12984a1f9fa20c39f171b56f4a46830df7a5f5e0 --- services/web/frontend/extracted-translations.json | 2 ++ services/web/locales/en.json | 2 ++ 2 files changed, 4 insertions(+) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 85094e14c5..89061439bf 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -1269,6 +1269,7 @@ "read_lines_from_path": "", "read_more": "", "read_more_about_free_compile_timeouts_servers": "", + "read_only_dropbox_sync_message": "", "read_only_token": "", "read_write_token": "", "ready_to_join_x": "", @@ -1372,6 +1373,7 @@ "review_panel_comments_and_track_changes": "", "review_your_peers_work": "", "reviewer": "", + "reviewer_dropbox_sync_message": "", "reviewing": "", "revoke": "", "revoke_invite": "", diff --git a/services/web/locales/en.json b/services/web/locales/en.json index d550e59d0a..229838179d 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1685,6 +1685,7 @@ "read_lines_from_path": "Read lines from __path__", "read_more": "Read more", "read_more_about_free_compile_timeouts_servers": "Read more about changes to free compile timeouts and servers", + "read_only_dropbox_sync_message": "As a read-only viewer you can sync the current project version to Dropbox, but changes made in Dropbox will <0>not sync back to Overleaf.", "read_only_token": "Read-Only Token", "read_write_token": "Read-Write Token", "ready_to_join_x": "You’re ready to join __inviterName__", @@ -1827,6 +1828,7 @@ "review_panel_comments_and_track_changes": "Review panel – Comments & track changes", "review_your_peers_work": "Review your peers’ work", "reviewer": "Reviewer", + "reviewer_dropbox_sync_message": "As a reviewer you can sync the current project version to Dropbox, but changes made in Dropbox will <0>not sync back to Overleaf.", "reviewing": "Reviewing", "revoke": "Revoke", "revoke_invite": "Revoke Invite", From 51250ca45fe9cab865d0bd71a74705d35a11f6e4 Mon Sep 17 00:00:00 2001 From: Domagoj Kriskovic Date: Fri, 4 Apr 2025 12:30:44 +0200 Subject: [PATCH 010/978] Add review access notification in Git bridge modal (#24623) GitOrigin-RevId: e9efc2f036445f610f2c1aa60a882faf09d2067f --- services/web/frontend/extracted-translations.json | 1 + services/web/locales/en.json | 1 + 2 files changed, 2 insertions(+) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 89061439bf..53330a94ec 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -612,6 +612,7 @@ "git_bridge_modal_git_clone_your_project": "", "git_bridge_modal_learn_more_about_authentication_tokens": "", "git_bridge_modal_read_only": "", + "git_bridge_modal_review_access": "", "git_bridge_modal_see_once": "", "git_bridge_modal_use_previous_token": "", "git_integration": "", diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 229838179d..7c40508cd3 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -815,6 +815,7 @@ "git_bridge_modal_git_clone_your_project": "Git clone your project by using the link below and a Git authentication token", "git_bridge_modal_learn_more_about_authentication_tokens": "Learn more about Git integration authentication tokens.", "git_bridge_modal_read_only": "You have read-only access to this project. This means you can pull from __appName__ but you can’t push any changes you make back to this project.", + "git_bridge_modal_review_access": "<0>You have review access to this project. This means you can pull from __appName__ but you can’t push any changes you make back to this project.", "git_bridge_modal_see_once": "You’ll only see this token once. To delete it or generate a new one, visit Account Settings. For detailed instructions and troubleshooting, read our <0>help page.", "git_bridge_modal_use_previous_token": "If you’re prompted for a password, you can use a previously generated Git authentication token. Or you can generate a new one in Account Settings. For more support, read our <0>help page.", "git_gitHub_dropbox_mendeley_papers_and_zotero_integrations": "Git, GitHub, Dropbox, Mendeley, Papers, and Zotero integrations", From 6169a5d3df50a254741f213374a9b91790816515 Mon Sep 17 00:00:00 2001 From: Domagoj Kriskovic Date: Fri, 4 Apr 2025 12:30:56 +0200 Subject: [PATCH 011/978] Update track changes paywall modal (#24620) * Update track changes paywall modal * update list styling GitOrigin-RevId: f5eda3a4b19c89105e163c8b5729ebcdd5dca2d0 --- .../web/frontend/extracted-translations.json | 5 + ...review-panel-track-changes-menu-button.tsx | 4 +- .../upgrade-track-changes-modal-legacy.tsx | 109 ++++++++++++++++++ .../upgrade-track-changes-modal.tsx | 25 ++-- .../bootstrap-5/components/modal.scss | 23 ++++ services/web/locales/en.json | 5 + 6 files changed, 154 insertions(+), 17 deletions(-) create mode 100644 services/web/frontend/js/features/review-panel-new/components/upgrade-track-changes-modal-legacy.tsx diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 53330a94ec..44633faea4 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -38,9 +38,11 @@ "accept_change_error_title": "", "accept_invitation": "", "accept_or_reject_each_changes_individually": "", + "accept_or_reject_individual_edits": "", "accept_selected_changes": "", "accept_terms_and_conditions": "", "accepted_invite": "", + "access_all_premium_features": "", "access_denied": "", "access_edit_your_projects": "", "access_levels_changed": "", @@ -603,6 +605,7 @@ "get_exclusive_access_to_labs": "", "get_in_touch": "", "get_most_subscription_discover_premium_features": "", + "get_real_time_track_changes": "", "git": "", "git_authentication_token": "", "git_authentication_token_create_modal_info_1": "", @@ -1429,6 +1432,7 @@ "searched_path_for_lines_containing": "", "security": "", "see_changes_in_your_documents_live": "", + "see_suggestions_from_collaborators": "", "select_a_column_or_a_merged_cell_to_align": "", "select_a_column_to_adjust_column_width": "", "select_a_file": "", @@ -1893,6 +1897,7 @@ "upgrade_summary": "", "upgrade_to_add_more_editors_and_access_collaboration_features": "", "upgrade_to_get_feature": "", + "upgrade_to_review": "", "upgrade_to_track_changes": "", "upgrade_to_unlock_more_time": "", "upgrade_your_subscription": "", diff --git a/services/web/frontend/js/features/review-panel-new/components/review-panel-track-changes-menu-button.tsx b/services/web/frontend/js/features/review-panel-new/components/review-panel-track-changes-menu-button.tsx index 60657e82d3..f4f3cea7fd 100644 --- a/services/web/frontend/js/features/review-panel-new/components/review-panel-track-changes-menu-button.tsx +++ b/services/web/frontend/js/features/review-panel-new/components/review-panel-track-changes-menu-button.tsx @@ -3,7 +3,7 @@ import { Trans } from 'react-i18next' import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' import MaterialIcon from '@/shared/components/material-icon' import { useProjectContext } from '@/shared/context/project-context' -import UpgradeTrackChangesModal from '@/features/review-panel-new/components/upgrade-track-changes-modal' +import UpgradeTrackChangesModalLegacy from './upgrade-track-changes-modal-legacy' import { send, sendMB } from '@/infrastructure/event-tracking' const sendAnalytics = () => { @@ -52,7 +52,7 @@ const ReviewPanelTrackChangesMenuButton: FC<{ - + ) } diff --git a/services/web/frontend/js/features/review-panel-new/components/upgrade-track-changes-modal-legacy.tsx b/services/web/frontend/js/features/review-panel-new/components/upgrade-track-changes-modal-legacy.tsx new file mode 100644 index 0000000000..ccc5f1d452 --- /dev/null +++ b/services/web/frontend/js/features/review-panel-new/components/upgrade-track-changes-modal-legacy.tsx @@ -0,0 +1,109 @@ +import { useTranslation } from 'react-i18next' +import { useProjectContext } from '@/shared/context/project-context' +import { useUserContext } from '@/shared/context/user-context' +import teaserVideo from '../images/teaser-track-changes.mp4' +import teaserImage from '../images/teaser-track-changes.gif' +import { startFreeTrial, upgradePlan } from '@/main/account-upgrade' +import { memo } from 'react' +import OLModal, { + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/features/ui/components/ol/ol-modal' +import OLButton from '@/features/ui/components/ol/ol-button' +import OLRow from '@/features/ui/components/ol/ol-row' +import OLCol from '@/features/ui/components/ol/ol-col' +import MaterialIcon from '@/shared/components/material-icon' + +type UpgradeTrackChangesModalProps = { + show: boolean + setShow: React.Dispatch> +} + +function UpgradeTrackChangesModalLegacy({ + show, + setShow, +}: UpgradeTrackChangesModalProps) { + const { t } = useTranslation() + const project = useProjectContext() + const user = useUserContext() + + return ( + setShow(false)}> + + {t('upgrade_to_track_changes')} + + +
+ {/* eslint-disable-next-line jsx-a11y/media-has-caption */} + +
+

+ {t('see_changes_in_your_documents_live')} +

+ + +
    + {[ + t('track_any_change_in_real_time'), + t('review_your_peers_work'), + t('accept_or_reject_each_changes_individually'), + ].map(translation => ( +
  • + +  {translation} +
  • + ))} +
+
+
+

+ {t('already_subscribed_try_refreshing_the_page')} +

+ {project.owner && ( +
+ {project.owner._id === user.id ? ( + user.allowedFreeTrial ? ( + startFreeTrial('track-changes')} + > + {t('try_it_for_free')} + + ) : ( + upgradePlan('project-sharing')} + > + {t('upgrade')} + + ) + ) : ( +

+ + {t( + 'please_ask_the_project_owner_to_upgrade_to_track_changes' + )} + +

+ )} +
+ )} +
+ + setShow(false)}> + {t('close')} + + +
+ ) +} + +export default memo(UpgradeTrackChangesModalLegacy) diff --git a/services/web/frontend/js/features/review-panel-new/components/upgrade-track-changes-modal.tsx b/services/web/frontend/js/features/review-panel-new/components/upgrade-track-changes-modal.tsx index 2c3205d902..4948a00ce3 100644 --- a/services/web/frontend/js/features/review-panel-new/components/upgrade-track-changes-modal.tsx +++ b/services/web/frontend/js/features/review-panel-new/components/upgrade-track-changes-modal.tsx @@ -32,9 +32,9 @@ function UpgradeTrackChangesModal({ return ( setShow(false)}> - {t('upgrade_to_track_changes')} + {t('upgrade_to_review')} - +
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
-

- {t('see_changes_in_your_documents_live')} -

+

{t('get_real_time_track_changes')}

    {[ - t('track_any_change_in_real_time'), - t('review_your_peers_work'), - t('accept_or_reject_each_changes_individually'), + t('see_suggestions_from_collaborators'), + t('accept_or_reject_individual_edits'), + t('access_all_premium_features'), ].map(translation => (
  • - -  {translation} + + {translation}
  • ))}
-

- {t('already_subscribed_try_refreshing_the_page')} -

{project.owner && (
{project.owner._id === user.id ? ( user.allowedFreeTrial ? ( startFreeTrial('track-changes')} > {t('try_it_for_free')} ) : ( upgradePlan('project-sharing')} > {t('upgrade')} diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/modal.scss b/services/web/frontend/stylesheets/bootstrap-5/components/modal.scss index 6689bdd0bb..e19e9c249e 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/modal.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/modal.scss @@ -85,3 +85,26 @@ .git-bridge-optional-tokens-actions { margin-top: var(--spacing-05); } + +.upgrade-track-changes-modal { + .teaser-title { + font-family: var(--font-sans); + font-size: var(--font-size-05); + margin-bottom: var(--spacing-05); + margin-top: var(--spacing-05); + color: var(--content-secondary); + } + + ul.list-unstyled li { + font-family: var(--font-sans); + padding: var(--spacing-02) 0; + color: var(--content-secondary); + display: flex; + align-items: flex-start; + gap: var(--spacing-04); + } + + ul.list-unstyled li .check-icon { + padding-top: 2px; + } +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 7c40508cd3..20074a55ed 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -43,10 +43,12 @@ "accept_change_error_title": "Accept Change Error", "accept_invitation": "Accept invitation", "accept_or_reject_each_changes_individually": "Accept or reject each change individually", + "accept_or_reject_individual_edits": "Accept or reject individual edits", "accept_selected_changes": "Accept selected changes", "accept_terms_and_conditions": "Accept terms and conditions", "accepted_invite": "Accepted invite", "accepting_invite_as": "You are accepting this invite as", + "access_all_premium_features": "Access all premium features, including more collaborators, full project history, and a longer compile time.", "access_denied": "Access Denied", "access_edit_your_projects": "Access and edit your projects", "access_levels_changed": "Access levels changed", @@ -804,6 +806,7 @@ "get_in_touch_having_problems": "Get in touch with support if you’re having problems", "get_involved": "Get involved", "get_most_subscription_discover_premium_features": "Get the most from your __appName__ subscription. <0>Discover premium features.", + "get_real_time_track_changes": "Get real-time track changes", "get_the_best_overleaf_experience": "Get the best Overleaf experience", "get_the_most_out_headline": "Get the most out of __appName__ with features such as:", "git": "Git", @@ -1894,6 +1897,7 @@ "secondary_email_password_reset": "That email is registered as a secondary email. Please enter the primary email for your account.", "security": "Security", "see_changes_in_your_documents_live": "See changes in your documents, live", + "see_suggestions_from_collaborators": "See suggestions from collaborators", "select_a_column_or_a_merged_cell_to_align": "Select a column or a merged cell to align", "select_a_column_to_adjust_column_width": "Select a column to adjust column width", "select_a_file": "Select a File", @@ -2431,6 +2435,7 @@ "upgrade_summary": "Upgrade summary", "upgrade_to_add_more_editors_and_access_collaboration_features": "Upgrade to add more editors and access collaboration features like track changes and full project history.", "upgrade_to_get_feature": "Upgrade to get __feature__, plus:", + "upgrade_to_review": "Upgrade to Review", "upgrade_to_track_changes": "Upgrade to track changes", "upgrade_to_unlock_more_time": "Upgrade now to unlock 12x more compile time on our fastest servers.", "upgrade_your_subscription": "Upgrade your subscription", From 1fb18b092d53eb40b1976f776734e8be033bea51 Mon Sep 17 00:00:00 2001 From: Domagoj Kriskovic Date: Fri, 4 Apr 2025 12:31:08 +0200 Subject: [PATCH 012/978] Add upgrade prompt for track changes in share modal (#24572) * Add upgrade prompt for track changes in share modal * remove message from invite.jsx * Fix itemToDisabled in Select GitOrigin-RevId: 5ba9e2b063c7e26a4c39b9e973eddce36a5b4733 --- .../web/frontend/extracted-translations.json | 1 + .../components/add-collaborators.jsx | 2 +- .../components/edit-member.tsx | 24 ++++++++++++++++--- .../components/share-modal-body.tsx | 3 +++ services/web/locales/en.json | 1 + .../components/share-project-modal.test.jsx | 4 ++-- 6 files changed, 29 insertions(+), 6 deletions(-) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 44633faea4..945d3a1f70 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -267,6 +267,7 @@ "comment": "", "comment_only": "", "comment_only_upgrade_for_track_changes": "", + "comment_only_upgrade_to_enable_track_changes": "", "common": "", "common_causes_of_compile_timeouts_include": "", "commons_plan_tooltip": "", diff --git a/services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx b/services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx index aa5948cb6f..904946775b 100644 --- a/services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx +++ b/services/web/frontend/js/features/share-project-modal/components/add-collaborators.jsx @@ -194,7 +194,7 @@ export default function AddCollaborators({ readOnly }) { itemToKey={item => item.key} itemToString={item => item.label} itemToSubtitle={item => item.description || ''} - itemToDisabled={item => readOnly && item.key === 'readAndWrite'} + itemToDisabled={item => readOnly && item.key !== 'readOnly'} selected={privilegeOptions.find( option => option.key === privileges )} diff --git a/services/web/frontend/js/features/share-project-modal/components/edit-member.tsx b/services/web/frontend/js/features/share-project-modal/components/edit-member.tsx index 433d16d9c7..5b7ac58fd7 100644 --- a/services/web/frontend/js/features/share-project-modal/components/edit-member.tsx +++ b/services/web/frontend/js/features/share-project-modal/components/edit-member.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useMemo } from 'react' import PropTypes from 'prop-types' -import { useTranslation } from 'react-i18next' +import { Trans, useTranslation } from 'react-i18next' import { useShareProjectContext } from './share-project-modal' import TransferOwnershipModal from './transfer-ownership-modal' import { removeMemberFromProject, updateMember } from '../utils/api' @@ -17,6 +17,7 @@ import MaterialIcon from '@/shared/components/material-icon' import getMeta from '@/utils/meta' import { useUserContext } from '@/shared/context/user-context' import { isSplitTestEnabled } from '@/utils/splitTestUtils' +import { upgradePlan } from '@/main/account-upgrade' type PermissionsOption = PermissionsLevel | 'removeAccess' | 'downgraded' @@ -25,6 +26,7 @@ type EditMemberProps = { hasExceededCollaboratorLimit: boolean hasBeenDowngraded: boolean canAddCollaborators: boolean + isReviewerOnFreeProject?: boolean } type Privilege = { @@ -37,6 +39,7 @@ export default function EditMember({ hasExceededCollaboratorLimit, hasBeenDowngraded, canAddCollaborators, + isReviewerOnFreeProject, }: EditMemberProps) { const [privileges, setPrivileges] = useState( member.privileges @@ -144,7 +147,7 @@ export default function EditMember({ }} > - +
)} + {isReviewerOnFreeProject && ( +
+ upgradePlan('track-changes')} + />, + ]} + /> +
+ )}
- + {confirmRemoval && ( setPrivileges(member.privileges)} diff --git a/services/web/frontend/js/features/share-project-modal/components/share-modal-body.tsx b/services/web/frontend/js/features/share-project-modal/components/share-modal-body.tsx index 500834d30d..886e877f55 100644 --- a/services/web/frontend/js/features/share-project-modal/components/share-modal-body.tsx +++ b/services/web/frontend/js/features/share-project-modal/components/share-modal-body.tsx @@ -117,6 +117,9 @@ export default function ShareModalBody() { member.pendingEditor || member.pendingReviewer )} canAddCollaborators={canAddCollaborators} + isReviewerOnFreeProject={ + member.privileges === 'review' && !features.trackChanges + } /> ) : ( diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 20074a55ed..d0bd81af5a 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -348,6 +348,7 @@ "comment": "Comment", "comment_only": "Comment only", "comment_only_upgrade_for_track_changes": "Comment only. Upgrade for track changes.", + "comment_only_upgrade_to_enable_track_changes": "Comment only. <0>Upgrade to enable track changes.", "common": "Common", "common_causes_of_compile_timeouts_include": "Common causes of compile timeouts include", "commons_plan_tooltip": "You’re on the __plan__ plan because of your affiliation with __institution__. Click to find out how to make the most of your Overleaf premium features.", diff --git a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx index 9e02710275..cee1bca5ab 100644 --- a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx +++ b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx @@ -700,7 +700,7 @@ describe('', function () { const viewerOption = screen.getByText('Viewer').closest('button') expect(editorOption.classList.contains('disabled')).to.be.true - expect(reviewerOption.classList.contains('disabled')).to.be.false + expect(reviewerOption.classList.contains('disabled')).to.be.true expect(viewerOption.classList.contains('disabled')).to.be.false screen.getByText( @@ -737,7 +737,7 @@ describe('', function () { const viewerOption = screen.getByText('Viewer').closest('button') expect(editorOption.classList.contains('disabled')).to.be.true - expect(reviewerOption.classList.contains('disabled')).to.be.false + expect(reviewerOption.classList.contains('disabled')).to.be.true expect(viewerOption.classList.contains('disabled')).to.be.false screen.getByText( From ee118894315c54a04853c33bb806e039c1e8b2f5 Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Fri, 4 Apr 2025 15:26:18 +0300 Subject: [PATCH 013/978] Merge pull request #24647 from overleaf/ii-group-members-table [web] Group members table colspan fix GitOrigin-RevId: 99d52f0081ef500f63d86d9bdc2fda5c3cdab1d9 --- .../group-management/components/members-table/members-list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/frontend/js/features/group-management/components/members-table/members-list.tsx b/services/web/frontend/js/features/group-management/components/members-table/members-list.tsx index 2fe9339dda..ca1a5cdf88 100644 --- a/services/web/frontend/js/features/group-management/components/members-table/members-list.tsx +++ b/services/web/frontend/js/features/group-management/components/members-table/members-list.tsx @@ -83,7 +83,7 @@ export default function MembersList({ groupId }: ManagedUsersListProps) { {users.length === 0 && ( - + {t('no_members')} From f2030789d14655ff23c785e25758504b83217e63 Mon Sep 17 00:00:00 2001 From: CloudBuild Date: Sat, 5 Apr 2025 01:05:47 +0000 Subject: [PATCH 014/978] auto update translation GitOrigin-RevId: 6363d21d4903cb8f4cdcca28ec1b1baca39406b1 --- services/web/locales/cs.json | 2 -- services/web/locales/da.json | 6 ------ services/web/locales/de.json | 3 --- services/web/locales/es.json | 4 +--- services/web/locales/fi.json | 2 -- services/web/locales/fr.json | 2 -- services/web/locales/it.json | 2 -- services/web/locales/ja.json | 2 -- services/web/locales/ko.json | 2 -- services/web/locales/nl.json | 2 -- services/web/locales/no.json | 3 +-- services/web/locales/pl.json | 2 -- services/web/locales/pt.json | 3 +-- services/web/locales/ru.json | 2 -- services/web/locales/sv.json | 2 -- services/web/locales/tr.json | 2 -- services/web/locales/zh-CN.json | 6 ------ 17 files changed, 3 insertions(+), 44 deletions(-) diff --git a/services/web/locales/cs.json b/services/web/locales/cs.json index c15ed1b021..5878b0e62c 100644 --- a/services/web/locales/cs.json +++ b/services/web/locales/cs.json @@ -24,7 +24,6 @@ "blank_project": "Prázdný projekt", "blog": "Blog", "built_in": "Vestavěný", - "can_edit": "Může upravovat", "cancel": "Zrušit", "cant_find_email": "Je nám líto, ale tato emailová adresa není registrována.", "cant_find_page": "Je nám líto, ale nemůžeme najít stránku, kterou hledáte.", @@ -224,7 +223,6 @@ "publishing": "Publikuji", "pull_github_changes_into_sharelatex": "Vložit změny z GitHubu do __appName__u.", "push_sharelatex_changes_to_github": "Vložit změny z __appName__u do GitHubu", - "read_only": "Jen pro čtení", "recent_commits_in_github": "Poslední commity do GitHubu", "recompile": "Překompilovat", "reconnecting": "Obnovuji připojení", diff --git a/services/web/locales/da.json b/services/web/locales/da.json index 43e2d4d672..01be7ea5c8 100644 --- a/services/web/locales/da.json +++ b/services/web/locales/da.json @@ -196,13 +196,11 @@ "by_joining_labs": "Ved at tilmelde dig Labs accepterer du at modtage emails og opdateringer fra Overleaf — for eksempel for at bede om din feedback. Du accepterer også vores <0>servicevilkår og <1>privatlivspolitik.", "by_registering_you_agree_to_our_terms_of_service": "Ved at registrere dig accepterer du vores <0>servicevilkår og <1>privatlivspolitik.", "by_subscribing_you_agree_to_our_terms_of_service": "Ved at abonnere accepterer du vores <0>servicevilkår.", - "can_edit": "Kan redigere", "can_link_institution_email_acct_to_institution_acct": "Du kan nu kæde din __appName__-konto __email__ sammen med din institutionelle konto fra __institutionName__.", "can_link_institution_email_by_clicking": "Du kan kæde din __appName__-konto __email__ sammen med din __institutionName__-konto ved at klikke __clickText__.", "can_link_institution_email_to_login": "Du kan kæde din __appName__-konto __email__ sammen med din __institutionName__-konto, hvilket vil gøre det muligt for dig at logge ind i __appName__ igennem din institution, og vil genbekræfte din institutionelle e-mailadresse.", "can_link_your_institution_acct_2": "Du kan nu kæde din <0>__appName__-konto sammen med din institutionelle konto fra <0>__institutionName__.", "can_now_relink_dropbox": "Du kan nu <0>genoprette forbindelsen med din Dropbox-konto", - "can_view": "Kan læse", "cancel": "Annuller", "cancel_anytime": "Vi er sikre på at du vil elske __appName__, men hvis ikke kan du altid annulere. Vi giver dig pengene tilbage uden spørgsmål, hvis bare du fortæller os det inden for 30 dage.", "cancel_my_account": "Ophæv dit abonnement", @@ -435,7 +433,6 @@ "disable_single_sign_on": "Deaktiver single sign-on", "disable_sso": "Deaktiver SSO", "disable_stop_on_first_error": "Slå “Stop ved første fejl” fra", - "disabling": "Deaktiverer", "disconnected": "Forbindelsen blev afbrudt", "discount_of": "Rabat på __amount__", "discover_latex_templates_and_examples": "Opdag LaTeX skabeloner og eksempler til at hjælpe med alt fra at skrive en artikel til at bruge en specifik LaTeX pakke.", @@ -546,7 +543,6 @@ "enable_sso": "Aktiver SSO", "enable_stop_on_first_error_under_recompile_dropdown_menu": "Slå <0>“Stop ved første fejl” til under <1>Genkompilér menuen for at hjælpe dig med at finde og rette fejl med det samme.", "enabled": "Aktiveret", - "enabling": "Aktiverer", "end_of_document": "Slutningen af dokumentet", "enter_6_digit_code": "Indtast 6-cifret kode", "enter_any_size_including_units_or_valid_latex_command": "Indtast en størrelse (inklusiv enhed) eller en gyldig LaTeX kommando", @@ -1444,7 +1440,6 @@ "read_lines_from_path": "Læste linjer fra __path__", "read_more": "Læs mere", "read_more_about_free_compile_timeouts_servers": "Læs mere om ændringer i kompileringstidsgrænser og servere", - "read_only": "Skrivebeskyttet", "read_only_token": "Skrivebeskyttet nøgle", "read_write_token": "Læse- og skrivenøgle", "ready_to_join_x": "Du er klar til at slutte dig til __inviterName__", @@ -1980,7 +1975,6 @@ "transfer_management_resolve_following_issues": "For at overdrage styring af din konto, skal du løse de følgende problemer:", "transfer_this_users_projects": "Overdrag denne brugers projekter", "transfer_this_users_projects_description": "Denne brugers projekter bliver overdraget til en ny ejer.", - "transferring": "Overdrager", "trash": "Kassér", "trash_projects": "Kassér projekter", "trashed": "Kasséret", diff --git a/services/web/locales/de.json b/services/web/locales/de.json index 41347d966e..c59da99ad7 100644 --- a/services/web/locales/de.json +++ b/services/web/locales/de.json @@ -142,7 +142,6 @@ "buy_now_no_exclamation_mark": "Jetzt kaufen", "by": "von", "by_subscribing_you_agree_to_our_terms_of_service": "Mit der Anmeldung stimmst du unseren <0>Nutzungsbedingungen zu.", - "can_edit": "Darf bearbeiten", "can_link_institution_email_acct_to_institution_acct": "Du kannst jetzt dein __email__ __appName__-Konto mit deinem institutionellen __institutionName__-Konto verknüpfen.", "can_link_institution_email_by_clicking": "Du kannst dein __email__ __appName__-Konto mit deinem __institutionName__-Konto verknüpfen, indem du auf „__clickText__“ klickst.", "can_link_institution_email_to_login": "Du kannst dein __email__ __appName__-Konto mit deinem __institutionName__-Konto verknüpfen, wodurch du dich bei __appName__ über dein institutionelles Portal anmelden und deine institutionelle E-Mail-Adresse erneut bestätigen kannst.", @@ -392,7 +391,6 @@ "empty_zip_file": "ZIP enthält keine Datei", "en": "Englisch", "enable_managed_users": "Aktiviere Verwaltete Benutzer", - "enabling": "Wird aktiviert", "end_of_document": "Ende des Dokuments", "enter_image_url": "Bild-URL eingeben", "enter_your_email_address": "Gib deine E-Mail-Adresse ein", @@ -1001,7 +999,6 @@ "push_sharelatex_changes_to_github": "__appName__-Änderungen an GitHub senden", "raw_logs": "Raw Logs", "raw_logs_description": "Raw Logs vom LaTeX-Compiler", - "read_only": "Nur Lesen", "realtime_track_changes": "Änderungen in Echtzeit nachverfolgen", "reauthorize_github_account": "Autorisiere dein GitHub-Konto erneut", "recaptcha_conditions": "Diese Website ist durch reCAPTCHA geschützt und es gelten die <1>Datenschutzerklärung und die <2>Nutzungsbedingungen von Google.", diff --git a/services/web/locales/es.json b/services/web/locales/es.json index 181baa5eec..36391d3bac 100644 --- a/services/web/locales/es.json +++ b/services/web/locales/es.json @@ -181,12 +181,10 @@ "by_joining_labs": "Al unirte a Labs, aceptas recibir ocasionalmente correos electrónicos y actualizaciones de Overleaf, por ejemplo, para solicitar tu opinión. También acepta nuestras <0>condiciones del servicio y nuestro <1>aviso de privacidad.", "by_registering_you_agree_to_our_terms_of_service": "Al registrarse, acepta nuestras <0>condiciones del servicio y <1>notificación de privacidad.", "by_subscribing_you_agree_to_our_terms_of_service": "Al suscribirse, acepta nuestras <0>condiciones del servicio.", - "can_edit": "Puede editar", "can_link_institution_email_acct_to_institution_acct": "Ahora puedes vincular tu cuenta __appName__ __email__ a tu cuenta institucional __institutionName__.", "can_link_institution_email_by_clicking": "Puedes vincular tu cuenta __appName__ __email__ a tu cuenta institucional __institutionName__ haciendo click en __clickText__.", "can_link_institution_email_to_login": "Puedes vincular tu cuenta __appName__ __email__ a tu cuenta institucional __institutionName__, lo cual te permitirá entrar en __appName__ a través de tu institución y confirmará de nuevo tu cuenta de correo institucional.", "can_now_relink_dropbox": "Ya puedes <0>vincular de nuevo tu cuenta de Dropbox.", - "can_view": "Se puede ver", "cancel": "Cancelar", "cancel_my_account": "Cancelar mi suscripción", "cancel_my_subscription": "Cancelar mi suscripción", @@ -492,7 +490,6 @@ "publishing": "Publicando", "pull_github_changes_into_sharelatex": "Actualiza cambios de GitHub en __appName__", "push_sharelatex_changes_to_github": "Envía cambios de __appName__ a GitHub", - "read_only": "Solo leer", "recent_commits_in_github": "Commits recientes en GitHub", "recompile": "Recompilar", "reconnecting": "Volviendo a conectar", @@ -642,6 +639,7 @@ "upgrade_now": "Sube de categoría ahora", "upgrade_to_get_feature": "Sube de categoría para conseguir __feature__, además de:", "upload": "Subir", + "upload_file": "Subir archivo", "upload_project": "Subir proyecto", "upload_zipped_project": "Subir proyecto en Zip", "user_wants_you_to_see_project": "__username__ quiere que veas __projectname__", diff --git a/services/web/locales/fi.json b/services/web/locales/fi.json index ac43ea11c7..c7eda844b3 100644 --- a/services/web/locales/fi.json +++ b/services/web/locales/fi.json @@ -26,7 +26,6 @@ "blank_project": "Tyhjä projekti", "blog": "Blogi", "built_in": "Sisäänrakennettu", - "can_edit": "Voi muokata", "cancel": "Peru", "cant_find_email": "Tämä sähköposti ei ole rekisteröity, pahoittelut.", "cant_find_page": "Anteeksi, emme löydä hakemaasi sivua.", @@ -234,7 +233,6 @@ "publishing": "Julkaistaan", "pull_github_changes_into_sharelatex": "Tuo __appName__-muutokset GitHubista", "push_sharelatex_changes_to_github": "Vie __appName__-muutokset GitHubiin", - "read_only": "Read Only", "recent_commits_in_github": "Viimeiset muutokset GitHubissa", "recompile": "Käännä uudestaan", "reconnecting": "Yhdistetään uudelleen", diff --git a/services/web/locales/fr.json b/services/web/locales/fr.json index 1a1f3787d1..c7c840dac7 100644 --- a/services/web/locales/fr.json +++ b/services/web/locales/fr.json @@ -164,7 +164,6 @@ "buy_now_no_exclamation_mark": "Acheter maintenant", "by": "par", "by_subscribing_you_agree_to_our_terms_of_service": "En vous inscrivant, vous acceptez nos <0>conditions d’utilisation.", - "can_edit": "Édition autorisée", "can_link_institution_email_acct_to_institution_acct": "Vous pouvez maintenant lier votre compte __appName__ en __email__ à votre compte institutionnel __institutionName__.", "can_link_institution_email_by_clicking": "Vous pouvez lier votre compte __appName__ __email__ à votre compte __institutionName__ en cliquant __clickText__.", "can_link_institution_email_to_login": "Vous pouvez lier votre compte __appName__ __email__ à votre compte __institutionName__, ce qui vous permettra de vous connecter à __appName__ via le portail de votre établissement.", @@ -906,7 +905,6 @@ "push_sharelatex_changes_to_github": "Pousser les modifications __appName__ vers GitHub", "raw_logs": "Journaux bruts", "raw_logs_description": "Journaux bruts issus du compilateur LaTeX", - "read_only": "Lecture seule", "realtime_track_changes": "Suivi des modifications en temps réel", "reauthorize_github_account": "Autorisez votre compte GitHub à nouveau", "recent_commits_in_github": "Commits récents dans GitHub", diff --git a/services/web/locales/it.json b/services/web/locales/it.json index ae10109eab..88b0fbce65 100644 --- a/services/web/locales/it.json +++ b/services/web/locales/it.json @@ -36,7 +36,6 @@ "blank_project": "Progetto Vuoto", "blog": "Blog", "built_in": "Built-In", - "can_edit": "Può Modificare", "cancel": "Annulla", "cancel_my_account": "Interrrompi il mio abbonamento", "cancel_your_subscription": "Interrrompi il tuo abbonamento", @@ -262,7 +261,6 @@ "publishing": "Pubblicazione", "pull_github_changes_into_sharelatex": "Aggiorna da modifiche in GitHub verso __appName__", "push_sharelatex_changes_to_github": "Invia le modifiche __appName__ a GitHub", - "read_only": "Sola Lettura", "recent_commits_in_github": "Commit recenti in GitHub", "recompile": "Ricompila", "reconnecting": "Riconnessione", diff --git a/services/web/locales/ja.json b/services/web/locales/ja.json index d8282b9087..7b6810a145 100644 --- a/services/web/locales/ja.json +++ b/services/web/locales/ja.json @@ -49,7 +49,6 @@ "blank_project": "空のプロジェクト", "blog": "ブログ", "built_in": "組み込み", - "can_edit": "編集可能", "cancel": "取消", "cancel_my_account": "購読をキャンセル", "cancel_personal_subscription_first": "個人購読をすでに申し込んでいます。これをキャンセルしてグループライセンスに参加しますか?", @@ -339,7 +338,6 @@ "publishing": "公開中", "pull_github_changes_into_sharelatex": "GitHubの変更を __appName__ に引き込む", "push_sharelatex_changes_to_github": "__appName__ の変更をGitHubに押し込む", - "read_only": "読み込み専用", "recent_commits_in_github": "GitHubの最新コミット", "recompile": "リコンパイル", "recompile_pdf": "PDFを再コンパイル", diff --git a/services/web/locales/ko.json b/services/web/locales/ko.json index db9b4718ac..658d005710 100644 --- a/services/web/locales/ko.json +++ b/services/web/locales/ko.json @@ -63,7 +63,6 @@ "blank_project": "빈 프로젝트", "blog": "블로그", "built_in": "빌트인", - "can_edit": "편집가능", "cancel": "취소", "cancel_my_account": "구독 취소하기", "cancel_personal_subscription_first": "이미 개인 구독을 하고 있습니다. 그룹 라이센스를 사용하기 전에 개인 구독을 취소하시겠습니까?", @@ -381,7 +380,6 @@ "publishing": "공개중", "pull_github_changes_into_sharelatex": "GitHub 변경사항들을 __appName__로 당겨주세요", "push_sharelatex_changes_to_github": "__appName__ 변경사항을 GitHub으로 밀어주세요", - "read_only": "읽기만 허용", "realtime_track_changes": "실시간 변경 내용 추적", "reauthorize_github_account": "GitHub 계정 재확인", "recent_commits_in_github": "GitHub의 최근 커밋", diff --git a/services/web/locales/nl.json b/services/web/locales/nl.json index 67f549dfb1..e19b474e34 100644 --- a/services/web/locales/nl.json +++ b/services/web/locales/nl.json @@ -65,7 +65,6 @@ "blank_project": "Blanco Project", "blog": "Blog", "built_in": "Ingebouwd", - "can_edit": "Kan Bewerken", "cancel": "Annuleren", "cancel_my_account": "Annuleer mijn abonnement", "cancel_personal_subscription_first": "U heeft al een persoonlijke inschrijving, wilt u dat wij deze annuleren voordat u deel neemt aan de groepslicentie?", @@ -389,7 +388,6 @@ "publishing": "Aan het publiceren", "pull_github_changes_into_sharelatex": "Wijzigingen op GitHub naar __appName__ halen", "push_sharelatex_changes_to_github": "Wijzigingen op __appName__ naar GitHub verplaatsen", - "read_only": "Alleen Lezen", "recent_commits_in_github": "Recente toevoegingen in GitHub", "recompile": "Hercompileren", "recompile_pdf": "Hercompileer de PDF", diff --git a/services/web/locales/no.json b/services/web/locales/no.json index 7b0c5b3a36..306970b1b3 100644 --- a/services/web/locales/no.json +++ b/services/web/locales/no.json @@ -38,7 +38,6 @@ "blank_project": "Tomt prosjekt", "blog": "Blogg", "built_in": "Innebygget", - "can_edit": "Kan redigere", "cancel": "Avbryt", "cancel_personal_subscription_first": "Du har allerede et personlig abonnement, vil du at vi skal kansellere det før du blir medlem av gruppelisensen?", "cant_find_email": "Epostadressen er ikke registrert, beklager.", @@ -270,7 +269,6 @@ "publishing": "Publiserer", "pull_github_changes_into_sharelatex": "Pull forandringer i GitHub til __appName__", "push_sharelatex_changes_to_github": "Push forandringer i __appName__ til GitHub", - "read_only": "Skrivebeskyttet", "recent_commits_in_github": "Nylige commits i GitHub", "recompile": "Rekompiler", "reconnecting": "Kobler til", @@ -379,6 +377,7 @@ "upgrade_now": "Oppgrader Nå", "upgrade_to_get_feature": "Oppgrader for å få __feature__, pluss:", "upload": "Last opp", + "upload_file": "Last opp fil", "upload_project": "Last opp prosjekt", "upload_zipped_project": "Last opp zippet prosjekt", "user_wants_you_to_see_project": "__username__ ønsker at du skal se __projectname__", diff --git a/services/web/locales/pl.json b/services/web/locales/pl.json index 3a81ec66ee..990995191e 100644 --- a/services/web/locales/pl.json +++ b/services/web/locales/pl.json @@ -17,7 +17,6 @@ "blank_project": "Pusty projekt", "blog": "Blog", "built_in": "Wbudowana", - "can_edit": "Może edytować", "cancel": "Anuluj", "cant_find_email": "Przykro nam, ale ten adres email nie jest zarejestrowany.", "cant_find_page": "Przykro nam, ale nie możemy znaleźć strony której szukasz.", @@ -152,7 +151,6 @@ "public": "Publiczny", "publish": "Publikuj", "publish_as_template": "Publikuj jako szablon", - "read_only": "tylko odczyt", "recompile": "Przekompiluj", "reconnecting": "Ponowne łączenie", "reconnecting_in_x_secs": "Próba połączenia za __seconds__ s", diff --git a/services/web/locales/pt.json b/services/web/locales/pt.json index cd2a5f078a..55272084c5 100644 --- a/services/web/locales/pt.json +++ b/services/web/locales/pt.json @@ -77,7 +77,6 @@ "brl_discount_offer_plans_page_banner": "__flag__ Aplicamos um desconto de 50% aos planos premium nesta página para nossos usuários no Brasil. Confira os novos preços mais baixos.", "built_in": "Embutido", "by": "por", - "can_edit": "Pode Editar", "cancel": "Cancelar", "cancel_my_account": "Cancelar minha inscrição", "cancel_personal_subscription_first": "Você já tem uma inscrição pessoal, você gostaria que cancelássemos a primeira antes de se juntar à licença de grupo?", @@ -464,7 +463,6 @@ "publishing": "Publicando", "pull_github_changes_into_sharelatex": "Puxar mudanças do GitHub no __appName__", "push_sharelatex_changes_to_github": "Empurrar mudanças do __appName__ no GitHub", - "read_only": "Somente Ler", "realtime_track_changes": "Acompanhe alterações em tempo real.", "reauthorize_github_account": "Reautorize sua conta GitHub", "recent_commits_in_github": "Commits recentes no GitHub", @@ -656,6 +654,7 @@ "upgrade_to_get_feature": "Aprimore para ter __feature__, mais:", "upgrade_to_track_changes": "Atualizar para acompanhar alterações", "upload": "Carregar", + "upload_file": "Atualizar Arquivo", "upload_project": "Carregar Projeto", "upload_zipped_project": "Subir Projeto Zipado", "user_already_added": "Usuário já foi adicionado", diff --git a/services/web/locales/ru.json b/services/web/locales/ru.json index 10f83e52e1..682a549783 100644 --- a/services/web/locales/ru.json +++ b/services/web/locales/ru.json @@ -60,7 +60,6 @@ "blank_project": "Новый проект", "blog": "Блог", "built_in": "встроенный", - "can_edit": "Могут править", "cancel": "Отмена", "cancel_my_account": "Отменить подписку", "cancel_personal_subscription_first": "У Вас уже имеется личная подписка. Хотите ли вы её отменить перед переходом на групповую лицензию?", @@ -327,7 +326,6 @@ "publishing": "Публикация", "pull_github_changes_into_sharelatex": "Скачать изменения с GitHub в __appName__", "push_sharelatex_changes_to_github": "Загрузить изменения из __appName__ на GitHub", - "read_only": "Только чтение", "recent_commits_in_github": "Последние коммиты на GitHub", "recompile": "Компилировать", "recompile_pdf": "Перекомпилировать PDF", diff --git a/services/web/locales/sv.json b/services/web/locales/sv.json index a3cc87f417..7cf4096190 100644 --- a/services/web/locales/sv.json +++ b/services/web/locales/sv.json @@ -96,7 +96,6 @@ "blog": "Blogg", "built_in": "Inbyggd", "by": "av", - "can_edit": "Kan redigera", "can_link_institution_email_acct_to_institution_acct": "Du kan nu länka ditt __email__ __appName__-konto till ditt __institutionName__ institutionella konto.", "cancel": "Avbryt", "cancel_my_account": "Avsluta min prenumeration", @@ -658,7 +657,6 @@ "push_sharelatex_changes_to_github": "Tryck __appName__ ändringar till GitHub", "raw_logs": "Ursprungliga loggar", "raw_logs_description": "Ursprungliga loggar från LaTeX-kompilatorn", - "read_only": "Endast läs", "realtime_track_changes": "Realtidsspåra ändringar", "reauthorize_github_account": "Återauktorisera ditt GitHub konto", "recent_commits_in_github": "Senaste commits på GitHub", diff --git a/services/web/locales/tr.json b/services/web/locales/tr.json index a1277f1f13..62c88c1832 100644 --- a/services/web/locales/tr.json +++ b/services/web/locales/tr.json @@ -40,7 +40,6 @@ "blank_project": "Boş Proje", "blog": "Blog", "built_in": "Yerleşik", - "can_edit": "Değişiklik Yapabilir", "cancel": "İptal", "cancel_my_account": "Hesabımı iptal et", "cancel_your_subscription": "Hesabınızı iptal edin", @@ -263,7 +262,6 @@ "publishing": "Yayınlanıyor", "pull_github_changes_into_sharelatex": "GitHub’daki değişiklikleri __appName__’e aktar", "push_sharelatex_changes_to_github": "__appName__’deki değişiklikleri GitHub’a aktar", - "read_only": "Yalnızca Görüntüleyebilir", "recent_commits_in_github": "GitHub’da yapılan güncel işlemler", "recompile": "Tekrar Derle", "reconnecting": "Yeniden bağlanıyor", diff --git a/services/web/locales/zh-CN.json b/services/web/locales/zh-CN.json index 7c2c885e9b..d37233d66f 100644 --- a/services/web/locales/zh-CN.json +++ b/services/web/locales/zh-CN.json @@ -199,13 +199,11 @@ "by_joining_labs": "加入实验室即表示您同意接收 Overleaf 不定期发送的电子邮件和更新信息(例如,征求您的反馈)。您还同意我们的<0>服务条款和<1>隐私声明。", "by_registering_you_agree_to_our_terms_of_service": "注册即表示您同意我们的 <0>服务条款 和 <1>隐私条款。", "by_subscribing_you_agree_to_our_terms_of_service": "订阅即表示您同意我们的<0>服务条款。", - "can_edit": "可以编辑", "can_link_institution_email_acct_to_institution_acct": "您现在可以将您的 __appName__ 账户 __email__ 与您的 __institutionName__ 机构账户关联。", "can_link_institution_email_by_clicking": "您可以通过单击 __clickText__ 将您的 __email__ __appName__ 账户链接到您的 __institutionName__ 帐户。", "can_link_institution_email_to_login": "您可以将您的 __email__ __appName__ 账户链接到你的 __institutionName__ 账户,这将允许您通过机构门户登录到__appName__ 。", "can_link_your_institution_acct_2": "您可以现在 <0>链接 您的 <0>__appName__ 账户到您的<0>__institutionName__ 机构账户。", "can_now_relink_dropbox": "您现在可以<0>重新关联您的 Dropbox 帐户。", - "can_view": "可以查看", "cancel": "取消", "cancel_anytime": "我们相信您会喜欢 __appName__,但如果不喜欢,您可以随时取消。如果您在30天内通知我们,我们无理由退款。", "cancel_my_account": "取消我的订购", @@ -437,7 +435,6 @@ "disable_single_sign_on": "禁用 单点登录(SSO)", "disable_sso": "关闭 SSO", "disable_stop_on_first_error": "禁用 “出现第一个错误时停止”", - "disabling": "禁用", "disconnected": "连接已断开", "discount_of": "__amount__的折扣", "dismiss_error_popup": "忽略第一个错误提示", @@ -547,7 +544,6 @@ "enable_sso": "开启 SSO", "enable_stop_on_first_error_under_recompile_dropdown_menu": "在<1>重新编译下拉菜单下启用<0>“第一次出现错误时停止”,以帮助您立即查找并修复错误。", "enabled": "已启用", - "enabling": "开启", "end_of_document": "文档末尾", "enter_6_digit_code": "输入6位数验证码", "enter_any_size_including_units_or_valid_latex_command": "输入任意大小(包括单位)或有效的 LaTeX 命令", @@ -1455,7 +1451,6 @@ "read_lines_from_path": "从 __path__ 读取行", "read_more": "阅读更多", "read_more_about_free_compile_timeouts_servers": "阅读有关免费计划编译超时和服务器更改的更多信息", - "read_only": "只读", "read_only_token": "只读令牌", "read_write_token": "可读写令牌", "ready_to_join_x": "您已加入 __inviterName__", @@ -2016,7 +2011,6 @@ "transfer_management_resolve_following_issues": "如需转移账户管理权,您需要解决以下问题:", "transfer_this_users_projects": "转移该用户的项目", "transfer_this_users_projects_description": "该用户的项目将转移给新所有者。", - "transferring": "正在转移中", "trash": "回收站", "trash_projects": "已删除项目", "trashed": "被删除", From 670ed44963b8136904b45043a0284e00af9687ef Mon Sep 17 00:00:00 2001 From: CloudBuild Date: Sun, 6 Apr 2025 01:07:27 +0000 Subject: [PATCH 015/978] auto update translation GitOrigin-RevId: 2a2199f74cf1e0c2506ba336624cd858e2f24d3e --- services/web/locales/cs.json | 1 + services/web/locales/da.json | 1 + services/web/locales/de.json | 1 + services/web/locales/fi.json | 1 + services/web/locales/fr.json | 1 + services/web/locales/it.json | 1 + services/web/locales/ja.json | 1 + services/web/locales/ko.json | 1 + services/web/locales/nl.json | 1 + services/web/locales/pl.json | 1 + services/web/locales/ru.json | 1 + services/web/locales/sv.json | 1 + services/web/locales/tr.json | 1 + services/web/locales/zh-CN.json | 1 + 14 files changed, 14 insertions(+) diff --git a/services/web/locales/cs.json b/services/web/locales/cs.json index 5878b0e62c..1b82889697 100644 --- a/services/web/locales/cs.json +++ b/services/web/locales/cs.json @@ -324,6 +324,7 @@ "updating_site": "Upravuji stránku", "upgrade": "Upgrade", "upload": "Nahrát", + "upload_file": "Nahrát soubor", "upload_project": "Nahrát projekt", "upload_zipped_project": "Nahrát zazipovaný projekt", "user_wants_you_to_see_project": "Uživatel __username__ by se rád přidal k projektu __projectname__", diff --git a/services/web/locales/da.json b/services/web/locales/da.json index 01be7ea5c8..f00e21c8a9 100644 --- a/services/web/locales/da.json +++ b/services/web/locales/da.json @@ -2055,6 +2055,7 @@ "upgrade_to_track_changes": "Opgrader til “Følg ændringer”", "upload": "Upload", "upload_failed": "Overførsel mislykkedes", + "upload_file": "Overfør Fil", "upload_from_computer": "Upload fra computer", "upload_project": "Overfør projekt", "upload_zipped_project": "Upload komprimeret projekt", diff --git a/services/web/locales/de.json b/services/web/locales/de.json index c59da99ad7..de9b463ce3 100644 --- a/services/web/locales/de.json +++ b/services/web/locales/de.json @@ -1319,6 +1319,7 @@ "upgrade_to_track_changes": "Upgrade, um Änderungen verfolgen zu können", "upload": "Hochladen", "upload_failed": "Hochladen fehlgeschlagen", + "upload_file": "Datei hochladen", "upload_project": "Projekt hochladen", "upload_zipped_project": "Projekt als ZIP hochladen", "url_to_fetch_the_file_from": "URL, von der die Datei abgerufen werden soll", diff --git a/services/web/locales/fi.json b/services/web/locales/fi.json index c7eda844b3..15c0c344b0 100644 --- a/services/web/locales/fi.json +++ b/services/web/locales/fi.json @@ -329,6 +329,7 @@ "upgrade": "Päivitä", "upgrade_now": "Päivitä Nyt", "upload": "Siirrä", + "upload_file": "Vie tiedosto", "upload_project": "Siirrä projekti palvelimelle", "upload_zipped_project": "Vie pakattu projekti", "user_wants_you_to_see_project": "__username__ haluaisi katsoa projektiasi __projectname__", diff --git a/services/web/locales/fr.json b/services/web/locales/fr.json index c7c840dac7..4830945afe 100644 --- a/services/web/locales/fr.json +++ b/services/web/locales/fr.json @@ -1192,6 +1192,7 @@ "upgrade_to_track_changes": "Mettez à niveau pour suivre les modifications", "upload": "Importer", "upload_failed": "Échec du téléversement", + "upload_file": "Importer un fichier", "upload_project": "Importer un projet", "upload_zipped_project": "Importer un projet zippé", "url_to_fetch_the_file_from": "Récupérer le fichier depuis l’URL", diff --git a/services/web/locales/it.json b/services/web/locales/it.json index 88b0fbce65..2c5c880ad7 100644 --- a/services/web/locales/it.json +++ b/services/web/locales/it.json @@ -366,6 +366,7 @@ "upgrade_now": "Effettua l’Upgrade", "upgrade_to_get_feature": "Esegui l’upgrade per avere __feature__, oltre a:", "upload": "Carica", + "upload_file": "Carica File", "upload_project": "Carica Progetto", "upload_zipped_project": "Carica Progetto Zip", "user_wants_you_to_see_project": "__username__ vorrebbe che tu vedessi __projectname__", diff --git a/services/web/locales/ja.json b/services/web/locales/ja.json index 7b6810a145..a6ddefe117 100644 --- a/services/web/locales/ja.json +++ b/services/web/locales/ja.json @@ -469,6 +469,7 @@ "upgrade_now": "今すぐアップグレード", "upgrade_to_get_feature": "__feature__のアップグレードを取得、プラス:", "upload": "アップロード", + "upload_file": "ファイルのアップロード", "upload_project": "プロジェクトのアップロード", "upload_zipped_project": "ZIPプロジェクトのアップロード", "user_wants_you_to_see_project": "__username__ が __projectname__ への参加を求めています。", diff --git a/services/web/locales/ko.json b/services/web/locales/ko.json index 658d005710..4f4e26af03 100644 --- a/services/web/locales/ko.json +++ b/services/web/locales/ko.json @@ -549,6 +549,7 @@ "upgrade_to_get_feature": "__feature__와 다음 기능 사용을 위해 업그레이드:", "upgrade_to_track_changes": "변경 내용 추적을 위해 업그레이드", "upload": "업로드", + "upload_file": "파일 업로드", "upload_project": "프로젝트 업로드", "upload_zipped_project": "압축된 프로젝트 업로드", "user_wants_you_to_see_project": "__username__ 님이 __projectname__에 참여하길 원합니다", diff --git a/services/web/locales/nl.json b/services/web/locales/nl.json index e19b474e34..39ab174b4d 100644 --- a/services/web/locales/nl.json +++ b/services/web/locales/nl.json @@ -549,6 +549,7 @@ "upgrade_to_get_feature": "Upgrade om __feature__ te krijgen, plus:", "upgrade_to_track_changes": "Upgrade naar Wijzigingen Bijhouden", "upload": "Uploaden", + "upload_file": "Bestand Uploaden", "upload_project": "Project Uploaden", "upload_zipped_project": "Gezipt Project Uploaden", "user_wants_you_to_see_project": "__username__ wil dat u deelneemt aan __projectname__", diff --git a/services/web/locales/pl.json b/services/web/locales/pl.json index 990995191e..3f4b70c4ef 100644 --- a/services/web/locales/pl.json +++ b/services/web/locales/pl.json @@ -216,6 +216,7 @@ "update_account_info": "Zaktualizuj informacje o koncie", "update_dropbox_settings": "Zaktualizuj ustawienia Dropbox", "upload": "Wyślij plik", + "upload_file": "Wyślij plik", "upload_project": "Wyślij projekt", "upload_zipped_project": "Wyślij projekt w pliku ZIP", "user_wants_you_to_see_project": "__username__ chce, żebyś zobaczył __projectname__", diff --git a/services/web/locales/ru.json b/services/web/locales/ru.json index 682a549783..012c24d73b 100644 --- a/services/web/locales/ru.json +++ b/services/web/locales/ru.json @@ -449,6 +449,7 @@ "upgrade": "Сменить тариф", "upgrade_now": "Сменить тариф", "upload": "Загрузить", + "upload_file": "Загрузить файл", "upload_project": "Загрузить проект", "upload_zipped_project": "Загрузить архив проекта (*.zip)", "user_wants_you_to_see_project": "__username__ приглашает вас к просмотру проекта __projectname__", diff --git a/services/web/locales/sv.json b/services/web/locales/sv.json index 7cf4096190..70e35fdc8f 100644 --- a/services/web/locales/sv.json +++ b/services/web/locales/sv.json @@ -917,6 +917,7 @@ "upgrade_to_track_changes": "Uppgradera för att spåra ändringar", "upload": "Ladda upp", "upload_failed": "Uppladdning misslyckades", + "upload_file": "Ladda upp fil", "upload_project": "Ladda upp projekt", "upload_zipped_project": "Ladda upp zippat projekt", "user_already_added": "Användare redan tillagd", diff --git a/services/web/locales/tr.json b/services/web/locales/tr.json index 62c88c1832..163264953a 100644 --- a/services/web/locales/tr.json +++ b/services/web/locales/tr.json @@ -362,6 +362,7 @@ "upgrade": "Yükselt", "upgrade_now": "Şimdi Yükselt", "upload": "Yükle", + "upload_file": "Dosya Yükle", "upload_project": "Proje Yükleyin", "upload_zipped_project": "Sıkıştırılmış Proje Yükle", "user_wants_you_to_see_project": "__username__ adlı kullanıcı __projectname__ isimli projeyi görmenizi istiyor", diff --git a/services/web/locales/zh-CN.json b/services/web/locales/zh-CN.json index d37233d66f..e477cc66bd 100644 --- a/services/web/locales/zh-CN.json +++ b/services/web/locales/zh-CN.json @@ -2093,6 +2093,7 @@ "upgrade_to_track_changes": "升级以记录文档修改历史", "upload": "上传", "upload_failed": "上传失败", + "upload_file": "上传文件", "upload_from_computer": "从电脑本地上传", "upload_project": "上传项目", "upload_zipped_project": "上传项目的压缩包", From 7eecfe9e27a5bf088c6399da33d678c562682852 Mon Sep 17 00:00:00 2001 From: Antoine Clausse Date: Mon, 7 Apr 2025 11:08:29 +0200 Subject: [PATCH 016/978] [web] Add another partial fix for `fix_malformed_filetree`: use `_id` instead of `path` to locate data (#24101) * Fix `fix_malformed_filetree`'s `fixName` * Fix findUniqueName with missing names in siblings * Add test showcasing another bug: shifted arrays in filetree folder * Update `removeNulls` to use `_id` * Update services/web/app/src/Features/Project/ProjectLocator.js Co-authored-by: Jakob Ackermann * Add FIXME about file names uniqueness * Rename `obj` to `project` --------- Co-authored-by: Jakob Ackermann GitOrigin-RevId: 3ed795ae0621800603395f7b50626ac89c39199d --- .../src/Features/Project/ProjectLocator.js | 23 +++ .../web/scripts/fix_malformed_filetree.mjs | 51 ++++-- .../src/MalformedFiletreesTests.mjs | 148 ++++++++++++++++-- 3 files changed, 193 insertions(+), 29 deletions(-) diff --git a/services/web/app/src/Features/Project/ProjectLocator.js b/services/web/app/src/Features/Project/ProjectLocator.js index c78dac1dbf..2feaa0cebf 100644 --- a/services/web/app/src/Features/Project/ProjectLocator.js +++ b/services/web/app/src/Features/Project/ProjectLocator.js @@ -7,6 +7,28 @@ const Errors = require('../Errors/Errors') const { promisifyMultiResult } = require('@overleaf/promise-utils') const { iterablePaths } = require('./IterablePath') +/** + * @param project + * @param predicate + * @returns {{path: string, value: *}} + */ +function findDeep(project, predicate) { + function find(value, path) { + if (predicate(value)) { + return { value, path: path.join('.') } + } + if (typeof value === 'object' && value !== null) { + for (const [childKey, childVal] of Object.entries(value)) { + const found = find(childVal, [...path, childKey]) + if (found) { + return found + } + } + } + } + return find(project.rootFolder, ['rootFolder']) +} + function findElement(options, _callback) { // The search algorithm below potentially invokes the callback multiple // times. @@ -308,6 +330,7 @@ module.exports = { findElementByPath, findRootDoc, findElementByMongoPath, + findDeep, promises: { findElement: promisifyMultiResult(findElement, [ 'element', diff --git a/services/web/scripts/fix_malformed_filetree.mjs b/services/web/scripts/fix_malformed_filetree.mjs index 491da4762d..2c2971ecfd 100644 --- a/services/web/scripts/fix_malformed_filetree.mjs +++ b/services/web/scripts/fix_malformed_filetree.mjs @@ -8,7 +8,9 @@ */ import mongodb from 'mongodb-legacy' import { db } from '../app/src/infrastructure/mongodb.js' -import ProjectLocator from '../app/src/Features/Project/ProjectLocator.js' +import ProjectLocator, { + findDeep, +} from '../app/src/Features/Project/ProjectLocator.js' import minimist from 'minimist' import readline from 'node:readline' import fs from 'node:fs' @@ -72,7 +74,7 @@ async function processBadPath(projectId, mongoPath, _id) { if (isRootFolder(mongoPath)) { modifiedCount = await fixRootFolder(projectId) } else if (isArrayElement(mongoPath)) { - modifiedCount = await removeNulls(projectId, parentPath(mongoPath)) + modifiedCount = await removeNulls(projectId, _id) } else if (isArray(mongoPath)) { modifiedCount = await fixArray(projectId, mongoPath) } else if (isFolderId(mongoPath)) { @@ -83,7 +85,7 @@ async function processBadPath(projectId, mongoPath, _id) { parentPath(parentPath(mongoPath)) ) } else if (isName(mongoPath)) { - modifiedCount = await fixName(projectId, mongoPath) + modifiedCount = await fixName(projectId, _id) } else if (isHash(mongoPath)) { console.error(`Missing file hash: ${projectId}/${_id} (${mongoPath})`) console.error('SaaS: likely needs filestore restore') @@ -164,10 +166,26 @@ async function fixRootFolder(projectId) { /** * Remove all nulls from the given docs/files/folders array */ -async function removeNulls(projectId, path) { +async function removeNulls(projectId, _id) { + if (!_id) { + throw new Error('missing _id') + } + const project = await db.projects.findOne( + { _id: new ObjectId(projectId) }, + { projection: { rootFolder: 1 } } + ) + const foundResult = findDeep(project, obj => obj?._id?.toString() === _id) + if (!foundResult) return + const { path } = foundResult const result = await db.projects.updateOne( - { _id: new ObjectId(projectId), [path]: { $type: 'array' } }, - { $pull: { [path]: null } } + { _id: new ObjectId(projectId) }, + { + $pull: { + [`${path}.folders`]: null, + [`${path}.docs`]: null, + [`${path}.fileRefs`]: null, + }, + } ) return result.modifiedCount } @@ -208,19 +226,26 @@ async function removeElementsWithoutIds(projectId, path) { /** * Give a name to a file/doc/folder that doesn't have one */ -async function fixName(projectId, path) { +async function fixName(projectId, _id) { + if (!_id) { + throw new Error('missing _id') + } const project = await db.projects.findOne( { _id: new ObjectId(projectId) }, { projection: { rootFolder: 1 } } ) - const arrayPath = parentPath(parentPath(path)) - const array = ProjectLocator.findElementByMongoPath(project, arrayPath) - const existingNames = new Set(array.map(x => x.name)) + const foundResult = findDeep(project, obj => obj?._id?.toString() === _id) + if (!foundResult) return + const { path } = foundResult + const array = ProjectLocator.findElementByMongoPath(project, parentPath(path)) const name = - path === 'rootFolder.0.name' ? 'rootFolder' : findUniqueName(existingNames) + path === 'rootFolder.0' + ? 'rootFolder' + : findUniqueName(new Set(array.map(x => x?.name))) + const pathToName = `${path}.name` const result = await db.projects.updateOne( - { _id: new ObjectId(projectId), [path]: { $in: [null, ''] } }, - { $set: { [path]: name } } + { _id: new ObjectId(projectId), [pathToName]: { $in: [null, ''] } }, + { $set: { [pathToName]: name } } ) return result.modifiedCount } diff --git a/services/web/test/acceptance/src/MalformedFiletreesTests.mjs b/services/web/test/acceptance/src/MalformedFiletreesTests.mjs index d1dd8ea937..744781f8c8 100644 --- a/services/web/test/acceptance/src/MalformedFiletreesTests.mjs +++ b/services/web/test/acceptance/src/MalformedFiletreesTests.mjs @@ -222,7 +222,7 @@ const testCases = [ msg: 'bad file-tree path', })), expectFixStdout: - '"gracefulShutdownInitiated":false,"processedLines":4,"success":3,"alreadyProcessed":1,"hash":0,"failed":0,"unmatched":0', + '"gracefulShutdownInitiated":false,"processedLines":4,"success":1,"alreadyProcessed":3,"hash":0,"failed":0,"unmatched":0', expectProject: updatedProject => { expect(updatedProject).to.deep.equal({ _id: projectId, @@ -495,7 +495,109 @@ const testCases = [ msg: 'bad file-tree path', })), expectFixStdout: - '"gracefulShutdownInitiated":false,"processedLines":9,"success":6,"alreadyProcessed":3,"hash":0,"failed":0,"unmatched":0', + '"gracefulShutdownInitiated":false,"processedLines":9,"success":4,"alreadyProcessed":5,"hash":0,"failed":0,"unmatched":0', + expectProject: updatedProject => { + expect(updatedProject).to.deep.equal({ + _id: projectId, + rootFolder: [ + { + _id: rootFolderId, + name: 'rootFolder', + folders: [{ ...wellFormedFolder('f02'), name: 'untitled' }], + docs: [{ ...wellFormedDoc('d02'), name: 'untitled' }], + fileRefs: [{ ...wellFormedFileRef('fr02'), name: 'untitled' }], + }, + ], + }) + }, + }, + { + name: 'bug: shifted arrays in filetree folder', + project: { + _id: projectId, + rootFolder: [ + { + _id: rootFolderId, + name: 'rootFolder', + folders: [ + null, + null, + { + ...wellFormedFolder('f02'), + name: 'folder 1', + folders: [null, null, { ...wellFormedFolder('f022') }], + docs: [null, null, { ...wellFormedDoc('d022'), name: null }], + fileRefs: [ + null, + null, + { ...wellFormedFileRef('fr022'), name: null }, + ], + }, + ], + + docs: [], + fileRefs: [], + }, + ], + }, + expectFind: [ + { + _id: rootFolderId.toString(), + path: 'rootFolder.0.folders.0', + reason: 'bad folder', + }, + { + _id: rootFolderId.toString(), + path: 'rootFolder.0.folders.1', + reason: 'bad folder', + }, + { + _id: strId('f02'), + path: 'rootFolder.0.folders.2.folders.0', + reason: 'bad folder', + }, + { + _id: strId('f02'), + path: 'rootFolder.0.folders.2.folders.1', + reason: 'bad folder', + }, + { + _id: strId('f02'), + path: 'rootFolder.0.folders.2.docs.0', + reason: 'bad doc', + }, + { + _id: strId('f02'), + path: 'rootFolder.0.folders.2.docs.1', + reason: 'bad doc', + }, + { + _id: strId('d022'), + path: 'rootFolder.0.folders.2.docs.2.name', + reason: 'bad doc name', + }, + { + _id: strId('f02'), + path: 'rootFolder.0.folders.2.fileRefs.0', + reason: 'bad file', + }, + { + _id: strId('f02'), + path: 'rootFolder.0.folders.2.fileRefs.1', + reason: 'bad file', + }, + { + _id: strId('fr022'), + path: 'rootFolder.0.folders.2.fileRefs.2.name', + reason: 'bad file name', + }, + ].map(entry => ({ + ...entry, + projectId: projectId.toString(), + msg: 'bad file-tree path', + })), + expectFixStdout: + '"gracefulShutdownInitiated":false,"processedLines":10,"success":4,"alreadyProcessed":6,"hash":0,"failed":0,"unmatched":0', expectProject: updatedProject => { expect(updatedProject).to.deep.equal({ _id: projectId, @@ -503,22 +605,36 @@ const testCases = [ { _id: rootFolderId, name: 'rootFolder', - // FIXME: The 3 arrays should only contain 1 item: the well-formed item with the name 'untitled'. folders: [ - { ...wellFormedFolder('f02'), name: null }, - null, - { name: 'untitled' }, - ], - docs: [ - { ...wellFormedDoc('d02'), name: null }, - null, - { name: 'untitled' }, - ], - fileRefs: [ - { ...wellFormedFileRef('fr02'), name: null }, - null, - { name: 'untitled' }, + { + ...wellFormedFolder('f02'), + name: 'folder 1', + docs: [ + { + ...wellFormedDoc('d022'), + name: 'untitled', + }, + ], + fileRefs: [ + { + ...wellFormedFileRef('fr022'), + // FIXME: Make the names unique across different file types + name: 'untitled', + }, + ], + folders: [ + { + ...wellFormedFolder('f022'), + name: 'f022', + folders: [], + docs: [], + fileRefs: [], + }, + ], + }, ], + docs: [], + fileRefs: [], }, ], }) From f08532dfb085b3eeedf0c5cffef791c4f86f4f8d Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 7 Apr 2025 11:05:19 +0100 Subject: [PATCH 017/978] Merge pull request #24637 from overleaf/bg-history-backup-uninitialised-projects backup uninitialised projects GitOrigin-RevId: 9310ef9f803decffbd674024a1ffd33d1960a2c4 --- .../storage/lib/backup_store/index.js | 24 +++++++++++++++++++ .../storage/scripts/backup_scheduler.mjs | 19 ++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/services/history-v1/storage/lib/backup_store/index.js b/services/history-v1/storage/lib/backup_store/index.js index 1253afea06..da7944786a 100644 --- a/services/history-v1/storage/lib/backup_store/index.js +++ b/services/history-v1/storage/lib/backup_store/index.js @@ -27,6 +27,29 @@ function listPendingBackups(timeIntervalMs = 0, limit = null) { return cursor } +// List projects that have never been backed up and are older than the specified interval +function listUninitializedBackups(timeIntervalMs = 0, limit = null) { + const cutoffTimeInSeconds = (Date.now() - timeIntervalMs) / 1000 + const options = { + projection: { _id: 1 }, + sort: { _id: 1 }, + } + // Apply limit if provided + if (limit) { + options.limit = limit + } + const cursor = projects.find( + { + 'overleaf.backup.lastBackedUpVersion': null, + _id: { + $lt: ObjectId.createFromTime(cutoffTimeInSeconds), + }, + }, + options + ) + return cursor +} + // Retrieve the history ID for a given project without giving direct access to the // projects collection. @@ -183,6 +206,7 @@ module.exports = { updateCurrentMetadataIfNotSet, updatePendingChangeTimestamp, listPendingBackups, + listUninitializedBackups, getBackedUpBlobHashes, unsetBackedUpBlobHashes, } diff --git a/services/history-v1/storage/scripts/backup_scheduler.mjs b/services/history-v1/storage/scripts/backup_scheduler.mjs index 32edc1d0af..164512701e 100644 --- a/services/history-v1/storage/scripts/backup_scheduler.mjs +++ b/services/history-v1/storage/scripts/backup_scheduler.mjs @@ -4,6 +4,7 @@ import commandLineArgs from 'command-line-args' import logger from '@overleaf/logger' import { listPendingBackups, + listUninitializedBackups, getBackupStatus, } from '../lib/backup_store/index.js' @@ -200,6 +201,18 @@ async function addDateRangeJob(input) { ) } +// Helper to list pending and uninitialized backups +// This function combines the two cursors into a single generator +// to yield projects from both lists +async function* pendingCursor(timeIntervalMs, limit) { + for await (const project of listPendingBackups(timeIntervalMs, limit)) { + yield project + } + for await (const project of listUninitializedBackups(timeIntervalMs, limit)) { + yield project + } +} + // Process pending projects with changes older than the specified seconds async function processPendingProjects( age, @@ -218,11 +231,11 @@ async function processPendingProjects( let addedCount = 0 let existingCount = 0 // Pass the limit directly to MongoDB query for better performance - const pendingCursor = listPendingBackups(timeIntervalMs, limit) const changeTimes = [] - for await (const project of pendingCursor) { + for await (const project of pendingCursor(timeIntervalMs, limit)) { const projectId = project._id.toHexString() - const pendingAt = project.overleaf?.backup?.pendingChangeAt + const pendingAt = + project.overleaf?.backup?.pendingChangeAt || project._id.getTimestamp() if (pendingAt) { changeTimes.push(pendingAt) const pendingAge = Math.floor((Date.now() - pendingAt.getTime()) / 1000) From 040f70471c74182b1e084c7683ae0bf3e0eb4ac6 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 7 Apr 2025 11:05:51 +0100 Subject: [PATCH 018/978] Merge pull request #24636 from overleaf/bg-history-backup-fix-broken-projects add --fix option to backup script GitOrigin-RevId: 568c9158669bb1cede0f0dd75e7507b10e8ff5a2 --- .../history-v1/storage/scripts/backup.mjs | 65 ++++++++++++++++++- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/services/history-v1/storage/scripts/backup.mjs b/services/history-v1/storage/scripts/backup.mjs index d73ef9dade..474192dc74 100644 --- a/services/history-v1/storage/scripts/backup.mjs +++ b/services/history-v1/storage/scripts/backup.mjs @@ -2,8 +2,12 @@ import logger from '@overleaf/logger' import commandLineArgs from 'command-line-args' -import { History } from 'overleaf-editor-core' -import { getProjectChunks, loadLatestRaw } from '../lib/chunk_store/index.js' +import { Chunk, History, Snapshot } from 'overleaf-editor-core' +import { + getProjectChunks, + loadLatestRaw, + create, +} from '../lib/chunk_store/index.js' import { client } from '../lib/mongodb.js' import knex from '../lib/knex.js' import { historyStore } from '../lib/history_store.js' @@ -321,6 +325,11 @@ const optionDefinitions = [ description: 'Time interval in seconds for pending backups (default: 3600)', defaultValue: 3600, }, + { + name: 'fix', + type: Number, + description: 'Fix projects without chunks', + }, { name: 'init', alias: 'I', @@ -367,6 +376,7 @@ function handleOptions() { !options.list && !options.pending && !options.init && + !(options.fix >= 0) && !(options.compare && options['start-date'] && options['end-date']) if (projectIdRequired && !options.projectId) { @@ -681,6 +691,54 @@ function convertToISODate(dateStr) { return new Date(dateStr + 'T00:00:00.000Z').toISOString() } +export async function fixProjectsWithoutChunks(options) { + const limit = options.fix || 1 + const query = { + 'overleaf.history.id': { $exists: true }, + 'overleaf.backup.lastBackedUpVersion': { $in: [null] }, + } + const cursor = client + .db() + .collection('projects') + .find(query, { + projection: { _id: 1, 'overleaf.history.id': 1 }, + readPreference: READ_PREFERENCE_SECONDARY, + }) + .limit(limit) + for await (const project of cursor) { + const historyId = project.overleaf.history.id.toString() + const chunks = await getProjectChunks(historyId) + if (chunks.length > 0) { + continue + } + if (DRY_RUN) { + console.log( + 'Would create new chunk for Project ID:', + project._id.toHexString(), + 'History ID:', + historyId, + 'Chunks:', + chunks + ) + } else { + console.log( + 'Creating new chunk for Project ID:', + project._id.toHexString(), + 'History ID:', + historyId, + 'Chunks:', + chunks + ) + const snapshot = new Snapshot() + const history = new History(snapshot, []) + const chunk = new Chunk(history, 0) + await create(historyId, chunk) + const newChunks = await getProjectChunks(historyId) + console.log('New chunk:', newChunks) + } + } +} + export async function initializeProjects(options) { await ensureGlobalBlobsLoaded() let totalErrors = 0 @@ -983,11 +1041,12 @@ async function main() { const options = handleOptions() await ensureGlobalBlobsLoaded() const projectId = options.projectId - if (options.status) { await displayBackupStatus(projectId) } else if (options.list) { await displayPendingBackups(options) + } else if (options.fix !== undefined) { + await fixProjectsWithoutChunks(options) } else if (options.pending) { await backupPendingProjects(options) } else if (options.init) { From 92dd62975ee708696fd48e5afb12c540fac4b4bd Mon Sep 17 00:00:00 2001 From: David <33458145+davidmcpowell@users.noreply.github.com> Date: Mon, 7 Apr 2025 11:07:37 +0100 Subject: [PATCH 019/978] Merge pull request #24575 from overleaf/dp-review-panel-dark-theme Add dark theme for review panel in new editor GitOrigin-RevId: 179cc257cd66f1ac477d7f4d428992019298ebc1 --- .../components/collapsible-file-header.scss | 12 ++- .../bootstrap-5/components/panel-heading.scss | 16 +++- .../pages/editor/review-panel-new.scss | 87 ++++++++++++------- 3 files changed, 80 insertions(+), 35 deletions(-) diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/collapsible-file-header.scss b/services/web/frontend/stylesheets/bootstrap-5/components/collapsible-file-header.scss index 4a510ae798..1474e54bb1 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/collapsible-file-header.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/collapsible-file-header.scss @@ -1,3 +1,13 @@ +:root { + --collapsible-file-header-bg-color: var(--bg-light-tertiary); + + @include theme('default') { + .ide-redesign-main { + --collapsible-file-header-bg-color: var(--bg-dark-tertiary); + } + } +} + .collapsible-file-header { all: unset; padding: var(--spacing-03) var(--spacing-04); @@ -11,7 +21,7 @@ } .collapsible-file-header-count { - background-color: var(--neutral-20); + background-color: var(--collapsible-file-header-bg-color); padding: var(--spacing-01) var(--spacing-02); margin-left: auto; border-radius: var(--border-radius-base); diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/panel-heading.scss b/services/web/frontend/stylesheets/bootstrap-5/components/panel-heading.scss index 0792665cc5..22125e36e6 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/panel-heading.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/panel-heading.scss @@ -1,3 +1,15 @@ +:root { + --panel-heading-color: var(--content-primary); + --panel-heading-button-hover-color: var(--bg-light-tertiary); + + @include theme('default') { + .ide-redesign-main { + --panel-heading-color: var(--content-primary-dark); + --panel-heading-button-hover-color: var(--bg-dark-tertiary); + } + } +} + .panel-heading { display: flex; align-items: center; @@ -21,11 +33,11 @@ align-items: center; border: none; background-color: transparent; - color: var(--content-primary); + color: var(--panel-heading-color); padding: var(--spacing-01); &:hover, &:focus { - background-color: var(--neutral-20); + background-color: var(--panel-heading-button-hover-color); } } diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/review-panel-new.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/review-panel-new.scss index ec766bd8d6..f3bb875a02 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/review-panel-new.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/review-panel-new.scss @@ -2,13 +2,34 @@ $rp-border-grey: #d9d9d9; $rp-type-blue: #6b7797; -$rp-type-darkgrey: #3f3f3f; :root { --rp-base-font-size: var(--font-size-01); --rp-border-grey: #{$rp-border-grey}; --rp-type-blue: #{$rp-type-blue}; - --rp-type-darkgrey: #{$rp-type-darkgrey}; + --review-panel-bg-color: var(--bg-light-secondary); + --review-panel-color: var(--content-primary); + --review-panel-color-secondary: var(--content-secondary); + --review-panel-header-bg-color: var(--bg-light-primary); + --review-panel-footer-bg-color: var(--bg-light-primary); + --review-panel-entry-bg-color: var(--bg-light-primary); + --review-panel-empty-state-bg-color: var(--bg-light-primary); + --review-panel-button-hover-bg-color: var(--bg-light-tertiary); + --review-panel-border-color: var(--border-divider); + + @include theme('default') { + .ide-redesign-main { + --review-panel-bg-color: var(--bg-dark-secondary); + --review-panel-color: var(--content-primary-dark); + --review-panel-color-secondary: var(--content-secondary-dark); + --review-panel-header-bg-color: var(--bg-dark-primary); + --review-panel-footer-bg-color: var(--bg-dark-primary); + --review-panel-entry-bg-color: var(--bg-dark-primary); + --review-panel-empty-state-bg-color: var(--bg-dark-primary); + --review-panel-button-hover-bg-color: var(--bg-dark-tertiary); + --review-panel-border-color: var(--border-divider-dark); + } + } } .review-panel-container { @@ -20,9 +41,9 @@ $rp-type-darkgrey: #3f3f3f; .review-panel-inner { z-index: 6; flex-shrink: 0; - background-color: var(--neutral-10); - border-left: 1px solid var(--border-divider); - color: var(--content-primary); + background-color: var(--review-panel-bg-color); + border-left: 1px solid var(--review-panel-border-color); + color: var(--review-panel-color); font-family: $font-family-base; line-height: $line-height-base; font-size: var(--font-size-01); @@ -37,9 +58,9 @@ $rp-type-darkgrey: #3f3f3f; } .review-panel-entry { - background-color: var(--white); + background-color: var(--review-panel-entry-bg-color); border-radius: var(--border-radius-base); - border: 1px solid var(--neutral-20); + border: 1px solid var(--review-panel-border-color); padding: var(--spacing-04); width: calc(100% - var(--spacing-04)); margin-left: var(--spacing-02); @@ -85,7 +106,7 @@ $rp-type-darkgrey: #3f3f3f; margin-bottom: var(--spacing-01); .review-panel-entry-user { - color: $content-primary; + color: var(--review-panel-color); font-size: 110%; max-width: 150px; white-space: nowrap; @@ -93,7 +114,7 @@ $rp-type-darkgrey: #3f3f3f; } .review-panel-entry-time { - color: var(--content-secondary); + color: var(--review-panel-color-secondary); } .review-panel-entry-actions { @@ -103,6 +124,7 @@ $rp-type-darkgrey: #3f3f3f; .btn { background-color: transparent; + color: var(--review-panel-color); border-width: 0; padding: 0; height: 24px; @@ -110,8 +132,8 @@ $rp-type-darkgrey: #3f3f3f; &:hover, &:focus { - background-color: var(--neutral-20); - color: var(--content-primary); + background-color: var(--review-panel-button-hover-bg-color); + color: var(--review-panel-color); } } @@ -137,13 +159,13 @@ $rp-type-darkgrey: #3f3f3f; .review-panel-change-body { display: flex; align-items: flex-start; - color: var(--content-secondary); + color: var(--review-panel-color-secondary); gap: var(--spacing-02); overflow-wrap: anywhere; } .review-panel-content-highlight { - color: var(--content-primary); + color: var(--review-panel-color); text-decoration: none; } @@ -184,8 +206,8 @@ del.review-panel-content-highlight { display: flex; flex-direction: column; justify-content: center; - border-bottom: 1px solid var(--rp-border-grey); - background-color: white; + border-bottom: 1px solid var(--review-panel-border-color); + background-color: var(--review-panel-header-bg-color); text-align: center; z-index: 4; } @@ -263,13 +285,13 @@ del.review-panel-content-highlight { align-items: center; border: none; background-color: transparent; - color: var(--content-primary); + color: var(--review-panel-color); padding: var(--spacing-01); border-radius: 100%; &:hover, &:focus { - background-color: var(--neutral-20); + background-color: var(--review-panel-button-hover-bg-color); } } @@ -413,7 +435,7 @@ del.review-panel-content-highlight { .review-panel-comment-body { font-size: var(--font-size-02); - color: var(--content-primary); + color: var(--review-panel-color); overflow-wrap: anywhere; white-space: pre-wrap; } @@ -446,8 +468,8 @@ del.review-panel-content-highlight { border-radius: var(--border-radius-base); border: solid 1px var(--neutral-60); resize: vertical; - color: var(--rp-type-darkgrey); - background-color: var(--white); + color: var(--review-panel-color-secondary); + background-color: var(--review-panel-entry-bg-color); height: 25px; min-height: 25px; overflow-x: hidden; @@ -487,7 +509,7 @@ del.review-panel-content-highlight { .review-panel-empty-state-comment-icon { width: 80px; height: 80px; - background-color: white; + background-color: var(--review-panel-empty-state-bg-color); border-radius: 100%; display: flex; align-items: center; @@ -515,7 +537,7 @@ del.review-panel-content-highlight { } .review-panel-overfile-divider { - border-bottom: 1px solid var(--border-divider); + border-bottom: 1px solid var(--review-panel-border-color); margin: var(--spacing-01) 0; } @@ -534,8 +556,8 @@ del.review-panel-content-highlight { bottom: 0; width: var(--review-panel-width); z-index: 2; - background-color: white; - border-top: 1px solid var(--rp-border-grey); + background-color: var(--review-panel-footer-bg-color); + border-top: 1px solid var(--review-panel-border-color); display: flex; .review-panel-tab { @@ -548,17 +570,17 @@ del.review-panel-content-highlight { border: 0; border-top: solid 3px transparent; background: none; - color: var(--content-secondary); + color: var(--review-panel-color-secondary); font-size: var(--font-size-02); &:hover, &:focus { text-decoration: none; - color: var(--content-primary); + color: var(--review-panel-color); } &-active { - color: var(--content-primary); + color: var(--review-panel-color); border-top: solid 3px var(--bg-accent-01); } } @@ -578,11 +600,12 @@ del.review-panel-content-highlight { .review-panel-add-comment-cancel-button { background-color: transparent; + color: var(--review-panel-color); &:hover, &:focus { - background-color: var(--neutral-20); - color: var(--content-primary); + background-color: var(--review-panel-button-hover-bg-color); + color: var(--review-panel-color); } } @@ -638,14 +661,14 @@ del.review-panel-content-highlight { left: 0; top: 0; display: flex; - color: var(--content-secondary); + color: var(--review-panel-color-secondary); cursor: pointer; } .review-panel-entry-content { display: none; - background: var(--white); - border: 1px solid var(--rp-border-grey); + background: var(--review-panel-entry-bg-color); + border: 1px solid var(--review-panel-border-color); border-radius: var(--border-radius-base); width: 200px; padding: var(--spacing-02); From 55a13ca1de1858db1f2b79e1049d44bb27ec365f Mon Sep 17 00:00:00 2001 From: David <33458145+davidmcpowell@users.noreply.github.com> Date: Mon, 7 Apr 2025 11:07:50 +0100 Subject: [PATCH 020/978] Merge pull request #24662 from overleaf/mj-wc-survey-cleanup [web] Remove unused component GitOrigin-RevId: dd525258349834a8bbb28e78a06445bafc9b2e99 --- services/web/frontend/extracted-translations.json | 2 -- services/web/locales/en.json | 2 -- 2 files changed, 4 deletions(-) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 945d3a1f70..a12d81b5d5 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -1094,7 +1094,6 @@ "open_path": "", "open_pdf_in_separate_tab": "", "open_project": "", - "open_survey": "", "open_target": "", "optional": "", "or": "", @@ -1968,7 +1967,6 @@ "visual_editor": "", "visual_editor_is_only_available_for_tex_files": "", "want_change_to_apply_before_plan_end": "", - "we_are_testing_a_new_reference_search": "", "we_are_unable_to_generate_the_pdf_at_this_time": "", "we_are_unable_to_opt_you_into_this_experiment": "", "we_cant_find_any_sections_or_subsections_in_this_file": "", diff --git a/services/web/locales/en.json b/services/web/locales/en.json index d0bd81af5a..ab75c53871 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1451,7 +1451,6 @@ "open_path": "Open __path__", "open_pdf_in_separate_tab": "Open PDF in separate tab", "open_project": "Open Project", - "open_survey": "Open survey", "open_target": "Go to target", "opted_out_linking": "You’ve opted out from linking your __email__ __appName__ account to your institutional account.", "optional": "Optional", @@ -2516,7 +2515,6 @@ "visual_editor_is_only_available_for_tex_files": "Visual Editor is only available for TeX files", "want_access_to_overleaf_premium_features_through_your_university": "Want access to __appName__ premium features through your university?", "want_change_to_apply_before_plan_end": "If you wish this change to apply before the end of your current billing period, please contact us.", - "we_are_testing_a_new_reference_search": "We are testing a new reference search.", "we_are_unable_to_generate_the_pdf_at_this_time": "We are unable to generate the pdf at this time.", "we_are_unable_to_opt_you_into_this_experiment": "We are unable to opt you into this experiment at this time, please ensure your organization has allowed this feature, or try again later.", "we_cant_confirm_this_email": "We can’t confirm this email", From 71bc4c45bcb58dfdb318e36c6262b0e2af49e0d0 Mon Sep 17 00:00:00 2001 From: MoxAmber Date: Mon, 7 Apr 2025 11:57:36 +0100 Subject: [PATCH 021/978] Merge pull request #24373 from overleaf/as-customerio-web-sdk [web] Set up customerio frontend SDK GitOrigin-RevId: 0e043163e1f6cd02d8ecf3a3e854e7799d776edd --- .../app/src/infrastructure/ExpressLocals.js | 2 ++ services/web/app/views/_customer_io.pug | 26 +++++++++++++++++++ services/web/app/views/layout-base.pug | 2 ++ 3 files changed, 30 insertions(+) create mode 100644 services/web/app/views/_customer_io.pug diff --git a/services/web/app/src/infrastructure/ExpressLocals.js b/services/web/app/src/infrastructure/ExpressLocals.js index 002c342eef..eae1b48219 100644 --- a/services/web/app/src/infrastructure/ExpressLocals.js +++ b/services/web/app/src/infrastructure/ExpressLocals.js @@ -429,6 +429,8 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) { wikiEnabled: Settings.overleaf != null || Settings.proxyLearn, templatesEnabled: Settings.overleaf != null || Settings.templates?.user_id != null, + cioWriteKey: Settings.analytics?.cio?.writeKey, + cioSiteId: Settings.analytics?.cio?.siteId, } next() }) diff --git a/services/web/app/views/_customer_io.pug b/services/web/app/views/_customer_io.pug new file mode 100644 index 0000000000..81d75f7d7f --- /dev/null +++ b/services/web/app/views/_customer_io.pug @@ -0,0 +1,26 @@ +if(customerIoEnabled && ExposedSettings.cioWriteKey && ExposedSettings.cioSiteId) + script(type="text/javascript", id="cio-loader", nonce=scriptNonce, data-cio-write-key=ExposedSettings.cioWriteKey, data-cio-site-id=ExposedSettings.cioSiteId, data-session-analytics-id=getSessionAnalyticsId(), data-user-id=getLoggedInUserId()). + var cioSettings = document.querySelector('#cio-loader').dataset; + var analyticsId = cioSettings.sessionAnalyticsId; + var siteId = cioSettings.cioSiteId; + var writeKey = cioSettings.cioWriteKey; + var userId = cioSettings.userId; + + !function(){var i="cioanalytics", analytics=(window[i]=window[i]||[]);if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on","addSourceMiddleware","addIntegrationMiddleware","setAnonymousId","addDestinationMiddleware"];analytics.factory=function(e){return function(){var t=Array.prototype.slice.call(arguments);t.unshift(e);analytics.push(t);return analytics}};for(var e=0;e Date: Mon, 7 Apr 2025 11:57:51 +0100 Subject: [PATCH 022/978] [clsi] upgrade dockerode to bump tar-fs (#24693) Diff: https://github.com/apocas/dockerode/compare/v3.3.1...v4.0.5 GitOrigin-RevId: 73ba2610d0c2e766a52e638754af410aaad94ec1 --- package-lock.json | 242 ++++++++++++++++++++++++------------- services/clsi/package.json | 2 +- 2 files changed, 159 insertions(+), 85 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9e3c53b799..5f0eb0baf1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3352,6 +3352,12 @@ "node": ">=6.9.0" } }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "license": "Apache-2.0" + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -15228,6 +15234,15 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/assert-never": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz", @@ -16406,6 +16421,15 @@ "node": ">= 0.10.x" } }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -18167,19 +18191,6 @@ "node": ">=10" } }, - "node_modules/cpu-features": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.2.tgz", - "integrity": "sha512-/2yieBqvMcRj8McNzkycjW2v3OIUOibBfd2dLEJ0nWts8NobAxwiyw9phVNS6oDL8x8tz9F7uNVFEVpJncQpeA==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "nan": "^2.14.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -19760,32 +19771,6 @@ "node": ">=6" } }, - "node_modules/docker-modem": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.3.tgz", - "integrity": "sha512-Tgkn2a+yiNP9FoZgMa/D9Wk+D2Db///0KOyKSYZRJa8w4+DzKyzQMkczKSdR/adQ0x46BOpeNkoyEOKjPhCzjw==", - "dependencies": { - "debug": "^4.1.1", - "readable-stream": "^3.5.0", - "split-ca": "^1.0.1", - "ssh2": "^1.4.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/dockerode": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.1.tgz", - "integrity": "sha512-AS2mr8Lp122aa5n6d99HkuTNdRV1wkkhHwBdcnY6V0+28D3DSYwhxAk85/mM9XwD3RMliTxyr63iuvn5ZblFYQ==", - "dependencies": { - "docker-modem": "^3.0.0", - "tar-fs": "~2.0.1" - }, - "engines": { - "node": ">= 8.0" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -37469,31 +37454,6 @@ "es5-ext": "^0.10.53" } }, - "node_modules/ssh2": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.6.0.tgz", - "integrity": "sha512-lxc+uvXqOxyQ99N2M7k5o4pkYDO5GptOTYduWw7hIM41icxvoBcCNHcj+LTKrjkL0vFcAl+qfZekthoSFRJn2Q==", - "hasInstallScript": true, - "dependencies": { - "asn1": "^0.2.4", - "bcrypt-pbkdf": "^1.0.2" - }, - "engines": { - "node": ">=10.16.0" - }, - "optionalDependencies": { - "cpu-features": "0.0.2", - "nan": "^2.15.0" - } - }, - "node_modules/ssh2/node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, "node_modules/sshpk": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", @@ -37518,14 +37478,6 @@ "node": ">=0.10.0" } }, - "node_modules/sshpk/node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, "node_modules/sshpk/node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -38769,17 +38721,6 @@ "node": ">=10" } }, - "node_modules/tar-fs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", - "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.0.0" - } - }, "node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -42075,7 +42016,7 @@ "body-parser": "^1.20.3", "bunyan": "^1.8.15", "diskusage": "^1.1.3", - "dockerode": "^3.1.0", + "dockerode": "^4.0.5", "express": "^4.21.2", "lodash": "^4.17.21", "p-limit": "^3.1.0", @@ -42126,6 +42067,33 @@ "node": ">= 0.6" } }, + "services/clsi/node_modules/@grpc/grpc-js": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.2.tgz", + "integrity": "sha512-nnR5nmL6lxF8YBqb6gWvEgLdLh/Fn+kvAdX5hUOnt48sNSb0riz/93ASd2E5gvanPA41X6Yp25bIfGRp1SMb2g==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "services/clsi/node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "services/clsi/node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -42135,6 +42103,70 @@ "node": ">=0.3.1" } }, + "services/clsi/node_modules/docker-modem": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz", + "integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "services/clsi/node_modules/dockerode": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.5.tgz", + "integrity": "sha512-ZPmKSr1k1571Mrh7oIBS/j0AqAccoecY2yH420ni5j1KyNMgnoTh4Nu4FWunh0HZIJmRSmSysJjBIpa/zyWUEA==", + "license": "Apache-2.0", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.6", + "protobufjs": "^7.3.2", + "tar-fs": "~2.1.2", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "services/clsi/node_modules/nan": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", + "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", + "license": "MIT", + "optional": true + }, + "services/clsi/node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "services/clsi/node_modules/sinon": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.0.3.tgz", @@ -42154,6 +42186,23 @@ "url": "https://opencollective.com/sinon" } }, + "services/clsi/node_modules/ssh2": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", + "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.20.0" + } + }, "services/clsi/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -42166,6 +42215,31 @@ "node": ">=8" } }, + "services/clsi/node_modules/tar-fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "services/clsi/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "services/contacts": { "name": "@overleaf/contacts", "dependencies": { diff --git a/services/clsi/package.json b/services/clsi/package.json index 5df3f0e8b9..3f05ab543d 100644 --- a/services/clsi/package.json +++ b/services/clsi/package.json @@ -28,7 +28,7 @@ "body-parser": "^1.20.3", "bunyan": "^1.8.15", "diskusage": "^1.1.3", - "dockerode": "^3.1.0", + "dockerode": "^4.0.5", "express": "^4.21.2", "lodash": "^4.17.21", "p-limit": "^3.1.0", From d08fa01110f24bc114458e92c653a4e1889c3ccd Mon Sep 17 00:00:00 2001 From: MoxAmber Date: Mon, 7 Apr 2025 11:58:06 +0100 Subject: [PATCH 023/978] Merge pull request #24545 from overleaf/as-customerio-toolbar-placeholders [web] Create placeholders for customer.io inline messages GitOrigin-RevId: 862362cd9336e5c1899dfaeeabac9f3da181ccf9 --- .../components/history-toggle-button.jsx | 1 + .../components/share-project-button.jsx | 1 + .../components/track-changes-toggle-button.jsx | 1 + .../bootstrap-5/pages/editor/toolbar.scss | 17 +++++++++++++++++ 4 files changed, 20 insertions(+) diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/history-toggle-button.jsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/history-toggle-button.jsx index 644efd7a1a..23a2adfd09 100644 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/history-toggle-button.jsx +++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/history-toggle-button.jsx @@ -11,6 +11,7 @@ function HistoryToggleButton({ onClick }) {

{t('history')}

+
) } diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/share-project-button.jsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/share-project-button.jsx index f0aed3674d..6c4123f03d 100644 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/share-project-button.jsx +++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/share-project-button.jsx @@ -11,6 +11,7 @@ function ShareProjectButton({ onClick }) {

{t('share')}

+
) } diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/track-changes-toggle-button.jsx b/services/web/frontend/js/features/editor-navigation-toolbar/components/track-changes-toggle-button.jsx index 5b74c71e84..469dc7cd29 100644 --- a/services/web/frontend/js/features/editor-navigation-toolbar/components/track-changes-toggle-button.jsx +++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/track-changes-toggle-button.jsx @@ -25,6 +25,7 @@ function TrackChangesToggleButton({

{t('review')}

+
) } diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss index 70b5dac91c..d3d2dca6fd 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss @@ -248,6 +248,23 @@ .toolbar-item { display: flex; + justify-content: center; + } + + .toolbar-cio-tooltip { + position: absolute; + top: var(--toolbar-height); + width: 391px; + } + + #toolbar-cio-history { + @include media-breakpoint-down(lg) { + right: 10px; + + /* The iframe a message renders in isn't aware of the page width, so we + make it 1px narrower to enable max-width media queries */ + width: 390px; + } } } From 0f3f78cde7c8bb22077eaf7b2d44896909be9697 Mon Sep 17 00:00:00 2001 From: Domagoj Kriskovic Date: Mon, 7 Apr 2025 14:27:38 +0200 Subject: [PATCH 024/978] Add mouse down listener in PreventSelectingEntry (#24665) GitOrigin-RevId: 97411fd45d10b850f41c3f6269550bc6fffb0e11 --- .../components/review-panel-prevent-selecting.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/services/web/frontend/js/features/review-panel-new/components/review-panel-prevent-selecting.tsx b/services/web/frontend/js/features/review-panel-new/components/review-panel-prevent-selecting.tsx index cad04048ce..30cc8a2439 100644 --- a/services/web/frontend/js/features/review-panel-new/components/review-panel-prevent-selecting.tsx +++ b/services/web/frontend/js/features/review-panel-new/components/review-panel-prevent-selecting.tsx @@ -1,5 +1,7 @@ -const stopPropagation = (e: React.FocusEvent | React.MouseEvent) => +const stopPropagation = (e: React.FocusEvent | React.MouseEvent) => { e.stopPropagation() + e.preventDefault() +} export const PreventSelectingEntry = ({ children, @@ -8,7 +10,11 @@ export const PreventSelectingEntry = ({ }) => { return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions -
+
{children}
) From a68e96400b0ea9a095a685cf032ed2729b9bce00 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Mon, 7 Apr 2025 13:53:15 +0100 Subject: [PATCH 025/978] Merge pull request #24670 from overleaf/bg-remove-logging-of-update-errors remove update parameter in applyOtUpdate error handling GitOrigin-RevId: 46fa9d669327850f956154b20676317a7b13eb78 --- services/real-time/app/js/Router.js | 1 - 1 file changed, 1 deletion(-) diff --git a/services/real-time/app/js/Router.js b/services/real-time/app/js/Router.js index e907a1b684..238dc386a3 100644 --- a/services/real-time/app/js/Router.js +++ b/services/real-time/app/js/Router.js @@ -579,7 +579,6 @@ module.exports = Router = { if (err) { Router._handleError(callback, err, client, 'applyOtUpdate', { doc_id: docId, - update, }) } else { callback() From eb276c7403cd2c559bac35d2d6505d22a2a865f3 Mon Sep 17 00:00:00 2001 From: Tim Down <158919+timdown@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:52:51 +0100 Subject: [PATCH 026/978] Merge pull request #24416 from overleaf/td-bs5-remove-platform-pages-flag Remove bs5-misc-pages-platform feature flag from code GitOrigin-RevId: 8da617e5d7703a56399b227b0c38acda86150b8d --- .../web/app/src/Features/User/UserPagesController.mjs | 8 -------- services/web/app/views/user/compromised_password.pug | 1 - 2 files changed, 9 deletions(-) diff --git a/services/web/app/src/Features/User/UserPagesController.mjs b/services/web/app/src/Features/User/UserPagesController.mjs index 1c6ce7b82c..cd456a4377 100644 --- a/services/web/app/src/Features/User/UserPagesController.mjs +++ b/services/web/app/src/Features/User/UserPagesController.mjs @@ -307,14 +307,6 @@ const UserPagesController = { }, async compromisedPasswordPage(req, res) { - // Populates splitTestVariants with a value for the split test name and allows - // Pug to read it - await SplitTestHandler.promises.getAssignment( - req, - res, - 'bs5-misc-pages-platform' - ) - res.render('user/compromised_password') }, diff --git a/services/web/app/views/user/compromised_password.pug b/services/web/app/views/user/compromised_password.pug index 27b85b718a..a6d83aeb92 100644 --- a/services/web/app/views/user/compromised_password.pug +++ b/services/web/app/views/user/compromised_password.pug @@ -5,7 +5,6 @@ block vars - var suppressFooter = true - var suppressGoogleAnalytics = true - bootstrap5PageStatus = 'enabled' - - bootstrap5PageSplitTest = 'bs5-misc-pages-platform' block entrypointVar - entrypoint = 'pages/compromised-password' From 4e8f982ca21af08b02f36c0370b92fdc78bff136 Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:55:51 +0300 Subject: [PATCH 027/978] Merge pull request #24682 from overleaf/ii-group-members-table-2 [web] Group members table colspan fix 2 GitOrigin-RevId: ddb7438da3c68b74b8f38feb8512175e8c24443d --- .../components/members-table/members-list.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/services/web/frontend/js/features/group-management/components/members-table/members-list.tsx b/services/web/frontend/js/features/group-management/components/members-table/members-list.tsx index ca1a5cdf88..408d7512d6 100644 --- a/services/web/frontend/js/features/group-management/components/members-table/members-list.tsx +++ b/services/web/frontend/js/features/group-management/components/members-table/members-list.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { User } from '../../../../../../types/group-management/user' import { useGroupMembersContext } from '../../context/group-members-context' @@ -28,6 +28,14 @@ export default function MembersList({ groupId }: ManagedUsersListProps) { const { users } = useGroupMembersContext() const managedUsersActive = getMeta('ol-managedUsersActive') const groupSSOActive = getMeta('ol-groupSSOActive') + const tHeadRowRef = useRef(null) + const [colSpan, setColSpan] = useState(0) + + useEffect(() => { + if (tHeadRowRef.current) { + setColSpan(tHeadRowRef.current.querySelectorAll('th').length) + } + }, []) return (
@@ -53,7 +61,7 @@ export default function MembersList({ groupId }: ManagedUsersListProps) { data-testid="managed-entities-table" > - + {t('email')} {t('name')} @@ -83,7 +91,7 @@ export default function MembersList({ groupId }: ManagedUsersListProps) { {users.length === 0 && ( - + {t('no_members')} From 4cee3768789fb48e3333dbd6841278da9418f200 Mon Sep 17 00:00:00 2001 From: Tim Down <158919+timdown@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:56:26 +0100 Subject: [PATCH 028/978] Merge pull request #24688 from overleaf/td-bs5-editor-beginner-switch-popover Migrate beginner editor switch popover to BS5 GitOrigin-RevId: c470df46989de7ad6477ee23ff13fc95dd580ea8 --- .../editor-switch-beginner-popover.tsx | 102 +++++++++++++++ .../editor-switch-beginner-tooltip.tsx | 118 ------------------ .../components/editor-switch.tsx | 18 ++- 3 files changed, 114 insertions(+), 124 deletions(-) create mode 100644 services/web/frontend/js/features/source-editor/components/editor-switch-beginner-popover.tsx delete mode 100644 services/web/frontend/js/features/source-editor/components/editor-switch-beginner-tooltip.tsx diff --git a/services/web/frontend/js/features/source-editor/components/editor-switch-beginner-popover.tsx b/services/web/frontend/js/features/source-editor/components/editor-switch-beginner-popover.tsx new file mode 100644 index 0000000000..40fea785bf --- /dev/null +++ b/services/web/frontend/js/features/source-editor/components/editor-switch-beginner-popover.tsx @@ -0,0 +1,102 @@ +import { ReactElement, useCallback, useEffect, useState } from 'react' +import Close from '@/shared/components/close' +import { useEditorContext } from '@/shared/context/editor-context' +import useTutorial from '@/shared/hooks/promotions/use-tutorial' +import { useUserContext } from '@/shared/context/user-context' +import useScopeValue from '@/shared/hooks/use-scope-value' +import { useTranslation } from 'react-i18next' +import getMeta from '@/utils/meta' +import OLPopover from '@/features/ui/components/ol/ol-popover' +import OLOverlay from '@/features/ui/components/ol/ol-overlay' + +const CODE_EDITOR_POPOVER_TIMEOUT = 1000 +export const codeEditorModePrompt = 'code-editor-mode-prompt' + +export const EditorSwitchBeginnerPopover = ({ + children, + targetRef, +}: { + children: ReactElement + targetRef: React.RefObject +}) => { + const user = useUserContext() + const { inactiveTutorials } = useEditorContext() + const { t } = useTranslation() + const [codeEditorOpened] = useScopeValue('editor.codeEditorOpened') + const { completeTutorial } = useTutorial(codeEditorModePrompt, { + location: 'logs', + name: codeEditorModePrompt, + }) + const [popoverShown, setPopoverShown] = useState(false) + + const shouldShowCodeEditorPopover = useCallback(() => { + if (inactiveTutorials.includes(codeEditorModePrompt)) { + return false + } + + if (getMeta('ol-usedLatex') !== 'never') { + // only show popover to the users that never used LaTeX (submitted in onboarding data collection) + return false + } + + if (codeEditorOpened) { + // dont show popover if code editor was opened at some point + return false + } + + const msSinceSignedUp = + user.signUpDate && Date.now() - new Date(user.signUpDate).getTime() + + if (msSinceSignedUp && msSinceSignedUp < 24 * 60 * 60 * 1000) { + // dont show popover if user has signed up is less than 24 hours + return false + } + + return true + }, [codeEditorOpened, inactiveTutorials, user.signUpDate]) + + useEffect(() => { + if (popoverShown && codeEditorOpened) { + setPopoverShown(false) + } + }, [codeEditorOpened, popoverShown]) + + useEffect(() => { + const timeout = setTimeout(() => { + if (shouldShowCodeEditorPopover()) { + setPopoverShown(true) + } + }, CODE_EDITOR_POPOVER_TIMEOUT) + + return () => clearTimeout(timeout) + }, [shouldShowCodeEditorPopover]) + + return ( + <> + {children} + setPopoverShown(false)} + target={targetRef.current} + > + +
+ { + setPopoverShown(false) + completeTutorial({ event: 'promo-click', action: 'complete' }) + }} + /> +
+ {t('code_editor_tooltip_title')} +
+
{t('code_editor_tooltip_message')}
+
+
+
+ + ) +} diff --git a/services/web/frontend/js/features/source-editor/components/editor-switch-beginner-tooltip.tsx b/services/web/frontend/js/features/source-editor/components/editor-switch-beginner-tooltip.tsx deleted file mode 100644 index 02da586eef..0000000000 --- a/services/web/frontend/js/features/source-editor/components/editor-switch-beginner-tooltip.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { ReactElement, useCallback, useEffect, useRef, useState } from 'react' -import Tooltip from '../../../shared/components/tooltip' -import Close from '@/shared/components/close' -import { useEditorContext } from '@/shared/context/editor-context' -import useTutorial from '@/shared/hooks/promotions/use-tutorial' -import { useUserContext } from '@/shared/context/user-context' -import useScopeValue from '@/shared/hooks/use-scope-value' -import { useTranslation } from 'react-i18next' -import getMeta from '@/utils/meta' - -const CODE_EDITOR_TOOLTIP_TIMEOUT = 1000 -export const codeEditorModePrompt = 'code-editor-mode-prompt' - -export const EditorSwitchBeginnerTooltip = ({ - children, -}: { - children: ReactElement -}) => { - const toolbarRef = useRef(null) - const user = useUserContext() - const { inactiveTutorials } = useEditorContext() - const { t } = useTranslation() - const [codeEditorOpened] = useScopeValue('editor.codeEditorOpened') - const { completeTutorial } = useTutorial(codeEditorModePrompt, { - location: 'logs', - name: codeEditorModePrompt, - }) - const [tooltipShown, setTooltipShown] = useState(false) - - const shouldShowCodeEditorTooltip = useCallback(() => { - if (inactiveTutorials.includes(codeEditorModePrompt)) { - return false - } - - if (getMeta('ol-usedLatex') !== 'never') { - // only show tooltip to the users that never used LaTeX (submitted in onboarding data collection) - return false - } - - if (codeEditorOpened) { - // dont show tooltip if code editor was opened at some point - return false - } - - const msSinceSignedUp = - user.signUpDate && Date.now() - new Date(user.signUpDate).getTime() - - if (msSinceSignedUp && msSinceSignedUp < 24 * 60 * 60 * 1000) { - // dont show tooltip if user has signed up is less than 24 hours - return false - } - - return true - }, [codeEditorOpened, inactiveTutorials, user.signUpDate]) - - const showCodeEditorTooltip = useCallback(() => { - if (toolbarRef.current && 'show' in toolbarRef.current) { - toolbarRef.current.show() - setTooltipShown(true) - } - }, []) - - const hideCodeEditorTooltip = useCallback(() => { - if (toolbarRef.current && 'hide' in toolbarRef.current) { - toolbarRef.current.hide() - setTooltipShown(false) - } - }, []) - - useEffect(() => { - if (tooltipShown && codeEditorOpened) { - hideCodeEditorTooltip() - } - }, [codeEditorOpened, hideCodeEditorTooltip, tooltipShown]) - - useEffect(() => { - const timeout = setTimeout(() => { - if (shouldShowCodeEditorTooltip()) { - showCodeEditorTooltip() - } - }, CODE_EDITOR_TOOLTIP_TIMEOUT) - - return () => clearTimeout(timeout) - }, [showCodeEditorTooltip, shouldShowCodeEditorTooltip]) - - return ( - - { - hideCodeEditorTooltip() - completeTutorial({ event: 'promo-click', action: 'complete' }) - }} - /> -
{t('code_editor_tooltip_title')}
-
{t('code_editor_tooltip_message')}
-
- } - tooltipProps={{ - className: 'editor-switch-tooltip', - }} - overlayProps={{ - ref: toolbarRef, - placement: 'bottom', - shouldUpdatePosition: true, - // @ts-ignore - // trigger: null is used to prevent the tooltip from showing on hover - // but it is not allowed in the type definition - trigger: null, - }} - > - {children} - - ) -} diff --git a/services/web/frontend/js/features/source-editor/components/editor-switch.tsx b/services/web/frontend/js/features/source-editor/components/editor-switch.tsx index bdeecfad79..525f979e1c 100644 --- a/services/web/frontend/js/features/source-editor/components/editor-switch.tsx +++ b/services/web/frontend/js/features/source-editor/components/editor-switch.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, FC, memo, useCallback } from 'react' +import { ChangeEvent, FC, memo, useCallback, useRef } from 'react' import useScopeValue from '@/shared/hooks/use-scope-value' import OLTooltip from '@/features/ui/components/ol/ol-tooltip' import useTutorial from '@/shared/hooks/promotions/use-tutorial' @@ -6,9 +6,9 @@ import { sendMB } from '../../../infrastructure/event-tracking' import { isValidTeXFile } from '../../../main/is-valid-tex-file' import { useTranslation } from 'react-i18next' import { - EditorSwitchBeginnerTooltip, + EditorSwitchBeginnerPopover, codeEditorModePrompt, -} from './editor-switch-beginner-tooltip' +} from './editor-switch-beginner-popover' import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' function EditorSwitch() { @@ -17,6 +17,8 @@ function EditorSwitch() { const [codeEditorOpened] = useScopeValue('editor.codeEditorOpened') const { openDocName } = useEditorManagerContext() + const codeEditorRef = useRef(null) + const richTextAvailable = openDocName ? isValidTeXFile(openDocName) : false const { completeTutorial } = useTutorial(codeEditorModePrompt, { location: 'logs', @@ -62,11 +64,15 @@ function EditorSwitch() { checked={!richTextAvailable || !visual} onChange={handleChange} /> - - + Date: Mon, 7 Apr 2025 14:56:51 +0100 Subject: [PATCH 029/978] Merge pull request #24606 from overleaf/td-bs5-make-default Make Bootstrap 5 the default everywhere GitOrigin-RevId: 024614d6f4f370fd9c9623a6f35c64e43d2a70c4 --- services/web/app/views/admin/index.pug | 3 --- services/web/app/views/layout-base.pug | 2 +- services/web/app/views/layout-website-redesign-bootstrap-5.pug | 1 - services/web/app/views/project/editor/socket_diagnostics.pug | 1 - services/web/app/views/project/ide-react-detached.pug | 1 - services/web/app/views/project/ide-react.pug | 1 - services/web/app/views/project/list-react.pug | 1 - services/web/app/views/project/token/access-react.pug | 1 - services/web/app/views/project/token/sharing-updates.pug | 1 - services/web/app/views/subscriptions/add-seats.pug | 3 --- .../app/views/subscriptions/canceled-subscription-react.pug | 3 --- services/web/app/views/subscriptions/dashboard-react.pug | 3 --- .../views/subscriptions/manually-collected-subscription.pug | 3 --- .../app/views/subscriptions/missing-billing-information.pug | 3 --- services/web/app/views/subscriptions/preview-change.pug | 3 --- .../web/app/views/subscriptions/subtotal-limit-exceeded.pug | 3 --- .../app/views/subscriptions/successful-subscription-react.pug | 3 --- services/web/app/views/subscriptions/team/group-invites.pug | 3 --- services/web/app/views/subscriptions/team/invite-managed.pug | 3 --- services/web/app/views/subscriptions/team/invite.pug | 3 --- .../web/app/views/subscriptions/team/invite_logged_out.pug | 3 --- .../views/subscriptions/upgrade-group-subscription-react.pug | 3 --- services/web/app/views/user/addSecondaryEmail.pug | 1 - services/web/app/views/user/compromised_password.pug | 1 - services/web/app/views/user/confirmSecondaryEmail.pug | 1 - services/web/app/views/user/settings.pug | 1 - .../web/app/views/user_membership/group-managers-react.pug | 3 --- services/web/app/views/user_membership/group-members-react.pug | 3 --- .../app/views/user_membership/institution-managers-react.pug | 3 --- .../web/app/views/user_membership/publisher-managers-react.pug | 3 --- 30 files changed, 1 insertion(+), 66 deletions(-) diff --git a/services/web/app/views/admin/index.pug b/services/web/app/views/admin/index.pug index dbc8cbdb79..aaf2228cbc 100644 --- a/services/web/app/views/admin/index.pug +++ b/services/web/app/views/admin/index.pug @@ -1,9 +1,6 @@ extends ../layout-marketing include ../_mixins/bookmarkable_tabset -block vars - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - block content .content.content-alt#main-content .container diff --git a/services/web/app/views/layout-base.pug b/services/web/app/views/layout-base.pug index df503b6356..0493281353 100644 --- a/services/web/app/views/layout-base.pug +++ b/services/web/app/views/layout-base.pug @@ -6,7 +6,7 @@ html( class=(fixedSizeDocument ? 'fixed-size-document' : undefined) ) - metadata = metadata || {} - - let bootstrap5PageStatus = 'disabled' // One of 'disabled', 'enabled', and 'queryStringOnly' + - let bootstrap5PageStatus = 'enabled' // One of 'disabled' and 'enabled' - let bootstrap5PageSplitTest = '' // Limits Bootstrap 5 usage on this page to users with an assignment of "enabled" for the specified split test. If left empty and bootstrap5PageStatus is "enabled", the page always uses Bootstrap 5. - let isWebsiteRedesign = false - let isApplicationPage = false diff --git a/services/web/app/views/layout-website-redesign-bootstrap-5.pug b/services/web/app/views/layout-website-redesign-bootstrap-5.pug index 092f5b765d..e6dc3a8827 100644 --- a/services/web/app/views/layout-website-redesign-bootstrap-5.pug +++ b/services/web/app/views/layout-website-redesign-bootstrap-5.pug @@ -5,7 +5,6 @@ include ./_mixins/bootstrap_js block entrypointVar - entrypoint = 'marketing' - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - isWebsiteRedesign = true block body diff --git a/services/web/app/views/project/editor/socket_diagnostics.pug b/services/web/app/views/project/editor/socket_diagnostics.pug index 841f4b517b..6876e7e39b 100644 --- a/services/web/app/views/project/editor/socket_diagnostics.pug +++ b/services/web/app/views/project/editor/socket_diagnostics.pug @@ -4,7 +4,6 @@ block vars - var suppressNavbar = true - var suppressFooter = true - var suppressGoogleAnalytics = true - - bootstrap5PageStatus = 'enabled' - isWebsiteRedesign = 'true' block entrypointVar diff --git a/services/web/app/views/project/ide-react-detached.pug b/services/web/app/views/project/ide-react-detached.pug index 4c9a633b5f..8109da7f74 100644 --- a/services/web/app/views/project/ide-react-detached.pug +++ b/services/web/app/views/project/ide-react-detached.pug @@ -8,7 +8,6 @@ block vars - var suppressFooter = true - var suppressSkipToContent = true - var suppressCookieBanner = true - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - metadata.robotsNoindexNofollow = true block content diff --git a/services/web/app/views/project/ide-react.pug b/services/web/app/views/project/ide-react.pug index a16345bbe9..bc30f69202 100644 --- a/services/web/app/views/project/ide-react.pug +++ b/services/web/app/views/project/ide-react.pug @@ -5,7 +5,6 @@ block vars - var suppressFooter = true - var suppressSkipToContent = true - var deferScripts = true - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - metadata.robotsNoindexNofollow = true - enableIeeeBranding = false diff --git a/services/web/app/views/project/list-react.pug b/services/web/app/views/project/list-react.pug index d24ce08449..be9233ecbb 100644 --- a/services/web/app/views/project/list-react.pug +++ b/services/web/app/views/project/list-react.pug @@ -7,7 +7,6 @@ block vars - const suppressNavContentLinks = true - const suppressNavbar = true - const suppressFooter = true - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' block append meta meta(name="ol-usersBestSubscription" data-type="json" content=usersBestSubscription) diff --git a/services/web/app/views/project/token/access-react.pug b/services/web/app/views/project/token/access-react.pug index 734ffa053f..83e9f79b61 100644 --- a/services/web/app/views/project/token/access-react.pug +++ b/services/web/app/views/project/token/access-react.pug @@ -7,7 +7,6 @@ block vars - var suppressFooter = true - var suppressCookieBanner = true - var suppressSkipToContent = true - - bootstrap5PageStatus = 'enabled' block append meta meta(name="ol-postUrl" data-type="string" content=postUrl) diff --git a/services/web/app/views/project/token/sharing-updates.pug b/services/web/app/views/project/token/sharing-updates.pug index 61cf8b5e6a..a0afb0c621 100644 --- a/services/web/app/views/project/token/sharing-updates.pug +++ b/services/web/app/views/project/token/sharing-updates.pug @@ -7,7 +7,6 @@ block vars - var suppressFooter = true - var suppressCookieBanner = true - var suppressSkipToContent = true - - bootstrap5PageStatus = 'enabled' block append meta meta(name="ol-project_id" data-type="string" content=projectId) diff --git a/services/web/app/views/subscriptions/add-seats.pug b/services/web/app/views/subscriptions/add-seats.pug index 9dd92da454..fb04ef91fb 100644 --- a/services/web/app/views/subscriptions/add-seats.pug +++ b/services/web/app/views/subscriptions/add-seats.pug @@ -1,8 +1,5 @@ extends ../layout-marketing -block vars - - bootstrap5PageStatus = 'enabled' // Enforce BS5 version - block entrypointVar - entrypoint = 'pages/user/subscription/group-management/add-seats' diff --git a/services/web/app/views/subscriptions/canceled-subscription-react.pug b/services/web/app/views/subscriptions/canceled-subscription-react.pug index 653f75c481..3a89234fd9 100644 --- a/services/web/app/views/subscriptions/canceled-subscription-react.pug +++ b/services/web/app/views/subscriptions/canceled-subscription-react.pug @@ -1,8 +1,5 @@ extends ../layout-react -block vars - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - block entrypointVar - entrypoint = 'pages/user/subscription/canceled-subscription' diff --git a/services/web/app/views/subscriptions/dashboard-react.pug b/services/web/app/views/subscriptions/dashboard-react.pug index 388f60c864..aa3e597dc5 100644 --- a/services/web/app/views/subscriptions/dashboard-react.pug +++ b/services/web/app/views/subscriptions/dashboard-react.pug @@ -1,8 +1,5 @@ extends ../layout-react -block vars - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - block entrypointVar - entrypoint = 'pages/user/subscription/dashboard' diff --git a/services/web/app/views/subscriptions/manually-collected-subscription.pug b/services/web/app/views/subscriptions/manually-collected-subscription.pug index b0e1db986b..1555ac2ea1 100644 --- a/services/web/app/views/subscriptions/manually-collected-subscription.pug +++ b/services/web/app/views/subscriptions/manually-collected-subscription.pug @@ -1,8 +1,5 @@ extends ../layout-marketing -block vars - - bootstrap5PageStatus = 'enabled' // Enforce BS5 version - block entrypointVar - entrypoint = 'pages/user/subscription/group-management/manually-collected-subscription' diff --git a/services/web/app/views/subscriptions/missing-billing-information.pug b/services/web/app/views/subscriptions/missing-billing-information.pug index ddb2ac1693..67d13f8e89 100644 --- a/services/web/app/views/subscriptions/missing-billing-information.pug +++ b/services/web/app/views/subscriptions/missing-billing-information.pug @@ -1,8 +1,5 @@ extends ../layout-marketing -block vars - - bootstrap5PageStatus = 'enabled' // Enforce BS5 version - block entrypointVar - entrypoint = 'pages/user/subscription/group-management/missing-billing-information' diff --git a/services/web/app/views/subscriptions/preview-change.pug b/services/web/app/views/subscriptions/preview-change.pug index 320d4d7bdc..ab70d2d6b6 100644 --- a/services/web/app/views/subscriptions/preview-change.pug +++ b/services/web/app/views/subscriptions/preview-change.pug @@ -1,8 +1,5 @@ extends ../layout-marketing -block vars - - bootstrap5PageStatus = 'enabled' - block entrypointVar - entrypoint = 'pages/user/subscription/preview-change' diff --git a/services/web/app/views/subscriptions/subtotal-limit-exceeded.pug b/services/web/app/views/subscriptions/subtotal-limit-exceeded.pug index bdbad595ee..15f79488fa 100644 --- a/services/web/app/views/subscriptions/subtotal-limit-exceeded.pug +++ b/services/web/app/views/subscriptions/subtotal-limit-exceeded.pug @@ -1,8 +1,5 @@ extends ../layout-marketing -block vars - - bootstrap5PageStatus = 'enabled' // Enforce BS5 version - block entrypointVar - entrypoint = 'pages/user/subscription/group-management/subtotal-limit-exceeded' diff --git a/services/web/app/views/subscriptions/successful-subscription-react.pug b/services/web/app/views/subscriptions/successful-subscription-react.pug index e7599e816a..5ce208b034 100644 --- a/services/web/app/views/subscriptions/successful-subscription-react.pug +++ b/services/web/app/views/subscriptions/successful-subscription-react.pug @@ -1,8 +1,5 @@ extends ../layout-react -block vars - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - block entrypointVar - entrypoint = 'pages/user/subscription/successful-subscription' diff --git a/services/web/app/views/subscriptions/team/group-invites.pug b/services/web/app/views/subscriptions/team/group-invites.pug index cef44909eb..81c70f1885 100644 --- a/services/web/app/views/subscriptions/team/group-invites.pug +++ b/services/web/app/views/subscriptions/team/group-invites.pug @@ -1,8 +1,5 @@ extends ../../layout-react -block vars - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - block entrypointVar - entrypoint = 'pages/user/subscription/group-invites' diff --git a/services/web/app/views/subscriptions/team/invite-managed.pug b/services/web/app/views/subscriptions/team/invite-managed.pug index 78599efb91..f59b8b4937 100644 --- a/services/web/app/views/subscriptions/team/invite-managed.pug +++ b/services/web/app/views/subscriptions/team/invite-managed.pug @@ -1,8 +1,5 @@ extends ../../layout-react -block vars - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - block entrypointVar - entrypoint = 'pages/user/subscription/invite-managed' diff --git a/services/web/app/views/subscriptions/team/invite.pug b/services/web/app/views/subscriptions/team/invite.pug index 75fd131ae2..dc1b509cbf 100644 --- a/services/web/app/views/subscriptions/team/invite.pug +++ b/services/web/app/views/subscriptions/team/invite.pug @@ -1,8 +1,5 @@ extends ../../layout-react -block vars - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - block entrypointVar - entrypoint = 'pages/user/subscription/invite' diff --git a/services/web/app/views/subscriptions/team/invite_logged_out.pug b/services/web/app/views/subscriptions/team/invite_logged_out.pug index e994a6ce24..d07fa5368c 100644 --- a/services/web/app/views/subscriptions/team/invite_logged_out.pug +++ b/services/web/app/views/subscriptions/team/invite_logged_out.pug @@ -1,8 +1,5 @@ extends ../../layout-react -block vars - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - block append meta meta(name="ol-user" data-type="json" content=user) diff --git a/services/web/app/views/subscriptions/upgrade-group-subscription-react.pug b/services/web/app/views/subscriptions/upgrade-group-subscription-react.pug index 4ac2999ee5..c482629463 100644 --- a/services/web/app/views/subscriptions/upgrade-group-subscription-react.pug +++ b/services/web/app/views/subscriptions/upgrade-group-subscription-react.pug @@ -1,8 +1,5 @@ extends ../layout-marketing -block vars - - bootstrap5PageStatus = 'enabled' // Enforce BS5 version - block entrypointVar - entrypoint = 'pages/user/subscription/group-management/upgrade-group-subscription' diff --git a/services/web/app/views/user/addSecondaryEmail.pug b/services/web/app/views/user/addSecondaryEmail.pug index 68c506c977..8f38df96aa 100644 --- a/services/web/app/views/user/addSecondaryEmail.pug +++ b/services/web/app/views/user/addSecondaryEmail.pug @@ -3,7 +3,6 @@ extends ../layout-react block vars - var suppressNavbar = true - var suppressSkipToContent = true - - bootstrap5PageStatus = 'enabled' block entrypointVar - entrypoint = 'pages/user/add-secondary-email' diff --git a/services/web/app/views/user/compromised_password.pug b/services/web/app/views/user/compromised_password.pug index a6d83aeb92..e56ffd9841 100644 --- a/services/web/app/views/user/compromised_password.pug +++ b/services/web/app/views/user/compromised_password.pug @@ -4,7 +4,6 @@ block vars - var suppressNavbar = true - var suppressFooter = true - var suppressGoogleAnalytics = true - - bootstrap5PageStatus = 'enabled' block entrypointVar - entrypoint = 'pages/compromised-password' diff --git a/services/web/app/views/user/confirmSecondaryEmail.pug b/services/web/app/views/user/confirmSecondaryEmail.pug index 95927f2f05..4d0c59e9db 100644 --- a/services/web/app/views/user/confirmSecondaryEmail.pug +++ b/services/web/app/views/user/confirmSecondaryEmail.pug @@ -3,7 +3,6 @@ extends ../layout-marketing block vars - var suppressNavbar = true - var suppressSkipToContent = true - - bootstrap5PageStatus = 'enabled' block entrypointVar - entrypoint = 'pages/user/confirm-secondary-email' diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 45d6ebeb90..57eef44c27 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -4,7 +4,6 @@ block entrypointVar - entrypoint = 'pages/user/settings' block vars - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - isWebsiteRedesign = true block append meta diff --git a/services/web/app/views/user_membership/group-managers-react.pug b/services/web/app/views/user_membership/group-managers-react.pug index ed2f3454db..f4d8c0e973 100644 --- a/services/web/app/views/user_membership/group-managers-react.pug +++ b/services/web/app/views/user_membership/group-managers-react.pug @@ -3,9 +3,6 @@ extends ../layout-marketing block entrypointVar - entrypoint = 'pages/user/subscription/group-management/group-managers' -block vars - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - block append meta meta(name="ol-users", data-type="json", content=users) meta(name="ol-groupId", data-type="string", content=groupId) diff --git a/services/web/app/views/user_membership/group-members-react.pug b/services/web/app/views/user_membership/group-members-react.pug index b146d0d143..314a332489 100644 --- a/services/web/app/views/user_membership/group-members-react.pug +++ b/services/web/app/views/user_membership/group-members-react.pug @@ -3,9 +3,6 @@ extends ../layout-marketing block entrypointVar - entrypoint = 'pages/user/subscription/group-management/group-members' -block vars - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - block append meta meta(name="ol-users", data-type="json", content=users) meta(name="ol-groupId", data-type="string", content=groupId) diff --git a/services/web/app/views/user_membership/institution-managers-react.pug b/services/web/app/views/user_membership/institution-managers-react.pug index dfe59364dc..690e8409f2 100644 --- a/services/web/app/views/user_membership/institution-managers-react.pug +++ b/services/web/app/views/user_membership/institution-managers-react.pug @@ -3,9 +3,6 @@ extends ../layout-marketing block entrypointVar - entrypoint = 'pages/user/subscription/group-management/institution-managers' -block vars - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - block append meta meta(name="ol-users", data-type="json", content=users) meta(name="ol-groupId", data-type="string", content=groupId) diff --git a/services/web/app/views/user_membership/publisher-managers-react.pug b/services/web/app/views/user_membership/publisher-managers-react.pug index 84bcbea80c..793bdf9602 100644 --- a/services/web/app/views/user_membership/publisher-managers-react.pug +++ b/services/web/app/views/user_membership/publisher-managers-react.pug @@ -3,9 +3,6 @@ extends ../layout-marketing block entrypointVar - entrypoint = 'pages/user/subscription/group-management/publisher-managers' -block vars - - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' - block append meta meta(name="ol-users", data-type="json", content=users) meta(name="ol-groupId", data-type="string", content=groupId) From bfd9ab6b8f465a117e164499af63e6e9749642da Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <5454374+emcsween@users.noreply.github.com> Date: Mon, 7 Apr 2025 10:05:56 -0400 Subject: [PATCH 030/978] Merge pull request #24604 from overleaf/em-docstore-errors Downgrade 4xx errors in docstore GitOrigin-RevId: ec6c73b4222876e6f58690779571e2e42106c36b --- services/docstore/app.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/docstore/app.js b/services/docstore/app.js index b4a26fc24a..76659e8411 100644 --- a/services/docstore/app.js +++ b/services/docstore/app.js @@ -88,14 +88,17 @@ app.get('/status', (req, res) => res.send('docstore is alive')) app.use(handleValidationErrors()) app.use(function (error, req, res, next) { - logger.error({ err: error, req }, 'request errored') if (error instanceof Errors.NotFoundError) { + logger.warn({ req }, 'not found') res.sendStatus(404) } else if (error instanceof Errors.DocModifiedError) { + logger.warn({ req }, 'conflict: doc modified') res.sendStatus(409) } else if (error instanceof Errors.DocVersionDecrementedError) { + logger.warn({ req }, 'conflict: doc version decremented') res.sendStatus(409) } else { + logger.error({ err: error, req }, 'request errored') res.status(500).send('Oops, something went wrong') } }) From b41f8164b8d48ee3ff52b75abe06791a347896bb Mon Sep 17 00:00:00 2001 From: Tim Down <158919+timdown@users.noreply.github.com> Date: Tue, 8 Apr 2025 08:46:56 +0100 Subject: [PATCH 031/978] Merge pull request #24709 from overleaf/td-prevent-spellcheck-after-destroy Prevent spell checks after spell checker is destroyed GitOrigin-RevId: 070f6c6ed05063e46960dad8099d61f585d6120c --- .../extensions/spelling/spellchecker.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/services/web/frontend/js/features/source-editor/extensions/spelling/spellchecker.ts b/services/web/frontend/js/features/source-editor/extensions/spelling/spellchecker.ts index eecccf5787..84054a8183 100644 --- a/services/web/frontend/js/features/source-editor/extensions/spelling/spellchecker.ts +++ b/services/web/frontend/js/features/source-editor/extensions/spelling/spellchecker.ts @@ -21,6 +21,7 @@ export class SpellChecker { private waitingForParser = false private firstCheckPending = false private trackedChanges: ChangeSet + private destroyed = false private readonly segmenter?: Intl.Segmenter // eslint-disable-next-line no-useless-constructor @@ -60,6 +61,7 @@ export class SpellChecker { destroy() { this._clearPendingSpellCheck() + this.destroyed = true } _abortRequest() { @@ -260,10 +262,22 @@ export class SpellChecker { } spellCheckAsap(view: EditorView) { + if (this.destroyed) { + debugConsole.warn( + 'spellCheckAsap called after spellchecker was destroyed. Ignoring.' + ) + return + } this._asyncSpellCheck(view, 0) } scheduleSpellCheck(view: EditorView) { + if (this.destroyed) { + debugConsole.warn( + 'scheduleSpellCheck called after spellchecker was destroyed. Ignoring.' + ) + return + } this._asyncSpellCheck(view, 1000) } From 44926d35199fc642e88b4b9a6e8a1079fb3018d8 Mon Sep 17 00:00:00 2001 From: Miguel Serrano Date: Tue, 8 Apr 2025 14:01:23 +0200 Subject: [PATCH 032/978] Merge pull request #24721 from overleaf/msm-cleanup-git-oauth-secret [git-bridge] Cleanup `oauth` clientID/secret GitOrigin-RevId: 48144d928119782d1c7b048b0cb6a4afb6072f28 --- services/git-bridge/README.md | 10 +++--- .../git-bridge/conf/envsubst_template.json | 6 +--- services/git-bridge/conf/example_config.json | 6 +--- services/git-bridge/conf/local.json | 6 +--- .../application/config/Config.java | 21 ++++-------- .../application/config/Oauth2.java | 33 ------------------- .../wlgitbridge/server/GitBridgeServer.java | 4 +-- .../ic/wlgitbridge/server/Oauth2Filter.java | 14 ++++---- .../WLGitBridgeIntegrationTest.java | 9 ++--- .../application/config/ConfigTest.java | 28 ++++------------ 10 files changed, 30 insertions(+), 107 deletions(-) delete mode 100644 services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/config/Oauth2.java diff --git a/services/git-bridge/README.md b/services/git-bridge/README.md index 13b24cc6d0..eadc2abc4f 100644 --- a/services/git-bridge/README.md +++ b/services/git-bridge/README.md @@ -76,12 +76,10 @@ The configuration file is in `.json` format. "postbackBaseUrl" (string): the postback url, "serviceName" (string): current name of writeLaTeX in case it ever changes, - "oauth2" (object): { null or missing if oauth2 shouldn't be used - "oauth2ClientID" (string): oauth2 client ID, - "oauth2ClientSecret" (string): oauth2 client secret, - "oauth2Server" (string): oauth2 server, - with protocol and - without trailing slash + "oauth2Server" (string): oauth2 server, + with protocol and + without trailing slash, + null or missing if oauth2 shouldn't be used }, "repoStore" (object, optional): { configure the repo store "maxFileSize" (long, optional): maximum size of a file, inclusive diff --git a/services/git-bridge/conf/envsubst_template.json b/services/git-bridge/conf/envsubst_template.json index 6aa91be700..4ede5bab7f 100644 --- a/services/git-bridge/conf/envsubst_template.json +++ b/services/git-bridge/conf/envsubst_template.json @@ -7,11 +7,7 @@ "apiBaseUrl": "${GIT_BRIDGE_API_BASE_URL:-https://localhost/api/v0}", "postbackBaseUrl": "${GIT_BRIDGE_POSTBACK_BASE_URL:-https://localhost}", "serviceName": "${GIT_BRIDGE_SERVICE_NAME:-Overleaf}", - "oauth2": { - "oauth2ClientID": "${GIT_BRIDGE_OAUTH2_CLIENT_ID}", - "oauth2ClientSecret": "${GIT_BRIDGE_OAUTH2_CLIENT_SECRET}", - "oauth2Server": "${GIT_BRIDGE_OAUTH2_SERVER:-https://localhost}" - }, + "oauth2Server": "${GIT_BRIDGE_OAUTH2_SERVER:-https://localhost}", "userPasswordEnabled": ${GIT_BRIDGE_USER_PASSWORD_ENABLED:-false}, "repoStore": { "maxFileNum": ${GIT_BRIDGE_REPOSTORE_MAX_FILE_NUM:-2000}, diff --git a/services/git-bridge/conf/example_config.json b/services/git-bridge/conf/example_config.json index 1e5b95e5a6..76b82eb6a0 100644 --- a/services/git-bridge/conf/example_config.json +++ b/services/git-bridge/conf/example_config.json @@ -7,11 +7,7 @@ "apiBaseUrl": "https://localhost/api/v0", "postbackBaseUrl": "https://localhost", "serviceName": "Overleaf", - "oauth2": { - "oauth2ClientID": "asdf", - "oauth2ClientSecret": "asdf", - "oauth2Server": "https://localhost" - }, + "oauth2Server": "https://localhost", "repoStore": { "maxFileNum": 2000, "maxFileSize": 52428800 diff --git a/services/git-bridge/conf/local.json b/services/git-bridge/conf/local.json index 69eb31ab2f..c4de48d819 100644 --- a/services/git-bridge/conf/local.json +++ b/services/git-bridge/conf/local.json @@ -7,11 +7,7 @@ "apiBaseUrl": "http://v2.overleaf.test:3000/api/v0", "postbackBaseUrl": "http://git-bridge:8000", "serviceName": "Overleaf", - "oauth2": { - "oauth2ClientID": "264c723c925c13590880751f861f13084934030c13b4452901e73bdfab226edc", - "oauth2ClientSecret": "e6b2e9eee7ae2bb653823250bb69594a91db0547fe3790a7135acb497108e62d", - "oauth2Server": "http://v2.overleaf.test:3000" - }, + "oauth2Server": "http://v2.overleaf.test:3000", "repoStore": { "maxFileNum": 2000, "maxFileSize": 52428800 diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/config/Config.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/config/Config.java index 492302721b..d5b530100e 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/config/Config.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/config/Config.java @@ -30,7 +30,7 @@ public class Config implements JSONSource { config.apiBaseURL, config.postbackURL, config.serviceName, - Oauth2.asSanitised(config.oauth2), + config.oauth2Server, config.userPasswordEnabled, config.repoStore, SwapStoreConfig.sanitisedCopy(config.swapStore), @@ -46,7 +46,7 @@ public class Config implements JSONSource { private String apiBaseURL; private String postbackURL; private String serviceName; - @Nullable private Oauth2 oauth2; + @Nullable private String oauth2Server; private boolean userPasswordEnabled; @Nullable private RepoStoreConfig repoStore; @Nullable private SwapStoreConfig swapStore; @@ -70,7 +70,7 @@ public class Config implements JSONSource { String apiBaseURL, String postbackURL, String serviceName, - Oauth2 oauth2, + String oauth2Server, boolean userPasswordEnabled, RepoStoreConfig repoStore, SwapStoreConfig swapStore, @@ -84,7 +84,7 @@ public class Config implements JSONSource { this.apiBaseURL = apiBaseURL; this.postbackURL = postbackURL; this.serviceName = serviceName; - this.oauth2 = oauth2; + this.oauth2Server = oauth2Server; this.userPasswordEnabled = userPasswordEnabled; this.repoStore = repoStore; this.swapStore = swapStore; @@ -116,7 +116,7 @@ public class Config implements JSONSource { if (!postbackURL.endsWith("/")) { postbackURL += "/"; } - oauth2 = new Gson().fromJson(configObject.get("oauth2"), Oauth2.class); + oauth2Server = getOptionalString(configObject, "oauth2Server"); userPasswordEnabled = getOptionalString(configObject, "userPasswordEnabled").equals("true"); repoStore = new Gson().fromJson(configObject.get("repoStore"), RepoStoreConfig.class); swapStore = new Gson().fromJson(configObject.get("swapStore"), SwapStoreConfig.class); @@ -166,19 +166,12 @@ public class Config implements JSONSource { return postbackURL; } - public boolean isUsingOauth2() { - return oauth2 != null; - } - public boolean isUserPasswordEnabled() { return userPasswordEnabled; } - public Oauth2 getOauth2() { - if (!isUsingOauth2()) { - throw new AssertionError("Getting oauth2 when not using it"); - } - return oauth2; + public String getOauth2Server() { + return oauth2Server; } public Optional getRepoStore() { diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/config/Oauth2.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/config/Oauth2.java deleted file mode 100644 index 1db7d3b4d2..0000000000 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/application/config/Oauth2.java +++ /dev/null @@ -1,33 +0,0 @@ -package uk.ac.ic.wlgitbridge.application.config; - -/* - * Created by winston on 25/10/15. - */ -public class Oauth2 { - - private final String oauth2ClientID; - private final String oauth2ClientSecret; - private final String oauth2Server; - - public Oauth2(String oauth2ClientID, String oauth2ClientSecret, String oauth2Server) { - this.oauth2ClientID = oauth2ClientID; - this.oauth2ClientSecret = oauth2ClientSecret; - this.oauth2Server = oauth2Server; - } - - public String getOauth2ClientID() { - return oauth2ClientID; - } - - public String getOauth2ClientSecret() { - return oauth2ClientSecret; - } - - public String getOauth2Server() { - return oauth2Server; - } - - public static Oauth2 asSanitised(Oauth2 oauth2) { - return new Oauth2("", "", oauth2.oauth2Server); - } -} diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/GitBridgeServer.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/GitBridgeServer.java index c576e2e9d8..57d1b34a7b 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/GitBridgeServer.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/GitBridgeServer.java @@ -151,9 +151,9 @@ public class GitBridgeServer { throws ServletException { final ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); - if (config.isUsingOauth2()) { + if (config.getOauth2Server() != null) { Filter filter = - new Oauth2Filter(snapshotApi, config.getOauth2(), config.isUserPasswordEnabled()); + new Oauth2Filter(snapshotApi, config.getOauth2Server(), config.isUserPasswordEnabled()); servletContextHandler.addFilter( new FilterHolder(filter), "/*", EnumSet.of(DispatcherType.REQUEST)); } diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/Oauth2Filter.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/Oauth2Filter.java index 5bd3904e47..586a21ab3f 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/Oauth2Filter.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/Oauth2Filter.java @@ -13,7 +13,6 @@ import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.codec.binary.Base64; -import uk.ac.ic.wlgitbridge.application.config.Oauth2; import uk.ac.ic.wlgitbridge.bridge.snapshot.SnapshotApi; import uk.ac.ic.wlgitbridge.util.Instance; import uk.ac.ic.wlgitbridge.util.Log; @@ -28,13 +27,13 @@ public class Oauth2Filter implements Filter { private final SnapshotApi snapshotApi; - private final Oauth2 oauth2; + private final String oauth2Server; private final boolean isUserPasswordEnabled; - public Oauth2Filter(SnapshotApi snapshotApi, Oauth2 oauth2, boolean isUserPasswordEnabled) { + public Oauth2Filter(SnapshotApi snapshotApi, String oauth2Server, boolean isUserPasswordEnabled) { this.snapshotApi = snapshotApi; - this.oauth2 = oauth2; + this.oauth2Server = oauth2Server; this.isUserPasswordEnabled = isUserPasswordEnabled; } @@ -108,7 +107,7 @@ public class Oauth2Filter implements Filter { // fail later (for example, in the unlikely event that the token // expired between the two requests). In that case, JGit will // return a 401 without a custom error message. - int statusCode = checkAccessToken(oauth2, password, getClientIp(request)); + int statusCode = checkAccessToken(this.oauth2Server, password, getClientIp(request)); if (statusCode == 429) { handleRateLimit(projectId, username, request, response); return; @@ -238,10 +237,9 @@ public class Oauth2Filter implements Filter { "your Overleaf Account Settings.")); } - private int checkAccessToken(Oauth2 oauth2, String accessToken, String clientIp) + private int checkAccessToken(String oauth2Server, String accessToken, String clientIp) throws IOException { - GenericUrl url = - new GenericUrl(oauth2.getOauth2Server() + "/oauth/token/info?client_ip=" + clientIp); + GenericUrl url = new GenericUrl(oauth2Server + "/oauth/token/info?client_ip=" + clientIp); HttpRequest request = Instance.httpRequestFactory.buildGetRequest(url); HttpHeaders headers = new HttpHeaders(); headers.setAuthorization("Bearer " + accessToken); diff --git a/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/WLGitBridgeIntegrationTest.java b/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/WLGitBridgeIntegrationTest.java index 8491aa8055..f706d98edf 100644 --- a/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/WLGitBridgeIntegrationTest.java +++ b/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/WLGitBridgeIntegrationTest.java @@ -1495,13 +1495,9 @@ public class WLGitBridgeIntegrationTest { + port + "\",\n" + " \"serviceName\": \"Overleaf\",\n" - + " \"oauth2\": {\n" - + " \"oauth2ClientID\": \"clientID\",\n" - + " \"oauth2ClientSecret\": \"oauth2 client secret\",\n" - + " \"oauth2Server\": \"http://127.0.0.1:" + + " \"oauth2Server\": \"http://127.0.0.1:" + apiPort - + "\"\n" - + " }"; + + "\""; if (swapCfg != null) { cfgStr += ",\n" @@ -1524,7 +1520,6 @@ public class WLGitBridgeIntegrationTest { + ",\n" + " \"intervalMillis\": " + swapCfg.getIntervalMillis() - + "\n" + " }\n"; } cfgStr += "}\n"; diff --git a/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/config/ConfigTest.java b/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/config/ConfigTest.java index cbb4265d5b..8c102dbda3 100644 --- a/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/config/ConfigTest.java +++ b/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/config/ConfigTest.java @@ -23,11 +23,7 @@ public class ConfigTest { + " \"apiBaseUrl\": \"http://127.0.0.1:60000/api/v0\",\n" + " \"postbackBaseUrl\": \"http://127.0.0.1\",\n" + " \"serviceName\": \"Overleaf\",\n" - + " \"oauth2\": {\n" - + " \"oauth2ClientID\": \"clientID\",\n" - + " \"oauth2ClientSecret\": \"oauth2 client secret\",\n" - + " \"oauth2Server\": \"https://www.overleaf.com\"\n" - + " }\n" + + " \"oauth2Server\": \"https://www.overleaf.com\"\n" + "}\n"); Config config = new Config(reader); assertEquals(80, config.getPort()); @@ -35,10 +31,7 @@ public class ConfigTest { assertEquals("http://127.0.0.1:60000/api/v0/", config.getAPIBaseURL()); assertEquals("http://127.0.0.1/", config.getPostbackURL()); assertEquals("Overleaf", config.getServiceName()); - assertTrue(config.isUsingOauth2()); - assertEquals("clientID", config.getOauth2().getOauth2ClientID()); - assertEquals("oauth2 client secret", config.getOauth2().getOauth2ClientSecret()); - assertEquals("https://www.overleaf.com", config.getOauth2().getOauth2Server()); + assertEquals("https://www.overleaf.com", config.getOauth2Server()); } @Test(expected = AssertionError.class) @@ -53,7 +46,7 @@ public class ConfigTest { + " \"apiBaseUrl\": \"http://127.0.0.1:60000/api/v0\",\n" + " \"postbackBaseUrl\": \"http://127.0.0.1\",\n" + " \"serviceName\": \"Overleaf\",\n" - + " \"oauth2\": null\n" + + " \"oauth2Server\": null\n" + "}\n"); Config config = new Config(reader); assertEquals(80, config.getPort()); @@ -61,8 +54,7 @@ public class ConfigTest { assertEquals("http://127.0.0.1:60000/api/v0/", config.getAPIBaseURL()); assertEquals("http://127.0.0.1/", config.getPostbackURL()); assertEquals("Overleaf", config.getServiceName()); - assertFalse(config.isUsingOauth2()); - config.getOauth2(); + assertNull(config.getOauth2Server()); } @Test @@ -77,11 +69,7 @@ public class ConfigTest { + " \"apiBaseUrl\": \"http://127.0.0.1:60000/api/v0\",\n" + " \"postbackBaseUrl\": \"http://127.0.0.1\",\n" + " \"serviceName\": \"Overleaf\",\n" - + " \"oauth2\": {\n" - + " \"oauth2ClientID\": \"my oauth2 client id\",\n" - + " \"oauth2ClientSecret\": \"my oauth2 client secret\",\n" - + " \"oauth2Server\": \"https://www.overleaf.com\"\n" - + " }\n" + + " \"oauth2Server\": \"https://www.overleaf.com\"\n" + "}\n"); Config config = new Config(reader); String expected = @@ -94,11 +82,7 @@ public class ConfigTest { + " \"apiBaseURL\": \"http://127.0.0.1:60000/api/v0/\",\n" + " \"postbackURL\": \"http://127.0.0.1/\",\n" + " \"serviceName\": \"Overleaf\",\n" - + " \"oauth2\": {\n" - + " \"oauth2ClientID\": \"\",\n" - + " \"oauth2ClientSecret\": \"\",\n" - + " \"oauth2Server\": \"https://www.overleaf.com\"\n" - + " },\n" + + " \"oauth2Server\": \"https://www.overleaf.com\",\n" + " \"userPasswordEnabled\": false,\n" + " \"repoStore\": null,\n" + " \"swapStore\": null,\n" From 4e192f760da30b647865c6499d97d1232a494305 Mon Sep 17 00:00:00 2001 From: M Fahru Date: Tue, 8 Apr 2025 05:44:11 -0700 Subject: [PATCH 033/978] Merge pull request #24458 from overleaf/mf-update-rocket-yellow-to-be-bigger [web] Update rocket-yellow sticker size GitOrigin-RevId: 0e5b39610687eff3d2ce51e48da2e7829f26f574 --- .../img/website-redesign/stickers/rocket-yellow.svg | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/web/public/img/website-redesign/stickers/rocket-yellow.svg b/services/web/public/img/website-redesign/stickers/rocket-yellow.svg index e544a00f33..db00ebda6f 100644 --- a/services/web/public/img/website-redesign/stickers/rocket-yellow.svg +++ b/services/web/public/img/website-redesign/stickers/rocket-yellow.svg @@ -1,6 +1,6 @@ - - - - - + + + + + From 620edfa34725fed89eacc70bc4bf9edd0351672e Mon Sep 17 00:00:00 2001 From: M Fahru Date: Tue, 8 Apr 2025 05:44:24 -0700 Subject: [PATCH 034/978] Merge pull request #24632 from overleaf/mf-tear-down-group-tab-improvements-split-test [web] Tear down `group-tab-improvements` split test and apply the `default` variant GitOrigin-RevId: c2fe07d0b4338f85b053637d85a05bbcbcce74ea --- services/web/frontend/stylesheets/app/plans.less | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/services/web/frontend/stylesheets/app/plans.less b/services/web/frontend/stylesheets/app/plans.less index 8811c59f6e..52ab87d629 100644 --- a/services/web/frontend/stylesheets/app/plans.less +++ b/services/web/frontend/stylesheets/app/plans.less @@ -470,18 +470,6 @@ line-height: var(--line-height-02); } - .plans-new-root-group-member-picker { - margin: var(--spacing-10) auto 0 auto; - - @media (min-width: @screen-sm-min) { - width: @group-member-picker-sm-width; - } - @media (min-width: @screen-md-min) { - width: @group-member-picker-md-width; - min-width: @group-member-picker-min-width; // ensure checkbox title doesn't wrap in English - } - } - .plans-new-group-member-picker { .plans-new-group-member-picker-text { font-size: var(--font-size-02); From 73e141a4a3ab942cf97e19c1ad60d8c563504361 Mon Sep 17 00:00:00 2001 From: M Fahru Date: Tue, 8 Apr 2025 05:44:38 -0700 Subject: [PATCH 035/978] Merge pull request #24635 from overleaf/mf-tear-down-period-toggle-improvements-test [web] Tear down `period-toggle-improvements` split test and apply the `default` variant GitOrigin-RevId: 154a291437afc6e4b1c87eef91e6f05ae5a454c3 --- .../web/frontend/stylesheets/app/plans.less | 78 ------------------- services/web/locales/en.json | 1 - 2 files changed, 79 deletions(-) diff --git a/services/web/frontend/stylesheets/app/plans.less b/services/web/frontend/stylesheets/app/plans.less index 52ab87d629..391a752436 100644 --- a/services/web/frontend/stylesheets/app/plans.less +++ b/services/web/frontend/stylesheets/app/plans.less @@ -22,10 +22,6 @@ @table-4-column-width: 25%; @table-5-column-width: 20%; -@plans-m-a-new-switch-height: 20px; - -@plans-m-a-switch-padding: 2px; - .plans-new-design { padding-top: var(--header-height); @@ -1200,77 +1196,3 @@ } } } - -/// Toggle for monthly/annual toggle for period-toggle-improvements test -/// Based on toggle in light-touch-redesign -.monthly-annual-switch { - color: var(--neutral-90); - display: flex; - align-items: center; - justify-content: center; - position: relative; - margin-top: var(--spacing-09); - margin-bottom: var(--spacing-13); - - .plans-new-discount-label { - margin-left: var(--spacing-04); - color: var(--green-50); - } - - @media (max-width: @screen-xs-max) { - margin-top: var(--spacing-08); - margin-bottom: var(--spacing-08); - } - - label { - position: relative; - display: inline-block; - width: 34px; - height: @plans-m-a-new-switch-height; - margin-right: var(--spacing-05); - margin-bottom: 0; - - input { - opacity: 0; - width: 0; - height: 0; - } - - input + span { - background-color: var(--neutral-70); - } - - input:checked + span { - background-color: var(--green-50); - } - - input:checked + span::before { - transform: translateX(14px); - } - - span { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - transition: 0.4s; - border-radius: @plans-m-a-new-switch-height; - - &::before { - position: absolute; - content: ''; - height: @plans-m-a-new-switch-height - @plans-m-a-switch-padding - - @plans-m-a-switch-padding; - width: @plans-m-a-new-switch-height - @plans-m-a-switch-padding - - @plans-m-a-switch-padding; - left: @plans-m-a-switch-padding; - bottom: @plans-m-a-switch-padding; - background-color: @white; - transition: 0.4s; - border-radius: 50%; - } - } - } -} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index ab75c53871..8e76ffc335 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1925,7 +1925,6 @@ "select_from_your_computer": "select from your computer", "select_github_repository": "Select a GitHub repository to import into __appName__.", "select_image_from_project_files": "Select image from project files", - "select_monthly_plans": "Select for monthly plans", "select_project": "Select __project__", "select_projects": "Select Projects", "select_tag": "Select tag __tagName__", From aa723a70c2e747195eeadecf209a4b75369e0f91 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe <5312836+lawshe@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:15:44 -0500 Subject: [PATCH 036/978] Merge pull request #24580 from overleaf/jel-bs5-loading-labels [web] Add accessibility labels for processing view on BS5 group buttons GitOrigin-RevId: bb79d3b73eb187097d036bc5a6e307c4232f32d0 --- services/web/frontend/extracted-translations.json | 4 ++++ .../js/features/group-management/components/group-members.tsx | 1 + .../components/members-table/offboard-managed-user-modal.tsx | 1 + services/web/locales/en.json | 4 ++++ 4 files changed, 10 insertions(+) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index a12d81b5d5..345d0c15ab 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -393,6 +393,7 @@ "disable_single_sign_on": "", "disable_sso": "", "disable_stop_on_first_error": "", + "disabling": "", "disconnected": "", "discount": "", "discount_of": "", @@ -499,6 +500,7 @@ "enable_stop_on_first_error_under_recompile_dropdown_menu_v2": "", "enabled": "", "enables_real_time_syntax_checking_in_the_editor": "", + "enabling": "", "end_of_document": "", "ensure_recover_account": "", "enter_6_digit_code": "", @@ -817,6 +819,7 @@ "invite_resend_limit_hit": "", "invited_to_group": "", "invited_to_group_have_individual_subcription": "", + "inviting": "", "ip_address": "", "is_email_affiliated": "", "issued_on": "", @@ -1817,6 +1820,7 @@ "transfer_management_resolve_following_issues": "", "transfer_this_users_projects": "", "transfer_this_users_projects_description": "", + "transferring": "", "trash": "", "trash_projects": "", "trashed": "", diff --git a/services/web/frontend/js/features/group-management/components/group-members.tsx b/services/web/frontend/js/features/group-management/components/group-members.tsx index 366c1b7640..be4f84375c 100644 --- a/services/web/frontend/js/features/group-management/components/group-members.tsx +++ b/services/web/frontend/js/features/group-management/components/group-members.tsx @@ -157,6 +157,7 @@ export default function GroupMembers() { variant="primary" onClick={onAddMembersSubmit} isLoading={inviteMemberLoading} + loadingLabel={t('inviting')} > {t('invite')} diff --git a/services/web/frontend/js/features/group-management/components/members-table/offboard-managed-user-modal.tsx b/services/web/frontend/js/features/group-management/components/members-table/offboard-managed-user-modal.tsx index 4edf771d55..5bc234a789 100644 --- a/services/web/frontend/js/features/group-management/components/members-table/offboard-managed-user-modal.tsx +++ b/services/web/frontend/js/features/group-management/components/members-table/offboard-managed-user-modal.tsx @@ -142,6 +142,7 @@ export default function OffboardManagedUserModal({ type="submit" variant="danger" disabled={isLoading || isSuccess || !shouldEnableDeleteUserButton} + loadingLabel={t('deleting')} isLoading={isLoading} > {t('delete_user')} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 8e76ffc335..20601f6577 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -515,6 +515,7 @@ "disable_single_sign_on": "Disable single sign-on", "disable_sso": "Disable SSO", "disable_stop_on_first_error": "Disable “Stop on first error”", + "disabling": "Disabling", "disconnected": "Disconnected", "discount": "Discount", "discount_of": "Discount of __amount__", @@ -648,6 +649,7 @@ "enable_stop_on_first_error_under_recompile_dropdown_menu_v2": "Enable <0>Stop on first error under the <1>Recompile drop-down menu to help you find and fix errors right away.", "enabled": "Enabled", "enables_real_time_syntax_checking_in_the_editor": "Enables real-time syntax checking in the editor", + "enabling": "Enabling", "end_of_document": "End of document", "ensure_recover_account": "This will ensure that it can be used to recover your __appName__ account in case you lose access to your primary email address.", "enter_6_digit_code": "Enter 6-digit code", @@ -1070,6 +1072,7 @@ "invited_to_group_login_benefits": "As part of this group, you’ll have access to __appName__ premium features such as additional collaborators, greater maximum compile time, and real-time track changes.", "invited_to_group_register": "To accept __inviterName__’s invitation you’ll need to create an account.", "invited_to_group_register_benefits": "__appName__ is a collaborative online LaTeX editor, with thousands of ready-to-use templates and an array of LaTeX learning resources to help you get started.", + "inviting": "Inviting", "ip_address": "IP Address", "is_email_affiliated": "Is your email affiliated with an institution? ", "is_longer_than_n_characters": "is at least __n__ characters long", @@ -2347,6 +2350,7 @@ "transfer_management_resolve_following_issues": "To transfer the management of your account, you need to resolve the following issues:", "transfer_this_users_projects": "Transfer this user’s projects", "transfer_this_users_projects_description": "This user’s projects will be transferred to a new owner.", + "transferring": "Transferring", "trash": "Trash", "trash_projects": "Trash Projects", "trashed": "Trashed", From fc56d1690d69d8633dba36330a36662b8238321b Mon Sep 17 00:00:00 2001 From: Jessica Lawshe <5312836+lawshe@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:15:54 -0500 Subject: [PATCH 037/978] Merge pull request #24617 from overleaf/jel-fix-status-labels [web] Switch to `OLBadge` to fix style in BS5 on admin SSO config labels GitOrigin-RevId: eb3745b602f33e9bd5aea3704ec6f0d2904ee5b1 --- services/web/frontend/extracted-translations.json | 1 + .../stylesheets/bootstrap-5/pages/group-settings.scss | 4 ++++ services/web/locales/en.json | 1 + 3 files changed, 6 insertions(+) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 345d0c15ab..f0c492800c 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -393,6 +393,7 @@ "disable_single_sign_on": "", "disable_sso": "", "disable_stop_on_first_error": "", + "disabled": "", "disabling": "", "disconnected": "", "discount": "", diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/group-settings.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/group-settings.scss index fb9c7de0fa..ea0d060667 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/group-settings.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/group-settings.scss @@ -91,6 +91,10 @@ h4.group-settings-title { border-bottom: 1px solid var(--border-disabled); margin-bottom: var(--spacing-06); flex-wrap: wrap; + + .badge { + max-width: none; + } } .sso-config-options-buttons { diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 20601f6577..be05835080 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -515,6 +515,7 @@ "disable_single_sign_on": "Disable single sign-on", "disable_sso": "Disable SSO", "disable_stop_on_first_error": "Disable “Stop on first error”", + "disabled": "Disabled", "disabling": "Disabling", "disconnected": "Disconnected", "discount": "Discount", From dadf6f0ed44be196f6e9d57c3f2d1f2aa9e830c8 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Tue, 8 Apr 2025 16:50:43 +0100 Subject: [PATCH 038/978] [real-time] record metrics on number of connected clients per project (#24727) Note: "connected" here means across all real-time pods. - editing_session_mode, counter - mode=connect - a client connected - mode=disconnect - a client disconnect - mode=update - continuous editing - status=empty - all clients disconnected - status=single - a single client is connected - status=multi - multiple clients are connected - project_not_empty_since histogram with buckets [0,1h,2h,1d,2d,1w,30d] - status=empty/single/multi as described above GitOrigin-RevId: 1cc42e72bbb5aae754399bdbc3f8771642f35c22 --- .../real-time/app/js/ConnectedUsersManager.js | 70 +++++- .../real-time/config/settings.defaults.js | 3 + .../unit/js/ConnectedUsersManagerTests.js | 237 +++++++++++++++++- 3 files changed, 304 insertions(+), 6 deletions(-) diff --git a/services/real-time/app/js/ConnectedUsersManager.js b/services/real-time/app/js/ConnectedUsersManager.js index 299a4b870a..d0739c28d1 100644 --- a/services/real-time/app/js/ConnectedUsersManager.js +++ b/services/real-time/app/js/ConnectedUsersManager.js @@ -3,6 +3,7 @@ const Settings = require('@overleaf/settings') const logger = require('@overleaf/logger') const redis = require('@overleaf/redis-wrapper') const OError = require('@overleaf/o-error') +const Metrics = require('@overleaf/metrics') const rclient = redis.createClient(Settings.redis.realtime) const Keys = Settings.redis.realtime.key_schema @@ -13,6 +14,20 @@ const FOUR_DAYS_IN_S = ONE_DAY_IN_S * 4 const USER_TIMEOUT_IN_S = ONE_HOUR_IN_S / 4 const REFRESH_TIMEOUT_IN_S = 10 // only show clients which have responded to a refresh request in the last 10 seconds +function recordProjectNotEmptySinceMetric(res, status) { + const diff = Date.now() / 1000 - parseInt(res, 10) + const BUCKETS = [ + 0, + ONE_HOUR_IN_S, + 2 * ONE_HOUR_IN_S, + ONE_DAY_IN_S, + 2 * ONE_DAY_IN_S, + 7 * ONE_DAY_IN_S, + 30 * ONE_DAY_IN_S, + ] + Metrics.histogram('project_not_empty_since', diff, BUCKETS, { status }) +} + module.exports = { // Use the same method for when a user connects, and when a user sends a cursor // update. This way we don't care if the connected_user key has expired when @@ -23,6 +38,7 @@ module.exports = { const multi = rclient.multi() multi.sadd(Keys.clientsInProject({ project_id: projectId }), clientId) + multi.scard(Keys.clientsInProject({ project_id: projectId })) multi.expire( Keys.clientsInProject({ project_id: projectId }), FOUR_DAYS_IN_S @@ -66,10 +82,15 @@ module.exports = { USER_TIMEOUT_IN_S ) - multi.exec(function (err) { + multi.exec(function (err, res) { if (err) { err = new OError('problem marking user as connected').withCause(err) } + const [, nConnectedClients] = res + Metrics.inc('editing_session_mode', 1, { + mode: cursorData ? 'update' : 'connect', + status: nConnectedClients === 1 ? 'single' : 'multi', + }) callback(err) }) }, @@ -100,6 +121,7 @@ module.exports = { logger.debug({ projectId, clientId }, 'marking user as disconnected') const multi = rclient.multi() multi.srem(Keys.clientsInProject({ project_id: projectId }), clientId) + multi.scard(Keys.clientsInProject({ project_id: projectId })) multi.expire( Keys.clientsInProject({ project_id: projectId }), FOUR_DAYS_IN_S @@ -107,10 +129,54 @@ module.exports = { multi.del( Keys.connectedUser({ project_id: projectId, client_id: clientId }) ) - multi.exec(function (err) { + multi.exec(function (err, res) { if (err) { err = new OError('problem marking user as disconnected').withCause(err) } + const [, nConnectedClients] = res + const status = + nConnectedClients === 0 + ? 'empty' + : nConnectedClients === 1 + ? 'single' + : 'multi' + Metrics.inc('editing_session_mode', 1, { + mode: 'disconnect', + status, + }) + if (status === 'empty') { + rclient.getdel(Keys.projectNotEmptySince({ projectId }), (err, res) => { + if (err) { + logger.warn( + { err, projectId }, + 'could not collect projectNotEmptySince' + ) + } else if (res) { + recordProjectNotEmptySinceMetric(res, status) + } + }) + } else { + // Only populate projectNotEmptySince when more clients remain connected. + const nowInSeconds = Math.ceil(Date.now() / 1000).toString() + rclient.set( + Keys.projectNotEmptySince({ projectId }), + nowInSeconds, + 'NX', + 'GET', + 'EX', + 31 * ONE_DAY_IN_S, + (err, res) => { + if (err) { + logger.warn( + { err, projectId }, + 'could not set/collect projectNotEmptySince' + ) + } else if (res) { + recordProjectNotEmptySinceMetric(res, status) + } + } + ) + } callback(err) }) }, diff --git a/services/real-time/config/settings.defaults.js b/services/real-time/config/settings.defaults.js index 1cc0c0f107..96c116fb2e 100644 --- a/services/real-time/config/settings.defaults.js +++ b/services/real-time/config/settings.defaults.js @@ -38,6 +38,9 @@ const settings = { connectedUser({ project_id, client_id }) { return `connected_user:{${project_id}}:${client_id}` }, + projectNotEmptySince({ projectId }) { + return `projectNotEmptySince:{${projectId}}` + }, }, maxRetriesPerRequest: parseInt( process.env.REAL_TIME_REDIS_MAX_RETRIES_PER_REQUEST || diff --git a/services/real-time/test/unit/js/ConnectedUsersManagerTests.js b/services/real-time/test/unit/js/ConnectedUsersManagerTests.js index 9026d0bb42..17437ce7eb 100644 --- a/services/real-time/test/unit/js/ConnectedUsersManagerTests.js +++ b/services/real-time/test/unit/js/ConnectedUsersManagerTests.js @@ -30,12 +30,18 @@ describe('ConnectedUsersManager', function () { connectedUser({ project_id: projectId, client_id: clientId }) { return `connected_user:${projectId}:${clientId}` }, + projectNotEmptySince({ projectId }) { + return `projectNotEmptySince:{${projectId}}` + }, }, }, }, } this.rClient = { auth() {}, + getdel: sinon.stub(), + scard: sinon.stub(), + set: sinon.stub(), setex: sinon.stub(), sadd: sinon.stub(), get: sinon.stub(), @@ -51,10 +57,15 @@ describe('ConnectedUsersManager', function () { }, } tk.freeze(new Date()) + this.Metrics = { + inc: sinon.stub(), + histogram: sinon.stub(), + } this.ConnectedUsersManager = SandboxedModule.require(modulePath, { requires: { '@overleaf/settings': this.settings, + '@overleaf/metrics': this.Metrics, '@overleaf/redis-wrapper': { createClient: () => { return this.rClient @@ -83,7 +94,7 @@ describe('ConnectedUsersManager', function () { describe('updateUserPosition', function () { beforeEach(function () { - return this.rClient.exec.callsArgWith(0) + this.rClient.exec.yields(null, [1, 1]) }) it('should set a key with the date and give it a ttl', function (done) { @@ -240,7 +251,7 @@ describe('ConnectedUsersManager', function () { ) }) - return it('should set the cursor position when provided', function (done) { + it('should set the cursor position when provided', function (done) { return this.ConnectedUsersManager.updateUserPosition( this.project_id, this.client_id, @@ -259,11 +270,72 @@ describe('ConnectedUsersManager', function () { } ) }) + + describe('editing_session_mode', function () { + const cases = { + 'should bump the metric when connecting to empty room': { + nConnectedClients: 1, + cursorData: null, + labels: { + mode: 'connect', + status: 'single', + }, + }, + 'should bump the metric when connecting to non-empty room': { + nConnectedClients: 2, + cursorData: null, + labels: { + mode: 'connect', + status: 'multi', + }, + }, + 'should bump the metric when updating in empty room': { + nConnectedClients: 1, + cursorData: { row: 42 }, + labels: { + mode: 'update', + status: 'single', + }, + }, + 'should bump the metric when updating in non-empty room': { + nConnectedClients: 2, + cursorData: { row: 42 }, + labels: { + mode: 'update', + status: 'multi', + }, + }, + } + + for (const [ + name, + { nConnectedClients, cursorData, labels }, + ] of Object.entries(cases)) { + it(name, function (done) { + this.rClient.exec.yields(null, [1, nConnectedClients]) + this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + cursorData, + err => { + if (err) return done(err) + expect(this.Metrics.inc).to.have.been.calledWith( + 'editing_session_mode', + 1, + labels + ) + done() + } + ) + }) + } + }) }) describe('markUserAsDisconnected', function () { beforeEach(function () { - return this.rClient.exec.callsArgWith(0) + this.rClient.exec.yields(null, [1, 0]) }) it('should remove the user from the set', function (done) { @@ -294,7 +366,7 @@ describe('ConnectedUsersManager', function () { ) }) - return it('should add a ttl to the connected user set so it stays clean', function (done) { + it('should add a ttl to the connected user set so it stays clean', function (done) { return this.ConnectedUsersManager.markUserAsDisconnected( this.project_id, this.client_id, @@ -310,6 +382,163 @@ describe('ConnectedUsersManager', function () { } ) }) + + describe('editing_session_mode', function () { + const cases = { + 'should bump the metric when disconnecting from now empty room': { + nConnectedClients: 0, + labels: { + mode: 'disconnect', + status: 'empty', + }, + }, + 'should bump the metric when disconnecting from now single room': { + nConnectedClients: 1, + labels: { + mode: 'disconnect', + status: 'single', + }, + }, + 'should bump the metric when disconnecting from now multi room': { + nConnectedClients: 2, + labels: { + mode: 'disconnect', + status: 'multi', + }, + }, + } + + for (const [name, { nConnectedClients, labels }] of Object.entries( + cases + )) { + it(name, function (done) { + this.rClient.exec.yields(null, [1, nConnectedClients]) + this.ConnectedUsersManager.markUserAsDisconnected( + this.project_id, + this.client_id, + err => { + if (err) return done(err) + expect(this.Metrics.inc).to.have.been.calledWith( + 'editing_session_mode', + 1, + labels + ) + done() + } + ) + }) + } + }) + + describe('projectNotEmptySince', function () { + it('should clear the projectNotEmptySince key when empty and skip metric if not set', function (done) { + this.rClient.exec.yields(null, [1, 0]) + this.rClient.getdel.yields(null, '') + this.ConnectedUsersManager.markUserAsDisconnected( + this.project_id, + this.client_id, + err => { + if (err) return done(err) + expect(this.rClient.getdel).to.have.been.calledWith( + `projectNotEmptySince:{${this.project_id}}` + ) + expect(this.Metrics.histogram).to.not.have.been.called + done() + } + ) + }) + it('should clear the projectNotEmptySince key when empty and record metric if set', function (done) { + this.rClient.exec.yields(null, [1, 0]) + tk.freeze(1_234_000) + this.rClient.getdel.yields(null, '1230') + this.ConnectedUsersManager.markUserAsDisconnected( + this.project_id, + this.client_id, + err => { + if (err) return done(err) + expect(this.rClient.getdel).to.have.been.calledWith( + `projectNotEmptySince:{${this.project_id}}` + ) + expect(this.Metrics.histogram).to.have.been.calledWith( + 'project_not_empty_since', + 4, + sinon.match.any, + { status: 'empty' } + ) + done() + } + ) + }) + it('should set projectNotEmptySince key when single and skip metric if not set before', function (done) { + this.rClient.exec.yields(null, [1, 1]) + tk.freeze(1_233_001) // should ceil up + this.rClient.set.yields(null, '') + this.ConnectedUsersManager.markUserAsDisconnected( + this.project_id, + this.client_id, + err => { + if (err) return done(err) + expect(this.rClient.set).to.have.been.calledWith( + `projectNotEmptySince:{${this.project_id}}`, + '1234', + 'NX', + 'GET', + 'EX', + 31 * 24 * 60 * 60 + ) + expect(this.Metrics.histogram).to.not.have.been.called + done() + } + ) + }) + const cases = { + 'should set projectNotEmptySince key when single and record metric if set before': + { + nConnectedClients: 1, + labels: { + status: 'single', + }, + }, + 'should set projectNotEmptySince key when multi and record metric if set before': + { + nConnectedClients: 2, + labels: { + status: 'multi', + }, + }, + } + for (const [name, { nConnectedClients, labels }] of Object.entries( + cases + )) { + it(name, function (done) { + this.rClient.exec.yields(null, [1, nConnectedClients]) + tk.freeze(1_235_000) + this.rClient.set.yields(null, '1230') + this.ConnectedUsersManager.markUserAsDisconnected( + this.project_id, + this.client_id, + err => { + if (err) return done(err) + expect(this.rClient.set).to.have.been.calledWith( + `projectNotEmptySince:{${this.project_id}}`, + '1235', + 'NX', + 'GET', + 'EX', + 31 * 24 * 60 * 60 + ) + expect(this.Metrics.histogram).to.have.been.calledWith( + 'project_not_empty_since', + 5, + sinon.match.any, + labels + ) + done() + } + ) + }) + } + }) }) describe('_getConnectedUser', function () { From ca111771c23462095a39dbcd2660b7891d93601d Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Tue, 8 Apr 2025 17:05:07 +0100 Subject: [PATCH 039/978] [real-time] rename metric label to align with common name (#24732) GitOrigin-RevId: fd161f2345f0c30aa82395dbca673025698f13fe --- services/real-time/app/js/ConnectedUsersManager.js | 4 ++-- .../test/unit/js/ConnectedUsersManagerTests.js | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/services/real-time/app/js/ConnectedUsersManager.js b/services/real-time/app/js/ConnectedUsersManager.js index d0739c28d1..212288e251 100644 --- a/services/real-time/app/js/ConnectedUsersManager.js +++ b/services/real-time/app/js/ConnectedUsersManager.js @@ -88,7 +88,7 @@ module.exports = { } const [, nConnectedClients] = res Metrics.inc('editing_session_mode', 1, { - mode: cursorData ? 'update' : 'connect', + method: cursorData ? 'update' : 'connect', status: nConnectedClients === 1 ? 'single' : 'multi', }) callback(err) @@ -141,7 +141,7 @@ module.exports = { ? 'single' : 'multi' Metrics.inc('editing_session_mode', 1, { - mode: 'disconnect', + method: 'disconnect', status, }) if (status === 'empty') { diff --git a/services/real-time/test/unit/js/ConnectedUsersManagerTests.js b/services/real-time/test/unit/js/ConnectedUsersManagerTests.js index 17437ce7eb..56ca15ab39 100644 --- a/services/real-time/test/unit/js/ConnectedUsersManagerTests.js +++ b/services/real-time/test/unit/js/ConnectedUsersManagerTests.js @@ -277,7 +277,7 @@ describe('ConnectedUsersManager', function () { nConnectedClients: 1, cursorData: null, labels: { - mode: 'connect', + method: 'connect', status: 'single', }, }, @@ -285,7 +285,7 @@ describe('ConnectedUsersManager', function () { nConnectedClients: 2, cursorData: null, labels: { - mode: 'connect', + method: 'connect', status: 'multi', }, }, @@ -293,7 +293,7 @@ describe('ConnectedUsersManager', function () { nConnectedClients: 1, cursorData: { row: 42 }, labels: { - mode: 'update', + method: 'update', status: 'single', }, }, @@ -301,7 +301,7 @@ describe('ConnectedUsersManager', function () { nConnectedClients: 2, cursorData: { row: 42 }, labels: { - mode: 'update', + method: 'update', status: 'multi', }, }, @@ -388,21 +388,21 @@ describe('ConnectedUsersManager', function () { 'should bump the metric when disconnecting from now empty room': { nConnectedClients: 0, labels: { - mode: 'disconnect', + method: 'disconnect', status: 'empty', }, }, 'should bump the metric when disconnecting from now single room': { nConnectedClients: 1, labels: { - mode: 'disconnect', + method: 'disconnect', status: 'single', }, }, 'should bump the metric when disconnecting from now multi room': { nConnectedClients: 2, labels: { - mode: 'disconnect', + method: 'disconnect', status: 'multi', }, }, From 3f10b29869db06b768a454c28fd48bb3d14333db Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Tue, 8 Apr 2025 18:12:33 +0100 Subject: [PATCH 040/978] [real-time] backwards compatibility fix for redis 6.2 (#24734) GitOrigin-RevId: ffc51a8280e0d0708e7dcb2638cabed2b7adfbf5 --- .../real-time/app/js/ConnectedUsersManager.js | 28 ++++++++++--------- .../unit/js/ConnectedUsersManagerTests.js | 12 ++++---- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/services/real-time/app/js/ConnectedUsersManager.js b/services/real-time/app/js/ConnectedUsersManager.js index 212288e251..1421e8eeef 100644 --- a/services/real-time/app/js/ConnectedUsersManager.js +++ b/services/real-time/app/js/ConnectedUsersManager.js @@ -158,24 +158,26 @@ module.exports = { } else { // Only populate projectNotEmptySince when more clients remain connected. const nowInSeconds = Math.ceil(Date.now() / 1000).toString() - rclient.set( + // We can go back to SET GET after upgrading to redis 7.0+ + const multi = rclient.multi() + multi.get(Keys.projectNotEmptySince({ projectId })) + multi.set( Keys.projectNotEmptySince({ projectId }), nowInSeconds, 'NX', - 'GET', 'EX', - 31 * ONE_DAY_IN_S, - (err, res) => { - if (err) { - logger.warn( - { err, projectId }, - 'could not set/collect projectNotEmptySince' - ) - } else if (res) { - recordProjectNotEmptySinceMetric(res, status) - } - } + 31 * ONE_DAY_IN_S ) + multi.exec((err, res) => { + if (err) { + logger.warn( + { err, projectId }, + 'could not get/set projectNotEmptySince' + ) + } else if (res[0]) { + recordProjectNotEmptySinceMetric(res[0], status) + } + }) } callback(err) }) diff --git a/services/real-time/test/unit/js/ConnectedUsersManagerTests.js b/services/real-time/test/unit/js/ConnectedUsersManagerTests.js index 56ca15ab39..dd4aeb35c9 100644 --- a/services/real-time/test/unit/js/ConnectedUsersManagerTests.js +++ b/services/real-time/test/unit/js/ConnectedUsersManagerTests.js @@ -448,7 +448,7 @@ describe('ConnectedUsersManager', function () { ) }) it('should clear the projectNotEmptySince key when empty and record metric if set', function (done) { - this.rClient.exec.yields(null, [1, 0]) + this.rClient.exec.onFirstCall().yields(null, [1, 0]) tk.freeze(1_234_000) this.rClient.getdel.yields(null, '1230') this.ConnectedUsersManager.markUserAsDisconnected( @@ -470,9 +470,9 @@ describe('ConnectedUsersManager', function () { ) }) it('should set projectNotEmptySince key when single and skip metric if not set before', function (done) { - this.rClient.exec.yields(null, [1, 1]) + this.rClient.exec.onFirstCall().yields(null, [1, 1]) tk.freeze(1_233_001) // should ceil up - this.rClient.set.yields(null, '') + this.rClient.exec.onSecondCall().yields(null, ['']) this.ConnectedUsersManager.markUserAsDisconnected( this.project_id, this.client_id, @@ -482,7 +482,6 @@ describe('ConnectedUsersManager', function () { `projectNotEmptySince:{${this.project_id}}`, '1234', 'NX', - 'GET', 'EX', 31 * 24 * 60 * 60 ) @@ -511,9 +510,9 @@ describe('ConnectedUsersManager', function () { cases )) { it(name, function (done) { - this.rClient.exec.yields(null, [1, nConnectedClients]) + this.rClient.exec.onFirstCall().yields(null, [1, nConnectedClients]) tk.freeze(1_235_000) - this.rClient.set.yields(null, '1230') + this.rClient.exec.onSecondCall().yields(null, ['1230']) this.ConnectedUsersManager.markUserAsDisconnected( this.project_id, this.client_id, @@ -523,7 +522,6 @@ describe('ConnectedUsersManager', function () { `projectNotEmptySince:{${this.project_id}}`, '1235', 'NX', - 'GET', 'EX', 31 * 24 * 60 * 60 ) From 94a067d7c8ef3ba68a09b4386552b9acd259111c Mon Sep 17 00:00:00 2001 From: Jimmy Domagala-Tang Date: Tue, 8 Apr 2025 13:18:21 -0400 Subject: [PATCH 041/978] Merge pull request #24208 from overleaf/jdt-wf-analytics-wrapper Create listener for Writefull to send events to our analytics GitOrigin-RevId: 8ad1866e3c81b1f6c3388b744f9e27810623436e --- .../web/frontend/js/shared/context/types/writefull-instance.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/frontend/js/shared/context/types/writefull-instance.ts b/services/web/frontend/js/shared/context/types/writefull-instance.ts index 0040422548..8dd0023f07 100644 --- a/services/web/frontend/js/shared/context/types/writefull-instance.ts +++ b/services/web/frontend/js/shared/context/types/writefull-instance.ts @@ -4,6 +4,7 @@ export interface WritefullEvents { } 'writefull-received-suggestions': { numberOfSuggestions: number } 'writefull-register-as-auto-account': { email: string } + 'writefull-shared-analytics': { eventName: string; segmentation: object } } type InsertPosition = { From 1741e48d59f98d47d870db86202e598eccacee64 Mon Sep 17 00:00:00 2001 From: roo hutton Date: Wed, 9 Apr 2025 08:47:34 +0100 Subject: [PATCH 042/978] Merge pull request #24619 from overleaf/rh-team-invites-index Add migration for subscriptions.teamInvites.email index GitOrigin-RevId: 5f4bca578ae0dcf92c422596aa7834c42dc63bee --- ...1120946_add_email_to_teamInvites_index.mjs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 services/web/migrations/20250331120946_add_email_to_teamInvites_index.mjs diff --git a/services/web/migrations/20250331120946_add_email_to_teamInvites_index.mjs b/services/web/migrations/20250331120946_add_email_to_teamInvites_index.mjs new file mode 100644 index 0000000000..828e3bddc8 --- /dev/null +++ b/services/web/migrations/20250331120946_add_email_to_teamInvites_index.mjs @@ -0,0 +1,37 @@ +import Helpers from './lib/helpers.mjs' + +const indexes = [ + { + key: { + 'teamInvites.email': 1, + }, + name: 'teamInvites.email_1', + partialFilterExpression: { + 'teamInvites.email': { + $exists: true, + }, + }, + }, +] + +const tags = ['saas'] + +const migrate = async client => { + const { db } = client + await Helpers.addIndexesToCollection(db.subscriptions, indexes) +} + +const rollback = async client => { + const { db } = client + try { + await Helpers.dropIndexesFromCollection(db.subscriptions, indexes) + } catch (err) { + console.error('Something went wrong rolling back the migrations', err) + } +} + +export default { + tags, + migrate, + rollback, +} From 28468e134c80ff0733a3c407bc1a0a8f8389e24e Mon Sep 17 00:00:00 2001 From: Tim Down <158919+timdown@users.noreply.github.com> Date: Wed, 9 Apr 2025 09:36:07 +0100 Subject: [PATCH 043/978] Merge pull request #24660 from overleaf/td-bs5-remove-react-bootstrap-0 Remove react-bootstrap 0.33.1 GitOrigin-RevId: c320a6b18c576afdc0fd49559915d3d2f3a7a1ef --- package-lock.json | 128 --- services/web/.storybook/preview.tsx | 38 +- .../utils/with-bootstrap-switcher.tsx | 20 - services/web/cypress/support/ct/window.ts | 1 - .../components/file-tree-context-menu.tsx | 19 - .../components/change-list/change-list.tsx | 5 +- .../change-list/dropdown/actions-dropdown.tsx | 22 +- .../dropdown/compare-version-dropdown.tsx | 1 - .../change-list/dropdown/history-dropdown.tsx | 1 - .../make-primary/confirmation-modal.tsx | 3 +- .../reconfirmation-info-prompt-text.tsx | 3 - .../dashboard/group-settings-button.tsx | 44 +- .../dashboard/leave-group-modal.tsx | 5 - .../dashboard/managed-group-subscriptions.tsx | 6 +- ...rsonal-subscription-recurly-sync-email.tsx | 3 - .../components/dashboard/row-link.tsx | 24 +- .../dashboard/states/active/active-new.tsx | 14 +- .../cancel-plan/downgrade-plan-button.tsx | 3 - .../cancel-plan/extend-trial-button.tsx | 3 - .../modals/cancel-ai-add-on-modal.tsx | 5 - .../modals/change-to-group-modal.tsx | 14 +- .../modals/confirm-change-plan-modal.tsx | 5 - .../modals/keep-current-plan-modal.tsx | 5 - .../bootstrap-3/dropdown-menu-with-ref.tsx | 71 -- .../dropdown-toggle-with-tooltip.tsx | 53 - .../bootstrap-3/form/form-control.tsx | 21 - .../bootstrap-3/toast-container.tsx | 14 - .../ui/components/bootstrap-3/toast.tsx | 51 - .../bootstrap-3/toggle-button-group.tsx | 99 -- .../bootstrap-version-switcher.tsx | 15 - .../ui/components/ol/icons/ol-tag-icon.tsx | 9 +- .../js/features/ui/components/ol/ol-badge.tsx | 37 +- .../ui/components/ol/ol-button-group.tsx | 27 +- .../ui/components/ol/ol-button-toolbar.tsx | 28 +- .../features/ui/components/ol/ol-button.tsx | 127 +-- .../js/features/ui/components/ol/ol-card.tsx | 13 +- .../ui/components/ol/ol-close-button.tsx | 22 +- .../js/features/ui/components/ol/ol-col.tsx | 70 +- .../components/ol/ol-dropdown-menu-item.tsx | 38 +- .../ui/components/ol/ol-form-checkbox.tsx | 135 +-- .../ui/components/ol/ol-form-control.tsx | 81 +- .../ui/components/ol/ol-form-feedback.tsx | 28 +- .../ui/components/ol/ol-form-group.tsx | 38 +- .../ui/components/ol/ol-form-label.tsx | 25 +- .../ui/components/ol/ol-form-select.tsx | 51 +- .../ui/components/ol/ol-form-switch.tsx | 48 +- .../ui/components/ol/ol-form-text.tsx | 31 +- .../js/features/ui/components/ol/ol-form.tsx | 41 +- .../ui/components/ol/ol-icon-button.tsx | 45 +- .../ui/components/ol/ol-list-group-item.tsx | 39 +- .../ui/components/ol/ol-list-group.tsx | 31 +- .../js/features/ui/components/ol/ol-modal.tsx | 126 +-- .../ui/components/ol/ol-notification.tsx | 40 +- .../features/ui/components/ol/ol-overlay.tsx | 58 +- .../ui/components/ol/ol-page-content-card.tsx | 14 +- .../features/ui/components/ol/ol-popover.tsx | 75 +- .../js/features/ui/components/ol/ol-row.tsx | 13 +- .../features/ui/components/ol/ol-spinner.tsx | 25 +- .../js/features/ui/components/ol/ol-table.tsx | 29 +- .../js/features/ui/components/ol/ol-tag.tsx | 31 +- .../ui/components/ol/ol-toast-container.tsx | 18 +- .../js/features/ui/components/ol/ol-toast.tsx | 35 +- .../components/ol/ol-toggle-button-group.tsx | 39 +- .../ui/components/ol/ol-toggle-button.tsx | 45 +- .../features/ui/components/ol/ol-tooltip.tsx | 45 +- .../frontend/js/features/utils/bootstrap-5.ts | 26 - .../js/shared/components/accessible-modal.tsx | 30 - .../frontend/js/shared/components/badge.tsx | 29 - .../js/shared/components/beta-badge-icon.tsx | 23 +- .../js/shared/components/beta-badge.tsx | 5 +- .../frontend/js/shared/components/close.tsx | 14 +- .../shared/components/controlled-dropdown.tsx | 58 -- .../shared/components/copy-to-clipboard.tsx | 8 +- .../generic-error-boundary-fallback.tsx | 27 +- .../frontend/js/shared/components/icon.tsx | 5 +- .../labs/labs-experiments-widget.tsx | 8 +- .../js/shared/components/loading-spinner.tsx | 13 +- .../js/shared/components/material-icon.tsx | 5 +- .../frontend/js/shared/components/select.tsx | 209 ++-- .../web/frontend/js/shared/components/tag.tsx | 56 - .../frontend/js/shared/components/tooltip.tsx | 57 -- .../js/shared/components/upgrade-benefits.jsx | 9 +- .../stories/contact-us-modal.stories.tsx | 60 +- .../web/frontend/stories/dropdown.stories.jsx | 45 - .../stories/feedback-badge.stories.tsx | 4 - .../frontend/stories/input-switch.stories.tsx | 7 +- .../stories/loading/loading.stories.tsx | 2 - .../web/frontend/stories/menu-bar.stories.tsx | 3 - .../unsaved-docs-locked-modal.stories.tsx | 7 - .../frontend/stories/pdf-preview.stories.jsx | 10 +- .../project-list/add-affiliation.stories.tsx | 3 - .../project-list/color-picker.stories.tsx | 3 - ...mpile-and-download-project-pdf.stories.tsx | 3 - .../current-plan-widget.stories.tsx | 3 - .../project-list/inr-banner.stories.tsx | 3 - .../new-project-button.stories.tsx | 3 - .../project-list/notifications.stories.tsx | 3 - .../project-list/project-list.stories.tsx | 3 - .../project-list/project-search.stories.tsx | 3 - .../project-list/survey-widget.stories.tsx | 3 - .../project-list/system-messages.stories.tsx | 3 - .../start-free-trial-button.stories.jsx | 4 - .../frontend/stories/style-guide.stories.jsx | 962 ------------------ .../group-invites/group-invites.stories.tsx | 2 - .../frontend/stories/ui/badge-bs3.stories.tsx | 59 -- ...adge-bs5.stories.tsx => badge.stories.tsx} | 5 +- .../frontend/stories/ui/button.stories.tsx | 7 +- .../stories/ui/dropdown-menu.stories.tsx | 5 +- .../ui/form/form-check-bs5.stories.tsx | 5 +- .../ui/form/form-input-bs5.stories.tsx | 5 +- .../ui/form/form-radio-bs5.stories.tsx | 5 +- .../ui/form/form-select-bs5.stories.tsx | 5 +- .../ui/form/form-textarea-bs5.stories.tsx | 5 +- .../stories/ui/icon-button.stories.tsx | 5 +- .../web/frontend/stories/ui/row.stories.tsx | 5 +- .../stories/ui/split-button.stories.tsx | 5 +- .../frontend/stories/ui/tag-bs3.stories.tsx | 70 -- .../{tag-bs5.stories.tsx => tag.stories.tsx} | 5 +- .../frontend/stories/ui/tooltip.stories.tsx | 19 +- .../stories/upgrade-prompt.stories.tsx | 5 - .../bootstrap-5/components/button.scss | 8 - .../bootstrap-5/pages/onboarding.scss | 4 - services/web/package.json | 3 - .../shared/accessible-modal.spec.tsx | 36 - .../components/shared/select.spec.tsx | 16 +- .../components/shared/tooltip.spec.tsx | 8 +- .../components/notifications.test.tsx | 13 +- .../components/project-list-title.tsx | 3 - .../project-tools-rename.test.tsx | 11 - .../components/share-project-modal.test.jsx | 5 - .../components/dashboard/pause-modal.test.tsx | 1 - .../dashboard/states/active/active.test.tsx | 12 +- .../group-invite/accepted-invite.test.tsx | 64 +- .../group-invite/group-invite.test.tsx | 181 ++-- .../web/test/frontend/helpers/bootstrap-3.ts | 5 - .../web/test/frontend/helpers/reset-meta.ts | 1 - 136 files changed, 428 insertions(+), 4132 deletions(-) delete mode 100644 services/web/.storybook/utils/with-bootstrap-switcher.tsx delete mode 100644 services/web/frontend/js/features/ui/components/bootstrap-3/dropdown-menu-with-ref.tsx delete mode 100644 services/web/frontend/js/features/ui/components/bootstrap-3/dropdown-toggle-with-tooltip.tsx delete mode 100644 services/web/frontend/js/features/ui/components/bootstrap-3/form/form-control.tsx delete mode 100644 services/web/frontend/js/features/ui/components/bootstrap-3/toast-container.tsx delete mode 100644 services/web/frontend/js/features/ui/components/bootstrap-3/toast.tsx delete mode 100644 services/web/frontend/js/features/ui/components/bootstrap-3/toggle-button-group.tsx delete mode 100644 services/web/frontend/js/features/ui/components/bootstrap-5/bootstrap-version-switcher.tsx delete mode 100644 services/web/frontend/js/features/utils/bootstrap-5.ts delete mode 100644 services/web/frontend/js/shared/components/accessible-modal.tsx delete mode 100644 services/web/frontend/js/shared/components/badge.tsx delete mode 100644 services/web/frontend/js/shared/components/controlled-dropdown.tsx delete mode 100644 services/web/frontend/js/shared/components/tag.tsx delete mode 100644 services/web/frontend/js/shared/components/tooltip.tsx delete mode 100644 services/web/frontend/stories/dropdown.stories.jsx delete mode 100644 services/web/frontend/stories/style-guide.stories.jsx delete mode 100644 services/web/frontend/stories/ui/badge-bs3.stories.tsx rename services/web/frontend/stories/ui/{badge-bs5.stories.tsx => badge.stories.tsx} (92%) delete mode 100644 services/web/frontend/stories/ui/tag-bs3.stories.tsx rename services/web/frontend/stories/ui/{tag-bs5.stories.tsx => tag.stories.tsx} (93%) delete mode 100644 services/web/test/frontend/components/shared/accessible-modal.spec.tsx delete mode 100644 services/web/test/frontend/helpers/bootstrap-3.ts diff --git a/package-lock.json b/package-lock.json index 5f0eb0baf1..bd3eb0b2e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3267,27 +3267,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/runtime-corejs2": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.16.7.tgz", - "integrity": "sha512-ec0BM0J/9M5Cncha++AlgvvDlk+uM+m6f7K0t74ClcYzsE8LgX4RstRreksMSCI82o3LJS//UswmA0pUWkJpqg==", - "dev": true, - "dependencies": { - "core-js": "^2.6.5", - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime-corejs2/node_modules/core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.4 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.", - "dev": true, - "hasInstallScript": true - }, "node_modules/@babel/runtime-corejs3": { "version": "7.16.8", "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.16.8.tgz", @@ -12634,16 +12613,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-overlays": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@types/react-overlays/-/react-overlays-1.1.3.tgz", - "integrity": "sha512-oOq5NWbyfNz2w2sKvjkHdvGQSMA+VDVfI5UOfGPR0wkik2welad1RDVnVgH15jKf58jrZNBa1Ee4SVBgCGFxCg==", - "dev": true, - "dependencies": { - "@types/react": "*", - "@types/react-transition-group": "*" - } - }, "node_modules/@types/react-redux": { "version": "7.1.33", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", @@ -19802,15 +19771,6 @@ "utila": "~0.4" } }, - "node_modules/dom-helpers": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", - "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.1.2" - } - }, "node_modules/dom-serializer": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", @@ -27577,12 +27537,6 @@ "node": ">=12.0.0" } }, - "node_modules/keycode": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz", - "integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==", - "dev": true - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -34620,30 +34574,6 @@ "react": ">=16.4.1" } }, - "node_modules/react-bootstrap": { - "version": "0.33.1", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-0.33.1.tgz", - "integrity": "sha512-qWTRravSds87P8WC82tETy2yIso8qDqlIm0czsrduCaYAFtHuyLu0XDbUlfLXeRzqgwm5sRk2wRaTNoiVkk/YQ==", - "dev": true, - "dependencies": { - "@babel/runtime-corejs2": "^7.0.0", - "classnames": "^2.2.5", - "dom-helpers": "^3.2.0", - "invariant": "^2.2.4", - "keycode": "^2.2.0", - "prop-types": "^15.6.1", - "prop-types-extra": "^1.0.1", - "react-overlays": "^0.9.0", - "react-prop-types": "^0.4.0", - "react-transition-group": "^2.0.0", - "uncontrollable": "^7.0.2", - "warning": "^3.0.0" - }, - "peerDependencies": { - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - } - }, "node_modules/react-chartjs-2": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.0.1.tgz", @@ -34849,36 +34779,6 @@ "uc.micro": "^1.0.1" } }, - "node_modules/react-overlays": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.9.3.tgz", - "integrity": "sha512-u2T7nOLnK+Hrntho4p0Nxh+BsJl0bl4Xuwj/Y0a56xywLMetgAfyjnDVrudLXsNcKGaspoC+t3C1V80W9QQTdQ==", - "dev": true, - "dependencies": { - "classnames": "^2.2.5", - "dom-helpers": "^3.2.1", - "prop-types": "^15.5.10", - "prop-types-extra": "^1.0.1", - "react-transition-group": "^2.2.1", - "warning": "^3.0.0" - }, - "peerDependencies": { - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - } - }, - "node_modules/react-prop-types": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/react-prop-types/-/react-prop-types-0.4.0.tgz", - "integrity": "sha1-+ZsL+0AGkpya8gUefBQUpcdbk9A=", - "dev": true, - "dependencies": { - "warning": "^3.0.0" - }, - "peerDependencies": { - "react": ">=0.14.0" - } - }, "node_modules/react-proxy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/react-proxy/-/react-proxy-1.1.8.tgz", @@ -34989,22 +34889,6 @@ "react-proxy": "^1.1.7" } }, - "node_modules/react-transition-group": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", - "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", - "dev": true, - "dependencies": { - "dom-helpers": "^3.4.0", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2", - "react-lifecycles-compat": "^3.0.4" - }, - "peerDependencies": { - "react": ">=15.0.0", - "react-dom": ">=15.0.0" - } - }, "node_modules/reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", @@ -40502,15 +40386,6 @@ "node": ">=6.0.0" } }, - "node_modules/warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", - "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", - "dev": true, - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", @@ -44482,7 +44357,6 @@ "@types/react-dom": "^17.0.13", "@types/react-google-recaptcha": "^2.1.5", "@types/react-linkify": "^1.0.0", - "@types/react-overlays": "^1.1.3", "@types/recurly__recurly-js": "^4.22.0", "@types/sinon-chai": "^3.2.8", "@types/uuid": "^9.0.8", @@ -44570,7 +44444,6 @@ "prop-types": "^15.7.2", "qrcode": "^1.4.4", "react": "^17.0.2", - "react-bootstrap": "^0.33.1", "react-bootstrap-5": "npm:react-bootstrap@^2.10.5", "react-chartjs-2": "^5.0.1", "react-color": "^2.19.3", @@ -44581,7 +44454,6 @@ "react-google-recaptcha": "^3.1.0", "react-i18next": "^13.3.1", "react-linkify": "^1.0.0-alpha", - "react-overlays": "^0.9.3", "react-refresh": "^0.14.0", "react-resizable-panels": "^2.1.1", "resolve-url-loader": "^5.0.0", diff --git a/services/web/.storybook/preview.tsx b/services/web/.storybook/preview.tsx index ed67abc3c4..e3838a6f97 100644 --- a/services/web/.storybook/preview.tsx +++ b/services/web/.storybook/preview.tsx @@ -9,14 +9,10 @@ import i18n from 'i18next' import { initReactI18next } from 'react-i18next' // @ts-ignore import en from '../../../services/web/locales/en.json' -import { bootstrapVersionArg } from './utils/with-bootstrap-switcher' -function resetMeta(bootstrapVersion?: 3 | 5) { +function resetMeta() { window.metaAttributesCache = new Map() window.metaAttributesCache.set('ol-i18n', { currentLangCode: 'en' }) - if (bootstrapVersion) { - window.metaAttributesCache.set('ol-bootstrapVersion', bootstrapVersion) - } window.metaAttributesCache.set('ol-ExposedSettings', { adminEmail: 'placeholder@example.com', appName: 'Overleaf', @@ -126,8 +122,6 @@ const preview: Preview = { // render stories in iframes, to isolate modals inlineStories: false, }, - // Default to Bootstrap 5 styles - bootstrap5: true, }, globalTypes: { theme: { @@ -144,17 +138,9 @@ const preview: Preview = { }, }, loaders: [ - async ({ globals }) => { - const { theme } = globals - + async () => { return { - // NOTE: this uses `${theme}style.less` rather than `${theme}.less` - // so that webpack only bundles files ending with "style.less" - bootstrap3Style: await import( - `!!to-string-loader!css-loader!less-loader!../../../services/web/frontend/stylesheets/${theme}style.less` - ), - // Themes are applied differently in Bootstrap 5 code - bootstrap5Style: await import( + mainStyle: await import( // @ts-ignore `!!to-string-loader!css-loader!resolve-url-loader!sass-loader!../../../services/web/frontend/stylesheets/bootstrap-5/main-style.scss` ), @@ -163,15 +149,9 @@ const preview: Preview = { ], decorators: [ (Story, context) => { - const { bootstrap3Style, bootstrap5Style } = context.loaded - const bootstrapVersion = Number( - context.args[bootstrapVersionArg] || - (context.parameters.bootstrap5 === false ? 3 : 5) - ) as 3 | 5 - const activeStyle = - bootstrapVersion === 3 ? bootstrap3Style : bootstrap5Style + const { mainStyle } = context.loaded - resetMeta(bootstrapVersion) + resetMeta() return (
- {activeStyle && } - + {mainStyle && } +
) }, diff --git a/services/web/.storybook/utils/with-bootstrap-switcher.tsx b/services/web/.storybook/utils/with-bootstrap-switcher.tsx deleted file mode 100644 index 640338dbb0..0000000000 --- a/services/web/.storybook/utils/with-bootstrap-switcher.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Meta } from '@storybook/react' - -export const bootstrapVersionArg = 'bootstrapVersion' - -export const bsVersionDecorator: Meta = { - argTypes: { - [bootstrapVersionArg]: { - name: 'Bootstrap Version', - description: 'Bootstrap version for components', - control: { type: 'inline-radio' }, - options: ['3', '5'], - table: { - defaultValue: { summary: '5' }, - }, - }, - }, - args: { - [bootstrapVersionArg]: '5', - }, -} diff --git a/services/web/cypress/support/ct/window.ts b/services/web/cypress/support/ct/window.ts index 3f415cf3c5..ae2a194bc1 100644 --- a/services/web/cypress/support/ct/window.ts +++ b/services/web/cypress/support/ct/window.ts @@ -10,7 +10,6 @@ export function resetMeta() { hasLinkedProjectOutputFileFeature: true, hasLinkUrlFeature: true, }) - window.metaAttributesCache.set('ol-bootstrapVersion', 5) } // Populate meta for top-level access in modules on import diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-context-menu.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-context-menu.tsx index 5e66614dcd..957ed0cbdb 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-context-menu.tsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-context-menu.tsx @@ -19,20 +19,11 @@ function FileTreeContextMenu() { toggleButtonRef.current = document.querySelector( '.entity-menu-toggle' ) as HTMLButtonElement | null - focusContextMenu() } }, [contextMenuCoords]) if (!contextMenuCoords || fileTreeReadOnly) return null - // A11y - Move the focus to the context menu when it opens - function focusContextMenu() { - const BS3contextMenu = document.querySelector( - '[aria-labelledby="dropdown-file-tree-context-menu"]' - ) as HTMLElement | null - BS3contextMenu?.focus() - } - function close() { setContextMenuCoords(null) if (toggleButtonRef.current) { @@ -79,14 +70,4 @@ function FileTreeContextMenu() { ) } -// fake component required as Dropdowns require a Toggle, even tho we don't want -// one for the context menu -const FakeDropDownToggle = React.forwardRef( - ({ bsRole }, ref) => { - return null - } -) - -FakeDropDownToggle.displayName = 'FakeDropDownToggle' - export default FileTreeContextMenu diff --git a/services/web/frontend/js/features/history/components/change-list/change-list.tsx b/services/web/frontend/js/features/history/components/change-list/change-list.tsx index cb0e93fae4..f072d24a57 100644 --- a/services/web/frontend/js/features/history/components/change-list/change-list.tsx +++ b/services/web/frontend/js/features/history/components/change-list/change-list.tsx @@ -11,10 +11,7 @@ function ChangeList() {
-
+
{labelsOnly ? : }
diff --git a/services/web/frontend/js/features/history/components/change-list/dropdown/actions-dropdown.tsx b/services/web/frontend/js/features/history/components/change-list/dropdown/actions-dropdown.tsx index 90f23cf746..9029260057 100644 --- a/services/web/frontend/js/features/history/components/change-list/dropdown/actions-dropdown.tsx +++ b/services/web/frontend/js/features/history/components/change-list/dropdown/actions-dropdown.tsx @@ -3,26 +3,20 @@ import { Dropdown, DropdownMenu, } from '@/features/ui/components/bootstrap-5/dropdown-menu' -import BS5DropdownToggleWithTooltip from '@/features/ui/components/bootstrap-5/dropdown-toggle-with-tooltip' +import DropdownToggleWithTooltip from '@/features/ui/components/bootstrap-5/dropdown-toggle-with-tooltip' type ActionDropdownProps = { id: string children: React.ReactNode - parentSelector?: string isOpened: boolean iconTag: ReactNode toolTipDescription: string setIsOpened: (isOpened: boolean) => void } -function BS5ActionsDropdown({ - id, - children, - isOpened, - iconTag, - setIsOpened, - toolTipDescription, -}: Omit) { +function ActionsDropdown(props: ActionDropdownProps) { + const { id, children, isOpened, iconTag, setIsOpened, toolTipDescription } = + props return ( setIsOpened(open)} > - {iconTag} - + {children} @@ -47,8 +41,4 @@ function BS5ActionsDropdown({ ) } -function ActionsDropdown(props: ActionDropdownProps) { - return -} - export default ActionsDropdown diff --git a/services/web/frontend/js/features/history/components/change-list/dropdown/compare-version-dropdown.tsx b/services/web/frontend/js/features/history/components/change-list/dropdown/compare-version-dropdown.tsx index 6cf2d54f1f..91f0bf991a 100644 --- a/services/web/frontend/js/features/history/components/change-list/dropdown/compare-version-dropdown.tsx +++ b/services/web/frontend/js/features/history/components/change-list/dropdown/compare-version-dropdown.tsx @@ -21,7 +21,6 @@ function CompareVersionDropdown({ id={id} isOpened={isOpened} setIsOpened={setIsOpened} - parentSelector="[data-history-version-list-container]" toolTipDescription={t('compare')} iconTag={ } - parentSelector="[data-history-version-list-container]" > {children} diff --git a/services/web/frontend/js/features/settings/components/emails/actions/make-primary/confirmation-modal.tsx b/services/web/frontend/js/features/settings/components/emails/actions/make-primary/confirmation-modal.tsx index cdc1b9d481..66f4da7c99 100644 --- a/services/web/frontend/js/features/settings/components/emails/actions/make-primary/confirmation-modal.tsx +++ b/services/web/frontend/js/features/settings/components/emails/actions/make-primary/confirmation-modal.tsx @@ -1,5 +1,4 @@ import { useTranslation, Trans } from 'react-i18next' -import AccessibleModal from '../../../../../../shared/components/accessible-modal' import { MergeAndOverride } from '../../../../../../../../types/utils' import OLButton from '@/features/ui/components/ol/ol-button' import OLModal, { @@ -11,7 +10,7 @@ import OLModal, { import { type UserEmailData } from '../../../../../../../../types/user-email' type ConfirmationModalProps = MergeAndOverride< - React.ComponentProps, + React.ComponentProps, { email: string isConfirmDisabled: boolean diff --git a/services/web/frontend/js/features/settings/components/emails/reconfirmation-info/reconfirmation-info-prompt-text.tsx b/services/web/frontend/js/features/settings/components/emails/reconfirmation-info/reconfirmation-info-prompt-text.tsx index 4dace050ef..898649504d 100644 --- a/services/web/frontend/js/features/settings/components/emails/reconfirmation-info/reconfirmation-info-prompt-text.tsx +++ b/services/web/frontend/js/features/settings/components/emails/reconfirmation-info/reconfirmation-info-prompt-text.tsx @@ -4,19 +4,16 @@ import { Institution } from '../../../../../../../types/institution' type ReconfirmationInfoPromptTextProps = { primary: boolean institutionName: Institution['name'] - icon?: React.ReactElement // BS3 only } function ReconfirmationInfoPromptText({ primary, institutionName, - icon, }: ReconfirmationInfoPromptTextProps) { const { t } = useTranslation() return ( <> - {icon}
+ ) } diff --git a/services/web/frontend/js/features/subscription/components/dashboard/leave-group-modal.tsx b/services/web/frontend/js/features/subscription/components/dashboard/leave-group-modal.tsx index d56b0c80b8..99a80da3f4 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/leave-group-modal.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/leave-group-modal.tsx @@ -71,11 +71,6 @@ export default function LeaveGroupModal() { disabled={inflight} isLoading={inflight} loadingLabel={t('processing_uppercase') + '…'} - bs3Props={{ - loading: inflight - ? t('processing_uppercase') + '…' - : t('leave_now'), - }} > {t('leave_now')} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx b/services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx index 18b43bd7da..a79cac06ac 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx @@ -7,8 +7,6 @@ import { Trans, useTranslation } from 'react-i18next' import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context' import { RowLink } from './row-link' import { ManagedGroupSubscription } from '../../../../../../types/subscription/dashboard/subscription' -import { bsVersion } from '@/features/utils/bootstrap-5' -import classnames from 'classnames' function ManagedGroupAdministrator({ subscription, @@ -104,9 +102,7 @@ export default function ManagedGroupSubscriptions() { {managedGroupSubscriptions.map(subscription => { return (
-

- {t('group_management')} -

+

{t('group_management')}

diff --git a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription-recurly-sync-email.tsx b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription-recurly-sync-email.tsx index 1425609264..9c08e175d6 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription-recurly-sync-email.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription-recurly-sync-email.tsx @@ -51,9 +51,6 @@ function PersonalSubscriptionRecurlySyncEmail() { disabled={isLoading} isLoading={isLoading} loadingLabel={t('updating')} - bs3Props={{ - loading: isLoading ? t('updating') + '…' : t('update'), - }} > {t('update')} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/row-link.tsx b/services/web/frontend/js/features/subscription/components/dashboard/row-link.tsx index fec6f03c53..42e331fdd0 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/row-link.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/row-link.tsx @@ -1,5 +1,4 @@ import MaterialIcon from '../../../../shared/components/material-icon' -import { isBootstrap5 } from '@/features/utils/bootstrap-5' type RowLinkProps = { href: string @@ -8,28 +7,7 @@ type RowLinkProps = { icon: string } -export function RowLink(props: RowLinkProps) { - return isBootstrap5() ? : -} - -function BS3RowLink({ href, heading, subtext, icon }: RowLinkProps) { - return ( - -
- -
-
-
{heading}
-
{subtext}
-
-
- -
-
- ) -} - -function BS5RowLink({ href, heading, subtext, icon }: RowLinkProps) { +export function RowLink({ href, heading, subtext, icon }: RowLinkProps) { return (
  • diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active-new.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active-new.tsx index 067888f918..8a60af4dad 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active-new.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active-new.tsx @@ -12,7 +12,6 @@ import { ChangeToGroupModal } from './change-plan/modals/change-to-group-modal' import { CancelAiAddOnModal } from '@/features/subscription/components/dashboard/states/active/change-plan/modals/cancel-ai-add-on-modal' import OLButton from '@/features/ui/components/ol/ol-button' import isInFreeTrial from '../../../../util/is-in-free-trial' -import { bsVersion } from '@/features/utils/bootstrap-5' import AddOns from '@/features/subscription/components/dashboard/states/active/add-ons' import { AI_ADD_ON_CODE, @@ -20,7 +19,6 @@ import { isStandaloneAiPlanCode, } from '@/features/subscription/data/add-on-codes' import getMeta from '@/utils/meta' -import classnames from 'classnames' import SubscriptionRemainder from '@/features/subscription/components/dashboard/states/active/subscription-remainder' import { sendMB } from '../../../../../../infrastructure/event-tracking' import PauseSubscriptionModal from '@/features/subscription/components/dashboard/pause-modal' @@ -100,9 +98,7 @@ export function ActiveSubscriptionNew({ /> )}
  • -

    - {t('billing')} -

    +

    {t('billing')}

    {subscription.plan.annual ? (


    -

    - {t('plan')} -

    -

    - {planName} -

    +

    {t('plan')}

    +

    {planName}

    {subscription.pendingPlan && subscription.pendingPlan.name !== subscription.plan.name && (

    {t('want_change_to_apply_before_plan_end')}

    diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/downgrade-plan-button.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/downgrade-plan-button.tsx index e0bc66ac63..c34e304e8b 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/downgrade-plan-button.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/downgrade-plan-button.tsx @@ -41,9 +41,6 @@ export default function DowngradePlanButton({ disabled={isButtonDisabled} isLoading={isLoading} loadingLabel={t('processing_uppercase') + '…'} - bs3Props={{ - loading: isLoading ? t('processing_uppercase') + '…' : buttonText, - }} > {buttonText} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/extend-trial-button.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/extend-trial-button.tsx index d5b2e4d95c..e108e934a0 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/extend-trial-button.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/extend-trial-button.tsx @@ -34,9 +34,6 @@ export default function ExtendTrialButton({ disabled={isButtonDisabled} isLoading={isLoading} loadingLabel={t('processing_uppercase') + '…'} - bs3Props={{ - loading: isLoading ? t('processing_uppercase') + '…' : buttonText, - }} > {buttonText} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/cancel-ai-add-on-modal.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/cancel-ai-add-on-modal.tsx index 87d63bc44e..41c006507a 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/cancel-ai-add-on-modal.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/cancel-ai-add-on-modal.tsx @@ -107,11 +107,6 @@ export function CancelAiAddOnModal() { isLoading={inflight} loadingLabel={t('processing_uppercase') + '…'} onClick={handleConfirmChange} - bs3Props={{ - loading: inflight - ? t('processing_uppercase') + '…' - : t('cancel_add_on'), - }} > {t('cancel_add_on')} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/change-to-group-modal.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/change-to-group-modal.tsx index 010522a357..6ebfc3e7e3 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/change-to-group-modal.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/change-to-group-modal.tsx @@ -22,9 +22,7 @@ import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox' import { useContactUsModal } from '@/shared/hooks/use-contact-us-modal' import { UserProvider } from '@/shared/context/user-context' import OLButton from '@/features/ui/components/ol/ol-button' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import OLNotification from '@/features/ui/components/ol/ol-notification' -import { bsVersion } from '@/features/utils/bootstrap-5' const educationalPercentDiscount = 40 @@ -79,8 +77,6 @@ function GroupPrice({ })} - } /> - {t('x_price_per_user', { @@ -221,10 +217,7 @@ export function ChangeToGroupModal() {
    {t('plan')} {groupPlans.plans.map(option => ( -
    +
    {t('upgrade_now')} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/confirm-change-plan-modal.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/confirm-change-plan-modal.tsx index 0a9a5841eb..08cbf1743f 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/confirm-change-plan-modal.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/confirm-change-plan-modal.tsx @@ -112,11 +112,6 @@ export function ConfirmChangePlanModal() { isLoading={inflight} loadingLabel={t('processing_uppercase') + '…'} onClick={handleConfirmChange} - bs3Props={{ - loading: inflight - ? t('processing_uppercase') + '…' - : t('change_plan'), - }} > {t('change_plan')} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/keep-current-plan-modal.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/keep-current-plan-modal.tsx index e6bbe4942f..793c570e25 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/keep-current-plan-modal.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/keep-current-plan-modal.tsx @@ -93,11 +93,6 @@ export function KeepCurrentPlanModal() { isLoading={inflight} loadingLabel={t('processing_uppercase') + '…'} onClick={confirmCancelPendingPlanChange} - bs3Props={{ - loading: inflight - ? t('processing_uppercase') + '…' - : t('revert_pending_plan_change'), - }} > {t('revert_pending_plan_change')} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-3/dropdown-menu-with-ref.tsx b/services/web/frontend/js/features/ui/components/bootstrap-3/dropdown-menu-with-ref.tsx deleted file mode 100644 index 04807ad825..0000000000 --- a/services/web/frontend/js/features/ui/components/bootstrap-3/dropdown-menu-with-ref.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { forwardRef, SyntheticEvent } from 'react' -import classnames from 'classnames' -import RootCloseWrapper from 'react-overlays/lib/RootCloseWrapper' -import { DropdownProps } from 'react-bootstrap' -import { MergeAndOverride } from '../../../../../../types/utils' - -type DropdownMenuWithRefProps = MergeAndOverride< - Pick, - { - children: React.ReactNode - bsRole: 'menu' - menuRef: React.MutableRefObject - className?: string - // The props below are passed by react-bootstrap - labelledBy?: string | undefined - rootCloseEvent?: 'click' | 'mousedown' | undefined - onClose?: (e: SyntheticEvent) => void - } -> - -const DropdownMenuWithRef = forwardRef< - HTMLUListElement, - DropdownMenuWithRefProps ->(function (props, ref) { - const { - children, - bsRole, - bsClass, - className, - open, - pullRight, - labelledBy, - menuRef, - onClose, - rootCloseEvent, - ...rest - } = props - - // expose the menu reference to both the `menuRef` and `ref callback` from react-bootstrap - const handleRefs = (node: HTMLUListElement) => { - if (typeof ref === 'function') { - ref(node) - } - menuRef.current = node - } - - // Implementation as suggested in - // https://react-bootstrap-v3.netlify.app/components/dropdowns/#btn-dropdowns-props-dropdown - return ( - -
      - {children} -
    -
    - ) -}) -DropdownMenuWithRef.displayName = 'DropdownMenuWithRef' - -export default DropdownMenuWithRef diff --git a/services/web/frontend/js/features/ui/components/bootstrap-3/dropdown-toggle-with-tooltip.tsx b/services/web/frontend/js/features/ui/components/bootstrap-3/dropdown-toggle-with-tooltip.tsx deleted file mode 100644 index 9f0dedf622..0000000000 --- a/services/web/frontend/js/features/ui/components/bootstrap-3/dropdown-toggle-with-tooltip.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { forwardRef } from 'react' -import Tooltip from '../../../../shared/components/tooltip' -import classnames from 'classnames' -import { DropdownProps } from 'react-bootstrap' -import { MergeAndOverride } from '../../../../../../types/utils' - -type CustomToggleProps = MergeAndOverride< - Pick, - { - children: React.ReactNode - isOpened: boolean - bsRole: 'toggle' - className?: string - tooltipProps: Omit, 'children'> - } -> - -const DropdownToggleWithTooltip = forwardRef< - HTMLButtonElement, - CustomToggleProps ->(function (props, ref) { - const { - tooltipProps, - isOpened, - children, - bsClass, - className, - open, - bsRole: _bsRole, - ...rest - } = props - - const button = ( - - ) - - return ( - <>{isOpened ? button : {button}} - ) -}) - -DropdownToggleWithTooltip.displayName = 'DropdownToggleWithTooltip' - -export default DropdownToggleWithTooltip diff --git a/services/web/frontend/js/features/ui/components/bootstrap-3/form/form-control.tsx b/services/web/frontend/js/features/ui/components/bootstrap-3/form/form-control.tsx deleted file mode 100644 index 1cd6c617e0..0000000000 --- a/services/web/frontend/js/features/ui/components/bootstrap-3/form/form-control.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { - FormControl as BS3FormControl, - FormControlProps as BS3FormControlProps, -} from 'react-bootstrap' - -type FormControlProps = BS3FormControlProps & { - prepend?: React.ReactNode - append?: React.ReactNode -} - -function FormControl({ prepend, append, ...props }: FormControlProps) { - return ( - <> - {prepend &&
    {prepend}
    } - - {append &&
    {append}
    } - - ) -} - -export default FormControl diff --git a/services/web/frontend/js/features/ui/components/bootstrap-3/toast-container.tsx b/services/web/frontend/js/features/ui/components/bootstrap-3/toast-container.tsx deleted file mode 100644 index 314de8ba77..0000000000 --- a/services/web/frontend/js/features/ui/components/bootstrap-3/toast-container.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import classNames from 'classnames' -import { FC, HTMLProps } from 'react' - -export const ToastContainer: FC> = ({ - children, - className, - ...props -}) => { - return ( -
    - {children} -
    - ) -} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-3/toast.tsx b/services/web/frontend/js/features/ui/components/bootstrap-3/toast.tsx deleted file mode 100644 index 4c35ddf4b5..0000000000 --- a/services/web/frontend/js/features/ui/components/bootstrap-3/toast.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import classNames from 'classnames' -import { FC, useCallback, useEffect, useRef } from 'react' - -type ToastProps = { - onClose?: () => void - onExited?: () => void - autohide?: boolean - delay?: number - show: boolean - className?: string -} -export const Toast: FC = ({ - children, - delay = 5000, - onClose, - onExited, - autohide, - show, - className, -}) => { - const delayRef = useRef(delay) - const onCloseRef = useRef(onClose) - const onExitedRef = useRef(onExited) - const shouldAutoHide = Boolean(autohide && show) - - const handleTimeout = useCallback(() => { - if (shouldAutoHide) { - onCloseRef.current?.() - onExitedRef.current?.() - } - }, [shouldAutoHide]) - - useEffect(() => { - const timeout = window.setTimeout(handleTimeout, delayRef.current) - return () => window.clearTimeout(timeout) - }, [handleTimeout]) - - if (!show) { - return null - } - - return ( -
    - {children} -
    - ) -} diff --git a/services/web/frontend/js/features/ui/components/bootstrap-3/toggle-button-group.tsx b/services/web/frontend/js/features/ui/components/bootstrap-3/toggle-button-group.tsx deleted file mode 100644 index 2224e6160b..0000000000 --- a/services/web/frontend/js/features/ui/components/bootstrap-3/toggle-button-group.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { - Children, - cloneElement, - isValidElement, - useState, - useEffect, -} from 'react' -import { - ToggleButtonGroup as BS3ToggleButtonGroup, - ToggleButtonGroupProps as BS3ToggleButtonGroupProps, - ToggleButtonProps as BS3ToggleButtonProps, -} from 'react-bootstrap' - -function ToggleButtonGroup({ - children, - value, - defaultValue, - onChange, - ...props -}: BS3ToggleButtonGroupProps) { - const [selectedValue, setSelectedValue] = useState( - defaultValue || (props.type === 'checkbox' ? [] : null) - ) - const isControlled = value !== undefined - - useEffect(() => { - if (isControlled) { - if (props.type === 'radio') { - setSelectedValue(value) - } else { - if (Array.isArray(value)) { - setSelectedValue(Array.from(value)) - } else { - setSelectedValue([value]) - } - } - } - }, [isControlled, value, props.type]) - - const handleButtonClick = (buttonValue: T) => { - if (props.type === 'radio') { - if (!isControlled) { - setSelectedValue(buttonValue) - } - - onChange?.(buttonValue as any) - } else if (props.type === 'checkbox') { - const newValue = Array.isArray(selectedValue) - ? selectedValue.includes(buttonValue) - ? selectedValue.filter(val => val !== buttonValue) // Deselect - : [...selectedValue, buttonValue] // Select - : [buttonValue] // Initial selection if value is not array yet - - if (!isControlled) { - setSelectedValue(newValue) - } - - onChange?.(newValue) - } - } - - // Clone children and add custom onClick handlers - const modifiedChildren = Children.map(children, child => { - if (isValidElement(child)) { - const childElement = child as React.ReactElement< - BS3ToggleButtonProps & { active?: boolean } - > - - const isActive = - props.type === 'radio' - ? selectedValue === childElement.props.value - : Array.isArray(selectedValue) && - selectedValue.includes(childElement.props.value as T) - - return cloneElement(childElement, { - onClick: () => { - handleButtonClick(childElement.props.value as T) - }, - active: isActive, - }) - } - - return child - }) - - return ( - {}} - > - {modifiedChildren} - - ) -} - -export default ToggleButtonGroup diff --git a/services/web/frontend/js/features/ui/components/bootstrap-5/bootstrap-version-switcher.tsx b/services/web/frontend/js/features/ui/components/bootstrap-5/bootstrap-version-switcher.tsx deleted file mode 100644 index 0bdfcca31f..0000000000 --- a/services/web/frontend/js/features/ui/components/bootstrap-5/bootstrap-version-switcher.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { isBootstrap5 } from '@/features/utils/bootstrap-5' - -type BootstrapVersionSwitcherProps = { - bs3?: React.ReactNode - bs5?: React.ReactNode -} - -function BootstrapVersionSwitcher({ - bs3, - bs5, -}: BootstrapVersionSwitcherProps): React.ReactElement { - return <>{isBootstrap5() ? bs5 : bs3} -} - -export default BootstrapVersionSwitcher diff --git a/services/web/frontend/js/features/ui/components/ol/icons/ol-tag-icon.tsx b/services/web/frontend/js/features/ui/components/ol/icons/ol-tag-icon.tsx index 8e81387bbe..4a2fbadec9 100644 --- a/services/web/frontend/js/features/ui/components/ol/icons/ol-tag-icon.tsx +++ b/services/web/frontend/js/features/ui/components/ol/icons/ol-tag-icon.tsx @@ -1,12 +1,5 @@ -import Icon from '@/shared/components/icon' import MaterialIcon from '@/shared/components/material-icon' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' export default function OLTagIcon() { - return ( - } - bs5={} - /> - ) + return } diff --git a/services/web/frontend/js/features/ui/components/ol/ol-badge.tsx b/services/web/frontend/js/features/ui/components/ol/ol-badge.tsx index 0e88529a86..47fd163d5c 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-badge.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-badge.tsx @@ -1,40 +1,7 @@ -import { Label } from 'react-bootstrap' import Badge from '@/features/ui/components/bootstrap-5/badge' -import BS3Badge from '@/shared/components/badge' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -type OLBadgeProps = React.ComponentProps & { - bs3Props?: { - bsStyle?: React.ComponentProps['bsStyle'] | null - } -} - -function OLBadge(props: OLBadgeProps) { - const { bs3Props, ...rest } = props - - let bs3BadgeProps: React.ComponentProps = { - prepend: rest.prepend, - children: rest.children, - className: rest.className, - bsStyle: rest.bg, - } - - if (bs3Props) { - const { bsStyle, ...restBs3Props } = bs3Props - - bs3BadgeProps = { - ...bs3BadgeProps, - ...restBs3Props, - bsStyle: 'bsStyle' in bs3Props ? bsStyle : rest.bg, - } - } - - return ( - } - bs5={} - /> - ) +function OLBadge(props: React.ComponentProps) { + return } export default OLBadge diff --git a/services/web/frontend/js/features/ui/components/ol/ol-button-group.tsx b/services/web/frontend/js/features/ui/components/ol/ol-button-group.tsx index c9a9d1188b..dc26595aac 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-button-group.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-button-group.tsx @@ -1,30 +1,7 @@ import { ButtonGroup, ButtonGroupProps } from 'react-bootstrap-5' -import { - ButtonGroup as BS3ButtonGroup, - ButtonGroupProps as BS3ButtonGroupProps, -} from 'react-bootstrap' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' -type OLButtonGroupProps = ButtonGroupProps & { - bs3Props?: Record -} - -function OLButtonGroup({ bs3Props, as, ...rest }: OLButtonGroupProps) { - const bs3ButtonGroupProps: BS3ButtonGroupProps = { - children: rest.children, - className: rest.className, - vertical: rest.vertical, - ...getAriaAndDataProps(rest), - ...bs3Props, - } - - return ( - } - bs5={} - /> - ) +function OLButtonGroup({ as, ...rest }: ButtonGroupProps) { + return } export default OLButtonGroup diff --git a/services/web/frontend/js/features/ui/components/ol/ol-button-toolbar.tsx b/services/web/frontend/js/features/ui/components/ol/ol-button-toolbar.tsx index 16d85c29eb..377228f72d 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-button-toolbar.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-button-toolbar.tsx @@ -1,31 +1,7 @@ import { ButtonToolbar, ButtonToolbarProps } from 'react-bootstrap-5' -import { - ButtonToolbar as BS3ButtonToolbar, - ButtonToolbarProps as BS3ButtonToolbarProps, -} from 'react-bootstrap' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' -type OLButtonToolbarProps = ButtonToolbarProps & { - bs3Props?: Record -} - -function OLButtonToolbar(props: OLButtonToolbarProps) { - const { bs3Props, ...rest } = props - - const bs3ButtonToolbarProps: BS3ButtonToolbarProps = { - children: rest.children, - className: rest.className, - ...getAriaAndDataProps(rest), - ...bs3Props, - } - - return ( - } - bs5={} - /> - ) +function OLButtonToolbar(props: ButtonToolbarProps) { + return } export default OLButtonToolbar diff --git a/services/web/frontend/js/features/ui/components/ol/ol-button.tsx b/services/web/frontend/js/features/ui/components/ol/ol-button.tsx index 6df9266744..3f5e06e8c4 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-button.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-button.tsx @@ -1,132 +1,13 @@ import { forwardRef } from 'react' -import BootstrapVersionSwitcher from '../bootstrap-5/bootstrap-version-switcher' -import { Button as BS3Button } from 'react-bootstrap' import type { ButtonProps } from '@/features/ui/components/types/button-props' -import type { ButtonProps as BS3ButtonPropsBase } from 'react-bootstrap' import Button from '../bootstrap-5/button' -import classnames from 'classnames' -import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' -import { callFnsInSequence } from '@/utils/functions' -import Icon from '@/shared/components/icon' -export type BS3ButtonSize = 'xsmall' | 'sm' | 'medium' | 'lg' +export type OLButtonProps = ButtonProps -export type OLButtonProps = ButtonProps & { - bs3Props?: { - loading?: React.ReactNode - bsSize?: BS3ButtonSize - block?: boolean - className?: string - onMouseOver?: React.MouseEventHandler - onMouseOut?: React.MouseEventHandler - onFocus?: React.FocusEventHandler - onBlur?: React.FocusEventHandler - } -} +const OLButton = forwardRef((props, ref) => { + return
    } - bs5={ - - {children} - - } - /> + + {children} + ) } diff --git a/services/web/frontend/js/features/ui/components/ol/ol-close-button.tsx b/services/web/frontend/js/features/ui/components/ol/ol-close-button.tsx index 7034392949..414a329eec 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-close-button.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-close-button.tsx @@ -1,6 +1,4 @@ -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import { CloseButton, CloseButtonProps } from 'react-bootstrap-5' -import classNames from 'classnames' import { useTranslation } from 'react-i18next' import { forwardRef } from 'react' @@ -8,25 +6,7 @@ const OLCloseButton = forwardRef( (props, ref) => { const { t } = useTranslation() - const bs3CloseButtonProps: React.ButtonHTMLAttributes = { - className: classNames('close', props.className), - onClick: props.onClick, - onMouseOver: props.onMouseOver, - onMouseOut: props.onMouseOut, - - 'aria-label': t('close'), - } - - return ( - - - - } - bs5={} - /> - ) + return } ) diff --git a/services/web/frontend/js/features/ui/components/ol/ol-col.tsx b/services/web/frontend/js/features/ui/components/ol/ol-col.tsx index e2c7c1ce7b..dc70ca23c8 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-col.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-col.tsx @@ -1,73 +1,7 @@ import { Col } from 'react-bootstrap-5' -import { Col as BS3Col } from 'react-bootstrap' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -type OLColProps = React.ComponentProps & { - bs3Props?: Record -} - -function OLCol(props: OLColProps) { - const { bs3Props, ...rest } = props - - const getBs3Sizes = (obj: typeof rest) => { - const bs5ToBs3SizeMap = { - xs: 'xs', - sm: 'xs', - md: 'sm', - lg: 'md', - xl: 'lg', - xxl: undefined, - } as const - - const isBs5ToBs3SizeMapKey = ( - key: string - ): key is keyof typeof bs5ToBs3SizeMap => { - return key in bs5ToBs3SizeMap - } - - const sizes = Object.entries(obj).reduce( - (prev, [key, value]) => { - if (isBs5ToBs3SizeMapKey(key)) { - const bs3Size = bs5ToBs3SizeMap[key] - - if (bs3Size) { - if (typeof value === 'object') { - prev[bs3Size] = value.span - prev[`${bs3Size}Offset`] = value.offset - } else { - prev[bs3Size] = value - } - } - } - - return prev - }, - {} as Record - ) - - // Add a default sizing for `col-xs-12` if no sizing is available - if ( - !Object.keys(sizes).some(key => ['xs', 'sm', 'md', 'lg'].includes(key)) - ) { - sizes.xs = 12 - } - - return sizes - } - - const bs3ColProps: React.ComponentProps = { - children: rest.children, - className: rest.className, - ...getBs3Sizes(rest), - ...bs3Props, - } - - return ( - } - bs5={} - /> - ) +function OLCol(props: React.ComponentProps) { + return } export default OLCol diff --git a/services/web/frontend/js/features/ui/components/ol/ol-dropdown-menu-item.tsx b/services/web/frontend/js/features/ui/components/ol/ol-dropdown-menu-item.tsx index 59567ecce9..20d05d5346 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-dropdown-menu-item.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-dropdown-menu-item.tsx @@ -1,43 +1,13 @@ -import { MenuItem, MenuItemProps } from 'react-bootstrap' import { DropdownItem } from '@/features/ui/components/bootstrap-5/dropdown-menu' import { DropdownItemProps } from '@/features/ui/components/types/dropdown-menu-props' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import DropdownListItem from '@/features/ui/components/bootstrap-5/dropdown-list-item' -type OLDropdownMenuItemProps = DropdownItemProps & { - bs3Props?: MenuItemProps -} - // This represents a menu item. It wraps the item within an
  • element. -function OLDropdownMenuItem(props: OLDropdownMenuItemProps) { - const { bs3Props, ...rest } = props - - const bs3MenuItemProps: MenuItemProps = { - children: rest.leadingIcon ? ( - <> - {rest.leadingIcon} -   - {rest.children} - - ) : ( - rest.children - ), - onClick: rest.onClick, - href: rest.href, - download: rest.download, - eventKey: rest.eventKey, - ...bs3Props, - } - +function OLDropdownMenuItem(props: DropdownItemProps) { return ( - } - bs5={ - - - - } - /> + + + ) } diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-checkbox.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-checkbox.tsx index 27de2f4ed3..d82e49ea2d 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-form-checkbox.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-form-checkbox.tsx @@ -1,7 +1,4 @@ import { Form, FormCheckProps } from 'react-bootstrap-5' -import { Checkbox as BS3Checkbox } from 'react-bootstrap' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' import { MergeAndOverride } from '../../../../../../types/utils' import FormText from '../bootstrap-5/form/form-text' @@ -9,128 +6,36 @@ type OLFormCheckboxProps = MergeAndOverride< FormCheckProps, { inputRef?: React.MutableRefObject - bs3Props?: Record } & ( | { description: string; id: string } | { description?: undefined; id?: string } ) > -type RadioButtonProps = { - checked?: boolean - className?: string - description?: string - disabled?: boolean - id: string - name?: string - onChange: (e: React.ChangeEvent) => void - required?: boolean - label: React.ReactElement | string - value: string -} - function OLFormCheckbox(props: OLFormCheckboxProps) { - const { bs3Props, inputRef, ...rest } = props + const { inputRef, ...rest } = props - const bs3FormCheckboxProps: React.ComponentProps = { - children: rest.label, - checked: rest.checked, - value: rest.value, - id: rest.id, - name: rest.name, - required: rest.required, - readOnly: rest.readOnly, - disabled: rest.disabled, - inline: rest.inline, - title: rest.title, - autoComplete: rest.autoComplete, - defaultChecked: rest.defaultChecked, - className: rest.className, - onChange: rest.onChange as (e: React.ChangeEvent) => void, - inputRef: node => { - if (inputRef) { - inputRef.current = node - } - }, - ...getAriaAndDataProps(rest), - ...bs3Props, - } - - return ( - - ) : ( - - ) - } - bs5={ - rest.type === 'radio' ? ( - - {rest.label} - {rest.description && ( - - {rest.description} - - )} - - } - /> - ) : ( - - ) + return rest.type === 'radio' ? ( + + {rest.label} + {rest.description && ( + + {rest.description} + + )} + } /> - ) -} - -function BS3Radio(props: RadioButtonProps) { - const { - label, - checked, - className, - description, - disabled, - id, - name, - onChange, - required, - value, - } = props - return ( -
    - - {' '} - {description && ( - - )} -
    + ) : ( + ) } diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-control.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-control.tsx index 398d2556b3..bae3282c6f 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-form-control.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-form-control.tsx @@ -1,96 +1,27 @@ -import { forwardRef, ComponentProps, useCallback } from 'react' -import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' +import { forwardRef } from 'react' import FormControl, { type OLBS5FormControlProps, } from '@/features/ui/components/bootstrap-5/form/form-control' -import BS3FormControl from '@/features/ui/components/bootstrap-3/form/form-control' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import OLSpinner from '@/features/ui/components/ol/ol-spinner' -import Icon from '@/shared/components/icon' import type { BsPrefixRefForwardingComponent } from 'react-bootstrap-5/helpers' type OLFormControlProps = OLBS5FormControlProps & { - bs3Props?: Record 'data-ol-dirty'?: unknown 'main-field'?: any // For the CM6's benefit in the editor search panel loading?: boolean } -type BS3FormControlProps = ComponentProps & { - 'main-field'?: any -} - const OLFormControl: BsPrefixRefForwardingComponent< 'input', OLFormControlProps > = forwardRef((props, ref) => { - const { bs3Props, append, ...rest } = props - - // Use a callback so that the ref passed to the BS3 FormControl is stable - const bs3InputRef = useCallback( - (inputElement: HTMLInputElement) => { - if (typeof ref === 'function') { - ref(inputElement) - } else if (ref) { - ref.current = inputElement - } - }, - [ref] - ) - - let bs3FormControlProps: BS3FormControlProps = { - inputRef: bs3InputRef, - componentClass: rest.as, - bsSize: rest.size, - id: rest.id, - name: rest.name, - className: rest.className, - style: rest.style, - type: rest.type, - value: rest.value, - defaultValue: rest.defaultValue, - required: rest.required, - disabled: rest.disabled, - placeholder: rest.placeholder, - readOnly: rest.readOnly, - autoComplete: rest.autoComplete, - autoFocus: rest.autoFocus, - minLength: rest.minLength, - maxLength: rest.maxLength, - onChange: rest.onChange as BS3FormControlProps['onChange'], - onKeyDown: rest.onKeyDown as BS3FormControlProps['onKeyDown'], - onFocus: rest.onFocus as BS3FormControlProps['onFocus'], - onBlur: rest.onBlur as BS3FormControlProps['onBlur'], - onInvalid: rest.onInvalid as BS3FormControlProps['onInvalid'], - onPaste: rest.onPaste as BS3FormControlProps['onPaste'], - prepend: rest.prepend, - size: rest.htmlSize, - 'main-field': rest['main-field'], - children: rest.children, - ...bs3Props, - } - - bs3FormControlProps = { - ...bs3FormControlProps, - ...getAriaAndDataProps(rest), - 'data-ol-dirty': rest['data-ol-dirty'], - } as typeof bs3FormControlProps & Record + const { append, ...rest } = props return ( - : append} - /> - } - bs5={ - : append} - /> - } + : append} /> ) }) diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-feedback.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-feedback.tsx index 2ff193e0d8..1c850c1ffc 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-form-feedback.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-form-feedback.tsx @@ -1,38 +1,14 @@ import { Form } from 'react-bootstrap-5' -import { - HelpBlock as BS3HelpBlock, - HelpBlockProps as BS3HelpBlockProps, -} from 'react-bootstrap' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import { ComponentProps } from 'react' -import classnames from 'classnames' import FormFeedback from '@/features/ui/components/bootstrap-5/form/form-feedback' type OLFormFeedbackProps = Pick< ComponentProps, 'type' | 'className' | 'children' -> & { - bs3Props?: Record -} +> function OLFormFeedback(props: OLFormFeedbackProps) { - const { bs3Props, children, ...bs5Props } = props - - const bs3HelpBlockProps: BS3HelpBlockProps = { - className: classnames( - bs5Props.className, - bs5Props.type === 'invalid' ? 'invalid-only' : null - ), - children, - ...bs3Props, - } - - return ( - } - bs5={{children}} - /> - ) + return } export default OLFormFeedback diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-group.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-group.tsx index 1da249865b..8ccc974d4e 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-form-group.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-form-group.tsx @@ -1,42 +1,8 @@ import { FormGroupProps } from 'react-bootstrap-5' -import { - FormGroup as BS3FormGroup, - FormGroupProps as BS3FormGroupProps, - FormControl, -} from 'react-bootstrap' import FormGroup from '@/features/ui/components/bootstrap-5/form/form-group' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -import classNames from 'classnames' -type OLFormGroupProps = FormGroupProps & { - bs3Props?: { - withFeedback?: boolean - hiddenLabel?: boolean - validationState?: BS3FormGroupProps['validationState'] - } -} - -function OLFormGroup(props: OLFormGroupProps) { - const { bs3Props, className, ...rest } = props - const { withFeedback, hiddenLabel, ...bs3PropsRest } = bs3Props || {} - - const bs3FormGroupProps: BS3FormGroupProps = { - controlId: rest.controlId, - className: classNames(className, { 'hidden-label': hiddenLabel }), - ...bs3PropsRest, - } - - return ( - - {rest.children} - {withFeedback ? : null} - - } - bs5={} - /> - ) +function OLFormGroup(props: FormGroupProps) { + return } export default OLFormGroup diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-label.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-label.tsx index 5ce2f7e556..1e1038f9e3 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-form-label.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-form-label.tsx @@ -1,28 +1,7 @@ import { Form } from 'react-bootstrap-5' -import { ControlLabel as BS3FormLabel } from 'react-bootstrap' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -type OLFormLabelProps = React.ComponentProps<(typeof Form)['Label']> & { - bs3Props?: Record -} - -function OLFormLabel(props: OLFormLabelProps) { - const { bs3Props, ...rest } = props - - const bs3FormLabelProps: React.ComponentProps = { - children: rest.children, - htmlFor: rest.htmlFor, - srOnly: rest.visuallyHidden, - className: rest.className, - ...bs3Props, - } - - return ( - } - bs5={} - /> - ) +function OLFormLabel(props: React.ComponentProps<(typeof Form)['Label']>) { + return } export default OLFormLabel diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-select.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-select.tsx index f9f757abcb..dfcd147dfd 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-form-select.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-form-select.tsx @@ -1,56 +1,9 @@ import { forwardRef } from 'react' import { Form, FormSelectProps } from 'react-bootstrap-5' -import { - FormControl as BS3FormControl, - FormControlProps as BS3FormControlProps, -} from 'react-bootstrap' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' -type OLFormSelectProps = FormSelectProps & { - bs3Props?: Record -} - -const OLFormSelect = forwardRef( +const OLFormSelect = forwardRef( (props, ref) => { - const { bs3Props, ...bs5Props } = props - - const bs3FormSelectProps: BS3FormControlProps = { - children: bs5Props.children, - bsSize: bs5Props.size, - name: bs5Props.name, - value: bs5Props.value, - defaultValue: bs5Props.defaultValue, - disabled: bs5Props.disabled, - onChange: bs5Props.onChange as BS3FormControlProps['onChange'], - required: bs5Props.required, - placeholder: bs5Props.placeholder, - className: bs5Props.className, - inputRef: (inputElement: HTMLInputElement) => { - if (typeof ref === 'function') { - ref(inputElement as unknown as HTMLSelectElement) - } else if (ref) { - ref.current = inputElement as unknown as HTMLSelectElement - } - }, - ...bs3Props, - } - - // Get all `aria-*` and `data-*` attributes - const extraProps = getAriaAndDataProps(bs5Props) - - return ( - - } - bs5={} - /> - ) + return } ) OLFormSelect.displayName = 'OLFormSelect' diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-switch.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-switch.tsx index b751642d8b..a9a6ffe041 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-form-switch.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-form-switch.tsx @@ -1,53 +1,19 @@ import { FormCheck, FormCheckProps, FormLabel } from 'react-bootstrap-5' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' type OLFormSwitchProps = FormCheckProps & { inputRef?: React.MutableRefObject - bs3Props?: React.InputHTMLAttributes } function OLFormSwitch(props: OLFormSwitchProps) { - const { bs3Props, inputRef, label, id, ...rest } = props - - const bs3FormSwitchProps: React.InputHTMLAttributes = { - id, - checked: rest.checked, - required: rest.required, - readOnly: rest.readOnly, - disabled: rest.disabled, - autoComplete: rest.autoComplete, - defaultChecked: rest.defaultChecked, - onChange: rest.onChange as (e: React.ChangeEvent) => void, - ...getAriaAndDataProps(rest), - ...bs3Props, - } + const { inputRef, label, id, ...rest } = props return ( - - - -
  • - } - bs5={ - <> - - - {label} - - - } - /> + <> + + + {label} + + ) } diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form-text.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form-text.tsx index d465eb4050..d35654fa79 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-form-text.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-form-text.tsx @@ -1,36 +1,9 @@ import FormText, { FormTextProps, - getFormTextClass, } from '@/features/ui/components/bootstrap-5/form/form-text' -import PolymorphicComponent from '@/shared/components/polymorphic-component' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -import classnames from 'classnames' -import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' -type OLFormTextProps = FormTextProps & { - bs3Props?: Record -} - -function OLFormText({ as = 'div', ...props }: OLFormTextProps) { - const { bs3Props, ...rest } = props - - const bs3HelpBlockProps = { - children: rest.children, - className: classnames('small', rest.className, getFormTextClass(rest.type)), - ...bs3Props, - } as const satisfies React.ComponentProps - - // Get all `aria-*` and `data-*` attributes - const extraProps = getAriaAndDataProps(rest) - - return ( - - } - bs5={} - /> - ) +function OLFormText({ as = 'div', ...rest }: FormTextProps) { + return } export default OLFormText diff --git a/services/web/frontend/js/features/ui/components/ol/ol-form.tsx b/services/web/frontend/js/features/ui/components/ol/ol-form.tsx index 32fd5073ea..724578769e 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-form.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-form.tsx @@ -1,45 +1,8 @@ import { Form } from 'react-bootstrap-5' -import { Form as BS3Form, FormProps as BS3FormProps } from 'react-bootstrap' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import { ComponentProps } from 'react' -import classnames from 'classnames' -import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' -type OLFormProps = ComponentProps & { - bs3Props?: ComponentProps -} - -function OLForm(props: OLFormProps) { - const { bs3Props, ...rest } = props - - const bs3FormProps: BS3FormProps = { - componentClass: rest.as, - children: rest.children, - id: rest.id, - onSubmit: rest.onSubmit as BS3FormProps['onSubmit'], - onClick: rest.onClick as BS3FormProps['onClick'], - name: rest.name, - noValidate: rest.noValidate, - role: rest.role, - ...bs3Props, - } - - const bs3ClassName = classnames( - rest.className, - rest.validated ? 'was-validated' : null - ) - - // Get all `aria-*` and `data-*` attributes - const extraProps = getAriaAndDataProps(rest) - - return ( - - } - bs5={
    } - /> - ) +function OLForm(props: ComponentProps) { + return } export default OLForm diff --git a/services/web/frontend/js/features/ui/components/ol/ol-icon-button.tsx b/services/web/frontend/js/features/ui/components/ol/ol-icon-button.tsx index 5d34b1c2ff..27e5960133 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-icon-button.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-icon-button.tsx @@ -1,54 +1,13 @@ import { forwardRef } from 'react' -import { bs3ButtonProps, BS3ButtonSize } from './ol-button' -import { Button as BS3Button } from 'react-bootstrap' import type { IconButtonProps } from '@/features/ui/components/types/icon-button-props' -import BootstrapVersionSwitcher from '../bootstrap-5/bootstrap-version-switcher' -import Icon, { IconProps } from '@/shared/components/icon' import IconButton from '../bootstrap-5/icon-button' -import { callFnsInSequence } from '@/utils/functions' -export type OLIconButtonProps = IconButtonProps & { - bs3Props?: { - loading?: React.ReactNode - fw?: IconProps['fw'] - className?: string - bsSize?: BS3ButtonSize - onMouseOver?: React.MouseEventHandler - onMouseOut?: React.MouseEventHandler - onFocus?: React.FocusEventHandler - onBlur?: React.FocusEventHandler - } -} +export type OLIconButtonProps = IconButtonProps const OLIconButton = forwardRef( (props, ref) => { - const { bs3Props, ...rest } = props - - const { fw, loading, ...bs3Rest } = bs3Props || {} - - // BS3 OverlayTrigger automatically provides 'onMouseOver', 'onMouseOut', 'onFocus', 'onBlur' event handlers - const bs3FinalProps = { - 'aria-label': rest.accessibilityLabel, - ...bs3ButtonProps(rest), - ...bs3Rest, - onMouseOver: callFnsInSequence(bs3Props?.onMouseOver, rest.onMouseOver), - onMouseOut: callFnsInSequence(bs3Props?.onMouseOut, rest.onMouseOut), - onFocus: callFnsInSequence(bs3Props?.onFocus, rest.onFocus), - onBlur: callFnsInSequence(bs3Props?.onBlur, rest.onBlur), - } - - // BS3 tooltip relies on the 'onMouseOver', 'onMouseOut', 'onFocus', 'onBlur' props // BS5 tooltip relies on the ref - return ( - - {loading || } - - } - bs5={} - /> - ) + return } ) OLIconButton.displayName = 'OLIconButton' diff --git a/services/web/frontend/js/features/ui/components/ol/ol-list-group-item.tsx b/services/web/frontend/js/features/ui/components/ol/ol-list-group-item.tsx index c177ee3a1c..a27457fa7a 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-list-group-item.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-list-group-item.tsx @@ -1,40 +1,13 @@ import { ListGroupItem, ListGroupItemProps } from 'react-bootstrap-5' -import { - ListGroupItem as BS3ListGroupItem, - ListGroupItemProps as BS3ListGroupItemProps, -} from 'react-bootstrap' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' -type OLListGroupItemProps = ListGroupItemProps & { - bs3Props?: BS3ListGroupItemProps -} - -function OLListGroupItem(props: OLListGroupItemProps) { - const { bs3Props, ...rest } = props - - const bs3ListGroupItemProps: BS3ListGroupItemProps = { - children: rest.children, - active: rest.active, - disabled: rest.disabled, - href: rest.href, - onClick: rest.onClick as BS3ListGroupItemProps['onClick'], - ...bs3Props, - } - - const extraProps = getAriaAndDataProps(rest) - const as = rest.as ?? 'button' +function OLListGroupItem(props: ListGroupItemProps) { + const as = props.as ?? 'button' return ( - } - bs5={ - - } + ) } diff --git a/services/web/frontend/js/features/ui/components/ol/ol-list-group.tsx b/services/web/frontend/js/features/ui/components/ol/ol-list-group.tsx index 65af7b3ec3..a28c7e977d 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-list-group.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-list-group.tsx @@ -1,34 +1,9 @@ import { ListGroup, ListGroupProps } from 'react-bootstrap-5' -import { - ListGroup as BS3ListGroup, - ListGroupProps as BS3ListGroupProps, -} from 'react-bootstrap' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' -type OLListGroupProps = ListGroupProps & { - bs3Props?: BS3ListGroupProps -} +function OLListGroup(props: ListGroupProps) { + const as = props.as ?? 'div' -function OLListGroup(props: OLListGroupProps) { - const { bs3Props, ...rest } = props - - const bs3ListGroupProps: BS3ListGroupProps = { - children: rest.children, - role: rest.role, - componentClass: rest.as, - ...bs3Props, - } - - const extraProps = getAriaAndDataProps(rest) - const as = rest.as ?? 'div' - - return ( - } - bs5={} - /> - ) + return } export default OLListGroup diff --git a/services/web/frontend/js/features/ui/components/ol/ol-modal.tsx b/services/web/frontend/js/features/ui/components/ol/ol-modal.tsx index 43dcddcf87..bf20d18eef 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-modal.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-modal.tsx @@ -1,133 +1,37 @@ import { - Modal as BS5Modal, + Modal, ModalProps, ModalHeaderProps, ModalTitleProps, - ModalBody, ModalFooterProps, } from 'react-bootstrap-5' -import { - Modal as BS3Modal, - ModalProps as BS3ModalProps, - ModalHeaderProps as BS3ModalHeaderProps, - ModalTitleProps as BS3ModalTitleProps, - ModalBodyProps as BS3ModalBodyProps, - ModalFooterProps as BS3ModalFooterProps, -} from 'react-bootstrap' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -import AccessibleModal from '@/shared/components/accessible-modal' +import { ModalBodyProps } from 'react-bootstrap-5/ModalBody' type OLModalProps = ModalProps & { - bs3Props?: Record size?: 'sm' | 'lg' onHide: () => void } -type OLModalHeaderProps = ModalHeaderProps & { - bs3Props?: Record -} - -type OLModalTitleProps = ModalTitleProps & { - bs3Props?: Record -} - -type OLModalBodyProps = React.ComponentProps & { - bs3Props?: Record -} - -type OLModalFooterProps = ModalFooterProps & { - bs3Props?: Record -} - export default function OLModal({ children, ...props }: OLModalProps) { - const { bs3Props, ...bs5Props } = props + return {children} +} - const bs3ModalProps: BS3ModalProps = { - bsClass: bs5Props.bsPrefix, - bsSize: bs5Props.size, - show: bs5Props.show, - onHide: bs5Props.onHide, - onExited: bs5Props.onExited, - backdrop: bs5Props.backdrop, - animation: bs5Props.animation, - id: bs5Props.id, - className: bs5Props.className, - backdropClassName: bs5Props.backdropClassName, - ...bs3Props, - } +export function OLModalHeader({ children, ...props }: ModalHeaderProps) { + return {children} +} +export function OLModalTitle({ children, ...props }: ModalTitleProps) { return ( - {children}} - bs5={{children}} - /> + + {children} + ) } -export function OLModalHeader({ children, ...props }: OLModalHeaderProps) { - const { bs3Props, ...bs5Props } = props - - const bs3ModalProps: BS3ModalHeaderProps = { - bsClass: bs5Props.bsPrefix, - onHide: bs5Props.onHide, - closeButton: bs5Props.closeButton, - closeLabel: bs5Props.closeLabel, - } - return ( - {children}} - bs5={{children}} - /> - ) +export function OLModalBody({ children, ...props }: ModalBodyProps) { + return {children} } -export function OLModalTitle({ children, ...props }: OLModalTitleProps) { - const { bs3Props, ...bs5Props } = props - - const bs3ModalProps: BS3ModalTitleProps = { - componentClass: bs5Props.as, - } - - return ( - {children}} - bs5={ - - {children} - - } - /> - ) -} - -export function OLModalBody({ children, ...props }: OLModalBodyProps) { - const { bs3Props, ...bs5Props } = props - - const bs3ModalProps: BS3ModalBodyProps = { - componentClass: bs5Props.as, - className: bs5Props.className, - } - - return ( - {children}} - bs5={{children}} - /> - ) -} - -export function OLModalFooter({ children, ...props }: OLModalFooterProps) { - const { bs3Props, ...bs5Props } = props - - const bs3ModalProps: BS3ModalFooterProps = { - componentClass: bs5Props.as, - className: bs5Props.className, - } - - return ( - {children}} - bs5={{children}} - /> - ) +export function OLModalFooter({ children, ...props }: ModalFooterProps) { + return {children} } diff --git a/services/web/frontend/js/features/ui/components/ol/ol-notification.tsx b/services/web/frontend/js/features/ui/components/ol/ol-notification.tsx index e312d48c91..7ca811a796 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-notification.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-notification.tsx @@ -1,42 +1,10 @@ import Notification from '@/shared/components/notification' -import { Alert, AlertProps } from 'react-bootstrap' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -import classnames from 'classnames' - -type OLNotificationProps = React.ComponentProps & { - bs3Props?: { - icon?: React.ReactElement - className?: string - } -} - -function OLNotification(props: OLNotificationProps) { - const { bs3Props, ...notificationProps } = props - - const alertProps = { - // Map `error` to `danger` - bsStyle: - notificationProps.type === 'error' ? 'danger' : notificationProps.type, - className: classnames(notificationProps.className, bs3Props?.className), - onDismiss: notificationProps.onDismiss, - } as const satisfies AlertProps +function OLNotification(props: React.ComponentProps) { return ( - - {bs3Props?.icon} - {bs3Props?.icon && ' '} - {notificationProps.content} - {notificationProps.action} - - } - bs5={ -
    - -
    - } - /> +
    + +
    ) } diff --git a/services/web/frontend/js/features/ui/components/ol/ol-overlay.tsx b/services/web/frontend/js/features/ui/components/ol/ol-overlay.tsx index 40b7872bc8..bcf2a024c2 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-overlay.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-overlay.tsx @@ -1,61 +1,7 @@ import { Overlay, OverlayProps } from 'react-bootstrap-5' -import { - Overlay as BS3Overlay, - OverlayProps as BS3OverlayProps, -} from 'react-bootstrap' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -type OLOverlayProps = OverlayProps & { - bs3Props?: BS3OverlayProps -} - -function OLOverlay(props: OLOverlayProps) { - const { bs3Props, ...bs5Props } = props - - let bs3OverlayProps: BS3OverlayProps = { - children: bs5Props.children, - target: bs5Props.target as BS3OverlayProps['target'], - container: bs5Props.container, - containerPadding: bs5Props.containerPadding, - show: bs5Props.show, - rootClose: bs5Props.rootClose, - animation: bs5Props.transition, - onHide: bs5Props.onHide as BS3OverlayProps['onHide'], - onEnter: bs5Props.onEnter as BS3OverlayProps['onEnter'], - onEntering: bs5Props.onEntering as BS3OverlayProps['onEntering'], - onEntered: bs5Props.onEntered as BS3OverlayProps['onEntered'], - onExit: bs5Props.onExit as BS3OverlayProps['onExit'], - onExiting: bs5Props.onExiting as BS3OverlayProps['onExiting'], - onExited: bs5Props.onExited as BS3OverlayProps['onExited'], - } - - if (bs5Props.placement) { - const bs3PlacementOptions = [ - 'top', - 'right', - 'bottom', - 'left', - ] satisfies Array< - Extract - > - - for (const placement of bs3PlacementOptions) { - // BS5 has more placement options than BS3, such as "left-start", so these are mapped to "left" etc. - if (bs5Props.placement.startsWith(placement)) { - bs3OverlayProps.placement = placement - break - } - } - } - - bs3OverlayProps = { ...bs3OverlayProps, ...bs3Props } - - return ( - } - bs5={} - /> - ) +function OLOverlay(props: OverlayProps) { + return } export default OLOverlay diff --git a/services/web/frontend/js/features/ui/components/ol/ol-page-content-card.tsx b/services/web/frontend/js/features/ui/components/ol/ol-page-content-card.tsx index ba6fc75ff9..549e63a5c1 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-page-content-card.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-page-content-card.tsx @@ -1,24 +1,18 @@ import { Card, CardBody } from 'react-bootstrap-5' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import { FC } from 'react' import classNames from 'classnames' // This wraps the Bootstrap 5 Card component but is restricted to the very // basic way we're using it, which is as a container for page content. The -// Bootstrap 3 equivalent in our codebase is a div with class "card" +// Bootstrap 3 equivalent previously in our codebase is a div with class "card" const OLPageContentCard: FC<{ className?: string }> = ({ children, className, }) => { return ( - {children}
    } - bs5={ - - {children} - - } - /> + + {children} + ) } diff --git a/services/web/frontend/js/features/ui/components/ol/ol-popover.tsx b/services/web/frontend/js/features/ui/components/ol/ol-popover.tsx index fbf74d3225..772084bc22 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-popover.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-popover.tsx @@ -1,83 +1,18 @@ import { forwardRef } from 'react' import { Popover, PopoverProps } from 'react-bootstrap-5' -import { - Popover as BS3Popover, - PopoverProps as BS3PopoverProps, -} from 'react-bootstrap' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' type OLPopoverProps = Omit & { title?: React.ReactNode - bs3Props?: BS3PopoverProps } const OLPopover = forwardRef((props, ref) => { - // BS3 passes in some props automatically so the `props` - // type should be adjusted to reflect the actual received object - const propsCombinedWithAutoInjectedBs3Props = props as OLPopoverProps & - Pick< - BS3PopoverProps, - 'arrowOffsetLeft' | 'arrowOffsetTop' | 'positionLeft' | 'positionTop' - > - - const { - bs3Props, - title, - children, - arrowOffsetLeft, - arrowOffsetTop, - positionLeft, - positionTop, - ...bs5Props - } = propsCombinedWithAutoInjectedBs3Props - - let bs3PopoverProps: BS3PopoverProps = { - children, - arrowOffsetLeft, - arrowOffsetTop, - positionLeft, - positionTop, - title, - id: bs5Props.id, - className: bs5Props.className, - style: bs5Props.style, - } - - if (bs5Props.placement) { - const bs3PlacementOptions = [ - 'top', - 'right', - 'bottom', - 'left', - ] satisfies Array< - Extract - > - - for (const placement of bs3PlacementOptions) { - if (placement === bs5Props.placement) { - bs3PopoverProps.placement = bs5Props.placement - break - } - } - } - - bs3PopoverProps = { ...bs3PopoverProps, ...bs3Props } + const { title, children, ...bs5Props } = props return ( - } - /> - } - bs5={ - - {title && {title}} - {children} - - } - /> + + {title && {title}} + {children} + ) }) OLPopover.displayName = 'OLPopover' diff --git a/services/web/frontend/js/features/ui/components/ol/ol-row.tsx b/services/web/frontend/js/features/ui/components/ol/ol-row.tsx index ef68a6887f..88c05ce102 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-row.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-row.tsx @@ -1,16 +1,7 @@ import { Row } from 'react-bootstrap-5' -import { Row as BS3Row } from 'react-bootstrap' -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -type OLRowProps = React.ComponentProps - -function OLRow(props: OLRowProps) { - return ( - {props.children}} - bs5={} - /> - ) +function OLRow(props: React.ComponentProps) { + return } export default OLRow diff --git a/services/web/frontend/js/features/ui/components/ol/ol-spinner.tsx b/services/web/frontend/js/features/ui/components/ol/ol-spinner.tsx index 615f5acd47..4c1be6b125 100644 --- a/services/web/frontend/js/features/ui/components/ol/ol-spinner.tsx +++ b/services/web/frontend/js/features/ui/components/ol/ol-spinner.tsx @@ -1,29 +1,14 @@ -import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' -import Icon from '@/shared/components/icon' import { Spinner } from 'react-bootstrap-5' -import classNames from 'classnames' export type OLSpinnerSize = 'sm' | 'lg' function OLSpinner({ size = 'sm' }: { size: OLSpinnerSize }) { return ( - - } - bs5={ -