Commit 0e908820 authored by Phil Hughes's avatar Phil Hughes

Merge branch '196066-add-milestone-expired-info' into 'master'

Show expired milestones at the bottom of the list within dropdown

Closes #196066

See merge request gitlab-org/gitlab!35595
parents 4e2a38e8 337585de
...@@ -9,6 +9,7 @@ const Api = { ...@@ -9,6 +9,7 @@ const Api = {
groupsPath: '/api/:version/groups.json', groupsPath: '/api/:version/groups.json',
groupPath: '/api/:version/groups/:id', groupPath: '/api/:version/groups/:id',
groupMembersPath: '/api/:version/groups/:id/members', groupMembersPath: '/api/:version/groups/:id/members',
groupMilestonesPath: '/api/:version/groups/:id/milestones',
subgroupsPath: '/api/:version/groups/:id/subgroups', subgroupsPath: '/api/:version/groups/:id/subgroups',
namespacesPath: '/api/:version/namespaces.json', namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json', groupProjectsPath: '/api/:version/groups/:id/projects.json',
...@@ -98,6 +99,14 @@ const Api = { ...@@ -98,6 +99,14 @@ const Api = {
return axios.get(url).then(({ data }) => data); return axios.get(url).then(({ data }) => data);
}, },
groupMilestones(groupId, params = {}) {
const url = Api.buildUrl(Api.groupMilestonesPath).replace(':id', encodeURIComponent(groupId));
return axios.get(url, {
params,
});
},
// Return namespaces list. Filtered by query // Return namespaces list. Filtered by query
namespaces(query, callback) { namespaces(query, callback) {
const url = Api.buildUrl(Api.namespacesPath); const url = Api.buildUrl(Api.namespacesPath);
...@@ -262,10 +271,12 @@ const Api = { ...@@ -262,10 +271,12 @@ const Api = {
}); });
}, },
projectMilestones(id) { projectMilestones(id, params = {}) {
const url = Api.buildUrl(Api.projectMilestonesPath).replace(':id', encodeURIComponent(id)); const url = Api.buildUrl(Api.projectMilestonesPath).replace(':id', encodeURIComponent(id));
return axios.get(url); return axios.get(url, {
params,
});
}, },
mergeRequests(params = {}) { mergeRequests(params = {}) {
......
...@@ -25,10 +25,6 @@ export default { ...@@ -25,10 +25,6 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
milestonePath: {
type: String,
required: true,
},
labelsPath: { labelsPath: {
type: String, type: String,
required: true, required: true,
...@@ -201,7 +197,6 @@ export default { ...@@ -201,7 +197,6 @@ export default {
:collapse-scope="isNewForm" :collapse-scope="isNewForm"
:board="board" :board="board"
:can-admin-board="canAdminBoard" :can-admin-board="canAdminBoard"
:milestone-path="milestonePath"
:labels-path="labelsPath" :labels-path="labelsPath"
:enable-scoped-labels="enableScopedLabels" :enable-scoped-labels="enableScopedLabels"
:project-id="projectId" :project-id="projectId"
......
...@@ -36,10 +36,6 @@ export default { ...@@ -36,10 +36,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
milestonePath: {
type: String,
required: true,
},
throttleDuration: { throttleDuration: {
type: Number, type: Number,
default: 200, default: 200,
...@@ -335,7 +331,6 @@ export default { ...@@ -335,7 +331,6 @@ export default {
<board-form <board-form
v-if="currentPage" v-if="currentPage"
:milestone-path="milestonePath"
:labels-path="labelsPath" :labels-path="labelsPath"
:project-id="projectId" :project-id="projectId"
:group-id="groupId" :group-id="groupId"
......
...@@ -17,10 +17,6 @@ export default { ...@@ -17,10 +17,6 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
milestonePath: {
type: String,
required: true,
},
labelPath: { labelPath: {
type: String, type: String,
required: true, required: true,
......
...@@ -38,10 +38,6 @@ export default { ...@@ -38,10 +38,6 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
milestonePath: {
type: String,
required: true,
},
labelPath: { labelPath: {
type: String, type: String,
required: true, required: true,
...@@ -149,11 +145,7 @@ export default { ...@@ -149,11 +145,7 @@ export default {
class="add-issues-modal d-flex position-fixed position-top-0 position-bottom-0 position-left-0 position-right-0 h-100" class="add-issues-modal d-flex position-fixed position-top-0 position-bottom-0 position-left-0 position-right-0 h-100"
> >
<div class="add-issues-container d-flex flex-column m-auto rounded"> <div class="add-issues-container d-flex flex-column m-auto rounded">
<modal-header <modal-header :project-id="projectId" :label-path="labelPath" />
:project-id="projectId"
:milestone-path="milestonePath"
:label-path="labelPath"
/>
<modal-list <modal-list
v-if="!loading && showList && !filterLoading" v-if="!loading && showList && !filterLoading"
:issue-link-base="issueLinkBase" :issue-link-base="issueLinkBase"
......
...@@ -27,7 +27,7 @@ export default () => { ...@@ -27,7 +27,7 @@ export default () => {
hasMissingBoards: parseBoolean(dataset.hasMissingBoards), hasMissingBoards: parseBoolean(dataset.hasMissingBoards),
canAdminBoard: parseBoolean(dataset.canAdminBoard), canAdminBoard: parseBoolean(dataset.canAdminBoard),
multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable), multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable),
projectId: Number(dataset.projectId), projectId: dataset.projectId ? Number(dataset.projectId) : 0,
groupId: Number(dataset.groupId), groupId: Number(dataset.groupId),
scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled), scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled),
weights: JSON.parse(dataset.weights), weights: JSON.parse(dataset.weights),
......
...@@ -4,10 +4,11 @@ ...@@ -4,10 +4,11 @@
import $ from 'jquery'; import $ from 'jquery';
import { template, escape } from 'lodash'; import { template, escape } from 'lodash';
import { __ } from '~/locale'; import { __, sprintf } from '~/locale';
import '~/gl_dropdown'; import '~/gl_dropdown';
import Api from '~/api';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility'; import { timeFor, parsePikadayDate, dateInWords } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store'; import ModalStore from './boards/stores/modal_store';
import boardsStore, { import boardsStore, {
boardStoreIssueSet, boardStoreIssueSet,
...@@ -34,10 +35,10 @@ export default class MilestoneSelect { ...@@ -34,10 +35,10 @@ export default class MilestoneSelect {
$els.each((i, dropdown) => { $els.each((i, dropdown) => {
let milestoneLinkNoneTemplate, let milestoneLinkNoneTemplate,
milestoneLinkTemplate, milestoneLinkTemplate,
milestoneExpiredLinkTemplate,
selectedMilestone, selectedMilestone,
selectedMilestoneDefault; selectedMilestoneDefault;
const $dropdown = $(dropdown); const $dropdown = $(dropdown);
const milestonesUrl = $dropdown.data('milestones');
const issueUpdateURL = $dropdown.data('issueUpdate'); const issueUpdateURL = $dropdown.data('issueUpdate');
const showNo = $dropdown.data('showNo'); const showNo = $dropdown.data('showNo');
const showAny = $dropdown.data('showAny'); const showAny = $dropdown.data('showAny');
...@@ -63,12 +64,43 @@ export default class MilestoneSelect { ...@@ -63,12 +64,43 @@ export default class MilestoneSelect {
milestoneLinkTemplate = template( milestoneLinkTemplate = template(
'<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>', '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
); );
milestoneExpiredLinkTemplate = template(
'<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %> (Past due)</a>',
);
milestoneLinkNoneTemplate = `<span class="no-value">${__('None')}</span>`; milestoneLinkNoneTemplate = `<span class="no-value">${__('None')}</span>`;
} }
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove, showMenuAbove,
data: (term, callback) => data: (term, callback) => {
axios.get(milestonesUrl).then(({ data }) => { let contextId = $dropdown.get(0).dataset.projectId;
let getMilestones = Api.projectMilestones;
if (!contextId) {
contextId = $dropdown.get(0).dataset.groupId;
getMilestones = Api.groupMilestones;
}
// We don't use $.data() as it caches initial value and never updates!
return getMilestones(contextId, { state: 'active' })
.then(({ data }) =>
data
.map(m => ({
...m,
// Public API includes `title` instead of `name`.
name: m.title,
}))
.sort((mA, mB) => {
// Move all expired milestones to the bottom.
if (mA.expired) {
return 1;
}
if (mB.expired) {
return -1;
}
return 0;
}),
)
.then(data => {
const extraOptions = []; const extraOptions = [];
if (showAny) { if (showAny) {
extraOptions.push({ extraOptions.push({
...@@ -106,15 +138,27 @@ export default class MilestoneSelect { ...@@ -106,15 +138,27 @@ export default class MilestoneSelect {
if (showMenuAbove) { if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove(); $dropdown.data('glDropdown').positionMenuAbove();
} }
$(`[data-milestone-id="${escape(selectedMilestone)}"] > a`).addClass('is-active'); $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
}), });
renderRow: milestone => ` },
<li data-milestone-id="${escape(milestone.name)}"> renderRow: milestone => {
const milestoneName = milestone.title || milestone.name;
let milestoneDisplayName = escape(milestoneName);
if (milestone.expired) {
milestoneDisplayName = sprintf(__('%{milestone} (expired)'), {
milestone: milestoneDisplayName,
});
}
return `
<li data-milestone-id="${escape(milestoneName)}">
<a href='#' class='dropdown-menu-milestone-link'> <a href='#' class='dropdown-menu-milestone-link'>
${escape(milestone.title)} ${milestoneDisplayName}
</a> </a>
</li> </li>
`, `;
},
filterable: true, filterable: true,
search: { search: {
fields: ['title'], fields: ['title'],
...@@ -149,7 +193,7 @@ export default class MilestoneSelect { ...@@ -149,7 +193,7 @@ export default class MilestoneSelect {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
} }
$('a.is-active', $el).removeClass('is-active'); $('a.is-active', $el).removeClass('is-active');
$(`[data-milestone-id="${escape(selectedMilestone)}"] > a`, $el).addClass('is-active'); $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
}, },
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: clickEvent => { clicked: clickEvent => {
...@@ -237,7 +281,16 @@ export default class MilestoneSelect { ...@@ -237,7 +281,16 @@ export default class MilestoneSelect {
if (data.milestone != null) { if (data.milestone != null) {
data.milestone.remaining = timeFor(data.milestone.due_date); data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title; data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone)); $value.html(
data.milestone.expired
? milestoneExpiredLinkTemplate({
...data.milestone,
remaining: sprintf(__('%{due_date} (Past due)'), {
due_date: dateInWords(parsePikadayDate(data.milestone.due_date)),
}),
})
: milestoneLinkTemplate(data.milestone),
);
return $sidebarCollapsedValue return $sidebarCollapsedValue
.attr( .attr(
'data-original-title', 'data-original-title',
......
...@@ -18,7 +18,8 @@ ...@@ -18,7 +18,8 @@
.dropdown .dropdown
%button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" }, %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" },
":data-selected" => "milestoneTitle", ":data-selected" => "milestoneTitle",
":data-issuable-id" => "issue.iid" } ":data-issuable-id" => "issue.iid",
":data-project-id" => "issue.project_id" }
= _("Milestone") = _("Milestone")
= icon("chevron-down") = icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable .dropdown-menu.dropdown-select.dropdown-menu-selectable
......
...@@ -45,7 +45,8 @@ ...@@ -45,7 +45,8 @@
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: "edit_milestone_link", track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" } = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: "edit_milestone_link", track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" }
.value.hide-collapsed .value.hide-collapsed
- if milestone.present? - if milestone.present?
= link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link', qa_title: milestone[:title] } - milestone_title = milestone[:expired] ? _("%{milestone_name} (Past due)").html_safe % { milestone_name: milestone[:title] } : milestone[:title]
= link_to milestone_title, milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link', qa_title: milestone[:title] }
- else - else
%span.no-value %span.no-value
= _('None') = _('None')
......
---
title: Show expired milestones at the bottom of the list within dropdown
merge_request: 35595
author:
type: changed
...@@ -54,6 +54,7 @@ Example Response: ...@@ -54,6 +54,7 @@ Example Response:
"state": "active", "state": "active",
"updated_at": "2013-10-02T09:24:18Z", "updated_at": "2013-10-02T09:24:18Z",
"created_at": "2013-10-02T09:24:18Z", "created_at": "2013-10-02T09:24:18Z",
"expired": false,
"web_url": "https://gitlab.com/groups/gitlab-org/-/milestones/42" "web_url": "https://gitlab.com/groups/gitlab-org/-/milestones/42"
} }
] ]
......
...@@ -51,7 +51,8 @@ Example Response: ...@@ -51,7 +51,8 @@ Example Response:
"start_date": "2013-11-10", "start_date": "2013-11-10",
"state": "active", "state": "active",
"updated_at": "2013-10-02T09:24:18Z", "updated_at": "2013-10-02T09:24:18Z",
"created_at": "2013-10-02T09:24:18Z" "created_at": "2013-10-02T09:24:18Z",
"expired": false
} }
] ]
``` ```
......
...@@ -27,10 +27,6 @@ export default { ...@@ -27,10 +27,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
milestonePath: {
type: String,
required: true,
},
labelsPath: { labelsPath: {
type: String, type: String,
required: true, required: true,
...@@ -106,7 +102,8 @@ export default { ...@@ -106,7 +102,8 @@ export default {
<div v-if="!collapseScope || expanded"> <div v-if="!collapseScope || expanded">
<board-milestone-select <board-milestone-select
:board="board" :board="board"
:milestone-path="milestonePath" :group-id="groupId"
:project-id="projectId"
:can-edit="canAdminBoard" :can-edit="canAdminBoard"
/> />
......
...@@ -15,9 +15,15 @@ export default { ...@@ -15,9 +15,15 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
milestonePath: { groupId: {
type: String, type: Number,
required: true, required: false,
default: 0,
},
projectId: {
type: Number,
required: false,
default: 0,
}, },
canEdit: { canEdit: {
type: Boolean, type: Boolean,
...@@ -84,7 +90,8 @@ export default { ...@@ -84,7 +90,8 @@ export default {
<button <button
ref="dropdownButton" ref="dropdownButton"
:data-selected="selected" :data-selected="selected"
:data-milestones="milestonePath" :data-project-id="projectId"
:data-group-id="groupId"
:data-show-no="true" :data-show-no="true"
:data-show-any="true" :data-show-any="true"
:data-show-started="true" :data-show-started="true"
......
{ {
"type": "object", "type": "object",
"properties" : { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
"iid": { "type": "integer" }, "iid": { "type": "integer" },
"project_id": { "type": ["integer", "null"] }, "project_id": { "type": ["integer", "null"] },
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
"updated_at": { "type": "string" }, "updated_at": { "type": "string" },
"start_date": { "type": ["date", "null"] }, "start_date": { "type": ["date", "null"] },
"due_date": { "type": ["date", "null"] }, "due_date": { "type": ["date", "null"] },
"expired": { "type": ["boolean", "null"] },
"web_url": { "type": "string" } "web_url": { "type": "string" }
}, },
"additionalProperties": false "additionalProperties": false
......
...@@ -14,7 +14,6 @@ describe('BoardScope', () => { ...@@ -14,7 +14,6 @@ describe('BoardScope', () => {
labels: [], labels: [],
assignee: {}, assignee: {},
}, },
milestonePath: `${TEST_HOST}/milestones`,
labelsPath: `${TEST_HOST}/labels`, labelsPath: `${TEST_HOST}/labels`,
}; };
......
import Vue from 'vue'; import Vue from 'vue';
import MockAdapater from 'axios-mock-adapter'; import Api from '~/api';
import MilestoneSelect from 'ee/boards/components/milestone_select.vue'; import MilestoneSelect from 'ee/boards/components/milestone_select.vue';
import { boardObj } from 'jest/boards/mock_data'; import { boardObj } from 'jest/boards/mock_data';
import axios from '~/lib/utils/axios_utils';
import IssuableContext from '~/issuable_context'; import IssuableContext from '~/issuable_context';
let vm; let vm;
...@@ -21,12 +20,16 @@ const milestone = { ...@@ -21,12 +20,16 @@ const milestone = {
id: 1, id: 1,
title: 'first milestone', title: 'first milestone',
name: 'first milestone', name: 'first milestone',
due_date: '2015-05-05',
expired: true,
}; };
const milestone2 = { const milestone2 = {
id: 2, id: 2,
title: 'second milestone', title: 'second milestone',
name: 'second milestone', name: 'second milestone',
due_date: null,
expired: false,
}; };
describe('Milestone select component', () => { describe('Milestone select component', () => {
...@@ -40,7 +43,8 @@ describe('Milestone select component', () => { ...@@ -40,7 +43,8 @@ describe('Milestone select component', () => {
vm = new Component({ vm = new Component({
propsData: { propsData: {
board: boardObj, board: boardObj,
milestonePath: '/test/issue-boards/milestones.json', groupId: 2,
projectId: 2,
canEdit: true, canEdit: true,
}, },
}).$mount('.test-container'); }).$mount('.test-container');
...@@ -92,15 +96,8 @@ describe('Milestone select component', () => { ...@@ -92,15 +96,8 @@ describe('Milestone select component', () => {
}); });
describe('clicking dropdown items', () => { describe('clicking dropdown items', () => {
let mock;
beforeEach(() => { beforeEach(() => {
mock = new MockAdapater(axios); jest.spyOn(Api, 'projectMilestones').mockResolvedValue({ data: [milestone, milestone2] });
mock.onGet('/test/issue-boards/milestones.json').reply(200, [milestone, milestone2]);
});
afterEach(() => {
mock.restore();
}); });
it('sets Any milestone', async done => { it('sets Any milestone', async done => {
...@@ -147,9 +144,10 @@ describe('Milestone select component', () => { ...@@ -147,9 +144,10 @@ describe('Milestone select component', () => {
}); });
setImmediate(() => { setImmediate(() => {
expect(activeDropdownItem(0)).toEqual('first milestone'); // "second milestone" is not expired, hence it shows up to the top.
expect(selectedText()).toEqual('first milestone'); expect(activeDropdownItem(0)).toBe('second milestone');
expect(vm.board.milestone).toEqual(milestone); expect(selectedText()).toBe('second milestone');
expect(vm.board.milestone).toEqual(milestone2);
done(); done();
}); });
}); });
......
...@@ -10,6 +10,7 @@ module API ...@@ -10,6 +10,7 @@ module API
expose :state, :created_at, :updated_at expose :state, :created_at, :updated_at
expose :due_date expose :due_date
expose :start_date expose :start_date
expose :expired?, as: :expired
expose :web_url do |milestone, _options| expose :web_url do |milestone, _options|
Gitlab::UrlBuilder.build(milestone) Gitlab::UrlBuilder.build(milestone)
......
...@@ -353,6 +353,9 @@ msgstr "" ...@@ -353,6 +353,9 @@ msgstr ""
msgid "%{description}- Sentry event: %{errorUrl}- First seen: %{firstSeen}- Last seen: %{lastSeen} %{countLabel}: %{count}%{userCountLabel}: %{userCount}" msgid "%{description}- Sentry event: %{errorUrl}- First seen: %{firstSeen}- Last seen: %{lastSeen} %{countLabel}: %{count}%{userCountLabel}: %{userCount}"
msgstr "" msgstr ""
msgid "%{due_date} (Past due)"
msgstr ""
msgid "%{duration}ms" msgid "%{duration}ms"
msgstr "" msgstr ""
...@@ -479,6 +482,12 @@ msgstr "" ...@@ -479,6 +482,12 @@ msgstr ""
msgid "%{mergeLength}/%{usersLength} can merge" msgid "%{mergeLength}/%{usersLength} can merge"
msgstr "" msgstr ""
msgid "%{milestone_name} (Past due)"
msgstr ""
msgid "%{milestone} (expired)"
msgstr ""
msgid "%{mrText}, this issue will be closed automatically." msgid "%{mrText}, this issue will be closed automatically."
msgstr "" msgstr ""
......
...@@ -12,11 +12,13 @@ ...@@ -12,11 +12,13 @@
"updated_at": { "type": "date" }, "updated_at": { "type": "date" },
"start_date": { "type": "date" }, "start_date": { "type": "date" },
"due_date": { "type": "date" }, "due_date": { "type": "date" },
"expired": { "type": ["boolean", "null"] },
"web_url": { "type": "string" } "web_url": { "type": "string" }
}, },
"required": [ "required": [
"id", "iid", "title", "description", "state", "id", "iid", "title", "description", "state",
"state", "created_at", "updated_at", "start_date", "due_date" "state", "created_at", "updated_at", "start_date",
"due_date", "expired"
], ],
"additionalProperties": false "additionalProperties": false
} }
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
"updated_at": { "type": "date" }, "updated_at": { "type": "date" },
"start_date": { "type": "date" }, "start_date": { "type": "date" },
"due_date": { "type": "date" }, "due_date": { "type": "date" },
"expired": { "type": ["boolean", "null"] },
"web_url": { "type": "string" }, "web_url": { "type": "string" },
"issue_stats": { "issue_stats": {
"required": ["total", "closed"], "required": ["total", "closed"],
...@@ -24,7 +25,8 @@ ...@@ -24,7 +25,8 @@
}, },
"required": [ "required": [
"id", "iid", "title", "description", "state", "id", "iid", "title", "description", "state",
"state", "created_at", "updated_at", "start_date", "due_date", "issue_stats" "state", "created_at", "updated_at", "start_date",
"due_date", "expired", "issue_stats"
], ],
"additionalProperties": false "additionalProperties": false
} }
...@@ -96,6 +96,29 @@ describe('Api', () => { ...@@ -96,6 +96,29 @@ describe('Api', () => {
}); });
}); });
describe('groupMilestones', () => {
it('fetches group milestones', done => {
const groupId = 1;
const options = { state: 'active' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/1/milestones`;
mock.onGet(expectedUrl).reply(200, [
{
id: 1,
title: 'milestone1',
state: 'active',
},
]);
Api.groupMilestones(groupId, options)
.then(({ data }) => {
expect(data.length).toBe(1);
expect(data[0].title).toBe('milestone1');
})
.then(done)
.catch(done.fail);
});
});
describe('namespaces', () => { describe('namespaces', () => {
it('fetches namespaces', done => { it('fetches namespaces', done => {
const query = 'dummy query'; const query = 'dummy query';
...@@ -296,6 +319,29 @@ describe('Api', () => { ...@@ -296,6 +319,29 @@ describe('Api', () => {
}); });
}); });
describe('projectMilestones', () => {
it('fetches project milestones', done => {
const projectId = 1;
const options = { state: 'active' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/1/milestones`;
mock.onGet(expectedUrl).reply(200, [
{
id: 1,
title: 'milestone1',
state: 'active',
},
]);
Api.projectMilestones(projectId, options)
.then(({ data }) => {
expect(data.length).toBe(1);
expect(data[0].title).toBe('milestone1');
})
.then(done)
.catch(done.fail);
});
});
describe('newLabel', () => { describe('newLabel', () => {
it('creates a new label', done => { it('creates a new label', done => {
const namespace = 'some namespace'; const namespace = 'some namespace';
......
...@@ -10,7 +10,6 @@ describe('board_form.vue', () => { ...@@ -10,7 +10,6 @@ describe('board_form.vue', () => {
const propsData = { const propsData = {
canAdminBoard: false, canAdminBoard: false,
labelsPath: `${gl.TEST_HOST}/labels/path`, labelsPath: `${gl.TEST_HOST}/labels/path`,
milestonePath: `${gl.TEST_HOST}/milestone/path`,
}; };
const findModal = () => wrapper.find(DeprecatedModal); const findModal = () => wrapper.find(DeprecatedModal);
......
...@@ -81,7 +81,6 @@ describe('BoardsSelector', () => { ...@@ -81,7 +81,6 @@ describe('BoardsSelector', () => {
assignee_id: null, assignee_id: null,
labels: [], labels: [],
}, },
milestonePath: `${TEST_HOST}/milestone/path`,
boardBaseUrl: `${TEST_HOST}/board/base/url`, boardBaseUrl: `${TEST_HOST}/board/base/url`,
hasMissingBoards: false, hasMissingBoards: false,
canAdminBoard: true, canAdminBoard: true,
......
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