Skip to content
Snippets Groups Projects
Commit 45963fbf authored by Jacob Schatz's avatar Jacob Schatz
Browse files

Merge branch 'mia_backort' into 'master'

Backport of Multiple Assignees feature

See merge request !11089

Former-commit-id: aa874a1c
parents 9b6b0b14 03ce04f3
No related branches found
No related tags found
No related merge requests found
Showing
with 255 additions and 172 deletions
Loading
Loading
@@ -99,7 +99,7 @@ export default class FileTemplateMediator {
});
}
 
selectTemplateType(item, el, e) {
selectTemplateType(item, e) {
if (e) {
e.preventDefault();
}
Loading
Loading
@@ -117,6 +117,10 @@ export default class FileTemplateMediator {
this.cacheToggleText();
}
 
selectTemplateTypeOptions(options) {
this.selectTemplateType(options.selectedObj, options.e);
}
selectTemplateFile(selector, query, data) {
selector.renderLoading();
// in case undo menu is already already there
Loading
Loading
Loading
Loading
@@ -52,9 +52,17 @@ export default class FileTemplateSelector {
.removeClass('fa-spinner fa-spin');
}
 
reportSelection(query, el, e, data) {
reportSelection(options) {
const { query, e, data } = options;
e.preventDefault();
return this.mediator.selectTemplateFile(this, query, data);
}
reportSelectionName(options) {
const opts = options;
opts.query = options.selectedObj.name;
this.reportSelection(opts);
}
}
 
Loading
Loading
@@ -37,8 +37,8 @@ class TargetBranchDropDown {
}
return SELECT_ITEM_MSG;
},
clicked(item, el, e) {
e.preventDefault();
clicked(options) {
options.e.preventDefault();
self.onClick.call(self);
},
fieldName: self.fieldName,
Loading
Loading
Loading
Loading
@@ -24,7 +24,7 @@ export default class TemplateSelector {
search: {
fields: ['name'],
},
clicked: (item, el, e) => this.fetchFileTemplate(item, el, e),
clicked: options => this.fetchFileTemplate(options),
text: item => item.name,
});
}
Loading
Loading
@@ -51,7 +51,10 @@ export default class TemplateSelector {
return this.$dropdownContainer.removeClass('hidden');
}
 
fetchFileTemplate(item, el, e) {
fetchFileTemplate(options) {
const { e } = options;
const item = options.selectedObj;
e.preventDefault();
return this.requestFile(item);
}
Loading
Loading
Loading
Loading
@@ -25,7 +25,7 @@ export default class BlobCiYamlSelector extends FileTemplateSelector {
search: {
fields: ['name'],
},
clicked: (query, el, e) => this.reportSelection(query.name, el, e),
clicked: options => this.reportSelectionName(options),
text: item => item.name,
});
}
Loading
Loading
Loading
Loading
@@ -25,7 +25,7 @@ export default class DockerfileSelector extends FileTemplateSelector {
search: {
fields: ['name'],
},
clicked: (query, el, e) => this.reportSelection(query.name, el, e),
clicked: options => this.reportSelectionName(options),
text: item => item.name,
});
}
Loading
Loading
Loading
Loading
@@ -24,7 +24,7 @@ export default class BlobGitignoreSelector extends FileTemplateSelector {
search: {
fields: ['name'],
},
clicked: (query, el, e) => this.reportSelection(query.name, el, e),
clicked: options => this.reportSelectionName(options),
text: item => item.name,
});
}
Loading
Loading
Loading
Loading
@@ -24,13 +24,22 @@ export default class BlobLicenseSelector extends FileTemplateSelector {
search: {
fields: ['name'],
},
clicked: (query, el, e) => {
clicked: (options) => {
const { e } = options;
const el = options.$el;
const query = options.selectedObj;
const data = {
project: this.$dropdown.data('project'),
fullname: this.$dropdown.data('fullname'),
};
 
this.reportSelection(query.id, el, e, data);
this.reportSelection({
query: query.id,
el,
e,
data,
});
},
text: item => item.name,
});
Loading
Loading
Loading
Loading
@@ -17,7 +17,7 @@ export default class FileTemplateTypeSelector extends FileTemplateSelector {
filterable: false,
selectable: true,
toggleLabel: item => item.name,
clicked: (item, el, e) => this.mediator.selectTemplateType(item, el, e),
clicked: options => this.mediator.selectTemplateTypeOptions(options),
text: item => item.name,
});
}
Loading
Loading
Loading
Loading
@@ -11,7 +11,7 @@ require('./models/issue');
require('./models/label');
require('./models/list');
require('./models/milestone');
require('./models/user');
require('./models/assignee');
require('./stores/boards_store');
require('./stores/modal_store');
require('./services/board_service');
Loading
Loading
Loading
Loading
@@ -26,6 +26,7 @@ export default {
title: this.title,
labels,
subscribed: true,
assignees: [],
});
 
