diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue index 2c7c8038af5cdbf154324b183e591db44fef836c..7649c363daa5b1ec6c82fa5e4ea100116cffcf34 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue @@ -1,8 +1,10 @@ <script> import { debounce } from 'lodash'; +import { mapActions } from 'vuex'; import { deprecatedCreateFlash as flash } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; +import { INTERACTIVE_RESOLVE_MODE } from '../constants'; export default { props: { @@ -10,14 +12,6 @@ export default { type: Object, required: true, }, - onCancelDiscardConfirmation: { - type: Function, - required: true, - }, - onAcceptDiscardConfirmation: { - type: Function, - required: true, - }, }, data() { return { @@ -50,6 +44,7 @@ export default { } }, methods: { + ...mapActions(['setFileResolveMode', 'setPromptConfirmationState', 'updateFile']), loadEditor() { const EditorPromise = import(/* webpackChunkName: 'EditorLite' */ '~/editor/editor_lite'); const DataPromise = axios.get(this.file.content_path); @@ -82,23 +77,24 @@ export default { saveDiffResolution() { this.saved = true; - // This probably be better placed in the data provider - /* eslint-disable vue/no-mutating-props */ - this.file.content = this.editor.getValue(); - this.file.resolveEditChanged = this.file.content !== this.originalContent; - this.file.promptDiscardConfirmation = false; - /* eslint-enable vue/no-mutating-props */ + this.updateFile({ + ...this.file, + content: this.editor.getValue(), + resolveEditChanged: this.file.content !== this.originalContent, + promptDiscardConfirmation: false, + }); }, resetEditorContent() { if (this.fileLoaded) { this.editor.setValue(this.originalContent); } }, - cancelDiscardConfirmation(file) { - this.onCancelDiscardConfirmation(file); - }, acceptDiscardConfirmation(file) { - this.onAcceptDiscardConfirmation(file); + this.setPromptConfirmationState({ file, promptDiscardConfirmation: false }); + this.setFileResolveMode({ file, mode: INTERACTIVE_RESOLVE_MODE }); + }, + cancelDiscardConfirmation(file) { + this.setPromptConfirmationState({ file, promptDiscardConfirmation: false }); }, }, }; diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue index 519fd53af1ea39f2ef23eae623769ba755406f05..9721481e6befb63f1d7f66f314c9ae2d867234e8 100644 --- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue +++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue @@ -1,34 +1,41 @@ <script> import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; -import actionsMixin from '../mixins/line_conflict_actions'; +import { mapActions } from 'vuex'; +import syntaxHighlight from '~/syntax_highlight'; +import { SYNTAX_HIGHLIGHT_CLASS } from '../constants'; import utilsMixin from '../mixins/line_conflict_utils'; export default { directives: { SafeHtml, }, - mixins: [utilsMixin, actionsMixin], + mixins: [utilsMixin], + SYNTAX_HIGHLIGHT_CLASS, props: { file: { type: Object, required: true, }, }, + mounted() { + syntaxHighlight(document.querySelectorAll(`.${SYNTAX_HIGHLIGHT_CLASS}`)); + }, + methods: { + ...mapActions(['handleSelected']), + }, }; </script> <template> - <table class="diff-wrap-lines code code-commit js-syntax-highlight"> - <tr - v-for="line in file.inlineLines" - :key="(line.isHeader ? line.id : line.new_line) + line.richText" - class="line_holder diff-inline" - > + <table :class="['diff-wrap-lines code code-commit', $options.SYNTAX_HIGHLIGHT_CLASS]"> + <!-- Unfortunately there isn't a good key for these sections --> + <!-- eslint-disable vue/require-v-for-key --> + <tr v-for="line in file.inlineLines" class="line_holder diff-inline"> <template v-if="line.isHeader"> <td :class="lineCssClass(line)" class="diff-line-num header"></td> <td :class="lineCssClass(line)" class="diff-line-num header"></td> <td :class="lineCssClass(line)" class="line_content header"> <strong>{{ line.richText }}</strong> - <button class="btn" @click="handleSelected(file, line.id, line.section)"> + <button class="btn" @click="handleSelected({ file, line })"> {{ line.buttonTitle }} </button> </td> diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue index e66f641f70de4ea594edcff47da8dc7806288e5f..7b1d947ccff6dbd1f7f2b876e0b2a22755ac015f 100644 --- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue @@ -1,32 +1,41 @@ <script> import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; -import actionsMixin from '../mixins/line_conflict_actions'; +import { mapActions } from 'vuex'; +import syntaxHighlight from '~/syntax_highlight'; +import { SYNTAX_HIGHLIGHT_CLASS } from '../constants'; import utilsMixin from '../mixins/line_conflict_utils'; export default { directives: { SafeHtml, }, - mixins: [utilsMixin, actionsMixin], + mixins: [utilsMixin], + SYNTAX_HIGHLIGHT_CLASS, props: { file: { type: Object, required: true, }, }, + mounted() { + syntaxHighlight(document.querySelectorAll(`.${SYNTAX_HIGHLIGHT_CLASS}`)); + }, + methods: { + ...mapActions(['handleSelected']), + }, }; </script> <template> <!-- Unfortunately there isn't a good key for these sections --> <!-- eslint-disable vue/require-v-for-key --> - <table class="diff-wrap-lines code js-syntax-highlight"> + <table :class="['diff-wrap-lines code', $options.SYNTAX_HIGHLIGHT_CLASS]"> <tr v-for="section in file.parallelLines" class="line_holder parallel"> <template v-for="line in section"> <template v-if="line.isHeader"> <td class="diff-line-num header" :class="lineCssClass(line)"></td> <td class="line_content header" :class="lineCssClass(line)"> <strong>{{ line.richText }}</strong> - <button class="btn" @click="handleSelected(file, line.id, line.section)"> + <button class="btn" @click="handleSelected({ file, line })"> {{ line.buttonTitle }} </button> </td> diff --git a/app/assets/javascripts/merge_conflicts/constants.js b/app/assets/javascripts/merge_conflicts/constants.js index 6f3ee339e363506067858b34492acebc8c294cdd..dddcc891e81595a0e2d85a096acb78c31489b704 100644 --- a/app/assets/javascripts/merge_conflicts/constants.js +++ b/app/assets/javascripts/merge_conflicts/constants.js @@ -13,6 +13,7 @@ export const VIEW_TYPES = { export const EDIT_RESOLVE_MODE = 'edit'; export const INTERACTIVE_RESOLVE_MODE = 'interactive'; export const DEFAULT_RESOLVE_MODE = INTERACTIVE_RESOLVE_MODE; +export const SYNTAX_HIGHLIGHT_CLASS = 'js-syntax-highlight'; export const HEAD_HEADER_TEXT = s__('MergeConflict|HEAD//our changes'); export const ORIGIN_HEADER_TEXT = s__('MergeConflict|origin//their changes'); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue b/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue index 16a7cfb2ba82554013bcfaa53ccf8364e1dcb599..0509cf0afa11face591297b375ebc58d45ebbb73 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue @@ -1,14 +1,15 @@ <script> import { GlSprintf } from '@gitlab/ui'; +import { mapGetters, mapState, mapActions } from 'vuex'; import { __ } from '~/locale'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import DiffFileEditor from './components/diff_file_editor.vue'; import InlineConflictLines from './components/inline_conflict_lines.vue'; import ParallelConflictLines from './components/parallel_conflict_lines.vue'; +import { INTERACTIVE_RESOLVE_MODE } from './constants'; /** - * NOTE: Most of this component is directly using $root, rather than props or a better data store. - * This is BAD and one shouldn't copy that behavior. Similarly a lot of the classes below should + * A lot of the classes below should * be replaced with GitLab UI components. * * We are just doing it temporarily in order to migrate the template from HAML => Vue in an iterative manner @@ -25,60 +26,88 @@ export default { InlineConflictLines, ParallelConflictLines, }, - inject: ['mergeRequestPath', 'sourceBranchPath'], + inject: ['mergeRequestPath', 'sourceBranchPath', 'resolveConflictsPath'], i18n: { commitStatSummary: __('Showing %{conflict} between %{sourceBranch} and %{targetBranch}'), resolveInfo: __( 'You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}', ), }, + computed: { + ...mapGetters([ + 'getConflictsCountText', + 'isReadyToCommit', + 'getCommitButtonText', + 'fileTextTypePresent', + ]), + ...mapState(['isLoading', 'hasError', 'isParallel', 'conflictsData']), + commitMessage: { + get() { + return this.conflictsData.commitMessage; + }, + set(value) { + this.updateCommitMessage(value); + }, + }, + }, + methods: { + ...mapActions([ + 'setViewType', + 'submitResolvedConflicts', + 'setFileResolveMode', + 'setPromptConfirmationState', + 'updateCommitMessage', + ]), + onClickResolveModeButton(file, mode) { + if (mode === INTERACTIVE_RESOLVE_MODE && file.resolveEditChanged) { + this.setPromptConfirmationState({ file, promptDiscardConfirmation: true }); + } else { + this.setFileResolveMode({ file, mode }); + } + }, + }, }; </script> <template> <div id="conflicts"> - <div v-if="$root.isLoading" class="loading"> + <div v-if="isLoading" class="loading"> <div class="spinner spinner-md"></div> </div> - <div v-if="$root.hasError" class="nothing-here-block"> - {{ $root.conflictsData.errorMessage }} + <div v-if="hasError" class="nothing-here-block"> + {{ conflictsData.errorMessage }} </div> - <template v-if="!$root.isLoading && !$root.hasError"> + <template v-if="!isLoading && !hasError"> <div class="content-block oneline-block files-changed"> - <div v-if="$root.showDiffViewTypeSwitcher" class="inline-parallel-buttons"> + <div v-if="fileTextTypePresent" class="inline-parallel-buttons"> <div class="btn-group"> <button - :class="{ active: !$root.isParallel }" + :class="{ active: !isParallel }" class="btn gl-button" - @click="$root.handleViewTypeChange('inline')" + @click="setViewType('inline')" > {{ __('Inline') }} </button> <button - :class="{ active: $root.isParallel }" + :class="{ active: isParallel }" class="btn gl-button" - @click="$root.handleViewTypeChange('parallel')" + data-testid="side-by-side" + @click="setViewType('parallel')" > {{ __('Side-by-side') }} </button> </div> </div> <div class="js-toggle-container"> - <div class="commit-stat-summary"> + <div class="commit-stat-summary" data-testid="conflicts-count"> <gl-sprintf :message="$options.i18n.commitStatSummary"> <template #conflict> - <strong class="cred"> - {{ $root.conflictsCountText }} - </strong> + <strong class="cred">{{ getConflictsCountText }}</strong> </template> <template #sourceBranch> - <strong class="ref-name"> - {{ $root.conflictsData.sourceBranch }} - </strong> + <strong class="ref-name">{{ conflictsData.sourceBranch }}</strong> </template> <template #targetBranch> - <strong class="ref-name"> - {{ $root.conflictsData.targetBranch }} - </strong> + <strong class="ref-name">{{ conflictsData.targetBranch }}</strong> </template> </gl-sprintf> </div> @@ -87,12 +116,13 @@ export default { <div class="files-wrapper"> <div class="files"> <div - v-for="file in $root.conflictsData.files" + v-for="file in conflictsData.files" :key="file.blobPath" class="diff-file file-holder conflict" + data-testid="files" > <div class="js-file-title file-title file-title-flex-parent cursor-default"> - <div class="file-header-content"> + <div class="file-header-content" data-testid="file-name"> <file-icon :file-name="file.filePath" :size="18" css-classes="gl-mr-2" /> <strong class="file-title-name">{{ file.filePath }}</strong> </div> @@ -102,7 +132,8 @@ export default { :class="{ active: file.resolveMode === 'interactive' }" class="btn gl-button" type="button" - @click="$root.onClickResolveModeButton(file, 'interactive')" + data-testid="interactive-button" + @click="onClickResolveModeButton(file, 'interactive')" > {{ __('Interactive mode') }} </button> @@ -110,7 +141,8 @@ export default { :class="{ active: file.resolveMode === 'edit' }" class="btn gl-button" type="button" - @click="$root.onClickResolveModeButton(file, 'edit')" + data-testid="inline-button" + @click="onClickResolveModeButton(file, 'edit')" > {{ __('Edit inline') }} </button> @@ -118,35 +150,23 @@ export default { <a :href="file.blobPath" class="btn gl-button view-file"> <gl-sprintf :message="__('View file @ %{commitSha}')"> <template #commitSha> - {{ $root.conflictsData.shortCommitSha }} + {{ conflictsData.shortCommitSha }} </template> </gl-sprintf> </a> </div> </div> <div class="diff-content diff-wrap-lines"> - <div - v-show=" - !$root.isParallel && file.resolveMode === 'interactive' && file.type === 'text' - " - class="file-content" - > - <inline-conflict-lines :file="file" /> - </div> - <div - v-show=" - $root.isParallel && file.resolveMode === 'interactive' && file.type === 'text' - " - class="file-content" - > - <parallel-conflict-lines :file="file" /> - </div> - <div v-show="file.resolveMode === 'edit' || file.type === 'text-editor'"> - <diff-file-editor - :file="file" - :on-accept-discard-confirmation="$root.acceptDiscardConfirmation" - :on-cancel-discard-confirmation="$root.cancelDiscardConfirmation" - /> + <template v-if="file.resolveMode === 'interactive' && file.type === 'text'"> + <div v-if="!isParallel" class="file-content"> + <inline-conflict-lines :file="file" /> + </div> + <div v-if="isParallel" class="file-content"> + <parallel-conflict-lines :file="file" /> + </div> + </template> + <div v-if="file.resolveMode === 'edit' || file.type === 'text-editor'"> + <diff-file-editor :file="file" /> </div> </div> </div> @@ -169,7 +189,7 @@ export default { </template> <template #branch_name> <a class="ref-name" :href="sourceBranchPath"> - {{ $root.conflictsData.sourceBranch }} + {{ conflictsData.sourceBranch }} </a> </template> </gl-sprintf> @@ -183,7 +203,8 @@ export default { <div class="max-width-marker"></div> <textarea id="commit-message" - v-model="$root.conflictsData.commitMessage" + v-model="commitMessage" + data-testid="commit-message" class="form-control js-commit-message" rows="5" ></textarea> @@ -195,12 +216,12 @@ export default { <div class="row"> <div class="col-6"> <button - :disabled="!$root.readyToCommit" + :disabled="!isReadyToCommit" class="btn gl-button btn-success js-submit-button" type="button" - @click="$root.commit()" + @click="submitResolvedConflicts(resolveConflictsPath)" > - <span>{{ $root.commitButtonText }}</span> + <span>{{ getCommitButtonText }}</span> </button> </div> <div class="col-6 text-right"> diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js deleted file mode 100644 index 64d691592225f27a0c840b4c1458c4e25a4c039d..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js +++ /dev/null @@ -1,16 +0,0 @@ -import axios from '../lib/utils/axios_utils'; - -export default class MergeConflictsService { - constructor(options) { - this.conflictsPath = options.conflictsPath; - this.resolveConflictsPath = options.resolveConflictsPath; - } - - fetchConflictsData() { - return axios.get(this.conflictsPath); - } - - submitResolveConflicts(data) { - return axios.post(this.resolveConflictsPath, data); - } -} diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js deleted file mode 100644 index fb3444262eacd985db4f6b3062fda1992f659420..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js +++ /dev/null @@ -1,432 +0,0 @@ -/* eslint-disable no-param-reassign, babel/camelcase, no-nested-ternary, no-continue */ - -import $ from 'jquery'; -import Cookies from 'js-cookie'; -import Vue from 'vue'; -import { s__ } from '~/locale'; - -((global) => { - global.mergeConflicts = global.mergeConflicts || {}; - - const diffViewType = Cookies.get('diff_view'); - const HEAD_HEADER_TEXT = s__('MergeConflict|HEAD//our changes'); - const ORIGIN_HEADER_TEXT = s__('MergeConflict|origin//their changes'); - const HEAD_BUTTON_TITLE = s__('MergeConflict|Use ours'); - const ORIGIN_BUTTON_TITLE = s__('MergeConflict|Use theirs'); - const INTERACTIVE_RESOLVE_MODE = 'interactive'; - const EDIT_RESOLVE_MODE = 'edit'; - const DEFAULT_RESOLVE_MODE = INTERACTIVE_RESOLVE_MODE; - const VIEW_TYPES = { - INLINE: 'inline', - PARALLEL: 'parallel', - }; - const CONFLICT_TYPES = { - TEXT: 'text', - TEXT_EDITOR: 'text-editor', - }; - - global.mergeConflicts.mergeConflictsStore = { - state: { - isLoading: true, - hasError: false, - isSubmitting: false, - isParallel: diffViewType === VIEW_TYPES.PARALLEL, - diffViewType, - conflictsData: {}, - }, - - setConflictsData(data) { - this.decorateFiles(data.files); - - this.state.conflictsData = { - files: data.files, - commitMessage: data.commit_message, - sourceBranch: data.source_branch, - targetBranch: data.target_branch, - shortCommitSha: data.commit_sha.slice(0, 7), - }; - }, - - decorateFiles(files) { - files.forEach((file) => { - file.content = ''; - file.resolutionData = {}; - file.promptDiscardConfirmation = false; - file.resolveMode = DEFAULT_RESOLVE_MODE; - file.filePath = this.getFilePath(file); - file.blobPath = file.blob_path; - - if (file.type === CONFLICT_TYPES.TEXT) { - file.showEditor = false; - file.loadEditor = false; - - this.setInlineLine(file); - this.setParallelLine(file); - } else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) { - file.showEditor = true; - file.loadEditor = true; - } - }); - }, - - setInlineLine(file) { - file.inlineLines = []; - - file.sections.forEach((section) => { - let currentLineType = 'new'; - const { conflict, lines, id } = section; - - if (conflict) { - file.inlineLines.push(this.getHeadHeaderLine(id)); - } - - lines.forEach((line) => { - const { type } = line; - - if ((type === 'new' || type === 'old') && currentLineType !== type) { - currentLineType = type; - file.inlineLines.push({ lineType: 'emptyLine', richText: '' }); - } - - this.decorateLineForInlineView(line, id, conflict); - file.inlineLines.push(line); - }); - - if (conflict) { - file.inlineLines.push(this.getOriginHeaderLine(id)); - } - }); - }, - - setParallelLine(file) { - file.parallelLines = []; - const linesObj = { left: [], right: [] }; - - file.sections.forEach((section) => { - const { conflict, lines, id } = section; - - if (conflict) { - linesObj.left.push(this.getOriginHeaderLine(id)); - linesObj.right.push(this.getHeadHeaderLine(id)); - } - - lines.forEach((line) => { - const { type } = line; - - if (conflict) { - if (type === 'old') { - linesObj.left.push(this.getLineForParallelView(line, id, 'conflict')); - } else if (type === 'new') { - linesObj.right.push(this.getLineForParallelView(line, id, 'conflict', true)); - } - } else { - const lineType = type || 'context'; - - linesObj.left.push(this.getLineForParallelView(line, id, lineType)); - linesObj.right.push(this.getLineForParallelView(line, id, lineType, true)); - } - }); - - this.checkLineLengths(linesObj); - }); - - for (let i = 0, len = linesObj.left.length; i < len; i += 1) { - file.parallelLines.push([linesObj.right[i], linesObj.left[i]]); - } - }, - - setLoadingState(state) { - this.state.isLoading = state; - }, - - setErrorState(state) { - this.state.hasError = state; - }, - - setFailedRequest(message) { - this.state.hasError = true; - this.state.conflictsData.errorMessage = message; - }, - - getConflictsCount() { - if (!this.state.conflictsData.files.length) { - return 0; - } - - const { files } = this.state.conflictsData; - let count = 0; - - files.forEach((file) => { - if (file.type === CONFLICT_TYPES.TEXT) { - file.sections.forEach((section) => { - if (section.conflict) { - count += 1; - } - }); - } else { - count += 1; - } - }); - - return count; - }, - - getConflictsCountText() { - const count = this.getConflictsCount(); - const text = count > 1 ? s__('MergeConflict|conflicts') : s__('MergeConflict|conflict'); - - return `${count} ${text}`; - }, - - setViewType(viewType) { - this.state.diffView = viewType; - this.state.isParallel = viewType === VIEW_TYPES.PARALLEL; - - Cookies.set('diff_view', viewType); - }, - - getHeadHeaderLine(id) { - return { - id, - richText: HEAD_HEADER_TEXT, - buttonTitle: HEAD_BUTTON_TITLE, - type: 'new', - section: 'head', - isHeader: true, - isHead: true, - isSelected: false, - isUnselected: false, - }; - }, - - decorateLineForInlineView(line, id, conflict) { - const { type } = line; - line.id = id; - line.hasConflict = conflict; - line.isHead = type === 'new'; - line.isOrigin = type === 'old'; - line.hasMatch = type === 'match'; - line.richText = line.rich_text; - line.isSelected = false; - line.isUnselected = false; - }, - - getLineForParallelView(line, id, lineType, isHead) { - const { old_line, new_line, rich_text } = line; - const hasConflict = lineType === 'conflict'; - - return { - id, - lineType, - hasConflict, - isHead: hasConflict && isHead, - isOrigin: hasConflict && !isHead, - hasMatch: lineType === 'match', - lineNumber: isHead ? new_line : old_line, - section: isHead ? 'head' : 'origin', - richText: rich_text, - isSelected: false, - isUnselected: false, - }; - }, - - getOriginHeaderLine(id) { - return { - id, - richText: ORIGIN_HEADER_TEXT, - buttonTitle: ORIGIN_BUTTON_TITLE, - type: 'old', - section: 'origin', - isHeader: true, - isOrigin: true, - isSelected: false, - isUnselected: false, - }; - }, - - getFilePath(file) { - const { old_path, new_path } = file; - return old_path === new_path ? new_path : `${old_path} → ${new_path}`; - }, - - checkLineLengths(linesObj) { - const { left, right } = linesObj; - - if (left.length !== right.length) { - if (left.length > right.length) { - const diff = left.length - right.length; - for (let i = 0; i < diff; i += 1) { - right.push({ lineType: 'emptyLine', richText: '' }); - } - } else { - const diff = right.length - left.length; - for (let i = 0; i < diff; i += 1) { - left.push({ lineType: 'emptyLine', richText: '' }); - } - } - } - }, - - setPromptConfirmationState(file, state) { - file.promptDiscardConfirmation = state; - }, - - setFileResolveMode(file, mode) { - if (mode === INTERACTIVE_RESOLVE_MODE) { - file.showEditor = false; - } else if (mode === EDIT_RESOLVE_MODE) { - // Restore Interactive mode when switching to Edit mode - file.showEditor = true; - file.loadEditor = true; - file.resolutionData = {}; - - this.restoreFileLinesState(file); - } - - file.resolveMode = mode; - }, - - restoreFileLinesState(file) { - file.inlineLines.forEach((line) => { - if (line.hasConflict || line.isHeader) { - line.isSelected = false; - line.isUnselected = false; - } - }); - - file.parallelLines.forEach((lines) => { - const left = lines[0]; - const right = lines[1]; - const isLeftMatch = left.hasConflict || left.isHeader; - const isRightMatch = right.hasConflict || right.isHeader; - - if (isLeftMatch || isRightMatch) { - left.isSelected = false; - left.isUnselected = false; - right.isSelected = false; - right.isUnselected = false; - } - }); - }, - - isReadyToCommit() { - const { files } = this.state.conflictsData; - const hasCommitMessage = $.trim(this.state.conflictsData.commitMessage).length; - let unresolved = 0; - - for (let i = 0, l = files.length; i < l; i += 1) { - const file = files[i]; - - if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) { - let numberConflicts = 0; - const resolvedConflicts = Object.keys(file.resolutionData).length; - - // We only check for conflicts type 'text' - // since conflicts `text_editor` can´t be resolved in interactive mode - if (file.type === CONFLICT_TYPES.TEXT) { - for (let j = 0, k = file.sections.length; j < k; j += 1) { - if (file.sections[j].conflict) { - numberConflicts += 1; - } - } - - if (resolvedConflicts !== numberConflicts) { - unresolved += 1; - } - } - } else if (file.resolveMode === EDIT_RESOLVE_MODE) { - // Unlikely to happen since switching to Edit mode saves content automatically. - // Checking anyway in case the save strategy changes in the future - if (!file.content) { - unresolved += 1; - continue; - } - } - } - - return !this.state.isSubmitting && hasCommitMessage && !unresolved; - }, - - getCommitButtonText() { - const initial = s__('MergeConflict|Commit to source branch'); - const inProgress = s__('MergeConflict|Committing...'); - - return this.state ? (this.state.isSubmitting ? inProgress : initial) : initial; - }, - - getCommitData() { - let commitData = {}; - - commitData = { - commit_message: this.state.conflictsData.commitMessage, - files: [], - }; - - this.state.conflictsData.files.forEach((file) => { - const addFile = { - old_path: file.old_path, - new_path: file.new_path, - }; - - if (file.type === CONFLICT_TYPES.TEXT) { - // Submit only one data for type of editing - if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) { - addFile.sections = file.resolutionData; - } else if (file.resolveMode === EDIT_RESOLVE_MODE) { - addFile.content = file.content; - } - } else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) { - addFile.content = file.content; - } - - commitData.files.push(addFile); - }); - - return commitData; - }, - - handleSelected(file, sectionId, selection) { - Vue.set(file.resolutionData, sectionId, selection); - - file.inlineLines.forEach((line) => { - if (line.id === sectionId && (line.hasConflict || line.isHeader)) { - this.markLine(line, selection); - } - }); - - file.parallelLines.forEach((lines) => { - const left = lines[0]; - const right = lines[1]; - const hasSameId = right.id === sectionId || left.id === sectionId; - const isLeftMatch = left.hasConflict || left.isHeader; - const isRightMatch = right.hasConflict || right.isHeader; - - if (hasSameId && (isLeftMatch || isRightMatch)) { - this.markLine(left, selection); - this.markLine(right, selection); - } - }); - }, - - markLine(line, selection) { - if (selection === 'head' && line.isHead) { - line.isSelected = true; - line.isUnselected = false; - } else if (selection === 'origin' && line.isOrigin) { - line.isSelected = true; - line.isUnselected = false; - } else { - line.isSelected = false; - line.isUnselected = true; - } - }, - - setSubmitState(state) { - this.state.isSubmitting = state; - }, - - fileTextTypePresent() { - return this.state.conflictsData.files.some((f) => f.type === CONFLICT_TYPES.TEXT); - }, - }; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index 4b73dd317cd1e2918c676c3319f57232497ef64f..cf02c6fbd6b03a71703d5890ef6f062c000b2779 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -1,100 +1,32 @@ -import $ from 'jquery'; import Vue from 'vue'; -import { __ } from '~/locale'; -import { deprecatedCreateFlash as createFlash } from '../flash'; import initIssuableSidebar from '../init_issuable_sidebar'; -import './merge_conflict_store'; -import syntaxHighlight from '../syntax_highlight'; import MergeConflictsResolverApp from './merge_conflict_resolver_app.vue'; -import MergeConflictsService from './merge_conflict_service'; +import { createStore } from './store'; export default function initMergeConflicts() { - const INTERACTIVE_RESOLVE_MODE = 'interactive'; const conflictsEl = document.querySelector('#conflicts'); - const { mergeConflictsStore } = gl.mergeConflicts; - const mergeConflictsService = new MergeConflictsService({ - conflictsPath: conflictsEl.dataset.conflictsPath, - resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath, - }); - const { sourceBranchPath, mergeRequestPath } = conflictsEl.dataset; + const { + sourceBranchPath, + mergeRequestPath, + conflictsPath, + resolveConflictsPath, + } = conflictsEl.dataset; initIssuableSidebar(); + const store = createStore(); + return new Vue({ el: conflictsEl, + store, provide: { sourceBranchPath, mergeRequestPath, - }, - data: mergeConflictsStore.state, - computed: { - conflictsCountText() { - return mergeConflictsStore.getConflictsCountText(); - }, - readyToCommit() { - return mergeConflictsStore.isReadyToCommit(); - }, - commitButtonText() { - return mergeConflictsStore.getCommitButtonText(); - }, - showDiffViewTypeSwitcher() { - return mergeConflictsStore.fileTextTypePresent(); - }, + resolveConflictsPath, }, created() { - mergeConflictsService - .fetchConflictsData() - .then(({ data }) => { - if (data.type === 'error') { - mergeConflictsStore.setFailedRequest(data.message); - } else { - mergeConflictsStore.setConflictsData(data); - } - - mergeConflictsStore.setLoadingState(false); - - this.$nextTick(() => { - syntaxHighlight($('.js-syntax-highlight')); - }); - }) - .catch(() => { - mergeConflictsStore.setLoadingState(false); - mergeConflictsStore.setFailedRequest(); - }); - }, - methods: { - handleViewTypeChange(viewType) { - mergeConflictsStore.setViewType(viewType); - }, - onClickResolveModeButton(file, mode) { - if (mode === INTERACTIVE_RESOLVE_MODE && file.resolveEditChanged) { - mergeConflictsStore.setPromptConfirmationState(file, true); - return; - } - - mergeConflictsStore.setFileResolveMode(file, mode); - }, - acceptDiscardConfirmation(file) { - mergeConflictsStore.setPromptConfirmationState(file, false); - mergeConflictsStore.setFileResolveMode(file, INTERACTIVE_RESOLVE_MODE); - }, - cancelDiscardConfirmation(file) { - mergeConflictsStore.setPromptConfirmationState(file, false); - }, - commit() { - mergeConflictsStore.setSubmitState(true); - - mergeConflictsService - .submitResolveConflicts(mergeConflictsStore.getCommitData()) - .then(({ data }) => { - window.location.href = data.redirect_to; - }) - .catch(() => { - mergeConflictsStore.setSubmitState(false); - createFlash(__('Failed to save merge conflicts resolutions. Please try again!')); - }); - }, + store.dispatch('fetchConflictsData', conflictsPath); }, render(createElement) { return createElement(MergeConflictsResolverApp); diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js deleted file mode 100644 index 364ae2b268891c44b86d47a14e0db4779aaaf2d8..0000000000000000000000000000000000000000 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js +++ /dev/null @@ -1,7 +0,0 @@ -export default { - methods: { - handleSelected(file, sectionId, selection) { - gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection); - }, - }, -}; diff --git a/app/assets/javascripts/merge_conflicts/store/actions.js b/app/assets/javascripts/merge_conflicts/store/actions.js index 8036e90c58c31e04207addac392009858e11e438..df515c4ac1a245f5b52a9843f6fb8c413dd5b343 100644 --- a/app/assets/javascripts/merge_conflicts/store/actions.js +++ b/app/assets/javascripts/merge_conflicts/store/actions.js @@ -118,3 +118,8 @@ export const handleSelected = ({ commit, state, getters }, { file, line: { id, s commit(types.UPDATE_FILE, { file: updated, index }); }; + +export const updateFile = ({ commit, getters }, file) => { + const index = getters.getFileIndex(file); + commit(types.UPDATE_FILE, { file, index }); +}; diff --git a/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js b/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..eaa3b1c5d53279fcc6aaf89de7b26e0d9b586454 --- /dev/null +++ b/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js @@ -0,0 +1,131 @@ +import { GlSprintf } from '@gitlab/ui'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import InlineConflictLines from '~/merge_conflicts/components/inline_conflict_lines.vue'; +import ParallelConflictLines from '~/merge_conflicts/components/parallel_conflict_lines.vue'; +import component from '~/merge_conflicts/merge_conflict_resolver_app.vue'; +import { createStore } from '~/merge_conflicts/store'; +import { decorateFiles } from '~/merge_conflicts/utils'; +import { conflictsMock } from '../mock_data'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('Merge Conflict Resolver App', () => { + let wrapper; + let store; + + const decoratedMockFiles = decorateFiles(conflictsMock.files); + + const mountComponent = () => { + wrapper = shallowMount(component, { + store, + stubs: { GlSprintf }, + provide() { + return { + mergeRequestPath: 'foo', + sourceBranchPath: 'foo', + resolveConflictsPath: 'bar', + }; + }, + }); + }; + + beforeEach(() => { + store = createStore(); + store.commit('SET_LOADING_STATE', false); + store.dispatch('setConflictsData', conflictsMock); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + const findConflictsCount = () => wrapper.find('[data-testid="conflicts-count"]'); + const findFiles = () => wrapper.findAll('[data-testid="files"]'); + const findFileHeader = (w = wrapper) => w.find('[data-testid="file-name"]'); + const findFileInteractiveButton = (w = wrapper) => w.find('[data-testid="interactive-button"]'); + const findFileInlineButton = (w = wrapper) => w.find('[data-testid="inline-button"]'); + const findSideBySideButton = () => wrapper.find('[data-testid="side-by-side"]'); + const findInlineConflictLines = (w = wrapper) => w.find(InlineConflictLines); + const findParallelConflictLines = (w = wrapper) => w.find(ParallelConflictLines); + const findCommitMessageTextarea = () => wrapper.find('[data-testid="commit-message"]'); + + it('shows the amount of conflicts', () => { + mountComponent(); + + const title = findConflictsCount(); + + expect(title.exists()).toBe(true); + expect(title.text().trim()).toBe('Showing 3 conflicts between test-conflicts and master'); + }); + + describe('files', () => { + it('shows one file area for each file', () => { + mountComponent(); + + expect(findFiles()).toHaveLength(conflictsMock.files.length); + }); + + it('has the appropriate file header', () => { + mountComponent(); + + const fileHeader = findFileHeader(findFiles().at(0)); + + expect(fileHeader.text()).toBe(decoratedMockFiles[0].filePath); + }); + + describe('editing', () => { + it('interactive mode is the default', () => { + mountComponent(); + + const interactiveButton = findFileInteractiveButton(findFiles().at(0)); + const inlineButton = findFileInlineButton(findFiles().at(0)); + + expect(interactiveButton.classes('active')).toBe(true); + expect(inlineButton.classes('active')).toBe(false); + }); + + it('clicking inline set inline as default', async () => { + mountComponent(); + + const inlineButton = findFileInlineButton(findFiles().at(0)); + expect(inlineButton.classes('active')).toBe(false); + + inlineButton.trigger('click'); + await wrapper.vm.$nextTick(); + + expect(inlineButton.classes('active')).toBe(true); + }); + + it('inline mode shows a inline-conflict-lines', () => { + mountComponent(); + + const inlineConflictLinesComponent = findInlineConflictLines(findFiles().at(0)); + + expect(inlineConflictLinesComponent.exists()).toBe(true); + expect(inlineConflictLinesComponent.props('file')).toEqual(decoratedMockFiles[0]); + }); + + it('parallel mode shows a parallel-conflict-lines', async () => { + mountComponent(); + + findSideBySideButton().trigger('click'); + await wrapper.vm.$nextTick(); + + const parallelConflictLinesComponent = findParallelConflictLines(findFiles().at(0)); + + expect(parallelConflictLinesComponent.exists()).toBe(true); + expect(parallelConflictLinesComponent.props('file')).toEqual(decoratedMockFiles[0]); + }); + }); + }); + + describe('submit form', () => { + it('contains a commit message textarea', () => { + mountComponent(); + + expect(findCommitMessageTextarea().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/merge_conflicts/mock_data.js b/spec/frontend/merge_conflicts/mock_data.js new file mode 100644 index 0000000000000000000000000000000000000000..8948f2a3c1e5b2c322e6f6602f413ed193c730ba --- /dev/null +++ b/spec/frontend/merge_conflicts/mock_data.js @@ -0,0 +1,340 @@ +export const conflictsMock = { + target_branch: 'master', + source_branch: 'test-conflicts', + commit_sha: '6dbf385a3c7bf01e09b5d2d9e5d72f8fb8c590a3', + commit_message: + "Merge branch 'master' into 'test-conflicts'\n\n# Conflicts:\n# .gitlab-ci.yml\n# README.md", + files: [ + { + old_path: '.gitlab-ci.yml', + new_path: '.gitlab-ci.yml', + blob_icon: 'doc-text', + blob_path: + '/gitlab-org/gitlab-test/-/blob/6dbf385a3c7bf01e09b5d2d9e5d72f8fb8c590a3/.gitlab-ci.yml', + sections: [ + { + conflict: false, + lines: [ + { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '@@ -7,10 +7,11 @@ upload:', + meta_data: { old_pos: 7, new_pos: 7 }, + rich_text: '@@ -7,10 +7,11 @@ upload:', + can_receive_suggestion: true, + }, + { + line_code: '587d266bb27a4dc3022bbed44dfa19849df3044c_7_7', + type: null, + old_line: 7, + new_line: 7, + text: ' stage: upload', + meta_data: null, + rich_text: + '\u003cspan id="LC7" class="line" lang="yaml"\u003e \u003cspan class="na"\u003estage\u003c/span\u003e\u003cspan class="pi"\u003e:\u003c/span\u003e \u003cspan class="s"\u003eupload\u003c/span\u003e\u003c/span\u003e\n', + can_receive_suggestion: true, + }, + { + line_code: '587d266bb27a4dc3022bbed44dfa19849df3044c_8_8', + type: null, + old_line: 8, + new_line: 8, + text: ' script:', + meta_data: null, + rich_text: + '\u003cspan id="LC8" class="line" lang="yaml"\u003e \u003cspan class="na"\u003escript\u003c/span\u003e\u003cspan class="pi"\u003e:\u003c/span\u003e\u003c/span\u003e\n', + can_receive_suggestion: true, + }, + { + line_code: '587d266bb27a4dc3022bbed44dfa19849df3044c_9_9', + type: null, + old_line: 9, + new_line: 9, + text: + // eslint-disable-next-line no-template-curly-in-string + ' - \'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file README.md ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/my_package/0.0.1/file.txt\'', + meta_data: null, + rich_text: + // eslint-disable-next-line no-template-curly-in-string + '\u003cspan id="LC9" class="line" lang="yaml"\u003e \u003cspan class="pi"\u003e-\u003c/span\u003e \u003cspan class="s1"\u003e\'\u003c/span\u003e\u003cspan class="s"\u003ecurl\u003c/span\u003e\u003cspan class="nv"\u003e \u003c/span\u003e\u003cspan class="s"\u003e--header\u003c/span\u003e\u003cspan class="nv"\u003e \u003c/span\u003e\u003cspan class="s"\u003e"JOB-TOKEN:\u003c/span\u003e\u003cspan class="nv"\u003e \u003c/span\u003e\u003cspan class="s"\u003e$CI_JOB_TOKEN"\u003c/span\u003e\u003cspan class="nv"\u003e \u003c/span\u003e\u003cspan class="s"\u003e--upload-file\u003c/span\u003e\u003cspan class="nv"\u003e \u003c/span\u003e\u003cspan class="s"\u003eREADME.md\u003c/span\u003e\u003cspan class="nv"\u003e \u003c/span\u003e\u003cspan class="s"\u003e${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/my_package/0.0.1/file.txt\'\u003c/span\u003e\u003c/span\u003e\n', + can_receive_suggestion: true, + }, + ], + }, + { + conflict: true, + lines: [ + { + line_code: '587d266bb27a4dc3022bbed44dfa19849df3044c_10_10', + type: 'new', + old_line: null, + new_line: 10, + text: '# some new comments', + meta_data: null, + rich_text: + '\u003cspan id="LC10" class="line" lang="yaml"\u003e\u003cspan class="c1"\u003e# some new comments\u003c/span\u003e\u003c/span\u003e', + can_receive_suggestion: true, + }, + { + line_code: '587d266bb27a4dc3022bbed44dfa19849df3044c_10_11', + type: 'old', + old_line: 10, + new_line: null, + text: '# a different comment', + meta_data: null, + rich_text: + '\u003cspan id="LC10" class="line" lang="yaml"\u003e\u003cspan class="c1"\u003e# a different comment\u003c/span\u003e\u003c/span\u003e', + can_receive_suggestion: false, + }, + ], + id: '587d266bb27a4dc3022bbed44dfa19849df3044c_10_10', + }, + ], + type: 'text', + content_path: + '/gitlab-org/gitlab-test/-/merge_requests/2/conflict_for_path?new_path=.gitlab-ci.yml\u0026old_path=.gitlab-ci.yml', + }, + { + old_path: 'README.md', + new_path: 'README.md', + blob_icon: 'doc-text', + blob_path: + '/gitlab-org/gitlab-test/-/blob/6dbf385a3c7bf01e09b5d2d9e5d72f8fb8c590a3/README.md', + sections: [ + { + conflict: false, + lines: [ + { + line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_1_1', + type: null, + old_line: 1, + new_line: 1, + text: '- 1', + meta_data: null, + rich_text: + '\u003cspan id="LC1" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 1\u003c/span\u003e\n', + can_receive_suggestion: true, + }, + { + line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_2_2', + type: null, + old_line: 2, + new_line: 2, + text: '- 2', + meta_data: null, + rich_text: + '\u003cspan id="LC2" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 2\u003c/span\u003e\n', + can_receive_suggestion: true, + }, + { + line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_3_3', + type: null, + old_line: 3, + new_line: 3, + text: '- 3', + meta_data: null, + rich_text: + '\u003cspan id="LC3" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 3\u003c/span\u003e\n', + can_receive_suggestion: true, + }, + ], + }, + { + conflict: true, + lines: [ + { + line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_4_4', + type: 'new', + old_line: null, + new_line: 4, + text: '- 4c', + meta_data: null, + rich_text: + '\u003cspan id="LC4" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 4c\u003c/span\u003e\n', + can_receive_suggestion: true, + }, + { + line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_4_5', + type: 'old', + old_line: 4, + new_line: null, + text: '- 4b', + meta_data: null, + rich_text: + '\u003cspan id="LC4" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 4b\u003c/span\u003e\n', + can_receive_suggestion: false, + }, + ], + id: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_4_4', + }, + { + conflict: false, + lines: [ + { + line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_5_5', + type: null, + old_line: 5, + new_line: 5, + text: '- 5', + meta_data: null, + rich_text: + '\u003cspan id="LC5" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 5\u003c/span\u003e\n', + can_receive_suggestion: true, + }, + { + line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_6_6', + type: null, + old_line: 6, + new_line: 6, + text: '- 6', + meta_data: null, + rich_text: + '\u003cspan id="LC6" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 6\u003c/span\u003e\n', + can_receive_suggestion: true, + }, + { + line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_7_7', + type: null, + old_line: 7, + new_line: 7, + text: '- 7', + meta_data: null, + rich_text: + '\u003cspan id="LC7" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 7\u003c/span\u003e\n', + can_receive_suggestion: true, + }, + ], + }, + { + conflict: false, + lines: [ + { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '@@ -9,15 +9,15 @@', + meta_data: { old_pos: 9, new_pos: 9 }, + rich_text: '@@ -9,15 +9,15 @@', + can_receive_suggestion: true, + }, + { + line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_9_9', + type: null, + old_line: 9, + new_line: 9, + text: '- 9', + meta_data: null, + rich_text: + '\u003cspan id="LC9" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 9\u003c/span\u003e\n', + can_receive_suggestion: true, + }, + { + line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_10_10', + type: null, + old_line: 10, + new_line: 10, + text: '- 10', + meta_data: null, + rich_text: + '\u003cspan id="LC10" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 10\u003c/span\u003e\n', + can_receive_suggestion: true, + }, + { + line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_11_11', + type: null, + old_line: 11, + new_line: 11, + text: '- 11', + meta_data: null, + rich_text: + '\u003cspan id="LC11" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 11\u003c/span\u003e\n', + can_receive_suggestion: true, + }, + ], + }, + { + conflict: true, + lines: [ + { + line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_12_12', + type: 'new', + old_line: null, + new_line: 12, + text: '- 12c', + meta_data: null, + rich_text: + '\u003cspan id="LC12" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 12c\u003c/span\u003e\n', + can_receive_suggestion: true, + }, + { + line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_12_13', + type: 'old', + old_line: 12, + new_line: null, + text: '- 12b', + meta_data: null, + rich_text: + '\u003cspan id="LC12" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 12b\u003c/span\u003e\n', + can_receive_suggestion: false, + }, + ], + id: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_12_12', + }, + { + conflict: false, + lines: [ + { + line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_13_13', + type: null, + old_line: 13, + new_line: 13, + text: '- 13', + meta_data: null, + rich_text: + '\u003cspan id="LC13" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 13\u003c/span\u003e\n', + can_receive_suggestion: true, + }, + { + line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_14_14', + type: null, + old_line: 14, + new_line: 14, + text: '- 14 ', + meta_data: null, + rich_text: + '\u003cspan id="LC14" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 14 \u003c/span\u003e\n', + can_receive_suggestion: true, + }, + { + line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_15_15', + type: null, + old_line: 15, + new_line: 15, + text: '- 15', + meta_data: null, + rich_text: + '\u003cspan id="LC15" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 15\u003c/span\u003e\n', + can_receive_suggestion: true, + }, + { + line_code: null, + type: 'match', + old_line: null, + new_line: null, + text: '', + meta_data: { old_pos: 15, new_pos: 15 }, + rich_text: '', + can_receive_suggestion: true, + }, + ], + }, + ], + type: 'text', + content_path: + '/gitlab-org/gitlab-test/-/merge_requests/2/conflict_for_path?new_path=README.md\u0026old_path=README.md', + }, + ], +};