Commit e9684fc5 authored by Coung Ngo's avatar Coung Ngo

Truncate counts on group issues list refactor

Truncate counts to mimic functionality on existing Haml page.
The issues list refactor is behind the feature flag
`vue_issues_list`.

https://gitlab.com/gitlab-org/gitlab/-/issues/322755
parent c29e5b6d
......@@ -644,6 +644,7 @@ export default {
:tabs="$options.IssuableListTabs"
:current-tab="state"
:tab-counts="tabCounts"
:truncate-counts="!isProject"
:issuables-loading="$apollo.queries.issues.loading"
:is-manual-ordering="isManualOrdering"
:show-bulk-edit-sidebar="showBulkEditSidebar"
......
export const BYTES_IN_KIB = 1024;
export const DEFAULT_DEBOUNCE_AND_THROTTLE_MS = 250;
export const HIDDEN_CLASS = 'hidden';
export const THOUSAND = 1000;
export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80;
export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12;
......
import { sprintf, __ } from '~/locale';
import { BYTES_IN_KIB } from './constants';
import { BYTES_IN_KIB, THOUSAND } from './constants';
/**
* Function that allows a number with an X amount of decimals
......@@ -85,6 +85,27 @@ export function numberToHumanSize(size, digits = 2) {
return sprintf(__('%{size} GiB'), { size: bytesToGiB(size).toFixed(digits) });
}
/**
* Converts a number to kilos or megas.
*
* For example:
* - 123 becomes 123
* - 123456 becomes 123.4k
* - 123456789 becomes 123.4m
*
* @param number Number to format
* @param digits The number of digits to appear after the decimal point
* @return {string} Formatted number
*/
export function numberToMetricPrefix(number, digits = 1) {
if (number < THOUSAND) {
return number.toString();
}
if (number < THOUSAND ** 2) {
return `${(number / THOUSAND).toFixed(digits)}k`;
}
return `${(number / THOUSAND ** 2).toFixed(digits)}m`;
}
/**
* A simple method that returns the value of a + b
* It seems unessesary, but when combined with a reducer it
......
......@@ -78,6 +78,11 @@ export default {
required: false,
default: null,
},
truncateCounts: {
type: Boolean,
required: false,
default: false,
},
currentTab: {
type: String,
required: true,
......@@ -261,6 +266,7 @@ export default {
:tabs="tabs"
:tab-counts="tabCounts"
:current-tab="currentTab"
:truncate-counts="truncateCounts"
@click="$emit('click-tab', $event)"
>
<template #nav-actions>
......
<script>
import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import { formatNumber } from '~/locale';
export default {
......@@ -22,6 +23,11 @@ export default {
type: String,
required: true,
},
truncateCounts: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
isTabActive(tabName) {
......@@ -31,7 +37,7 @@ export default {
return Number.isInteger(this.tabCounts[tab.name]);
},
formatNumber(count) {
return formatNumber(count);
return this.truncateCounts ? numberToMetricPrefix(count) : formatNumber(count);
},
},
};
......
......@@ -174,6 +174,7 @@ Object {
},
],
"totalItems": 3,
"truncateCounts": false,
"urlParams": Object {
"labels[]": undefined,
"search": undefined,
......
......@@ -4,6 +4,7 @@ import {
bytesToMiB,
bytesToGiB,
numberToHumanSize,
numberToMetricPrefix,
sum,
isOdd,
median,
......@@ -99,6 +100,21 @@ describe('Number Utils', () => {
});
});
describe('numberToMetricPrefix', () => {
it.each`
number | expected
${123} | ${'123'}
${1234} | ${'1.2k'}
${12345} | ${'12.3k'}
${123456} | ${'123.5k'}
${1234567} | ${'1.2m'}
${12345678} | ${'12.3m'}
${123456789} | ${'123.5m'}
`('returns $expected given $number', ({ number, expected }) => {
expect(numberToMetricPrefix(number)).toBe(expected);
});
});
describe('sum', () => {
it('should add up two values', () => {
expect(sum(1, 2)).toEqual(3);
......
import { GlTab, GlBadge } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { mount, shallowMount } from '@vue/test-utils';
import { setLanguage } from 'helpers/locale_helper';
import IssuableTabs from '~/vue_shared/issuable/list/components/issuable_tabs.vue';
......@@ -10,17 +11,18 @@ const createComponent = ({
tabs = mockIssuableListProps.tabs,
tabCounts = mockIssuableListProps.tabCounts,
currentTab = mockIssuableListProps.currentTab,
truncateCounts = false,
mountFn = shallowMount,
} = {}) =>
mount(IssuableTabs, {
mountFn(IssuableTabs, {
propsData: {
tabs,
tabCounts,
currentTab,
truncateCounts,
},
slots: {
'nav-actions': `
<button class="js-new-issuable">New issuable</button>
`,
'nav-actions': `<button class="js-new-issuable">New issuable</button>`,
},
});
......@@ -29,7 +31,6 @@ describe('IssuableTabs', () => {
beforeEach(() => {
setLanguage('en');
wrapper = createComponent();
});
afterEach(() => {
......@@ -40,60 +41,71 @@ describe('IssuableTabs', () => {
const findAllGlBadges = () => wrapper.findAllComponents(GlBadge);
const findAllGlTabs = () => wrapper.findAllComponents(GlTab);
describe('methods', () => {
describe('isTabActive', () => {
it.each`
tabName | currentTab | returnValue
${'opened'} | ${'opened'} | ${true}
${'opened'} | ${'closed'} | ${false}
`(
'returns $returnValue when tab name is "$tabName" is current tab is "$currentTab"',
async ({ tabName, currentTab, returnValue }) => {
wrapper.setProps({
currentTab,
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.isTabActive(tabName)).toBe(returnValue);
},
);
});
describe('tabs', () => {
it.each`
currentTab | returnValue
${'opened'} | ${'true'}
${'closed'} | ${undefined}
`(
'when "$currentTab" is the selected tab, the Open tab is active=$returnValue',
({ currentTab, returnValue }) => {
wrapper = createComponent({ currentTab });
const openTab = findAllGlTabs().at(0);
expect(openTab.attributes('active')).toBe(returnValue);
},
);
});
describe('template', () => {
it('renders gl-tab for each tab within `tabs` array', () => {
const tabsEl = findAllGlTabs();
wrapper = createComponent();
const tabs = findAllGlTabs();
expect(tabsEl.exists()).toBe(true);
expect(tabsEl).toHaveLength(mockIssuableListProps.tabs.length);
expect(tabs).toHaveLength(mockIssuableListProps.tabs.length);
});
it('renders gl-badge component within a tab', () => {
it('renders gl-badge component within a tab', async () => {
wrapper = createComponent({ mountFn: mount });
await nextTick();
const badges = findAllGlBadges();
// Does not render `All` badge since it has an undefined count
expect(badges).toHaveLength(2);
expect(badges.at(0).text()).toBe('5,000');
expect(badges.at(0).text()).toBe('5,678');
expect(badges.at(1).text()).toBe(`${mockIssuableListProps.tabCounts.closed}`);
});
it('renders contents for slot "nav-actions"', () => {
const buttonEl = wrapper.find('button.js-new-issuable');
wrapper = createComponent();
expect(buttonEl.exists()).toBe(true);
expect(buttonEl.text()).toBe('New issuable');
const button = wrapper.find('button.js-new-issuable');
expect(button.text()).toBe('New issuable');
});
});
describe('counts', () => {
it('can display as truncated', async () => {
wrapper = createComponent({ truncateCounts: true, mountFn: mount });
await nextTick();
expect(findAllGlBadges().at(0).text()).toBe('5.7k');
});
});
describe('events', () => {
it('gl-tab component emits `click` event on `click` event', () => {
const tabEl = findAllGlTabs().at(0);
wrapper = createComponent();
const openTab = findAllGlTabs().at(0);
tabEl.vm.$emit('click', 'opened');
openTab.vm.$emit('click', 'opened');
expect(wrapper.emitted('click')).toBeTruthy();
expect(wrapper.emitted('click')[0]).toEqual(['opened']);
expect(wrapper.emitted('click')).toEqual([['opened']]);
});
});
});
......@@ -133,7 +133,7 @@ export const mockTabs = [
];
export const mockTabCounts = {
opened: 5000,
opened: 5678,
closed: 0,
all: undefined,
};
......
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