Commit 2f02dc07 authored by Savas Vedova's avatar Savas Vedova

Merge branch '343523-use-date-attributes-when-listing-iterations' into 'master'

Use start and due date attributes when listing iterations

See merge request gitlab-org/gitlab!72847
parents 2da743f6 393773cf
......@@ -15,6 +15,8 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { n__, s__, __ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
import Tracking from '~/tracking';
import { formatDate } from '~/lib/utils/datetime_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import AccessorUtilities from '../../lib/utils/accessor';
import { inactiveId, LIST, ListType, toggleFormEventPrefix } from '../constants';
import eventHub from '../eventhub';
......@@ -40,7 +42,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [Tracking.mixin()],
mixins: [Tracking.mixin(), glFeatureFlagMixin()],
inject: {
boardId: {
default: '',
......@@ -86,6 +88,13 @@ export default {
listTitle() {
return this.list?.label?.description || this.list?.assignee?.name || this.list.title || '';
},
listIterationPeriod() {
const iteration = this.list?.iteration;
return iteration ? this.getIterationPeriod(iteration) : '';
},
isIterationList() {
return this.listType === ListType.iteration;
},
showListHeaderButton() {
return !this.disabled && this.listType !== ListType.closed;
},
......@@ -96,7 +105,10 @@ export default {
return this.listType === ListType.assignee && this.showListDetails;
},
showIterationListDetails() {
return this.listType === ListType.iteration && this.showListDetails;
return this.isIterationList && this.showListDetails;
},
iterationCadencesAvailable() {
return this.isIterationList && this.glFeatures.iterationCadences;
},
showListDetails() {
return !this.list.collapsed || !this.isSwimlanesHeader;
......@@ -208,6 +220,16 @@ export default {
updateListFunction() {
this.updateList({ listId: this.list.id, collapsed: this.list.collapsed });
},
/**
* TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/344619
* This method also exists as a utility function in ee/../iterations/utils.js
* Remove the duplication when the EE code is separated from this compoment.
*/
getIterationPeriod({ startDate, dueDate }) {
const start = formatDate(startDate, 'mmm d, yyyy', true);
const due = formatDate(dueDate, 'mmm d, yyyy', true);
return `${start} - ${due}`;
},
},
};
</script>
......@@ -307,6 +329,13 @@ export default {
class="board-title-main-text gl-text-truncate"
>
{{ listTitle }}
<div
v-if="iterationCadencesAvailable"
class="gl-display-inline-block"
data-testid="board-list-iteration-period"
>
<time class="gl-text-gray-400">{{ listIterationPeriod }}</time>
</div>
</span>
<span
v-if="listType === 'assignee'"
......
......@@ -13,6 +13,9 @@ import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form
import { ListType } from '~/boards/constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getIterationPeriod } from 'ee/iterations/utils';
import IterationPeriod from 'ee/iterations/components/iteration_period.vue';
export const listTypeInfo = {
[ListType.label]: {
......@@ -57,10 +60,12 @@ export default {
GlFormGroup,
GlFormRadio,
GlFormRadioGroup,
IterationPeriod,
},
directives: {
GlTooltip,
},
mixins: [glFeatureFlagMixin()],
inject: [
'scopedLabelsAvailable',
'milestoneListsAvailable',
......@@ -221,6 +226,7 @@ export default {
this.selectedItem = { ...item };
}
},
getIterationPeriod,
},
};
</script>
......@@ -318,7 +324,14 @@ export default {
:sub-label="`@${item.username}`"
:src="item.avatarUrl"
/>
<span v-else>{{ item.title }}</span>
<div v-else class="gl-display-inline-block">
{{ item.title }}
<IterationPeriod
v-if="iterationTypeSelected && glFeatures.iterationCadences"
data-testid="new-column-iteration-period"
>{{ getIterationPeriod(item) }}</IterationPeriod
>
</div>
</label>
</gl-form-radio-group>
......
......@@ -20,6 +20,8 @@ fragment BoardListFragment on BoardList {
iteration {
id
title
startDate
dueDate
webPath
description
}
......
......@@ -13,6 +13,8 @@ import {
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { fetchPolicies } from '~/lib/graphql';
import { __, s__ } from '~/locale';
import IterationPeriod from 'ee/iterations/components/iteration_period.vue';
import { getIterationPeriod } from '../utils';
import { Namespace } from '../constants';
import groupQuery from '../queries/group_iterations_in_cadence.query.graphql';
import projectQuery from '../queries/project_iterations_in_cadence.query.graphql';
......@@ -51,6 +53,7 @@ export default {
GlModal,
GlSkeletonLoader,
TimeboxStatusBadge,
IterationPeriod,
},
apollo: {
workspace: {
......@@ -218,6 +221,7 @@ export default {
focusMenu() {
this.$refs.menu.$el.focus();
},
getIterationPeriod,
},
};
</script>
......@@ -300,11 +304,12 @@ export default {
<li
v-for="iteration in iterations"
:key="iteration.id"
class="gl-bg-gray-10 gl-p-5 gl-border-t-solid gl-border-gray-100 gl-border-t-1 gl-list-style-position-inside"
class="gl-bg-gray-10 gl-p-5 gl-border-t-solid gl-border-gray-100 gl-border-t-1"
>
<router-link :to="path(iteration.id)">
{{ iteration.title }}
</router-link>
<IterationPeriod class="gl-pt-2">{{ getIterationPeriod(iteration) }}</IterationPeriod>
<timebox-status-badge v-if="showStateBadge" :state="iteration.state" />
</li>
</ol>
......
<template>
<div>
<time class="gl-text-gray-400">
<slot></slot>
</time>
</div>
</template>
import { formatDate } from '~/lib/utils/datetime_utility';
const PERIOD_DATE_FORMAT = 'mmm d, yyyy';
/**
* The arguments are two date strings in formatted in ISO 8601 (YYYY-MM-DD)
*
* @returns {string} ex. "Oct 1, 2021 - Oct 10, 2021"
*/
export function getIterationPeriod({ startDate, dueDate }) {
const start = formatDate(startDate, PERIOD_DATE_FORMAT, true);
const due = formatDate(dueDate, PERIOD_DATE_FORMAT, true);
return `${start} - ${due}`;
}
......@@ -10,6 +10,8 @@ import {
} from '@gitlab/ui';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import IterationPeriod from 'ee/iterations/components/iteration_period.vue';
import { getIterationPeriod } from 'ee/iterations/utils';
import { iterationSelectTextMap, iterationDisplayState } from '../constants';
import groupIterationsQuery from '../queries/iterations.query.graphql';
......@@ -25,6 +27,7 @@ export default {
GlSearchBoxByType,
GlDropdownSectionHeader,
GlLoadingIcon,
IterationPeriod,
},
mixins: [glFeatureFlagMixin()],
apollo: {
......@@ -73,7 +76,11 @@ export default {
return;
}
const { title } = iteration.iterationCadence;
const cadenceIteration = { id: iteration.id, title: iteration.title };
const cadenceIteration = {
id: iteration.id,
title: iteration.title,
period: getIterationPeriod(iteration),
};
const cadence = cadences.find((cad) => cad.title === title);
if (cadence) {
cadence.iterations.push(cadenceIteration);
......@@ -144,8 +151,10 @@ export default {
:is-check-item="true"
:is-checked="isIterationChecked(iterationItem.id)"
@click="onClick(iterationItem)"
>{{ iterationItem.title }}</gl-dropdown-item
>
{{ iterationItem.title }}
<IterationPeriod>{{ iterationItem.period }}</IterationPeriod>
</gl-dropdown-item>
</template>
</template>
</gl-dropdown>
......
......@@ -7,7 +7,9 @@ import {
GlLink,
} from '@gitlab/ui';
import SidebarDropdownWidget from 'ee/sidebar/components/sidebar_dropdown_widget.vue';
import IterationPeriod from 'ee/iterations/components/iteration_period.vue';
import { IssuableType } from '~/issue_show/constants';
import { getIterationPeriod } from 'ee/iterations/utils';
import { IssuableAttributeType } from '../constants';
export default {
......@@ -19,6 +21,7 @@ export default {
GlIcon,
GlLink,
SidebarDropdownWidget,
IterationPeriod,
},
props: {
attrWorkspacePath: {
......@@ -45,6 +48,9 @@ export default {
getCadenceTitle(currentIteration) {
return currentIteration?.iterationCadence?.title;
},
getIterationPeriod(iteration) {
return getIterationPeriod({ startDate: iteration?.startDate, dueDate: iteration?.dueDate });
},
getIterationCadences(iterations) {
const cadences = [];
iterations.forEach((iteration) => {
......@@ -52,7 +58,11 @@ export default {
return;
}
const { title } = iteration.iterationCadence;
const cadenceIteration = { id: iteration.id, title: iteration.title };
const cadenceIteration = {
id: iteration.id,
title: iteration.title,
period: this.getIterationPeriod(iteration),
};
const cadence = cadences.find((cad) => cad.title === title);
if (cadence) {
cadence.iterations.push(cadenceIteration);
......@@ -83,8 +93,11 @@ export default {
:href="attributeUrl"
data-qa-selector="iteration_link"
>
<gl-icon name="iteration" class="gl-mr-1" />
{{ attributeTitle }}
<div>
<gl-icon name="iteration" class="gl-mr-1" />
{{ attributeTitle }}
</div>
<IterationPeriod>{{ getIterationPeriod(currentAttribute) }}</IterationPeriod>
</gl-link>
</template>
<template #list="{ attributesList = [], isAttributeChecked, updateAttribute }">
......@@ -102,6 +115,7 @@ export default {
@click="updateAttribute(iteration.id)"
>
{{ iteration.title }}
<IterationPeriod>{{ iteration.period }}</IterationPeriod>
</gl-dropdown-item>
</template>
</template>
......
fragment IterationFragment on Iteration {
id
title
startDate
dueDate
webUrl
iterationCadence {
id
......
......@@ -209,6 +209,7 @@ RSpec.describe 'Issue Sidebar' do
within '[data-testid="iteration-edit"]' do
expect(page).to have_text(iteration_cadence.title)
expect(page).to have_text(iteration.title)
expect(page).to have_text(iteration_period(iteration))
end
select_iteration(iteration.title)
......@@ -216,6 +217,7 @@ RSpec.describe 'Issue Sidebar' do
within '[data-testid="select-iteration"]' do
expect(page).to have_text(iteration_cadence.title)
expect(page).to have_text(iteration.title)
expect(page).to have_text(iteration_period(iteration))
end
find_and_click_edit_iteration
......@@ -295,4 +297,8 @@ RSpec.describe 'Issue Sidebar' do
wait_for_requests
end
end
def iteration_period(iteration)
iteration.start_date.strftime("%b%e, %Y") + ' - ' + iteration.due_date.strftime("%b%e, %Y")
end
end
......@@ -38,6 +38,7 @@ describe('BoardAddNewColumn', () => {
iterations = [],
getListByTypeId = jest.fn(),
actions = {},
glFeatures = {},
} = {}) => {
wrapper = extendedWrapper(
shallowMount(BoardAddNewColumn, {
......@@ -75,6 +76,7 @@ describe('BoardAddNewColumn', () => {
milestoneListsAvailable: true,
assigneeListsAvailable: true,
iterationListsAvailable: true,
glFeatures,
},
}),
);
......@@ -92,6 +94,8 @@ describe('BoardAddNewColumn', () => {
const findForm = () => wrapper.findComponent(BoardAddNewColumnForm);
const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn');
const submitButton = () => wrapper.findByTestId('addNewColumnButton');
const findLabels = () => wrapper.findComponent(GlDropdown).findAll('label');
const findIterationPeriod = (item) => item.find('[data-testid="new-column-iteration-period"]');
const listTypeSelect = (type) => {
const radio = wrapper
.findAllComponents(GlFormRadio)
......@@ -100,6 +104,11 @@ describe('BoardAddNewColumn', () => {
radio.element.value = type;
radio.vm.$emit('change', type);
};
const selectIteration = async () => {
listTypeSelect(ListType.iteration);
await nextTick();
};
it('clicking cancel hides the form', () => {
const setAddColumnFormVisibility = jest.fn();
......@@ -203,17 +212,19 @@ describe('BoardAddNewColumn', () => {
});
describe('iteration list', () => {
const iterationMountOptions = {
iterations: mockIterations,
actions: {
fetchIterations: jest.fn(),
},
};
beforeEach(async () => {
mountComponent({
iterations: mockIterations,
actions: {
fetchIterations: jest.fn(),
},
...iterationMountOptions,
});
listTypeSelect(ListType.iteration);
await nextTick();
await selectIteration();
});
it('sets iteration placeholder text in form', () => {
......@@ -230,5 +241,32 @@ describe('BoardAddNewColumn', () => {
expect(itemList.at(0).attributes('value')).toBe(mockIterations[0].id);
expect(itemList.at(1).attributes('value')).toBe(mockIterations[1].id);
});
describe('iteration_cadences feature flag is off', () => {
it('does not display iteration period', async () => {
const labels = findLabels();
expect(findIterationPeriod(labels.at(0)).exists()).toBe(false);
expect(findIterationPeriod(labels.at(1)).exists()).toBe(false);
});
});
describe('iteration_cadences feature flag is on', () => {
it('displays iteration period', async () => {
mountComponent({
...iterationMountOptions,
glFeatures: {
iterationCadences: true,
},
});
await selectIteration();
const labels = findLabels();
expect(labels.at(0).text()).toContain('Oct 5, 2021 - Oct 10, 2021');
expect(findIterationPeriod(labels.at(0)).isVisible()).toBe(true);
expect(labels.at(1).text()).toContain('Oct 12, 2021 - Oct 17, 2021');
expect(findIterationPeriod(labels.at(1)).isVisible()).toBe(true);
});
});
});
});
......@@ -4,7 +4,7 @@ import Vuex from 'vuex';
import BoardListHeader from 'ee/boards/components/board_list_header.vue';
import defaultGetters from 'ee/boards/stores/getters';
import { mockLabelList } from 'jest/boards/mock_data';
import { mockList, mockLabelList } from 'jest/boards/mock_data';
import { ListType, inactiveId } from '~/boards/constants';
import boardsEventHub from '~/boards/eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
......@@ -13,6 +13,24 @@ const localVue = createLocalVue();
localVue.use(Vuex);
const listMocks = {
[ListType.assignee]: {
assignee: {},
},
[ListType.iteration]: {
iteration: {
startDate: '2021-11-01',
dueDate: '2021-11-05',
},
},
[ListType.label]: {
...mockLabelList,
},
[ListType.backlog]: {
...mockList,
},
};
describe('Board List Header Component', () => {
let store;
let wrapper;
......@@ -26,20 +44,16 @@ describe('Board List Header Component', () => {
currentUserId = 1,
state = { activeId: inactiveId },
getters = {},
glFeatures = {},
} = {}) => {
const boardId = '1';
const listMock = {
...mockLabelList,
...listMocks[listType],
listType,
collapsed,
};
if (listType === ListType.assignee) {
delete listMock.label;
listMock.assignee = {};
}
if (withLocalStorage) {
localStorage.setItem(
`boards.${boardId}.${listMock.listType}.${listMock.id}.expanded`,
......@@ -69,11 +83,13 @@ describe('Board List Header Component', () => {
boardId,
weightFeatureAvailable,
currentUserId,
glFeatures,
},
});
};
const findSettingsButton = () => wrapper.find({ ref: 'settingsBtn' });
const findIterationPeriod = () => wrapper.find('[data-testid="board-list-iteration-period"]');
afterEach(() => {
wrapper.destroy();
......@@ -107,7 +123,7 @@ describe('Board List Header Component', () => {
it('emits `toggle-epic-form` event on Sidebar eventHub when clicked', async () => {
await newEpicButton.vm.$emit('click');
expect(boardsEventHub.$emit).toHaveBeenCalledWith(`toggle-epic-form-${mockLabelList.id}`);
expect(boardsEventHub.$emit).toHaveBeenCalledWith(`toggle-epic-form-${mockList.id}`);
expect(boardsEventHub.$emit).toHaveBeenCalledTimes(1);
});
});
......@@ -184,4 +200,28 @@ describe('Board List Header Component', () => {
expect(wrapper.find({ ref: 'weightTooltip' }).exists()).toBe(false);
});
});
describe('iteration cadence', () => {
describe('iteration_cadences feature flag is on', () => {
it('displays iteration period', () => {
createComponent({
listType: ListType.iteration,
glFeatures: {
iterationCadences: true,
},
});
expect(findIterationPeriod().text()).toContain('Nov 1, 2021 - Nov 5, 2021');
expect(findIterationPeriod().isVisible()).toBe(true);
});
});
describe('iteration_cadences feature flag is off', () => {
it('does not display iteration period', () => {
createComponent({ listType: ListType.iteration });
expect(findIterationPeriod().exists()).toBe(false);
});
});
});
});
......@@ -108,10 +108,14 @@ export const mockIterations = [
{
id: 'gid://gitlab/Iteration/1',
title: 'Iteration 1',
startDate: '2021-10-05',
dueDate: '2021-10-10',
},
{
id: 'gid://gitlab/Iteration/2',
title: 'Iteration 2',
startDate: '2021-10-12',
dueDate: '2021-10-17',
},
];
......
......@@ -44,6 +44,8 @@ describe('Iteration cadence list item', () => {
},
];
const iterationPeriods = ['Aug 13, 2021 - Aug 14, 2021'];
const cadence = {
id: 'gid://gitlab/Iterations::Cadence/561',
title: 'Weekly cadence',
......@@ -180,15 +182,16 @@ describe('Iteration cadence list item', () => {
expect(findCreateIterationButton().exists()).toBe(canCreateIteration);
});
it('shows iterations after loading', async () => {
it('shows iterations with dates after loading', async () => {
await createComponent();
expand();
await waitForPromises();
iterations.forEach(({ title }) => {
iterations.forEach(({ title }, i) => {
expect(wrapper.text()).toContain(title);
expect(wrapper.text()).toContain(iterationPeriods[i]);
});
});
......
......@@ -22,6 +22,8 @@ const TEST_ITERATIONS = [
{
id: '11',
title: 'Test Title',
startDate: '2021-10-01',
dueDate: '2021-10-05',
webUrl: '',
state: '',
iterationCadence: {
......@@ -32,6 +34,8 @@ const TEST_ITERATIONS = [
{
id: '22',
title: 'Another Test Title',
startDate: '2021-10-06',
dueDate: '2021-10-10',
webUrl: '',
state: '',
iterationCadence: {
......@@ -42,6 +46,8 @@ const TEST_ITERATIONS = [
{
id: '33',
title: 'Yet Another Test Title',
startDate: '2021-10-11',
dueDate: '2021-10-15',
webUrl: '',
state: '',
iterationCadence: {
......@@ -265,16 +271,19 @@ describe('IterationDropdown', () => {
const dropdownItems = wrapper.findAll('li');
expect(dropdownItems.at(0).text()).toBe('Assign Iteration');
expect(dropdownItems.at(1).text()).toBe('No iteration');
expect(dropdownItems.at(1).text()).toContain('No iteration');
expect(dropdownItems.at(2).findComponent(GlDropdownDivider).exists()).toBe(true);
expect(dropdownItems.at(3).findComponent(GlDropdownSectionHeader).text()).toBe('My Cadence');
expect(dropdownItems.at(4).text()).toBe('Test Title');
expect(dropdownItems.at(5).text()).toBe('Yet Another Test Title');
expect(dropdownItems.at(4).text()).toContain('Test Title');
expect(dropdownItems.at(4).text()).toContain('Oct 1, 2021 - Oct 5, 2021');
expect(dropdownItems.at(5).text()).toContain('Yet Another Test Title');
expect(dropdownItems.at(5).text()).toContain('Oct 11, 2021 - Oct 15, 2021');
expect(dropdownItems.at(6).findComponent(GlDropdownDivider).exists()).toBe(true);
expect(dropdownItems.at(7).findComponent(GlDropdownSectionHeader).text()).toBe(
'My Second Cadence',
);
expect(dropdownItems.at(8).text()).toBe('Another Test Title');
expect(dropdownItems.at(8).text()).toContain('Another Test Title');
expect(dropdownItems.at(8).text()).toContain('Oct 6, 2021 - Oct 10, 2021');
});
});
});
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