Commit f9bb919f authored by Kamil Trzcinski's avatar Kamil Trzcinski

Merge remote-tracking branch 'origin-ee/master' into zj-object-store-artifacts

parents 559a85ec 63305cfe
......@@ -3,7 +3,7 @@
/* global ActiveTabMemoizer */
/* global ShortcutsNavigation */
/* global Build */
/* global Issuable */
/* global IssuableIndex */
/* global ShortcutsIssuable */
/* global ZenMode */
/* global Milestone */
......@@ -134,10 +134,9 @@ import AuditLogs from './audit_logs';
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
Issuable.init();
new gl.IssuableBulkActions({
prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
});
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
IssuableIndex.init(pagePrefix);
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
break;
......
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
/* global IssuableIndex */
/* global Flash */
export default {
init({ container, form, issues, prefixId } = {}) {
this.prefixId = prefixId || 'issue_';
this.form = form || this.getElement('.bulk-update');
this.$labelDropdown = this.form.find('.js-label-select');
this.issues = issues || this.getElement('.issues-list .issue');
this.willUpdateLabels = false;
this.bindEvents();
},
bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
},
onFormSubmit(e) {
e.preventDefault();
return this.submit();
},
submit() {
const _this = this;
const xhr = $.ajax({
url: this.form.attr('action'),
method: this.form.attr('method'),
dataType: 'JSON',
data: this.getFormDataAsObject()
});
xhr.done(() => window.location.reload());
xhr.fail(() => this.onFormSubmitFailure());
},
onFormSubmitFailure() {
this.form.find('[type="submit"]').enable();
return new Flash("Issue update failed");
},
getSelectedIssues() {
return this.issues.has('.selected_issue:checked');
},
getLabelsFromSelection() {
const labels = [];
this.getSelectedIssues().map(function() {
const labelsData = $(this).data('labels');
if (labelsData) {
return labelsData.map(function(labelId) {
if (labels.indexOf(labelId) === -1) {
return labels.push(labelId);
}
});
}
});
return labels;
},
/**
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
*/
getUnmarkedIndeterminedLabels() {
const result = [];
const labelsToKeep = this.$labelDropdown.data('indeterminate');
this.getLabelsFromSelection().forEach((id) => {
if (labelsToKeep.indexOf(id) === -1) {
result.push(id);
}
});
return result;
},
/**
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
*/
getFormDataAsObject() {
const formData = {
update: {
state_event: this.form.find('input[name="update[state_event]"]').val(),
// For Merge Requests
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
// For Issues
assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
add_label_ids: [],
remove_label_ids: []
}
};
if (this.willUpdateLabels) {
formData.update.add_label_ids = this.$labelDropdown.data('marked');
formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
}
return formData;
},
setOriginalDropdownData() {
const $labelSelect = $('.bulk-update .js-label-select');
$labelSelect.data('common', this.getOriginalCommonIds());
$labelSelect.data('marked', this.getOriginalMarkedIds());
$labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
},
// From issuable's initial bulk selection
getOriginalCommonIds() {
const labelIds = [];
this.getElement('.selected_issue:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
return _.intersection.apply(this, labelIds);
},
// From issuable's initial bulk selection
getOriginalMarkedIds() {
const labelIds = [];
this.getElement('.selected_issue:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
return _.intersection.apply(this, labelIds);
},
// From issuable's initial bulk selection
getOriginalIndeterminateIds() {
const uniqueIds = [];
const labelIds = [];
let issuableLabels = [];
// Collect unique label IDs for all checked issues
this.getElement('.selected_issue:checked').each((i, el) => {
issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
issuableLabels.forEach((labelId) => {
// Store unique IDs
if (uniqueIds.indexOf(labelId) === -1) {
uniqueIds.push(labelId);
}
});
// Store array of IDs per issuable
labelIds.push(issuableLabels);
});
// Add uniqueIds to add it as argument for _.intersection
labelIds.unshift(uniqueIds);
// Return IDs that are present but not in all selected issueables
return _.difference(uniqueIds, _.intersection.apply(this, labelIds));
},
getElement(selector) {
this.scopeEl = this.scopeEl || $('.content');
return this.scopeEl.find(selector);
},
};
/* eslint-disable class-methods-use-this, no-new */
/* global LabelsSelect */
/* global MilestoneSelect */
/* global IssueStatusSelect */
/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
const SIDEBAR_EXPANDED_CLASS = 'right-sidebar-expanded issuable-bulk-update-sidebar';
const SIDEBAR_COLLAPSED_CLASS = 'right-sidebar-collapsed issuable-bulk-update-sidebar';
export default class IssuableBulkUpdateSidebar {
constructor() {
this.initDomElements();
this.bindEvents();
this.initDropdowns();
this.setupBulkUpdateActions();
}
initDomElements() {
this.$page = $('.page-with-sidebar');
this.$sidebar = $('.right-sidebar');
this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide');
this.$bulkEditSubmitBtn = $('.update-selected-issues');
this.$bulkUpdateEnableBtn = $('.js-bulk-update-toggle');
this.$otherFilters = $('.issues-other-filters');
this.$checkAllContainer = $('.check-all-holder');
this.$issueChecks = $('.issue-check');
this.$issuesList = $('.selected_issue');
this.$issuableIdsInput = $('#update_issuable_ids');
}
bindEvents() {
this.$bulkUpdateEnableBtn.on('click', e => this.toggleBulkEdit(e, true));
this.$bulkEditCancelBtn.on('click', e => this.toggleBulkEdit(e, false));
this.$checkAllContainer.on('click', e => this.selectAll(e));
this.$issuesList.on('change', () => this.updateFormState());
this.$bulkEditSubmitBtn.on('click', () => this.prepForSubmit());
this.$checkAllContainer.on('click', () => this.updateFormState());
}
initDropdowns() {
new LabelsSelect();
new MilestoneSelect();
new IssueStatusSelect();
new SubscriptionSelect();
}
getNavHeight() {
const navbarHeight = $('.navbar-gitlab').outerHeight();
const layoutNavHeight = $('.layout-nav').outerHeight();
const subNavScroll = $('.sub-nav-scroll').outerHeight();
return navbarHeight + layoutNavHeight + subNavScroll;
}
initSidebar() {
if (!this.navHeight) {
this.navHeight = this.getNavHeight();
}
if (!this.sidebarInitialized) {
$(document).off('scroll').on('scroll', _.throttle(this.setSidebarHeight, 10).bind(this));
$(window).off('resize').on('resize', _.throttle(this.setSidebarHeight, 10).bind(this));
this.sidebarInitialized = true;
}
}
setupBulkUpdateActions() {
IssuableBulkUpdateActions.setOriginalDropdownData();
}
updateFormState() {
const noCheckedIssues = !$('.selected_issue:checked').length;
this.toggleSubmitButtonDisabled(noCheckedIssues);
this.updateSelectedIssuableIds();
IssuableBulkUpdateActions.setOriginalDropdownData();
}
prepForSubmit() {
// if submit button is disabled, submission is blocked. This ensures we disable after
// form submission is carried out
setTimeout(() => this.$bulkEditSubmitBtn.disable());
this.updateSelectedIssuableIds();
}
toggleBulkEdit(e, enable) {
e.preventDefault();
this.toggleSidebarDisplay(enable);
this.toggleBulkEditButtonDisabled(enable);
this.toggleOtherFiltersDisabled(enable);
this.toggleCheckboxDisplay(enable);
if (enable) {
this.initSidebar();
}
}
updateSelectedIssuableIds() {
this.$issuableIdsInput.val(IssuableBulkUpdateSidebar.getCheckedIssueIds());
}
selectAll() {
const checkAllButtonState = this.$checkAllContainer.find('input').prop('checked');
this.$issuesList.prop('checked', checkAllButtonState);
}
toggleSidebarDisplay(show) {
this.$page.toggleClass(SIDEBAR_EXPANDED_CLASS, show);
this.$page.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
this.$sidebar.toggleClass(SIDEBAR_EXPANDED_CLASS, show);
this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
}
toggleBulkEditButtonDisabled(disable) {
if (disable) {
this.$bulkUpdateEnableBtn.disable();
} else {
this.$bulkUpdateEnableBtn.enable();
}
}
toggleCheckboxDisplay(show) {
this.$checkAllContainer.toggleClass(HIDDEN_CLASS, !show);
this.$issueChecks.toggleClass(HIDDEN_CLASS, !show);
}
toggleOtherFiltersDisabled(disable) {
this.$otherFilters.toggleClass(DISABLED_CONTENT_CLASS, disable);
}
toggleSubmitButtonDisabled(disable) {
if (disable) {
this.$bulkEditSubmitBtn.disable();
} else {
this.$bulkEditSubmitBtn.enable();
}
}
// loosely based on method of the same name in right_sidebar.js
setSidebarHeight() {
const currentScrollDepth = window.pageYOffset || 0;
const diff = this.navHeight - currentScrollDepth;
if (diff > 0) {
this.$sidebar.outerHeight(window.innerHeight - diff);
} else {
this.$sidebar.outerHeight('100%');
}
}
static getCheckedIssueIds() {
const $checkedIssues = $('.selected_issue:checked');
if ($checkedIssues.length > 0) {
return $.map($checkedIssues, value => $(value).data('id'));
}
return [];
}
}
/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */
/* global Issuable */
/* global IssuableIndex */
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
((global) => {
var issuable_created;
issuable_created = false;
global.Issuable = {
init: function() {
Issuable.initTemplates();
Issuable.initSearch();
Issuable.initChecks();
Issuable.initResetFilters();
Issuable.resetIncomingEmailToken();
return Issuable.initLabelFilterRemove();
global.IssuableIndex = {
init: function(pagePrefix) {
IssuableIndex.initTemplates();
IssuableIndex.initSearch();
IssuableIndex.initBulkUpdate(pagePrefix);
IssuableIndex.initResetFilters();
IssuableIndex.resetIncomingEmailToken();
IssuableIndex.initLabelFilterRemove();
},
initTemplates: function() {
return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
return IssuableIndex.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
},
initSearch: function() {
const $searchInput = $('#issuable_search');
Issuable.initSearchState($searchInput);
IssuableIndex.initSearchState($searchInput);
// `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false);
const debouncedExecSearch = _.debounce(IssuableIndex.executeSearch, 1000, false);
$searchInput.off('keyup').on('keyup', debouncedExecSearch);
......@@ -37,16 +40,16 @@
initSearchState: function($searchInput) {
const currentSearchVal = $searchInput.val();
Issuable.searchState = {
IssuableIndex.searchState = {
elem: $searchInput,
current: currentSearchVal
};
Issuable.maybeFocusOnSearch();
IssuableIndex.maybeFocusOnSearch();
},
accessSearchPristine: function(set) {
// store reference to previous value to prevent search on non-mutating keyup
const state = Issuable.searchState;
const state = IssuableIndex.searchState;
const currentSearchVal = state.elem.val();
if (set) {
......@@ -56,10 +59,10 @@
}
},
maybeFocusOnSearch: function() {
const currentSearchVal = Issuable.searchState.current;
const currentSearchVal = IssuableIndex.searchState.current;
if (currentSearchVal && currentSearchVal !== '') {
const queryLength = currentSearchVal.length;
const $searchInput = Issuable.searchState.elem;
const $searchInput = IssuableIndex.searchState.elem;
/* The following ensures that the cursor is initially placed at
* the end of search input when focus is applied. It accounts
......@@ -80,7 +83,7 @@
const $searchValue = $search.val();
const $filtersForm = $('.js-filter-form');
const $input = $(`input[name='${$searchName}']`, $filtersForm);
const isPristine = Issuable.accessSearchPristine();
const isPristine = IssuableIndex.accessSearchPristine();
if (isPristine) {
return;
......@@ -92,7 +95,7 @@
$input.val($searchValue);
}
Issuable.filterResults($filtersForm);
IssuableIndex.filterResults($filtersForm);
},
initLabelFilterRemove: function() {
return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
......@@ -103,7 +106,7 @@
return this.value === $button.data('label');
}).remove();
// Submit the form to get new data
Issuable.filterResults($('.filter-form'));
IssuableIndex.filterResults($('.filter-form'));
});
},
filterResults: (function(_this) {
......@@ -132,38 +135,18 @@
gl.utils.visitUrl(baseIssuesUrl);
});
},
initChecks: function() {
this.issuableBulkActions = $('.bulk-update').data('bulkActions');
$('.check_all_issues').off('click').on('click', function() {
$('.selected_issue').prop('checked', this.checked);
return Issuable.checkChanged();
});
return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this));
},
checkChanged: function() {
const $checkedIssues = $('.selected_issue:checked');
const $updateIssuesIds = $('#update_issuable_ids');
const $issuesOtherFilters = $('.issues-other-filters');
const $issuesBulkUpdate = $('.issues_bulk_update');
this.issuableBulkActions.willUpdateLabels = false;
this.issuableBulkActions.setOriginalDropdownData();
if ($checkedIssues.length > 0) {
const ids = $.map($checkedIssues, function(value) {
return $(value).data('id');
initBulkUpdate: function(pagePrefix) {
const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
const alreadyInitialized = !!this.bulkUpdateSidebar;
if (userCanBulkUpdate && !alreadyInitialized) {
IssuableBulkUpdateActions.init({
prefixId: pagePrefix,
});
$updateIssuesIds.val(ids);
$issuesOtherFilters.hide();
$issuesBulkUpdate.show();
} else {
$updateIssuesIds.val([]);
$issuesBulkUpdate.hide();
$issuesOtherFilters.show();
this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
}
return true;
},
resetIncomingEmailToken: function() {
$('.incoming-email-token-reset').on('click', function(e) {
e.preventDefault();
......
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
/* global Issuable */
/* global Flash */
((global) => {
class IssuableBulkActions {
constructor({ container, form, issues, prefixId } = {}) {
this.prefixId = prefixId || 'issue_';
this.form = form || this.getElement('.bulk-update');
this.$labelDropdown = this.form.find('.js-label-select');
this.issues = issues || this.getElement('.issues-list .issue');
this.form.data('bulkActions', this);
this.willUpdateLabels = false;
this.bindEvents();
// Fixes bulk-assign not working when navigating through pages
Issuable.initChecks();
}
bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
}
onFormSubmit(e) {
e.preventDefault();
return this.submit();
}
submit() {
const _this = this;
const xhr = $.ajax({
url: this.form.attr('action'),
method: this.form.attr('method'),
dataType: 'JSON',
data: this.getFormDataAsObject()
});
xhr.done(() => window.location.reload());
xhr.fail(() => new Flash("Issue update failed"));
return xhr.always(this.onFormSubmitAlways.bind(this));
}
onFormSubmitAlways() {
return this.form.find('[type="submit"]').enable();
}
getSelectedIssues() {
return this.issues.has('.selected_issue:checked');
}
getLabelsFromSelection() {
const labels = [];
this.getSelectedIssues().map(function() {
const labelsData = $(this).data('labels');
if (labelsData) {
return labelsData.map(function(labelId) {
if (labels.indexOf(labelId) === -1) {
return labels.push(labelId);
}
});
}
});
return labels;
}
/**
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
*/
getUnmarkedIndeterminedLabels() {
const result = [];
const labelsToKeep = this.$labelDropdown.data('indeterminate');
this.getLabelsFromSelection().forEach((id) => {
if (labelsToKeep.indexOf(id) === -1) {
result.push(id);
}
});
return result;
}
/**
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
*/
getFormDataAsObject() {
const formData = {
update: {
state_event: this.form.find('input[name="update[state_event]"]').val(),
// For Merge Requests
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
// For Issues
assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
add_label_ids: [],
remove_label_ids: []
}
};
if (this.willUpdateLabels) {
formData.update.add_label_ids = this.$labelDropdown.data('marked');
formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
}
return formData;
}
setOriginalDropdownData() {
const $labelSelect = $('.bulk-update .js-label-select');
$labelSelect.data('common', this.getOriginalCommonIds());
$labelSelect.data('marked', this.getOriginalMarkedIds());
$labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
}
// From issuable's initial bulk selection
getOriginalCommonIds() {
const labelIds = [];
this.getElement('.selected_issue:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
return _.intersection.apply(this, labelIds);
}
// From issuable's initial bulk selection
getOriginalMarkedIds() {
const labelIds = [];
this.getElement('.selected_issue:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
return _.intersection.apply(this, labelIds);
}
// From issuable's initial bulk selection
getOriginalIndeterminateIds() {
const uniqueIds = [];
const labelIds = [];
let issuableLabels = [];
// Collect unique label IDs for all checked issues
this.getElement('.selected_issue:checked').each((i, el) => {
issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
issuableLabels.forEach((labelId) => {
// Store unique IDs
if (uniqueIds.indexOf(labelId) === -1) {
uniqueIds.push(labelId);
}
});
// Store array of IDs per issuable
labelIds.push(issuableLabels);
});
// Add uniqueIds to add it as argument for _.intersection
labelIds.unshift(uniqueIds);
// Return IDs that are present but not in all selected issueables
return _.difference(uniqueIds, _.intersection.apply(this, labelIds));
}
getElement(selector) {
this.scopeEl = this.scopeEl || $('.content');
return this.scopeEl.find(selector);
}
}
global.IssuableBulkActions = IssuableBulkActions;
})(window.gl || (window.gl = {}));
......@@ -2,6 +2,8 @@
/* global Issuable */
/* global ListLabel */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
(function() {
this.LabelsSelect = (function() {
function LabelsSelect(els) {
......@@ -430,20 +432,15 @@
if ($('.selected_issue:checked').length) {
return;
}
return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label');
return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label');
};
LabelsSelect.prototype.enableBulkLabelDropdown = function() {
var issuableBulkActions;
if ($('.selected_issue:checked').length) {
issuableBulkActions = $('.bulk-update').data('bulkActions');
return issuableBulkActions.willUpdateLabels = true;
}
IssuableBulkUpdateActions.willUpdateLabels = true;
};
LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) {
var i, markedIds, unmarkedIds, indeterminateIds;
var issuableBulkActions = $('.bulk-update').data('bulkActions');
markedIds = $dropdown.data('marked') || [];
unmarkedIds = $dropdown.data('unmarked') || [];
......@@ -469,13 +466,13 @@
}
// If an indeterminate item is being unmarked
if (issuableBulkActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
if (IssuableBulkUpdateActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
unmarkedIds.push(value);
}
// If a marked item is being unmarked
// (a marked item could also be a label that is present in all selection)
if (issuableBulkActions.getOriginalCommonIds().indexOf(value) > -1) {
if (IssuableBulkUpdateActions.getOriginalCommonIds().indexOf(value) > -1) {
unmarkedIds.push(value);
}
}
......
......@@ -104,12 +104,11 @@ import './group_label_subscription';
import './groups_select';
import './header';
import './importer_status';
import './issuable';
import './issuable_index';
import './issuable_context';
import './issuable_form';
import './issue';
import './issue_status_select';
import './issues_bulk_assignment';
import './label_manager';
import './labels';
import './labels_select';
......
<script>
import linkedPipelinesColumn from './linked_pipelines_column.vue';
import stageColumnComponent from './stage_column_component.vue';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import '../../../flash';
export default {
props: {
......@@ -16,6 +16,7 @@
},
components: {
linkedPipelinesColumn,
stageColumnComponent,
loadingIcon,
},
......@@ -24,6 +25,18 @@
graph() {
return this.pipeline.details && this.pipeline.details.stages;
},
triggered() {
return this.pipeline.triggered || [];
},
triggeredBy() {
return this.pipeline.triggeredBy || [];
},
hasTriggered() {
return !!this.triggered.length;
},
hasTriggeredBy() {
return !!this.triggeredBy.length;
},
},
methods: {
......@@ -61,17 +74,42 @@
/>
</div>
<linked-pipelines-column
v-if="hasTriggeredBy"
:linked-pipelines="triggeredBy"
column-title="Upstream"
graph-position="left"
/>
<ul
v-if="!isLoading"
class="stage-column-list">
class="stage-column-list"
:class="{
'has-linked-pipelines': hasTriggered || hasTriggeredBy
}"
>
<stage-column-component
v-for="(stage, index) in graph"
:class="{
'has-upstream': index === 0 && hasTriggeredBy,
'has-downstream': index === graph.length - 1 && hasTriggered,
'has-only-one-job': stage.groups.length === 1
}"
:title="capitalizeStageName(stage.name)"
:jobs="stage.groups"
:key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"/>
:is-first-column="isFirstColumn(index)"
:has-triggered-by="hasTriggeredBy"
/>
</ul>
<linked-pipelines-column
v-if="hasTriggered"
:linked-pipelines="triggered"
column-title="Downstream"
graph-position="right"
/>
</div>
</div>
</template>
<script>
import ciStatus from '../../../vue_shared/components/ci_icon.vue';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
export default {
props: {
pipelineId: {
type: Number,
required: true,
},
pipelinePath: {
type: String,
required: true,
},
pipelineStatus: {
type: Object,
required: true,
},
projectName: {
type: String,
required: true,
},
},
mixins: [
tooltipMixin,
],
components: {
ciStatus,
},
computed: {
tooltipText() {
return `${this.projectName} - ${this.pipelineStatus.label}`;
},
},
};
</script>
<template>
<li class="linked-pipeline build">
<div class="curve"></div>
<div>
<a
class="linked-pipeline-content"
:href="pipelinePath"
:title="tooltipText"
ref="tooltip"
data-toggle="tooltip"
data-container="body">
<span class="linked-pipeline-status ci-status-text">
<ci-status :status="pipelineStatus"/>
</span>
<span class="linked-pipeline-project-name">{{ projectName }}</span>
<span class="project-name-pipeline-id-separator">&#8226;</span>
<span class="linked-pipeline-id">#{{ pipelineId }}</span>
</a>
</div>
</li>
</template>
<script>
import linkedPipeline from './linked_pipeline.vue';
export default {
props: {
columnTitle: {
type: String,
required: true,
},
linkedPipelines: {
type: Array,
required: true,
},
graphPosition: {
type: String,
required: true,
},
},
components: {
linkedPipeline,
},
computed: {
columnClass() {
return `graph-position-${this.graphPosition}`;
},
},
};
</script>
<template>
<div
class="stage-column linked-pipelines-column"
:class="columnClass"
>
<div class="stage-name linked-pipelines-column-title"> {{ columnTitle }} </div>
<div class="cross-project-triangle"></div>
<ul>
<linked-pipeline
v-for="(pipeline, index) in linkedPipelines"
:class="{
'flat-connector-before': index === 0 && graphPosition === 'right'
}"
:key="pipeline.id"
:pipeline-id="pipeline.id"
:project-name="pipeline.project.name"
:pipeline-status="pipeline.details.status"
:pipeline-path="pipeline.path"
/>
</ul>
</div>
</template>
......@@ -25,6 +25,10 @@ export default {
required: false,
default: '',
},
hasTriggeredBy: {
type: Boolean,
required: true,
},
},
components: {
......@@ -40,10 +44,6 @@ export default {
jobId(job) {
return `ci-badge-${job.name}`;
},
buildConnnectorClass(index) {
return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
},
},
};
</script>
......@@ -60,7 +60,9 @@ export default {
v-for="(job, index) in jobs"
:key="job.id"
class="build"
:class="buildConnnectorClass(index)"
:class="{
'left-connector': index === 0 && (!isFirstColumn || hasTriggeredBy)
}"
:id="jobId(job)">
<div class="curve"></div>
......
......@@ -21,6 +21,7 @@
* - Jobs table
* - Jobs show view header
* - Jobs show view sidebar
* - Linked pipelines
*/
export default {
props: {
......
......@@ -449,3 +449,9 @@ table {
word-wrap: break-word;
}
}
.disabled-content {
pointer-events: none;
opacity: .5;
}
......@@ -22,12 +22,6 @@
}
@media (min-width: $screen-sm-min) {
.issues_bulk_update {
.dropdown-menu-toggle:not(.wide) {
width: 132px;
}
}
.filter-item:not(:last-child) {
margin-right: 6px;
}
......@@ -375,12 +369,6 @@
padding: 0;
}
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
.issue-bulk-update-dropdown-toggle {
width: 100px;
}
}
@media (max-width: $screen-xs-max) {
.issues-details-filters {
padding: 0 0 10px;
......
......@@ -29,10 +29,6 @@
display: none;
}
.issues-holder .issue-check {
display: none;
}
.rss-btn {
display: none;
}
......
......@@ -33,7 +33,7 @@
padding-right: 0;
@media (min-width: $screen-sm-min) {
&:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
}
......@@ -56,7 +56,7 @@
z-index: 300;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
}
}
......@@ -88,3 +88,35 @@
min-height: 100%;
}
}
@mixin maintain-sidebar-dimensions {
display: block;
width: $gutter-width;
padding: 10px 20px;
}
.issues-bulk-update.right-sidebar {
@include maintain-sidebar-dimensions;
transition: right $sidebar-transition-duration;
right: -$gutter-width;
&.right-sidebar-expanded {
@include maintain-sidebar-dimensions;
right: 0;
}
&.right-sidebar-collapsed {
@include maintain-sidebar-dimensions;
right: -$gutter-width;
.block {
padding: 16px 0;
width: 250px;
border-bottom: 1px solid $border-color;
}
}
.issuable-sidebar {
padding: 0 3px;
}
}
......@@ -250,6 +250,7 @@ $dark-diff-match-bg: rgba(255, 255, 255, 0.3);
$dark-diff-match-color: rgba(255, 255, 255, 0.1);
$file-mode-changed: #777;
$file-mode-changed: #777;
$diff-image-bg: #ddd;
$diff-image-info-color: grey;
$diff-swipe-border: #999;
$diff-view-modes-color: grey;
......@@ -581,3 +582,10 @@ $gl-gold-plan: #d4af37;
$gl-silver-plan: #91a1ab;
$gl-bronze-plan: #cd7f32;
$gl-no-plan: $gl-gray-light;
/*
Cross-project Pipelines
*/
$linked-project-column-margin: 60px;
@mixin flat-connector-before($length: 44px) {
&::before {
content: '';
position: absolute;
top: 48%;
left: -$length;
border-top: 2px solid $border-color;
width: $length;
height: 1px;
}
}
@mixin build-content($border-radius: 30px) {
display: inline-block;
padding: 8px 10px 9px;
width: 100%;
border: 1px solid $border-color;
border-radius: $border-radius;
background-color: $white-light;
&:hover {
background-color: $stage-hover-bg;
border: 1px solid $stage-hover-border;
color: $gl-text-color;
}
}
.pipelines {
.stage {
max-width: 90px;
......@@ -381,15 +408,7 @@
margin-left: 44px;
.left-connector {
&::before {
content: '';
position: absolute;
top: 48%;
left: -44px;
border-top: 2px solid $border-color;
width: 44px;
height: 1px;
}
@include flat-connector-before;
}
}
}
......@@ -402,7 +421,8 @@
list-style: none;
}
&:last-child {
// when downstream pipelines are present, the last stage isn't the last column
&:last-child:not(.has-downstream) {
.build {
// Remove right connecting horizontal line from first build in last stage
&:first-child {
......@@ -425,7 +445,8 @@
}
}
&:first-child {
// when upstream pipelines are present, the first stage isn't the first column
&:first-child:not(.has-upstream) {
.build {
// Remove left curved connectors from all builds in first stage
&:not(:first-child) {
......@@ -543,21 +564,9 @@
}
.build-content {
display: inline-block;
padding: 8px 10px 9px;
width: 100%;
border: 1px solid $border-color;
border-radius: 30px;
background-color: $white-light;
&:hover {
background-color: $stage-hover-bg;
border: 1px solid $stage-hover-border;
color: $gl-text-color;
}
@include build-content();
}
// Connect first build in each stage with right horizontal line
&:first-child {
&::after {
......@@ -988,3 +997,115 @@
width: 12px;
}
}
/**
* Cross-project pipelines (applied conditionally to pipeline graph)
*/
.has-linked-pipelines.stage-column-list {
display: inline-block;
}
.linked-pipelines-column.stage-column {
position: relative;
& > ul {
padding: 0;
}
&.graph-position-left {
margin-right: 36px;
.cross-project-triangle {
right: -42px;
}
}
&.graph-position-right {
margin-left: 60px;
.cross-project-triangle {
left: -64px;
}
}
.linked-pipeline.build {
height: 40px;
// apply custom dimensions to connector before and after for triangle arrow
&.flat-connector-before {
@include flat-connector-before($linked-project-column-margin);
}
&::after {
right: -$linked-project-column-margin;
width: $linked-project-column-margin;
}
.linked-pipeline-content {
@include build-content(0);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.stage-column.has-upstream {
margin-left: 60px;
.left-connector {
@include flat-connector-before(60px)
}
&.has-only-one-job {
margin-left: 30px;
margin-right: 0;
.left-connector {
@include flat-connector-before;
}
}
}
.stage-column.has-downstream {
margin-right: $linked-project-column-margin;
&.has-only-one-job:not(:first-child) {
margin-right: 36px;
margin-left: 0;
.left-connector {
@include flat-connector-before;
}
}
.build {
&:first-child {
&::after {
right: -$linked-project-column-margin;
width: $linked-project-column-margin;
}
}
}
}
.cross-project-triangle {
position: absolute;
top: 48px;
width: 0;
height: 0;
border-bottom: 7px solid transparent;
border-top: 7px solid transparent;
border-left: 7px solid $gray-darkest;
font-size: 0;
line-height: 0;
z-index: 10;
}
.project-name-pipeline-id-separator {
display: inline-block;
margin: 4px 2px 0;
font-size: 10px;
vertical-align: top;
}
module EE
module GitlabRoutingHelper
include ProjectsHelper
include ApplicationSettingsHelper
def geo_primary_web_url(project)
File.join(::Gitlab::Geo.primary_node.url, ::Gitlab::Routing.url_helpers.namespace_project_path(project.namespace, project))
end
def geo_primary_ssh_url_to_repo(project)
"#{::Gitlab::Geo.primary_node.clone_url_prefix}#{project.path_with_namespace}"
end
def geo_primary_http_url_to_repo(project)
"#{geo_primary_web_url(project)}.git"
end
def geo_primary_default_url_to_repo(project)
case default_clone_protocol
when 'http'
when 'ssh'
geo_primary_ssh_url_to_repo(project)
else
geo_primary_http_url_to_repo(project)
end
end
......
......@@ -5,10 +5,10 @@ class GeoNode < ActiveRecord::Base
belongs_to :oauth_application, class_name: 'Doorkeeper::Application', dependent: :destroy
belongs_to :system_hook, dependent: :destroy
default_values schema: 'http',
default_values schema: lambda { Gitlab.config.gitlab.protocol },
host: lambda { Gitlab.config.gitlab.host },
port: 80,
relative_url_root: '',
port: lambda { Gitlab.config.gitlab.port },
relative_url_root: lambda { Gitlab.config.gitlab.relative_url_root },
primary: false
accepts_nested_attributes_for :geo_node_key, :system_hook
......
%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
.issue-box
- if @bulk_edit
.issue-check
- if @can_bulk_update
.issue-check.hidden
= check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
.issue-info-container
.issue-title.title
......
- @no_container = true
- @bulk_edit = can?(current_user, :admin_issue, @project)
- @can_bulk_update = can?(current_user, :admin_issue, @project)
- page_title "Issues"
- new_issue_email = @project.new_issue_address(current_user)
......@@ -26,6 +26,8 @@
- if current_user
%button.csv_download_link.btn.append-right-10.has-tooltip{ title: 'Export as CSV' }
= icon('download')
- if @can_bulk_update
= button_tag "Edit Issues", class: "btn btn-default js-bulk-update-toggle"
= link_to new_namespace_project_issue_path(@project.namespace,
@project,
issue: { assignee_id: issues_finder.assignee.try(:id),
......@@ -36,6 +38,9 @@
New issue
= render 'shared/issuable/search_bar', type: :issues
- if @can_bulk_update
= render 'shared/issuable/bulk_update_sidebar', type: :issues
.issues-holder
= render 'issues'
- if new_issue_email
......
%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } }
- if @bulk_edit
.issue-check
- if @can_bulk_update
.issue-check.hidden
= check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
.issue-info-container
......
- @no_container = true
- @bulk_edit = can?(current_user, :admin_merge_request, @project)
- @can_bulk_update = can?(current_user, :admin_merge_request, @project)
- page_title "Merge Requests"
- unless @project.default_issues_tracker?
......@@ -18,6 +18,8 @@
.top-area
= render 'shared/issuable/nav', type: :merge_requests
.nav-controls
- if @can_bulk_update
= button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle"
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- if merge_project
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New merge request" do
......@@ -25,6 +27,9 @@
= render 'shared/issuable/search_bar', type: :merge_requests
- if @can_bulk_update
= render 'shared/issuable/bulk_update_sidebar', type: :merge_requests
.merge-requests-holder
= render 'merge_requests'
- else
......
......@@ -21,7 +21,7 @@
remote:
= clipboard_button(target: 'pre#geo-info-2')
%pre#geo-info-2.dark
git remote set-url --push origin &lt;clone url for primary repository&gt;
git remote set-url --push origin #{geo_primary_default_url_to_repo(project)}
%p
%strong= 'Done.'
......@@ -37,7 +37,7 @@
'git clone ' +
(data.cloneUrlSecondary || '<clone url for secondary repository>')
);
$('geo-info-2').text(
$('#geo-info-2').text(
'git remote set-url --push origin ' +
(data.cloneUrlPrimary || '<clone url for primary repository>')
);
......
- type = local_assigns.fetch(:type)
%aside.issues-bulk-update.js-right-sidebar.right-sidebar.affix-top{ data: { "offset-top" => "50", "spy" => "affix" }, "aria-live" => "polite" }
.issuable-sidebar
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do
.block
.filter-item.inline.update-issues-btn.pull-left
= button_tag "Update all", class: "btn update-selected-issues btn-info", disabled: true
= button_tag "Cancel", class: "btn btn-default js-bulk-update-menu-hide pull-right"
.block
.title
Status
.filter-item
= dropdown_tag("Select status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
%ul
%li
%a{ href: "#", data: { id: "reopen" } } Open
%li
%a{ href: "#", data: { id: "close" } } Closed
.block
.title
Assignee
.filter-item
- if type == :issues
- field_name = "update[assignee_ids][]"
- else
- field_name = "update[assignee_id]"
= dropdown_tag("Select assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
.block
.title
Milestone
.filter-item
= dropdown_tag("Select milestone", options: { title: "Assign milestone", toggle_class: "js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true, default_label: "Milestone" } })
.block
.title
Labels
.filter-item.labels-filter
= render "shared/issuable/label_dropdown", classes: ["js-filter-bulk-update", "js-multiselect"], dropdown_title: "Apply a label", show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true, default_label: "Labels" }, label_name: "Select labels", no_default_styles: true
.block
.title
Subscriptions
.filter-item
= dropdown_tag("Select subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
%ul
%li
%a{ href: "#", data: { id: "subscribe" } } Subscribe
%li
%a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
= hidden_field_tag "update[issuable_ids]", []
= hidden_field_tag :state_event, params[:state_event]
......@@ -7,10 +7,6 @@
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
- if params[:search].present?
= hidden_field_tag :search, params[:search]
- if @bulk_edit
.check-all-holder
= check_box_tag "check_all_issues", nil, false,
class: "check_all_issues left"
.issues-other-filters
.filter-item.inline
- if params[:author_id].present?
......@@ -37,35 +33,6 @@
.pull-right
= render 'shared/sort_dropdown'
- if @bulk_edit
.issues_bulk_update.hide
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
.filter-item.inline
= dropdown_tag("Status", options: { toggle_class: "issue-bulk-update-dropdown-toggle js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
%ul
%li
%a{ href: "#", data: { id: "reopen" } } Open
%li
%a{ href: "#", data: {id: "close" } } Closed
.filter-item.inline
= dropdown_tag("Assignee", options: { toggle_class: "issue-bulk-update-dropdown-toggle js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]", default_label: "Assignee" } })
.filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'issue-bulk-update-dropdown-toggle js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", default_label: "Milestone", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline
= dropdown_tag("Subscription", options: { toggle_class: "issue-bulk-update-dropdown-toggle js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
%ul
%li
%a{ href: "#", data: { id: "subscribe" } } Subscribe
%li
%a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
= hidden_field_tag 'update[issuable_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
- has_labels = @labels && @labels.any?
.row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
- if has_labels
......
......@@ -11,6 +11,8 @@
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label")
- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"}
- dropdown_data.merge!(data_options)
- label_name = local_assigns.fetch(:label_name, "Labels")
- no_default_styles = local_assigns.fetch(:no_default_styles, false)
- classes << 'js-extra-options' if extra_options
- classes << 'js-filter-submit' if filter_submit
......@@ -20,8 +22,9 @@
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect{ class: classes.join(' '), type: "button", data: dropdown_data }
%span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) }
= multi_label_name(selected, "Labels")
- apply_is_default_styles = (selected.nil? || selected.empty?) && !no_default_styles
%span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) }
= multi_label_name(selected, label_name)
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create }
......
......@@ -10,10 +10,9 @@
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
- if params[:search].present?
= hidden_field_tag :search, params[:search]
- if @bulk_edit
.check-all-holder
= check_box_tag "check_all_issues", nil, false,
class: "check_all_issues left"
- if @can_bulk_update
.check-all-holder.hidden
= check_box_tag "check-all-issues", nil, false, class: "check-all-issues left"
.issues-other-filters.filtered-search-wrapper
.filtered-search-box
- if type != :boards_modal && type != :boards
......@@ -130,55 +129,11 @@
- elsif type != :boards_modal
= render 'shared/sort_dropdown', type: local_assigns[:type]
- if @bulk_edit
.issues_bulk_update.hide
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
.filter-item.inline
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
%ul
%li
%a{ href: "#", data: { id: "reopen" } } Open
%li
%a{ href: "#", data: { id: "close" } } Closed
.filter-item.inline
- if type == :issues
- field_name = "update[assignee_ids][]"
- else
- field_name = "update[assignee_id]"
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
.filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true, default_label: "Milestone" } })
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true, default_label: "Labels" }
.filter-item.inline
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
%ul
%li
%a{ href: "#", data: { id: "subscribe" } } Subscribe
%li
%a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
= hidden_field_tag 'update[issuable_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline.update-issues-btn
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
- unless type === :boards_modal
:javascript
new LabelsSelect();
new MilestoneSelect();
new IssueStatusSelect();
new SubscriptionSelect();
$(document).off('page:restore').on('page:restore', function (event) {
if (gl.FilteredSearchManager) {
const filteredSearchManager = new gl.FilteredSearchManager();
filteredSearchManager.setup();
}
Issuable.init();
new gl.IssuableBulkActions({
prefixId: 'issue_',
});
});
---
title: Add primary node clone URL to Geo secondary 'How to work faster with Geo' popover
merge_request:
author:
---
title: Use the current node configuration to populate suggested new URL for Geo node
merge_request:
author:
......@@ -18,13 +18,13 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'can bulk assign' do
before do
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
end
context 'a label' do
context 'to all issues' do
before do
check 'check_all_issues'
check 'check-all-issues'
open_labels_dropdown ['bug']
update_issues
end
......@@ -52,7 +52,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'multiple labels' do
context 'to all issues' do
before do
check 'check_all_issues'
check 'check-all-issues'
open_labels_dropdown %w(bug feature)
update_issues
end
......@@ -86,9 +86,10 @@ feature 'Issues > Labels bulk assignment', feature: true do
before do
issue2.labels << bug
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
check 'check_all_issues'
enable_bulk_update
check 'check-all-issues'
open_labels_dropdown ['bug']
update_issues
end
......@@ -107,9 +108,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue2.labels << bug
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
check 'check_all_issues'
enable_bulk_update
check 'check-all-issues'
unmark_labels_in_dropdown %w(bug feature)
update_issues
end
......@@ -127,8 +127,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue1.labels << bug
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
check_issue issue1
unmark_labels_in_dropdown ['bug']
update_issues
......@@ -147,8 +146,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue2.labels << bug
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
check_issue issue1
check_issue issue2
unmark_labels_in_dropdown ['bug']
......@@ -171,14 +169,15 @@ feature 'Issues > Labels bulk assignment', feature: true do
before do
issue1.labels << bug
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
end
it 'keeps labels' do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
check 'check_all_issues'
check 'check-all-issues'
open_milestone_dropdown(['First Release'])
update_issues
......@@ -192,14 +191,13 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'setting a milestone and adding another label' do
before do
issue1.labels << bug
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
end
it 'keeps existing label and new label is present' do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
check 'check_all_issues'
check 'check-all-issues'
open_milestone_dropdown ['First Release']
open_labels_dropdown ['feature']
update_issues
......@@ -218,7 +216,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue1.labels << feature
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
end
it 'keeps existing label and new label is present' do
......@@ -226,7 +224,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
check 'check_all_issues'
check 'check-all-issues'
open_milestone_dropdown ['First Release']
unmark_labels_in_dropdown ['feature']
update_issues
......@@ -248,7 +247,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue1.labels << bug
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
end
it 'keeps labels' do
......@@ -257,7 +256,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
expect(find("#issue_#{issue2.id}")).to have_content 'First Release'
check 'check_all_issues'
check 'check-all-issues'
open_milestone_dropdown(['No Milestone'])
update_issues
......@@ -272,8 +271,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'toggling checked issues' do
before do
issue1.labels << bug
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
end
it do
......@@ -298,14 +296,14 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue1.labels << feature
issue2.labels << bug
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
end
it 'applies label from filtered results' do
check 'check_all_issues'
check 'check-all-issues'
page.within('.issues_bulk_update') do
click_button 'Labels'
page.within('.issues-bulk-update') do
click_button 'Select labels'
wait_for_requests
expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active')
......@@ -340,15 +338,16 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'cannot bulk assign labels' do
it do
expect(page).not_to have_css '.check_all_issues'
expect(page).not_to have_button 'Edit Issues'
expect(page).not_to have_css '.check-all-issues'
expect(page).not_to have_css '.issue-check'
end
end
end
def open_milestone_dropdown(items = [])
page.within('.issues_bulk_update') do
click_button 'Milestone'
page.within('.issues-bulk-update') do
click_button 'Select milestone'
wait_for_requests
items.map do |item|
click_link item
......@@ -357,8 +356,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
end
def open_labels_dropdown(items = [], unmark = false)
page.within('.issues_bulk_update') do
click_button 'Labels'
page.within('.issues-bulk-update') do
click_button 'Select labels'
wait_for_requests
items.map do |item|
click_link item
......@@ -391,7 +390,12 @@ feature 'Issues > Labels bulk assignment', feature: true do
end
def update_issues
click_button 'Update issues'
click_button 'Update all'
wait_for_requests
end
def enable_bulk_update
visit namespace_project_issues_path(project.namespace, project)
click_button 'Edit Issues'
end
end
......@@ -14,7 +14,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
it 'sets to closed' do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
click_button 'Edit Issues'
find('#check-all-issues').click
find('.js-issue-status').click
find('.dropdown-menu-status a', text: 'Closed').click
......@@ -26,7 +27,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
create_closed
visit namespace_project_issues_path(project.namespace, project, state: 'closed')
find('#check_all_issues').click
click_button 'Edit Issues'
find('#check-all-issues').click
find('.js-issue-status').click
find('.dropdown-menu-status a', text: 'Open').click
......@@ -39,7 +41,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
it 'updates to current user' do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
click_button 'Edit Issues'
find('#check-all-issues').click
click_update_assignee_button
find('.dropdown-menu-user-link', text: user.username).click
......@@ -54,7 +57,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
create_assigned
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
click_button 'Edit Issues'
find('#check-all-issues').click
click_update_assignee_button
click_link 'Unassigned'
......@@ -69,8 +73,9 @@ feature 'Multiple issue updating from issues#index', feature: true do
it 'updates milestone' do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
find('.issues_bulk_update .js-milestone-select').click
click_button 'Edit Issues'
find('#check-all-issues').click
find('.issues-bulk-update .js-milestone-select').click
find('.dropdown-menu-milestone a', text: milestone.title).click
click_update_issues_button
......@@ -84,8 +89,9 @@ feature 'Multiple issue updating from issues#index', feature: true do
expect(first('.issue')).to have_content milestone.title
find('#check_all_issues').click
find('.issues_bulk_update .js-milestone-select').click
click_button 'Edit Issues'
find('#check-all-issues').click
find('.issues-bulk-update .js-milestone-select').click
find('.dropdown-menu-milestone a', text: "No Milestone").click
click_update_issues_button
......@@ -112,7 +118,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
end
def click_update_issues_button
find('.update_selected_issues').click
find('.update-selected-issues').click
wait_for_requests
end
end
......@@ -98,14 +98,16 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t
end
def change_status(text)
find('#check_all_issues').click
click_button 'Edit Merge Requests'
find('#check-all-issues').click
find('.js-issue-status').click
find('.dropdown-menu-status a', text: text).click
click_update_merge_requests_button
end
def change_assignee(text)
find('#check_all_issues').click
click_button 'Edit Merge Requests'
find('#check-all-issues').click
find('.js-update-assignee').click
wait_for_requests
......@@ -117,14 +119,15 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t
end
def change_milestone(text)
find('#check_all_issues').click
find('.issues_bulk_update .js-milestone-select').click
click_button 'Edit Merge Requests'
find('#check-all-issues').click
find('.issues-bulk-update .js-milestone-select').click
find('.dropdown-menu-milestone a', text: text).click
click_update_merge_requests_button
end
def click_update_merge_requests_button
find('.update_selected_issues').click
find('.update-selected-issues').click
wait_for_requests
end
end
require 'rails_helper'
feature 'Geo clone instructions', feature: true, js: true do
include Devise::Test::IntegrationHelpers
let(:project) { create(:empty_project, :empty_repo) }
let(:developer) { create(:user) }
background do
primary = create(:geo_node, :primary, schema: 'https', host: 'primary.domain.com', port: 443)
primary.update_attribute(:clone_url_prefix, 'git@primary.domain.com:')
create(:geo_node, :current)
allow(Gitlab::Geo).to receive(:secondary?).and_return(true)
project.team << [developer, :developer]
sign_in(developer)
end
context 'with an SSH key' do
background do
create(:personal_key, user: developer)
end
scenario 'defaults to SSH' do
visit_project
show_geo_clone_instructions
expect_instructions_for('ssh')
end
scenario 'switches to HTTP' do
visit_project
select_protocol('HTTP')
show_geo_clone_instructions
expect_instructions_for('http')
end
end
def visit_project
visit namespace_project_path(project.namespace, project)
end
def select_protocol(protocol)
find('#clone-dropdown').click
find(".#{protocol.downcase}-selector").click
end
def show_geo_clone_instructions
find('.btn-geo').click
end
def expect_instructions_for(protocol)
primary_remote = primary_url(protocol)
secondary_remote = secondary_url(protocol)
expect(page).to have_content('How to work faster with Geo')
expect(page).to have_content("git clone #{secondary_remote}")
expect(page).to have_content("git remote set-url --push origin #{primary_remote}")
end
def primary_url(protocol)
case protocol
when 'ssh'
'git@primary.domain.com:'
when 'http'
'https://primary.domain.com'
end
end
def secondary_url(protocol)
case protocol
when 'ssh'
project.ssh_url_to_repo
when 'http'
project.http_url_to_repo(developer)
end
end
end
require 'spec_helper'
describe EE::GitlabRoutingHelper do
include ProjectsHelper
include ApplicationSettingsHelper
let!(:primary_node) { create(:geo_node, :primary) }
let(:project) { build_stubbed(:empty_project) }
describe '#geo_primary_default_url_to_repo' do
it 'returns an HTTP URL' do
allow(helper).to receive(:default_clone_protocol).and_return('http')
result = helper.geo_primary_default_url_to_repo(project)
expect(result).to start_with('http://')
expect(result).to eq(helper.geo_primary_http_url_to_repo(project))
end
it 'returns an HTTPS URL' do
primary_node.update_attribute(:schema, 'https')
allow(helper).to receive(:default_clone_protocol).and_return('https')
result = helper.geo_primary_default_url_to_repo(project)
expect(result).to start_with('https://')
expect(result).to eq(helper.geo_primary_http_url_to_repo(project))
end
it 'returns an SSH URL' do
allow(helper).to receive(:default_clone_protocol).and_return('ssh')
result = helper.geo_primary_default_url_to_repo(project)
expect(result).to start_with('git@')
expect(result).to eq(helper.geo_primary_ssh_url_to_repo(project))
end
end
end
%form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'}
%input{id: 'utf8', name: 'utf8', value: '✓'}
%input{id: 'check_all_issues', name: 'check_all_issues'}
%input{id: 'check-all-issues', name: 'check-all-issues'}
%input{id: 'search', name: 'search'}
%input{id: 'author_id', name: 'author_id'}
%input{id: 'assignee_id', name: 'assignee_id'}
......
/* global Issuable */
/* global IssuableIndex */
import '~/lib/utils/url_utility';
import '~/issuable';
import '~/issuable_index';
(() => {
const BASE_URL = '/user/project/issues?scope=all&state=closed';
......@@ -24,11 +24,11 @@ import '~/issuable';
beforeEach(() => {
loadFixtures('static/issuable_filter.html.raw');
Issuable.init();
IssuableIndex.init();
});
it('should be defined', () => {
expect(window.Issuable).toBeDefined();
expect(window.IssuableIndex).toBeDefined();
});
describe('filtering', () => {
......@@ -43,7 +43,7 @@ import '~/issuable';
it('should contain only the default parameters', () => {
spyOn(gl.utils, 'visitUrl');
Issuable.filterResults($filtersForm);
IssuableIndex.filterResults($filtersForm);
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
});
......@@ -52,7 +52,7 @@ import '~/issuable';
spyOn(gl.utils, 'visitUrl');
updateForm({ search: 'broken' }, $filtersForm);
Issuable.filterResults($filtersForm);
IssuableIndex.filterResults($filtersForm);
const params = `${DEFAULT_PARAMS}&search=broken`;
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
......@@ -64,14 +64,14 @@ import '~/issuable';
// initial filter
updateForm({ milestone_title: 'v1.0' }, $filtersForm);
Issuable.filterResults($filtersForm);
IssuableIndex.filterResults($filtersForm);
let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`;
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
// update filter
updateForm({ label_name: 'Frontend' }, $filtersForm);
Issuable.filterResults($filtersForm);
IssuableIndex.filterResults($filtersForm);
params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`;
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
});
......
import Vue from 'vue';
import graphComponent from '~/pipelines/components/graph/graph_component.vue';
import graphJSON from './mock_data';
import linkedPipelineJSON from './linked_pipelines_mock_data';
describe('graph component', () => {
preloadFixtures('static/graph.html.raw');
const GraphComponent = Vue.extend(graphComponent);
let GraphComponent;
const pipelineJSON = Object.assign(graphJSON, {
triggered: linkedPipelineJSON.triggered,
triggeredBy: linkedPipelineJSON.triggered_by,
});
beforeEach(() => {
loadFixtures('static/graph.html.raw');
GraphComponent = Vue.extend(graphComponent);
});
const defaultPropsData = {
pipeline: pipelineJSON,
isLoading: false,
};
describe('graph component', function () {
describe('while is loading', function () {
beforeEach(function () {
this.component = new GraphComponent({
propsData: { pipeline: {}, isLoading: true },
}).$mount();
});
describe('while is loading', () => {
it('should render a loading icon', () => {
const component = new GraphComponent({
propsData: {
isLoading: true,
pipeline: {},
},
}).$mount('#js-pipeline-graph-vue');
expect(component.$el.querySelector('.loading-icon')).toBeDefined();
it('should render a loading icon', function () {
expect(this.component.$el.querySelector('.fa-spinner')).not.toBeNull();
});
});
describe('with data', () => {
it('should render the graph', () => {
const component = new GraphComponent({
propsData: {
isLoading: false,
pipeline: graphJSON,
},
}).$mount('#js-pipeline-graph-vue');
describe('when linked pipelines are present', function () {
beforeEach(function () {
this.component = new GraphComponent({
propsData: defaultPropsData,
}).$mount();
});
describe('rendered output', function () {
it('should include the pipelines graph', function () {
expect(this.component.$el.classList.contains('js-pipeline-graph')).toEqual(true);
});
expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true);
it('should not include the loading icon', function () {
expect(this.component.$el.querySelector('.fa-spinner')).toBeNull();
});
expect(
component.$el.querySelector('.stage-column:first-child').classList.contains('no-margin'),
).toEqual(true);
it('should include the stage column list', function () {
expect(this.component.$el.querySelector('.stage-column-list')).not.toBeNull();
});
it('should include the no-margin class on the first child', function () {
const firstStageColumnElement = this.component.$el.querySelector('.stage-column-list .stage-column');
expect(firstStageColumnElement.classList.contains('no-margin')).toEqual(true);
});
it('should include the has-only-one-job class on the first child', function () {
const firstStageColumnElement = this.component.$el.querySelector('.stage-column-list .stage-column');
expect(firstStageColumnElement.classList.contains('has-only-one-job')).toEqual(true);
});
it('should include the left-margin class on the second child', function () {
const firstStageColumnElement = this.component.$el.querySelector('.stage-column-list .stage-column:last-child');
expect(firstStageColumnElement.classList.contains('left-margin')).toEqual(true);
});
it('should include the has-linked-pipelines flag', function () {
expect(this.component.$el.querySelector('.has-linked-pipelines')).not.toBeNull();
});
});
expect(
component.$el.querySelector('.stage-column:nth-child(2)').classList.contains('left-margin'),
).toEqual(true);
describe('computeds and methods', function () {
describe('capitalizeStageName', function () {
it('it capitalizes the stage name', function () {
expect(this.component.capitalizeStageName('mystage')).toBe('Mystage');
});
});
expect(
component.$el.querySelector('.stage-column:nth-child(2) .build:nth-child(1)').classList.contains('left-connector'),
).toEqual(true);
describe('stageConnectorClass', function () {
it('it returns left-margin when there is a triggerer', function () {
expect(this.component.stageConnectorClass(0, { groups: ['job'] })).toBe('no-margin');
});
});
});
describe('linked pipelines components', function () {
it('should render an upstream pipelines column', function () {
expect(this.component.$el.querySelector('.linked-pipelines-column')).not.toBeNull();
expect(this.component.$el.innerHTML).toContain('Upstream');
});
it('should render a downstream pipelines column', function () {
expect(this.component.$el.querySelector('.linked-pipelines-column')).not.toBeNull();
expect(this.component.$el.innerHTML).toContain('Downstream');
});
});
});
describe('when linked pipelines are not present', function () {
beforeEach(function () {
const pipeline = Object.assign(graphJSON, { triggered: [], triggeredBy: [] });
this.component = new GraphComponent({
propsData: { pipeline, isLoading: false },
}).$mount();
});
describe('rendered output', function () {
it('should include the first column with a no margin', function () {
const firstColumn = this.component.$el.querySelector('.stage-column:first-child');
expect(firstColumn.classList.contains('no-margin')).toEqual(true);
});
it('should not render a linked pipelines column', function () {
expect(this.component.$el.querySelector('.linked-pipelines-column')).toBeNull();
});
});
expect(component.$el.querySelector('loading-icon')).toBe(null);
describe('stageConnectorClass', function () {
it('it returns left-margin when no triggerer and there is one job', function () {
expect(this.component.stageConnectorClass(0, { groups: ['job'] })).toBe('no-margin');
});
expect(component.$el.querySelector('.stage-column-list')).toBeDefined();
it('it returns left-margin when no triggerer and not the first stage', function () {
expect(this.component.stageConnectorClass(99, { groups: ['job'] })).toBe('left-margin');
});
});
});
});
import Vue from 'vue';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
import mockData from './linked_pipelines_mock_data';
const LinkedPipeline = Vue.extend(LinkedPipelineComponent);
const mockPipeline = mockData.triggered[0];
describe('Linked pipeline', () => {
beforeEach(() => {
this.propsData = {
pipelineId: mockPipeline.id,
pipelinePath: mockPipeline.path,
pipelineStatus: mockPipeline.details.status,
projectName: mockPipeline.project.name,
};
this.linkedPipeline = new LinkedPipeline({
propsData: this.propsData,
}).$mount();
});
it('should return a defined Vue component', () => {
expect(this.linkedPipeline).toBeDefined();
});
it('should render a list item as the containing element', () => {
expect(this.linkedPipeline.$el.tagName).toBe('LI');
});
it('should render a link', () => {
const linkElement = this.linkedPipeline.$el.querySelector('.linked-pipeline-content');
expect(linkElement).not.toBeNull();
});
it('should link to the correct path', () => {
const linkElement = this.linkedPipeline.$el.querySelector('.linked-pipeline-content');
expect(linkElement.getAttribute('href')).toBe(this.propsData.pipelinePath);
});
it('should render the project name', () => {
const projectNameElement = this.linkedPipeline.$el.querySelector('.linked-pipeline-project-name');
expect(projectNameElement.innerText).toContain(this.propsData.projectName);
});
it('should render an svg within the status container', () => {
const pipelineStatusElement = this.linkedPipeline.$el.querySelector('.linked-pipeline-status');
expect(pipelineStatusElement.querySelector('svg')).not.toBeNull();
});
it('should render the pipeline status icon svg', () => {
const pipelineStatusElement = this.linkedPipeline.$el.querySelector('.linked-pipeline-status');
expect(pipelineStatusElement.querySelector('.ci-status-icon-running')).not.toBeNull();
expect(pipelineStatusElement.innerHTML).toContain('<svg');
});
it('should render the correct pipeline status icon style selector', () => {
const pipelineStatusElement = this.linkedPipeline.$el.querySelector('.linked-pipeline-status');
expect(pipelineStatusElement.firstChild.classList.contains('ci-status-icon-running')).toBe(true);
});
it('should have a ci-status child component', () => {
const ciStatusComponent = this.linkedPipeline.$children[0];
expect(ciStatusComponent).toBeDefined();
expect(ciStatusComponent.$el.classList.contains('ci-status-icon')).toBe(true);
});
it('should render the pipeline id', () => {
const pipelineIdElement = this.linkedPipeline.$el.querySelector('.linked-pipeline-id');
expect(pipelineIdElement.innerText).toContain(`#${this.propsData.pipelineId}`);
});
it('should correctly compute the tooltip text', () => {
expect(this.linkedPipeline.tooltipText).toContain(mockPipeline.project.name);
expect(this.linkedPipeline.tooltipText).toContain(mockPipeline.details.status.label);
});
it('should render the tooltip text as the title attribute', () => {
const tooltipRef = this.linkedPipeline.$el.querySelector('.linked-pipeline-content');
const titleAttr = tooltipRef.getAttribute('data-original-title');
expect(titleAttr).toContain(mockPipeline.project.name);
expect(titleAttr).toContain(mockPipeline.details.status.label);
});
});
import Vue from 'vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
import mockData from './linked_pipelines_mock_data';
const LinkedPipelinesColumnComponent = Vue.extend(LinkedPipelinesColumn);
describe('Linked Pipelines Column', () => {
beforeEach(() => {
this.propsData = {
columnTitle: 'Upstream',
linkedPipelines: mockData.triggered,
graphPosition: 'right',
};
this.linkedPipelinesColumn = new LinkedPipelinesColumnComponent({
propsData: this.propsData,
}).$mount();
});
it('instantiates a defined Vue component', () => {
expect(this.linkedPipelinesColumn).toBeDefined();
});
it('renders the pipeline orientation', () => {
const titleElement = this.linkedPipelinesColumn.$el.querySelector('.linked-pipelines-column-title');
expect(titleElement.innerText).toContain(this.propsData.columnTitle);
});
it('has the correct number of linked pipeline child components', () => {
expect(this.linkedPipelinesColumn.$children.length).toBe(this.propsData.linkedPipelines.length);
});
it('renders the correct number of linked pipelines', () => {
const linkedPipelineElements = this.linkedPipelinesColumn.$el.querySelectorAll('.linked-pipeline');
expect(linkedPipelineElements.length).toBe(this.propsData.linkedPipelines.length);
});
});
/* eslint-disable quote-props, quotes, comma-dangle */
export default {
"triggered_by": [
{
"id": 129,
"active": true,
"path": "/gitlab-org/gitlab-ce/pipelines/129",
"project": {
"name": "GitLabCE"
},
"details": {
"status": {
"icon": "icon_status_running",
"text": "running",
"label": "running",
"group": "running",
"has_details": true,
"details_path": "/gitlab-org/gitlab-ce/pipelines/129",
"favicon": "/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico"
}
},
"flags": {
"latest": false,
"triggered": false,
"stuck": false,
"yaml_errors": false,
"retryable": true,
"cancelable": true
},
"ref": {
"name": "7-5-stable",
"path": "/gitlab-org/gitlab-ce/commits/7-5-stable",
"tag": false,
"branch": true
},
"commit": {
"id": "23433d4d8b20d7e45c103d0b6048faad38a130ab",
"short_id": "23433d4d",
"title": "Version 7.5.0.rc1",
"created_at": "2014-11-17T15:44:14.000+01:00",
"parent_ids": [
"30ac909f30f58d319b42ed1537664483894b18cd"
],
"message": "Version 7.5.0.rc1\n",
"author_name": "Jacob Vosmaer",
"author_email": "contact@jacobvosmaer.nl",
"authored_date": "2014-11-17T15:44:14.000+01:00",
"committer_name": "Jacob Vosmaer",
"committer_email": "contact@jacobvosmaer.nl",
"committed_date": "2014-11-17T15:44:14.000+01:00",
"author_gravatar_url": "http://www.gravatar.com/avatar/e66d11c0eedf8c07b3b18fca46599807?s=80&d=identicon",
"commit_url": "http://localhost:3000/gitlab-org/gitlab-ce/commit/23433d4d8b20d7e45c103d0b6048faad38a130ab",
"commit_path": "/gitlab-org/gitlab-ce/commit/23433d4d8b20d7e45c103d0b6048faad38a130ab"
},
"retry_path": "/gitlab-org/gitlab-ce/pipelines/129/retry",
"cancel_path": "/gitlab-org/gitlab-ce/pipelines/129/cancel",
"created_at": "2017-05-24T14:46:20.090Z",
"updated_at": "2017-05-24T14:46:29.906Z"
}
],
"triggered": [
{
"id": 132,
"active": true,
"path": "/gitlab-org/gitlab-ce/pipelines/132",
"project": {
"name": "GitLabCE"
},
"details": {
"status": {
"icon": "icon_status_running",
"text": "running",
"label": "running",
"group": "running",
"has_details": true,
"details_path": "/gitlab-org/gitlab-ce/pipelines/132",
"favicon": "/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico"
}
},
"flags": {
"latest": false,
"triggered": false,
"stuck": false,
"yaml_errors": false,
"retryable": true,
"cancelable": true
},
"ref": {
"name": "crowd",
"path": "/gitlab-org/gitlab-ce/commits/crowd",
"tag": false,
"branch": true
},
"commit": {
"id": "b9d58c4cecd06be74c3cc32ccfb522b31544ab2e",
"short_id": "b9d58c4c",
"title": "getting user keys publically through http without any authentication, the github…",
"created_at": "2013-10-03T12:50:33.000+05:30",
"parent_ids": [
"e219cf7246c6a0495e4507deaffeba11e79f13b8"
],
"message": "getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n\nchangelog updated to include ssh key retrieval feature update\n",
"author_name": "devaroop",
"author_email": "devaroop123@yahoo.co.in",
"authored_date": "2013-10-02T20:39:29.000+05:30",
"committer_name": "devaroop",
"committer_email": "devaroop123@yahoo.co.in",
"committed_date": "2013-10-03T12:50:33.000+05:30",
"author_gravatar_url": "http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon",
"commit_url": "http://localhost:3000/gitlab-org/gitlab-ce/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e",
"commit_path": "/gitlab-org/gitlab-ce/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e"
},
"retry_path": "/gitlab-org/gitlab-ce/pipelines/132/retry",
"cancel_path": "/gitlab-org/gitlab-ce/pipelines/132/cancel",
"created_at": "2017-05-24T14:46:24.644Z",
"updated_at": "2017-05-24T14:48:55.226Z"
},
{
"id": 133,
"active": true,
"path": "/gitlab-org/gitlab-ce/pipelines/133",
"project": {
"name": "GitLabCE"
},
"details": {
"status": {
"icon": "icon_status_running",
"text": "running",
"label": "running",
"group": "running",
"has_details": true,
"details_path": "/gitlab-org/gitlab-ce/pipelines/133",
"favicon": "/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico"
}
},
"flags": {
"latest": false,
"triggered": false,
"stuck": false,
"yaml_errors": false,
"retryable": true,
"cancelable": true
},
"ref": {
"name": "crowd",
"path": "/gitlab-org/gitlab-ce/commits/crowd",
"tag": false,
"branch": true
},
"commit": {
"id": "b6bd4856a33df3d144be66c4ed1f1396009bb08b",
"short_id": "b6bd4856",
"title": "getting user keys publically through http without any authentication, the github…",
"created_at": "2013-10-02T20:39:29.000+05:30",
"parent_ids": [
"e219cf7246c6a0495e4507deaffeba11e79f13b8"
],
"message": "getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n",
"author_name": "devaroop",
"author_email": "devaroop123@yahoo.co.in",
"authored_date": "2013-10-02T20:39:29.000+05:30",
"committer_name": "devaroop",
"committer_email": "devaroop123@yahoo.co.in",
"committed_date": "2013-10-02T20:39:29.000+05:30",
"author_gravatar_url": "http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon",
"commit_url": "http://localhost:3000/gitlab-org/gitlab-ce/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b",
"commit_path": "/gitlab-org/gitlab-ce/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b"
},
"retry_path": "/gitlab-org/gitlab-ce/pipelines/133/retry",
"cancel_path": "/gitlab-org/gitlab-ce/pipelines/133/cancel",
"created_at": "2017-05-24T14:46:24.648Z",
"updated_at": "2017-05-24T14:48:59.673Z"
},
{
"id": 130,
"active": true,
"path": "/gitlab-org/gitlab-ce/pipelines/130",
"project": {
"name": "GitLabCE"
},
"details": {
"status": {
"icon": "icon_status_running",
"text": "running",
"label": "running",
"group": "running",
"has_details": true,
"details_path": "/gitlab-org/gitlab-ce/pipelines/130",
"favicon": "/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico"
}
},
"flags": {
"latest": false,
"triggered": false,
"stuck": false,
"yaml_errors": false,
"retryable": true,
"cancelable": true
},
"ref": {
"name": "crowd",
"path": "/gitlab-org/gitlab-ce/commits/crowd",
"tag": false,
"branch": true
},
"commit": {
"id": "6d7ced4a2311eeff037c5575cca1868a6d3f586f",
"short_id": "6d7ced4a",
"title": "Whitespace fixes to patch",
"created_at": "2013-10-08T13:53:22.000-05:00",
"parent_ids": [
"1875141a963a4238bda29011d8f7105839485253"
],
"message": "Whitespace fixes to patch\n",
"author_name": "Dale Hamel",
"author_email": "dale.hamel@srvthe.net",
"authored_date": "2013-10-08T13:53:22.000-05:00",
"committer_name": "Dale Hamel",
"committer_email": "dale.hamel@invenia.ca",
"committed_date": "2013-10-08T13:53:22.000-05:00",
"author_gravatar_url": "http://www.gravatar.com/avatar/cd08930e69fa5ad1a669206e7bafe476?s=80&d=identicon",
"commit_url": "http://localhost:3000/gitlab-org/gitlab-ce/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f",
"commit_path": "/gitlab-org/gitlab-ce/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f"
},
"retry_path": "/gitlab-org/gitlab-ce/pipelines/130/retry",
"cancel_path": "/gitlab-org/gitlab-ce/pipelines/130/cancel",
"created_at": "2017-05-24T14:46:24.630Z",
"updated_at": "2017-05-24T14:49:45.091Z"
}
]
};
......@@ -162,6 +162,15 @@ describe GeoNode, type: :model do
expected_url = 'https://localhost:3000/gitlab'
expect(new_node.url).to eq(expected_url)
end
it 'defaults to existing HTTPS and relative URL if present' do
stub_config_setting(port: 443)
stub_config_setting(protocol: 'https')
stub_config_setting(relative_url_root: '/gitlab')
node = GeoNode.new
expect(node.url).to eq('https://localhost/gitlab')
end
end
describe '#url=' do
......
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