Merge branch 'master' into auto-search-when-state-changed
This commit is contained in:
commit
b6bded14be
|
@ -1 +1 @@
|
||||||
0.4.2
|
0.4.3
|
||||||
|
|
|
@ -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';
|
||||||
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 {
|
||||||
</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 {
|
||||||
};
|
};
|
||||||
</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 {
|
||||||
{{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 {
|
||||||
: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 {
|
||||||
: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">
|
||||||
<span
|
|
||||||
v-if="!model.isFolder && canShowDate"
|
|
||||||
class="environment-created-date-timeago">
|
|
||||||
{{createdDate}}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="environments-actions">
|
|
||||||
<div
|
<div
|
||||||
v-if="!model.isFolder"
|
v-if="!model.isFolder"
|
||||||
class="btn-group pull-right"
|
class="table-mobile-header">
|
||||||
|
Updated
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="!model.isFolder && canShowDate"
|
||||||
|
class="environment-created-date-timeago table-mobile-content">
|
||||||
|
{{createdDate}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-section section-30 environments-actions table-button-footer" role="gridcell">
|
||||||
|
<div
|
||||||
|
v-if="!model.isFolder"
|
||||||
|
class="btn-group environment-action-buttons"
|
||||||
role="group">
|
role="group">
|
||||||
|
|
||||||
<actions-component
|
<actions-component
|
||||||
|
@ -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 {
|
||||||
</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 {
|
||||||
<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 {
|
||||||
<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 {
|
||||||
</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,68 +45,59 @@ 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
|
</div>
|
||||||
</th>
|
<div class="table-section section-10 environments-deploy" role="rowheader">
|
||||||
<th class="environments-deploy">
|
Deployment
|
||||||
Last deployment
|
</div>
|
||||||
</th>
|
<div class="table-section section-15 environments-build" role="rowheader">
|
||||||
<th class="environments-build">
|
Job
|
||||||
Job
|
</div>
|
||||||
</th>
|
<div class="table-section section-25 environments-commit" role="rowheader">
|
||||||
<th class="environments-commit">
|
Commit
|
||||||
Commit
|
</div>
|
||||||
</th>
|
<div class="table-section section-10 environments-date" role="rowheader">
|
||||||
<th class="environments-date">
|
Updated
|
||||||
Updated
|
</div>
|
||||||
</th>
|
</div>
|
||||||
<th class="environments-actions"></th>
|
<template
|
||||||
</tr>
|
v-for="model in environments"
|
||||||
</thead>
|
v-bind:model="model">
|
||||||
<tbody>
|
<div
|
||||||
<template
|
is="environment-item"
|
||||||
v-for="model in environments"
|
:model="model"
|
||||||
v-bind:model="model">
|
:can-create-deployment="canCreateDeployment"
|
||||||
<tr
|
:can-read-environment="canReadEnvironment"
|
||||||
is="environment-item"
|
/>
|
||||||
:model="model"
|
|
||||||
:can-create-deployment="canCreateDeployment"
|
|
||||||
:can-read-environment="canReadEnvironment"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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" />
|
</div>
|
||||||
</td>
|
|
||||||
</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"
|
||||||
:can-create-deployment="canCreateDeployment"
|
:can-create-deployment="canCreateDeployment"
|
||||||
:can-read-environment="canReadEnvironment"
|
:can-read-environment="canReadEnvironment"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<tr>
|
<div>
|
||||||
<td
|
<div class="text-center prepend-top-10">
|
||||||
colspan="6"
|
<a
|
||||||
class="text-center">
|
:href="folderUrl(model)"
|
||||||
<a
|
class="btn btn-default">
|
||||||
:href="folderUrl(model)"
|
Show all
|
||||||
class="btn btn-default">
|
</a>
|
||||||
Show all
|
</div>
|
||||||
</a>
|
</div>
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
</template>
|
||||||
</table>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
/* 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);
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,165 @@
|
||||||
|
/* 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 [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,30 +1,33 @@
|
||||||
/* 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 @@
|
||||||
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 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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 @@
|
||||||
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 @@
|
||||||
$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 @@
|
||||||
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 @@
|
||||||
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();
|
|
||||||
});
|
|
||||||
return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this));
|
|
||||||
},
|
|
||||||
checkChanged: function() {
|
|
||||||
const $checkedIssues = $('.selected_issue:checked');
|
|
||||||
const $updateIssuesIds = $('#update_issuable_ids');
|
|
||||||
const $issuesOtherFilters = $('.issues-other-filters');
|
|
||||||
const $issuesBulkUpdate = $('.issues_bulk_update');
|
|
||||||
|
|
||||||
this.issuableBulkActions.willUpdateLabels = false;
|
if (userCanBulkUpdate && !alreadyInitialized) {
|
||||||
this.issuableBulkActions.setOriginalDropdownData();
|
IssuableBulkUpdateActions.init({
|
||||||
|
prefixId: pagePrefix,
|
||||||
if ($checkedIssues.length > 0) {
|
|
||||||
const ids = $.map($checkedIssues, function(value) {
|
|
||||||
return $(value).data('id');
|
|
||||||
});
|
});
|
||||||
$updateIssuesIds.val(ids);
|
|
||||||
$issuesOtherFilters.hide();
|
|
||||||
$issuesBulkUpdate.show();
|
|
||||||
} else {
|
|
||||||
$updateIssuesIds.val([]);
|
|
||||||
$issuesBulkUpdate.hide();
|
|
||||||
$issuesOtherFilters.show();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
|
this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
|
||||||
|
}
|
||||||
|
},
|
||||||
resetIncomingEmailToken: function() {
|
resetIncomingEmailToken: function() {
|
||||||
$('.incoming-email-token-reset').on('click', function(e) {
|
$('.incoming-email-token-reset').on('click', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
|
@ -1,166 +0,0 @@
|
||||||
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
|
|
||||||
/* global Issuable */
|
|
||||||
/* global Flash */
|
|
||||||
|
|
||||||
((global) => {
|
|
||||||
class IssuableBulkActions {
|
|
||||||
constructor({ container, form, issues, prefixId } = {}) {
|
|
||||||
this.prefixId = prefixId || 'issue_';
|
|
||||||
this.form = form || this.getElement('.bulk-update');
|
|
||||||
this.$labelDropdown = this.form.find('.js-label-select');
|
|
||||||
this.issues = issues || this.getElement('.issues-list .issue');
|
|
||||||
this.form.data('bulkActions', this);
|
|
||||||
this.willUpdateLabels = false;
|
|
||||||
this.bindEvents();
|
|
||||||
// Fixes bulk-assign not working when navigating through pages
|
|
||||||
Issuable.initChecks();
|
|
||||||
}
|
|
||||||
|
|
||||||
bindEvents() {
|
|
||||||
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
onFormSubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
return this.submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
submit() {
|
|
||||||
const _this = this;
|
|
||||||
const xhr = $.ajax({
|
|
||||||
url: this.form.attr('action'),
|
|
||||||
method: this.form.attr('method'),
|
|
||||||
dataType: 'JSON',
|
|
||||||
data: this.getFormDataAsObject()
|
|
||||||
});
|
|
||||||
xhr.done(() => window.location.reload());
|
|
||||||
xhr.fail(() => new Flash("Issue update failed"));
|
|
||||||
return xhr.always(this.onFormSubmitAlways.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
onFormSubmitAlways() {
|
|
||||||
return this.form.find('[type="submit"]').enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedIssues() {
|
|
||||||
return this.issues.has('.selected_issue:checked');
|
|
||||||
}
|
|
||||||
|
|
||||||
getLabelsFromSelection() {
|
|
||||||
const labels = [];
|
|
||||||
this.getSelectedIssues().map(function() {
|
|
||||||
const labelsData = $(this).data('labels');
|
|
||||||
if (labelsData) {
|
|
||||||
return labelsData.map(function(labelId) {
|
|
||||||
if (labels.indexOf(labelId) === -1) {
|
|
||||||
return labels.push(labelId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will return only labels that were marked previously and the user has unmarked
|
|
||||||
* @return {Array} Label IDs
|
|
||||||
*/
|
|
||||||
|
|
||||||
getUnmarkedIndeterminedLabels() {
|
|
||||||
const result = [];
|
|
||||||
const labelsToKeep = this.$labelDropdown.data('indeterminate');
|
|
||||||
|
|
||||||
this.getLabelsFromSelection().forEach((id) => {
|
|
||||||
if (labelsToKeep.indexOf(id) === -1) {
|
|
||||||
result.push(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple form serialization, it will return just what we need
|
|
||||||
* Returns key/value pairs from form data
|
|
||||||
*/
|
|
||||||
|
|
||||||
getFormDataAsObject() {
|
|
||||||
const formData = {
|
|
||||||
update: {
|
|
||||||
state_event: this.form.find('input[name="update[state_event]"]').val(),
|
|
||||||
// For Merge Requests
|
|
||||||
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
|
|
||||||
// For Issues
|
|
||||||
assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
|
|
||||||
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
|
|
||||||
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
|
|
||||||
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
|
|
||||||
add_label_ids: [],
|
|
||||||
remove_label_ids: []
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (this.willUpdateLabels) {
|
|
||||||
formData.update.add_label_ids = this.$labelDropdown.data('marked');
|
|
||||||
formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
|
|
||||||
}
|
|
||||||
return formData;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOriginalDropdownData() {
|
|
||||||
const $labelSelect = $('.bulk-update .js-label-select');
|
|
||||||
$labelSelect.data('common', this.getOriginalCommonIds());
|
|
||||||
$labelSelect.data('marked', this.getOriginalMarkedIds());
|
|
||||||
$labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
|
|
||||||
}
|
|
||||||
|
|
||||||
// From issuable's initial bulk selection
|
|
||||||
getOriginalCommonIds() {
|
|
||||||
const labelIds = [];
|
|
||||||
|
|
||||||
this.getElement('.selected_issue:checked').each((i, el) => {
|
|
||||||
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
|
|
||||||
});
|
|
||||||
return _.intersection.apply(this, labelIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// From issuable's initial bulk selection
|
|
||||||
getOriginalMarkedIds() {
|
|
||||||
const labelIds = [];
|
|
||||||
this.getElement('.selected_issue:checked').each((i, el) => {
|
|
||||||
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
|
|
||||||
});
|
|
||||||
return _.intersection.apply(this, labelIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// From issuable's initial bulk selection
|
|
||||||
getOriginalIndeterminateIds() {
|
|
||||||
const uniqueIds = [];
|
|
||||||
const labelIds = [];
|
|
||||||
let issuableLabels = [];
|
|
||||||
|
|
||||||
// Collect unique label IDs for all checked issues
|
|
||||||
this.getElement('.selected_issue:checked').each((i, el) => {
|
|
||||||
issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
|
|
||||||
issuableLabels.forEach((labelId) => {
|
|
||||||
// Store unique IDs
|
|
||||||
if (uniqueIds.indexOf(labelId) === -1) {
|
|
||||||
uniqueIds.push(labelId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Store array of IDs per issuable
|
|
||||||
labelIds.push(issuableLabels);
|
|
||||||
});
|
|
||||||
// Add uniqueIds to add it as argument for _.intersection
|
|
||||||
labelIds.unshift(uniqueIds);
|
|
||||||
// Return IDs that are present but not in all selected issueables
|
|
||||||
return _.difference(uniqueIds, _.intersection.apply(this, labelIds));
|
|
||||||
}
|
|
||||||
|
|
||||||
getElement(selector) {
|
|
||||||
this.scopeEl = this.scopeEl || $('.content');
|
|
||||||
return this.scopeEl.find(selector);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
global.IssuableBulkActions = IssuableBulkActions;
|
|
||||||
})(window.gl || (window.gl = {}));
|
|
|
@ -2,6 +2,8 @@
|
||||||
/* global Issuable */
|
/* global 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 @@
|
||||||
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 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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';
|
||||||
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';
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
/* 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 @@
|
||||||
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 @@
|
||||||
|
|
||||||
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 @@
|
||||||
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 {
|
||||||
{{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 {
|
||||||
<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 @@
|
||||||
@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 {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled-content {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 @@
|
||||||
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 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.issues-holder .issue-check {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rss-btn {
|
.rss-btn {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
@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 @@
|
||||||
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 @@
|
||||||
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 @@
|
||||||
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;
|
||||||
$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 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 @@
|
||||||
& > .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 {
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
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
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
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 @@
|
||||||
|
|
||||||
- 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
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
- 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 @@
|
||||||
= 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 @@
|
||||||
.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 @@
|
||||||
.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
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
%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
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
- @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 @@
|
||||||
.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 @@
|
||||||
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
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
%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
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
- @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 @@
|
||||||
.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 @@
|
||||||
|
|
||||||
= 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 @@
|
||||||
.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
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
- 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
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
- 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 @@
|
||||||
= 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 @@
|
||||||
.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 @@
|
||||||
- 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 @@
|
||||||
|
|
||||||
.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 @@
|
||||||
= 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 @@
|
||||||
- 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 @@
|
||||||
.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")
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Introduce an Events API
|
||||||
|
merge_request: 11755
|
||||||
|
author:
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Automatically adjust project settings to match changes in project visibility
|
||||||
|
merge_request: 11831
|
||||||
|
author:
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Add a Rake task to aid in rotating otp_key_base
|
||||||
|
merge_request: 11881
|
||||||
|
author:
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: 'Introduce optimistic locking support via optional parameter last_commit_sha on File Update API'
|
||||||
|
merge_request: 11694
|
||||||
|
author: electroma
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Allow numeric pages domain
|
||||||
|
merge_request: 11550
|
||||||
|
author:
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Fixed style on unsubscribe page
|
||||||
|
merge_request:
|
||||||
|
author: Gustav Ernberg
|
|
@ -1,10 +1,7 @@
|
||||||
# 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
|
||||||
|
@ -514,8 +511,8 @@ configurable in future releases.
|
||||||
|
|
||||||
## Configure Container Registry notifications
|
## Configure Container Registry notifications
|
||||||
|
|
||||||
You can configure the Container Registry to send webhook notifications in
|
You can configure the Container Registry to send webhook notifications in
|
||||||
response to events happening within the registry.
|
response to events happening within the registry.
|
||||||
|
|
||||||
Read more about the Container Registry notifications config options in the
|
Read more about the Container Registry notifications config options in the
|
||||||
[Docker Registry notifications documentation][notifications-config].
|
[Docker Registry notifications documentation][notifications-config].
|
||||||
|
@ -568,12 +565,25 @@ notifications:
|
||||||
backoff: 1000
|
backoff: 1000
|
||||||
```
|
```
|
||||||
|
|
||||||
## Changelog
|
## Using self-signed certificates with Container Registry
|
||||||
|
|
||||||
**GitLab 8.8 ([source docs][8-8-docs])**
|
If you're using a self-signed certificate with your Container Registry, you
|
||||||
|
might encounter issues during the CI jobs like the following:
|
||||||
|
|
||||||
- GitLab Container Registry feature was introduced.
|
```
|
||||||
|
Error response from daemon: Get registry.example.com/v1/users/: x509: certificate signed by unknown authority
|
||||||
|
```
|
||||||
|
|
||||||
|
The Docker daemon running the command expects a cert signed by a recognized CA,
|
||||||
|
thus the error above.
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -589,4 +599,4 @@ notifications:
|
||||||
[existing-domain]: #configure-container-registry-under-an-existing-gitlab-domain
|
[existing-domain]: #configure-container-registry-under-an-existing-gitlab-domain
|
||||||
[new-domain]: #configure-container-registry-under-its-own-domain
|
[new-domain]: #configure-container-registry-under-its-own-domain
|
||||||
[notifications-config]: https://docs.docker.com/registry/notifications/
|
[notifications-config]: https://docs.docker.com/registry/notifications/
|
||||||
[registry-notifications-config]: https://docs.docker.com/registry/configuration/#notifications
|
[registry-notifications-config]: https://docs.docker.com/registry/configuration/#notifications
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -0,0 +1,347 @@
|
||||||
|
# Events
|
||||||
|
|
||||||
|
## Filter parameters
|
||||||
|
|
||||||
|
### Action Types
|
||||||
|
|
||||||
|
Available action types for the `action` parameter are:
|
||||||
|
|
||||||
|
- `created`
|
||||||
|
- `updated`
|
||||||
|
- `closed`
|
||||||
|
- `reopened`
|
||||||
|
- `pushed`
|
||||||
|
- `commented`
|
||||||
|
- `merged`
|
||||||
|
- `joined`
|
||||||
|
- `left`
|
||||||
|
- `destroyed`
|
||||||
|
- `expired`
|
||||||
|
|
||||||
|
Note that these options are downcased.
|
||||||
|
|
||||||
|
### Target Types
|
||||||
|
|
||||||
|
Available target types for the `target_type` parameter are:
|
||||||
|
|
||||||
|
- `issue`
|
||||||
|
- `milestone`
|
||||||
|
- `merge_request`
|
||||||
|
- `note`
|
||||||
|
- `project`
|
||||||
|
- `snippet`
|
||||||
|
- `user`
|
||||||
|
|
||||||
|
Note that these options are downcased.
|
||||||
|
|
||||||
|
### Date formatting
|
||||||
|
|
||||||
|
Dates for the `before` and `after` parameters should be supplied in the following format:
|
||||||
|
|
||||||
|
```
|
||||||
|
YYYY-MM-DD
|
||||||
|
```
|
||||||
|
|
||||||
|
## List currently authenticated user's events
|
||||||
|
|
||||||
|
>**Note:** This endpoint was introduced in GitLab 9.3.
|
||||||
|
|
||||||
|
Get a list of events for the authenticated user.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /events
|
||||||
|
```
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
| Attribute | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `action` | string | no | Include only events of a particular [action type][action-types] |
|
||||||
|
| `target_type` | string | no | Include only events of a particular [target type][target-types] |
|
||||||
|
| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
|
||||||
|
| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
|
||||||
|
| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
|
||||||
|
|
||||||
|
Example request:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl --header "PRIVATE-TOKEN 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01
|
||||||
|
```
|
||||||
|
|
||||||
|
Example response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"title":null,
|
||||||
|
"project_id":1,
|
||||||
|
"action_name":"opened",
|
||||||
|
"target_id":160,
|
||||||
|
"target_type":"Issue",
|
||||||
|
"author_id":25,
|
||||||
|
"data":null,
|
||||||
|
"target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.",
|
||||||
|
"created_at":"2017-02-09T10:43:19.667Z",
|
||||||
|
"author":{
|
||||||
|
"name":"User 3",
|
||||||
|
"username":"user3",
|
||||||
|
"id":25,
|
||||||
|
"state":"active",
|
||||||
|
"avatar_url":"http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80\u0026d=identicon",
|
||||||
|
"web_url":"https://gitlab.example.com/user3"
|
||||||
|
},
|
||||||
|
"author_username":"user3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title":null,
|
||||||
|
"project_id":1,
|
||||||
|
"action_name":"opened",
|
||||||
|
"target_id":159,
|
||||||
|
"target_type":"Issue",
|
||||||
|
"author_id":21,
|
||||||
|
"data":null,
|
||||||
|
"target_title":"Nostrum enim non et sed optio illo deleniti non.",
|
||||||
|
"created_at":"2017-02-09T10:43:19.426Z",
|
||||||
|
"author":{
|
||||||
|
"name":"Test User",
|
||||||
|
"username":"ted",
|
||||||
|
"id":21,
|
||||||
|
"state":"active",
|
||||||
|
"avatar_url":"http://www.gravatar.com/avatar/80fb888c9a48b9a3f87477214acaa63f?s=80\u0026d=identicon",
|
||||||
|
"web_url":"https://gitlab.example.com/ted"
|
||||||
|
},
|
||||||
|
"author_username":"ted"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get user contribution events
|
||||||
|
|
||||||
|
>**Note:** Documentation was formerly located in the [Users API pages][users-api].
|
||||||
|
|
||||||
|
Get the contribution events for the specified user, sorted from newest to oldest.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /users/:id/events
|
||||||
|
```
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
| Attribute | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `id` | integer | yes | The ID or Username of the user |
|
||||||
|
| `action` | string | no | Include only events of a particular [action type][action-types] |
|
||||||
|
| `target_type` | string | no | Include only events of a particular [target type][target-types] |
|
||||||
|
| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
|
||||||
|
| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
|
||||||
|
| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
|
||||||
|
|
||||||
|
```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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## List a Project's visible events
|
||||||
|
|
||||||
|
>**Note:** This endpoint has been around longer than the others. Documentation was formerly located in the [Projects API pages][projects-api].
|
||||||
|
|
||||||
|
Get a list of visible events for a particular project.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /:project_id/events
|
||||||
|
```
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
| Attribute | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
|
||||||
|
| `action` | string | no | Include only events of a particular [action type][action-types] |
|
||||||
|
| `target_type` | string | no | Include only events of a particular [target type][target-types] |
|
||||||
|
| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
|
||||||
|
| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
|
||||||
|
| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
|
||||||
|
|
||||||
|
Example request:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl --header "PRIVATE-TOKEN 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:project_id/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01
|
||||||
|
```
|
||||||
|
|
||||||
|
Example response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"title":null,
|
||||||
|
"project_id":1,
|
||||||
|
"action_name":"opened",
|
||||||
|
"target_id":160,
|
||||||
|
"target_type":"Issue",
|
||||||
|
"author_id":25,
|
||||||
|
"data":null,
|
||||||
|
"target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.",
|
||||||
|
"created_at":"2017-02-09T10:43:19.667Z",
|
||||||
|
"author":{
|
||||||
|
"name":"User 3",
|
||||||
|
"username":"user3",
|
||||||
|
"id":25,
|
||||||
|
"state":"active",
|
||||||
|
"avatar_url":"http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80\u0026d=identicon",
|
||||||
|
"web_url":"https://gitlab.example.com/user3"
|
||||||
|
},
|
||||||
|
"author_username":"user3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title":null,
|
||||||
|
"project_id":1,
|
||||||
|
"action_name":"opened",
|
||||||
|
"target_id":159,
|
||||||
|
"target_type":"Issue",
|
||||||
|
"author_id":21,
|
||||||
|
"data":null,
|
||||||
|
"target_title":"Nostrum enim non et sed optio illo deleniti non.",
|
||||||
|
"created_at":"2017-02-09T10:43:19.426Z",
|
||||||
|
"author":{
|
||||||
|
"name":"Test User",
|
||||||
|
"username":"ted",
|
||||||
|
"id":21,
|
||||||
|
"state":"active",
|
||||||
|
"avatar_url":"http://www.gravatar.com/avatar/80fb888c9a48b9a3f87477214acaa63f?s=80\u0026d=identicon",
|
||||||
|
"web_url":"https://gitlab.example.com/ted"
|
||||||
|
},
|
||||||
|
"author_username":"ted"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
[target-types]: #target-types "Target Type parameter"
|
||||||
|
[action-types]: #action-types "Action Type parameter"
|
||||||
|
[date-formatting]: #date-formatting "Date Formatting guidance"
|
||||||
|
[projects-api]: projects.md "Projects API pages"
|
||||||
|
[users-api]: users.md "Users API pages"
|
|
@ -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:
|
||||||
- `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:
|
||||||
|
|
141
doc/api/users.md
141
doc/api/users.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
# 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:
|
||||||
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
|
||||||
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
|
||||||
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"
|
||||||
[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:
|
||||||
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.
|
||||||
|
|
||||||
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:
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
### Components
|
### Components
|
||||||
|
|
||||||
![GitLab Diagram Overview](gitlab_architecture_diagram.png)
|
<img src="https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/pub?w=987&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
|
||||||
|
|
||||||
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.
|
||||||
>**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
|
||||||
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>
|
||||||
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
|
||||||
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.
|
||||||
## 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
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.7 KiB |
|
@ -1,10 +1,8 @@
|
||||||
# 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,18 +6,13 @@ 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)
|
![Project services list](img/project_services.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)
|
|
||||||
|
|
||||||
Below, you will find a list of the currently supported ones accompanied with
|
Below, you will find a list of the currently supported ones accompanied with
|
||||||
comprehensive documentation.
|
comprehensive documentation.
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# 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:
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
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.
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
# 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.
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
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
|
||||||
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
|
||||||
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
|
||||||
authorize! :push_code, user_project
|
authorize! :push_code, user_project
|
||||||
|
|
||||||
file_params = declared_params(include_missing: false)
|
file_params = declared_params(include_missing: false)
|
||||||
result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute
|
|
||||||
|
begin
|
||||||
|
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
|
||||||
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
|
||||||
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
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
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
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
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'
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
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
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
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', () => {
|
||||||
|
|
||||||
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', () => {
|
||||||
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', () => {
|
||||||
// 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', () => {
|
||||||
},
|
},
|
||||||
}).$mount();
|
}).$mount();
|
||||||
|
|
||||||
expect(component.$el.tagName).toEqual('TABLE');
|
expect(component.$el.getAttribute('class')).toContain('ci-table');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
%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'}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* 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';
|
||||||
|
|
||||||
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';
|
||||||
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';
|
||||||
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';
|
||||||
// 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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
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
|
||||||
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
|
||||||
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,
|
||||||
|
'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
|
||||||
|
|
||||||
it { is_expected.to be_valid }
|
let(:domain) { value }
|
||||||
end
|
|
||||||
|
|
||||||
context 'valid hexadecimal-looking domain' do
|
it { expect(pages_domain.valid?).to eq(validity) }
|
||||||
let(:domain) { '0x12345.com'}
|
end
|
||||||
|
|
||||||
it { is_expected.to be_valid }
|
|
||||||
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
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue