Commit b6bded14 authored by Clement Ho's avatar Clement Ho

Merge branch 'master' into auto-search-when-state-changed

parents aaad42dd 02a877ac
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
/* global ActiveTabMemoizer */ /* global ActiveTabMemoizer */
/* global ShortcutsNavigation */ /* global ShortcutsNavigation */
/* global Build */ /* global Build */
/* global Issuable */ /* global IssuableIndex */
/* global ShortcutsIssuable */ /* global ShortcutsIssuable */
/* global ZenMode */ /* global ZenMode */
/* global Milestone */ /* global Milestone */
...@@ -127,10 +127,9 @@ import ShortcutsBlob from './shortcuts_blob'; ...@@ -127,10 +127,9 @@ import ShortcutsBlob from './shortcuts_blob';
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests'); const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
filteredSearchManager.setup(); filteredSearchManager.setup();
} }
Issuable.init(); const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
new gl.IssuableBulkActions({ IssuableIndex.init(pagePrefix);
prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
});
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new UsersSelect(); new UsersSelect();
break; break;
......
...@@ -70,7 +70,7 @@ export default { ...@@ -70,7 +70,7 @@ export default {
</span> </span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-align-right"> <ul class="dropdown-menu">
<li v-for="action in actions"> <li v-for="action in actions">
<button <button
type="button" type="button"
......
...@@ -421,14 +421,19 @@ export default { ...@@ -421,14 +421,19 @@ export default {
}; };
</script> </script>
<template> <template>
<tr :class="{ 'js-child-row': model.isChildren }"> <div
<td> :class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }">
<div class="table-section section-10" role="gridcell">
<div
v-if="!model.isFolder"
class="table-mobile-header">
Environment
</div>
<a <a
v-if="!model.isFolder" v-if="!model.isFolder"
class="environment-name" class="environment-name flex-truncate-parent table-mobile-content"
:class="{ 'prepend-left-default': model.isChildren }"
:href="environmentPath"> :href="environmentPath">
{{model.name}} <span class="flex-truncate-child">{{model.name}}</span>
</a> </a>
<span <span
v-else v-else
...@@ -461,9 +466,9 @@ export default { ...@@ -461,9 +466,9 @@ export default {
{{model.size}} {{model.size}}
</span> </span>
</span> </span>
</td> </div>
<td class="deployment-column"> <div class="table-section section-10 deployment-column hidden-xs hidden-sm" role="gridcell">
<span v-if="shouldRenderDeploymentID"> <span v-if="shouldRenderDeploymentID">
{{deploymentInternalId}} {{deploymentInternalId}}
</span> </span>
...@@ -478,21 +483,26 @@ export default { ...@@ -478,21 +483,26 @@ export default {
:tooltip-text="deploymentUser.username" :tooltip-text="deploymentUser.username"
/> />
</span> </span>
</td> </div>
<td class="environments-build-cell"> <div class="table-section section-15 hidden-xs hidden-sm" role="gridcell">
<a <a
v-if="shouldRenderBuildName" v-if="shouldRenderBuildName"
class="build-link" class="build-link"
:href="buildPath"> :href="buildPath">
{{buildName}} {{buildName}}
</a> </a>
</td> </div>
<td> <div class="table-section section-25" role="gridcell">
<div
v-if="!model.isFolder"
class="table-mobile-header">
Commit
</div>
<div <div
v-if="!model.isFolder && hasLastDeploymentKey" v-if="!model.isFolder && hasLastDeploymentKey"
class="js-commit-component"> class="js-commit-component table-mobile-content">
<commit-component <commit-component
:tag="commitTag" :tag="commitTag"
:commit-ref="commitRef" :commit-ref="commitRef"
...@@ -501,25 +511,30 @@ export default { ...@@ -501,25 +511,30 @@ export default {
:title="commitTitle" :title="commitTitle"
:author="commitAuthor"/> :author="commitAuthor"/>
</div> </div>
<p <div
v-if="!model.isFolder && !hasLastDeploymentKey" v-if="!model.isFolder && !hasLastDeploymentKey"
class="commit-title"> class="commit-title">
No deployments yet No deployments yet
</p> </div>
</td> </div>
<td> <div class="table-section section-10" role="gridcell">
<div
v-if="!model.isFolder"
class="table-mobile-header">
Updated
</div>
<span <span
v-if="!model.isFolder && canShowDate" v-if="!model.isFolder && canShowDate"
class="environment-created-date-timeago"> class="environment-created-date-timeago table-mobile-content">
{{createdDate}} {{createdDate}}
</span> </span>
</td> </div>
<td class="environments-actions"> <div class="table-section section-30 environments-actions table-button-footer" role="gridcell">
<div <div
v-if="!model.isFolder" v-if="!model.isFolder"
class="btn-group pull-right" class="btn-group environment-action-buttons"
role="group"> role="group">
<actions-component <actions-component
...@@ -553,6 +568,6 @@ export default { ...@@ -553,6 +568,6 @@ export default {
:retry-url="retryUrl" :retry-url="retryUrl"
/> />
</div> </div>
</td> </div>
</tr> </div>
</template> </template>
...@@ -19,7 +19,7 @@ export default { ...@@ -19,7 +19,7 @@ export default {
</script> </script>
<template> <template>
<a <a
class="btn monitoring-url has-tooltip" class="btn monitoring-url has-tooltip hidden-xs hidden-sm"
data-container="body" data-container="body"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
:href="monitoringUrl" :href="monitoringUrl"
......
...@@ -43,7 +43,7 @@ export default { ...@@ -43,7 +43,7 @@ export default {
<template> <template>
<button <button
type="button" type="button"
class="btn" class="btn hidden-xs hidden-sm"
@click="onClick" @click="onClick"
:disabled="isLoading"> :disabled="isLoading">
......
...@@ -47,7 +47,7 @@ export default { ...@@ -47,7 +47,7 @@ export default {
<template> <template>
<button <button
type="button" type="button"
class="btn stop-env-link has-tooltip" class="btn stop-env-link has-tooltip hidden-xs hidden-sm"
data-container="body" data-container="body"
@click="onClick" @click="onClick"
:disabled="isLoading" :disabled="isLoading"
......
...@@ -29,7 +29,7 @@ export default { ...@@ -29,7 +29,7 @@ export default {
</script> </script>
<template> <template>
<a <a
class="btn terminal-button has-tooltip" class="btn terminal-button has-tooltip hidden-xs hidden-sm"
data-container="body" data-container="body"
:title="title" :title="title"
:aria-label="title" :aria-label="title"
......
...@@ -45,32 +45,28 @@ export default { ...@@ -45,32 +45,28 @@ export default {
}; };
</script> </script>
<template> <template>
<table class="table ci-table"> <div class="ci-table" role="grid">
<thead> <div class="gl-responsive-table-row table-row-header" role="row">
<tr> <div class="table-section section-10 environments-name" role="rowheader">
<th class="environments-name">
Environment Environment
</th> </div>
<th class="environments-deploy"> <div class="table-section section-10 environments-deploy" role="rowheader">
Last deployment Deployment
</th> </div>
<th class="environments-build"> <div class="table-section section-15 environments-build" role="rowheader">
Job Job
</th> </div>
<th class="environments-commit"> <div class="table-section section-25 environments-commit" role="rowheader">
Commit Commit
</th> </div>
<th class="environments-date"> <div class="table-section section-10 environments-date" role="rowheader">
Updated Updated
</th> </div>
<th class="environments-actions"></th> </div>
</tr>
</thead>
<tbody>
<template <template
v-for="model in environments" v-for="model in environments"
v-bind:model="model"> v-bind:model="model">
<tr <div
is="environment-item" is="environment-item"
:model="model" :model="model"
:can-create-deployment="canCreateDeployment" :can-create-deployment="canCreateDeployment"
...@@ -78,14 +74,12 @@ export default { ...@@ -78,14 +74,12 @@ export default {
/> />
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0"> <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<tr v-if="isLoadingFolderContent"> <div v-if="isLoadingFolderContent">
<td colspan="6">
<loading-icon size="2" /> <loading-icon size="2" />
</td> </div>
</tr>
<template v-else> <template v-else>
<tr <div
is="environment-item" is="environment-item"
v-for="children in model.children" v-for="children in model.children"
:model="children" :model="children"
...@@ -93,20 +87,17 @@ export default { ...@@ -93,20 +87,17 @@ export default {
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
/> />
<tr> <div>
<td <div class="text-center prepend-top-10">
colspan="6"
class="text-center">
<a <a
:href="folderUrl(model)" :href="folderUrl(model)"
class="btn btn-default"> class="btn btn-default">
Show all Show all
</a> </a>
</td> </div>
</tr> </div>
</template> </template>
</template> </template>
</template> </template>
</tbody> </div>
</table>
</template> </template>
/* 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 */ /* 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) => { ((global) => {
var issuable_created; var issuable_created;
issuable_created = false; issuable_created = false;
global.Issuable = { global.IssuableIndex = {
init: function() { init: function(pagePrefix) {
Issuable.initTemplates(); IssuableIndex.initTemplates();
Issuable.initSearch(); IssuableIndex.initSearch();
Issuable.initChecks(); IssuableIndex.initBulkUpdate(pagePrefix);
Issuable.initResetFilters(); IssuableIndex.initResetFilters();
Issuable.resetIncomingEmailToken(); IssuableIndex.resetIncomingEmailToken();
return Issuable.initLabelFilterRemove(); IssuableIndex.initLabelFilterRemove();
}, },
initTemplates: function() { 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() { initSearch: function() {
const $searchInput = $('#issuable_search'); const $searchInput = $('#issuable_search');
Issuable.initSearchState($searchInput); IssuableIndex.initSearchState($searchInput);
// `immediate` param set to false debounces on the `trailing` edge, lets user finish typing // `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); $searchInput.off('keyup').on('keyup', debouncedExecSearch);
...@@ -37,16 +40,16 @@ ...@@ -37,16 +40,16 @@
initSearchState: function($searchInput) { initSearchState: function($searchInput) {
const currentSearchVal = $searchInput.val(); const currentSearchVal = $searchInput.val();
Issuable.searchState = { IssuableIndex.searchState = {
elem: $searchInput, elem: $searchInput,
current: currentSearchVal current: currentSearchVal
}; };
Issuable.maybeFocusOnSearch(); IssuableIndex.maybeFocusOnSearch();
}, },
accessSearchPristine: function(set) { accessSearchPristine: function(set) {
// store reference to previous value to prevent search on non-mutating keyup // 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(); const currentSearchVal = state.elem.val();
if (set) { if (set) {
...@@ -56,10 +59,10 @@ ...@@ -56,10 +59,10 @@
} }
}, },
maybeFocusOnSearch: function() { maybeFocusOnSearch: function() {
const currentSearchVal = Issuable.searchState.current; const currentSearchVal = IssuableIndex.searchState.current;
if (currentSearchVal && currentSearchVal !== '') { if (currentSearchVal && currentSearchVal !== '') {
const queryLength = currentSearchVal.length; 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 following ensures that the cursor is initially placed at
* the end of search input when focus is applied. It accounts * the end of search input when focus is applied. It accounts
...@@ -80,7 +83,7 @@ ...@@ -80,7 +83,7 @@
const $searchValue = $search.val(); const $searchValue = $search.val();
const $filtersForm = $('.js-filter-form'); const $filtersForm = $('.js-filter-form');
const $input = $(`input[name='${$searchName}']`, $filtersForm); const $input = $(`input[name='${$searchName}']`, $filtersForm);
const isPristine = Issuable.accessSearchPristine(); const isPristine = IssuableIndex.accessSearchPristine();
if (isPristine) { if (isPristine) {
return; return;
...@@ -92,7 +95,7 @@ ...@@ -92,7 +95,7 @@
$input.val($searchValue); $input.val($searchValue);
} }
Issuable.filterResults($filtersForm); IssuableIndex.filterResults($filtersForm);
}, },
initLabelFilterRemove: function() { initLabelFilterRemove: function() {
return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) { return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
...@@ -103,7 +106,7 @@ ...@@ -103,7 +106,7 @@
return this.value === $button.data('label'); return this.value === $button.data('label');
}).remove(); }).remove();
// Submit the form to get new data // Submit the form to get new data
Issuable.filterResults($('.filter-form')); IssuableIndex.filterResults($('.filter-form'));
}); });
}, },
filterResults: (function(_this) { filterResults: (function(_this) {
...@@ -132,38 +135,18 @@ ...@@ -132,38 +135,18 @@
gl.utils.visitUrl(baseIssuesUrl); gl.utils.visitUrl(baseIssuesUrl);
}); });
}, },
initChecks: function() { initBulkUpdate: function(pagePrefix) {
this.issuableBulkActions = $('.bulk-update').data('bulkActions'); const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
$('.check_all_issues').off('click').on('click', function() { const alreadyInitialized = !!this.bulkUpdateSidebar;
$('.selected_issue').prop('checked', this.checked);
return Issuable.checkChanged(); if (userCanBulkUpdate && !alreadyInitialized) {
}); IssuableBulkUpdateActions.init({
return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this)); prefixId: pagePrefix,
},
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');
}); });
$updateIssuesIds.val(ids);
$issuesOtherFilters.hide(); this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
$issuesBulkUpdate.show();
} else {
$updateIssuesIds.val([]);
$issuesBulkUpdate.hide();
$issuesOtherFilters.show();
} }
return true;
}, },
resetIncomingEmailToken: function() { resetIncomingEmailToken: function() {
$('.incoming-email-token-reset').on('click', function(e) { $('.incoming-email-token-reset').on('click', function(e) {
e.preventDefault(); 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 @@ ...@@ -2,6 +2,8 @@
/* global Issuable */ /* global Issuable */
/* global ListLabel */ /* global ListLabel */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
(function() { (function() {
this.LabelsSelect = (function() { this.LabelsSelect = (function() {
function LabelsSelect(els) { function LabelsSelect(els) {
...@@ -430,20 +432,15 @@ ...@@ -430,20 +432,15 @@
if ($('.selected_issue:checked').length) { if ($('.selected_issue:checked').length) {
return; 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() { LabelsSelect.prototype.enableBulkLabelDropdown = function() {
var issuableBulkActions; IssuableBulkUpdateActions.willUpdateLabels = true;
if ($('.selected_issue:checked').length) {
issuableBulkActions = $('.bulk-update').data('bulkActions');
return issuableBulkActions.willUpdateLabels = true;
}
}; };
LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) { LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) {
var i, markedIds, unmarkedIds, indeterminateIds; var i, markedIds, unmarkedIds, indeterminateIds;
var issuableBulkActions = $('.bulk-update').data('bulkActions');
markedIds = $dropdown.data('marked') || []; markedIds = $dropdown.data('marked') || [];
unmarkedIds = $dropdown.data('unmarked') || []; unmarkedIds = $dropdown.data('unmarked') || [];
...@@ -469,13 +466,13 @@ ...@@ -469,13 +466,13 @@
} }
// If an indeterminate item is being unmarked // If an indeterminate item is being unmarked
if (issuableBulkActions.getOriginalIndeterminateIds().indexOf(value) > -1) { if (IssuableBulkUpdateActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
unmarkedIds.push(value); unmarkedIds.push(value);
} }
// If a marked item is being unmarked // If a marked item is being unmarked
// (a marked item could also be a label that is present in all selection) // (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); unmarkedIds.push(value);
} }
} }
......
...@@ -104,12 +104,11 @@ import './group_label_subscription'; ...@@ -104,12 +104,11 @@ import './group_label_subscription';
import './groups_select'; import './groups_select';
import './header'; import './header';
import './importer_status'; import './importer_status';
import './issuable'; import './issuable_index';
import './issuable_context'; import './issuable_context';
import './issuable_form'; import './issuable_form';
import './issue'; import './issue';
import './issue_status_select'; import './issue_status_select';
import './issues_bulk_assignment';
import './label_manager'; import './label_manager';
import './labels'; import './labels';
import './labels_select'; import './labels_select';
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */
function highlightChanges($elm) {
$elm.addClass('highlight-changes');
setTimeout(() => $elm.removeClass('highlight-changes'), 10);
}
(function() { (function() {
this.ProjectNew = (function() { this.ProjectNew = (function() {
function ProjectNew() { function ProjectNew() {
this.toggleSettings = this.toggleSettings.bind(this); this.toggleSettings = this.toggleSettings.bind(this);
this.$selects = $('.features select'); this.$selects = $('.features select');
this.$repoSelects = this.$selects.filter('.js-repo-select'); this.$repoSelects = this.$selects.filter('.js-repo-select');
this.$projectSelects = this.$selects.not('.js-repo-select');
$('.project-edit-container').on('ajax:before', (function(_this) { $('.project-edit-container').on('ajax:before', (function(_this) {
return function() { return function() {
...@@ -26,6 +32,42 @@ ...@@ -26,6 +32,42 @@
if (!visibilityContainer) return; if (!visibilityContainer) return;
const visibilitySelect = new gl.VisibilitySelect(visibilityContainer); const visibilitySelect = new gl.VisibilitySelect(visibilityContainer);
visibilitySelect.init(); visibilitySelect.init();
const $visibilitySelect = $(visibilityContainer).find('select');
let projectVisibility = $visibilitySelect.val();
const PROJECT_VISIBILITY_PRIVATE = '0';
$visibilitySelect.on('change', () => {
const newProjectVisibility = $visibilitySelect.val();
if (projectVisibility !== newProjectVisibility) {
this.$projectSelects.each((idx, select) => {
const $select = $(select);
const $options = $select.find('option');
const values = $.map($options, e => e.value);
// if switched to "private", limit visibility options
if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) {
if ($select.val() !== values[0] && $select.val() !== values[1]) {
$select.val(values[1]).trigger('change');
highlightChanges($select);
}
$options.slice(2).disable();
}
// if switched from "private", increase visibility for non-disabled options
if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) {
$options.enable();
if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) {
$select.val(values[values.length - 1]).trigger('change');
highlightChanges($select);
}
}
});
projectVisibility = newProjectVisibility;
}
});
}; };
ProjectNew.prototype.toggleSettings = function() { ProjectNew.prototype.toggleSettings = function() {
...@@ -56,8 +98,10 @@ ...@@ -56,8 +98,10 @@
ProjectNew.prototype.toggleRepoVisibility = function () { ProjectNew.prototype.toggleRepoVisibility = function () {
var $repoAccessLevel = $('.js-repo-access-level select'); var $repoAccessLevel = $('.js-repo-access-level select');
var $lfsEnabledOption = $('.js-lfs-enabled select');
var containerRegistry = document.querySelectorAll('.js-container-registry')[0]; var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled'); var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
var prevSelectedVal = parseInt($repoAccessLevel.val(), 10);
this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']") this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
.nextAll() .nextAll()
...@@ -71,29 +115,40 @@ ...@@ -71,29 +115,40 @@
var $this = $(this); var $this = $(this);
var repoSelectVal = parseInt($this.val(), 10); var repoSelectVal = parseInt($this.val(), 10);
$this.find('option').show(); $this.find('option').enable();
if (selectedVal < repoSelectVal) { if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) {
$this.val(selectedVal); $this.val(selectedVal).trigger('change');
highlightChanges($this);
} }
$this.find("option[value='" + selectedVal + "']").nextAll().hide(); $this.find("option[value='" + selectedVal + "']").nextAll().disable();
}); });
if (selectedVal) { if (selectedVal) {
this.$repoSelects.removeClass('disabled'); this.$repoSelects.removeClass('disabled');
if ($lfsEnabledOption.length) {
$lfsEnabledOption.removeClass('disabled');
highlightChanges($lfsEnabledOption);
}
if (containerRegistry) { if (containerRegistry) {
containerRegistry.style.display = ''; containerRegistry.style.display = '';
} }
} else { } else {
this.$repoSelects.addClass('disabled'); this.$repoSelects.addClass('disabled');
if ($lfsEnabledOption.length) {
$lfsEnabledOption.val('false').addClass('disabled');
highlightChanges($lfsEnabledOption);
}
if (containerRegistry) { if (containerRegistry) {
containerRegistry.style.display = 'none'; containerRegistry.style.display = 'none';
containerRegistryCheckbox.checked = false; containerRegistryCheckbox.checked = false;
} }
} }
prevSelectedVal = selectedVal;
}.bind(this)); }.bind(this));
}; };
......
...@@ -135,8 +135,8 @@ export default { ...@@ -135,8 +135,8 @@ export default {
{{shortSha}} {{shortSha}}
</a> </a>
<p class="commit-title"> <div class="commit-title flex-truncate-parent">
<span v-if="title"> <span v-if="title" class="flex-truncate-child">
<user-avatar-link <user-avatar-link
v-if="hasAuthor" v-if="hasAuthor"
class="avatar-image-container" class="avatar-image-container"
...@@ -153,7 +153,7 @@ export default { ...@@ -153,7 +153,7 @@ export default {
<span v-else> <span v-else>
Cant find HEAD commit for this branch Cant find HEAD commit for this branch
</span> </span>
</p> </div>
</div> </div>
`, `,
}; };
...@@ -49,3 +49,4 @@ ...@@ -49,3 +49,4 @@
@import "framework/icons.scss"; @import "framework/icons.scss";
@import "framework/snippets.scss"; @import "framework/snippets.scss";
@import "framework/memory_graph.scss"; @import "framework/memory_graph.scss";
@import "framework/responsive-tables.scss";
...@@ -445,3 +445,9 @@ table { ...@@ -445,3 +445,9 @@ table {
word-wrap: break-word; word-wrap: break-word;
} }
} }
.disabled-content {
pointer-events: none;
opacity: .5;
}
...@@ -22,12 +22,6 @@ ...@@ -22,12 +22,6 @@
} }
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
.issues_bulk_update {
.dropdown-menu-toggle {
width: 132px;
}
}
.filter-item:not(:last-child) { .filter-item:not(:last-child) {
margin-right: 6px; margin-right: 6px;
} }
...@@ -376,12 +370,6 @@ ...@@ -376,12 +370,6 @@
padding: 0; 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) { @media (max-width: $screen-xs-max) {
.issues-details-filters { .issues-details-filters {
padding: 0 0 10px; padding: 0 0 10px;
......
...@@ -29,10 +29,6 @@ ...@@ -29,10 +29,6 @@
display: none; display: none;
} }
.issues-holder .issue-check {
display: none;
}
.rss-btn { .rss-btn {
display: none; display: none;
} }
......
@mixin flex-max-width($max) {
flex: 0 0 #{$max + '%'};
max-width: #{$max + '%'};
}
.gl-responsive-table-row {
margin-top: 10px;
border: 1px solid $border-color;
@media (min-width: $screen-md-min) {
padding: 15px 0;
margin: 0;
display: flex;
align-items: center;
border: none;
border-bottom: 1px solid $white-normal;
}
.table-section {
white-space: nowrap;
$section-widths: 10 15 25 30;
@each $width in $section-widths {
&.section-#{$width} {
flex: 0 0 #{$width + '%'};
@media (min-width: $screen-md-min) {
max-width: #{$width + '%'};
}
}
}
&:not(.table-button-footer) {
@media (max-width: $screen-sm-max) {
display: flex;
align-self: stretch;
padding: 10px;
align-items: center;
height: 62px;
&:not(:first-of-type) {
border-top: 1px solid $white-normal;
}
}
}
}
}
.table-row-header {
font-size: 13px;
@media (max-width: $screen-sm-max) {
display: none;
}
}
.table-mobile-header {
color: $gl-text-color-secondary;
@include flex-max-width(40);
@media (min-width: $screen-md-min) {
display: none;
}
}
.table-mobile-content {
@media (max-width: $screen-sm-max) {
@include flex-max-width(60);
text-align: right;
}
}
.flex-truncate-parent {
display: flex;
}
.flex-truncate-child {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@media (min-width: $screen-md-min) {
flex: 0 0 90%;
}
}
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
padding-right: 0; padding-right: 0;
@media (min-width: $screen-sm-min) { @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; padding-right: $gutter_collapsed_width;
} }
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
z-index: 300; z-index: 300;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { @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; padding-right: $gutter_collapsed_width;
} }
} }
...@@ -88,3 +88,35 @@ ...@@ -88,3 +88,35 @@
min-height: 100%; 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;
}
}
...@@ -187,6 +187,7 @@ $divergence-graph-bar-bg: #ccc; ...@@ -187,6 +187,7 @@ $divergence-graph-bar-bg: #ccc;
$divergence-graph-separator-bg: #ccc; $divergence-graph-separator-bg: #ccc;
$general-hover-transition-duration: 100ms; $general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear; $general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232);
/* /*
......
...@@ -11,34 +11,7 @@ ...@@ -11,34 +11,7 @@
} }
.environments-container { .environments-container {
.table-holder { .ci-table {
width: 100%;
@media (max-width: $screen-sm-max) {
overflow: auto;
}
}
.table.ci-table {
.environments-actions {
min-width: 300px;
}
.environments-commit,
.environments-actions {
width: 20%;
}
.environments-date {
width: 10%;
}
.environments-name,
.environments-deploy,
.environments-build {
width: 15%;
}
.deployment-column { .deployment-column {
> span { > span {
word-break: break-all; word-break: break-all;
...@@ -150,6 +123,49 @@ ...@@ -150,6 +123,49 @@
} }
} }
.gl-responsive-table-row {
.environments-actions {
@media (min-width: $screen-md-min) {
text-align: right;
}
@media (max-width: $screen-sm-max) {
background-color: $gray-normal;
align-self: stretch;
border-top: 1px solid $border-color;
.environment-action-buttons {
padding: 10px;
display: flex;
.btn {
border-radius: 3px;
}
> .btn-group,
.external-url {
flex: 1;
flex-basis: 28px;
}
.dropdown-new {
width: 100%;
}
}
}
}
}
.folder-row {
padding: 15px 0;
border-bottom: 1px solid $white-normal;
@media (max-width: $screen-sm-max) {
border-top: 1px solid $white-normal;
margin-top: 10px;
}
}
.prometheus-graph { .prometheus-graph {
text { text {
fill: $gl-text-color; fill: $gl-text-color;
......
...@@ -29,6 +29,20 @@ ...@@ -29,6 +29,20 @@
& > .form-group { & > .form-group {
padding-left: 0; padding-left: 0;
} }
select option[disabled] {
display: none;
}
}
select {
background: transparent;
transition: background 2s ease-out;
&.highlight-changes {
background: $highlight-changes-color;
transition: none;
}
} }
.help-block { .help-block {
......
class EventsFinder
attr_reader :source, :params, :current_user
# Used to filter Events
#
# Arguments:
# source - which user or project to looks for events on
# current_user - only return events for projects visible to this user
# params:
# action: string
# target_type: string
# before: datetime
# after: datetime
#
def initialize(params = {})
@source = params.delete(:source)
@current_user = params.delete(:current_user)
@params = params
end
def execute
events = source.events
events = by_current_user_access(events)
events = by_action(events)
events = by_target_type(events)
events = by_created_at_before(events)
events = by_created_at_after(events)
events
end
private
def by_current_user_access(events)
events.merge(ProjectsFinder.new(current_user: current_user).execute).references(:project)
end
def by_action(events)
return events unless Event::ACTIONS[params[:action]]
events.where(action: Event::ACTIONS[params[:action]])
end
def by_target_type(events)
return events unless Event::TARGET_TYPES[params[:target_type]]
events.where(target_type: Event::TARGET_TYPES[params[:target_type]])
end
def by_created_at_before(events)
return events unless params[:before]
events.where('events.created_at < ?', params[:before].beginning_of_day)
end
def by_created_at_after(events)
return events unless params[:after]
events.where('events.created_at > ?', params[:after].end_of_day)
end
end
...@@ -138,11 +138,15 @@ module ProjectsHelper ...@@ -138,11 +138,15 @@ module ProjectsHelper
if @project.private? if @project.private?
level = @project.project_feature.send(field) level = @project.project_feature.send(field)
options.delete('Everyone with access') disabled_option = ProjectFeature::ENABLED
highest_available_option = options.values.max if level == ProjectFeature::ENABLED highest_available_option = ProjectFeature::PRIVATE if level == disabled_option
end end
options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field)) options = options_for_select(
options,
selected: highest_available_option || @project.project_feature.public_send(field),
disabled: disabled_option
)
content_tag( content_tag(
:select, :select,
......
...@@ -31,9 +31,9 @@ module VisibilityLevelHelper ...@@ -31,9 +31,9 @@ module VisibilityLevelHelper
when Gitlab::VisibilityLevel::PRIVATE when Gitlab::VisibilityLevel::PRIVATE
"Project access must be granted explicitly to each user." "Project access must be granted explicitly to each user."
when Gitlab::VisibilityLevel::INTERNAL when Gitlab::VisibilityLevel::INTERNAL
"The project can be cloned by any logged in user." "The project can be accessed by any logged in user."
when Gitlab::VisibilityLevel::PUBLIC when Gitlab::VisibilityLevel::PUBLIC
"The project can be cloned without any authentication." "The project can be accessed without any authentication."
end end
end end
......
...@@ -14,6 +14,30 @@ class Event < ActiveRecord::Base ...@@ -14,6 +14,30 @@ class Event < ActiveRecord::Base
DESTROYED = 10 DESTROYED = 10
EXPIRED = 11 # User left project due to expiry EXPIRED = 11 # User left project due to expiry
ACTIONS = HashWithIndifferentAccess.new(
created: CREATED,
updated: UPDATED,
closed: CLOSED,
reopened: REOPENED,
pushed: PUSHED,
commented: COMMENTED,
merged: MERGED,
joined: JOINED,
left: LEFT,
destroyed: DESTROYED,
expired: EXPIRED
).freeze
TARGET_TYPES = HashWithIndifferentAccess.new(
issue: Issue,
milestone: Milestone,
merge_request: MergeRequest,
note: Note,
project: Project,
snippet: Snippet,
user: User
).freeze
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
...@@ -55,6 +79,14 @@ class Event < ActiveRecord::Base ...@@ -55,6 +79,14 @@ class Event < ActiveRecord::Base
def limit_recent(limit = 20, offset = nil) def limit_recent(limit = 20, offset = nil)
recent.limit(limit).offset(offset) recent.limit(limit).offset(offset)
end end
def actions
ACTIONS.keys
end
def target_types
TARGET_TYPES.keys
end
end end
def visible_to_user?(user = nil) def visible_to_user?(user = nil)
......
class PagesDomain < ActiveRecord::Base class PagesDomain < ActiveRecord::Base
belongs_to :project belongs_to :project
validates :domain, hostname: true validates :domain, hostname: { allow_numeric_hostname: true }
validates :domain, uniqueness: { case_sensitive: false } validates :domain, uniqueness: { case_sensitive: false }
validates :certificate, certificate: true, allow_nil: true, allow_blank: true validates :certificate, certificate: true, allow_nil: true, allow_blank: true
validates :key, certificate_key: true, allow_nil: true, allow_blank: true validates :key, certificate_key: true, allow_nil: true, allow_blank: true
...@@ -98,7 +98,7 @@ class PagesDomain < ActiveRecord::Base ...@@ -98,7 +98,7 @@ class PagesDomain < ActiveRecord::Base
def validate_pages_domain def validate_pages_domain
return unless domain return unless domain
if domain.downcase.ends_with?(".#{Settings.pages.host}".downcase) if domain.downcase.ends_with?(Settings.pages.host.downcase)
self.errors.add(:domain, "*.#{Settings.pages.host} is restricted") self.errors.add(:domain, "*.#{Settings.pages.host} is restricted")
end end
end end
......
...@@ -16,7 +16,7 @@ module Users ...@@ -16,7 +16,7 @@ module Users
def record_activity def record_activity
Gitlab::UserActivities.record(@author.id) Gitlab::UserActivities.record(@author.id)
Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username}") Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username})")
end end
end end
end end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- if !project.empty_repo? && can?(current_user, :download_code, project) - if !project.empty_repo? && can?(current_user, :download_code, project)
.project-action-button.dropdown.inline> .project-action-button.dropdown.inline>
%button.btn{ 'data-toggle' => 'dropdown' } %button.btn.has-tooltip{ title: 'Download', 'data-toggle' => 'dropdown', 'aria-label' => 'Download' }
= icon('download') = icon('download')
= icon("caret-down") = icon("caret-down")
%span.sr-only %span.sr-only
......
- if current_user - if current_user
.project-action-button.dropdown.inline .project-action-button.dropdown.inline
%a.btn.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" } %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: 'Create new...', 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => 'Create new...' }
= icon('plus') = icon('plus')
= icon("caret-down") = icon("caret-down")
%ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
= custom_icon('icon_fork') = custom_icon('icon_fork')
%span Fork %span Fork
- else - else
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn' do = link_to new_namespace_project_fork_path(@project.namespace, @project), class: 'btn' do
= custom_icon('icon_fork') = custom_icon('icon_fork')
%span Fork %span Fork
.count-with-arrow .count-with-arrow
......
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
.col-md-9 .col-md-9
.label-light .label-light
= label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level
= link_to "(?)", help_page_path("public_access/public_access") = link_to icon('question-circle'), help_page_path("public_access/public_access")
%span.help-block %span.help-block
.col-md-3.visibility-select-container .col-md-3.visibility-select-container
= render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level) = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
...@@ -92,14 +92,14 @@ ...@@ -92,14 +92,14 @@
.form-group .form-group
= render 'shared/allow_request_access', form: f = render 'shared/allow_request_access', form: f
- if Gitlab.config.lfs.enabled && current_user.admin? - if Gitlab.config.lfs.enabled && current_user.admin?
.row .row.js-lfs-enabled
.col-md-9 .col-md-9
= f.label :lfs_enabled, 'LFS', class: 'label-light' = f.label :lfs_enabled, 'LFS', class: 'label-light'
%span.help-block %span.help-block
Git Large File Storage Git Large File Storage
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
.col-md-3 .col-md-3
= f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control', data: { field: 'lfs_enabled' } = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select', data: { field: 'lfs_enabled' }
- if Gitlab.config.registry.enabled - if Gitlab.config.registry.enabled
......
%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } } %li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
.issue-box .issue-box
- if @bulk_edit - if @can_bulk_update
.issue-check .issue-check.hidden
= check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
.issue-info-container .issue-info-container
.issue-title.title .issue-title.title
......
- @no_container = true - @no_container = true
- @bulk_edit = can?(current_user, :admin_issue, @project) - @can_bulk_update = can?(current_user, :admin_issue, @project)
- page_title "Issues" - page_title "Issues"
- new_issue_email = @project.new_issue_address(current_user) - new_issue_email = @project.new_issue_address(current_user)
...@@ -20,6 +20,8 @@ ...@@ -20,6 +20,8 @@
.nav-controls .nav-controls
= link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do = link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss') = icon('rss')
- 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, = link_to new_namespace_project_issue_path(@project.namespace,
@project, @project,
issue: { assignee_id: issues_finder.assignee.try(:id), issue: { assignee_id: issues_finder.assignee.try(:id),
...@@ -30,6 +32,9 @@ ...@@ -30,6 +32,9 @@
New issue New issue
= render 'shared/issuable/search_bar', type: :issues = render 'shared/issuable/search_bar', type: :issues
- if @can_bulk_update
= render 'shared/issuable/bulk_update_sidebar', type: :issues
.issues-holder .issues-holder
= render 'issues' = render 'issues'
- if new_issue_email - 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 } } %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 - if @can_bulk_update
.issue-check .issue-check.hidden
= check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue" = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
.issue-info-container .issue-info-container
......
- @no_container = true - @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" - page_title "Merge Requests"
- unless @project.default_issues_tracker? - unless @project.default_issues_tracker?
...@@ -18,6 +18,8 @@ ...@@ -18,6 +18,8 @@
.top-area .top-area
= render 'shared/issuable/nav', type: :merge_requests = render 'shared/issuable/nav', type: :merge_requests
.nav-controls .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)) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- if merge_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 = 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 @@ ...@@ -25,6 +27,9 @@
= render 'shared/issuable/search_bar', type: :merge_requests = render 'shared/issuable/search_bar', type: :merge_requests
- if @can_bulk_update
= render 'shared/issuable/bulk_update_sidebar', type: :merge_requests
.merge-requests-holder .merge-requests-holder
= render 'merge_requests' = render 'merge_requests'
- else - else
......
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
.form-group.project-visibility-level-holder .form-group.project-visibility-level-holder
= f.label :visibility_level, class: 'label-light' do = f.label :visibility_level, class: 'label-light' do
Visibility Level Visibility Level
= link_to icon('question-circle'), help_page_path("public_access/public_access") = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }
= render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
......
- noteable = @sent_notification.noteable - noteable = @sent_notification.noteable
- noteable_type = @sent_notification.noteable_type.humanize(capitalize: false) - noteable_type = @sent_notification.noteable_type.titleize.downcase
- noteable_text = %(#{noteable.title} (#{noteable.to_reference})) - noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.name_with_namespace
- page_title "Unsubscribe", noteable_text, @sent_notification.noteable_type.humanize.pluralize, @sent_notification.project.name_with_namespace
%h3.page-title %h3.page-title
Unsubscribe from #{noteable_type} #{noteable_text} Unsubscribe from #{noteable_type}
%p %p
= succeed '?' do = succeed '?' do
Are you sure you want to unsubscribe from #{noteable_type} Are you sure you want to unsubscribe from the #{noteable_type}:
= link_to noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable]) = link_to noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable])
%p %p
......
- 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]
...@@ -6,10 +6,6 @@ ...@@ -6,10 +6,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 = 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? - if params[:search].present?
= hidden_field_tag :search, params[:search] = 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 .issues-other-filters
.filter-item.inline .filter-item.inline
- if params[:author_id].present? - if params[:author_id].present?
...@@ -36,35 +32,6 @@ ...@@ -36,35 +32,6 @@
.pull-right .pull-right
= render 'shared/sort_dropdown' = 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? - has_labels = @labels && @labels.any?
.row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) } .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
- if has_labels - if has_labels
......
...@@ -11,6 +11,8 @@ ...@@ -11,6 +11,8 @@
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label") - 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 = {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) - 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-extra-options' if extra_options
- classes << 'js-filter-submit' if filter_submit - classes << 'js-filter-submit' if filter_submit
...@@ -20,8 +22,9 @@ ...@@ -20,8 +22,9 @@
.dropdown .dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect{ class: classes.join(' '), type: "button", data: dropdown_data } %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?) } - apply_is_default_styles = (selected.nil? || selected.empty?) && !no_default_styles
= multi_label_name(selected, "Labels") %span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) }
= multi_label_name(selected, label_name)
= icon('chevron-down') = icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable .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 } = render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create }
......
...@@ -6,10 +6,9 @@ ...@@ -6,10 +6,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 = 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? - if params[:search].present?
= hidden_field_tag :search, params[:search] = hidden_field_tag :search, params[:search]
- if @bulk_edit - if @can_bulk_update
.check-all-holder .check-all-holder.hidden
= check_box_tag "check_all_issues", nil, false, = check_box_tag "check-all-issues", nil, false, class: "check-all-issues left"
class: "check_all_issues left"
.issues-other-filters.filtered-search-wrapper .issues-other-filters.filtered-search-wrapper
.filtered-search-box .filtered-search-box
- if type != :boards_modal && type != :boards - if type != :boards_modal && type != :boards
...@@ -110,55 +109,11 @@ ...@@ -110,55 +109,11 @@
- elsif type != :boards_modal - elsif type != :boards_modal
= render 'shared/sort_dropdown' = 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: "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 - unless type === :boards_modal
:javascript :javascript
new LabelsSelect();
new MilestoneSelect();
new IssueStatusSelect();
new SubscriptionSelect();
$(document).off('page:restore').on('page:restore', function (event) { $(document).off('page:restore').on('page:restore', function (event) {
if (gl.FilteredSearchManager) { if (gl.FilteredSearchManager) {
const filteredSearchManager = new gl.FilteredSearchManager(); const filteredSearchManager = new gl.FilteredSearchManager();
filteredSearchManager.setup(); filteredSearchManager.setup();
} }
Issuable.init();
new gl.IssuableBulkActions({
prefixId: 'issue_',
});
}); });
...@@ -6,14 +6,14 @@ ...@@ -6,14 +6,14 @@
.js-notification-toggle-btns .js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) } %div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom? - if notification_setting.custom?
%button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting) } } %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting) } }
= icon("bell", class: "js-notification-loading") = icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level) = notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } } %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
= icon('caret-down') = icon('caret-down')
.sr-only Toggle dropdown .sr-only Toggle dropdown
- else - else
%button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } } %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
= icon("bell", class: "js-notification-loading") = icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level) = notification_title(notification_setting.level)
= icon("caret-down") = icon("caret-down")
......
---
title: Introduce an Events API
merge_request: 11755
author:
---
title: Automatically adjust project settings to match changes in project visibility
merge_request: 11831
author:
---
title: Add a Rake task to aid in rotating otp_key_base
merge_request: 11881
author:
---
title: 'Introduce optimistic locking support via optional parameter last_commit_sha on File Update API'
merge_request: 11694
author: electroma
---
title: Allow numeric pages domain
merge_request: 11550
author:
---
title: Fixed style on unsubscribe page
merge_request:
author: Gustav Ernberg
# GitLab Container Registry administration # GitLab Container Registry administration
> [Introduced][ce-4040] in GitLab 8.8.
---
> **Notes:** > **Notes:**
- [Introduced][ce-4040] in GitLab 8.8.
- Container Registry manifest `v1` support was added in GitLab 8.9 to support - Container Registry manifest `v1` support was added in GitLab 8.9 to support
Docker versions earlier than 1.10. Docker versions earlier than 1.10.
- This document is about the admin guide. To learn how to use GitLab Container - This document is about the admin guide. To learn how to use GitLab Container
...@@ -568,12 +565,25 @@ notifications: ...@@ -568,12 +565,25 @@ notifications:
backoff: 1000 backoff: 1000
``` ```
## Changelog ## Using self-signed certificates with Container Registry
If you're using a self-signed certificate with your Container Registry, you
might encounter issues during the CI jobs like the following:
```
Error response from daemon: Get registry.example.com/v1/users/: x509: certificate signed by unknown authority
```
**GitLab 8.8 ([source docs][8-8-docs])** The Docker daemon running the command expects a cert signed by a recognized CA,
thus the error above.
- GitLab Container Registry feature was introduced. While GitLab doesn't support using self-signed certificates with Container
Registry out of the box, it is possible to make it work if you follow
[Docker's documentation][docker-insecure]. You may find some additional
information in [issue 18239][ce-18239].
[ce-18239]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18239
[docker-insecure]: https://docs.docker.com/registry/insecure/#using-self-signed-certificates
[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure [reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure
[restart gitlab]: restart_gitlab.md#installations-from-source [restart gitlab]: restart_gitlab.md#installations-from-source
[wildcard certificate]: https://en.wikipedia.org/wiki/Wildcard_certificate [wildcard certificate]: https://en.wikipedia.org/wiki/Wildcard_certificate
......
...@@ -15,6 +15,8 @@ following locations: ...@@ -15,6 +15,8 @@ following locations:
- [Commits](commits.md) - [Commits](commits.md)
- [Deployments](deployments.md) - [Deployments](deployments.md)
- [Deploy Keys](deploy_keys.md) - [Deploy Keys](deploy_keys.md)
- [Environments](environments.md)
- [Events](events.md)
- [Gitignores templates](templates/gitignores.md) - [Gitignores templates](templates/gitignores.md)
- [GitLab CI Config templates](templates/gitlab_ci_ymls.md) - [GitLab CI Config templates](templates/gitlab_ci_ymls.md)
- [Groups](groups.md) - [Groups](groups.md)
......
This diff is collapsed.
...@@ -310,143 +310,7 @@ GET /projects/:id/users ...@@ -310,143 +310,7 @@ GET /projects/:id/users
### Get project events ### Get project events
Get the events for the specified project sorted from newest to oldest. This Please refer to the [Events API documentation](events.md#list-a-projects-visible-events)
endpoint can be accessed without authentication if the project is publicly
accessible.
```
GET /projects/:id/events
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
```json
[
{
"title": null,
"project_id": 15,
"action_name": "closed",
"target_id": 830,
"target_type": "Issue",
"author_id": 1,
"data": null,
"target_title": "Public project search field",
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "root"
},
{
"title": null,
"project_id": 15,
"action_name": "opened",
"target_id": null,
"target_type": null,
"author_id": 1,
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "john",
"data": {
"before": "50d4420237a9de7be1304607147aec22e4a14af7",
"after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
"ref": "refs/heads/master",
"user_id": 1,
"user_name": "Dmitriy Zaporozhets",
"repository": {
"name": "gitlabhq",
"url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
"description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
"homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
},
"commits": [
{
"id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
"message": "Add simple search to projects in public area",
"timestamp": "2013-05-13T18:18:08+00:00",
"url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
"author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}
],
"total_commits_count": 1
},
"target_title": null
},
{
"title": null,
"project_id": 15,
"action_name": "closed",
"target_id": 840,
"target_type": "Issue",
"author_id": 1,
"data": null,
"target_title": "Finish & merge Code search PR",
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "root"
},
{
"title": null,
"project_id": 15,
"action_name": "commented on",
"target_id": 1312,
"target_type": "Note",
"author_id": 1,
"data": null,
"target_title": null,
"created_at": "2015-12-04T10:33:58.089Z",
"note": {
"id": 1312,
"body": "What an awesome day!",
"attachment": null,
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"created_at": "2015-12-04T10:33:56.698Z",
"system": false,
"noteable_id": 377,
"noteable_type": "Issue"
},
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "root"
}
]
```
### Create project ### Create project
......
...@@ -111,6 +111,7 @@ Parameters: ...@@ -111,6 +111,7 @@ Parameters:
- `author_name` (optional) - Specify the commit author's name - `author_name` (optional) - Specify the commit author's name
- `content` (required) - New file content - `content` (required) - New file content
- `commit_message` (required) - Commit message - `commit_message` (required) - Commit message
- `last_commit_id` (optional) - Last known file commit id
If the commit fails for any reason we return a 400 error with a non-specific If the commit fails for any reason we return a 400 error with a non-specific
error message. Possible causes for a failed commit include: error message. Possible causes for a failed commit include:
......
...@@ -701,147 +701,8 @@ Will return `201 OK` on success, `404 User Not Found` is user cannot be found or ...@@ -701,147 +701,8 @@ Will return `201 OK` on success, `404 User Not Found` is user cannot be found or
### Get user contribution events ### Get user contribution events
Get the contribution events for the specified user, sorted from newest to oldest. Please refer to the [Events API documentation](events.md#get-user-contribution-events)
```
GET /users/:id/events
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/:id/events
```
Example response:
```json
[
{
"title": null,
"project_id": 15,
"action_name": "closed",
"target_id": 830,
"target_type": "Issue",
"author_id": 1,
"data": null,
"target_title": "Public project search field",
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "root"
},
{
"title": null,
"project_id": 15,
"action_name": "opened",
"target_id": null,
"target_type": null,
"author_id": 1,
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "john",
"data": {
"before": "50d4420237a9de7be1304607147aec22e4a14af7",
"after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
"ref": "refs/heads/master",
"user_id": 1,
"user_name": "Dmitriy Zaporozhets",
"repository": {
"name": "gitlabhq",
"url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
"description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
"homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
},
"commits": [
{
"id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
"message": "Add simple search to projects in public area",
"timestamp": "2013-05-13T18:18:08+00:00",
"url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
"author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}
],
"total_commits_count": 1
},
"target_title": null
},
{
"title": null,
"project_id": 15,
"action_name": "closed",
"target_id": 840,
"target_type": "Issue",
"author_id": 1,
"data": null,
"target_title": "Finish & merge Code search PR",
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "root"
},
{
"title": null,
"project_id": 15,
"action_name": "commented on",
"target_id": 1312,
"target_type": "Note",
"author_id": 1,
"data": null,
"target_title": null,
"created_at": "2015-12-04T10:33:58.089Z",
"note": {
"id": 1312,
"body": "What an awesome day!",
"attachment": null,
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"created_at": "2015-12-04T10:33:56.698Z",
"system": false,
"noteable_id": 377,
"noteable_type": "Issue"
},
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "root"
}
]
```
## Get all impersonation tokens of a user ## Get all impersonation tokens of a user
......
# Analyze project code quality with Code Climate CLI # Analyze project code quality with Code Climate CLI
This example shows how to run [Code Climate CLI][cli] on your code by using\ This example shows how to run [Code Climate CLI][cli] on your code by using
GitLab CI and Docker. GitLab CI and Docker.
First, you need GitLab Runner with [docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor). First, you need GitLab Runner with [docker-in-docker executor][dind].
Once you setup the Runner add new job to `.gitlab-ci.yml`: Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `codeclimate`:
```yaml ```yaml
codeclimate: codeclimate:
...@@ -25,4 +25,10 @@ codeclimate: ...@@ -25,4 +25,10 @@ codeclimate:
This will create a `codeclimate` job in your CI pipeline and will allow you to This will create a `codeclimate` job in your CI pipeline and will allow you to
download and analyze the report artifact in JSON format. download and analyze the report artifact in JSON format.
For GitLab [Enterprise Edition Starter][ee] users, this information can be automatically
extracted and shown right in the merge request widget. [Learn more on code quality
diffs in merge requests](http://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.md).
[cli]: https://github.com/codeclimate/codeclimate [cli]: https://github.com/codeclimate/codeclimate
[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor
[ee]: https://about.gitlab.com/gitlab-ee/
...@@ -120,7 +120,7 @@ The YAML-defined variables are also set to all created ...@@ -120,7 +120,7 @@ The YAML-defined variables are also set to all created
tune them. tune them.
Variables can be defined at a global level, but also at a job level. To turn off Variables can be defined at a global level, but also at a job level. To turn off
global defined variables in your job, define an empty array: global defined variables in your job, define an empty hash:
```yaml ```yaml
job_name: job_name:
...@@ -345,20 +345,45 @@ All variables are set as environment variables in the build environment, and ...@@ -345,20 +345,45 @@ All variables are set as environment variables in the build environment, and
they are accessible with normal methods that are used to access such variables. they are accessible with normal methods that are used to access such variables.
In most cases `bash` or `sh` is used to execute the job script. In most cases `bash` or `sh` is used to execute the job script.
To access the variables (predefined and user-defined) in a `bash`/`sh` environment, To access environment variables, use the syntax for your Runner's [shell][shellexecutors].
prefix the variable name with the dollar sign (`$`):
``` | Shell | Usage |
|----------------------|-----------------|
| bash/sh | `$variable` |
| windows batch | `%variable%` |
| PowerShell | `$env:variable` |
To access environment variables in bash, prefix the variable name with (`$`):
```yaml
job_name: job_name:
script: script:
- echo $CI_JOB_ID - echo $CI_JOB_ID
``` ```
To access environment variables in **Windows Batch**, surround the variable
with (`%`):
```yaml
job_name:
script:
- echo %CI_JOB_ID%
```
To access environment variables in a **Windows PowerShell** environment, prefix
the variable name with (`$env:`):
```yaml
job_name:
script:
- echo $env:CI_JOB_ID
```
You can also list all environment variables with the `export` command, You can also list all environment variables with the `export` command,
but be aware that this will also expose the values of all the secret variables but be aware that this will also expose the values of all the secret variables
you set, in the job log: you set, in the job log:
``` ```yaml
job_name: job_name:
script: script:
- export - export
...@@ -405,3 +430,4 @@ export CI_REGISTRY_PASSWORD="longalfanumstring" ...@@ -405,3 +430,4 @@ export CI_REGISTRY_PASSWORD="longalfanumstring"
[triggers]: ../triggers/README.md#pass-job-variables-to-a-trigger [triggers]: ../triggers/README.md#pass-job-variables-to-a-trigger
[protected branches]: ../../user/project/protected_branches.md [protected branches]: ../../user/project/protected_branches.md
[protected tags]: ../../user/project/protected_tags.md [protected tags]: ../../user/project/protected_tags.md
[shellexecutors]: https://docs.gitlab.com/runner/executors/
...@@ -297,6 +297,15 @@ cache: ...@@ -297,6 +297,15 @@ cache:
untracked: true untracked: true
``` ```
If you use **Windows PowerShell** to run your shell scripts you need to replace
`$` with `$env:`:
```yaml
cache:
key: "$env:CI_JOB_STAGE/$env:CI_COMMIT_REF_NAME"
untracked: true
```
## Jobs ## Jobs
`.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job `.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job
...@@ -434,7 +443,7 @@ but allows you to define job-specific variables. ...@@ -434,7 +443,7 @@ but allows you to define job-specific variables.
When the `variables` keyword is used on a job level, it overrides the global YAML When the `variables` keyword is used on a job level, it overrides the global YAML
job variables and predefined ones. To turn off global defined variables job variables and predefined ones. To turn off global defined variables
in your job, define an empty array: in your job, define an empty hash:
```yaml ```yaml
job_name: job_name:
...@@ -909,6 +918,16 @@ job: ...@@ -909,6 +918,16 @@ job:
untracked: true untracked: true
``` ```
If you use **Windows PowerShell** to run your shell scripts you need to replace
`$` with `$env:`:
```yaml
job:
artifacts:
name: "$env:CI_JOB_STAGE_$env:CI_COMMIT_REF_NAME"
untracked: true
```
#### artifacts:when #### artifacts:when
> Introduced in GitLab 8.9 and GitLab Runner v1.3.0. > Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
......
...@@ -12,7 +12,7 @@ Both EE and CE require some add-on components called gitlab-shell and Gitaly. Th ...@@ -12,7 +12,7 @@ Both EE and CE require some add-on components called gitlab-shell and Gitaly. Th
You can imagine GitLab as a physical office. You can imagine GitLab as a physical office.
**The repositories** are the goods GitLab handling. **The repositories** are the goods GitLab handles.
They can be stored in a warehouse. They can be stored in a warehouse.
This can be either a hard disk, or something more complex, such as a NFS filesystem; This can be either a hard disk, or something more complex, such as a NFS filesystem;
...@@ -54,7 +54,7 @@ To serve repositories over SSH there's an add-on application called gitlab-shell ...@@ -54,7 +54,7 @@ To serve repositories over SSH there's an add-on application called gitlab-shell
### Components ### Components
![GitLab Diagram Overview](gitlab_architecture_diagram.png) <img src="https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/pub?w=987&amp;h=797">
_[edit diagram (for GitLab team members only)](https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/edit)_ _[edit diagram (for GitLab team members only)](https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/edit)_
...@@ -66,7 +66,9 @@ When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to reso ...@@ -66,7 +66,9 @@ When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to reso
The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories through Gitaly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access. The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories through Gitaly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access.
Gitaly executes git operations from gitlab-shell and Workhorse, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files) Gitaly executes git operations from gitlab-shell and the GitLab web app, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files).
You may also be interested in the [production architecture of GitLab.com](https://about.gitlab.com/handbook/infrastructure/production-architecture/).
### Installation Folder Summary ### Installation Folder Summary
......
...@@ -207,7 +207,9 @@ its class in an annotation. ...@@ -207,7 +207,9 @@ its class in an annotation.
>**Note:** >**Note:**
The Ingress alone doesn't expose GitLab externally. You need to have a Ingress controller setup to do that. The Ingress alone doesn't expose GitLab externally. You need to have a Ingress controller setup to do that.
Setting up an Ingress controller can be done by installing the `nginx-ingress` helm chart. But be sure Setting up an Ingress controller can be done by installing the `nginx-ingress` helm chart. But be sure
to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md) to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md).
>**Note:**
If you would like to use the Registry, you will also need to ensure your Ingress supports a [sufficiently large request size](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size).
#### Preserving Source IPs #### Preserving Source IPs
......
...@@ -141,7 +141,7 @@ Once you [have configured](#configuration) GitLab Runner in your `values.yml` fi ...@@ -141,7 +141,7 @@ Once you [have configured](#configuration) GitLab Runner in your `values.yml` fi
run the following: run the following:
```bash ```bash
helm install --namepace <NAMEPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE> gitlab/gitlab-runner helm install --namespace <NAMESPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE> gitlab/gitlab-runner
``` ```
- `<NAMESPACE>` is the Kubernetes namespace where you want to install the GitLab Runner. - `<NAMESPACE>` is the Kubernetes namespace where you want to install the GitLab Runner.
...@@ -153,7 +153,7 @@ helm install --namepace <NAMEPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE> ...@@ -153,7 +153,7 @@ helm install --namepace <NAMEPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE>
Once your GitLab Runner Chart is installed, configuration changes and chart updates should we done using `helm upgrade` Once your GitLab Runner Chart is installed, configuration changes and chart updates should we done using `helm upgrade`
```bash ```bash
helm upgrade --namepace <NAMEPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab-runner helm upgrade --namespace <NAMESPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab-runner
``` ```
Where: Where:
......
...@@ -71,6 +71,85 @@ sudo gitlab-rake gitlab:two_factor:disable_for_all_users ...@@ -71,6 +71,85 @@ sudo gitlab-rake gitlab:two_factor:disable_for_all_users
bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
``` ```
## Rotate Two-factor Authentication (2FA) encryption key
GitLab stores the secret data enabling 2FA to work in an encrypted database
column. The encryption key for this data is known as `otp_key_base`, and is
stored in `config/secrets.yml`.
If that file is leaked, but the individual 2FA secrets have not, it's possible
to re-encrypt those secrets with a new encryption key. This allows you to change
the leaked key without forcing all users to change their 2FA details.
First, look up the old key. This is in the `config/secrets.yml` file, but
**make sure you're working with the production section**. The line you're
interested in will look like this:
```yaml
production:
otp_key_base: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
```
Next, generate a new secret:
```
# omnibus-gitlab
sudo gitlab-rake secret
# installation from source
bundle exec rake secret RAILS_ENV=production
```
Now you need to stop the GitLab server, back up the existing secrets file and
update the database:
```
# omnibus-gitlab
sudo gitlab-ctl stop
sudo cp config/secrets.yml config/secrets.yml.bak
sudo gitlab-rake gitlab:two_factor:rotate_key:apply filename=backup.csv old_key=<old key> new_key=<new key>
# installation from source
sudo /etc/init.d/gitlab stop
cp config/secrets.yml config/secrets.yml.bak
bundle exec rake gitlab:two_factor:rotate_key:apply filename=backup.csv old_key=<old key> new_key=<new key> RAILS_ENV=production
```
The `<old key>` value can be read from `config/secrets.yml`; `<new key>` was
generated earlier. The **encrypted** values for the user 2FA secrets will be
written to the specified `filename` - you can use this to rollback in case of
error.
Finally, change `config/secrets.yml` to set `otp_key_base` to `<new key>` and
restart. Again, make sure you're operating in the **production** section.
```
# omnibus-gitlab
sudo gitlab-ctl start
# installation from source
sudo /etc/init.d/gitlab start
```
If there are any problems (perhaps using the wrong value for `old_key`), you can
restore your backup of `config/secrets.yml` and rollback the changes:
```
# omnibus-gitlab
sudo gitlab-ctl stop
sudo gitlab-rake gitlab:two_factor:rotate_key:rollback filename=backup.csv
sudo cp config/secrets.yml.bak config/secrets.yml
sudo gitlab-ctl start
# installation from source
sudo /etc/init.d/gitlab start
bundle exec rake gitlab:two_factor:rotate_key:rollback filename=backup.csv RAILS_ENV=production
cp config/secrets.yml.bak config/secrets.yml
sudo /etc/init.d/gitlab start
```
## Clear authentication tokens for all users. Important! Data loss! ## Clear authentication tokens for all users. Important! Data loss!
Clear authentication tokens for all users in the GitLab database. This Clear authentication tokens for all users in the GitLab database. This
......
...@@ -126,7 +126,7 @@ which visibility level you select on project settings. ...@@ -126,7 +126,7 @@ which visibility level you select on project settings.
## GitLab CI ## GitLab CI
GitLab CI permissions rely on the role the user has in GitLab. There are four GitLab CI permissions rely on the role the user has in GitLab. There are four
permission levels it total: permission levels in total:
- admin - admin
- master - master
......
# Project integrations # Project integrations
You can find the available integrations under the **Integrations** page by You can find the available integrations under your project's
navigating to the cog icon in the upper right corner of your project. You need **Settings ➔ Integrations** page. You need to have at least
to have at least [master permission][permissions] on the project. [master permission][permissions] on the project.
![Accessing the integrations](img/accessing_integrations.png)
## Project services ## Project services
......
...@@ -6,16 +6,11 @@ functionality to GitLab. ...@@ -6,16 +6,11 @@ functionality to GitLab.
## Accessing the project services ## Accessing the project services
You can find the available services under the **Integrations** page in your You can find the available services under your project's
project's settings. **Settings ➔ Integrations** page.
1. Navigate to the cog icon in the upper right corner of your project. You need There are more than 20 services to integrate with. Click on the one that you
to have at least [master permission][permissions] on the project. want to configure.
![Accessing the services](img/accessing_integrations.png)
1. There are more than 20 services to integrate with. Click on the one that you
want to configure.
![Project services list](img/project_services.png) ![Project services list](img/project_services.png)
......
...@@ -14,11 +14,8 @@ to the webhook URL. ...@@ -14,11 +14,8 @@ to the webhook URL.
Webhooks can be used to update an external issue tracker, trigger CI jobs, Webhooks can be used to update an external issue tracker, trigger CI jobs,
update a backup mirror, or even deploy to your production server. update a backup mirror, or even deploy to your production server.
Navigate to the webhooks page by going to the **Integrations** page from your Navigate to the webhooks page by going to your project's
project's settings which can be found under the wheel icon in the upper right **Settings ➔ Integrations**.
corner.
![Accessing the integrations](img/accessing_integrations.png)
## Webhook endpoint tips ## Webhook endpoint tips
......
# GitLab Issues Documentation # Issues documentation
The GitLab Issue Tracker is an advanced and complete tool The GitLab Issue Tracker is an advanced and complete tool
for tracking the evolution of a new idea or the process for tracking the evolution of a new idea or the process
...@@ -41,13 +41,13 @@ The image bellow illustrates how an issue looks like: ...@@ -41,13 +41,13 @@ The image bellow illustrates how an issue looks like:
Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md). Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md).
## New Issue ## New issue
Read through the [documentation on creating issues](create_new_issue.md). Read through the [documentation on creating issues](create_new_issue.md).
## Closing issues ## Closing issues
Read through the distinct ways to [close issues](closing_issues.md) on GitLab. Learn distinct ways to [close issues](closing_issues.md) in GitLab.
## Create a merge request from an issue ## Create a merge request from an issue
...@@ -84,7 +84,7 @@ Learn more about them on the [issue templates documentation](../../project/descr ...@@ -84,7 +84,7 @@ Learn more about them on the [issue templates documentation](../../project/descr
Learn more about [crosslinking](crosslinking_issues.md) issues and merge requests. Learn more about [crosslinking](crosslinking_issues.md) issues and merge requests.
### GitLab Issue Board ### Issue Board
The [GitLab Issue Board](https://about.gitlab.com/features/issueboard/) is a way to The [GitLab Issue Board](https://about.gitlab.com/features/issueboard/) is a way to
enhance your workflow by organizing and prioritizing issues in GitLab. enhance your workflow by organizing and prioritizing issues in GitLab.
......
# Pipelines settings # Pipelines settings
To reach the pipelines settings: To reach the pipelines settings navigate to your project's
**Settings ➔ CI/CD Pipelines**.
1. Navigate to your project and click the cog icon in the upper right corner.
![Project settings menu](../img/project_settings_list.png)
1. Select **Pipelines** from the menu.
The following settings can be configured per project. The following settings can be configured per project.
......
...@@ -27,11 +27,8 @@ See the [Changelog](#changelog) section for changes over time. ...@@ -27,11 +27,8 @@ See the [Changelog](#changelog) section for changes over time.
To protect a branch, you need to have at least Master permission level. Note To protect a branch, you need to have at least Master permission level. Note
that the `master` branch is protected by default. that the `master` branch is protected by default.
1. Navigate to the main page of the project. 1. Navigate to your project's **Settings ➔ Repository**
1. In the upper right corner, click the settings wheel and select **Protected branches**. 1. Scroll to find the **Protected branches** section.
![Project settings list](img/project_settings_list.png)
1. From the **Branch** dropdown menu, select the branch you want to protect and 1. From the **Branch** dropdown menu, select the branch you want to protect and
click **Protect**. In the screenshot below, we chose the `develop` branch. click **Protect**. In the screenshot below, we chose the `develop` branch.
......
...@@ -5,7 +5,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps ...@@ -5,7 +5,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
step 'I click link "Fork"' do step 'I click link "Fork"' do
expect(page).to have_content "Shop" expect(page).to have_content "Shop"
click_link "Fork project" click_link "Fork"
end end
step 'I am a member of project "Shop"' do step 'I am a member of project "Shop"' do
......
...@@ -94,6 +94,7 @@ module API ...@@ -94,6 +94,7 @@ module API
mount ::API::DeployKeys mount ::API::DeployKeys
mount ::API::Deployments mount ::API::Deployments
mount ::API::Environments mount ::API::Environments
mount ::API::Events
mount ::API::Features mount ::API::Features
mount ::API::Files mount ::API::Files
mount ::API::Groups mount ::API::Groups
......
module API
class Events < Grape::API
include PaginationParams
helpers do
params :event_filter_params do
optional :action, type: String, values: Event.actions, desc: 'Event action to filter on'
optional :target_type, type: String, values: Event.target_types, desc: 'Event target type to filter on'
optional :before, type: Date, desc: 'Include only events created before this date'
optional :after, type: Date, desc: 'Include only events created after this date'
end
params :sort_params do
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return events sorted in ascending and descending order'
end
def present_events(events)
events = events.reorder(created_at: params[:sort])
present paginate(events), with: Entities::Event
end
end
resource :events do
desc "List currently authenticated user's events" do
detail 'This feature was introduced in GitLab 9.3.'
success Entities::Event
end
params do
use :pagination
use :event_filter_params
use :sort_params
end
get do
authenticate!
events = EventsFinder.new(params.merge(source: current_user, current_user: current_user)).execute.preload(:author, :target)
present_events(events)
end
end
params do
requires :id, type: String, desc: 'The ID or Username of the user'
end
resource :users do
desc 'Get the contribution events of a specified user' do
detail 'This feature was introduced in GitLab 8.13.'
success Entities::Event
end
params do
use :pagination
use :event_filter_params
use :sort_params
end
get ':id/events' do
user = find_user(params[:id])
not_found!('User') unless user
events = EventsFinder.new(params.merge(source: user, current_user: current_user)).execute.preload(:author, :target)
present_events(events)
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc "List a Project's visible events" do
success Entities::Event
end
params do
use :pagination
use :event_filter_params
use :sort_params
end
get ":id/events" do
events = EventsFinder.new(params.merge(source: user_project, current_user: current_user)).execute.preload(:author, :target)
present_events(events)
end
end
end
end
...@@ -10,7 +10,8 @@ module API ...@@ -10,7 +10,8 @@ module API
file_content: attrs[:content], file_content: attrs[:content],
file_content_encoding: attrs[:encoding], file_content_encoding: attrs[:encoding],
author_email: attrs[:author_email], author_email: attrs[:author_email],
author_name: attrs[:author_name] author_name: attrs[:author_name],
last_commit_sha: attrs[:last_commit_id]
} }
end end
...@@ -46,6 +47,7 @@ module API ...@@ -46,6 +47,7 @@ module API
use :simple_file_params use :simple_file_params
requires :content, type: String, desc: 'File content' requires :content, type: String, desc: 'File content'
optional :encoding, type: String, values: %w[base64], desc: 'File encoding' optional :encoding, type: String, values: %w[base64], desc: 'File encoding'
optional :last_commit_id, type: String, desc: 'Last known commit id for this file'
end end
end end
...@@ -111,7 +113,12 @@ module API ...@@ -111,7 +113,12 @@ module API
authorize! :push_code, user_project authorize! :push_code, user_project
file_params = declared_params(include_missing: false) file_params = declared_params(include_missing: false)
begin
result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute
rescue ::Files::UpdateService::FileChangedError => e
render_api_error!(e.message, 400)
end
if result[:status] == :success if result[:status] == :success
status(200) status(200)
......
...@@ -167,16 +167,6 @@ module API ...@@ -167,16 +167,6 @@ module API
user_can_admin_project: can?(current_user, :admin_project, user_project), statistics: params[:statistics] user_can_admin_project: can?(current_user, :admin_project, user_project), statistics: params[:statistics]
end end
desc 'Get events for a single project' do
success Entities::Event
end
params do
use :pagination
end
get ":id/events" do
present paginate(user_project.events.recent), with: Entities::Event
end
desc 'Fork new project for the current user or provided namespace.' do desc 'Fork new project for the current user or provided namespace.' do
success Entities::Project success Entities::Project
end end
......
...@@ -328,27 +328,6 @@ module API ...@@ -328,27 +328,6 @@ module API
end end
end end
desc 'Get the contribution events of a specified user' do
detail 'This feature was introduced in GitLab 8.13.'
success Entities::Event
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
use :pagination
end
get ':id/events' do
user = User.find_by(id: params[:id])
not_found!('User') unless user
events = user.events.
merge(ProjectsFinder.new(current_user: current_user).execute).
references(:project).
with_associations.
recent
present paginate(events), with: Entities::Event
end
params do params do
requires :user_id, type: Integer, desc: 'The ID of the user' requires :user_id, type: Integer, desc: 'The ID of the user'
end end
......
module Gitlab
# The +otp_key_base+ param is used to encrypt the User#otp_secret attribute.
#
# When +otp_key_base+ is changed, it invalidates the current encrypted values
# of User#otp_secret. This class can be used to decrypt all the values with
# the old key, encrypt them with the new key, and and update the database
# with the new values.
#
# For persistence between runs, a CSV file is used with the following columns:
#
# user_id, old_value, new_value
#
# Only the encrypted values are stored in this file.
#
# As users may have their 2FA settings changed at any time, this is only
# guaranteed to be safe if run offline.
class OtpKeyRotator
HEADERS = %w[user_id old_value new_value].freeze
attr_reader :filename
# Create a new rotator. +filename+ is used to store values by +calculate!+,
# and to update the database with new and old values in +apply!+ and
# +rollback!+, respectively.
def initialize(filename)
@filename = filename
end
def rotate!(old_key:, new_key:)
old_key ||= Gitlab::Application.secrets.otp_key_base
raise ArgumentError.new("Old key is the same as the new key") if old_key == new_key
raise ArgumentError.new("New key is too short! Must be 256 bits") if new_key.size < 64
write_csv do |csv|
ActiveRecord::Base.transaction do
User.with_two_factor.in_batches do |relation|
rows = relation.pluck(:id, :encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt)
rows.each do |row|
user = %i[id ciphertext iv salt].zip(row).to_h
new_value = reencrypt(user, old_key, new_key)
User.where(id: user[:id]).update_all(encrypted_otp_secret: new_value)
csv << [user[:id], user[:ciphertext], new_value]
end
end
end
end
end
def rollback!
ActiveRecord::Base.transaction do
CSV.foreach(filename, headers: HEADERS, return_headers: false) do |row|
User.where(id: row['user_id']).update_all(encrypted_otp_secret: row['old_value'])
end
end
end
private
attr_reader :old_key, :new_key
def otp_secret_settings
@otp_secret_settings ||= User.encrypted_attributes[:otp_secret]
end
def reencrypt(user, old_key, new_key)
original = user[:ciphertext].unpack("m").join
opts = {
iv: user[:iv].unpack("m").join,
salt: user[:salt].unpack("m").join,
algorithm: otp_secret_settings[:algorithm],
insecure_mode: otp_secret_settings[:insecure_mode]
}
decrypted = Encryptor.decrypt(original, opts.merge(key: old_key))
encrypted = Encryptor.encrypt(decrypted, opts.merge(key: new_key))
[encrypted].pack("m")
end
def write_csv(&blk)
File.open(filename, "w") do |file|
yield CSV.new(file, headers: HEADERS, write_headers: false)
end
end
end
end
...@@ -19,5 +19,21 @@ namespace :gitlab do ...@@ -19,5 +19,21 @@ namespace :gitlab do
puts "There are currently no users with 2FA enabled.".color(:yellow) puts "There are currently no users with 2FA enabled.".color(:yellow)
end end
end end
namespace :rotate_key do
def rotator
@rotator ||= Gitlab::OtpKeyRotator.new(ENV['filename'])
end
desc "Encrypt user OTP secrets with a new encryption key"
task apply: :environment do |t, args|
rotator.rotate!(old_key: ENV['old_key'], new_key: ENV['new_key'])
end
desc "Rollback to secrets encrypted with the old encryption key"
task rollback: :environment do
rotator.rollback!
end
end
end end
end end
...@@ -18,13 +18,13 @@ feature 'Issues > Labels bulk assignment', feature: true do ...@@ -18,13 +18,13 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'can bulk assign' do context 'can bulk assign' do
before do before do
visit namespace_project_issues_path(project.namespace, project) enable_bulk_update
end end
context 'a label' do context 'a label' do
context 'to all issues' do context 'to all issues' do
before do before do
check 'check_all_issues' check 'check-all-issues'
open_labels_dropdown ['bug'] open_labels_dropdown ['bug']
update_issues update_issues
end end
...@@ -52,7 +52,7 @@ feature 'Issues > Labels bulk assignment', feature: true do ...@@ -52,7 +52,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'multiple labels' do context 'multiple labels' do
context 'to all issues' do context 'to all issues' do
before do before do
check 'check_all_issues' check 'check-all-issues'
open_labels_dropdown %w(bug feature) open_labels_dropdown %w(bug feature)
update_issues update_issues
end end
...@@ -86,9 +86,10 @@ feature 'Issues > Labels bulk assignment', feature: true do ...@@ -86,9 +86,10 @@ feature 'Issues > Labels bulk assignment', feature: true do
before do before do
issue2.labels << bug issue2.labels << bug
issue2.labels << feature 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'] open_labels_dropdown ['bug']
update_issues update_issues
end end
...@@ -107,9 +108,8 @@ feature 'Issues > Labels bulk assignment', feature: true do ...@@ -107,9 +108,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue2.labels << bug issue2.labels << bug
issue2.labels << feature issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project) enable_bulk_update
check 'check-all-issues'
check 'check_all_issues'
unmark_labels_in_dropdown %w(bug feature) unmark_labels_in_dropdown %w(bug feature)
update_issues update_issues
end end
...@@ -127,8 +127,7 @@ feature 'Issues > Labels bulk assignment', feature: true do ...@@ -127,8 +127,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue1.labels << bug issue1.labels << bug
issue2.labels << feature issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project) enable_bulk_update
check_issue issue1 check_issue issue1
unmark_labels_in_dropdown ['bug'] unmark_labels_in_dropdown ['bug']
update_issues update_issues
...@@ -147,8 +146,7 @@ feature 'Issues > Labels bulk assignment', feature: true do ...@@ -147,8 +146,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue2.labels << bug issue2.labels << bug
issue2.labels << feature issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project) enable_bulk_update
check_issue issue1 check_issue issue1
check_issue issue2 check_issue issue2
unmark_labels_in_dropdown ['bug'] unmark_labels_in_dropdown ['bug']
...@@ -171,14 +169,15 @@ feature 'Issues > Labels bulk assignment', feature: true do ...@@ -171,14 +169,15 @@ feature 'Issues > Labels bulk assignment', feature: true do
before do before do
issue1.labels << bug issue1.labels << bug
issue2.labels << feature issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project) enable_bulk_update
end end
it 'keeps labels' do it 'keeps labels' do
expect(find("#issue_#{issue1.id}")).to have_content 'bug' expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).to have_content 'feature' expect(find("#issue_#{issue2.id}")).to have_content 'feature'
check 'check_all_issues' check 'check-all-issues'
open_milestone_dropdown(['First Release']) open_milestone_dropdown(['First Release'])
update_issues update_issues
...@@ -192,14 +191,13 @@ feature 'Issues > Labels bulk assignment', feature: true do ...@@ -192,14 +191,13 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'setting a milestone and adding another label' do context 'setting a milestone and adding another label' do
before do before do
issue1.labels << bug issue1.labels << bug
enable_bulk_update
visit namespace_project_issues_path(project.namespace, project)
end end
it 'keeps existing label and new label is present' do it 'keeps existing label and new label is present' do
expect(find("#issue_#{issue1.id}")).to have_content 'bug' expect(find("#issue_#{issue1.id}")).to have_content 'bug'
check 'check_all_issues' check 'check-all-issues'
open_milestone_dropdown ['First Release'] open_milestone_dropdown ['First Release']
open_labels_dropdown ['feature'] open_labels_dropdown ['feature']
update_issues update_issues
...@@ -218,7 +216,7 @@ feature 'Issues > Labels bulk assignment', feature: true do ...@@ -218,7 +216,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue1.labels << feature issue1.labels << feature
issue2.labels << feature issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project) enable_bulk_update
end end
it 'keeps existing label and new label is present' do it 'keeps existing label and new label is present' do
...@@ -226,7 +224,8 @@ feature 'Issues > Labels bulk assignment', feature: true 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_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).to have_content 'feature' expect(find("#issue_#{issue2.id}")).to have_content 'feature'
check 'check_all_issues' check 'check-all-issues'
open_milestone_dropdown ['First Release'] open_milestone_dropdown ['First Release']
unmark_labels_in_dropdown ['feature'] unmark_labels_in_dropdown ['feature']
update_issues update_issues
...@@ -248,7 +247,7 @@ feature 'Issues > Labels bulk assignment', feature: true do ...@@ -248,7 +247,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue1.labels << bug issue1.labels << bug
issue2.labels << feature issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project) enable_bulk_update
end end
it 'keeps labels' do it 'keeps labels' do
...@@ -257,7 +256,7 @@ feature 'Issues > Labels bulk assignment', feature: true 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 'feature'
expect(find("#issue_#{issue2.id}")).to have_content 'First Release' expect(find("#issue_#{issue2.id}")).to have_content 'First Release'
check 'check_all_issues' check 'check-all-issues'
open_milestone_dropdown(['No Milestone']) open_milestone_dropdown(['No Milestone'])
update_issues update_issues
...@@ -272,8 +271,7 @@ feature 'Issues > Labels bulk assignment', feature: true do ...@@ -272,8 +271,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'toggling checked issues' do context 'toggling checked issues' do
before do before do
issue1.labels << bug issue1.labels << bug
enable_bulk_update
visit namespace_project_issues_path(project.namespace, project)
end end
it do it do
...@@ -298,14 +296,14 @@ feature 'Issues > Labels bulk assignment', feature: true do ...@@ -298,14 +296,14 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue1.labels << feature issue1.labels << feature
issue2.labels << bug issue2.labels << bug
visit namespace_project_issues_path(project.namespace, project) enable_bulk_update
end end
it 'applies label from filtered results' do it 'applies label from filtered results' do
check 'check_all_issues' check 'check-all-issues'
page.within('.issues_bulk_update') do page.within('.issues-bulk-update') do
click_button 'Labels' click_button 'Select labels'
wait_for_requests wait_for_requests
expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active') 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 ...@@ -340,15 +338,16 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'cannot bulk assign labels' do context 'cannot bulk assign labels' do
it 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' expect(page).not_to have_css '.issue-check'
end end
end end
end end
def open_milestone_dropdown(items = []) def open_milestone_dropdown(items = [])
page.within('.issues_bulk_update') do page.within('.issues-bulk-update') do
click_button 'Milestone' click_button 'Select milestone'
wait_for_requests wait_for_requests
items.map do |item| items.map do |item|
click_link item click_link item
...@@ -357,8 +356,8 @@ feature 'Issues > Labels bulk assignment', feature: true do ...@@ -357,8 +356,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
end end
def open_labels_dropdown(items = [], unmark = false) def open_labels_dropdown(items = [], unmark = false)
page.within('.issues_bulk_update') do page.within('.issues-bulk-update') do
click_button 'Labels' click_button 'Select labels'
wait_for_requests wait_for_requests
items.map do |item| items.map do |item|
click_link item click_link item
...@@ -391,7 +390,12 @@ feature 'Issues > Labels bulk assignment', feature: true do ...@@ -391,7 +390,12 @@ feature 'Issues > Labels bulk assignment', feature: true do
end end
def update_issues def update_issues
click_button 'Update issues' click_button 'Update all'
wait_for_requests wait_for_requests
end end
def enable_bulk_update
visit namespace_project_issues_path(project.namespace, project)
click_button 'Edit Issues'
end
end end
...@@ -14,7 +14,8 @@ feature 'Multiple issue updating from issues#index', feature: true do ...@@ -14,7 +14,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
it 'sets to closed' do it 'sets to closed' do
visit namespace_project_issues_path(project.namespace, project) 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('.js-issue-status').click
find('.dropdown-menu-status a', text: 'Closed').click find('.dropdown-menu-status a', text: 'Closed').click
...@@ -26,7 +27,8 @@ feature 'Multiple issue updating from issues#index', feature: true do ...@@ -26,7 +27,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
create_closed create_closed
visit namespace_project_issues_path(project.namespace, project, state: '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('.js-issue-status').click
find('.dropdown-menu-status a', text: 'Open').click find('.dropdown-menu-status a', text: 'Open').click
...@@ -39,7 +41,8 @@ feature 'Multiple issue updating from issues#index', feature: true do ...@@ -39,7 +41,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
it 'updates to current user' do it 'updates to current user' do
visit namespace_project_issues_path(project.namespace, project) 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_update_assignee_button
find('.dropdown-menu-user-link', text: user.username).click find('.dropdown-menu-user-link', text: user.username).click
...@@ -54,7 +57,8 @@ feature 'Multiple issue updating from issues#index', feature: true do ...@@ -54,7 +57,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
create_assigned create_assigned
visit namespace_project_issues_path(project.namespace, project) 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_update_assignee_button
click_link 'Unassigned' click_link 'Unassigned'
...@@ -69,8 +73,9 @@ feature 'Multiple issue updating from issues#index', feature: true do ...@@ -69,8 +73,9 @@ feature 'Multiple issue updating from issues#index', feature: true do
it 'updates milestone' do it 'updates milestone' do
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click click_button 'Edit Issues'
find('.issues_bulk_update .js-milestone-select').click find('#check-all-issues').click
find('.issues-bulk-update .js-milestone-select').click
find('.dropdown-menu-milestone a', text: milestone.title).click find('.dropdown-menu-milestone a', text: milestone.title).click
click_update_issues_button click_update_issues_button
...@@ -84,8 +89,9 @@ feature 'Multiple issue updating from issues#index', feature: true do ...@@ -84,8 +89,9 @@ feature 'Multiple issue updating from issues#index', feature: true do
expect(first('.issue')).to have_content milestone.title expect(first('.issue')).to have_content milestone.title
find('#check_all_issues').click click_button 'Edit Issues'
find('.issues_bulk_update .js-milestone-select').click find('#check-all-issues').click
find('.issues-bulk-update .js-milestone-select').click
find('.dropdown-menu-milestone a', text: "No Milestone").click find('.dropdown-menu-milestone a', text: "No Milestone").click
click_update_issues_button click_update_issues_button
...@@ -112,7 +118,7 @@ feature 'Multiple issue updating from issues#index', feature: true do ...@@ -112,7 +118,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
end end
def click_update_issues_button def click_update_issues_button
find('.update_selected_issues').click find('.update-selected-issues').click
wait_for_requests wait_for_requests
end end
end end
...@@ -98,14 +98,16 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t ...@@ -98,14 +98,16 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t
end end
def change_status(text) 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('.js-issue-status').click
find('.dropdown-menu-status a', text: text).click find('.dropdown-menu-status a', text: text).click
click_update_merge_requests_button click_update_merge_requests_button
end end
def change_assignee(text) def change_assignee(text)
find('#check_all_issues').click click_button 'Edit Merge Requests'
find('#check-all-issues').click
find('.js-update-assignee').click find('.js-update-assignee').click
wait_for_requests wait_for_requests
...@@ -117,14 +119,15 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t ...@@ -117,14 +119,15 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t
end end
def change_milestone(text) def change_milestone(text)
find('#check_all_issues').click click_button 'Edit Merge Requests'
find('.issues_bulk_update .js-milestone-select').click find('#check-all-issues').click
find('.issues-bulk-update .js-milestone-select').click
find('.dropdown-menu-milestone a', text: text).click find('.dropdown-menu-milestone a', text: text).click
click_update_merge_requests_button click_update_merge_requests_button
end end
def click_update_merge_requests_button def click_update_merge_requests_button
find('.update_selected_issues').click find('.update-selected-issues').click
wait_for_requests wait_for_requests
end end
end end
...@@ -31,7 +31,7 @@ feature 'Environments page', :feature, :js do ...@@ -31,7 +31,7 @@ feature 'Environments page', :feature, :js do
it 'should show one environment' do it 'should show one environment' do
visit namespace_project_environments_path(project.namespace, project, scope: 'available') visit namespace_project_environments_path(project.namespace, project, scope: 'available')
expect(page).to have_css('.environments-container') expect(page).to have_css('.environments-container')
expect(page.all('tbody > tr').length).to eq(1) expect(page.all('.environment-name').length).to eq(1)
end end
end end
...@@ -59,7 +59,7 @@ feature 'Environments page', :feature, :js do ...@@ -59,7 +59,7 @@ feature 'Environments page', :feature, :js do
it 'should show one environment' do it 'should show one environment' do
visit namespace_project_environments_path(project.namespace, project, scope: 'stopped') visit namespace_project_environments_path(project.namespace, project, scope: 'stopped')
expect(page).to have_css('.environments-container') expect(page).to have_css('.environments-container')
expect(page.all('tbody > tr').length).to eq(1) expect(page.all('.environment-name').length).to eq(1)
end end
end end
end end
......
...@@ -14,7 +14,7 @@ feature 'Visibility settings', feature: true, js: true do ...@@ -14,7 +14,7 @@ feature 'Visibility settings', feature: true, js: true do
visibility_select_container = find('.js-visibility-select') visibility_select_container = find('.js-visibility-select')
expect(visibility_select_container.find('.visibility-select').value).to eq project.visibility_level.to_s expect(visibility_select_container.find('.visibility-select').value).to eq project.visibility_level.to_s
expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.' expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.'
end end
scenario 'project visibility description updates on change' do scenario 'project visibility description updates on change' do
...@@ -41,7 +41,7 @@ feature 'Visibility settings', feature: true, js: true do ...@@ -41,7 +41,7 @@ feature 'Visibility settings', feature: true, js: true do
expect(visibility_select_container).not_to have_select '.visibility-select' expect(visibility_select_container).not_to have_select '.visibility-select'
expect(visibility_select_container).to have_content 'Public' expect(visibility_select_container).to have_content 'Public'
expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.' expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.'
end end
end end
end end
...@@ -24,8 +24,8 @@ describe 'Unsubscribe links', feature: true do ...@@ -24,8 +24,8 @@ describe 'Unsubscribe links', feature: true do
visit body_link visit body_link
expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last) expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last)
expect(page).to have_text(%(Unsubscribe from issue #{issue.title} (#{issue.to_reference}))) expect(page).to have_text(%(Unsubscribe from issue))
expect(page).to have_text(%(Are you sure you want to unsubscribe from issue #{issue.title} (#{issue.to_reference})?)) expect(page).to have_text(%(Are you sure you want to unsubscribe from the issue: #{issue.title} (#{issue.to_reference})?))
expect(issue.subscribed?(recipient, project)).to be_truthy expect(issue.subscribed?(recipient, project)).to be_truthy
click_link 'Unsubscribe' click_link 'Unsubscribe'
......
require 'spec_helper'
describe EventsFinder do
let(:user) { create(:user) }
let(:other_user) { create(:user) }
let(:project1) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) }
let(:project2) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) }
let(:closed_issue) { create(:closed_issue, project: project1, author: user) }
let(:opened_merge_request) { create(:merge_request, source_project: project2, author: user) }
let!(:closed_issue_event) { create(:event, project: project1, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 12, 30)) }
let!(:opened_merge_request_event) { create(:event, project: project2, author: user, target: opened_merge_request, action: Event::CREATED, created_at: Date.new(2017, 1, 31)) }
let(:closed_issue2) { create(:closed_issue, project: project1, author: user) }
let(:opened_merge_request2) { create(:merge_request, source_project: project2, author: user) }
let!(:closed_issue_event2) { create(:event, project: project1, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 2, 2)) }
let!(:opened_merge_request_event2) { create(:event, project: project2, author: user, target: opened_merge_request, action: Event::CREATED, created_at: Date.new(2017, 2, 2)) }
context 'when targeting a user' do
it 'returns events between specified dates filtered on action and type' do
events = described_class.new(source: user, current_user: user, action: 'created', target_type: 'merge_request', after: Date.new(2017, 1, 1), before: Date.new(2017, 2, 1)).execute
expect(events).to eq([opened_merge_request_event])
end
it 'does not return events the current_user does not have access to' do
events = described_class.new(source: user, current_user: other_user).execute
expect(events).not_to include(opened_merge_request_event)
end
end
context 'when targeting a project' do
it 'returns project events between specified dates filtered on action and type' do
events = described_class.new(source: project1, current_user: user, action: 'closed', target_type: 'issue', after: Date.new(2016, 12, 1), before: Date.new(2017, 1, 1)).execute
expect(events).to eq([closed_issue_event])
end
it 'does not return events the current_user does not have access to' do
events = described_class.new(source: project2, current_user: other_user).execute
expect(events).to be_empty
end
end
end
...@@ -257,7 +257,7 @@ describe ProjectsHelper do ...@@ -257,7 +257,7 @@ describe ProjectsHelper do
result = helper.project_feature_access_select(:issues_access_level) result = helper.project_feature_access_select(:issues_access_level)
expect(result).to include("Disabled") expect(result).to include("Disabled")
expect(result).to include("Only team members") expect(result).to include("Only team members")
expect(result).not_to include("Everyone with access") expect(result).to have_selector('option[disabled]', text: "Everyone with access")
end end
end end
...@@ -272,7 +272,7 @@ describe ProjectsHelper do ...@@ -272,7 +272,7 @@ describe ProjectsHelper do
expect(result).to include("Disabled") expect(result).to include("Disabled")
expect(result).to include("Only team members") expect(result).to include("Only team members")
expect(result).not_to include("Everyone with access") expect(result).to have_selector('option[disabled]', text: "Everyone with access")
expect(result).to have_selector('option[selected]', text: "Only team members") expect(result).to have_selector('option[selected]', text: "Only team members")
end end
end end
......
...@@ -37,7 +37,7 @@ describe VisibilityLevelHelper do ...@@ -37,7 +37,7 @@ describe VisibilityLevelHelper do
it "describes public projects" do it "describes public projects" do
expect(project_visibility_level_description(Gitlab::VisibilityLevel::PUBLIC)) expect(project_visibility_level_description(Gitlab::VisibilityLevel::PUBLIC))
.to eq "The project can be cloned without any authentication." .to eq "The project can be accessed without any authentication."
end end
end end
......
...@@ -71,7 +71,7 @@ describe('Pipelines table in Commits and Merge requests', () => { ...@@ -71,7 +71,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
it('should render a table with the received pipelines', (done) => { it('should render a table with the received pipelines', (done) => {
setTimeout(() => { setTimeout(() => {
expect(this.component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1); expect(this.component.$el.querySelectorAll('.ci-table .commit').length).toEqual(1);
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null); expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
expect(this.component.$el.querySelector('.empty-state')).toBe(null); expect(this.component.$el.querySelector('.empty-state')).toBe(null);
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBe(null); expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBe(null);
...@@ -108,7 +108,7 @@ describe('Pipelines table in Commits and Merge requests', () => { ...@@ -108,7 +108,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null); expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
expect(this.component.$el.querySelector('.js-empty-state')).toBe(null); expect(this.component.$el.querySelector('.js-empty-state')).toBe(null);
expect(this.component.$el.querySelector('table')).toBe(null); expect(this.component.$el.querySelector('.ci-table')).toBe(null);
done(); done();
}, 0); }, 0);
}); });
......
...@@ -271,7 +271,7 @@ describe('Environment', () => { ...@@ -271,7 +271,7 @@ describe('Environment', () => {
// wait for next async request // wait for next async request
setTimeout(() => { setTimeout(() => {
expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1); expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
expect(component.$el.querySelector('td.text-center > a.btn').textContent).toContain('Show all'); expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain('Show all');
Vue.http.interceptors = _.without(Vue.http.interceptors, folderInterceptor); Vue.http.interceptors = _.without(Vue.http.interceptors, folderInterceptor);
done(); done();
......
...@@ -29,6 +29,6 @@ describe('Environment item', () => { ...@@ -29,6 +29,6 @@ describe('Environment item', () => {
}, },
}).$mount(); }).$mount();
expect(component.$el.tagName).toEqual('TABLE'); expect(component.$el.getAttribute('class')).toContain('ci-table');
}); });
}); });
%form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'} %form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'}
%input{id: 'utf8', name: 'utf8', value: '✓'} %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: 'search', name: 'search'}
%input{id: 'author_id', name: 'author_id'} %input{id: 'author_id', name: 'author_id'}
%input{id: 'assignee_id', name: 'assignee_id'} %input{id: 'assignee_id', name: 'assignee_id'}
......
/* global Issuable */ /* global IssuableIndex */
import '~/lib/utils/url_utility'; import '~/lib/utils/url_utility';
import '~/issuable'; import '~/issuable_index';
(() => { (() => {
const BASE_URL = '/user/project/issues?scope=all&state=closed'; const BASE_URL = '/user/project/issues?scope=all&state=closed';
...@@ -24,11 +24,11 @@ import '~/issuable'; ...@@ -24,11 +24,11 @@ import '~/issuable';
beforeEach(() => { beforeEach(() => {
loadFixtures('static/issuable_filter.html.raw'); loadFixtures('static/issuable_filter.html.raw');
Issuable.init(); IssuableIndex.init();
}); });
it('should be defined', () => { it('should be defined', () => {
expect(window.Issuable).toBeDefined(); expect(window.IssuableIndex).toBeDefined();
}); });
describe('filtering', () => { describe('filtering', () => {
...@@ -43,7 +43,7 @@ import '~/issuable'; ...@@ -43,7 +43,7 @@ import '~/issuable';
it('should contain only the default parameters', () => { it('should contain only the default parameters', () => {
spyOn(gl.utils, 'visitUrl'); spyOn(gl.utils, 'visitUrl');
Issuable.filterResults($filtersForm); IssuableIndex.filterResults($filtersForm);
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS); expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
}); });
...@@ -52,7 +52,7 @@ import '~/issuable'; ...@@ -52,7 +52,7 @@ import '~/issuable';
spyOn(gl.utils, 'visitUrl'); spyOn(gl.utils, 'visitUrl');
updateForm({ search: 'broken' }, $filtersForm); updateForm({ search: 'broken' }, $filtersForm);
Issuable.filterResults($filtersForm); IssuableIndex.filterResults($filtersForm);
const params = `${DEFAULT_PARAMS}&search=broken`; const params = `${DEFAULT_PARAMS}&search=broken`;
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params); expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
...@@ -64,14 +64,14 @@ import '~/issuable'; ...@@ -64,14 +64,14 @@ import '~/issuable';
// initial filter // initial filter
updateForm({ milestone_title: 'v1.0' }, $filtersForm); updateForm({ milestone_title: 'v1.0' }, $filtersForm);
Issuable.filterResults($filtersForm); IssuableIndex.filterResults($filtersForm);
let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`; let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`;
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params); expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
// update filter // update filter
updateForm({ label_name: 'Frontend' }, $filtersForm); updateForm({ label_name: 'Frontend' }, $filtersForm);
Issuable.filterResults($filtersForm); IssuableIndex.filterResults($filtersForm);
params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`; params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`;
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params); expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
}); });
......
require 'spec_helper'
describe Gitlab::OtpKeyRotator do
let(:file) { Tempfile.new("otp-key-rotator-test") }
let(:filename) { file.path }
let(:old_key) { Gitlab::Application.secrets.otp_key_base }
let(:new_key) { "00" * 32 }
let!(:users) { create_list(:user, 5, :two_factor) }
after do
file.close
file.unlink
end
def data
CSV.read(filename)
end
def build_row(user, applied = false)
[user.id.to_s, encrypt_otp(user, old_key), encrypt_otp(user, new_key)]
end
def encrypt_otp(user, key)
opts = {
value: user.otp_secret,
iv: user.encrypted_otp_secret_iv.unpack("m").join,
salt: user.encrypted_otp_secret_salt.unpack("m").join,
algorithm: 'aes-256-cbc',
insecure_mode: true,
key: key
}
[Encryptor.encrypt(opts)].pack("m")
end
subject(:rotator) { described_class.new(filename) }
describe '#rotate!' do
subject(:rotation) { rotator.rotate!(old_key: old_key, new_key: new_key) }
it 'stores the calculated values in a spreadsheet' do
rotation
expect(data).to match_array(users.map {|u| build_row(u) })
end
context 'new key is too short' do
let(:new_key) { "00" * 31 }
it { expect { rotation }.to raise_error(ArgumentError) }
end
context 'new key is the same as the old key' do
let(:new_key) { old_key }
it { expect { rotation }.to raise_error(ArgumentError) }
end
end
describe '#rollback!' do
it 'updates rows to the old value' do
file.puts("#{users[0].id},old,new")
file.close
rotator.rollback!
expect(users[0].reload.encrypted_otp_secret).to eq('old')
expect(users[1].reload.encrypted_otp_secret).not_to eq('old')
end
end
end
...@@ -6,7 +6,7 @@ describe PagesDomain, models: true do ...@@ -6,7 +6,7 @@ describe PagesDomain, models: true do
end end
describe 'validate domain' do describe 'validate domain' do
subject { build(:pages_domain, domain: domain) } subject(:pages_domain) { build(:pages_domain, domain: domain) }
context 'is unique' do context 'is unique' do
let(:domain) { 'my.domain.com' } let(:domain) { 'my.domain.com' }
...@@ -14,36 +14,25 @@ describe PagesDomain, models: true do ...@@ -14,36 +14,25 @@ describe PagesDomain, models: true do
it { is_expected.to validate_uniqueness_of(:domain) } it { is_expected.to validate_uniqueness_of(:domain) }
end end
context 'valid domain' do {
let(:domain) { 'my.domain.com' } 'my.domain.com' => true,
'123.456.789' => true,
it { is_expected.to be_valid } '0x12345.com' => true,
'0123123' => true,
'_foo.com' => false,
'reserved.com' => false,
'a.reserved.com' => false,
nil => false
}.each do |value, validity|
context "domain #{value.inspect} validity" do
before do
allow(Settings.pages).to receive(:host).and_return('reserved.com')
end end
context 'valid hexadecimal-looking domain' do let(:domain) { value }
let(:domain) { '0x12345.com'}
it { is_expected.to be_valid } it { expect(pages_domain.valid?).to eq(validity) }
end end
context 'no domain' do
let(:domain) { nil }
it { is_expected.not_to be_valid }
end
context 'invalid domain' do
let(:domain) { '0123123' }
it { is_expected.not_to be_valid }
end
context 'domain from .example.com' do
let(:domain) { 'my.domain.com' }
before { allow(Settings.pages).to receive(:host).and_return('domain.com') }
it { is_expected.not_to be_valid }
end end
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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