this.list.newIssue(issue)
Loading
Loading
Loading
Loading
@@ -3,8 +3,13 @@
/* global MilestoneSelect */
/* global LabelsSelect */
/* global Sidebar */
/* global Flash */
 
import Vue from 'vue';
import eventHub from '../../sidebar/event_hub';
import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
import Assignees from '../../sidebar/components/assignees/assignees';
 
require('./sidebar/remove_issue');
 
Loading
Loading
@@ -22,6 +27,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
detail: Store.detail,
issue: {},
list: {},
loadingAssignees: false,
};
},
computed: {
Loading
Loading
@@ -43,6 +49,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({
 
this.issue = this.detail.issue;
this.list = this.detail.list;
this.$nextTick(() => {
this.endpoint = this.$refs.assigneeDropdown.dataset.issueUpdate;
});
},
deep: true
},
Loading
Loading
@@ -53,12 +63,57 @@ gl.issueBoards.BoardSidebar = Vue.extend({
$('.right-sidebar').getNiceScroll().resize();
});
}
}
this.issue = this.detail.issue;
this.list = this.detail.list;
},
deep: true
},
methods: {
closeSidebar () {
this.detail.issue = {};
}
},
assignSelf () {
// Notify gl dropdown that we are now assigning to current user
this.$refs.assigneeBlock.dispatchEvent(new Event('assignYourself'));
this.addAssignee(this.currentUser);
this.saveAssignees();
},
removeAssignee (a) {
gl.issueBoards.BoardsStore.detail.issue.removeAssignee(a);
},
addAssignee (a) {
gl.issueBoards.BoardsStore.detail.issue.addAssignee(a);
},
removeAllAssignees () {
gl.issueBoards.BoardsStore.detail.issue.removeAllAssignees();
},
saveAssignees () {
this.loadingAssignees = true;
gl.issueBoards.BoardsStore.detail.issue.update(this.endpoint)
.then(() => {
this.loadingAssignees = false;
})
.catch(() => {
this.loadingAssignees = false;
return new Flash('An error occurred while saving assignees');
});
},
},
created () {
// Get events from glDropdown
eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
eventHub.$on('sidebar.addAssignee', this.addAssignee);
eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$on('sidebar.saveAssignees', this.saveAssignees);
},
beforeDestroy() {
eventHub.$off('sidebar.removeAssignee', this.removeAssignee);
eventHub.$off('sidebar.addAssignee', this.addAssignee);
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
},
mounted () {
new IssuableContext(this.currentUser);
Loading
Loading
@@ -70,5 +125,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
},
components: {
removeBtn: gl.issueBoards.RemoveIssueBtn,
'assignee-title': AssigneeTitle,
assignees: Assignees,
},
});
Loading
Loading
@@ -31,18 +31,36 @@ gl.issueBoards.IssueCardInner = Vue.extend({
default: false,
},
},
data() {
return {
limitBeforeCounter: 3,
maxRender: 4,
maxCounter: 99,
};
},
computed: {
cardUrl() {
return `${this.issueLinkBase}/${this.issue.id}`;
numberOverLimit() {
return this.issue.assignees.length - this.limitBeforeCounter;
},
assigneeUrl() {
return `${this.rootPath}${this.issue.assignee.username}`;
assigneeCounterTooltip() {
return `${this.assigneeCounterLabel} more`;
},
assigneeCounterLabel() {
if (this.numberOverLimit > this.maxCounter) {
return `${this.maxCounter}+`;
}
return `+${this.numberOverLimit}`;
},
assigneeUrlTitle() {
return `Assigned to ${this.issue.assignee.name}`;
shouldRenderCounter() {
if (this.issue.assignees.length <= this.maxRender) {
return false;
}
return this.issue.assignees.length > this.numberOverLimit;
},
avatarUrlTitle() {
return `Avatar for ${this.issue.assignee.name}`;
cardUrl() {
return `${this.issueLinkBase}/${this.issue.id}`;
},
issueId() {
return `#${this.issue.id}`;
Loading
Loading
@@ -52,6 +70,28 @@ gl.issueBoards.IssueCardInner = Vue.extend({
},
},
methods: {
isIndexLessThanlimit(index) {
return index < this.limitBeforeCounter;
},
shouldRenderAssignee(index) {
// Eg. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
// Otherwise render up to the limitBeforeCounter
if (this.issue.assignees.length <= this.maxRender) {
return index < this.maxRender;
}
return index < this.limitBeforeCounter;
},
assigneeUrl(assignee) {
return `${this.rootPath}${assignee.username}`;
},
assigneeUrlTitle(assignee) {
return `Assigned to ${assignee.name}`;
},
avatarUrlTitle(assignee) {
return `Avatar for ${assignee.name}`;
},
showLabel(label) {
if (!this.list) return true;
 
Loading
Loading
@@ -105,25 +145,39 @@ gl.issueBoards.IssueCardInner = Vue.extend({
{{ issueId }}
</span>
</h4>
<a
class="card-assignee has-tooltip js-no-trigger"
:href="assigneeUrl"
:title="assigneeUrlTitle"
v-if="issue.assignee"
data-container="body"
>
<img
class="avatar avatar-inline s20 js-no-trigger"
:src="issue.assignee.avatar"
width="20"
height="20"
:alt="avatarUrlTitle"
/>
</a>
<div class="card-assignee">
<a
class="has-tooltip js-no-trigger"
:href="assigneeUrl(assignee)"
:title="assigneeUrlTitle(assignee)"
v-for="(assignee, index) in issue.assignees"
v-if="shouldRenderAssignee(index)"
data-container="body"
data-placement="bottom"
>
<img
class="avatar avatar-inline s20"
:src="assignee.avatar"
width="20"
height="20"
:alt="avatarUrlTitle(assignee)"
/>
</a>
<span
class="avatar-counter has-tooltip"
:title="assigneeCounterTooltip"
v-if="shouldRenderCounter"
>
{{ assigneeCounterLabel }}
</span>
</div>
</div>
<div class="card-footer" v-if="showLabelFooter">
<div
class="card-footer"
v-if="showLabelFooter"
>
<button
class="label color-label has-tooltip js-no-trigger"
class="label color-label has-tooltip"
v-for="label in issue.labels"
type="button"
v-if="showLabel(label)"
Loading
Loading
Loading
Loading
@@ -52,7 +52,9 @@ gl.issueBoards.newListDropdownInit = () => {
filterable: true,
selectable: true,
multiSelect: true,
clicked (label, $el, e) {
clicked (options) {
const { e } = options;
const label = options.selectedObj;
e.preventDefault();
 
if (!Store.findList('title', label.title)) {
Loading
Loading
class ListUser {
/* eslint-disable no-unused-vars */
class ListAssignee {
constructor(user, defaultAvatar) {
this.id = user.id;
this.name = user.name;
Loading
Loading
@@ -7,4 +9,4 @@ class ListUser {
}
}
 
window.ListUser = ListUser;
window.ListAssignee = ListAssignee;
/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */
/* global ListLabel */
/* global ListMilestone */
/* global ListUser */
/* global ListAssignee */
 
import Vue from 'vue';
 
Loading
Loading
@@ -14,14 +14,10 @@ class ListIssue {
this.dueDate = obj.due_date;
this.subscribed = obj.subscribed;
this.labels = [];
this.assignees = [];
this.selected = false;
this.assignee = false;
this.position = obj.relative_position || Infinity;
 
if (obj.assignee) {
this.assignee = new ListUser(obj.assignee, defaultAvatar);
}
if (obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
}
Loading
Loading
@@ -29,6 +25,8 @@ class ListIssue {
obj.labels.forEach((label) => {
this.labels.push(new ListLabel(label));
});
this.assignees = obj.assignees.map(a => new ListAssignee(a, defaultAvatar));
}
 
addLabel (label) {
Loading
Loading
@@ -51,6 +49,26 @@ class ListIssue {
labels.forEach(this.removeLabel.bind(this));
}
 
addAssignee (assignee) {
if (!this.findAssignee(assignee)) {
this.assignees.push(new ListAssignee(assignee));
}
}
findAssignee (findAssignee) {
return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0];
}
removeAssignee (removeAssignee) {
if (removeAssignee) {
this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id);
}
}
removeAllAssignees () {
this.assignees = [];
}
getLists () {
return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id));
}
Loading
Loading
@@ -60,7 +78,7 @@ class ListIssue {
issue: {
milestone_id: this.milestone ? this.milestone.id : null,
due_date: this.dueDate,
assignee_id: this.assignee ? this.assignee.id : null,
assignee_ids: this.assignees.length > 0 ? this.assignees.map((u) => u.id) : [0],
label_ids: this.labels.map((label) => label.id)
}
};
Loading
Loading
Loading
Loading
@@ -255,7 +255,8 @@ GitLabDropdown = (function() {
}
};
// Remote data
})(this)
})(this),
instance: this,
});
}
}
Loading
Loading
@@ -269,6 +270,7 @@ GitLabDropdown = (function() {
remote: this.options.filterRemote,
query: this.options.data,
keys: searchFields,
instance: this,
elements: (function(_this) {
return function() {
selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
Loading
Loading
@@ -343,21 +345,26 @@ GitLabDropdown = (function() {
}
this.dropdown.on("click", selector, function(e) {
var $el, selected, selectedObj, isMarking;
$el = $(this);
$el = $(e.currentTarget);
selected = self.rowClicked($el);
selectedObj = selected ? selected[0] : null;
isMarking = selected ? selected[1] : null;
if (self.options.clicked) {
self.options.clicked(selectedObj, $el, e, isMarking);
if (this.options.clicked) {
this.options.clicked.call(this, {
selectedObj,
$el,
e,
isMarking,
});
}
 
// Update label right after all modifications in dropdown has been done
if (self.options.toggleLabel) {
self.updateLabel(selectedObj, $el, self);
if (this.options.toggleLabel) {
this.updateLabel(selectedObj, $el, this);
}
 
$el.trigger('blur');
});
}.bind(this));
}
}
 
Loading
Loading
@@ -439,15 +446,34 @@ GitLabDropdown = (function() {
}
};
 
GitLabDropdown.prototype.filteredFullData = function() {
return this.fullData.filter(r => typeof r === 'object'
&& !Object.prototype.hasOwnProperty.call(r, 'beforeDivider')
&& !Object.prototype.hasOwnProperty.call(r, 'header')
);
};
GitLabDropdown.prototype.opened = function(e) {
var contentHtml;
this.resetRows();
this.addArrowKeyEvent();
 
const dropdownToggle = this.dropdown.find('.dropdown-menu-toggle');
const hasFilterBulkUpdate = dropdownToggle.hasClass('js-filter-bulk-update');
const hasMultiSelect = dropdownToggle.hasClass('js-multiselect');
// Makes indeterminate items effective
if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
if (this.fullData && hasFilterBulkUpdate) {
this.parseData(this.fullData);
}
// Process the data to make sure rendered data
// matches the correct layout
if (this.fullData && hasMultiSelect && this.options.processData) {
const inputValue = this.filterInput.val();
this.options.processData.call(this.options, inputValue, this.filteredFullData(), this.parseData.bind(this));
}
contentHtml = $('.dropdown-content', this.dropdown).html();
if (this.remote && contentHtml === "") {
this.remote.execute();
Loading
Loading
@@ -709,6 +735,11 @@ GitLabDropdown = (function() {
if (this.options.inputId != null) {
$input.attr('id', this.options.inputId);
}
if (this.options.inputMeta) {
$input.attr('data-meta', selectedObject[this.options.inputMeta]);
}
return this.dropdown.before($input);
};
 
Loading
Loading
@@ -829,7 +860,14 @@ GitLabDropdown = (function() {
if (instance == null) {
instance = null;
}
return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance));
let toggleText = this.options.toggleLabel(selected, el, instance);
if (this.options.updateLabel) {
// Option to override the dropdown label text
toggleText = this.options.updateLabel;
}
return $(this.el).find(".dropdown-toggle-text").text(toggleText);
};
 
GitLabDropdown.prototype.clearField = function(field, isInput) {
Loading
Loading
require('./time_tracking/time_tracking_bundle');
import Vue from 'vue';
import stopwatchSvg from 'icons/_icon_stopwatch.svg';
require('../../../lib/utils/pretty_time');
(() => {
Vue.component('time-tracking-collapsed-state', {
name: 'time-tracking-collapsed-state',
props: [
'showComparisonState',
'showSpentOnlyState',
'showEstimateOnlyState',
'showNoTimeTrackingState',
'timeSpentHumanReadable',
'timeEstimateHumanReadable',
],
methods: {
abbreviateTime(timeStr) {
return gl.utils.prettyTime.abbreviateTime(timeStr);
},
},
template: `
<div class='sidebar-collapsed-icon'>
${stopwatchSvg}
<div class='time-tracking-collapsed-summary'>
<div class='compare' v-if='showComparisonState'>
<span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
</div>
<div class='estimate-only' v-if='showEstimateOnlyState'>
<span class='bold'>-- / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
</div>
<div class='spend-only' v-if='showSpentOnlyState'>
<span class='bold'>{{ abbreviateTime(timeSpentHumanReadable) }} / --</span>
</div>
<div class='no-tracking' v-if='showNoTimeTrackingState'>
<span class='no-value'>None</span>
</div>
</div>
</div>
`,
});
})();
import Vue from 'vue';
require('../../../lib/utils/pretty_time');
(() => {
const prettyTime = gl.utils.prettyTime;
Vue.component('time-tracking-comparison-pane', {
name: 'time-tracking-comparison-pane',
props: [
'timeSpent',
'timeEstimate',
'timeSpentHumanReadable',
'timeEstimateHumanReadable',
],
computed: {
parsedRemaining() {
const diffSeconds = this.timeEstimate - this.timeSpent;
return prettyTime.parseSeconds(diffSeconds);
},
timeRemainingHumanReadable() {
return prettyTime.stringifyTime(this.parsedRemaining);
},
timeRemainingTooltip() {
const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:';
return `${prefix} ${this.timeRemainingHumanReadable}`;
},
/* Diff values for comparison meter */
timeRemainingMinutes() {
return this.timeEstimate - this.timeSpent;
},
timeRemainingPercent() {
return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`;
},
timeRemainingStatusClass() {
return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
},
/* Parsed time values */
parsedEstimate() {
return prettyTime.parseSeconds(this.timeEstimate);
},
parsedSpent() {
return prettyTime.parseSeconds(this.timeSpent);
},
},
template: `
<div class='time-tracking-comparison-pane'>
<div class='compare-meter' data-toggle='tooltip' data-placement='top' role='timeRemainingDisplay'
:aria-valuenow='timeRemainingTooltip'
:title='timeRemainingTooltip'
:data-original-title='timeRemainingTooltip'
:class='timeRemainingStatusClass'>
<div class='meter-container' role='timeSpentPercent' :aria-valuenow='timeRemainingPercent'>
<div :style='{ width: timeRemainingPercent }' class='meter-fill'></div>
</div>
<div class='compare-display-container'>
<div class='compare-display pull-left'>
<span class='compare-label'>Spent</span>
<span class='compare-value spent'>{{ timeSpentHumanReadable }}</span>
</div>
<div class='compare-display estimated pull-right'>
<span class='compare-label'>Est</span>
<span class='compare-value'>{{ timeEstimateHumanReadable }}</span>
</div>
</div>
</div>
</div>
`,
});
})();
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment