Commit 8b6589fe authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '349739-roadmap-open-closed-as-progress-bar' into 'master'

Roadmap - Introduce progress tracking setting

See merge request gitlab-org/gitlab!79593
parents aeef5e96 fad39868
<script>
import { GlPopover, GlProgressBar, GlIcon } from '@gitlab/ui';
import { mapState } from 'vuex';
import { __, sprintf } from '~/locale';
import {
EPIC_DETAILS_CELL_WIDTH,
......@@ -7,6 +8,7 @@ import {
PRESET_TYPES,
SMALL_TIMELINE_BAR,
TIMELINE_CELL_MIN_WIDTH,
PROGRESS_COUNT,
} from '../constants';
import CommonMixin from '../mixins/common_mixin';
......@@ -55,6 +57,7 @@ export default {
},
},
computed: {
...mapState(['progressTracking']),
timelineBarInnerStyle() {
return {
maxWidth: `${this.clientWidth - EPIC_DETAILS_CELL_WIDTH}px`,
......@@ -75,33 +78,49 @@ export default {
timelineBarTitle() {
return this.isTimelineBarSmall ? '...' : this.epic.title;
},
epicTotalWeight() {
if (this.epic.descendantWeightSum) {
const { openedIssues, closedIssues } = this.epic.descendantWeightSum;
progressTrackingIsCount() {
return this.progressTracking === PROGRESS_COUNT;
},
epicDescendants() {
return this.progressTrackingIsCount
? this.epic.descendantCounts
: this.epic.descendantWeightSum;
},
epicTotal() {
if (this.epicDescendants) {
const { openedIssues, closedIssues } = this.epicDescendants;
return openedIssues + closedIssues;
}
return undefined;
},
epicWeightPercentage() {
return this.epicTotalWeight
? Math.round(
(this.epic.descendantWeightSum.closedIssues / this.epicTotalWeight) * PERCENTAGE,
)
epicPercentage() {
return this.epicTotal
? Math.round((this.epicDescendants.closedIssues / this.epicTotal) * PERCENTAGE)
: 0;
},
epicWeightPercentageText() {
return sprintf(__(`%{percentage}%% weight completed`), {
percentage: this.epicWeightPercentage,
});
epicPercentageText() {
const str = this.progressTrackingIsCount
? __('%{percentage}%% issues closed')
: __('%{percentage}%% weight completed');
return sprintf(str, { percentage: this.epicPercentage });
},
popoverWeightText() {
if (this.epic.descendantWeightSum) {
return sprintf(__('%{completedWeight} of %{totalWeight} weight completed'), {
completedWeight: this.epic.descendantWeightSum.closedIssues,
totalWeight: this.epicTotalWeight,
popoverText() {
if (this.epicDescendants) {
const str = this.progressTrackingIsCount
? __('%{completed} of %{total} issues closed')
: __('%{completed} of %{total} weight completed');
return sprintf(str, {
completed: this.epicDescendants.closedIssues,
total: this.epicTotal,
});
}
return __('- of - weight completed');
return this.progressTrackingIsCount
? __('- of - issues closed')
: __('- of - weight completed');
},
progressIcon() {
return this.progressTrackingIsCount ? 'issue-closed' : 'weight';
},
},
methods: {
......@@ -125,19 +144,19 @@ export default {
<div v-if="!isTimelineBarSmall" class="gl-display-flex gl-align-items-center">
<gl-progress-bar
class="epic-bar-progress gl-flex-grow-1 gl-mr-2"
:value="epicWeightPercentage"
:value="epicPercentage"
aria-hidden="true"
/>
<div class="gl-font-sm gl-display-flex gl-align-items-center gl-white-space-nowrap">
<gl-icon class="gl-mr-1" :size="12" name="weight" />
<p class="gl-m-0" :aria-label="epicWeightPercentageText">{{ epicWeightPercentage }}%</p>
<gl-icon class="gl-mr-1" :size="12" :name="progressIcon" />
<p class="gl-m-0" :aria-label="epicPercentageText">{{ epicPercentage }}%</p>
</div>
</div>
</div>
</a>
<gl-popover :target="generateKey(epic)" :title="epic.title" placement="left">
<p class="gl-text-gray-500 gl-m-0">{{ timeframeString(epic) }}</p>
<p class="gl-m-0">{{ popoverWeightText }}</p>
<p class="gl-m-0">{{ popoverText }}</p>
</gl-popover>
</div>
</template>
......@@ -71,7 +71,7 @@ export default {
};
},
computed: {
...mapState(['presetType', 'epicsState', 'sortedBy', 'filterParams']),
...mapState(['presetType', 'epicsState', 'sortedBy', 'filterParams', 'progressTracking']),
selectedEpicStateTitle() {
if (this.epicsState === EPICS_STATES.ALL) {
return __('All epics');
......
<script>
import { GlFormGroup, GlFormRadioGroup } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
import { PROGRESS_TRACKING_OPTIONS } from '../constants';
export default {
components: {
GlFormGroup,
GlFormRadioGroup,
},
computed: {
...mapState(['progressTracking']),
},
methods: {
...mapActions(['setProgressTracking']),
handleProgressTrackingChange(option) {
if (option !== this.progressTracking) {
this.setProgressTracking(option);
}
},
},
i18n: {
header: __('Progress tracking'),
},
PROGRESS_TRACKING_OPTIONS,
};
</script>
<template>
<div>
<gl-form-group
class="gl-mb-0"
:label="$options.i18n.header"
data-testid="roadmap-progress-tracking"
>
<gl-form-radio-group
:checked="progressTracking"
stacked
:options="$options.PROGRESS_TRACKING_OPTIONS"
@change="handleProgressTrackingChange"
/>
</gl-form-group>
</div>
</template>
......@@ -2,12 +2,14 @@
import { GlDrawer } from '@gitlab/ui';
import RoadmapDaterange from './roadmap_daterange.vue';
import RoadmapEpicsState from './roadmap_epics_state.vue';
import RoadmapProgressTracking from './roadmap_progress_tracking.vue';
export default {
components: {
GlDrawer,
RoadmapDaterange,
RoadmapEpicsState,
RoadmapProgressTracking,
},
props: {
isOpen: {
......@@ -47,6 +49,7 @@ export default {
<template #default>
<roadmap-daterange :timeframe-range-type="timeframeRangeType" />
<roadmap-epics-state />
<roadmap-progress-tracking />
</template>
</gl-drawer>
</template>
import { s__ } from '~/locale';
import { s__, __ } from '~/locale';
/*
Update the counterparts in roadmap.scss when making changes.
......@@ -61,3 +61,21 @@ export const EPIC_LEVEL_MARGIN = {
};
export const ROADMAP_PAGE_SIZE = 50;
export const PROGRESS_WEIGHT = 'WEIGHT';
export const PROGRESS_COUNT = 'COUNT';
export const PROGRESS_TRACKING_OPTIONS = [
{ text: __('Use issue weight'), value: PROGRESS_WEIGHT },
{ text: __('Use issue count'), value: PROGRESS_COUNT },
];
export const UNSUPPORTED_ROADMAP_PARAMS = [
'scope',
'utf8',
'state',
'sort',
'timeframe_range_type',
'layout',
'progress',
];
......@@ -51,6 +51,7 @@ export default {
'not[author_username]': notAuthorUsername,
'not[my_reaction_emoji]': notMyReactionEmoji,
'not[label_name][]': notLabelName,
progress: this.progressTracking,
};
},
},
......
......@@ -17,6 +17,8 @@ fragment BaseEpic on Epic {
descendantCounts {
openedEpics
closedEpics
closedIssues
openedIssues
}
group {
id
......
......@@ -11,7 +11,7 @@ import EpicItem from './components/epic_item.vue';
import EpicItemContainer from './components/epic_item_container.vue';
import roadmapApp from './components/roadmap_app.vue';
import { DATE_RANGES } from './constants';
import { DATE_RANGES, PROGRESS_WEIGHT, UNSUPPORTED_ROADMAP_PARAMS } from './constants';
import createStore from './store';
import {
......@@ -74,7 +74,7 @@ export default () => {
});
const filterParams = {
...convertObjectPropsToCamelCase(rawFilterParams, {
dropKeys: ['scope', 'utf8', 'state', 'sort', 'timeframe_range_type', 'layout'], // These keys are unsupported/unnecessary
dropKeys: UNSUPPORTED_ROADMAP_PARAMS,
}),
// We shall put parsed value of `confidential` only
// when it is defined.
......@@ -103,6 +103,7 @@ export default () => {
timeframeRangeType,
presetType,
timeframe,
progressTracking: rawFilterParams.progress || PROGRESS_WEIGHT,
};
},
created() {
......@@ -121,6 +122,7 @@ export default () => {
isChildEpics: this.isChildEpics,
hasFiltersApplied: this.hasFiltersApplied,
allowSubEpics: this.allowSubEpics,
progressTracking: this.progressTracking,
});
},
methods: {
......
......@@ -326,3 +326,6 @@ export const setFilterParams = ({ commit }, filterParams) =>
commit(types.SET_FILTER_PARAMS, filterParams);
export const setSortedBy = ({ commit }, sortedBy) => commit(types.SET_SORTED_BY, sortedBy);
export const setProgressTracking = ({ commit }, progressTracking) =>
commit(types.SET_PROGRESS_TRACKING, progressTracking);
......@@ -32,3 +32,4 @@ export const SET_EPICS_STATE = 'SET_EPICS_STATE';
export const SET_DATERANGE = 'SET_DATERANGE';
export const SET_FILTER_PARAMS = 'SET_FILTER_PARAMS';
export const SET_SORTED_BY = 'SET_SORTED_BY';
export const SET_PROGRESS_TRACKING = 'SET_PROGRESS_TRACKING';
......@@ -140,4 +140,8 @@ export default {
state.sortedBy = sortedBy;
resetEpics(state);
},
[types.SET_PROGRESS_TRACKING](state, progressTracking) {
state.progressTracking = progressTracking;
},
};
......@@ -2,6 +2,7 @@ export default () => ({
// API Calls
basePath: '',
epicsState: '',
progressTracking: '',
filterParams: null,
// Data
......
import { GlPopover, GlProgressBar } from '@gitlab/ui';
import { GlIcon, GlPopover, GlProgressBar } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import EpicItemTimeline from 'ee/roadmap/components/epic_item_timeline.vue';
import { DATE_RANGES, PRESET_TYPES } from 'ee/roadmap/constants';
import { DATE_RANGES, PRESET_TYPES, PROGRESS_COUNT, PROGRESS_WEIGHT } from 'ee/roadmap/constants';
import createStore from 'ee/roadmap/store';
import { getTimeframeForRangeType } from 'ee/roadmap/utils/roadmap_utils';
import { mockTimeframeInitialDate, mockFormattedEpic } from 'ee_jest/roadmap/mock_data';
Vue.use(Vuex);
const mockTimeframeMonths = getTimeframeForRangeType({
timeframeRangeType: DATE_RANGES.CURRENT_YEAR,
presetType: PRESET_TYPES.MONTHS,
......@@ -17,8 +22,16 @@ const createComponent = ({
timeframe = mockTimeframeMonths,
timeframeItem = mockTimeframeMonths[0],
timeframeString = '',
progressTracking = PROGRESS_WEIGHT,
} = {}) => {
const store = createStore();
store.dispatch('setInitialData', {
progressTracking,
});
return shallowMount(EpicItemTimeline, {
store,
propsData: {
epic,
startDate: epic.originalStartDate,
......@@ -51,7 +64,7 @@ describe('EpicItemTimelineComponent', () => {
});
it('shows the progress bar with correct value', () => {
expect(wrapper.find(GlProgressBar).attributes('value')).toBe('60');
expect(wrapper.findComponent(GlProgressBar).attributes('value')).toBe('60');
});
it('shows the percentage', () => {
......@@ -61,6 +74,19 @@ describe('EpicItemTimelineComponent', () => {
it('contains a link to the epic', () => {
expect(getEpicBar(wrapper).attributes('href')).toBe(mockFormattedEpic.webUrl);
});
it.each`
progressTracking | icon
${PROGRESS_WEIGHT} | ${'weight'}
${PROGRESS_COUNT} | ${'issue-closed'}
`(
'displays icon $icon when progressTracking equals $progressTracking',
({ progressTracking, icon }) => {
wrapper = createComponent({ progressTracking });
expect(wrapper.findComponent(GlIcon).props('name')).toBe(icon);
},
);
});
describe('popover', () => {
......@@ -70,21 +96,37 @@ describe('EpicItemTimelineComponent', () => {
expect(wrapper.find(GlPopover).text()).toContain('Jun 26, 2017 – Mar 10, 2018');
});
it('shows the weight completed', () => {
wrapper = createComponent();
expect(wrapper.find(GlPopover).text()).toContain('3 of 5 weight completed');
});
it('shows the weight completed with no numbers when there is no weights information', () => {
wrapper = createComponent({
epic: {
...mockFormattedEpic,
descendantWeightSum: undefined,
},
});
expect(wrapper.find(GlPopover).text()).toContain('- of - weight completed');
});
it.each`
progressTracking | option | text
${PROGRESS_WEIGHT} | ${'weight'} | ${'3 of 5 weight completed'}
${PROGRESS_COUNT} | ${'issues'} | ${'3 of 5 issues closed'}
`(
'shows $option completed when progressTracking equals $progressTracking',
({ progressTracking, text }) => {
wrapper = createComponent({ progressTracking });
expect(wrapper.findComponent(GlPopover).text()).toContain(text);
},
);
it.each`
progressTracking | option | text
${PROGRESS_WEIGHT} | ${'weight'} | ${'- of - weight completed'}
${PROGRESS_COUNT} | ${'issues'} | ${'- of - issues closed'}
`(
'shows $option completed with no numbers when there is no $option information and progressTracking equals $progressTracking',
({ progressTracking, text }) => {
wrapper = createComponent({
progressTracking,
epic: {
...mockFormattedEpic,
descendantWeightSum: undefined,
descendantCounts: undefined,
},
});
expect(wrapper.findComponent(GlPopover).text()).toContain(text);
},
);
});
});
......@@ -3,7 +3,7 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import RoadmapFilters from 'ee/roadmap/components/roadmap_filters.vue';
import { PRESET_TYPES, EPICS_STATES, DATE_RANGES } from 'ee/roadmap/constants';
import { PRESET_TYPES, EPICS_STATES, DATE_RANGES, PROGRESS_WEIGHT } from 'ee/roadmap/constants';
import createStore from 'ee/roadmap/store';
import { getTimeframeForRangeType } from 'ee/roadmap/utils/roadmap_utils';
import {
......@@ -55,6 +55,7 @@ const createComponent = ({
sortedBy,
filterParams,
timeframe,
progressTracking: PROGRESS_WEIGHT,
});
return shallowMountExtended(RoadmapFilters, {
......@@ -116,7 +117,7 @@ describe('RoadmapFilters', () => {
await nextTick();
expect(global.window.location.href).toBe(
`${TEST_HOST}/?state=${EPICS_STATES.CLOSED}&sort=end_date_asc&layout=MONTHS&author_username=root&label_name%5B%5D=Bug&milestone_title=4.0&confidential=true`,
`${TEST_HOST}/?state=${EPICS_STATES.CLOSED}&sort=end_date_asc&layout=MONTHS&author_username=root&label_name%5B%5D=Bug&milestone_title=4.0&confidential=true&progress=WEIGHT`,
);
});
});
......@@ -148,10 +149,10 @@ describe('RoadmapFilters', () => {
});
it('renders epics state toggling dropdown', () => {
const epicsStateDropdown = wrapper.find(GlDropdown);
const epicsStateDropdown = wrapper.findComponent(GlDropdown);
expect(epicsStateDropdown.exists()).toBe(true);
expect(epicsStateDropdown.findAll(GlDropdownItem)).toHaveLength(3);
expect(epicsStateDropdown.findAllComponents(GlDropdownItem)).toHaveLength(3);
});
it('does not render settings button', () => {
......
import { GlFormGroup, GlFormRadioGroup } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createStore from 'ee/roadmap/store';
import RoadmapProgressTracking from 'ee/roadmap/components/roadmap_progress_tracking.vue';
import { PROGRESS_WEIGHT, PROGRESS_TRACKING_OPTIONS } from 'ee/roadmap/constants';
describe('RoadmapProgressTracking', () => {
let wrapper;
const createComponent = () => {
const store = createStore();
store.dispatch('setInitialData', {
progressTracking: PROGRESS_WEIGHT,
});
wrapper = shallowMountExtended(RoadmapProgressTracking, {
store,
});
};
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findFormRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
it('renders form group', () => {
expect(findFormGroup().exists()).toBe(true);
expect(findFormGroup().attributes('label')).toBe('Progress tracking');
});
it('renders radio form group', () => {
expect(findFormRadioGroup().exists()).toBe(true);
expect(findFormRadioGroup().props('options')).toEqual(PROGRESS_TRACKING_OPTIONS);
});
});
});
......@@ -3,6 +3,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RoadmapSettings from 'ee/roadmap/components/roadmap_settings.vue';
import RoadmapDaterange from 'ee/roadmap/components/roadmap_daterange.vue';
import RoadmapEpicsState from 'ee/roadmap/components/roadmap_epics_state.vue';
import RoadmapProgressTracking from 'ee/roadmap/components/roadmap_progress_tracking.vue';
describe('RoadmapSettings', () => {
let wrapper;
......@@ -16,6 +17,7 @@ describe('RoadmapSettings', () => {
const findSettingsDrawer = () => wrapper.findComponent(GlDrawer);
const findDaterange = () => wrapper.findComponent(RoadmapDaterange);
const findEpicsSate = () => wrapper.findComponent(RoadmapEpicsState);
const findProgressTracking = () => wrapper.findComponent(RoadmapProgressTracking);
beforeEach(() => {
createComponent();
......@@ -38,5 +40,9 @@ describe('RoadmapSettings', () => {
it('renders roadmap epics state component', () => {
expect(findEpicsSate().exists()).toBe(true);
});
it('renders roadmap progress tracking component', () => {
expect(findProgressTracking().exists()).toBe(true);
});
});
});
import Vue from 'vue';
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import EpicItemTimelineComponent from 'ee/roadmap/components/epic_item_timeline.vue';
import { DATE_RANGES, PRESET_TYPES } from 'ee/roadmap/constants';
import { DATE_RANGES, PRESET_TYPES, PROGRESS_WEIGHT } from 'ee/roadmap/constants';
import createStore from 'ee/roadmap/store';
import { getTimeframeForRangeType } from 'ee/roadmap/utils/roadmap_utils';
import { mockTimeframeInitialDate, mockEpic } from 'ee_jest/roadmap/mock_data';
Vue.use(Vuex);
const mockTimeframeMonths = getTimeframeForRangeType({
timeframeRangeType: DATE_RANGES.CURRENT_YEAR,
presetType: PRESET_TYPES.MONTHS,
......@@ -20,7 +25,14 @@ describe('MonthsPresetMixin', () => {
timeframeItem = mockTimeframeMonths[0],
epic = mockEpic,
} = {}) => {
const store = createStore();
store.dispatch('setInitialData', {
progressTracking: PROGRESS_WEIGHT,
});
return shallowMount(EpicItemTimelineComponent, {
store,
propsData: {
presetType,
timeframe,
......
import Vue from 'vue';
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import EpicItemTimelineComponent from 'ee/roadmap/components/epic_item_timeline.vue';
import { DATE_RANGES, PRESET_TYPES } from 'ee/roadmap/constants';
import { DATE_RANGES, PRESET_TYPES, PROGRESS_WEIGHT } from 'ee/roadmap/constants';
import createStore from 'ee/roadmap/store';
import { getTimeframeForRangeType } from 'ee/roadmap/utils/roadmap_utils';
import { mockTimeframeInitialDate, mockEpic } from 'ee_jest/roadmap/mock_data';
Vue.use(Vuex);
const mockTimeframeQuarters = getTimeframeForRangeType({
timeframeRangeType: DATE_RANGES.THREE_YEARS,
presetType: PRESET_TYPES.QUARTERS,
......@@ -20,7 +25,14 @@ describe('QuartersPresetMixin', () => {
timeframeItem = mockTimeframeQuarters[0],
epic = mockEpic,
} = {}) => {
const store = createStore();
store.dispatch('setInitialData', {
progressTracking: PROGRESS_WEIGHT,
});
return shallowMount(EpicItemTimelineComponent, {
store,
propsData: {
presetType,
timeframe,
......
import Vue from 'vue';
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import EpicItemTimelineComponent from 'ee/roadmap/components/epic_item_timeline.vue';
import { DATE_RANGES, PRESET_TYPES } from 'ee/roadmap/constants';
import { DATE_RANGES, PRESET_TYPES, PROGRESS_WEIGHT } from 'ee/roadmap/constants';
import createStore from 'ee/roadmap/store';
import { getTimeframeForRangeType } from 'ee/roadmap/utils/roadmap_utils';
import { mockTimeframeInitialDate, mockEpic } from 'ee_jest/roadmap/mock_data';
Vue.use(Vuex);
const mockTimeframeWeeks = getTimeframeForRangeType({
timeframeRangeType: DATE_RANGES.CURRENT_QUARTER,
presetType: PRESET_TYPES.WEEKS,
......@@ -20,7 +25,14 @@ describe('WeeksPresetMixin', () => {
timeframeItem = mockTimeframeWeeks[0],
epic = mockEpic,
} = {}) => {
const store = createStore();
store.dispatch('setInitialData', {
progressTracking: PROGRESS_WEIGHT,
});
return shallowMount(EpicItemTimelineComponent, {
store,
propsData: {
presetType,
timeframe,
......
......@@ -222,6 +222,8 @@ export const mockRawEpic = {
descendantCounts: {
openedEpics: 3,
closedEpics: 2,
closedIssues: 3,
openedIssues: 2,
__typename: 'EpicDescendantCount',
},
group: mockGroup1,
......
......@@ -657,6 +657,17 @@ describe('Roadmap Vuex Actions', () => {
payload: { timeframeRangeType: 'CURRENT_YEAR', presetType: 'MONTHS' },
},
],
);
});
});
describe('setProgressTracking', () => {
it('should set progressTracking in store state', () => {
return testAction(
actions.setProgressTracking,
'COUNT',
state,
[{ type: types.SET_PROGRESS_TRACKING, payload: 'COUNT' }],
[],
);
});
......
import * as types from 'ee/roadmap/store/mutation_types';
import mutations from 'ee/roadmap/store/mutations';
import { PROGRESS_COUNT } from 'ee/roadmap/constants';
import defaultState from 'ee/roadmap/store/state';
import { getTimeframeForRangeType } from 'ee/roadmap/utils/roadmap_utils';
......@@ -342,4 +343,17 @@ describe('Roadmap Store Mutations', () => {
});
});
});
describe('SET_PROGRESS_TRACKING', () => {
it('Should set `progressTracking` to the state', () => {
const progressTracking = PROGRESS_COUNT;
setEpicMockData(state);
mutations[types.SET_PROGRESS_TRACKING](state, progressTracking);
expect(state).toMatchObject({
progressTracking,
});
});
});
});
......@@ -510,6 +510,12 @@ msgstr[1] ""
msgid "%{completedWeight} of %{totalWeight} weight completed"
msgstr ""
msgid "%{completed} of %{total} issues closed"
msgstr ""
msgid "%{completed} of %{total} weight completed"
msgstr ""
msgid "%{cores} cores"
msgstr ""
......@@ -858,6 +864,9 @@ msgstr ""
msgid "%{openedIssues} open, %{closedIssues} closed"
msgstr ""
msgid "%{percentage}%% issues closed"
msgstr ""
msgid "%{percentage}%% weight completed"
msgstr ""
......@@ -1287,6 +1296,9 @@ msgid_plural "- Users"
msgstr[0] ""
msgstr[1] ""
msgid "- of - issues closed"
msgstr ""
msgid "- of - weight completed"
msgstr ""
......@@ -27709,6 +27721,9 @@ msgstr ""
msgid "Progress"
msgstr ""
msgid "Progress tracking"
msgstr ""
msgid "Project"
msgstr ""
......@@ -38998,6 +39013,12 @@ msgstr ""
msgid "Use hashed storage paths for newly created and renamed repositories. Always enabled since 13.0."
msgstr ""
msgid "Use issue count"
msgstr ""
msgid "Use issue weight"
msgstr ""
msgid "Use one line per URI"
msgstr ""
......
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