Commit 228b70ae authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Diff stats dropdown styling update due to migration to GlDropdown

Changelog: changed
parent 119b05ce
import $ from 'jquery';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { stickyMonitor } from './lib/utils/sticky';
export default (stickyTop) => {
stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
initDeprecatedJQueryDropdown($('.js-diff-stats-dropdown'), {
filterable: true,
remoteFilter: false,
});
};
import Vue from 'vue';
import DiffStatsDropdown from '~/vue_shared/components/diff_stats_dropdown.vue';
import { stickyMonitor } from './lib/utils/sticky';
export const initDiffStatsDropdown = (stickyTop) => {
if (stickyTop) {
stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
}
const el = document.querySelector('.js-diff-stats-dropdown');
if (!el) {
return false;
}
const { changed, added, deleted, files } = el.dataset;
return new Vue({
el,
render: (createElement) =>
createElement(DiffStatsDropdown, {
props: {
changed: parseInt(changed, 10),
added: parseInt(added, 10),
deleted: parseInt(deleted, 10),
files: JSON.parse(files),
},
}),
});
};
......@@ -7,7 +7,7 @@ import createEventHub from '~/helpers/event_hub_factory';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import Diff from './diff';
import createFlash from './flash';
import initChangesDropdown from './init_changes_dropdown';
import { initDiffStatsDropdown } from './init_diff_stats_dropdown';
import axios from './lib/utils/axios_utils';
import {
parseUrlPathname,
......@@ -433,8 +433,7 @@ export default class MergeRequestTabs {
.then(({ data }) => {
const $container = $('#diffs');
$container.html(data.html);
initChangesDropdown(this.stickyTop);
initDiffStatsDropdown(this.stickyTop);
localTimeAgo(document.querySelectorAll('#diffs .js-timeago'));
syntaxHighlight($('#diffs .js-syntax-highlight'));
......
......@@ -4,8 +4,8 @@ import loadAwardsHandler from '~/awards_handler';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Diff from '~/diff';
import createFlash from '~/flash';
import initChangesDropdown from '~/init_changes_dropdown';
import initDeprecatedNotes from '~/init_deprecated_notes';
import { initDiffStatsDropdown } from '~/init_diff_stats_dropdown';
import axios from '~/lib/utils/axios_utils';
import { handleLocationHash } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
......@@ -17,7 +17,7 @@ import '~/sourcegraph/load';
const hasPerfBar = document.querySelector('.with-performance-bar');
const performanceHeight = hasPerfBar ? 35 : 0;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
initDiffStatsDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
new ZenMode();
new ShortcutsNavigation();
......
import Diff from '~/diff';
import GpgBadges from '~/gpg_badges';
import initChangesDropdown from '~/init_changes_dropdown';
import { initDiffStatsDropdown } from '~/init_diff_stats_dropdown';
import initCompareSelector from '~/projects/compare';
initCompareSelector();
new Diff(); // eslint-disable-line no-new
const paddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
initDiffStatsDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
GpgBadges.fetch();
import { initDiffStatsDropdown } from '~/init_diff_stats_dropdown';
import initWikis from '~/pages/shared/wikis';
initWikis();
initDiffStatsDropdown();
<script>
import {
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlSearchBoxByType,
GlSprintf,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { __, n__, s__, sprintf } from '~/locale';
export const i18n = {
messageAdditionsDeletions: s__('Diffs|with %{additions} and %{deletions}'),
noFilesFound: __('No files found.'),
noFileNameAvailable: s__('Diffs|No file name available'),
searchFiles: __('Search files'),
};
export default {
i18n,
components: {
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlSearchBoxByType,
GlSprintf,
},
props: {
changed: {
type: Number,
required: true,
},
added: {
type: Number,
required: true,
},
deleted: {
type: Number,
required: true,
},
files: {
type: Array,
required: true,
},
},
data() {
return {
search: '',
};
},
computed: {
filteredFiles() {
return this.search.length > 0
? fuzzaldrinPlus.filter(this.files, this.search, { key: 'name' })
: this.files;
},
messageChanged() {
return sprintf(
n__(
'Diffs|Showing %{dropdownStart}%{count} changed file%{dropdownEnd}',
'Diffs|Showing %{dropdownStart}%{count} changed files%{dropdownEnd}',
this.changed,
),
{ count: this.changed },
);
},
additionsText() {
return n__('Diffs|%d addition', 'Diffs|%d additions', this.added);
},
deletionsText() {
return n__('Diffs|%d deletion', 'Diffs|%d deletions', this.deleted);
},
},
methods: {
jumpToFile(fileHash) {
window.location.hash = fileHash;
},
focusInput() {
this.$refs.search.focusInput();
},
},
};
</script>
<template>
<div>
<gl-sprintf :message="messageChanged">
<template #dropdown="{ content: dropdownText }">
<gl-dropdown
category="tertiary"
variant="confirm"
:text="dropdownText"
data-testid="diff-stats-dropdown"
class="gl-vertical-align-baseline"
toggle-class="gl-px-0! gl-font-weight-bold!"
menu-class="gl-w-auto!"
no-flip
@shown="focusInput"
>
<template #header>
<gl-search-box-by-type
ref="search"
v-model.trim="search"
:placeholder="$options.i18n.searchFiles"
/>
</template>
<gl-dropdown-item
v-for="file in filteredFiles"
:key="file.href"
:icon-name="file.icon"
:icon-color="file.iconColor"
@click="jumpToFile(file.href)"
>
<div class="gl-display-flex">
<span v-if="file.name" class="gl-font-weight-bold gl-mr-3 gl-text-truncate">{{
file.name
}}</span>
<span v-else class="gl-mr-3 gl-font-weight-bold gl-font-style-italic gl-gray-400">{{
$options.i18n.noFileNameAvailable
}}</span>
<span class="gl-ml-auto gl-white-space-nowrap">
<span class="gl-text-green-600">+{{ file.added }}</span>
<span class="gl-text-red-500">-{{ file.removed }}</span>
</span>
</div>
<div class="gl-text-gray-700 gl-overflow-hidden gl-text-overflow-ellipsis">
{{ file.path }}
</div>
</gl-dropdown-item>
<gl-dropdown-text v-if="!filteredFiles.length">
{{ $options.i18n.noFilesFound }}
</gl-dropdown-text>
</gl-dropdown>
</template>
</gl-sprintf>
<span
class="diff-stats-additions-deletions-expanded"
data-testid="diff-stats-additions-deletions-expanded"
>
<gl-sprintf :message="$options.i18n.messageAdditionsDeletions">
<template #additions>
<span class="gl-text-green-600 gl-font-weight-bold">{{ additionsText }}</span>
</template>
<template #deletions>
<span class="gl-text-red-500 gl-font-weight-bold">{{ deletionsText }}</span>
</template>
</gl-sprintf>
</span>
<div
class="diff-stats-additions-deletions-collapsed gl-float-right gl-display-none"
data-testid="diff-stats-additions-deletions-collapsed"
>
<span class="gl-text-green-600 gl-font-weight-bold">+{{ added }}</span>
<span class="gl-text-red-500 gl-font-weight-bold">-{{ deleted }}</span>
</div>
</div>
</template>
......@@ -682,26 +682,6 @@ table.code {
max-height: 50vh;
}
.diff-stats-summary-toggler {
padding: 0;
background-color: transparent;
border: 0;
color: $blue-600;
font-weight: $gl-font-weight-bold;
&:hover,
&:focus {
outline: none;
color: $blue-800;
}
.caret-icon {
position: relative;
top: 2px;
left: -1px;
}
}
// Mobile
@media (max-width: 480px) {
.diff-title {
......@@ -853,21 +833,14 @@ table.code {
.diff-files-changed {
.inline-parallel-buttons {
position: relative;
@include gl-relative;
z-index: 1;
}
.commit-stat-summary {
@include media-breakpoint-up(sm) {
background-color: $white;
}
}
@include media-breakpoint-up(sm) {
position: -webkit-sticky;
position: sticky;
@include gl-sticky;
top: $header-height + $mr-tabs-height;
background-color: $white;
@include gl-bg-white;
z-index: 200;
.with-performance-bar & {
......@@ -875,14 +848,13 @@ table.code {
}
&.is-stuck {
padding-top: 0;
padding-bottom: 0;
@include gl-py-0;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
.diff-stats-additions-deletions-expanded,
.inline-parallel-buttons {
display: none !important;
@include gl-display-none;
}
}
}
......@@ -890,12 +862,13 @@ table.code {
@include media-breakpoint-up(lg) {
&.is-stuck {
.diff-stats-additions-deletions-collapsed {
display: block !important;
@include gl-display-block;
}
}
}
}
.diff-file-changes {
max-width: 560px;
width: 100%;
......
......@@ -183,9 +183,9 @@ module DiffHelper
def diff_file_changed_icon_color(diff_file)
if diff_file.deleted_file?
"cred"
"danger"
elsif diff_file.new_file?
"cgreen"
"success"
end
end
......@@ -248,6 +248,23 @@ module DiffHelper
toggle_whitespace_link(url, options)
end
def diff_files_data(diff_files)
diffs_map = diff_files.map do |f|
{
href: "##{hexdigest(f.file_path)}",
title: f.new_path,
name: f.file_path,
path: diff_file_path_text(f),
icon: diff_file_changed_icon(f),
iconColor: "#{diff_file_changed_icon_color(f)}",
added: f.added_lines,
removed: f.removed_lines
}
end
diffs_map.to_json
end
def hide_whitespace?
params[:w] == '1'
end
......
- sum_added_lines = diff_files.sum(&:added_lines)
- sum_removed_lines = diff_files.sum(&:removed_lines)
.commit-stat-summary.dropdown
Showing
%button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown", display: "static" } }<
= pluralize(diff_files.size, "changed file")
= sprite_icon("chevron-down", css_class: "gl-ml-2")
%span.diff-stats-additions-deletions-expanded#diff-stats
with
%strong.cgreen= pluralize(sum_added_lines, 'addition')
and
%strong.cred= pluralize(sum_removed_lines, 'deletion')
.diff-stats-additions-deletions-collapsed.float-right.d-none{ "aria-hidden": "true", "aria-describedby": "diff-stats" }
%strong.cgreen<
+#{sum_added_lines}
%strong.cred<
\-#{sum_removed_lines}
.dropdown-menu.diff-file-changes
= dropdown_filter("Search files")
.dropdown-content
%ul
- diff_files.each do |diff_file|
%li
%a.diff-changed-file{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path }
= sprite_icon(diff_file_changed_icon(diff_file), css_class: "#{diff_file_changed_icon_color(diff_file)} diff-file-changed-icon gl-mr-3")
%span.diff-changed-file-content.gl-mr-3
- if diff_file.file_path
%strong.diff-changed-file-name
= diff_file.file_path
- else
%strong.diff-changed-blank-file-name
= s_('Diffs|No file name available')
%span.diff-changed-file-path.gl-mt-2= diff_file_path_text(diff_file)
%span.diff-changed-stats
%span.cgreen<
+#{diff_file.added_lines}
%span.cred<
\-#{diff_file.removed_lines}
%li.dropdown-menu-empty-item.hidden
%a
= _("No files found.")
.js-diff-stats-dropdown{ data: { changed: diff_files.size, added: diff_files.sum(&:added_lines), deleted: diff_files.sum(&:removed_lines), files: diff_files_data(diff_files) } }
import initClonePanel from '~/clone_panel';
import { initDiffStatsDropdown } from '~/init_diff_stats_dropdown';
import initWikis from '~/pages/shared/wikis';
initWikis();
initClonePanel();
initDiffStatsDropdown();
......@@ -11729,6 +11729,16 @@ msgstr ""
msgid "DiffsCompareBaseBranch|(base)"
msgstr ""
msgid "Diffs|%d addition"
msgid_plural "Diffs|%d additions"
msgstr[0] ""
msgstr[1] ""
msgid "Diffs|%d deletion"
msgid_plural "Diffs|%d deletions"
msgstr[0] ""
msgstr[1] ""
msgid "Diffs|No file name available"
msgstr ""
......@@ -11738,9 +11748,17 @@ msgstr ""
msgid "Diffs|Show all unchanged lines"
msgstr ""
msgid "Diffs|Showing %{dropdownStart}%{count} changed file%{dropdownEnd}"
msgid_plural "Diffs|Showing %{dropdownStart}%{count} changed files%{dropdownEnd}"
msgstr[0] ""
msgstr[1] ""
msgid "Diffs|Something went wrong while fetching diff lines."
msgstr ""
msgid "Diffs|with %{additions} and %{deletions}"
msgstr ""
msgid "Direct member"
msgstr ""
......
......@@ -42,7 +42,7 @@ RSpec.describe 'Commit' do
visit project_commit_path(project, commit)
end
it "shows an adjusted count for changed files on this page" do
it "shows an adjusted count for changed files on this page", :js do
expect(page).to have_content("Showing 1 changed file")
end
......
......@@ -12,7 +12,7 @@ RSpec.describe 'User browses commits' do
sign_in(user)
end
it 'renders commit' do
it 'renders commit', :js do
visit project_commit_path(project, sample_commit.id)
expect(page).to have_content(sample_commit.message.gsub(/\s+/, ' '))
......@@ -103,7 +103,7 @@ RSpec.describe 'User browses commits' do
context 'when the blob does not exist' do
let(:commit) { create(:commit, project: project) }
it 'renders successfully' do
it 'renders successfully', :js do
allow_next_instance_of(Gitlab::Diff::File) do |instance|
allow(instance).to receive(:blob).and_return(nil)
end
......@@ -113,7 +113,9 @@ RSpec.describe 'User browses commits' do
visit(project_commit_path(project, commit))
expect(find('.diff-file-changes', visible: false)).to have_content('files/ruby/popen.rb')
click_button '2 changed files'
expect(find('[data-testid="diff-stats-dropdown"]')).to have_content('files/ruby/popen.rb')
end
end
......
import {
GlSprintf,
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlSearchBoxByType,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DiffStatsDropdown, { i18n } from '~/vue_shared/components/diff_stats_dropdown.vue';
jest.mock('fuzzaldrin-plus', () => ({
filter: jest.fn().mockReturnValue([]),
}));
const mockFiles = [
{
added: 0,
href: '#a5cc2925ca8258af241be7e5b0381edf30266302',
icon: 'file-modified',
iconColor: '',
name: '',
path: '.gitignore',
removed: 3,
title: '.gitignore',
},
{
added: 1,
href: '#fa288d1472d29beccb489a676f68739ad365fc47',
icon: 'file-modified',
iconColor: 'danger',
name: 'package-lock.json',
path: 'lock/file/path',
removed: 1,
},
];
describe('Diff Stats Dropdown', () => {
let wrapper;
const createComponent = ({ changed = 0, added = 0, deleted = 0, files = [] } = {}) => {
wrapper = shallowMountExtended(DiffStatsDropdown, {
propsData: {
changed,
added,
deleted,
files,
},
stubs: {
GlSprintf,
GlDropdown,
},
});
};
const findChanged = () => wrapper.findComponent(GlDropdown);
const findChangedFiles = () => findChanged().findAllComponents(GlDropdownItem);
const findNoFilesText = () => findChanged().findComponent(GlDropdownText);
const findCollapsed = () => wrapper.findByTestId('diff-stats-additions-deletions-expanded');
const findExpanded = () => wrapper.findByTestId('diff-stats-additions-deletions-collapsed');
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
describe('file item', () => {
beforeEach(() => {
createComponent({ files: mockFiles });
});
it('when no file name provided ', () => {
expect(findChangedFiles().at(0).text()).toContain(i18n.noFileNameAvailable);
});
it('when all file data is available', () => {
const fileData = findChangedFiles().at(1);
const fileText = findChangedFiles().at(1).text();
expect(fileText).toContain(mockFiles[1].name);
expect(fileText).toContain(mockFiles[1].path);
expect(fileData.props()).toMatchObject({
iconName: mockFiles[1].icon,
iconColor: mockFiles[1].iconColor,
});
});
it('when no files changed', () => {
createComponent({ files: [] });
expect(findNoFilesText().text()).toContain(i18n.noFilesFound);
});
});
describe.each`
changed | added | deleted | expectedDropdownHeader | expectedAddedDeletedExpanded | expectedAddedDeletedCollapsed
${0} | ${0} | ${0} | ${'0 changed files'} | ${'+0 -0'} | ${'with 0 additions and 0 deletions'}
${2} | ${0} | ${2} | ${'2 changed files'} | ${'+0 -2'} | ${'with 0 additions and 2 deletions'}
${2} | ${2} | ${0} | ${'2 changed files'} | ${'+2 -0'} | ${'with 2 additions and 0 deletions'}
${2} | ${1} | ${1} | ${'2 changed files'} | ${'+1 -1'} | ${'with 1 addition and 1 deletion'}
${1} | ${0} | ${1} | ${'1 changed file'} | ${'+0 -1'} | ${'with 0 additions and 1 deletion'}
${1} | ${1} | ${0} | ${'1 changed file'} | ${'+1 -0'} | ${'with 1 addition and 0 deletions'}
${4} | ${2} | ${2} | ${'4 changed files'} | ${'+2 -2'} | ${'with 2 additions and 2 deletions'}
`(
'when there are $changed changed file(s), $added added and $deleted deleted file(s)',
({
changed,
added,
deleted,
expectedDropdownHeader,
expectedAddedDeletedExpanded,
expectedAddedDeletedCollapsed,
}) => {
beforeAll(() => {
createComponent({ changed, added, deleted });
});
afterAll(() => {
wrapper.destroy();
});
it(`dropdown header should be '${expectedDropdownHeader}'`, () => {
expect(findChanged().props('text')).toBe(expectedDropdownHeader);
});
it(`added and deleted count in expanded section should be '${expectedAddedDeletedExpanded}'`, () => {
expect(findExpanded().text()).toBe(expectedAddedDeletedExpanded);
});
it(`added and deleted count in collapsed section should be '${expectedAddedDeletedCollapsed}'`, () => {
expect(findCollapsed().text()).toBe(expectedAddedDeletedCollapsed);
});
},
);
describe('fuzzy file search', () => {
beforeEach(() => {
createComponent({ files: mockFiles });
});
it('should call `fuzzaldrinPlus.filter` to search for files when the search query is NOT empty', async () => {
const searchStr = 'file name';
findSearchBox().vm.$emit('input', searchStr);
await nextTick();
expect(fuzzaldrinPlus.filter).toHaveBeenCalledWith(mockFiles, searchStr, { key: 'name' });
});
it('should NOT call `fuzzaldrinPlus.filter` to search for files when the search query is empty', async () => {
const searchStr = '';
findSearchBox().vm.$emit('input', searchStr);
await nextTick();
expect(fuzzaldrinPlus.filter).not.toHaveBeenCalled();
});
});
describe('selecting file dropdown item', () => {
beforeEach(() => {
createComponent({ files: mockFiles });
});
it('updates the URL ', () => {
findChangedFiles().at(0).vm.$emit('click');
expect(window.location.hash).toBe(mockFiles[0].href);
findChangedFiles().at(1).vm.$emit('click');
expect(window.location.hash).toBe(mockFiles[1].href);
});
});
describe('on dropdown open', () => {
beforeEach(() => {
createComponent();
});
it('should set the search input focus', () => {
wrapper.vm.$refs.search.focusInput = jest.fn();
findChanged().vm.$emit('shown');
expect(wrapper.vm.$refs.search.focusInput).toHaveBeenCalled();
});
});
});
......@@ -157,7 +157,7 @@ RSpec.shared_examples 'User views a wiki page' do
expect(page).to have_link('updated home', href: wiki_page_path(wiki, wiki_page, version_id: commit2, action: :diff))
end
it 'between the current and the previous version of a page' do
it 'between the current and the previous version of a page', :js do
commit = wiki.commit
visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff)
......@@ -169,7 +169,7 @@ RSpec.shared_examples 'User views a wiki page' do
expect_diff_links(commit)
end
it 'between two old versions of a page' do
it 'between two old versions of a page', :js do
wiki_page.update(message: 'latest home change', content: 'updated [another link](other-page)') # rubocop:disable Rails/SaveBang:
commit = wiki.commit('HEAD^')
visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff)
......@@ -184,7 +184,7 @@ RSpec.shared_examples 'User views a wiki page' do
expect_diff_links(commit)
end
it 'for the oldest version of a page' do
it 'for the oldest version of a page', :js do
commit = wiki.commit('HEAD^')
visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'projects/diffs/_stats.html.haml' do
let(:project) { create(:project, :repository) }
let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') }
def render_view
render partial: "projects/diffs/stats", locals: { diff_files: commit.diffs.diff_files }
end
context 'when the commit contains several changes' do
it 'uses plural for additions' do
render_view
expect(rendered).to have_text('additions')
end
it 'uses plural for deletions' do
render_view
end
end
context 'when the commit contains no addition and no deletions' do
let(:commit) { project.commit('4cd80ccab63c82b4bad16faa5193fbd2aa06df40') }
it 'uses plural for additions' do
render_view
expect(rendered).to have_text('additions')
end
it 'uses plural for deletions' do
render_view
expect(rendered).to have_text('deletions')
end
end
context 'when the commit contains exactly one addition and one deletion' do
let(:commit) { project.commit('08f22f255f082689c0d7d39d19205085311542bc') }
it 'uses singular for additions' do
render_view
expect(rendered).to have_text('addition')
expect(rendered).not_to have_text('additions')
end
it 'uses singular for deletions' do
render_view
expect(rendered).to have_text('deletion')
expect(rendered).not_to have_text('deletions')
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment