From fdd225e48e6cd461b023c5d0e2b23136786dcc91 Mon Sep 17 00:00:00 2001 From: Valeria Golovina Date: Tue, 28 Oct 2025 18:01:25 +0200 Subject: [PATCH] [gitlab] IJPL-82619: "/uploads/*" links are rendered correctly and targets correct gitlab-based urls * works for comments in the editor, MR timeline notes, MR title, and MR diff comments, and also for title/details of commits in Gitlab toolwindow * doesn't work for the titles in the list of MRs in Gitlab toolwindow GitOrigin-RevId: 7a19d6520edb6f13077fa32f9d5ce7ae8c545834 --- .../graphql/fragment/project.graphql | 1 + .../gitlab/api/dto/GitLabProjectDTO.kt | 6 +++ .../gitlab/mergerequest/data/GitLabProject.kt | 10 ++++ .../ui/GitLabContextDataLoader.kt | 12 +++++ .../ui/details/model/GitLabCommitViewModel.kt | 9 ++-- .../GitLabMergeRequestChangesViewModel.kt | 6 ++- .../GitLabMergeRequestDetailsViewModel.kt | 8 ++-- ...GitLabMergeRequestDiscussionsViewModels.kt | 3 +- ...GitLabMergeRequestTimelineItemViewModel.kt | 46 +++++++++++-------- .../GitLabMergeRequestTimelineViewModel.kt | 14 +++--- .../plugins/gitlab/ui/GitLabUIUtil.kt | 21 +++++++-- .../GitLabMergeRequestDiscussionViewModel.kt | 4 +- .../gitlab/ui/comment/GitLabNoteViewModel.kt | 4 +- .../plugins/gitlab/ui/GitLabUIUtilTest.kt | 14 +++++- 14 files changed, 117 insertions(+), 41 deletions(-) create mode 100644 plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/GitLabContextDataLoader.kt diff --git a/plugins/gitlab/gitlab-core/resources/graphql/fragment/project.graphql b/plugins/gitlab/gitlab-core/resources/graphql/fragment/project.graphql index dad60f65473b..96643b0becbb 100644 --- a/plugins/gitlab/gitlab-core/resources/graphql/fragment/project.graphql +++ b/plugins/gitlab/gitlab-core/resources/graphql/fragment/project.graphql @@ -1,4 +1,5 @@ fragment project on Project { + id name nameWithNamespace path diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/api/dto/GitLabProjectDTO.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/api/dto/GitLabProjectDTO.kt index 91f7064d0967..538e5aede199 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/api/dto/GitLabProjectDTO.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/api/dto/GitLabProjectDTO.kt @@ -1,15 +1,20 @@ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.plugins.gitlab.api.dto +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty import com.intellij.collaboration.api.dto.GraphQLFragment import com.intellij.openapi.util.NlsSafe import org.jetbrains.annotations.Nls +import org.jetbrains.plugins.gitlab.api.GitLabGid +import org.jetbrains.plugins.gitlab.api.GitLabGidData import org.jetbrains.plugins.gitlab.api.SinceGitLab // namespace can be null for some reason, so we need to parse paths manually @SinceGitLab("12.0") @GraphQLFragment("/graphql/fragment/project.graphql") data class GitLabProjectDTO( + @JsonProperty("id") private val _id: String, val name: @Nls String, val nameWithNamespace: @Nls String, val path: @NlsSafe String, @@ -24,6 +29,7 @@ data class GitLabProjectDTO( @SinceGitLab("16.8") val allowsMultipleMergeRequestReviewers: Boolean?, val repository: Repository? ) { + @JsonIgnore val id: GitLabGid = GitLabGidData(_id) val ownerPath: @NlsSafe String = fullPath.split("/").dropLast(1).joinToString("/") data class Repository( diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabProject.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabProject.kt index f0dc34e5df28..6ac6031d3cd7 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabProject.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabProject.kt @@ -20,9 +20,11 @@ import org.jetbrains.plugins.gitlab.api.request.* import org.jetbrains.plugins.gitlab.mergerequest.api.dto.GitLabMergeRequestDTO import org.jetbrains.plugins.gitlab.mergerequest.api.request.loadMergeRequest import org.jetbrains.plugins.gitlab.mergerequest.api.request.mergeRequestSetReviewers +import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader import org.jetbrains.plugins.gitlab.util.GitLabProjectMapping import org.jetbrains.plugins.gitlab.util.GitLabRegistry + private val LOG = logger() @CodeReviewDomainEntity @@ -38,6 +40,7 @@ interface GitLabProject { fun getMembersBatches(): Flow> val defaultBranch: String? + val gitLabProjectId: GitLabId suspend fun isMultipleReviewersAllowed(): Boolean /** @@ -50,6 +53,8 @@ interface GitLabProject { suspend fun adjustReviewers(mrIid: String, reviewers: List): GitLabMergeRequestDTO fun reloadData() + + val contextDataLoader: GitLabContextDataLoader } @CodeReviewDomainEntity @@ -77,6 +82,11 @@ class GitLabLazyProject( private val multipleReviewersAllowedRequest = cs.async(Dispatchers.IO, start = CoroutineStart.LAZY) { loadMultipleReviewersAllowed(initialData) } + override val gitLabProjectId: GitLabId = initialData.id + + val uploadFileUrlBase: String = projectMapping.repository.serverPath.toString() + "/-/project/" + gitLabProjectId.guessRestId() + "/uploads/" + + override val contextDataLoader: GitLabContextDataLoader = GitLabContextDataLoader(uploadFileUrlBase) override val mergeRequests by lazy { CachingGitLabProjectMergeRequestsStore(project, cs, api, glMetadata, projectMapping, currentUser, tokenRefreshFlow) diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/GitLabContextDataLoader.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/GitLabContextDataLoader.kt new file mode 100644 index 000000000000..036ef1729d96 --- /dev/null +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/GitLabContextDataLoader.kt @@ -0,0 +1,12 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.plugins.gitlab.mergerequest.ui + +import org.jetbrains.annotations.ApiStatus + +/** + * Class which handles external files loading + */ +@ApiStatus.Internal +class GitLabContextDataLoader( + val uploadFileUrlBase: String +) \ No newline at end of file diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabCommitViewModel.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabCommitViewModel.kt index 8852a50cf0e3..c4493d59805b 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabCommitViewModel.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabCommitViewModel.kt @@ -5,13 +5,16 @@ import com.intellij.openapi.project.Project import org.jetbrains.annotations.ApiStatus import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabCommit import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequest +import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader import org.jetbrains.plugins.gitlab.ui.GitLabUIUtil + @ApiStatus.Internal class GitLabCommitViewModel( private val project: Project, private val mr: GitLabMergeRequest, - model: GitLabCommit + model: GitLabCommit, + private val contextDataLoader: GitLabContextDataLoader ) { internal val sha = model.sha internal val shortId = model.shortId @@ -20,9 +23,9 @@ class GitLabCommitViewModel( internal val authoredDate = model.authoredDate internal val titleHtml = model.fullTitle?.let { - GitLabUIUtil.convertToHtml(project, mr.gitRepository, mr.glProject.projectPath, it) + GitLabUIUtil.convertToHtml(project, mr.gitRepository, mr.glProject.projectPath, it, contextDataLoader.uploadFileUrlBase) } internal val descriptionHtml = model.description?.removePrefix(model.fullTitle.orEmpty())?.let { - GitLabUIUtil.convertToHtml(project, mr.gitRepository, mr.glProject.projectPath, it) + GitLabUIUtil.convertToHtml(project, mr.gitRepository, mr.glProject.projectPath, it, contextDataLoader.uploadFileUrlBase) } } \ No newline at end of file diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestChangesViewModel.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestChangesViewModel.kt index 3f6003fcdcf1..0863f44887f1 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestChangesViewModel.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestChangesViewModel.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import org.jetbrains.annotations.ApiStatus import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequest +import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader @ApiStatus.Internal interface GitLabMergeRequestChangesViewModel : CodeReviewChangesViewModel { @@ -34,7 +35,8 @@ private val LOG = logger() internal class GitLabMergeRequestChangesViewModelImpl( private val project: Project, parentCs: CoroutineScope, - mergeRequest: GitLabMergeRequest + mergeRequest: GitLabMergeRequest, + private val contextDataLoader: GitLabContextDataLoader, ) : GitLabMergeRequestChangesViewModel, CodeReviewChangesViewModel { private val cs = parentCs.childScope(javaClass.name) @@ -54,7 +56,7 @@ internal class GitLabMergeRequestChangesViewModelImpl( override val reviewCommits: SharedFlow> = mergeRequest.changes - .map { it.getCommits().map { commit -> GitLabCommitViewModel(project, mergeRequest, commit) } } + .map { it.getCommits().map { commit -> GitLabCommitViewModel(project, mergeRequest, commit, contextDataLoader) } } .modelFlow(cs, LOG) override val selectedCommitIndex: SharedFlow = reviewCommits.combine(delegate.selectedCommit) { commits, sha -> diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestDetailsViewModel.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestDetailsViewModel.kt index 477103526b0f..3341361fd76a 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestDetailsViewModel.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/details/model/GitLabMergeRequestDetailsViewModel.kt @@ -56,13 +56,15 @@ internal class GitLabMergeRequestDetailsViewModelImpl( override val author: GitLabUserDTO = mergeRequest.author override val title: SharedFlow = mergeRequest.details.map { it.title }.map { title -> - GitLabUIUtil.convertToHtml(project, mergeRequest.gitRepository, mergeRequest.glProject.projectPath, title) + GitLabUIUtil.convertToHtml(project, mergeRequest.gitRepository, mergeRequest.glProject.projectPath, title, + projectData.contextDataLoader.uploadFileUrlBase) }.modelFlow(cs, LOG) override val description: SharedFlow = mergeRequest.details.map { it.description }.map { description -> processIssueIdsHtml(project, description) }.modelFlow(cs, LOG) override val descriptionHtml: SharedFlow = mergeRequest.details.map { it.description }.map { - if (it.isNotBlank()) GitLabUIUtil.convertToHtml(project, mergeRequest.gitRepository, mergeRequest.glProject.projectPath, it) else it + if (it.isNotBlank()) GitLabUIUtil.convertToHtml(project, mergeRequest.gitRepository, mergeRequest.glProject.projectPath, it, + projectData.contextDataLoader.uploadFileUrlBase) else it }.modelFlow(cs, LOG) override val reviewRequestState: SharedFlow = mergeRequest.details.map { it.reviewState } .modelFlow(cs, LOG) @@ -84,7 +86,7 @@ internal class GitLabMergeRequestDetailsViewModelImpl( override val branchesVm = GitLabMergeRequestBranchesViewModel(cs, mergeRequest, projectData.projectMapping) override val statusVm = GitLabMergeRequestStatusViewModelImpl(project, cs, projectData.projectMapping.gitRepository, projectData.projectMapping.repository.serverPath, mergeRequest) - override val changesVm = GitLabMergeRequestChangesViewModelImpl(project, cs, mergeRequest) + override val changesVm = GitLabMergeRequestChangesViewModelImpl(project, cs, mergeRequest, projectData.contextDataLoader) override fun reloadData() { cs.launchNow { diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/review/GitLabMergeRequestDiscussionsViewModels.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/review/GitLabMergeRequestDiscussionsViewModels.kt index 439c464738ec..304b05b19e32 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/review/GitLabMergeRequestDiscussionsViewModels.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/review/GitLabMergeRequestDiscussionsViewModels.kt @@ -88,7 +88,8 @@ internal class GitLabMergeRequestDiscussionsViewModelsImpl( .map { ComputedResult.fromResult(it) } .transformConsecutiveSuccesses { mapFiltered { it.discussionId == null } - .mapStatefulToStateful { GitLabMergeRequestStandaloneDraftNoteViewModelBase(project, this, it, mergeRequest) } + .mapStatefulToStateful { GitLabMergeRequestStandaloneDraftNoteViewModelBase(project, this, it, mergeRequest, + projectData.contextDataLoader) } } .stateInNow(cs, ComputedResult.loading()) diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineItemViewModel.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineItemViewModel.kt index 8c227db57b28..15b4e92d6b2c 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineItemViewModel.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineItemViewModel.kt @@ -26,14 +26,15 @@ sealed interface GitLabMergeRequestTimelineItemViewModel { sealed class Immutable( private val project: Project, private val mr: GitLabMergeRequest, - private val model: GitLabMergeRequestTimelineItem.Immutable + private val model: GitLabMergeRequestTimelineItem.Immutable, + private val uploadFileUrlBase: String, ) : GitLabMergeRequestTimelineItemViewModel { val date = model.date val actor = model.actor val contentHtml: String? by lazy { (model as? GitLabMergeRequestTimelineItem.SystemNote)?.content?.let { - GitLabUIUtil.convertToHtml(project, mr.gitRepository, mr.glProject.projectPath, it) + GitLabUIUtil.convertToHtml(project, mr.gitRepository, mr.glProject.projectPath, it, uploadFileUrlBase) } } @@ -41,33 +42,41 @@ sealed interface GitLabMergeRequestTimelineItemViewModel { fun fromModel( project: Project, mr: GitLabMergeRequest, - model: GitLabMergeRequestTimelineItem.Immutable - ): Immutable = when (model) { - is GitLabMergeRequestTimelineItem.StateEvent -> StateEvent(project, mr, model) - is GitLabMergeRequestTimelineItem.LabelEvent -> LabelEvent(project, mr, model) - is GitLabMergeRequestTimelineItem.MilestoneEvent -> MilestoneEvent(project, mr, model) - is GitLabMergeRequestTimelineItem.SystemNote -> SystemNote(project, mr, model) + model: GitLabMergeRequestTimelineItem.Immutable, + uploadFileUrlBase: String, + ): Immutable { + + return when (model) { + is GitLabMergeRequestTimelineItem.StateEvent -> StateEvent(project, mr, model, uploadFileUrlBase) + is GitLabMergeRequestTimelineItem.LabelEvent -> LabelEvent(project, mr, model, uploadFileUrlBase) + is GitLabMergeRequestTimelineItem.MilestoneEvent -> MilestoneEvent(project, mr, model, uploadFileUrlBase) + is GitLabMergeRequestTimelineItem.SystemNote -> SystemNote(project, mr, model, uploadFileUrlBase) + } } } } - class StateEvent(project: Project, mr: GitLabMergeRequest, model: GitLabMergeRequestTimelineItem.StateEvent) - : Immutable(project, mr, model) { + class StateEvent(project: Project, mr: GitLabMergeRequest, model: GitLabMergeRequestTimelineItem.StateEvent, + uploadFileUrlBase: String) + : Immutable(project, mr, model, uploadFileUrlBase) { val event = model.event } - class LabelEvent(project: Project, mr: GitLabMergeRequest, model: GitLabMergeRequestTimelineItem.LabelEvent) - : Immutable(project, mr, model) { + class LabelEvent(project: Project, mr: GitLabMergeRequest, model: GitLabMergeRequestTimelineItem.LabelEvent, + uploadFileUrlBase: String) + : Immutable(project, mr, model, uploadFileUrlBase) { val event = model.event } - class MilestoneEvent(project: Project, mr: GitLabMergeRequest, model: GitLabMergeRequestTimelineItem.MilestoneEvent) - : Immutable(project, mr, model) { + class MilestoneEvent(project: Project, mr: GitLabMergeRequest, model: GitLabMergeRequestTimelineItem.MilestoneEvent, + uploadFileUrlBase: String) + : Immutable(project, mr, model, uploadFileUrlBase) { val event = model.event } - class SystemNote(project: Project, mr: GitLabMergeRequest, model: GitLabMergeRequestTimelineItem.SystemNote) - : Immutable(project, mr, model) { + class SystemNote(project: Project, mr: GitLabMergeRequest, model: GitLabMergeRequestTimelineItem.SystemNote, + uploadFileUrlBase: String) + : Immutable(project, mr, model, uploadFileUrlBase) { val content = model.content } @@ -97,7 +106,8 @@ sealed interface GitLabMergeRequestTimelineItemViewModel { project: Project, parentCs: CoroutineScope, mr: GitLabMergeRequest, - note: GitLabMergeRequestNote + note: GitLabMergeRequestNote, + projectData: GitLabProject ) : GitLabMergeRequestTimelineItemViewModel, GitLabNoteViewModel { private val cs = parentCs.childScope(this::class) @@ -113,7 +123,7 @@ sealed interface GitLabMergeRequestTimelineItemViewModel { override val body: StateFlow = note.body override val bodyHtml: StateFlow = body.mapStateInNow(cs) { - GitLabUIUtil.convertToHtml(project, mr.gitRepository, mr.glProject.projectPath, it) + GitLabUIUtil.convertToHtml(project, mr.gitRepository, mr.glProject.projectPath, it, projectData.contextDataLoader.uploadFileUrlBase) } override val discussionState: StateFlow = diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineViewModel.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineViewModel.kt index e633a537bf2d..6bff0f721388 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineViewModel.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineViewModel.kt @@ -47,10 +47,12 @@ internal class LoadAllGitLabMergeRequestTimelineViewModel( override val number: String = "!${mergeRequest.iid}" override val author: GitLabUserDTO = mergeRequest.author override val title: SharedFlow = mergeRequest.details.map { it.title }.map { title -> - GitLabUIUtil.convertToHtml(project, mergeRequest.gitRepository, mergeRequest.glProject.projectPath, title) + GitLabUIUtil.convertToHtml(project, mergeRequest.gitRepository, mergeRequest.glProject.projectPath, title, + projectData.contextDataLoader.uploadFileUrlBase) }.modelFlow(cs, LOG) override val descriptionHtml: SharedFlow = mergeRequest.details.map { it.description }.map { description -> - GitLabUIUtil.convertToHtml(project, mergeRequest.gitRepository, mergeRequest.glProject.projectPath, description) + GitLabUIUtil.convertToHtml(project, mergeRequest.gitRepository, mergeRequest.glProject.projectPath, description, + projectData.contextDataLoader.uploadFileUrlBase) }.modelFlow(cs, LOG) override val url: String = mergeRequest.url @@ -60,7 +62,7 @@ internal class LoadAllGitLabMergeRequestTimelineViewModel( override val timelineItems: SharedFlow>> = mergeRequest.createTimelineItemsFlow(showEvents) .transformConsecutiveSuccesses { - mapDataToModel({ it.id }, { createItemVm(it) }, {}) + mapDataToModel({ it.id }, { createItemVm(it, projectData.contextDataLoader.uploadFileUrlBase) }, {}) } .modelFlow(cs, LOG) @@ -142,17 +144,17 @@ internal class LoadAllGitLabMergeRequestTimelineViewModel( } } - private fun CoroutineScope.createItemVm(item: GitLabMergeRequestTimelineItem) + private fun CoroutineScope.createItemVm(item: GitLabMergeRequestTimelineItem, basePath: String) : GitLabMergeRequestTimelineItemViewModel = when (item) { is GitLabMergeRequestTimelineItem.Immutable -> - GitLabMergeRequestTimelineItemViewModel.Immutable.fromModel(project, mergeRequest, item) + GitLabMergeRequestTimelineItemViewModel.Immutable.fromModel(project, mergeRequest, item, basePath) is GitLabMergeRequestTimelineItem.UserDiscussion -> GitLabMergeRequestTimelineItemViewModel.Discussion(project, cs, projectData, currentUser, mergeRequest, item.discussion).also { handleDiffRequests(it.diffVm, _diffRequests::emit) } is GitLabMergeRequestTimelineItem.DraftNote -> - GitLabMergeRequestTimelineItemViewModel.DraftNote(project, cs, mergeRequest, item.note).also { + GitLabMergeRequestTimelineItemViewModel.DraftNote(project, cs, mergeRequest, item.note, projectData).also { handleDiffRequests(it.diffVm, _diffRequests::emit) } } diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/ui/GitLabUIUtil.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/ui/GitLabUIUtil.kt index 01b468ed0a5e..7911a61621dd 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/ui/GitLabUIUtil.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/ui/GitLabUIUtil.kt @@ -47,6 +47,8 @@ import java.net.URI private val MARKDOWN_IMAGE_SETTINGS: IElementType = MarkdownElementType("MARKDOWN_IMAGE_SETTINGS") +private const val UPLOADS_PATH = "/uploads/" + object GitLabUIUtil { const val OPEN_FILE_LINK_PREFIX = "glfilelink:" const val OPEN_MR_LINK_PREFIX = "glmergerequest:" @@ -64,12 +66,14 @@ object GitLabUIUtil { project: Project, gitRepository: GitRepository, projectPath: GitLabProjectPath, - markdownSource: @NonNls String + markdownSource: @NonNls String, + uploadFileUrlBase: String ): @NlsSafe String { if (markdownSource.isBlank()) return markdownSource // TODO: fix bug with CRLF line endings from markdown library val text = preprocessMergeRequestIds(processIssueIdsMarkdown(project, markdownSource)).replace("\r", "") - val flavourDescriptor = GitLabFlavourDescriptor(gitRepository, projectPath, CodeBlockHtmlSyntaxHighlighter(project)) + val flavourDescriptor = GitLabFlavourDescriptor(gitRepository, projectPath, CodeBlockHtmlSyntaxHighlighter(project), + uploadFileUrlBase) return MarkdownToHtmlConverter(flavourDescriptor).convertMarkdownToHtml(text, null) } @@ -80,7 +84,8 @@ object GitLabUIUtil { private class GitLabFlavourDescriptor( private val gitRepository: GitRepository, private val projectPath: GitLabProjectPath, - private val htmlSyntaxHighlighter: HtmlSyntaxHighlighter + private val htmlSyntaxHighlighter: HtmlSyntaxHighlighter, + private val uploadFileUrlBase: String, ) : GFMFlavourDescriptor() { /** @@ -104,7 +109,7 @@ object GitLabUIUtil { MarkdownElementTypes.IMAGE to GitLabImageWithSettingsGeneratingProvider(linkMap, baseURI).makeXssSafe(useSafeLinks), GFMElementTypes.STRIKETHROUGH to SimpleInlineTagProvider("strike", 2, -2), MarkdownElementTypes.CODE_FENCE to CodeFenceSyntaxHighlighterGeneratingProvider(htmlSyntaxHighlighter), - MarkdownElementTypes.INLINE_LINK to GitLabLinkGeneratingProvider(gitRepository, projectPath, map[MarkdownElementTypes.INLINE_LINK]), + MarkdownElementTypes.INLINE_LINK to GitLabLinkGeneratingProvider(gitRepository, projectPath, uploadFileUrlBase, map[MarkdownElementTypes.INLINE_LINK]), ) } } @@ -198,6 +203,7 @@ object GitLabUIUtil { private class GitLabLinkGeneratingProvider( private val gitRepository: GitRepository, private val projectPath: GitLabProjectPath, + private val uploadFileUrlBase: String, private val fallback: GeneratingProvider? ) : GeneratingProvider { override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) { @@ -231,6 +237,13 @@ object GitLabUIUtil { return } + // "uploads" files links should be updated to absolute URLs of the uploads + if (linkDestination.startsWith(UPLOADS_PATH)) { + visitor.consumeHtml(createLink( + uploadFileUrlBase + linkDestination.substring(UPLOADS_PATH.length), linkText).toString()) + return + } + // Otherwise, the destination is a file in the current git repo, so we can make the link go to it directly try { val fileDestination = gitRepository.root.toNioPath().resolve(linkDestination.replace('\\', '/')).toString() diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/ui/comment/GitLabMergeRequestDiscussionViewModel.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/ui/comment/GitLabMergeRequestDiscussionViewModel.kt index 3af99c277050..c648f3d4cae3 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/ui/comment/GitLabMergeRequestDiscussionViewModel.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/ui/comment/GitLabMergeRequestDiscussionViewModel.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.* import org.jetbrains.plugins.gitlab.api.GitLabId import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO import org.jetbrains.plugins.gitlab.mergerequest.data.* +import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader import org.jetbrains.plugins.gitlab.mergerequest.ui.emoji.GitLabReactionsViewModel import org.jetbrains.plugins.gitlab.ui.GitLabUIUtil import org.jetbrains.plugins.gitlab.ui.comment.GitLabMergeRequestDiscussionViewModel.NoteItem @@ -116,6 +117,7 @@ class GitLabMergeRequestStandaloneDraftNoteViewModelBase internal constructor( parentCs: CoroutineScope, note: GitLabMergeRequestDraftNote, mr: GitLabMergeRequest, + contextDataLoader: GitLabContextDataLoader, ) : GitLabNoteViewModel { private val cs = parentCs.childScope(this::class) @@ -132,7 +134,7 @@ class GitLabMergeRequestStandaloneDraftNoteViewModelBase internal constructor( override val body: StateFlow = note.body override val bodyHtml: StateFlow = body.mapStateInNow(cs) { - GitLabUIUtil.convertToHtml(project, mr.gitRepository, mr.glProject.projectPath, it) + GitLabUIUtil.convertToHtml(project, mr.gitRepository, mr.glProject.projectPath, it, contextDataLoader.uploadFileUrlBase) } override val discussionState: StateFlow = diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/ui/comment/GitLabNoteViewModel.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/ui/comment/GitLabNoteViewModel.kt index 6dab2638d487..4b3c4adcfd10 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/ui/comment/GitLabNoteViewModel.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/ui/comment/GitLabNoteViewModel.kt @@ -17,6 +17,7 @@ import org.jetbrains.annotations.Nls import org.jetbrains.plugins.gitlab.api.GitLabId import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO import org.jetbrains.plugins.gitlab.mergerequest.data.* +import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader import org.jetbrains.plugins.gitlab.mergerequest.ui.emoji.GitLabReactionsViewModel import org.jetbrains.plugins.gitlab.mergerequest.ui.emoji.GitLabReactionsViewModelImpl import org.jetbrains.plugins.gitlab.ui.GitLabUIUtil @@ -64,7 +65,8 @@ class GitLabNoteViewModelImpl( override val body: StateFlow = note.body override val bodyHtml: StateFlow = body.mapStateInNow(cs) { - GitLabUIUtil.convertToHtml(project, projectData.projectMapping.gitRepository, projectData.projectMapping.repository.projectPath,it) + GitLabUIUtil.convertToHtml(project, projectData.projectMapping.gitRepository, projectData.projectMapping.repository.projectPath, it, + projectData.contextDataLoader.uploadFileUrlBase) } override val discussionState: StateFlow = isMainNote.map { diff --git a/plugins/gitlab/gitlab-core/test/org/jetbrains/plugins/gitlab/ui/GitLabUIUtilTest.kt b/plugins/gitlab/gitlab-core/test/org/jetbrains/plugins/gitlab/ui/GitLabUIUtilTest.kt index 34bd3eccfcbd..b22745e3a830 100644 --- a/plugins/gitlab/gitlab-core/test/org/jetbrains/plugins/gitlab/ui/GitLabUIUtilTest.kt +++ b/plugins/gitlab/gitlab-core/test/org/jetbrains/plugins/gitlab/ui/GitLabUIUtilTest.kt @@ -17,6 +17,8 @@ class GitLabUIUtilTest : LightPlatformTestCase() { private lateinit var gitRepository: GitRepository private lateinit var gitRootVf: VirtualFile + private val baseUrl = "http://base/url/uploads/" + override fun setUp() { super.setUp() @@ -36,7 +38,7 @@ class GitLabUIUtilTest : LightPlatformTestCase() { GitLabUIUtil.convertToHtml( project, gitRepository, GitLabProjectPath("test-account", "mr-test"), """ [link](/some/invalid/file/path?query=123) - """.trimIndent()) + """.trimIndent(), baseUrl) } } @@ -72,6 +74,14 @@ class GitLabUIUtilTest : LightPlatformTestCase() { assertThat(parsed.substituteSeparators()).contains("${GitLabUIUtil.OPEN_FILE_LINK_PREFIX}${gitRoot}/directory/a/b/bla.md") } + fun `test uploads files link rendering`() { + val parsed = convertToHtml(""" + [link](/uploads/a/b/c.jpg) some text + """.trimIndent()) + + assertThat(parsed).isEqualTo("""

link some text

""") + } + fun `test simple MR link gets MR link prefix`() { val parsed = convertToHtml(""" !53 @@ -240,7 +250,7 @@ class GitLabUIUtilTest : LightPlatformTestCase() { private fun convertToHtml(markdownSource: String): @NlsSafe String { val parsed = GitLabUIUtil.convertToHtml( - project, gitRepository, GitLabProjectPath("test-account", "mr-test"), markdownSource) + project, gitRepository, GitLabProjectPath("test-account", "mr-test"), markdownSource, baseUrl) return parsed } } \ No newline at end of file