From d458e519180750fff37c42957fe39410dbc8f240 Mon Sep 17 00:00:00 2001 From: Florian Porrmann <58858968+fporrmann@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:31:36 +0200 Subject: [PATCH] GitLab Synchronization: Initial commit --- .../app/src/infrastructure/ExpressLocals.mjs | 1 + .../web/app/src/infrastructure/Features.mjs | 3 + .../web/app/src/util/third-party-icons.mjs | 1 + services/web/config/settings.defaults.js | 21 + .../history/components/change-list/origin.tsx | 1 + .../features/history/services/types/shared.ts | 1 + .../ide-react/components/rail/rail.tsx | 5 +- .../context/editor-manager-context.tsx | 2 +- .../integrations-panel/integrations-panel.tsx | 1 + .../components/new-project-button.tsx | 22 +- .../new-project-button-modal.tsx | 11 + ...me-message-create-new-project-dropdown.tsx | 19 +- .../settings/components/linking-section.tsx | 5 +- .../js/shared/svgs/gitlab-logo-black.tsx | 18 + .../frontend/js/shared/svgs/gitlab-logo.tsx | 32 + .../web/frontend/stylesheets/modules/all.scss | 1 + services/web/locales/en.json | 73 ++ .../app/models/gitlabSyncProjectStates.mjs | 24 + .../app/models/gitlabSyncUserCredentials.mjs | 17 + .../app/src/AccessTokenEncryptorHelper.mjs | 82 ++ .../gitlab-sync/app/src/GitLabApiClient.mjs | 642 +++++++++++++++ .../app/src/GitLabSyncController.mjs | 242 ++++++ .../gitlab-sync/app/src/GitLabSyncHandler.mjs | 255 ++++++ .../gitlab-sync/app/src/GitLabSyncRouter.mjs | 100 +++ .../modules/gitlab-sync/app/src/GitMerge.mjs | 768 ++++++++++++++++++ .../gitlab-sync/app/src/GitSyncErrors.mjs | 28 + .../gitlab-sync/app/src/HistoryManager.mjs | 97 +++ .../gitlab-sync/app/src/SyncStateManager.mjs | 32 + .../gitlab-sync/app/src/TokenManager.mjs | 115 +++ .../js/components/gitlab-integration-card.tsx | 41 + .../js/components/gitlab-sync-widget.tsx | 174 ++++ .../js/components/import-from-gitlab-menu.tsx | 12 + .../import-from-gitlab-modal-wrapper.tsx | 221 +++++ .../modals/git-sync-cannot-export-modal.tsx | 47 ++ .../modals/git-sync-confirm-unlink-modal.tsx | 105 +++ .../modals/git-sync-conflict-modal.tsx | 57 ++ .../modals/git-sync-export-modal.tsx | 229 ++++++ .../modals/git-sync-loading-modal.tsx | 54 ++ .../modals/git-sync-merge-modal.tsx | 129 +++ .../modals/git-sync-merge-overview-modal.tsx | 242 ++++++ .../js/components/modals/git-sync-modal.tsx | 226 ++++++ .../modals/git-sync-need-auth-modal.tsx | 43 + .../modals/git-sync-need-permission-modal.tsx | 60 ++ .../git-sync-unlink-unavailable-modal.tsx | 85 ++ .../frontend/js/types/git-sync-types.ts | 19 + .../frontend/styles/gitlab-sync.scss | 8 + services/web/modules/gitlab-sync/index.mjs | 55 ++ services/web/package.json | 1 - .../unit/src/infrastructure/Features.test.mjs | 5 + services/web/types/exposed-settings.ts | 2 + 50 files changed, 4426 insertions(+), 8 deletions(-) create mode 100644 services/web/frontend/js/shared/svgs/gitlab-logo-black.tsx create mode 100644 services/web/frontend/js/shared/svgs/gitlab-logo.tsx create mode 100644 services/web/modules/gitlab-sync/app/models/gitlabSyncProjectStates.mjs create mode 100644 services/web/modules/gitlab-sync/app/models/gitlabSyncUserCredentials.mjs create mode 100644 services/web/modules/gitlab-sync/app/src/AccessTokenEncryptorHelper.mjs create mode 100644 services/web/modules/gitlab-sync/app/src/GitLabApiClient.mjs create mode 100644 services/web/modules/gitlab-sync/app/src/GitLabSyncController.mjs create mode 100644 services/web/modules/gitlab-sync/app/src/GitLabSyncHandler.mjs create mode 100644 services/web/modules/gitlab-sync/app/src/GitLabSyncRouter.mjs create mode 100644 services/web/modules/gitlab-sync/app/src/GitMerge.mjs create mode 100644 services/web/modules/gitlab-sync/app/src/GitSyncErrors.mjs create mode 100644 services/web/modules/gitlab-sync/app/src/HistoryManager.mjs create mode 100644 services/web/modules/gitlab-sync/app/src/SyncStateManager.mjs create mode 100644 services/web/modules/gitlab-sync/app/src/TokenManager.mjs create mode 100644 services/web/modules/gitlab-sync/frontend/js/components/gitlab-integration-card.tsx create mode 100644 services/web/modules/gitlab-sync/frontend/js/components/gitlab-sync-widget.tsx create mode 100644 services/web/modules/gitlab-sync/frontend/js/components/import-from-gitlab-menu.tsx create mode 100644 services/web/modules/gitlab-sync/frontend/js/components/import-from-gitlab-modal-wrapper.tsx create mode 100644 services/web/modules/gitlab-sync/frontend/js/components/modals/git-sync-cannot-export-modal.tsx create mode 100644 services/web/modules/gitlab-sync/frontend/js/components/modals/git-sync-confirm-unlink-modal.tsx create mode 100644 services/web/modules/gitlab-sync/frontend/js/components/modals/git-sync-conflict-modal.tsx create mode 100644 services/web/modules/gitlab-sync/frontend/js/components/modals/git-sync-export-modal.tsx create mode 100644 services/web/modules/gitlab-sync/frontend/js/components/modals/git-sync-loading-modal.tsx create mode 100644 services/web/modules/gitlab-sync/frontend/js/components/modals/git-sync-merge-modal.tsx create mode 100644 services/web/modules/gitlab-sync/frontend/js/components/modals/git-sync-merge-overview-modal.tsx create mode 100644 services/web/modules/gitlab-sync/frontend/js/components/modals/git-sync-modal.tsx create mode 100644 services/web/modules/gitlab-sync/frontend/js/components/modals/git-sync-need-auth-modal.tsx create mode 100644 services/web/modules/gitlab-sync/frontend/js/components/modals/git-sync-need-permission-modal.tsx create mode 100644 services/web/modules/gitlab-sync/frontend/js/components/modals/git-sync-unlink-unavailable-modal.tsx create mode 100644 services/web/modules/gitlab-sync/frontend/js/types/git-sync-types.ts create mode 100644 services/web/modules/gitlab-sync/frontend/styles/gitlab-sync.scss create mode 100644 services/web/modules/gitlab-sync/index.mjs diff --git a/services/web/app/src/infrastructure/ExpressLocals.mjs b/services/web/app/src/infrastructure/ExpressLocals.mjs index a48cbc56fc..bae4e2deac 100644 --- a/services/web/app/src/infrastructure/ExpressLocals.mjs +++ b/services/web/app/src/infrastructure/ExpressLocals.mjs @@ -424,6 +424,7 @@ export default async function (webRouter, privateApiRouter, publicApiRouter) { cioSiteId: Settings.analytics?.cio?.siteId, linkedInInsightsPartnerId: Settings.analytics?.linkedIn?.partnerId, githubSyncEnabled: !!Settings.githubSync?.clientID && !!Settings.githubSync?.clientSecret, + gitlabSyncEnabled: !!Settings.gitlabSync?.clientID && !!Settings.gitlabSync?.clientSecret, zoteroEnabled: !!Settings.zotero?.clientKey && !!Settings.zotero?.clientSecret, enablePandocConversions: Settings.enablePandocConversions, mixpanelLabsToken: diff --git a/services/web/app/src/infrastructure/Features.mjs b/services/web/app/src/infrastructure/Features.mjs index 2a1a7a38e2..7085b9ca9e 100644 --- a/services/web/app/src/infrastructure/Features.mjs +++ b/services/web/app/src/infrastructure/Features.mjs @@ -16,6 +16,7 @@ const trackChangesModuleAvailable = * @property {Object | undefined} apis.linkedUrlProxy * @property {string | undefined} apis.linkedUrlProxy.url * @property {boolean | undefined} enableGithubSync + * @property {boolean | undefined} enableGitLabSync * @property {boolean | undefined} enableGitBridge * @property {boolean | undefined} enableHomepage * @property {boolean | undefined} enableSaml @@ -61,6 +62,8 @@ const Features = { return Boolean(Settings.disableLinkSharing) === false case 'github-sync': return Boolean(Settings.enableGithubSync) + case 'gitlab-sync': + return Boolean(Settings.enableGitLabSync) case 'git-bridge': return Boolean(Settings.enableGitBridge) case 'oauth': diff --git a/services/web/app/src/util/third-party-icons.mjs b/services/web/app/src/util/third-party-icons.mjs index c049d3d5d6..dba8131df6 100644 --- a/services/web/app/src/util/third-party-icons.mjs +++ b/services/web/app/src/util/third-party-icons.mjs @@ -6,6 +6,7 @@ const thirdPartyIcons = [ { file: 'git.svg', label: 'Git' }, { file: 'github.svg', label: 'GitHub' }, + { file: 'gitlab.svg', label: 'GitLab' }, { file: 'dropbox-transparent.svg', label: 'Dropbox' }, { file: 'zotero_64x64x32.png', label: 'Zotero' }, { file: 'mendeley.svg', label: 'Mendeley' }, diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index 35c7ac477f..92e633306c 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -1076,6 +1076,10 @@ module.exports = { __dirname, '../modules/github-sync/frontend/js/components/github-sync-widget.tsx' ), + Path.resolve( + __dirname, + '../modules/gitlab-sync/frontend/js/components/gitlab-sync-widget.tsx' + ), ], referenceLinkingWidgets: [ Path.resolve( @@ -1095,6 +1099,18 @@ module.exports = { '../modules/github-sync/frontend/js/components/import-from-github-menu.tsx' ), ], + importProjectFromGitLabModalWrapper: [ + Path.resolve( + __dirname, + '../modules/gitlab-sync/frontend/js/components/import-from-gitlab-modal-wrapper.tsx' + ), + ], + importProjectFromGitLabMenu: [ + Path.resolve( + __dirname, + '../modules/gitlab-sync/frontend/js/components/import-from-gitlab-menu.tsx' + ), + ], editorLeftMenuSync: [], editorLeftMenuManageTemplate: [], menubarExtraComponents: [ @@ -1162,6 +1178,10 @@ module.exports = { __dirname, '../modules/github-sync/frontend/js/components/github-integration-card.tsx' ), + Path.resolve( + __dirname, + '../modules/gitlab-sync/frontend/js/components/gitlab-integration-card.tsx' + ), Path.resolve( __dirname, '../modules/git-bridge/frontend/js/card/components/git-integration-card.tsx' @@ -1204,6 +1224,7 @@ module.exports = { 'template-gallery', 'git-bridge', 'github-sync', + 'gitlab-sync', 'zotero', ], viewIncludes: {}, diff --git a/services/web/frontend/js/features/history/components/change-list/origin.tsx b/services/web/frontend/js/features/history/components/change-list/origin.tsx index 98d8767019..2311328456 100644 --- a/services/web/frontend/js/features/history/components/change-list/origin.tsx +++ b/services/web/frontend/js/features/history/components/change-list/origin.tsx @@ -9,6 +9,7 @@ function Origin({ origin }: Pick) { if (origin?.kind === 'upload') result = t('history_entry_origin_upload') if (origin?.kind === 'git-bridge') result = t('history_entry_origin_git') if (origin?.kind === 'github') result = t('history_entry_origin_github') + if (origin?.kind === 'gitlab') result = t('history_entry_origin_gitlab') if (result) { return ({result}) diff --git a/services/web/frontend/js/features/history/services/types/shared.ts b/services/web/frontend/js/features/history/services/types/shared.ts index 1e66f64afe..a67075cd85 100644 --- a/services/web/frontend/js/features/history/services/types/shared.ts +++ b/services/web/frontend/js/features/history/services/types/shared.ts @@ -20,6 +20,7 @@ export interface Meta { | 'upload' | 'git-bridge' | 'github' + | 'gitlab' | 'history-resync' | 'history-migration' } diff --git a/services/web/frontend/js/features/ide-react/components/rail/rail.tsx b/services/web/frontend/js/features/ide-react/components/rail/rail.tsx index 796fe5d369..ea0f08e507 100644 --- a/services/web/frontend/js/features/ide-react/components/rail/rail.tsx +++ b/services/web/frontend/js/features/ide-react/components/rail/rail.tsx @@ -71,7 +71,7 @@ export const RailLayout = () => { const { features } = useProjectContext() const { isRestrictedTokenMember } = useEditorContext() const gitBridgeEnabled = getMeta('ol-gitBridgeEnabled') - const { isOverleaf, githubSyncEnabled, zoteroEnabled } = getMeta('ol-ExposedSettings') + const { isOverleaf, githubSyncEnabled, gitlabSyncEnabled, zoteroEnabled } = getMeta('ol-ExposedSettings') const { view, setSettingsShown, focusMode } = useLayoutContext() @@ -119,7 +119,7 @@ export const RailLayout = () => { icon: 'integration_instructions', title: t('integrations'), component: , - hide: !isOverleaf && !gitBridgeEnabled && !githubSyncEnabled && !zoteroEnabled, + hide: !isOverleaf && !gitBridgeEnabled && !githubSyncEnabled && !gitlabSyncEnabled && !zoteroEnabled, }, { key: 'review-panel', @@ -149,6 +149,7 @@ export const RailLayout = () => { isOverleaf, gitBridgeEnabled, githubSyncEnabled, + gitlabSyncEnabled, zoteroEnabled, ] ) diff --git a/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx b/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx index d697db73a2..9aa6cd452a 100644 --- a/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx @@ -251,7 +251,7 @@ export const EditorManagerProvider: FC = ({ } if ( update.meta.type === 'external' && - (update.meta.source === 'git-bridge' || update.meta.source === 'github') + (update.meta.source === 'git-bridge' || update.meta.source === 'github' || update.meta.source === 'gitlab') ) { return } diff --git a/services/web/frontend/js/features/integrations-panel/integrations-panel.tsx b/services/web/frontend/js/features/integrations-panel/integrations-panel.tsx index c931dd79b8..f7f54bebb2 100644 --- a/services/web/frontend/js/features/integrations-panel/integrations-panel.tsx +++ b/services/web/frontend/js/features/integrations-panel/integrations-panel.tsx @@ -12,6 +12,7 @@ const integrationPanelComponents = allIntegrationPanelComponents.filter( ({ path }) => (getMeta('ol-gitBridgeEnabled') || !path.includes('git-bridge')) && (getMeta('ol-ExposedSettings').githubSyncEnabled || !path.includes('github-sync')) && + (getMeta('ol-ExposedSettings').gitlabSyncEnabled || !path.includes('gitlab-sync')) && (getMeta('ol-ExposedSettings').zoteroEnabled || !path.includes('zotero')) ) diff --git a/services/web/frontend/js/features/project-list/components/new-project-button.tsx b/services/web/frontend/js/features/project-list/components/new-project-button.tsx index 90cba4d730..c65ad92899 100644 --- a/services/web/frontend/js/features/project-list/components/new-project-button.tsx +++ b/services/web/frontend/js/features/project-list/components/new-project-button.tsx @@ -58,7 +58,7 @@ function NewProjectButton({ align = 'start', }: NewProjectButtonProps) { const { t } = useTranslation() - const { templateLinks, githubSyncEnabled } = getMeta('ol-ExposedSettings') + const { templateLinks, githubSyncEnabled, gitlabSyncEnabled } = getMeta('ol-ExposedSettings') const [modal, setModal] = useState>(null) const portalTemplates = getMeta('ol-portalTemplates') || [] @@ -167,11 +167,19 @@ function NewProjectButton({ const [importProjectFromGithubMenu] = !githubSyncEnabled ? [] : importOverleafModules( 'importProjectFromGithubMenu' ) + + const [importProjectFromGitLabMenu] = !gitlabSyncEnabled ? [] : importOverleafModules( + 'importProjectFromGitLabMenu' + ) const ImportProjectFromGithubMenu: JSXElementConstructor<{ onClick: (e: React.MouseEvent) => void }> = importProjectFromGithubMenu?.import.default + const ImportProjectFromGitLabMenu: JSXElementConstructor<{ + onClick: (e: React.MouseEvent) => void + }> = importProjectFromGitLabMenu?.import.default + return ( <> )} + {ImportProjectFromGitLabMenu && ( +
  • + + handleModalMenuClick(e, { + modalVariant: 'import_from_gitlab', + dropdownMenuEvent: 'import-from-gitlab', + }) + } + /> +
  • + )}