diff options
Diffstat (limited to 'app/assets/javascripts/diffs/components')
7 files changed, 131 insertions, 30 deletions
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 00fd9f43a4f..698fd3909ed 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -144,6 +144,11 @@ export default { required: false, default: '', }, + pinnedFileUrl: { + type: String, + required: false, + default: '', + }, }, data() { return { @@ -153,6 +158,7 @@ export default { autoScrolled: false, activeProject: undefined, hasScannerError: false, + pinnedFileStatus: '', }; }, apollo: { @@ -215,7 +221,6 @@ export default { ...mapState('findingsDrawer', ['activeDrawer']), ...mapState('diffs', [ 'isLoading', - 'diffFiles', 'diffViewType', 'commit', 'renderOverflowWarning', @@ -245,6 +250,7 @@ export default { 'isBatchLoading', 'isBatchLoadingError', 'flatBlobsList', + 'diffFiles', ]), ...mapGetters(['isNotesFetched', 'getNoteableData']), ...mapGetters('findingsDrawer', ['activeDrawer']), @@ -355,7 +361,7 @@ export default { const id = window?.location?.hash; if (id && id.indexOf('#note') !== 0) { - this.setHighlightedRow(id.split('diff-content').pop().slice(1)); + this.setHighlightedRow({ lineCode: id.split('diff-content').pop().slice(1) }); } const events = []; @@ -438,6 +444,7 @@ export default { 'setFileByFile', 'disableVirtualScroller', 'setGenerateTestFilePath', + 'fetchPinnedFile', ]), ...mapActions('findingsDrawer', ['setDrawer']), closeDrawer() { @@ -509,6 +516,20 @@ export default { return !this.diffFiles.length; }, fetchData({ toggleTree = true, fetchMeta = true } = {}) { + if (this.pinnedFileUrl && this.pinnedFileStatus !== 'loaded') { + this.pinnedFileStatus = 'loading'; + this.fetchPinnedFile(this.pinnedFileUrl) + .then(() => { + this.pinnedFileStatus = 'loaded'; + if (toggleTree) this.setTreeDisplay(); + }) + .catch(() => { + this.pinnedFileStatus = 'error'; + createAlert({ + message: __("Couldn't fetch the pinned file."), + }); + }); + } if (fetchMeta) { this.fetchDiffFilesMeta() .then((data) => { @@ -539,7 +560,7 @@ export default { } if (!this.viewDiffsFileByFile) { - this.fetchDiffFilesBatch() + this.fetchDiffFilesBatch(Boolean(this.pinnedFileUrl)) .then(() => { if (toggleTree) this.setTreeDisplay(); // Guarantee the discussions are assigned after the batch finishes. @@ -724,6 +745,9 @@ export default { <gl-loading-icon size="lg" /> </div> <template v-else-if="renderDiffFiles"> + <div v-if="pinnedFileStatus === 'loading'" class="loading"> + <gl-loading-icon size="lg" /> + </div> <dynamic-scroller v-if="isVirtualScrollingEnabled" :items="diffs" diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue index 7493bd5fdf7..3545eb4ed73 100644 --- a/app/assets/javascripts/diffs/components/commit_item.vue +++ b/app/assets/javascripts/diffs/components/commit_item.vue @@ -94,7 +94,7 @@ export default { class="d-block d-sm-flex flex-row-reverse justify-content-between align-items-start flex-lg-row-reverse" > <div - class="commit-actions flex-row d-none d-sm-flex align-items-start flex-wrap justify-content-end" + class="commit-actions flex-row d-none d-sm-flex align-items-center flex-wrap justify-content-end" > <div v-if="commit.signature_html" @@ -105,7 +105,7 @@ export default { :endpoint="commit.pipeline_status_path" class="d-inline-flex mb-2" /> - <gl-button-group class="gl-ml-4 gl-mb-4" data-testid="commit-sha-group"> + <gl-button-group class="gl-ml-4" data-testid="commit-sha-group"> <gl-button label class="gl-font-monospace" data-testid="commit-sha-short-id">{{ commit.short_id }}</gl-button> diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 82b721da493..39f642b0831 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -17,6 +17,7 @@ import DiffFileDrafts from '~/batch_comments/components/diff_file_drafts.vue'; import NoteForm from '~/notes/components/note_form.vue'; import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form'; +import { fileContentsId } from '~/diffs/components/diff_row_utils'; import { DIFF_FILE_AUTOMATIC_COLLAPSE, DIFF_FILE_MANUAL_COLLAPSE, @@ -110,7 +111,10 @@ export default { 'canMerge', ]), ...mapGetters(['isNotesFetched', 'getNoteableData', 'noteableType']), - ...mapGetters('diffs', ['getDiffFileDiscussions', 'isVirtualScrollingEnabled']), + ...mapGetters('diffs', ['getDiffFileDiscussions', 'isVirtualScrollingEnabled', 'pinnedFile']), + isPinnedFile() { + return this.file === this.pinnedFile; + }, viewBlobHref() { return escape(this.file.view_path); }, @@ -206,6 +210,9 @@ export default { diffFileHash() { return this.file.file_hash; }, + fileId() { + return fileContentsId(this.file); + }, }, watch: { 'file.id': { @@ -293,7 +300,7 @@ export default { }, handleToggle({ viaUserInteraction = false } = {}) { const collapsingNow = !this.isCollapsed; - const contentElement = this.$el.querySelector(`#diff-content-${this.file.file_hash}`); + const contentElement = this.$el.querySelector(`#${fileContentsId(this.file)}`); this.setFileCollapsedByUser({ filePath: this.file.file_path, @@ -386,6 +393,7 @@ export default { 'comments-disabled': Boolean(file.brokenSymlink), 'has-body': showBody, 'is-virtual-scrolling': isVirtualScrollingEnabled, + 'pinned-file': isPinnedFile, }" :data-path="file.new_path" class="diff-file file-holder gl-border-none gl-mb-0! gl-pb-5" @@ -400,6 +408,7 @@ export default { :add-merge-request-buttons="true" :view-diffs-file-by-file="viewDiffsFileByFile" :show-local-file-reviews="showLocalFileReviews" + :pinned="isPinnedFile" class="js-file-title file-title gl-border-1 gl-border-solid gl-border-gray-100" :class="hasBodyClasses.header" @toggleFile="handleToggle({ viaUserInteraction: true })" @@ -428,7 +437,7 @@ export default { </div> <template v-else> <div - :id="`diff-content-${file.file_hash}`" + :id="fileId" :class="hasBodyClasses.contentByHash" class="diff-content" data-testid="content-area" diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index e45fd508a5b..97db0fc1c24 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -22,6 +22,7 @@ import { __, s__, sprintf } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { fileContentsId, pinnedFileHref } from '~/diffs/components/diff_row_utils'; import { DIFF_FILE_AUTOMATIC_COLLAPSE } from '../constants'; import { DIFF_FILE_HEADER } from '../i18n'; import { collapsedType, isCollapsed } from '../utils/diff_file'; @@ -102,6 +103,11 @@ export default { required: false, default: false, }, + pinned: { + type: Boolean, + required: false, + default: false, + }, }, idState() { return { @@ -113,9 +119,8 @@ export default { ...mapGetters('diffs', ['diffHasExpandedDiscussions', 'diffHasDiscussions']), ...mapGetters(['getNoteableData']), diffContentIDSelector() { - return `#diff-content-${this.diffFile.file_hash}`; + return `${pinnedFileHref(this.diffFile)}#${fileContentsId(this.diffFile)}`; }, - titleLink() { if (this.diffFile.submodule) { return this.diffFile.submodule_tree_url || this.diffFile.submodule_link; @@ -222,6 +227,7 @@ export default { 'setFileForcedOpen', 'setGenerateTestFilePath', 'toggleFileCommentForm', + 'unpinFile', ]), handleToggleFile() { this.setFileForcedOpen({ @@ -295,7 +301,19 @@ export default { > <div class="file-header-content"> <gl-button - v-if="collapsible" + v-if="pinned" + v-gl-tooltip.hover.focus + :title="__('Unpin the file')" + :aria-label="__('Unpin the file')" + icon="thumbtack" + size="small" + class="btn-icon gl-mr-2" + category="tertiary" + data-testid="unpin-button" + @click="unpinFile" + /> + <gl-button + v-else-if="collapsible" ref="collapseButton" class="gl-mr-2" category="tertiary" @@ -305,10 +323,10 @@ export default { @click.stop="handleToggleFile" /> <a - ref="titleWrapper" :v-once="!viewDiffsFileByFile" class="gl-mr-2 gl-text-decoration-none! gl-word-break-all" :href="titleLink" + data-testid="file-title" @click="handleFileNameClick" > <span v-if="isFileRenamed"> @@ -354,7 +372,7 @@ export default { <small v-if="isModeChanged" ref="fileMode" - v-gl-tooltip.hover + v-gl-tooltip.hover.focus class="mr-1" :title="$options.i18n.fileModeTooltip" > @@ -377,7 +395,7 @@ export default { /> <gl-form-checkbox v-if="isReviewable && showLocalFileReviews" - v-gl-tooltip.hover + v-gl-tooltip.hover.focus data-testid="fileReviewCheckbox" class="gl-mr-5 gl-mb-n3 gl-display-flex gl-align-items-center" :title="$options.i18n.fileReviewTooltip" @@ -388,7 +406,7 @@ export default { </gl-form-checkbox> <gl-button v-if="showCommentButton" - v-gl-tooltip.hover + v-gl-tooltip.hover.focus :title="__('Comment on this file')" :aria-label="__('Comment on this file')" icon="comment" @@ -402,7 +420,7 @@ export default { <gl-button v-if="diffFile.external_url" ref="externalLink" - v-gl-tooltip.hover + v-gl-tooltip.hover.focus :href="diffFile.external_url" :title="externalUrlLabel" :aria-label="externalUrlLabel" diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue index 3dad7a1a8e4..a9da77104d4 100644 --- a/app/assets/javascripts/diffs/components/diff_row.vue +++ b/app/assets/javascripts/diffs/components/diff_row.vue @@ -292,7 +292,7 @@ export default { v-if="props.line.left.old_line && props.line.left.type !== $options.CONFLICT_THEIR" :data-linenumber="props.line.left.old_line" :href="props.line.lineHrefOld" - @click="listeners.setHighlightedRow(props.line.lineCode)" + @click="listeners.setHighlightedRow({ lineCode: props.line.lineCode, event: $event })" > </a> <component @@ -318,7 +318,7 @@ export default { v-if="props.line.left.new_line && props.line.left.type !== $options.CONFLICT_OUR" :data-linenumber="props.line.left.new_line" :href="props.line.lineHrefOld" - @click="listeners.setHighlightedRow(props.line.lineCode)" + @click="listeners.setHighlightedRow({ lineCode: props.line.lineCode, event: $event })" > </a> </div> @@ -446,7 +446,7 @@ export default { v-if="props.line.right.new_line" :data-linenumber="props.line.right.new_line" :href="props.line.lineHrefNew" - @click="listeners.setHighlightedRow(props.line.lineCode)" + @click="listeners.setHighlightedRow({ lineCode: props.line.lineCode, event: $event })" > </a> <component diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js index a489c96b0c9..5c62e0179ac 100644 --- a/app/assets/javascripts/diffs/components/diff_row_utils.js +++ b/app/assets/javascripts/diffs/components/diff_row_utils.js @@ -33,7 +33,19 @@ export const shouldRenderCommentButton = (isLoggedIn, isCommentButtonRendered) = export const hasDiscussions = (line) => line?.discussions?.length > 0; -export const lineHref = (line) => `#${line?.line_code || ''}`; +export const pinnedFileHref = (diffFile) => { + if (!window?.gon?.features?.pinnedFile) return ''; + return `?pin=${diffFile.file_hash}`; +}; + +export const lineHref = (line, content) => { + if (!line || !line.line_code) return ''; + return `${pinnedFileHref(content.diffFile)}#${line.line_code}`; +}; + +export const fileContentsId = (diffFile) => { + return `diff-content-${diffFile.file_hash}`; +}; export const lineCode = (line) => { if (!line) return undefined; @@ -179,8 +191,8 @@ export const mapParallel = (content) => (line) => { isContextLineRight: isContextLine(right?.type), hasDiscussionsLeft: hasDiscussions(left), hasDiscussionsRight: hasDiscussions(right), - lineHrefOld: lineHref(left), - lineHrefNew: lineHref(right), + lineHrefOld: lineHref(left, content), + lineHrefNew: lineHref(right, content), lineCode: lineCode(line), isMetaLineLeft: isMetaLine(left?.type), isMetaLineRight: isMetaLine(right?.type), diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index 07984beb709..ab21391b364 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -34,7 +34,7 @@ export default { }, computed: { ...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId', 'viewedDiffFileIds']), - ...mapGetters('diffs', ['allBlobs']), + ...mapGetters('diffs', ['allBlobs', 'pinnedFile']), filteredTreeList() { let search = this.search.toLowerCase().trim(); @@ -71,21 +71,59 @@ export default { // out: [{ path: 'a', tree: [{ path: 'b' }] }, { path: 'b' }, { path: 'c' }] flatFilteredTreeList() { const result = []; - const createFlatten = (level) => (item) => { + const createFlatten = (level, hidden) => (item) => { result.push({ ...item, + hidden, level: item.isHeader ? 0 : level, key: item.key || item.path, }); - if (item.opened || item.isHeader) { - item.tree.forEach(createFlatten(level + 1)); - } + const isHidden = hidden || (item.type === 'tree' && !item.opened); + item.tree.forEach(createFlatten(level + 1, isHidden)); }; this.filteredTreeList.forEach(createFlatten(0)); return result; }, + flatListWithPinnedFile() { + const result = [...this.flatFilteredTreeList]; + const pinnedIndex = result.findIndex((item) => item.path === this.pinnedFile.file_path); + const [pinnedItem] = result.splice(pinnedIndex, 1); + + if (pinnedItem.parentPath === '/') + return [{ ...pinnedItem, level: 0, pinned: true, hidden: false }, ...result]; + + // remove detached folder from the tree + const next = result[pinnedIndex]; + const prev = result[pinnedIndex - 1]; + const hasContainingFolder = + prev && prev.type === 'tree' && prev.level === pinnedItem.level - 1; + const hasSibling = next && next.type !== 'tree' && next.level === pinnedItem.level; + if (hasContainingFolder && !hasSibling) { + // folder tree is always condensed so we only need to remove the parent folder + result.splice(pinnedIndex - 1, 1); + } + + return [ + { + level: 0, + key: 'pinned-path', + isHeader: true, + opened: true, + path: pinnedItem.parentPath, + type: 'tree', + hidden: false, + }, + { ...pinnedItem, level: 1, pinned: true, hidden: false }, + ...result, + ]; + }, + treeList() { + const list = this.pinnedFile ? this.flatListWithPinnedFile : this.flatFilteredTreeList; + if (this.search) return list; + return list.filter((item) => !item.hidden); + }, }, methods: { ...mapActions('diffs', ['toggleTreeOpen', 'goToFile']), @@ -125,13 +163,13 @@ export default { </button> </div> </div> - <tree-list-height class="gl-flex-grow-1 gl-min-h-0" :items-count="flatFilteredTreeList.length"> + <tree-list-height class="gl-flex-grow-1 gl-min-h-0" :items-count="treeList.length"> <template #default="{ scrollerHeight, rowHeight }"> <div :class="{ 'tree-list-blobs': !renderTreeList || search }" class="mr-tree-list"> <recycle-scroller - v-if="flatFilteredTreeList.length" + v-if="treeList.length" :style="{ height: `${scrollerHeight}px` }" - :items="flatFilteredTreeList" + :items="treeList" :item-size="rowHeight" :buffer="100" key-field="key" |