diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 207f39340f7b4c6c9ad3ca7e42791ab81d9fd452..dd5addbf1e33611f5d711bfb3fd3a5e85752e759 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -18,6 +18,7 @@ import TreeList from './tree_list.vue'; import HiddenFilesWarning from './hidden_files_warning.vue'; import MergeConflictWarning from './merge_conflict_warning.vue'; +import CollapsedFilesWarning from './collapsed_files_warning.vue'; import { TREE_LIST_WIDTH_STORAGE_KEY, @@ -27,6 +28,9 @@ import { TREE_HIDE_STATS_WIDTH, MR_TREE_SHOW_KEY, CENTERED_LIMITED_CONTAINER_CLASSES, + ALERT_OVERFLOW_HIDDEN, + ALERT_MERGE_CONFLICT, + ALERT_COLLAPSED_FILES, } from '../constants'; export default { @@ -37,6 +41,7 @@ export default { NoChanges, HiddenFilesWarning, MergeConflictWarning, + CollapsedFilesWarning, CommitWidget, TreeList, GlLoadingIcon, @@ -45,6 +50,11 @@ export default { GlSprintf, }, mixins: [glFeatureFlagsMixin()], + alerts: { + ALERT_OVERFLOW_HIDDEN, + ALERT_MERGE_CONFLICT, + ALERT_COLLAPSED_FILES, + }, props: { endpoint: { type: String, @@ -114,6 +124,7 @@ export default { return { treeWidth, diffFilesLength: 0, + collapsedWarningDismissed: false, }; }, computed: { @@ -142,7 +153,7 @@ export default { 'canMerge', 'hasConflicts', ]), - ...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']), + ...mapGetters('diffs', ['hasCollapsedFile', 'isParallelView', 'currentDiffIndex']), ...mapGetters(['isNotesFetched', 'getNoteableData']), diffs() { if (!this.viewDiffsFileByFile) { @@ -188,6 +199,23 @@ export default { return currentFileNumber < diffFiles.length ? currentFileNumber + 1 : null; }, + visibleWarning() { + let visible = false; + + if (this.renderOverflowWarning) { + visible = this.$options.alerts.ALERT_OVERFLOW_HIDDEN; + } else if (this.isDiffHead && this.hasConflicts) { + visible = this.$options.alerts.ALERT_MERGE_CONFLICT; + } else if ( + this.hasCollapsedFile && + !this.collapsedWarningDismissed && + !this.viewDiffsFileByFile + ) { + visible = this.$options.alerts.ALERT_COLLAPSED_FILES; + } + + return visible; + }, }, watch: { commit(newCommit, oldCommit) { @@ -401,6 +429,9 @@ export default { this.toggleShowTreeList(false); } }, + dismissCollapsedWarning() { + this.collapsedWarningDismissed = true; + }, }, minTreeWidth: MIN_TREE_WIDTH, maxTreeWidth: MAX_TREE_WIDTH, @@ -418,18 +449,23 @@ export default { /> +
+import { mapActions } from 'vuex'; + +import { GlAlert, GlButton } from '@gitlab/ui'; + +import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants'; + +export default { + components: { + GlAlert, + GlButton, + }, + props: { + limited: { + type: Boolean, + required: false, + default: false, + }, + dismissed: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + isDismissed: this.dismissed, + }; + }, + computed: { + containerClasses() { + return { + [CENTERED_LIMITED_CONTAINER_CLASSES]: this.limited, + }; + }, + }, + + methods: { + ...mapActions('diffs', ['expandAllFiles']), + dismiss() { + this.isDismissed = true; + this.$emit('dismiss'); + }, + expand() { + this.expandAllFiles(); + this.dismiss(); + }, + }, +}; + + + diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index a79a385b9bc204e64aa9f466cd4b17bd4ea7a10a..dc97d9993da21bb6577b74348ca3dcdaef1c4d95 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -68,6 +68,11 @@ export const DIFFS_PER_PAGE = 20; export const DIFF_COMPARE_BASE_VERSION_INDEX = -1; export const DIFF_COMPARE_HEAD_VERSION_INDEX = -2; +// Diff View Alerts +export const ALERT_OVERFLOW_HIDDEN = 'overflow'; +export const ALERT_MERGE_CONFLICT = 'merge-conflict'; +export const ALERT_COLLAPSED_FILES = 'collapsed'; + // State machine states export const STATE_IDLING = 'idle'; export const STATE_LOADING = 'loading'; diff --git a/changelogs/unreleased/feature-collapsed-files-warning.yml b/changelogs/unreleased/feature-collapsed-files-warning.yml new file mode 100644 index 0000000000000000000000000000000000000000..8fc1cc6b701a15a01676addeb36e6c2df917dfd8 --- /dev/null +++ b/changelogs/unreleased/feature-collapsed-files-warning.yml @@ -0,0 +1,5 @@ +--- +title: Add a warning when any diff files are collapsed +merge_request: 40752 +author: +type: added diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a59786d97ca3aa20666c5204cbb09ffd883b0f69..906cf574b3facb5756ec110793a18554cbc27459 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10374,6 +10374,9 @@ msgstr "" msgid "Expand all" msgstr "" +msgid "Expand all files" +msgstr "" + msgid "Expand approvers" msgstr "" @@ -11216,6 +11219,9 @@ msgstr "" msgid "Footer message" msgstr "" +msgid "For a faster browsing experience, some files are collapsed by default." +msgstr "" + msgid "For help setting up the Service Desk for your instance, please contact an administrator." msgstr "" @@ -23556,6 +23562,9 @@ msgstr "" msgid "Solution" msgstr "" +msgid "Some changes are not shown" +msgstr "" + msgid "Some child epics may be hidden due to applied filters" msgstr "" diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js index 5c2b684e165e0c4e4984e6565ce5458b06558c32..cd3a6aa0e28431e8f450762e1eca9b58e9c8c027 100644 --- a/spec/frontend/diffs/components/app_spec.js +++ b/spec/frontend/diffs/components/app_spec.js @@ -9,6 +9,7 @@ import NoChanges from '~/diffs/components/no_changes.vue'; import DiffFile from '~/diffs/components/diff_file.vue'; import CompareVersions from '~/diffs/components/compare_versions.vue'; import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue'; +import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue'; import CommitWidget from '~/diffs/components/commit_widget.vue'; import TreeList from '~/diffs/components/tree_list.vue'; import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants'; @@ -22,6 +23,10 @@ const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`; const COMMIT_URL = '[BASE URL]/OLD'; const UPDATED_COMMIT_URL = '[BASE URL]/NEW'; +function getCollapsedFilesWarning(wrapper) { + return wrapper.find(CollapsedFilesWarning); +} + describe('diffs/components/app', () => { const oldMrTabs = window.mrTabs; let store; @@ -668,24 +673,51 @@ describe('diffs/components/app', () => { ); }); - it('should render hidden files warning if render overflow warning is present', () => { - createComponent({}, ({ state }) => { - state.diffs.renderOverflowWarning = true; - state.diffs.realSize = '5'; - state.diffs.plainDiffPath = 'plain diff path'; - state.diffs.emailPatchPath = 'email patch path'; - state.diffs.size = 1; + describe('warnings', () => { + describe('hidden files', () => { + it('should render hidden files warning if render overflow warning is present', () => { + createComponent({}, ({ state }) => { + state.diffs.renderOverflowWarning = true; + state.diffs.realSize = '5'; + state.diffs.plainDiffPath = 'plain diff path'; + state.diffs.emailPatchPath = 'email patch path'; + state.diffs.size = 1; + }); + + expect(wrapper.find(HiddenFilesWarning).exists()).toBe(true); + expect(wrapper.find(HiddenFilesWarning).props()).toEqual( + expect.objectContaining({ + total: '5', + plainDiffPath: 'plain diff path', + emailPatchPath: 'email patch path', + visible: 1, + }), + ); + }); }); - expect(wrapper.find(HiddenFilesWarning).exists()).toBe(true); - expect(wrapper.find(HiddenFilesWarning).props()).toEqual( - expect.objectContaining({ - total: '5', - plainDiffPath: 'plain diff path', - emailPatchPath: 'email patch path', - visible: 1, - }), - ); + describe('collapsed files', () => { + it('should render the collapsed files warning if there are any collapsed files', () => { + createComponent({}, ({ state }) => { + state.diffs.diffFiles = [{ viewer: { collapsed: true } }]; + }); + + expect(getCollapsedFilesWarning(wrapper).exists()).toBe(true); + }); + + it('should not render the collapsed files warning if the user has dismissed the alert already', async () => { + createComponent({}, ({ state }) => { + state.diffs.diffFiles = [{ viewer: { collapsed: true } }]; + }); + + expect(getCollapsedFilesWarning(wrapper).exists()).toBe(true); + + wrapper.vm.collapsedWarningDismissed = true; + await wrapper.vm.$nextTick(); + + expect(getCollapsedFilesWarning(wrapper).exists()).toBe(false); + }); + }); }); it('should display commit widget if store has a commit', () => { diff --git a/spec/frontend/diffs/components/collapsed_files_warning_spec.js b/spec/frontend/diffs/components/collapsed_files_warning_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..670eab5472f95c5c7181414f939194d80dda77de --- /dev/null +++ b/spec/frontend/diffs/components/collapsed_files_warning_spec.js @@ -0,0 +1,88 @@ +import Vuex from 'vuex'; +import { shallowMount, mount, createLocalVue } from '@vue/test-utils'; +import createStore from '~/diffs/store/modules'; +import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue'; +import { CENTERED_LIMITED_CONTAINER_CLASSES } from '~/diffs/constants'; + +const propsData = { + limited: true, + mergeable: true, + resolutionPath: 'a-path', +}; +const limitedClasses = CENTERED_LIMITED_CONTAINER_CLASSES.split(' '); + +describe('CollapsedFilesWarning', () => { + const localVue = createLocalVue(); + let store; + let wrapper; + + localVue.use(Vuex); + + const getAlertActionButton = () => + wrapper.find(CollapsedFilesWarning).find('button.gl-alert-action:first-child'); + const getAlertCloseButton = () => wrapper.find(CollapsedFilesWarning).find('button'); + + const createComponent = (props = {}, { full } = { full: false }) => { + const mounter = full ? mount : shallowMount; + store = new Vuex.Store({ + modules: { + diffs: createStore(), + }, + }); + + wrapper = mounter(CollapsedFilesWarning, { + propsData: { ...propsData, ...props }, + localVue, + store, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it.each` + limited | containerClasses + ${true} | ${limitedClasses} + ${false} | ${[]} + `( + 'has the correct container classes when limited is $limited', + ({ limited, containerClasses }) => { + createComponent({ limited }); + + expect(wrapper.classes()).toEqual(containerClasses); + }, + ); + + it.each` + present | dismissed + ${false} | ${true} + ${true} | ${false} + `('toggles the alert when dismissed is $dismissed', ({ present, dismissed }) => { + createComponent({ dismissed }); + + expect(wrapper.find('[data-testid="root"]').exists()).toBe(present); + }); + + it('dismisses the component when the alert "x" is clicked', async () => { + createComponent({}, { full: true }); + + expect(wrapper.find('[data-testid="root"]').exists()).toBe(true); + + getAlertCloseButton().element.click(); + + await wrapper.vm.$nextTick(); + + expect(wrapper.find('[data-testid="root"]').exists()).toBe(false); + }); + + it('triggers the expandAllFiles action when the alert action button is clicked', () => { + createComponent({}, { full: true }); + + jest.spyOn(wrapper.vm.$store, 'dispatch').mockReturnValue(undefined); + + getAlertActionButton().vm.$emit('click'); + + expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/expandAllFiles', undefined); + }); +});