Commit 337585de authored by Kushal Pandya's avatar Kushal Pandya

Show expired milestone with different text color

Shows expired milestones within the dropdown with different
text color and "(expired)" string suffixed.
parent 37d86926
...@@ -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,58 +64,101 @@ export default class MilestoneSelect { ...@@ -63,58 +64,101 @@ 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;
const extraOptions = []; let getMilestones = Api.projectMilestones;
if (showAny) {
extraOptions.push({
id: null,
name: null,
title: __('Any milestone'),
});
}
if (showNo) {
extraOptions.push({
id: -1,
name: __('No milestone'),
title: __('No milestone'),
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
title: __('Upcoming'),
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
title: __('Started'),
});
}
if (extraOptions.length) {
extraOptions.push({ type: 'divider' });
}
callback(extraOptions.concat(data)); if (!contextId) {
if (showMenuAbove) { contextId = $dropdown.get(0).dataset.groupId;
$dropdown.data('glDropdown').positionMenuAbove(); getMilestones = Api.groupMilestones;
} }
$(`[data-milestone-id="${escape(selectedMilestone)}"] > a`).addClass('is-active');
}), // We don't use $.data() as it caches initial value and never updates!
renderRow: milestone => ` return getMilestones(contextId, { state: 'active' })
<li data-milestone-id="${escape(milestone.name)}"> .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 = [];
if (showAny) {
extraOptions.push({
id: null,
name: null,
title: __('Any milestone'),
});
}
if (showNo) {
extraOptions.push({
id: -1,
name: __('No milestone'),
title: __('No milestone'),
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
title: __('Upcoming'),
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
title: __('Started'),
});
}
if (extraOptions.length) {
extraOptions.push({ type: 'divider' });
}
callback(extraOptions.concat(data));
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
});
},
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)
......
...@@ -356,6 +356,9 @@ msgstr "" ...@@ -356,6 +356,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 ""
...@@ -482,6 +485,12 @@ msgstr "" ...@@ -482,6 +485,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