Commit 39afe3bc authored by Sean McGivern's avatar Sean McGivern

Merge branch '2518-saved-configuration-for-issue-board' into 'master'

[CE backport] Saved configuration for issue board

See merge request gitlab-org/gitlab-ce!15108
parents 859cdd3d 9bd3d255
Pipeline #13649698 failed with stages
in 62 minutes and 47 seconds
...@@ -11,7 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { ...@@ -11,7 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Issue boards is slightly different, we handle all the requests async // Issue boards is slightly different, we handle all the requests async
// instead or reloading the page, we just re-fire the list ajax requests // instead or reloading the page, we just re-fire the list ajax requests
this.isHandledAsync = true; this.isHandledAsync = true;
this.cantEdit = cantEdit; this.cantEdit = cantEdit.filter(i => typeof i === 'string');
this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object');
} }
updateObject(path) { updateObject(path) {
...@@ -42,7 +43,9 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { ...@@ -42,7 +43,9 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
this.filteredSearchInput.dispatchEvent(new Event('input')); this.filteredSearchInput.dispatchEvent(new Event('input'));
} }
canEdit(tokenName) { canEdit(tokenName, tokenValue) {
return this.cantEdit.indexOf(tokenName) === -1; if (this.cantEdit.includes(tokenName)) return false;
return this.cantEditWithValue.findIndex(token => token.name === tokenName &&
token.value === tokenValue) === -1;
} }
} }
...@@ -14,16 +14,18 @@ gl.issueBoards.BoardsStore = { ...@@ -14,16 +14,18 @@ gl.issueBoards.BoardsStore = {
}, },
state: {}, state: {},
detail: { detail: {
issue: {} issue: {},
}, },
moving: { moving: {
issue: {}, issue: {},
list: {} list: {},
}, },
create () { create () {
this.state.lists = []; this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&'); this.filter.path = getUrlParamsArray().join('&');
this.detail = { issue: {} }; this.detail = {
issue: {},
};
}, },
addList (listObj, defaultAvatar) { addList (listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar); const list = new List(listObj, defaultAvatar);
......
...@@ -147,6 +147,16 @@ class DropdownUtils { ...@@ -147,6 +147,16 @@ class DropdownUtils {
return dataValue !== null; return dataValue !== null;
} }
static getVisualTokenValues(visualToken) {
const tokenName = visualToken && visualToken.querySelector('.name').textContent.trim();
let tokenValue = visualToken && visualToken.querySelector('.value') && visualToken.querySelector('.value').textContent.trim();
if (tokenName === 'label' && tokenValue) {
// remove leading symbol and wrapping quotes
tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, '');
}
return { tokenName, tokenValue };
}
// Determines the full search query (visual tokens + input) // Determines the full search query (visual tokens + input)
static getSearchQuery(untilInput = false) { static getSearchQuery(untilInput = false) {
const container = FilteredSearchContainer.container; const container = FilteredSearchContainer.container;
......
...@@ -185,8 +185,8 @@ class FilteredSearchManager { ...@@ -185,8 +185,8 @@ class FilteredSearchManager {
if (e.keyCode === 8 || e.keyCode === 46) { if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim(); const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName); const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) { if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial(); gl.FilteredSearchVisualTokens.removeLastTokenPartial();
...@@ -336,8 +336,8 @@ class FilteredSearchManager { ...@@ -336,8 +336,8 @@ class FilteredSearchManager {
let canClearToken = t.classList.contains('js-visual-token'); let canClearToken = t.classList.contains('js-visual-token');
if (canClearToken) { if (canClearToken) {
const tokenKey = t.querySelector('.name').textContent.trim(); const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(t);
canClearToken = this.canEdit && this.canEdit(tokenKey); canClearToken = this.canEdit && this.canEdit(tokenName, tokenValue);
} }
if (canClearToken) { if (canClearToken) {
...@@ -469,7 +469,7 @@ class FilteredSearchManager { ...@@ -469,7 +469,7 @@ class FilteredSearchManager {
} }
hasFilteredSearch = true; hasFilteredSearch = true;
const canEdit = this.canEdit && this.canEdit(sanitizedKey); const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue);
gl.FilteredSearchVisualTokens.addFilterVisualToken( gl.FilteredSearchVisualTokens.addFilterVisualToken(
sanitizedKey, sanitizedKey,
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
......
...@@ -38,21 +38,14 @@ class FilteredSearchVisualTokens { ...@@ -38,21 +38,14 @@ class FilteredSearchVisualTokens {
} }
static createVisualTokenElementHTML(canEdit = true) { static createVisualTokenElementHTML(canEdit = true) {
let removeTokenMarkup = '';
if (canEdit) {
removeTokenMarkup = `
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
`;
}
return ` return `
<div class="selectable" role="button"> <div class="${canEdit ? 'selectable' : 'hidden'}" role="button">
<div class="name"></div> <div class="name"></div>
<div class="value-container"> <div class="value-container">
<div class="value"></div> <div class="value"></div>
${removeTokenMarkup} <div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
</div> </div>
</div> </div>
`; `;
......
...@@ -7,7 +7,7 @@ import DropdownUtils from './filtered_search/dropdown_utils'; ...@@ -7,7 +7,7 @@ import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label'; import CreateLabelDropdown from './create_label';
export default class LabelsSelect { export default class LabelsSelect {
constructor(els) { constructor(els, options = {}) {
var _this, $els; var _this, $els;
_this = this; _this = this;
...@@ -57,6 +57,7 @@ export default class LabelsSelect { ...@@ -57,6 +57,7 @@ export default class LabelsSelect {
labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>'); labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
labelNoneHTMLTemplate = '<span class="no-value">None</span>'; labelNoneHTMLTemplate = '<span class="no-value">None</span>';
} }
const handleClick = options.handleClick;
$sidebarLabelTooltip.tooltip(); $sidebarLabelTooltip.tooltip();
...@@ -390,6 +391,10 @@ export default class LabelsSelect { ...@@ -390,6 +391,10 @@ export default class LabelsSelect {
.then(fadeOutLoader) .then(fadeOutLoader)
.catch(fadeOutLoader); .catch(fadeOutLoader);
} }
else if (handleClick) {
e.preventDefault();
handleClick(label);
}
else { else {
if ($dropdown.hasClass('js-multiselect')) { if ($dropdown.hasClass('js-multiselect')) {
......
...@@ -5,7 +5,7 @@ import _ from 'underscore'; ...@@ -5,7 +5,7 @@ import _ from 'underscore';
(function() { (function() {
this.MilestoneSelect = (function() { this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject, els) { function MilestoneSelect(currentProject, els, options = {}) {
var _this, $els; var _this, $els;
if (currentProject != null) { if (currentProject != null) {
_this = this; _this = this;
...@@ -136,19 +136,26 @@ import _ from 'underscore'; ...@@ -136,19 +136,26 @@ import _ from 'underscore';
}, },
opened: function(e) { opened: function(e) {
const $el = $(e.currentTarget); const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar')) { if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
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="${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: function(options) { clicked: function(clickEvent) {
const { $el, e } = options; const { $el, e } = clickEvent;
let selected = options.selectedObj; let selected = clickEvent.selectedObj;
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore; var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
if (!selected) return; if (!selected) return;
if (options.handleClick) {
e.preventDefault();
options.handleClick(selected);
return;
}
page = $('body').attr('data-page'); page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
......
...@@ -6,7 +6,7 @@ import _ from 'underscore'; ...@@ -6,7 +6,7 @@ import _ from 'underscore';
// TODO: remove eventHub hack after code splitting refactor // TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop; window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
function UsersSelect(currentUser, els) { function UsersSelect(currentUser, els, options = {}) {
var $els; var $els;
this.users = this.users.bind(this); this.users = this.users.bind(this);
this.user = this.user.bind(this); this.user = this.user.bind(this);
...@@ -20,6 +20,8 @@ function UsersSelect(currentUser, els) { ...@@ -20,6 +20,8 @@ function UsersSelect(currentUser, els) {
} }
} }
const { handleClick } = options;
$els = $(els); $els = $(els);
if (!els) { if (!els) {
...@@ -442,6 +444,9 @@ function UsersSelect(currentUser, els) { ...@@ -442,6 +444,9 @@ function UsersSelect(currentUser, els) {
} }
if ($el.closest('.add-issues-modal').length) { if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
} else if (handleClick) {
e.preventDefault();
handleClick(user, isMarking);
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form')); return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) { } else if ($dropdown.hasClass('js-filter-submit')) {
......
...@@ -5,17 +5,27 @@ export default { ...@@ -5,17 +5,27 @@ export default {
props: { props: {
title: { title: {
type: String, type: String,
required: true, required: false,
}, },
text: { text: {
type: String, type: String,
required: false, required: false,
}, },
hideFooter: {
type: Boolean,
required: false,
default: false,
},
kind: { kind: {
type: String, type: String,
required: false, required: false,
default: 'primary', default: 'primary',
}, },
modalDialogClass: {
type: String,
required: false,
default: '',
},
closeKind: { closeKind: {
type: String, type: String,
required: false, required: false,
...@@ -30,6 +40,11 @@ export default { ...@@ -30,6 +40,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
submitDisabled: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
...@@ -57,43 +72,58 @@ export default { ...@@ -57,43 +72,58 @@ export default {
</script> </script>
<template> <template>
<div <div class="modal-open">
class="modal popup-dialog" <div
role="dialog" class="modal popup-dialog"
tabindex="-1"> role="dialog"
<div class="modal-dialog" role="document"> tabindex="-1"
<div class="modal-content"> >
<div class="modal-header"> <div
<button type="button" :class="modalDialogClass"
class="close" class="modal-dialog"
@click="close" role="document"
aria-label="Close"> >
<span aria-hidden="true">&times;</span> <div class="modal-content">
</button> <div class="modal-header">
<h4 class="modal-title">{{this.title}}</h4> <slot name="header">
</div> <h4 class="modal-title pull-left">
<div class="modal-body"> {{this.title}}
<slot name="body" :text="text"> </h4>
<p>{{text}}</p> <button
</slot> type="button"
</div> class="close pull-right"
<div class="modal-footer"> @click="close"
<button aria-label="Close"
type="button" >
class="btn" <span aria-hidden="true">&times;</span>
:class="btnCancelKindClass" </button>
@click="close"> </slot>
{{ closeButtonLabel }} </div>
</button> <div class="modal-body">
<button <slot name="body" :text="text">
type="button" <p>{{this.text}}</p>
class="btn" </slot>
:class="btnKindClass" </div>
@click="emitSubmit(true)"> <div class="modal-footer" v-if="!hideFooter">
{{ primaryButtonLabel }} <button
</button> type="button"
class="btn pull-left"
:class="btnCancelKindClass"
@click="close">
{{ closeButtonLabel }}
</button>
<button
type="button"
class="btn pull-right"
:disabled="submitDisabled"
:class="btnKindClass"
@click="emitSubmit(true)">
{{ primaryButtonLabel }}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal-backdrop fade in" />
</div> </div>
</template> </template>
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
.cred { color: $common-red; } .cred { color: $common-red; }
.cgreen { color: $common-green; } .cgreen { color: $common-green; }
.cdark { color: $common-gray-dark; } .cdark { color: $common-gray-dark; }
.text-secondary {
color: $gl-text-color-secondary;
}
.underlined-link { text-decoration: underline; } .underlined-link { text-decoration: underline; }
.hint { font-style: italic; color: $hint-color; } .hint { font-style: italic; color: $hint-color; }
......
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
.dropdown-menu-nav { .dropdown-menu-nav {
@include set-visible; @include set-visible;
display: block; display: block;
min-height: 40px;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
width: 100%; width: 100%;
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
} }
.modal-body { .modal-body {
background-color: $modal-body-bg;
position: relative; position: relative;
padding: #{3 * $grid-size} #{2 * $grid-size}; padding: #{3 * $grid-size} #{2 * $grid-size};
...@@ -42,3 +43,8 @@ body.modal-open { ...@@ -42,3 +43,8 @@ body.modal-open {
width: 98%; width: 98%;
} }
} }
.modal.popup-dialog {
display: block;
}
...@@ -164,3 +164,36 @@ $pre-border-color: $border-color; ...@@ -164,3 +164,36 @@ $pre-border-color: $border-color;
$table-bg-accent: $gray-light; $table-bg-accent: $gray-light;
$zindex-popover: 900; $zindex-popover: 900;
//== Modals
//
//##
//** Padding applied to the modal body
$modal-inner-padding: $gl-padding;
//** Padding applied to the modal title
$modal-title-padding: $gl-padding;
//** Modal title line-height
// $modal-title-line-height: $line-height-base
//** Background color of modal content area
$modal-content-bg: $gray-light;
$modal-body-bg: $white-light;
//** Modal content border color
// $modal-content-border-color: rgba(0,0,0,.2)
//** Modal content border color **for IE8**
// $modal-content-fallback-border-color: #999
//** Modal backdrop background color
// $modal-backdrop-bg: #000
//** Modal backdrop opacity
// $modal-backdrop-opacity: .5
//** Modal header border color
// $modal-header-border-color: #e5e5e5
//** Modal footer border color
// $modal-footer-border-color: $modal-header-border-color
// $modal-lg: 900px
// $modal-md: 600px
// $modal-sm: 300px
...@@ -20,17 +20,6 @@ module BoardsHelper ...@@ -20,17 +20,6 @@ module BoardsHelper
project_issues_path(@project) project_issues_path(@project)
end end
def current_board_json
board = @board || @boards.first
board.to_json(
only: [:id, :name, :milestone_id],
include: {
milestone: { only: [:title] }
}
)
end
def board_base_url def board_base_url
project_boards_path(@project) project_boards_path(@project)
end end
......
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