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();
+ },
+ },
+};
+
+
+
+
+
+
+ {{ __('For a faster browsing experience, some files are collapsed by default.') }}
+
+
+
+ {{ __('Expand all files') }}
+
+
+
+
+
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);
+ });
+});