Commit b3ec93e2 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge remote-tracking branch 'ce-com/master' into ce-to-ee

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parents ba01638b a3b5381e
...@@ -206,7 +206,36 @@ rake config_lint: *exec ...@@ -206,7 +206,36 @@ rake config_lint: *exec
rake brakeman: *exec rake brakeman: *exec
rake flay: *exec rake flay: *exec
license_finder: *exec license_finder: *exec
<<<<<<< HEAD
rake downtime_check: *exec rake downtime_check: *exec
=======
rake downtime_check:
<<: *exec
except:
- master
- tags
- /^[\d-]+-stable(-ee)?$/
rake ee_compat_check:
<<: *exec
only:
- branches@gitlab-org/gitlab-ce
except:
- master
- tags
- /^[\d-]+-stable(-ee)?$/
allow_failure: yes
cache:
key: "ee_compat_check_repo"
paths:
- ee_compat_check/ee-repo/
artifacts:
name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
when: on_failure
expire_in: 10d
paths:
- ee_compat_check/patches/*.patch
>>>>>>> ce-com/master
rake db:migrate:reset: rake db:migrate:reset:
stage: test stage: test
......
...@@ -139,7 +139,10 @@ entry. ...@@ -139,7 +139,10 @@ entry.
- Fix API group/issues default state filter. (Alexander Randa) - Fix API group/issues default state filter. (Alexander Randa)
- Prevent builds dropdown to close when the user clicks in a build. - Prevent builds dropdown to close when the user clicks in a build.
- Display all closed issues in “done” board list. - Display all closed issues in “done” board list.
<<<<<<< HEAD
- added focus mode button to issue boards. - added focus mode button to issue boards.
=======
>>>>>>> ce-com/master
- Remove no-new annotation from file_template_mediator.js. - Remove no-new annotation from file_template_mediator.js.
- Changed dropdown style slightly. - Changed dropdown style slightly.
- Change gfm textarea to use monospace font. - Change gfm textarea to use monospace font.
......
9.1.0-pre 9.2.0-pre
function BlobForkSuggestion(openButton, cancelButton, suggestionSection) { const defaults = {
if (openButton) { // Buttons that will show the `suggestionSections`
openButton.addEventListener('click', () => { // has `data-fork-path`, and `data-action`
openButtons: [],
// Update the href(from `openButton` -> `data-fork-path`)
// whenever a `openButton` is clicked
forkButtons: [],
// Buttons to hide the `suggestionSections`
cancelButtons: [],
// Section to show/hide
suggestionSections: [],
// Pieces of text that need updating depending on the action, `edit`, `replace`, `delete`
actionTextPieces: [],
};
class BlobForkSuggestion {
constructor(options) {
this.elementMap = Object.assign({}, defaults, options);
this.onClickWrapper = this.onClick.bind(this);
document.addEventListener('click', this.onClickWrapper);
}
showSuggestionSection(forkPath, action = 'edit') {
[].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => {
suggestionSection.classList.remove('hidden'); suggestionSection.classList.remove('hidden');
}); });
[].forEach.call(this.elementMap.forkButtons, (forkButton) => {
forkButton.setAttribute('href', forkPath);
});
[].forEach.call(this.elementMap.actionTextPieces, (actionTextPiece) => {
// eslint-disable-next-line no-param-reassign
actionTextPiece.textContent = action;
});
} }
if (cancelButton) { hideSuggestionSection() {
cancelButton.addEventListener('click', () => { [].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => {
suggestionSection.classList.add('hidden'); suggestionSection.classList.add('hidden');
}); });
} }
onClick(e) {
const el = e.target;
if ([].includes.call(this.elementMap.openButtons, el)) {
const { forkPath, action } = el.dataset;
this.showSuggestionSection(forkPath, action);
}
if ([].includes.call(this.elementMap.cancelButtons, el)) {
this.hideSuggestionSection();
}
}
destroy() {
document.removeEventListener('click', this.onClickWrapper);
}
} }
export default BlobForkSuggestion; export default BlobForkSuggestion;
// ECMAScript polyfills // ECMAScript polyfills
import 'core-js/fn/array/find'; import 'core-js/fn/array/find';
import 'core-js/fn/array/from'; import 'core-js/fn/array/from';
import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign'; import 'core-js/fn/object/assign';
import 'core-js/fn/promise'; import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at'; import 'core-js/fn/string/code-point-at';
......
...@@ -97,11 +97,13 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -97,11 +97,13 @@ const ShortcutsBlob = require('./shortcuts_blob');
fileBlobPermalinkUrl, fileBlobPermalinkUrl,
}); });
new BlobForkSuggestion( new BlobForkSuggestion({
document.querySelector('.js-edit-blob-link-fork-toggler'), openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'),
document.querySelector('.js-cancel-fork-suggestion'), forkButtons: document.querySelectorAll('.js-fork-suggestion-button'),
document.querySelector('.js-file-fork-suggestion-section'), cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'),
); suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'),
actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'),
});
} }
switch (page) { switch (page) {
......
...@@ -170,8 +170,9 @@ class DueDateSelectors { ...@@ -170,8 +170,9 @@ class DueDateSelectors {
const $datePicker = $(this); const $datePicker = $(this);
const calendar = new Pikaday({ const calendar = new Pikaday({
field: $datePicker.get(0), field: $datePicker.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd', format: 'yyyy-mm-dd',
container: $datePicker.parent().get(0),
onSelect(dateText) { onSelect(dateText) {
$datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
} }
......
...@@ -4,9 +4,9 @@ import '../../lib/utils/text_utility'; ...@@ -4,9 +4,9 @@ import '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue'; import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue'; import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue'; import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback'; import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button.vue'; import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring'; import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit'; import CommitComponent from '../../vue_shared/components/commit';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -441,6 +441,7 @@ export default { ...@@ -441,6 +441,7 @@ export default {
<template> <template>
<tr :class="{ 'js-child-row': model.isChildren }"> <tr :class="{ 'js-child-row': model.isChildren }">
<td> <td>
<<<<<<< HEAD:app/assets/javascripts/environments/components/environment_item.vue
<span <span
class="deploy-board-icon" class="deploy-board-icon"
v-if="model.hasDeployBoard" v-if="model.hasDeployBoard"
...@@ -457,6 +458,8 @@ export default { ...@@ -457,6 +458,8 @@ export default {
aria-hidden="true" /> aria-hidden="true" />
</span> </span>
=======
>>>>>>> ce-com/master:app/assets/javascripts/environments/components/environment_item.vue
<a <a
v-if="!model.isFolder" v-if="!model.isFolder"
class="environment-name" class="environment-name"
......
<script>
/** /**
* Renders the Monitoring (Metrics) link in environments table. * Renders the Monitoring (Metrics) link in environments table.
*/ */
...@@ -5,7 +6,6 @@ export default { ...@@ -5,7 +6,6 @@ export default {
props: { props: {
monitoringUrl: { monitoringUrl: {
type: String, type: String,
default: '',
required: true, required: true,
}, },
}, },
...@@ -15,16 +15,19 @@ export default { ...@@ -15,16 +15,19 @@ export default {
return 'Monitoring'; return 'Monitoring';
}, },
}, },
template: `
<a
class="btn monitoring-url has-tooltip"
data-container="body"
:href="monitoringUrl"
rel="noopener noreferrer nofollow"
:title="title"
:aria-label="title">
<i class="fa fa-area-chart" aria-hidden="true"></i>
</a>
`,
}; };
</script>
<template>
<a
class="btn monitoring-url has-tooltip"
data-container="body"
target="_blank"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"
:title="title"
:aria-label="title">
<i
class="fa fa-area-chart"
aria-hidden="true" />
</a>
</template>
<script>
/* global Flash */ /* global Flash */
/* eslint-disable no-new */ /* eslint-disable no-new */
/** /**
...@@ -50,21 +51,25 @@ export default { ...@@ -50,21 +51,25 @@ export default {
}); });
}, },
}, },
};
</script>
<template>
<button
type="button"
class="btn"
@click="onClick"
:disabled="isLoading">
template: ` <span v-if="isLastDeployment">
<button type="button" Re-deploy
class="btn" </span>
@click="onClick" <span v-else>
:disabled="isLoading"> Rollback
</span>
<span v-if="isLastDeployment">
Re-deploy
</span>
<span v-else>
Rollback
</span>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i> <i
</button> v-if="isLoading"
`, class="fa fa-spinner fa-spin"
}; aria-hidden="true" />
</button>
</template>
...@@ -3,12 +3,18 @@ ...@@ -3,12 +3,18 @@
* Render environments table. * Render environments table.
*/ */
import EnvironmentTableRowComponent from './environment_item.vue'; import EnvironmentTableRowComponent from './environment_item.vue';
<<<<<<< HEAD
import DeployBoard from './deploy_board_component'; import DeployBoard from './deploy_board_component';
=======
>>>>>>> ce-com/master
export default { export default {
components: { components: {
'environment-item': EnvironmentTableRowComponent, 'environment-item': EnvironmentTableRowComponent,
<<<<<<< HEAD
DeployBoard, DeployBoard,
=======
>>>>>>> ce-com/master
}, },
props: { props: {
...@@ -40,6 +46,7 @@ export default { ...@@ -40,6 +46,7 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
<<<<<<< HEAD
toggleDeployBoard: { toggleDeployBoard: {
type: Function, type: Function,
...@@ -52,6 +59,8 @@ export default { ...@@ -52,6 +59,8 @@ export default {
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
=======
>>>>>>> ce-com/master
}, },
methods: { methods: {
...@@ -92,6 +101,7 @@ export default { ...@@ -92,6 +101,7 @@ export default {
:model="model" :model="model"
:can-create-deployment="canCreateDeployment" :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
<<<<<<< HEAD
:service="service" :service="service"
:toggleDeployBoard="toggleDeployBoard" :toggleDeployBoard="toggleDeployBoard"
/> />
...@@ -107,6 +117,9 @@ export default { ...@@ -107,6 +117,9 @@ export default {
/> />
</td> </td>
</tr> </tr>
=======
:service="service" />
>>>>>>> ce-com/master
<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"> <tr v-if="isLoadingFolderContent">
......
...@@ -41,8 +41,9 @@ ...@@ -41,8 +41,9 @@
if ($issuableDueDate.length) { if ($issuableDueDate.length) {
calendar = new Pikaday({ calendar = new Pikaday({
field: $issuableDueDate.get(0), field: $issuableDueDate.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd', format: 'yyyy-mm-dd',
container: $issuableDueDate.parent().get(0),
onSelect: function(dateText) { onSelect: function(dateText) {
$issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); $issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
} }
......
...@@ -34,17 +34,6 @@ export default { ...@@ -34,17 +34,6 @@ export default {
}; };
}, },
methods: { methods: {
fetch() {
this.poll.makeRequest();
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
},
renderResponse(res) { renderResponse(res) {
const body = JSON.parse(res.body); const body = JSON.parse(res.body);
this.triggerAnimation(body); this.triggerAnimation(body);
...@@ -71,7 +60,17 @@ export default { ...@@ -71,7 +60,17 @@ export default {
}, },
}, },
created() { created() {
this.fetch(); if (!Visibility.hidden()) {
this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
}, },
}; };
</script> </script>
......
...@@ -169,7 +169,10 @@ ...@@ -169,7 +169,10 @@
w.gl.utils.getSelectedFragment = () => { w.gl.utils.getSelectedFragment = () => {
const selection = window.getSelection(); const selection = window.getSelection();
if (selection.rangeCount === 0) return null; if (selection.rangeCount === 0) return null;
const documentFragment = selection.getRangeAt(0).cloneContents(); const documentFragment = document.createDocumentFragment();
for (let i = 0; i < selection.rangeCount; i += 1) {
documentFragment.appendChild(selection.getRangeAt(i).cloneContents());
}
if (documentFragment.textContent.length === 0) return null; if (documentFragment.textContent.length === 0) return null;
return documentFragment; return documentFragment;
......
...@@ -18,9 +18,10 @@ ...@@ -18,9 +18,10 @@
const calendar = new Pikaday({ const calendar = new Pikaday({
field: $input.get(0), field: $input.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd', format: 'yyyy-mm-dd',
minDate: new Date(), minDate: new Date(),
container: $input.parent().get(0),
onSelect(dateText) { onSelect(dateText) {
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); $input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
......
...@@ -57,8 +57,11 @@ import findAndFollowLink from './shortcuts_dashboard_navigation'; ...@@ -57,8 +57,11 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
Shortcuts.prototype.toggleMarkdownPreview = function(e) { Shortcuts.prototype.toggleMarkdownPreview = function(e) {
// Check if short-cut was triggered while in Write Mode // Check if short-cut was triggered while in Write Mode
if ($(e.target).hasClass('js-note-text')) { const $target = $(e.target);
$('.js-md-preview-button').focus(); const $form = $target.closest('form');
if ($target.hasClass('js-note-text')) {
$('.js-md-preview-button', $form).focus();
} }
return $(document).triggerHandler('markdown-preview:toggle', [e]); return $(document).triggerHandler('markdown-preview:toggle', [e]);
}; };
......
...@@ -94,15 +94,17 @@ content on the Users#show page. ...@@ -94,15 +94,17 @@ content on the Users#show page.
e.preventDefault(); e.preventDefault();
$('.tab-pane.active').empty(); $('.tab-pane.active').empty();
this.loadTab($(e.target).attr('href'), this.getCurrentAction()); const endpoint = $(e.target).attr('href');
this.loadTab(this.getCurrentAction(), endpoint);
} }
tabShown(event) { tabShown(event) {
const $target = $(event.target); const $target = $(event.target);
const action = $target.data('action'); const action = $target.data('action');
const source = $target.attr('href'); const source = $target.attr('href');
this.setTab(source, action); const endpoint = $target.data('endpoint');
return this.setCurrentAction(source, action); this.setTab(action, endpoint);
return this.setCurrentAction(source);
} }
activateTab(action) { activateTab(action) {
...@@ -110,27 +112,27 @@ content on the Users#show page. ...@@ -110,27 +112,27 @@ content on the Users#show page.
.tab('show'); .tab('show');
} }
setTab(source, action) { setTab(action, endpoint) {
if (this.loaded[action]) { if (this.loaded[action]) {
return; return;
} }
if (action === 'activity') { if (action === 'activity') {
this.loadActivities(source); this.loadActivities();
} }
const loadableActions = ['groups', 'contributed', 'projects', 'snippets']; const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
if (loadableActions.indexOf(action) > -1) { if (loadableActions.indexOf(action) > -1) {
return this.loadTab(source, action); return this.loadTab(action, endpoint);
} }
} }
loadTab(source, action) { loadTab(action, endpoint) {
return $.ajax({ return $.ajax({
beforeSend: () => this.toggleLoading(true), beforeSend: () => this.toggleLoading(true),
complete: () => this.toggleLoading(false), complete: () => this.toggleLoading(false),
dataType: 'json', dataType: 'json',
type: 'GET', type: 'GET',
url: source, url: endpoint,
success: (data) => { success: (data) => {
const tabSelector = `div#${action}`; const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html); this.$parentEl.find(tabSelector).html(data.html);
...@@ -140,7 +142,7 @@ content on the Users#show page. ...@@ -140,7 +142,7 @@ content on the Users#show page.
}); });
} }
loadActivities(source) { loadActivities() {
if (this.loaded['activity']) { if (this.loaded['activity']) {
return; return;
} }
...@@ -155,7 +157,7 @@ content on the Users#show page. ...@@ -155,7 +157,7 @@ content on the Users#show page.
.toggle(status); .toggle(status);
} }
setCurrentAction(source, action) { setCurrentAction(source) {
let new_state = source; let new_state = source;
new_state = new_state.replace(/\/+$/, ''); new_state = new_state.replace(/\/+$/, '');
new_state += this._location.search + this._location.hash; new_state += this._location.search + this._location.hash;
......
...@@ -230,7 +230,6 @@ ...@@ -230,7 +230,6 @@
float: right; float: right;
margin-top: 8px; margin-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
border-bottom: 1px solid $border-color;
} }
} }
......
...@@ -14,14 +14,32 @@ ...@@ -14,14 +14,32 @@
} }
} }
@mixin set-visible {
transform: translateY(0);
visibility: visible;
opacity: 1;
transition-duration: 100ms, 150ms, 25ms;
transition-delay: 35ms, 50ms, 25ms;
}
@mixin set-invisible {
transform: translateY(-10px);
visibility: hidden;
opacity: 0;
transition-property: opacity, transform, visibility;
transition-duration: 70ms, 250ms, 250ms;
transition-timing-function: linear, $dropdown-animation-timing;
transition-delay: 25ms, 50ms, 0ms;
}
.open { .open {
.dropdown-menu, .dropdown-menu,
.dropdown-menu-nav { .dropdown-menu-nav {
display: block; display: block;
@include set-visible;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
width: 100%; width: 100%;
min-width: 240px;
} }
} }
...@@ -161,8 +179,9 @@ ...@@ -161,8 +179,9 @@
.dropdown-menu, .dropdown-menu,
.dropdown-menu-nav { .dropdown-menu-nav {
display: none; display: block;
position: absolute; position: absolute;
width: 100%;
top: 100%; top: 100%;
left: 0; left: 0;
z-index: 9; z-index: 9;
...@@ -176,6 +195,12 @@ ...@@ -176,6 +195,12 @@
border: 1px solid $dropdown-border-color; border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base; border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color; box-shadow: 0 2px 4px $dropdown-shadow-color;
overflow: hidden;
@include set-invisible;
@media (max-width: $screen-sm-min) {
width: 100%;
}
&.is-loading { &.is-loading {
.dropdown-content { .dropdown-content {
...@@ -252,6 +277,23 @@ ...@@ -252,6 +277,23 @@
} }
} }
.filtered-search-box-input-container .dropdown-menu,
.filtered-search-box-input-container .dropdown-menu-nav,
.comment-type-dropdown .dropdown-menu {
display: none;
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.filtered-search-box-input-container {
.dropdown-menu,
.dropdown-menu-nav {
max-width: 280px;
width: auto;
}
}
.dropdown-menu-drop-up { .dropdown-menu-drop-up {
top: auto; top: auto;
bottom: 100%; bottom: 100%;
...@@ -326,6 +368,10 @@ ...@@ -326,6 +368,10 @@
.dropdown-select { .dropdown-select {
width: $dropdown-width; width: $dropdown-width;
@media (max-width: $screen-sm-min) {
width: 100%;
}
} }
.dropdown-menu-align-right { .dropdown-menu-align-right {
...@@ -568,3 +614,24 @@ ...@@ -568,3 +614,24 @@
.droplab-item-ignore { .droplab-item-ignore {
pointer-events: none; pointer-events: none;
} }
.pika-single.animate-picker.is-bound,
.pika-single.animate-picker.is-bound.is-hidden {
/*
* Having `!important` is not recommended but
* since `pikaday` sets positioning inline
* there's no way it can be gracefully overridden
* using config options.
*/
position: absolute !important;
display: block;
}
.pika-single.animate-picker.is-bound {
@include set-visible;
}
.pika-single.animate-picker.is-bound.is-hidden {
@include set-invisible;
overflow: hidden;
}
...@@ -329,6 +329,7 @@ header { ...@@ -329,6 +329,7 @@ header {
.header-user { .header-user {
.dropdown-menu-nav { .dropdown-menu-nav {
width: auto;
min-width: 140px; min-width: 140px;
margin-top: -5px; margin-top: -5px;
......
...@@ -110,7 +110,7 @@ ...@@ -110,7 +110,7 @@
.top-area { .top-area {
@include clearfix; @include clearfix;
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $border-color;
.nav-text { .nav-text {
padding-top: 16px; padding-top: 16px;
......
...@@ -338,3 +338,32 @@ h4 { ...@@ -338,3 +338,32 @@ h4 {
.idiff.addition { .idiff.addition {
background: $line-added-dark; background: $line-added-dark;
} }
/**
* form text input i.e. search bar, comments, forms, etc.
*/
input,
textarea {
&::-webkit-input-placeholder {
color: $placeholder-text-color;
}
// support firefox 19+ vendor prefix
&::-moz-placeholder {
color: $placeholder-text-color;
opacity: 1; // FF defaults to 0.54
}
// scss-lint:disable PseudoElement
// support Edge vendor prefix
&::-ms-input-placeholder {
color: $placeholder-text-color;
}
// scss-lint:disable PseudoElement
// support IE vendor prefix
&:-ms-input-placeholder {
color: $placeholder-text-color;
}
}
...@@ -111,6 +111,7 @@ $gl-gray: $gl-text-color; ...@@ -111,6 +111,7 @@ $gl-gray: $gl-text-color;
$gl-gray-dark: #313236; $gl-gray-dark: #313236;
$gl-header-color: #4c4e54; $gl-header-color: #4c4e54;
$gl-header-nav-hover-color: #434343; $gl-header-nav-hover-color: #434343;
$placeholder-text-color: rgba(0, 0, 0, .42);
/* /*
* Lists * Lists
...@@ -564,3 +565,8 @@ $filter-name-text-color: rgba(0, 0, 0, 0.55); ...@@ -564,3 +565,8 @@ $filter-name-text-color: rgba(0, 0, 0, 0.55);
$filter-value-text-color: rgba(0, 0, 0, 0.85); $filter-value-text-color: rgba(0, 0, 0, 0.85);
$filter-name-selected-color: #ebebeb; $filter-name-selected-color: #ebebeb;
$filter-value-selected-color: #d7d7d7; $filter-value-selected-color: #d7d7d7;
/*
Animation Functions
*/
$dropdown-animation-timing: cubic-bezier(0.23, 1, 0.32, 1);
...@@ -124,7 +124,13 @@ input[type="checkbox"]:hover { ...@@ -124,7 +124,13 @@ input[type="checkbox"]:hover {
// Custom dropdown positioning // Custom dropdown positioning
.dropdown-menu { .dropdown-menu {
top: 37px; transition-property: opacity, transform;
transition-duration: 250ms, 250ms;
transition-delay: 0ms, 25ms;
transition-timing-function: $dropdown-animation-timing;
transform: translateY(0);
opacity: 0;
display: block;
left: -5px; left: -5px;
padding: 0; padding: 0;
...@@ -156,6 +162,13 @@ input[type="checkbox"]:hover { ...@@ -156,6 +162,13 @@ input[type="checkbox"]:hover {
color: $layout-link-gray; color: $layout-link-gray;
} }
} }
.dropdown-menu {
transition-duration: 100ms, 75ms;
transition-delay: 75ms, 100ms;
transform: translateY(13px);
opacity: 1;
}
} }
&.has-value { &.has-value {
......
...@@ -43,9 +43,13 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -43,9 +43,13 @@ class Admin::GroupsController < Admin::ApplicationController
end end
def members_update def members_update
@group.add_users(params[:user_ids].split(','), params[:access_level], current_user: current_user) status = Members::CreateService.new(@group, current_user, params).execute
redirect_to [:admin, @group], notice: 'Users were successfully added.' if status
redirect_to [:admin, @group], notice: 'Users were successfully added.'
else
redirect_to [:admin, @group], alert: 'No users specified.'
end
end end
def destroy def destroy
......
module MembershipActions module MembershipActions
extend ActiveSupport::Concern extend ActiveSupport::Concern
def create
status = Members::CreateService.new(membershipable, current_user, params).execute
redirect_url = members_page_url
if status
redirect_to redirect_url, notice: 'Users were successfully added.'
else
redirect_to redirect_url, alert: 'No users specified.'
end
end
def destroy
Members::DestroyService.new(membershipable, current_user, params).
execute(:all)
respond_to do |format|
format.html do
message = "User was successfully removed from #{source_type}."
redirect_to members_page_url, notice: message
end
format.js { head :ok }
end
end
def request_access def request_access
membershipable.request_access(current_user) membershipable.request_access(current_user)
...@@ -13,14 +39,13 @@ module MembershipActions ...@@ -13,14 +39,13 @@ module MembershipActions
log_audit_event(member, action: :create) log_audit_event(member, action: :create)
redirect_to polymorphic_url([membershipable, :members]) redirect_to members_page_url
end end
def leave def leave
member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id). member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id).
execute(:all) execute(:all)
source_type = membershipable.class.to_s.humanize(capitalize: false)
notice = notice =
if member.request? if member.request?
"Your access request to the #{source_type} has been withdrawn." "Your access request to the #{source_type} has been withdrawn."
...@@ -28,8 +53,11 @@ module MembershipActions ...@@ -28,8 +53,11 @@ module MembershipActions
"You left the \"#{membershipable.human_name}\" #{source_type}." "You left the \"#{membershipable.human_name}\" #{source_type}."
end end
<<<<<<< HEAD
log_audit_event(member, action: :destroy) unless member.request? log_audit_event(member, action: :destroy) unless member.request?
=======
>>>>>>> ce-com/master
redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize] redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
redirect_to redirect_path, notice: notice redirect_to redirect_path, notice: notice
...@@ -41,8 +69,21 @@ module MembershipActions ...@@ -41,8 +69,21 @@ module MembershipActions
raise NotImplementedError raise NotImplementedError
end end
<<<<<<< HEAD
def log_audit_event(member, options = {}) def log_audit_event(member, options = {})
AuditEventService.new(current_user, membershipable, options) AuditEventService.new(current_user, membershipable, options)
.for_member(member).security_event .for_member(member).security_event
=======
def members_page_url
if membershipable.is_a?(Project)
project_settings_members_path(membershipable)
else
polymorphic_url([membershipable, :members])
end
end
def source_type
@source_type ||= membershipable.class.to_s.humanize(capitalize: false)
>>>>>>> ce-com/master
end end
end end
...@@ -24,6 +24,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -24,6 +24,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
@group_member = @group.group_members.new @group_member = @group.group_members.new
end end
<<<<<<< HEAD
def create def create
if params[:user_ids].blank? if params[:user_ids].blank?
return redirect_to(group_group_members_path(@group), alert: 'No users specified.') return redirect_to(group_group_members_path(@group), alert: 'No users specified.')
...@@ -45,6 +46,8 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -45,6 +46,8 @@ class Groups::GroupMembersController < Groups::ApplicationController
redirect_to group_group_members_path(@group), notice: 'Users were successfully added.' redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
end end
=======
>>>>>>> ce-com/master
def update def update
@group_member = @group.group_members.find(params[:id]) @group_member = @group.group_members.find(params[:id])
...@@ -57,6 +60,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -57,6 +60,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end end
end end
<<<<<<< HEAD
def destroy def destroy
member = Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all) member = Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all)
...@@ -68,6 +72,8 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -68,6 +72,8 @@ class Groups::GroupMembersController < Groups::ApplicationController
end end
end end
=======
>>>>>>> ce-com/master
def resend_invite def resend_invite
redirect_path = group_group_members_path(@group) redirect_path = group_group_members_path(@group)
......
...@@ -10,6 +10,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -10,6 +10,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort) redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort)
end end
<<<<<<< HEAD
def create def create
status = Members::CreateService.new(@project, current_user, params).execute status = Members::CreateService.new(@project, current_user, params).execute
...@@ -28,6 +29,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -28,6 +29,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
end end
=======
>>>>>>> ce-com/master
def update def update
@project_member = @project.project_members.find(params[:id]) @project_member = @project.project_members.find(params[:id])
...@@ -40,6 +43,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -40,6 +43,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
end end
<<<<<<< HEAD
def destroy def destroy
member = Members::DestroyService.new(@project, current_user, params). member = Members::DestroyService.new(@project, current_user, params).
execute(:all) execute(:all)
...@@ -54,6 +58,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -54,6 +58,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end end
end end
=======
>>>>>>> ce-com/master
def resend_invite def resend_invite
redirect_path = namespace_project_settings_members_path(@project.namespace, @project) redirect_path = namespace_project_settings_members_path(@project.namespace, @project)
......
...@@ -14,15 +14,6 @@ module BlobHelper ...@@ -14,15 +14,6 @@ module BlobHelper
options[:link_opts]) options[:link_opts])
end end
def fork_path(project = @project, ref = @ref, path = @path, options = {})
continue_params = {
to: edit_path,
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
end
def edit_blob_link(project = @project, ref = @ref, path = @path, options = {}) def edit_blob_link(project = @project, ref = @ref, path = @path, options = {})
blob = options.delete(:blob) blob = options.delete(:blob)
blob ||= project.repository.blob_at(ref, path) rescue nil blob ||= project.repository.blob_at(ref, path) rescue nil
...@@ -37,7 +28,16 @@ module BlobHelper ...@@ -37,7 +28,16 @@ module BlobHelper
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref)) elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm" link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
elsif current_user && can?(current_user, :fork_project, project) elsif current_user && can?(current_user, :fork_project, project)
button_tag 'Edit', class: "#{common_classes} js-edit-blob-link-fork-toggler" continue_params = {
to: edit_path,
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
button_tag 'Edit',
class: "#{common_classes} js-edit-blob-link-fork-toggler",
data: { action: 'edit', fork_path: fork_path }
end end
end end
...@@ -48,21 +48,25 @@ module BlobHelper ...@@ -48,21 +48,25 @@ module BlobHelper
return unless blob return unless blob
common_classes = "btn btn-#{btn_class}"
if !on_top_of_branch?(project, ref) if !on_top_of_branch?(project, ref)
button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' } button_tag label, class: "#{common_classes} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
elsif blob.lfs_pointer? elsif blob.lfs_pointer?
button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' } button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
elsif can_modify_blob?(blob, project, ref) elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
elsif can?(current_user, :fork_project, project) elsif can?(current_user, :fork_project, project)
continue_params = { continue_params = {
to: request.fullpath, to: request.fullpath,
notice: edit_in_new_fork_notice + " Try to #{action} this file again.", notice: edit_in_new_fork_notice + " Try to #{action} this file again.",
notice_now: edit_in_new_fork_notice_now notice_now: edit_in_new_fork_notice_now
} }
fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
link_to label, fork_path, class: "btn btn-#{btn_class}", method: :post button_tag label,
class: "#{common_classes} js-edit-blob-link-fork-toggler",
data: { action: action, fork_path: fork_path }
end end
end end
......
...@@ -162,6 +162,7 @@ module IssuablesHelper ...@@ -162,6 +162,7 @@ module IssuablesHelper
html.html_safe html.html_safe
end end
<<<<<<< HEAD
def cached_issuables_count_for_state(issuable_type, state) def cached_issuables_count_for_state(issuable_type, state)
Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do
issuables_count_for_state(issuable_type, state) issuables_count_for_state(issuable_type, state)
...@@ -173,6 +174,10 @@ module IssuablesHelper ...@@ -173,6 +174,10 @@ module IssuablesHelper
Rails.cache.fetch(cache_key, expires_in: 2.minutes) do Rails.cache.fetch(cache_key, expires_in: 2.minutes) do
assigned_issuables_count(assignee, issuable_type, state) assigned_issuables_count(assignee, issuable_type, state)
end end
=======
def assigned_issuables_count(issuable_type)
current_user.public_send("assigned_open_#{issuable_type}_count")
>>>>>>> ce-com/master
end end
def issuable_filter_params def issuable_filter_params
...@@ -196,10 +201,6 @@ module IssuablesHelper ...@@ -196,10 +201,6 @@ module IssuablesHelper
private private
def assigned_issuables_count(assignee, issuable_type, state)
assignee.public_send("assigned_#{issuable_type}").public_send(state).count
end
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?
cookies[:collapsed_gutter] == 'true' cookies[:collapsed_gutter] == 'true'
end end
......
...@@ -5,7 +5,7 @@ module SubmoduleHelper ...@@ -5,7 +5,7 @@ module SubmoduleHelper
def submodule_links(submodule_item, ref = nil, repository = @repository) def submodule_links(submodule_item, ref = nil, repository = @repository)
url = repository.submodule_url_for(ref, submodule_item.path) url = repository.submodule_url_for(ref, submodule_item.path)
return url, nil unless url =~ /([^\/:]+)\/([^\/]+\.git)\Z/ return url, nil unless url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
namespace = $1 namespace = $1
project = $2 project = $2
...@@ -37,14 +37,16 @@ module SubmoduleHelper ...@@ -37,14 +37,16 @@ module SubmoduleHelper
end end
def self_url?(url, namespace, project) def self_url?(url, namespace, project)
return true if url == [Gitlab.config.gitlab.url, '/', namespace, '/', url_no_dotgit = url.chomp('.git')
project, '.git'].join('') return true if url_no_dotgit == [Gitlab.config.gitlab.url, '/', namespace, '/',
url == gitlab_shell.url_to_repo([namespace, '/', project].join('')) project].join('')
url_with_dotgit = url_no_dotgit + '.git'
url_with_dotgit == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
end end
def relative_self_url?(url) def relative_self_url?(url)
# (./)?(../repo.git) || (./)?(../../project/repo.git) ) # (./)?(../repo.git) || (./)?(../../project/repo.git) )
url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\z/ url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*(\.git)?\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*(\.git)?\z/
end end
def standard_links(host, namespace, project, commit) def standard_links(host, namespace, project, commit)
......
...@@ -8,6 +8,14 @@ ...@@ -8,6 +8,14 @@
# #
# Corresponding foo_html, bar_html and baz_html fields should exist. # Corresponding foo_html, bar_html and baz_html fields should exist.
module CacheMarkdownField module CacheMarkdownField
extend ActiveSupport::Concern
# Increment this number every time the renderer changes its output
CACHE_VERSION = 1
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
# Knows about the relationship between markdown and html field names, and # Knows about the relationship between markdown and html field names, and
# stores the rendering contexts for the latter # stores the rendering contexts for the latter
class FieldData class FieldData
...@@ -30,60 +38,71 @@ module CacheMarkdownField ...@@ -30,60 +38,71 @@ module CacheMarkdownField
end end
end end
# Dynamic registries don't really work in Rails as it's not guaranteed that
# every class will be loaded, so hardcode the list.
CACHING_CLASSES = %w[
AbuseReport
Appearance
ApplicationSetting
BroadcastMessage
Issue
Label
MergeRequest
Milestone
Namespace
Note
Project
Release
Snippet
].freeze
def self.caching_classes
CACHING_CLASSES.map(&:constantize)
end
def skip_project_check? def skip_project_check?
false false
end end
extend ActiveSupport::Concern # Returns the default Banzai render context for the cached markdown field.
def banzai_render_context(field)
raise ArgumentError.new("Unknown field: #{field.inspect}") unless
cached_markdown_fields.markdown_fields.include?(field)
included do # Always include a project key, or Banzai complains
cattr_reader :cached_markdown_fields do project = self.project if self.respond_to?(:project)
FieldData.new context = cached_markdown_fields[field].merge(project: project)
end
# Returns the default Banzai render context for the cached markdown field. # Banzai is less strict about authors, so don't always have an author key
def banzai_render_context(field) context[:author] = self.author if self.respond_to?(:author)
raise ArgumentError.new("Unknown field: #{field.inspect}") unless
cached_markdown_fields.markdown_fields.include?(field)
# Always include a project key, or Banzai complains context
project = self.project if self.respond_to?(:project) end
context = cached_markdown_fields[field].merge(project: project)
# Banzai is less strict about authors, so don't always have an author key # Update every column in a row if any one is invalidated, as we only store
context[:author] = self.author if self.respond_to?(:author) # one version per row
def refresh_markdown_cache!(do_update: false)
options = { skip_project_check: skip_project_check? }
context updates = cached_markdown_fields.markdown_fields.map do |markdown_field|
end [
cached_markdown_fields.html_field(markdown_field),
Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
]
end.to_h
updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION
# Allow callers to look up the cache field name, rather than hardcoding it updates.each {|html_field, data| write_attribute(html_field, data) }
def markdown_cache_field_for(field)
raise ArgumentError.new("Unknown field: #{field}") unless
cached_markdown_fields.markdown_fields.include?(field)
cached_markdown_fields.html_field(field) update_columns(updates) if persisted? && do_update
end
def cached_html_up_to_date?(markdown_field)
html_field = cached_markdown_fields.html_field(markdown_field)
markdown_changed = attribute_changed?(markdown_field) || false
html_changed = attribute_changed?(html_field) || false
CacheMarkdownField::CACHE_VERSION == cached_markdown_version &&
(html_changed || markdown_changed == html_changed)
end
def invalidated_markdown_cache?
cached_markdown_fields.html_fields.any? {|html_field| attribute_invalidated?(html_field) }
end
def attribute_invalidated?(attr)
__send__("#{attr}_invalidated?")
end
def cached_html_for(markdown_field)
raise ArgumentError.new("Unknown field: #{field}") unless
cached_markdown_fields.markdown_fields.include?(markdown_field)
__send__(cached_markdown_fields.html_field(markdown_field))
end
included do
cattr_reader :cached_markdown_fields do
FieldData.new
end end
# Always exclude _html fields from attributes (including serialization). # Always exclude _html fields from attributes (including serialization).
...@@ -92,12 +111,16 @@ module CacheMarkdownField ...@@ -92,12 +111,16 @@ module CacheMarkdownField
def attributes def attributes
attrs = attributes_before_markdown_cache attrs = attributes_before_markdown_cache
attrs.delete('cached_markdown_version')
cached_markdown_fields.html_fields.each do |field| cached_markdown_fields.html_fields.each do |field|
attrs.delete(field) attrs.delete(field)
end end
attrs attrs
end end
before_save :refresh_markdown_cache!, if: :invalidated_markdown_cache?
end end
class_methods do class_methods do
...@@ -107,31 +130,18 @@ module CacheMarkdownField ...@@ -107,31 +130,18 @@ module CacheMarkdownField
# a corresponding _html field. Any custom rendering options may be provided # a corresponding _html field. Any custom rendering options may be provided
# as a context. # as a context.
def cache_markdown_field(markdown_field, context = {}) def cache_markdown_field(markdown_field, context = {})
raise "Add #{self} to CacheMarkdownField::CACHING_CLASSES" unless
CacheMarkdownField::CACHING_CLASSES.include?(self.to_s)
cached_markdown_fields[markdown_field] = context cached_markdown_fields[markdown_field] = context
html_field = cached_markdown_fields.html_field(markdown_field) html_field = cached_markdown_fields.html_field(markdown_field)
cache_method = "#{markdown_field}_cache_refresh".to_sym
invalidation_method = "#{html_field}_invalidated?".to_sym invalidation_method = "#{html_field}_invalidated?".to_sym
define_method(cache_method) do
options = { skip_project_check: skip_project_check? }
html = Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
__send__("#{html_field}=", html)
true
end
# The HTML becomes invalid if any dependent fields change. For now, assume # The HTML becomes invalid if any dependent fields change. For now, assume
# author and project invalidate the cache in all circumstances. # author and project invalidate the cache in all circumstances.
define_method(invalidation_method) do define_method(invalidation_method) do
changed_fields = changed_attributes.keys changed_fields = changed_attributes.keys
invalidations = changed_fields & [markdown_field.to_s, "author", "project"] invalidations = changed_fields & [markdown_field.to_s, *INVALIDATED_BY]
!invalidations.empty? !invalidations.empty? || !cached_html_up_to_date?(markdown_field)
end end
before_save cache_method, if: invalidation_method
end end
end end
end end
...@@ -141,7 +141,7 @@ class Group < Namespace ...@@ -141,7 +141,7 @@ class Group < Namespace
end end
def add_users(users, access_level, current_user: nil, expires_at: nil) def add_users(users, access_level, current_user: nil, expires_at: nil)
GroupMember.add_users_to_group( GroupMember.add_users(
self, self,
users, users,
access_level, access_level,
......
...@@ -219,7 +219,7 @@ class Issue < ActiveRecord::Base ...@@ -219,7 +219,7 @@ class Issue < ActiveRecord::Base
# Returns `true` if the current issue can be viewed by either a logged in User # Returns `true` if the current issue can be viewed by either a logged in User
# or an anonymous user. # or an anonymous user.
def visible_to_user?(user = nil) def visible_to_user?(user = nil)
return false unless project.feature_available?(:issues, user) return false unless project && project.feature_available?(:issues, user)
user ? readable_by?(user) : publicly_visible? user ? readable_by?(user) : publicly_visible?
end end
......
...@@ -154,6 +154,22 @@ class Member < ActiveRecord::Base ...@@ -154,6 +154,22 @@ class Member < ActiveRecord::Base
member member
end end
def add_users(source, users, access_level, current_user: nil, expires_at: nil)
return [] unless users.present?
self.transaction do
users.map do |user|
add_user(
source,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
end
def access_levels def access_levels
Gitlab::Access.sym_options Gitlab::Access.sym_options
end end
...@@ -176,18 +192,6 @@ class Member < ActiveRecord::Base ...@@ -176,18 +192,6 @@ class Member < ActiveRecord::Base
# There is no current user for bulk actions, in which case anything is allowed # There is no current user for bulk actions, in which case anything is allowed
!current_user || current_user.can?(:"update_#{member.type.underscore}", member) !current_user || current_user.can?(:"update_#{member.type.underscore}", member)
end end
def add_users_to_source(source, users, access_level, current_user: nil, expires_at: nil)
users.each do |user|
add_user(
source,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
end end
def real_source_type def real_source_type
......
...@@ -26,18 +26,6 @@ class GroupMember < Member ...@@ -26,18 +26,6 @@ class GroupMember < Member
Gitlab::Access.sym_options_with_owner Gitlab::Access.sym_options_with_owner
end end
def self.add_users_to_group(group, users, access_level, current_user: nil, expires_at: nil)
self.transaction do
add_users_to_source(
group,
users,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
def group def group
source source
end end
......
...@@ -17,7 +17,7 @@ class ProjectMember < Member ...@@ -17,7 +17,7 @@ class ProjectMember < Member
before_destroy :delete_member_branch_protection before_destroy :delete_member_branch_protection
class << self class << self
# Add users to project teams with passed access option # Add users to projects with passed access option
# #
# access can be an integer representing a access code # access can be an integer representing a access code
# or symbol like :master representing role # or symbol like :master representing role
...@@ -40,7 +40,7 @@ class ProjectMember < Member ...@@ -40,7 +40,7 @@ class ProjectMember < Member
project_ids.each do |project_id| project_ids.each do |project_id|
project = Project.find(project_id) project = Project.find(project_id)
add_users_to_source( add_users(
project, project,
users, users,
access_level, access_level,
......
...@@ -190,7 +190,7 @@ class Project < ActiveRecord::Base ...@@ -190,7 +190,7 @@ class Project < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :count, to: :forks, prefix: true delegate :count, to: :forks, prefix: true
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
delegate :add_user, to: :team delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
delegate :empty_repo?, to: :repository delegate :empty_repo?, to: :repository
......
...@@ -50,10 +50,15 @@ class ProjectTeam ...@@ -50,10 +50,15 @@ class ProjectTeam
end end
def add_users(users, access_level, current_user: nil, expires_at: nil) def add_users(users, access_level, current_user: nil, expires_at: nil)
<<<<<<< HEAD
return false if group_member_lock return false if group_member_lock
ProjectMember.add_users_to_projects( ProjectMember.add_users_to_projects(
[project.id], [project.id],
=======
ProjectMember.add_users(
project,
>>>>>>> ce-com/master
users, users,
access_level, access_level,
current_user: current_user, current_user: current_user,
......
...@@ -109,9 +109,6 @@ class User < ActiveRecord::Base ...@@ -109,9 +109,6 @@ class User < ActiveRecord::Base
has_many :protected_branch_push_access_levels, dependent: :destroy, class_name: ProtectedBranch::PushAccessLevel has_many :protected_branch_push_access_levels, dependent: :destroy, class_name: ProtectedBranch::PushAccessLevel
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id
has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue"
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
# Issues that a user owns are expected to be moved to the "ghost" user before # Issues that a user owns are expected to be moved to the "ghost" user before
# the user is destroyed. If the user owns any issues during deletion, this # the user is destroyed. If the user owns any issues during deletion, this
# should be treated as an exceptional condition. # should be treated as an exceptional condition.
...@@ -921,20 +918,20 @@ class User < ActiveRecord::Base ...@@ -921,20 +918,20 @@ class User < ActiveRecord::Base
@global_notification_setting @global_notification_setting
end end
def assigned_open_merge_request_count(force: false) def assigned_open_merge_requests_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force) do
assigned_merge_requests.opened.count MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end end
end end
def assigned_open_issues_count(force: false) def assigned_open_issues_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do
assigned_issues.opened.count IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end end
end end
def update_cache_counts def update_cache_counts
assigned_open_merge_request_count(force: true) assigned_open_merge_requests_count(force: true)
assigned_open_issues_count(force: true) assigned_open_issues_count(force: true)
end end
......
...@@ -9,7 +9,11 @@ module Members ...@@ -9,7 +9,11 @@ module Members
def execute def execute
return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user) return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
member.destroy Member.transaction do
unassign_issues_and_merge_requests(member)
member.destroy
end
if member.request? && member.user != user if member.request? && member.user != user
notification_service.decline_access_request(member) notification_service.decline_access_request(member)
...@@ -17,5 +21,23 @@ module Members ...@@ -17,5 +21,23 @@ module Members
member member
end end
private
def unassign_issues_and_merge_requests(member)
if member.is_a?(GroupMember)
IssuesFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
execute.
update_all(assignee_id: nil)
MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
execute.
update_all(assignee_id: nil)
else
project = member.source
project.issues.opened.assigned_to(member.user).update_all(assignee_id: nil)
project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
member.user.update_cache_counts
end
end
end end
end end
module Members module Members
class CreateService < BaseService class CreateService < BaseService
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user
@params = params
end
def execute def execute
return false if params[:user_ids].blank? return false if params[:user_ids].blank?
project.team.add_users( @source.add_users(
params[:user_ids].split(','), params[:user_ids].split(','),
params[:access_level], params[:access_level],
expires_at: params[:expires_at], expires_at: params[:expires_at],
......
...@@ -47,13 +47,13 @@ ...@@ -47,13 +47,13 @@
%li %li
= link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('hashtag fw') = icon('hashtag fw')
- issues_count = cached_assigned_issuables_count(current_user, :issues, :opened) - issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count) = number_with_delimiter(issues_count)
%li %li
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold') = custom_icon('mr_bold')
- merge_requests_count = cached_assigned_issuables_count(current_user, :merge_requests, :opened) - merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count) = number_with_delimiter(merge_requests_count)
%li %li
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
I I
%span %span
Issues Issues
.badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened)) .badge= number_with_delimiter(assigned_issuables_count(:issues))
= nav_link(path: 'dashboard#merge_requests') do = nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
.shortcut-mappings .shortcut-mappings
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
M M
%span %span
Merge Requests Merge Requests
.badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened)) .badge= number_with_delimiter(assigned_issuables_count(:merge_requests))
= nav_link(controller: 'dashboard/snippets') do = nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
.shortcut-mappings .shortcut-mappings
......
...@@ -40,12 +40,17 @@ ...@@ -40,12 +40,17 @@
- if current_user - if current_user
= replace_blob_link = replace_blob_link
= delete_blob_link = delete_blob_link
- if current_user - if current_user
.js-file-fork-suggestion-section.file-fork-suggestion.hidden .js-file-fork-suggestion-section.file-fork-suggestion.hidden
%span.file-fork-suggestion-note %span.file-fork-suggestion-note
You don't have permission to edit this file. Try forking this project to edit the file. You're not allowed to
= link_to 'Fork', fork_path, method: :post, class: 'btn btn-grouped btn-inverted btn-new' %span.js-file-fork-suggestion-section-action
%button.js-cancel-fork-suggestion.btn.btn-grouped{ type: 'button' } edit
files in this project directly. Please fork this project,
make your changes there, and submit a merge request.
= link_to 'Fork', nil, method: :post, class: 'js-fork-suggestion-button btn btn-grouped btn-inverted btn-new'
%button.js-cancel-fork-suggestion-button.btn.btn-grouped{ type: 'button' }
Cancel Cancel
- if license_allows_file_locks? - if license_allows_file_locks?
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
%div{ class: container_class } %div{ class: container_class }
%h3.page-title %h3.page-title
Edit Milestone #{@milestone.to_reference} Edit Milestone
%hr %hr
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
.header-text-content .header-text-content
%span.identifier %span.identifier
%strong %strong
Milestone #{@milestone.to_reference} Milestone
- if @milestone.due_date || @milestone.start_date - if @milestone.due_date || @milestone.start_date
= milestone_date_range(@milestone) = milestone_date_range(@milestone)
.milestone-buttons .milestone-buttons
......
...@@ -145,10 +145,16 @@ ...@@ -145,10 +145,16 @@
} }
}); });
$('#project_import_url').disable();
$('.import_git').click(function( event ) { $('.import_git').click(function( event ) {
<<<<<<< HEAD
$projectImportUrl = $('#project_import_url') $projectImportUrl = $('#project_import_url')
$projectMirror = $('#project_mirror') $projectMirror = $('#project_mirror')
$projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled')) $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'))
$projectMirror.attr('disabled', !$projectMirror.attr('disabled')) $projectMirror.attr('disabled', !$projectMirror.attr('disabled'))
=======
$projectImportUrl = $('#project_import_url');
$projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'));
>>>>>>> ce-com/master
}); });
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
%strong Schedule trigger (experimental) %strong Schedule trigger (experimental)
.help-block .help-block
If checked, this trigger will be executed periodically according to cron and timezone. If checked, this trigger will be executed periodically according to cron and timezone.
= link_to icon('question-circle'), help_page_path('ci/triggers', anchor: 'schedule') = link_to icon('question-circle'), help_page_path('ci/triggers/README', anchor: 'using-scheduled-triggers')
.form-group .form-group
= schedule_fields.label :cron, "Cron", class: "label-light" = schedule_fields.label :cron, "Cron", class: "label-light"
= schedule_fields.text_field :cron, class: "form-control", title: 'Cron specification is required.', placeholder: "0 1 * * *" = schedule_fields.text_field :cron, class: "form-control", title: 'Cron specification is required.', placeholder: "0 1 * * *"
......
...@@ -30,9 +30,10 @@ ...@@ -30,9 +30,10 @@
new Pikaday({ new Pikaday({
field: $dateField.get(0), field: $dateField.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd', format: 'yyyy-mm-dd',
minDate: new Date(), minDate: new Date(),
container: $dateField.parent().get(0),
onSelect: function(dateText) { onSelect: function(dateText) {
$dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); $dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
} }
......
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1280 896q0 14-9 23l-320 320q-9 9-23 9-13 0-22.5-9.5t-9.5-22.5v-192h-352q-13 0-22.5-9.5t-9.5-22.5v-192q0-13 9.5-22.5t22.5-9.5h352v-192q0-14 9-23t23-9q12 0 24 10l319 319q9 9 9 23zm160 0q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14"><g fill-rule="evenodd"><path fill-rule="nonzero" d="m0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7m1 0c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6"/><path d="m7 6h-2.702c-.154 0-.298.132-.298.295v1.41c0 .164.133.295.298.295h2.702v1.694c0 .18.095.209.213.09l2.539-2.568c.115-.116.118-.312 0-.432l-2.539-2.568c-.115-.116-.213-.079-.213.09v1.694"/></g></svg>
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
= f.label :start_date, "Start Date", class: "control-label" = f.label :start_date, "Start Date", class: "control-label"
.col-sm-10 .col-sm-10
= f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date" = f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date"
%a.inline.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date %a.inline.pull-right.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
.col-md-6 .col-md-6
.form-group .form-group
= f.label :due_date, "Due Date", class: "control-label" = f.label :due_date, "Due Date", class: "control-label"
.col-sm-10 .col-sm-10
= f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date" = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
%a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date %a.inline.pull-right.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
...@@ -84,19 +84,19 @@ ...@@ -84,19 +84,19 @@
.fade-right= icon('angle-right') .fade-right= icon('angle-right')
%ul.nav-links.center.user-profile-nav.scrolling-tabs %ul.nav-links.center.user-profile-nav.scrolling-tabs
%li.js-activity-tab %li.js-activity-tab
= link_to user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do = link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
Activity Activity
%li.js-groups-tab %li.js-groups-tab
= link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do = link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
Groups Groups
%li.js-contributed-tab %li.js-contributed-tab
= link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do = link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
Contributed projects Contributed projects
%li.js-projects-tab %li.js-projects-tab
= link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do = link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
Personal projects Personal projects
%li.js-snippets-tab %li.js-snippets-tab
= link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do = link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
Snippets Snippets
%div{ class: container_class } %div{ class: container_class }
......
# This worker clears all cache fields in the database, working in batches.
class ClearDatabaseCacheWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
BATCH_SIZE = 1000
def perform
CacheMarkdownField.caching_classes.each do |kls|
fields = kls.cached_markdown_fields.html_fields
clear_cache_fields = fields.each_with_object({}) do |field, memo|
memo[field] = nil
end
Rails.logger.debug("Clearing Markdown cache for #{kls}: #{fields.inspect}")
kls.unscoped.in_batches(of: BATCH_SIZE) do |relation|
relation.update_all(clear_cache_fields)
end
end
nil
end
end
---
title: Add animations to all the dropdowns
merge_request: 8419
author:
---
title: Replace rake cache:clear:db with an automatic mechanism
merge_request: 10597
author:
---
title: fix inline diff copy in firefox
merge_request:
author:
---
title: Refactor add_users method for project and group
merge_request: 10850
author:
---
title: Refactor Admin::GroupsController#members_update method and add some specs
merge_request: 10735
author:
---
title: Refactor code that creates project/group members
merge_request: 10735
author:
---
title: Prevent user profile tabs to display raw json when going back and forward in
browser history
merge_request:
author:
---
title: Fixued preview shortcut focusing wrong preview tab
merge_request:
author:
---
title: Removed the milestone references from the milestone views
merge_request:
author:
---
title: 'repository browser: handle submodule urls that don''t end with .git'
merge_request:
author: David Turner
---
title: Unassign all Issues and Merge Requests when member leaves a team
merge_request:
author:
...@@ -34,7 +34,6 @@ ...@@ -34,7 +34,6 @@
- [repository_fork, 1] - [repository_fork, 1]
- [repository_import, 1] - [repository_import, 1]
- [project_service, 1] - [project_service, 1]
- [clear_database_cache, 1]
- [delete_user, 1] - [delete_user, 1]
- [delete_merged_branches, 1] - [delete_merged_branches, 1]
- [authorized_projects, 1] - [authorized_projects, 1]
......
class AddVersionFieldToMarkdownCache < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
%i[
abuse_reports
appearances
application_settings
broadcast_messages
issues
labels
merge_requests
milestones
namespaces
notes
projects
releases
snippets
].each do |table|
add_column table, :cached_markdown_version, :integer, limit: 4
end
end
end
...@@ -24,6 +24,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do ...@@ -24,6 +24,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.text "message_html" t.text "message_html"
t.integer "cached_markdown_version"
end end
create_table "appearances", force: :cascade do |t| create_table "appearances", force: :cascade do |t|
...@@ -35,6 +36,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do ...@@ -35,6 +36,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.datetime "updated_at" t.datetime "updated_at"
t.string "header_logo" t.string "header_logo"
t.text "description_html" t.text "description_html"
t.integer "cached_markdown_version"
end end
create_table "application_settings", force: :cascade do |t| create_table "application_settings", force: :cascade do |t|
...@@ -121,6 +123,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do ...@@ -121,6 +123,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.integer "unique_ips_limit_per_user" t.integer "unique_ips_limit_per_user"
t.integer "unique_ips_limit_time_window" t.integer "unique_ips_limit_time_window"
t.boolean "unique_ips_limit_enabled", default: false, null: false t.boolean "unique_ips_limit_enabled", default: false, null: false
<<<<<<< HEAD
t.integer "minimum_mirror_sync_time", default: 15, null: false t.integer "minimum_mirror_sync_time", default: 15, null: false
t.string "default_artifacts_expire_in", default: "0", null: false t.string "default_artifacts_expire_in", default: "0", null: false
t.string "elasticsearch_url", default: "http://localhost:9200" t.string "elasticsearch_url", default: "http://localhost:9200"
...@@ -129,6 +132,11 @@ ActiveRecord::Schema.define(version: 20170421113144) do ...@@ -129,6 +132,11 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.string "elasticsearch_aws_access_key" t.string "elasticsearch_aws_access_key"
t.string "elasticsearch_aws_secret_access_key" t.string "elasticsearch_aws_secret_access_key"
t.integer "geo_status_timeout", default: 10 t.integer "geo_status_timeout", default: 10
=======
t.decimal "polling_interval_multiplier", default: 1.0, null: false
t.integer "cached_markdown_version"
t.boolean "usage_ping_enabled", default: true, null: false
>>>>>>> ce-com/master
t.string "uuid" t.string "uuid"
t.decimal "polling_interval_multiplier", default: 1.0, null: false t.decimal "polling_interval_multiplier", default: 1.0, null: false
t.boolean "elasticsearch_experimental_indexer" t.boolean "elasticsearch_experimental_indexer"
...@@ -209,6 +217,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do ...@@ -209,6 +217,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.string "color" t.string "color"
t.string "font" t.string "font"
t.text "message_html" t.text "message_html"
t.integer "cached_markdown_version"
end end
create_table "chat_names", force: :cascade do |t| create_table "chat_names", force: :cascade do |t|
...@@ -566,7 +575,11 @@ ActiveRecord::Schema.define(version: 20170421113144) do ...@@ -566,7 +575,11 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.integer "time_estimate" t.integer "time_estimate"
t.integer "relative_position" t.integer "relative_position"
t.datetime "closed_at" t.datetime "closed_at"
<<<<<<< HEAD
t.string "service_desk_reply_to" t.string "service_desk_reply_to"
=======
t.integer "cached_markdown_version"
>>>>>>> ce-com/master
end end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
...@@ -631,6 +644,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do ...@@ -631,6 +644,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.text "description_html" t.text "description_html"
t.string "type" t.string "type"
t.integer "group_id" t.integer "group_id"
t.integer "cached_markdown_version"
end end
add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
...@@ -770,7 +784,11 @@ ActiveRecord::Schema.define(version: 20170421113144) do ...@@ -770,7 +784,11 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.text "title_html" t.text "title_html"
t.text "description_html" t.text "description_html"
t.integer "time_estimate" t.integer "time_estimate"
<<<<<<< HEAD
t.boolean "squash", default: false, null: false t.boolean "squash", default: false, null: false
=======
t.integer "cached_markdown_version"
>>>>>>> ce-com/master
end end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
...@@ -808,6 +826,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do ...@@ -808,6 +826,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.text "title_html" t.text "title_html"
t.text "description_html" t.text "description_html"
t.date "start_date" t.date "start_date"
t.integer "cached_markdown_version"
end end
add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
...@@ -848,8 +867,12 @@ ActiveRecord::Schema.define(version: 20170421113144) do ...@@ -848,8 +867,12 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.integer "parent_id" t.integer "parent_id"
t.boolean "require_two_factor_authentication", default: false, null: false t.boolean "require_two_factor_authentication", default: false, null: false
t.integer "two_factor_grace_period", default: 48, null: false t.integer "two_factor_grace_period", default: 48, null: false
<<<<<<< HEAD
t.integer "shared_runners_minutes_limit" t.integer "shared_runners_minutes_limit"
t.integer "repository_size_limit", limit: 8 t.integer "repository_size_limit", limit: 8
=======
t.integer "cached_markdown_version"
>>>>>>> ce-com/master
end end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
...@@ -886,6 +909,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do ...@@ -886,6 +909,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.integer "resolved_by_id" t.integer "resolved_by_id"
t.string "discussion_id" t.string "discussion_id"
t.text "note_html" t.text "note_html"
t.integer "cached_markdown_version"
end end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
...@@ -1110,7 +1134,11 @@ ActiveRecord::Schema.define(version: 20170421113144) do ...@@ -1110,7 +1134,11 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.integer "sync_time", default: 60, null: false t.integer "sync_time", default: 60, null: false
t.boolean "printing_merge_request_link_enabled", default: true, null: false t.boolean "printing_merge_request_link_enabled", default: true, null: false
t.string "import_jid" t.string "import_jid"
<<<<<<< HEAD
t.boolean "service_desk_enabled" t.boolean "service_desk_enabled"
=======
t.integer "cached_markdown_version"
>>>>>>> ce-com/master
end end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
...@@ -1209,6 +1237,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do ...@@ -1209,6 +1237,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.text "description_html" t.text "description_html"
t.integer "cached_markdown_version"
end end
add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
...@@ -1300,6 +1329,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do ...@@ -1300,6 +1329,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.integer "visibility_level", default: 0, null: false t.integer "visibility_level", default: 0, null: false
t.text "title_html" t.text "title_html"
t.text "content_html" t.text "content_html"
t.integer "cached_markdown_version"
end end
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
......
# PlantUML & GitLab # PlantUML & GitLab
> [Introduced][ce-7810] in GitLab 8.16. > [Introduced][ce-8537] in GitLab 8.16.
When [PlantUML](http://plantuml.com) integration is enabled and configured in When [PlantUML](http://plantuml.com) integration is enabled and configured in
GitLab we are able to create simple diagrams in AsciiDoc and Markdown documents GitLab we are able to create simple diagrams in AsciiDoc and Markdown documents
...@@ -28,7 +28,7 @@ using Tomcat: ...@@ -28,7 +28,7 @@ using Tomcat:
sudo apt-get install tomcat7 sudo apt-get install tomcat7
sudo cp target/plantuml.war /var/lib/tomcat7/webapps/plantuml.war sudo cp target/plantuml.war /var/lib/tomcat7/webapps/plantuml.war
sudo chown tomcat7:tomcat7 /var/lib/tomcat7/webapps/plantuml.war sudo chown tomcat7:tomcat7 /var/lib/tomcat7/webapps/plantuml.war
sudo service restart tomcat7 sudo service tomcat7 restart
``` ```
Once the Tomcat service restarts the PlantUML service will be ready and Once the Tomcat service restarts the PlantUML service will be ready and
...@@ -93,3 +93,5 @@ Some parameters can be added to the AsciiDoc block definition: ...@@ -93,3 +93,5 @@ Some parameters can be added to the AsciiDoc block definition:
- *height*: Height attribute added to the img tag. - *height*: Height attribute added to the img tag.
Markdown does not support any parameters and will always use PNG format. Markdown does not support any parameters and will always use PNG format.
[ce-8537]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8537
\ No newline at end of file
...@@ -1029,7 +1029,7 @@ Parameters: ...@@ -1029,7 +1029,7 @@ Parameters:
| `from` | string | no | Date string in the format YEAR-MONTH-DAY, e.g. `2016-03-11`. Defaults to 6 months ago. | | `from` | string | no | Date string in the format YEAR-MONTH-DAY, e.g. `2016-03-11`. Defaults to 6 months ago. |
```bash ```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/user/activities curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/activities
``` ```
Example response: Example response:
......
# Technical Articles
[Technical Articles](../development/writing_documentation.md#technical-articles) are
topic-related documentation, written with an user-friendly approach and language, aiming
to provide the community with guidance on specific processes to achieve certain objectives.
They are written by members of the GitLab Team and by
[Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
## GitLab Pages
- **GitLab Pages from A to Z**
- [Part 1: Static sites and GitLab Pages domains](../user/project/pages/getting_started_part_one.md)
- [Part 2: Quick start guide - Setting up GitLab Pages](../user/project/pages/getting_started_part_two.md)
- [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](../user/project/pages/getting_started_part_three.md)
- [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](../user/project/pages/getting_started_part_four.md)
...@@ -35,6 +35,7 @@ enable [Kubernetes service][kubernetes-service]. ...@@ -35,6 +35,7 @@ enable [Kubernetes service][kubernetes-service].
1. Test your deployment configuration using a [Review App][review-app] that was 1. Test your deployment configuration using a [Review App][review-app] that was
created automatically for you. created automatically for you.
<<<<<<< HEAD
## Using the Kubernetes deploy example project with Nginx ## Using the Kubernetes deploy example project with Nginx
The Autodeploy templates are based on the [kubernetes-deploy][kube-deploy] The Autodeploy templates are based on the [kubernetes-deploy][kube-deploy]
...@@ -114,6 +115,8 @@ Next, we replace `__CI_ENVIRONMENT_SLUG__` with the content of the ...@@ -114,6 +115,8 @@ Next, we replace `__CI_ENVIRONMENT_SLUG__` with the content of the
Finally, the Nginx pod is created from the definition of the Finally, the Nginx pod is created from the definition of the
`nginx-deployment.yaml` file. `nginx-deployment.yaml` file.
=======
>>>>>>> ce-com/master
## Private Project Support ## Private Project Support
> Experimental support [introduced][mr-2] in GitLab 9.1. > Experimental support [introduced][mr-2] in GitLab 9.1.
...@@ -146,7 +149,10 @@ PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRE ...@@ -146,7 +149,10 @@ PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRE
[kubernetes-service]: ../../user/project/integrations/kubernetes.md [kubernetes-service]: ../../user/project/integrations/kubernetes.md
[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor [docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor
[review-app]: ../review_apps/index.md [review-app]: ../review_apps/index.md
<<<<<<< HEAD
[kube-image]: https://gitlab.com/gitlab-examples/kubernetes-deploy/container_registry "Kubernetes deploy Container Registry" [kube-image]: https://gitlab.com/gitlab-examples/kubernetes-deploy/container_registry "Kubernetes deploy Container Registry"
[kube-deploy]: https://gitlab.com/gitlab-examples/kubernetes-deploy "Kubernetes deploy example project" [kube-deploy]: https://gitlab.com/gitlab-examples/kubernetes-deploy "Kubernetes deploy example project"
=======
>>>>>>> ce-com/master
[container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html [container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html
[postgresql]: https://www.postgresql.org/ [postgresql]: https://www.postgresql.org/
...@@ -227,3 +227,31 @@ branch of project with ID `9` every night at `00:30`: ...@@ -227,3 +227,31 @@ branch of project with ID `9` every night at `00:30`:
``` ```
[ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229 [ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229
## Using scheduled triggers
> [Introduced][ci-10533] in GitLab CE 9.1 as experimental.
In order to schedule a trigger, navigate to your project's **Settings ➔ CI/CD Pipelines ➔ Triggers** and edit an existing trigger token.
![Triggers Schedule edit](img/trigger_schedule_edit.png)
To set up a scheduled trigger:
1. Check the **Schedule trigger (experimental)** checkbox
1. Enter a cron value for the frequency of the trigger ([learn more about cron notation](http://www.nncron.ru/help/EN/working/cron-format.htm))
1. Enter the timezone of the cron trigger ([see a list of timezones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones))
1. Enter the branch or tag that the trigger will target
1. Hit **Save trigger** for the changes to take effect
![Triggers Schedule create](img/trigger_schedule_create.png)
You can check a next execution date of the scheduled trigger, which is automatically calculated by a server.
![Triggers Schedule create](img/trigger_schedule_updated_next_run_at.png)
> **Notes**:
- Those triggers won't be executed precicely. Because scheduled triggers are handled by Sidekiq, which runs according to its interval. For exmaple, if you set a trigger to be executed every minute (`* * * * *`) and the Sidekiq worker performs 00:00 and 12:00 o'clock every day (`0 */12 * * *`), then your trigger will be executed only 00:00 and 12:00 o'clock every day. To change the Sidekiq worker's frequency, you have to edit the `trigger_schedule_worker` value in `config/gitlab.yml` and restart GitLab. The Sidekiq worker's configuration on GiLab.com is able to be looked up at [here](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example#L185).
- Cron notation is parsed by [Rufus-Scheduler](https://github.com/jmettraux/rufus-scheduler).
[ci-10533]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10533
...@@ -29,7 +29,8 @@ The table below shows what kind of documentation goes where. ...@@ -29,7 +29,8 @@ The table below shows what kind of documentation goes where.
| `doc/legal/` | Legal documents about contributing to GitLab. | | `doc/legal/` | Legal documents about contributing to GitLab. |
| `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). | | `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). |
| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. | | `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`); Technical Articles: user guides, admin guides, technical overviews, tutorials (`doc/topics/topic-name/`). | | `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) |
| `doc/articles/` | [Technical Articles](writing_documentation.md#technical-articles): user guides, admin guides, technical overviews, tutorials (`doc/articles/article-title/index.md`). |
--- ---
...@@ -61,8 +62,8 @@ The table below shows what kind of documentation goes where. ...@@ -61,8 +62,8 @@ The table below shows what kind of documentation goes where.
located at `doc/user/admin_area/settings/visibility_and_access_controls.md`. located at `doc/user/admin_area/settings/visibility_and_access_controls.md`.
1. The `doc/topics/` directory holds topic-related technical content. Create 1. The `doc/topics/` directory holds topic-related technical content. Create
`doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary. `doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary.
Note that `topics` holds the index page per topic, and technical articles. General General user- and admin- related documentation, should be placed accordingly.
user- and admin- related documentation, should be placed accordingly. 1. For technical articles, place their images under `doc/articles/article-title/img/`.
--- ---
......
...@@ -25,6 +25,59 @@ Working with our frontend assets requires Node (v4.3 or greater) and Yarn ...@@ -25,6 +25,59 @@ Working with our frontend assets requires Node (v4.3 or greater) and Yarn
For our currently-supported browsers, see our [requirements][requirements]. For our currently-supported browsers, see our [requirements][requirements].
---
## Development Process
When you are assigned an issue please follow the next steps:
### Divide a big feature into small Merge Requests
1. Big Merge Request are painful to review. In order to make this process easier we
must break a big feature into smaller ones and create a Merge Request for each step.
1. First step is to create a branch from `master`, let's call it `new-feature`. This branch
will be the recipient of all the smaller Merge Requests. Only this one will be merged to master.
1. Don't do any work on this one, let's keep it synced with master.
1. Create a new branch from `new-feature`, let's call it `new-feature-step-1`. We advise you
to clearly identify which step the branch represents.
1. Do the first part of the modifications in this branch. The target branch of this Merge Request
should be `new-feature`.
1. Once `new-feature-step-1` gets merged into `new-feature` we can continue our work. Create a new
branch from `new-feature`, let's call it `new-feature-step-2` and repeat the process done before.
```shell
* master
|\
| * new-feature
| |\
| | * new-feature-step-1
| |\
| | * new-feature-step-2
| |\
| | * new-feature-step-3
```
**Tips**
- Make sure `new-feature` branch is always synced with `master`: merge master frequently.
- Do the same for the feature branch you have opened. This can be accomplished by merging `master` into `new-feature` and `new-feature` into `new-feature-step-*`
- Avoid rewriting history.
### Share your work early
1. Before writing code guarantee your vision of the architecture is aligned with
GitLab's architecture.
1. Add a diagram to the issue and ask a Frontend Architecture about it.
![Diagram of Issue Boards Architecture](img/boards_diagram.png)
1. Don't take more than one week between starting work on a feature and
sharing a Merge Request with a reviewer or a maintainer.
### Vue features
1. Follow the steps in [Vue.js Best Practices](vue.md)
1. Follow the style guide.
1. Only a handful of people are allowed to merge Vue related features.
Reach out to @jschatz, @iamphill, @fatihacet or @filipa early in this process.
--- ---
## [Architecture](architecture.md) ## [Architecture](architecture.md)
......
...@@ -58,7 +58,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. ...@@ -58,7 +58,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
import Bar from './bar'; import Bar from './bar';
``` ```
- **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead. - **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead.
- When declaring multiple globals, always use one `/* global [name] */` line per variable. - When declaring multiple globals, always use one `/* global [name] */` line per variable.
...@@ -71,6 +71,16 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. ...@@ -71,6 +71,16 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
/* global Cookies */ /* global Cookies */
/* global jQuery */ /* global jQuery */
``` ```
- Use up to 3 parameters for a function or class. If you need more accept an Object instead.
```javascript
// bad
fn(p1, p2, p3, p4) {}
// good
fn(options) {}
```
#### Modules, Imports, and Exports #### Modules, Imports, and Exports
- Use ES module syntax to import modules - Use ES module syntax to import modules
...@@ -168,6 +178,23 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. ...@@ -168,6 +178,23 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
- Avoid constructors with side-effects - Avoid constructors with side-effects
- Prefer `.map`, `.reduce` or `.filter` over `.forEach`
A forEach will cause side effects, it will be mutating the array being iterated. Prefer using `.map`,
`.reduce` or `.filter`
```javascript
const users = [ { name: 'Foo' }, { name: 'Bar' } ];
// bad
users.forEach((user, index) => {
user.id = index;
});
// good
const usersWithId = users.map((user, index) => {
return Object.assign({}, user, { id: index });
});
```
#### Parse Strings into Numbers #### Parse Strings into Numbers
- `parseInt()` is preferable over `Number()` or `+` - `parseInt()` is preferable over `Number()` or `+`
...@@ -183,6 +210,19 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. ...@@ -183,6 +210,19 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
parseInt('10', 10); parseInt('10', 10);
``` ```
#### CSS classes used for JavaScript
- If the class is being used in Javascript it needs to be prepend with `js-`
```html
// bad
<button class="add-user">
Add User
</button>
// good
<button class="js-add-user">
Add User
</button>
```
### Vue.js ### Vue.js
...@@ -200,6 +240,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. ...@@ -200,6 +240,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
#### Naming #### Naming
- **Extensions**: Use `.vue` extension for Vue components. - **Extensions**: Use `.vue` extension for Vue components.
- **Reference Naming**: Use PascalCase for Vue components and camelCase for their instances: - **Reference Naming**: Use PascalCase for Vue components and camelCase for their instances:
```javascript ```javascript
// bad // bad
import cardBoard from 'cardBoard'; import cardBoard from 'cardBoard';
...@@ -217,6 +258,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. ...@@ -217,6 +258,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
cardBoard: CardBoard cardBoard: CardBoard
}; };
``` ```
- **Props Naming:** - **Props Naming:**
- Avoid using DOM component prop names. - Avoid using DOM component prop names.
- Use kebab-case instead of camelCase to provide props in templates. - Use kebab-case instead of camelCase to provide props in templates.
...@@ -243,12 +285,18 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns. ...@@ -243,12 +285,18 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
<component v-if="bar" <component v-if="bar"
param="baz" /> param="baz" />
<button class="btn">Click me</button>
// good // good
<component <component
v-if="bar" v-if="bar"
param="baz" param="baz"
/> />
<button class="btn">
Click me
</button>
// if props fit in one line then keep it on the same line // if props fit in one line then keep it on the same line
<component bar="bar" /> <component bar="bar" />
``` ```
......
...@@ -26,6 +26,10 @@ browser and you will not have access to certain APIs, such as ...@@ -26,6 +26,10 @@ browser and you will not have access to certain APIs, such as
[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification), [`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
which will have to be stubbed. which will have to be stubbed.
### Writing tests
### Vue.js unit tests
See this [section][vue-test].
### Running frontend tests ### Running frontend tests
`rake karma` runs the frontend-only (JavaScript) tests. `rake karma` runs the frontend-only (JavaScript) tests.
...@@ -134,3 +138,4 @@ Scenario: Developer can approve merge request ...@@ -134,3 +138,4 @@ Scenario: Developer can approve merge request
[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html [jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html
[jasmine-jquery]: https://github.com/velesin/jasmine-jquery [jasmine-jquery]: https://github.com/velesin/jasmine-jquery
[karma]: http://karma-runner.github.io/ [karma]: http://karma-runner.github.io/
[vue-test]:https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components
This diff is collapsed.
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- **General Documentation**: written by the developers responsible by creating features. Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers. - **General Documentation**: written by the developers responsible by creating features. Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers.
- **Technical Articles**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/). - **Technical Articles**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
- **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs, in the same merge request containing code. - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs).
## Distinction between General Documentation and Technical Articles ## Distinction between General Documentation and Technical Articles
...@@ -18,7 +18,7 @@ They are topic-related documentation, written with an user-friendly approach and ...@@ -18,7 +18,7 @@ They are topic-related documentation, written with an user-friendly approach and
A technical article guides users and/or admins to achieve certain objectives (within guides and tutorials), or provide an overview of that particular topic or feature (within technical overviews). It can also describe the use, implementation, or integration of third-party tools with GitLab. A technical article guides users and/or admins to achieve certain objectives (within guides and tutorials), or provide an overview of that particular topic or feature (within technical overviews). It can also describe the use, implementation, or integration of third-party tools with GitLab.
They live under `doc/topics/topic-name/`, and can be searched per topic, within "Indexes per Topic" pages. The topics are listed on the main [Indexes per Topic](../topics/index.md) page. They live under `doc/articles/article-title/index.md`, and their images should be placed under `doc/articles/article-title/img/`. Find a list of existing [technical articles](../articles/index.md) here.
#### Types of Technical Articles #### Types of Technical Articles
......
...@@ -18,10 +18,12 @@ another is through backup restore. ...@@ -18,10 +18,12 @@ another is through backup restore.
To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json` To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json`
(for omnibus packages) or `/home/git/gitlab/.secret` (for installations (for omnibus packages) or `/home/git/gitlab/.secret` (for installations
from source). This file contains the database encryption key and CI secret from source). This file contains the database encryption key,
variables used for two-factor authentication. If you fail to restore this [CI secret variables](../ci/variables/README.md#secret-variables), and
encryption key file along with the application data backup, users with two-factor secret variables used for [two-factor authentication](../security/two_factor_authentication.md).
authentication enabled will lose access to your GitLab server. If you fail to restore this encryption key file along with the application data
backup, users with two-factor authentication enabled and GitLab Runners will
lose access to your GitLab server.
## Create a backup of the GitLab system ## Create a backup of the GitLab system
......
...@@ -333,7 +333,11 @@ A [platform](https://www.meteor.com) for building javascript apps. ...@@ -333,7 +333,11 @@ A [platform](https://www.meteor.com) for building javascript apps.
### Milestones ### Milestones
<<<<<<< HEAD
Allow you to [organize issues](https://docs.gitlab.com/ce/user/project/milestones/) and merge requests in GitLab into a cohesive group, optionally setting a due date. A common use is keeping track of an upcoming software version. Milestones are created per-project. Allow you to [organize issues](https://docs.gitlab.com/ce/user/project/milestones/) and merge requests in GitLab into a cohesive group, optionally setting a due date. A common use is keeping track of an upcoming software version. Milestones are created per-project.
=======
Allow you to [organize issues](../../user/project/milestones/index.md) and merge requests in GitLab into a cohesive group, optionally setting a due date. A common use is keeping track of an upcoming software version. Milestones are created per-project.
>>>>>>> ce-com/master
### Mirror Repositories ### Mirror Repositories
......
# Cohorts # Cohorts
> **Notes:** > **Notes:**
- [Introduced][ce-23361] in GitLab 9.1. > [Introduced][ce-23361] in GitLab 9.1.
As a benefit of having the [usage ping active](settings/usage_statistics.md), As a benefit of having the [usage ping active](settings/usage_statistics.md),
GitLab lets you analyze the users' activities of your GitLab installation. GitLab lets you analyze the users' activities of your GitLab installation.
Under `/admin/cohorts`, when the usage ping is active, GitLab will show the Under `/admin/cohorts`, when the usage ping is active, GitLab will show the
monthly cohorts of new users and their activities over time. monthly cohorts of new users and their activities over time.
## Overview
How do we read the user cohorts table? Let's take an example with the following How do we read the user cohorts table? Let's take an example with the following
user cohorts. user cohorts.
![User cohort example](img/cohorts.png) ![User cohort example](img/cohorts.png)
For the cohort of June 2016, 163 users have been created on this server. One For the cohort of June 2016, 163 users have been added on this server and have
month later, in July 2016, 155 users (or 95% of the June cohort) are still been active since this month. One month later, in July 2016, out of
active. Two months later, 139 users (or 85%) are still active. 9 months later, these 163 users, 155 users (or 95% of the June cohort) are still active. Two
we can see that only 6% of this cohort are still active. months later, 139 users (or 85%) are still active. 9 months later, we can see
that only 6% of this cohort are still active.
The Inactive users column shows the number of users who have been added during
the month, but who have never actually had any activity in the instance.
How do we measure the activity of users? GitLab considers a user active if: How do we measure the activity of users? GitLab considers a user active if:
* the user signs in * the user signs in
* the user has Git activity (whether push or pull). * the user has Git activity (whether push or pull).
### Setup ## Setup
1. Activate the usage ping as defined [in the documentation](settings/usage_statistics.md) 1. [Activate the usage ping](settings/usage_statistics.md)
2. Go to `/admin/cohorts` to see the user cohorts of the server 2. Go to `/admin/cohorts` to see the user cohorts of the server
[ce-23361]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23361 [ce-23361]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23361
...@@ -7,6 +7,9 @@ project itself, the highest permission level is used. ...@@ -7,6 +7,9 @@ project itself, the highest permission level is used.
On public and internal projects the Guest role is not enforced. All users will On public and internal projects the Guest role is not enforced. All users will
be able to create issues, leave comments, and pull or download the project code. be able to create issues, leave comments, and pull or download the project code.
When a member leaves the team the all assigned Issues and Merge Requests
will be unassigned automatically.
GitLab administrators receive all permissions. GitLab administrators receive all permissions.
To add or import a user, you can follow the [project users and members To add or import a user, you can follow the [project users and members
......
...@@ -56,8 +56,12 @@ GitLab CI build environment: ...@@ -56,8 +56,12 @@ GitLab CI build environment:
- `KUBE_URL` - equal to the API URL - `KUBE_URL` - equal to the API URL
- `KUBE_TOKEN` - `KUBE_TOKEN`
- `KUBE_NAMESPACE` - `KUBE_NAMESPACE` - The Kubernetes namespace is auto-generated if not specified.
- `KUBE_CA_PEM_FILE` - only present if a custom CA bundle was specified. Path to a file containing PEM data. The default value is `<project_name>-<project_id>`. You can overwrite it to
use different one if needed, otherwise the `KUBE_NAMESPACE` variable will
receive the default value.
- `KUBE_CA_PEM_FILE` - only present if a custom CA bundle was specified. Path
to a file containing PEM data.
- `KUBE_CA_PEM` (deprecated)- only if a custom CA bundle was specified. Raw PEM data. - `KUBE_CA_PEM` (deprecated)- only if a custom CA bundle was specified. Raw PEM data.
## Web terminals ## Web terminals
......
# Microsoft Teams Service # Microsoft Teams service
## On Microsoft Teams ## On Microsoft Teams
To enable Microsoft Teams integration you must create an incoming webhook integration on Microsoft Teams by following the steps described in this [document](https://msdn.microsoft.com/en-us/microsoft-teams/connectors) To enable Microsoft Teams integration you must create an incoming webhook integration on Microsoft Teams by following the steps described in this [document](https://msdn.microsoft.com/en-us/microsoft-teams/connectors).
## On GitLab ## On GitLab
...@@ -30,4 +30,4 @@ At the end fill in your Microsoft Teams details: ...@@ -30,4 +30,4 @@ At the end fill in your Microsoft Teams details:
After you are all done, click **Save changes** for the changes to take effect. After you are all done, click **Save changes** for the changes to take effect.
![Microsoft Teams configuration](img/microsoft_teams_configuration.png) ![Microsoft Teams configuration](img/microsoft_teams_configuration.png)
\ No newline at end of file
...@@ -47,6 +47,7 @@ Click on the service links to see further configuration instructions and details ...@@ -47,6 +47,7 @@ Click on the service links to see further configuration instructions and details
| [Kubernetes](kubernetes.md) | A containerized deployment service | | [Kubernetes](kubernetes.md) | A containerized deployment service |
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | | [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost | | [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
| [Microsoft teams](microsoft_teams.md) | Receive notifications for actions that happen on GitLab into a room on Microsoft Teams using Office 365 Connectors |
| Pipelines emails | Email the pipeline status to a list of recipients | | Pipelines emails | Email the pipeline status to a list of recipients |
| [Slack Notifications](slack.md) | Receive event notifications in Slack | | [Slack Notifications](slack.md) | Receive event notifications in Slack |
| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands | | [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands |
......
...@@ -35,6 +35,7 @@ Resolving comments prevents you from forgetting to address feedback and lets ...@@ -35,6 +35,7 @@ Resolving comments prevents you from forgetting to address feedback and lets
you hide discussions that are no longer relevant. you hide discussions that are no longer relevant.
[Read more about resolving discussion comments in merge requests reviews.](../../discussions/index.md) [Read more about resolving discussion comments in merge requests reviews.](../../discussions/index.md)
<<<<<<< HEAD
## Squash and merge ## Squash and merge
...@@ -42,6 +43,8 @@ GitLab allows you to squash all changes present in a merge request into a single ...@@ -42,6 +43,8 @@ GitLab allows you to squash all changes present in a merge request into a single
commit when merging, to allow for a neater commit history. commit when merging, to allow for a neater commit history.
[Learn more about squash and merge.](squash_and_merge.md) [Learn more about squash and merge.](squash_and_merge.md)
=======
>>>>>>> ce-com/master
## Resolve conflicts ## Resolve conflicts
......
# Milestones # Milestones
<<<<<<< HEAD
Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date. Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date.
A common use is keeping track of an upcoming software version. Milestones are created per-project. A common use is keeping track of an upcoming software version. Milestones are created per-project.
...@@ -7,16 +8,37 @@ You can find the milestones page under your project's **Issues ➔ Milestones**. ...@@ -7,16 +8,37 @@ You can find the milestones page under your project's **Issues ➔ Milestones**.
## Creating a milestone ## Creating a milestone
=======
Milestones allow you to organize issues and merge requests into a cohesive group,
optionally setting a due date. A common use is keeping track of an upcoming
software version. Milestones can be created per-project or per-group.
## Creating a project milestone
>**Note:**
You need [Master permissions](../../permissions.md) in order to create a milestone.
You can find the milestones page under your project's **Issues ➔ Milestones**.
>>>>>>> ce-com/master
To create a new milestone, simply click the **New milestone** button when in the To create a new milestone, simply click the **New milestone** button when in the
milestones page. A milestone can have a title, a description and start/due dates. milestones page. A milestone can have a title, a description and start/due dates.
Once you fill in all the details, hit the **Create milestone** button. Once you fill in all the details, hit the **Create milestone** button.
<<<<<<< HEAD
>**Note:** >**Note:**
The start/due dates are required if you intend to use [Burndown charts](#burndown-charts). The start/due dates are required if you intend to use [Burndown charts](#burndown-charts).
![Creating a milestone](img/milestone_create.png) ![Creating a milestone](img/milestone_create.png)
## Groups and milestones ## Groups and milestones
=======
![Creating a milestone](img/milestone_create.png)
## Creating a group milestone
>**Note:**
You need [Master permissions](../../permissions.md) in order to create a milestone.
>>>>>>> ce-com/master
You can create a milestone for several projects in the same group simultaneously. You can create a milestone for several projects in the same group simultaneously.
On the group's **Issues ➔ Milestones** page, you will be able to see the status On the group's **Issues ➔ Milestones** page, you will be able to see the status
...@@ -41,6 +63,7 @@ special options available when filtering by milestone: ...@@ -41,6 +63,7 @@ special options available when filtering by milestone:
* **Started** - show issues or merge requests from any milestone with a start * **Started** - show issues or merge requests from any milestone with a start
date less than today. Note that this can return results from several date less than today. Note that this can return results from several
milestones in the same project. milestones in the same project.
<<<<<<< HEAD
## Burndown charts ## Burndown charts
...@@ -70,3 +93,5 @@ cumulative value. ...@@ -70,3 +93,5 @@ cumulative value.
[ee-1540]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1540 [ee-1540]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1540
[ee]: https://about.gitlab.com/gitlab-ee [ee]: https://about.gitlab.com/gitlab-ee
=======
>>>>>>> ce-com/master
...@@ -4,40 +4,6 @@ Feature: Group Members ...@@ -4,40 +4,6 @@ Feature: Group Members
And "John Doe" is owner of group "Owned" And "John Doe" is owner of group "Owned"
And "John Doe" is guest of group "Guest" And "John Doe" is guest of group "Guest"
@javascript
Scenario: I should add user to group "Owned"
Given User "Mary Jane" exists
When I visit group "Owned" members page
And I select user "Mary Jane" from list with role "Reporter"
Then I should see user "Mary Jane" in team list
@javascript
Scenario: Add user to group
Given gitlab user "Mike"
When I visit group "Owned" members page
When I select "Mike" as "Reporter"
Then I should see "Mike" in team list as "Reporter"
@javascript
Scenario: Ignore add user to group when is already Owner
Given gitlab user "Mike"
When I visit group "Owned" members page
When I select "Mike" as "Reporter"
Then I should see "Mike" in team list as "Owner"
@javascript
Scenario: Invite user to group
When I visit group "Owned" members page
When I select "sjobs@apple.com" as "Reporter"
Then I should see "sjobs@apple.com" in team list as invited "Reporter"
@javascript
Scenario: Edit group member permissions
Given "Mary Jane" is guest of group "Owned"
And I visit group "Owned" members page
When I change the "Mary Jane" role to "Developer"
Then I should see "Mary Jane" as "Developer"
# Leave # Leave
@javascript @javascript
......
...@@ -117,6 +117,8 @@ Feature: Project Source Browse Files ...@@ -117,6 +117,8 @@ Feature: Project Source Browse Files
And I click on ".gitignore" file in repo And I click on ".gitignore" file in repo
And I see the ".gitignore" And I see the ".gitignore"
And I click on "Replace" And I click on "Replace"
Then I should see a Fork/Cancel combo
And I click button "Fork"
Then I should see a notice about a new fork having been created Then I should see a notice about a new fork having been created
When I click on "Replace" When I click on "Replace"
And I replace it with a text file And I replace it with a text file
...@@ -265,6 +267,8 @@ Feature: Project Source Browse Files ...@@ -265,6 +267,8 @@ Feature: Project Source Browse Files
And I click on ".gitignore" file in repo And I click on ".gitignore" file in repo
And I see the ".gitignore" And I see the ".gitignore"
And I click on "Delete" And I click on "Delete"
Then I should see a Fork/Cancel combo
And I click button "Fork"
Then I should see a notice about a new fork having been created Then I should see a notice about a new fork having been created
When I click on "Delete" When I click on "Delete"
And I fill the commit message And I fill the commit message
......
...@@ -7,26 +7,6 @@ Feature: Project Team Management ...@@ -7,26 +7,6 @@ Feature: Project Team Management
And "Dmitriy" is "Shop" developer And "Dmitriy" is "Shop" developer
And I visit project "Shop" team page And I visit project "Shop" team page
Scenario: See all team members
Then I should be able to see myself in team
And I should see "Dmitriy" in team list
@javascript
Scenario: Add user to project
When I select "Mike" as "Reporter"
Then I should see "Mike" in team list as "Reporter"
@javascript
Scenario: Invite user to project
When I select "sjobs@apple.com" as "Reporter"
Then I should see "sjobs@apple.com" in team list as invited "Reporter"
@javascript
Scenario: Update user access
Given I should see "Dmitriy" in team list as "Developer"
And I change "Dmitriy" role to "Reporter"
And I should see "Dmitriy" in team list as "Reporter"
Scenario: Cancel team member Scenario: Cancel team member
Given I click cancel link for "Dmitriy" Given I click cancel link for "Dmitriy"
Then I visit project "Shop" team page Then I visit project "Shop" team page
......
...@@ -4,71 +4,6 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps ...@@ -4,71 +4,6 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
include SharedPaths include SharedPaths
include SharedGroup include SharedGroup
include SharedUser include SharedUser
include Select2Helper
step 'I select "Mike" as "Reporter"' do
user = User.find_by(name: "Mike")
page.within ".users-group-form" do
select2(user.id, from: "#user_ids", multiple: true)
select "Reporter", from: "access_level"
end
click_button "Add to group"
end
step 'I select "Mike" as "Master"' do
user = User.find_by(name: "Mike")
page.within ".users-group-form" do
select2(user.id, from: "#user_ids", multiple: true)
select "Master", from: "access_level"
end
click_button "Add to group"
end
step 'I should see "Mike" in team list as "Reporter"' do
page.within '.content-list' do
expect(page).to have_content('Mike')
expect(page).to have_content('Reporter')
end
end
step 'I should see "Mike" in team list as "Owner"' do
page.within '.content-list' do
expect(page).to have_content('Mike')
expect(page).to have_content('Owner')
end
end
step 'I select "sjobs@apple.com" as "Reporter"' do
page.within ".users-group-form" do
select2("sjobs@apple.com", from: "#user_ids", multiple: true)
select "Reporter", from: "access_level"
end
click_button "Add to group"
end
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
page.within '.content-list' do
expect(page).to have_content('sjobs@apple.com')
expect(page).to have_content('Invited')
expect(page).to have_content('Reporter')
end
end
step 'I select user "Mary Jane" from list with role "Reporter"' do
user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane")
page.within ".users-group-form" do
select2(user.id, from: "#user_ids", multiple: true)
select "Reporter", from: "access_level"
end
click_button "Add to group"
end
step 'I should see user "John Doe" in team list' do step 'I should see user "John Doe" in team list' do
expect(group_members_list).to have_content("John Doe") expect(group_members_list).to have_content("John Doe")
......
...@@ -178,11 +178,13 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps ...@@ -178,11 +178,13 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
def select_using_dropdown(dropdown_type, selection, is_commit = false) def select_using_dropdown(dropdown_type, selection, is_commit = false)
dropdown = find(".js-compare-#{dropdown_type}-dropdown") dropdown = find(".js-compare-#{dropdown_type}-dropdown")
dropdown.find(".compare-dropdown-toggle").click dropdown.find(".compare-dropdown-toggle").click
dropdown.find('.dropdown-menu', visible: true)
dropdown.fill_in("Filter by Git revision", with: selection) dropdown.fill_in("Filter by Git revision", with: selection)
if is_commit if is_commit
dropdown.find('input[type="search"]').send_keys(:return) dropdown.find('input[type="search"]').send_keys(:return)
else else
find_link(selection, visible: true).click find_link(selection, visible: true).click
end end
dropdown.find('.dropdown-menu', visible: false)
end end
end end
...@@ -87,9 +87,9 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps ...@@ -87,9 +87,9 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I fill the new branch name' do step 'I fill the new branch name' do
first('button.js-target-branch', visible: true).click first('button.js-target-branch', visible: true).click
first('.create-new-branch', visible: true).click find('.create-new-branch', visible: true).click
first('#new_branch_name', visible: true).set('new_branch_name') find('#new_branch_name', visible: true).set('new_branch_name')
first('.js-new-branch-btn', visible: true).click find('.js-new-branch-btn', visible: true).click
end end
step 'I fill the new file name with an illegal name' do step 'I fill the new file name with an illegal name' do
...@@ -377,7 +377,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps ...@@ -377,7 +377,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I should see a Fork/Cancel combo' do step 'I should see a Fork/Cancel combo' do
expect(page).to have_link 'Fork' expect(page).to have_link 'Fork'
expect(page).to have_button 'Cancel' expect(page).to have_button 'Cancel'
expect(page).to have_content 'You don\'t have permission to edit this file. Try forking this project to edit the file.'
end end
step 'I should see a notice about a new fork having been created' do step 'I should see a notice about a new fork having been created' do
......
...@@ -4,25 +4,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps ...@@ -4,25 +4,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
include SharedPaths include SharedPaths
include Select2Helper include Select2Helper
step 'I should be able to see myself in team' do step 'I should not see "Dmitriy" in team list' do
expect(page).to have_content(@user.name)
expect(page).to have_content(@user.username)
end
step 'I should see "Dmitriy" in team list' do
user = User.find_by(name: "Dmitriy") user = User.find_by(name: "Dmitriy")
expect(page).to have_content(user.name) expect(page).not_to have_content(user.name)
expect(page).to have_content(user.username) expect(page).not_to have_content(user.username)
end
step 'I select "Mike" as "Reporter"' do
user = User.find_by(name: "Mike")
page.within ".users-project-form" do
select2(user.id, from: "#user_ids", multiple: true)
select "Reporter", from: "access_level"
end
click_button "Add to project"
end end
step 'I should see "Mike" in team list as "Reporter"' do step 'I should see "Mike" in team list as "Reporter"' do
...@@ -34,60 +19,6 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps ...@@ -34,60 +19,6 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end end
end end
step 'I select "sjobs@apple.com" as "Reporter"' do
page.within ".users-project-form" do
find('#user_ids', visible: false).set('sjobs@apple.com')
select "Reporter", from: "access_level"
end
click_button "Add to project"
end
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
project_member = project.project_members.find_by(invite_email: 'sjobs@apple.com')
page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('sjobs@apple.com')
expect(page).to have_content('Invited')
expect(page).to have_content('Reporter')
end
end
step 'I should see "Dmitriy" in team list as "Developer"' do
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('Dmitriy')
expect(page).to have_content('Developer')
end
end
step 'I change "Dmitriy" role to "Reporter"' do
project = Project.find_by(name: "Shop")
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
click_button project_member.human_access
page.within '.dropdown-menu' do
click_link 'Reporter'
end
end
end
step 'I should see "Dmitriy" in team list as "Reporter"' do
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('Dmitriy')
expect(page).to have_content('Reporter')
end
end
step 'I should not see "Dmitriy" in team list' do
user = User.find_by(name: "Dmitriy")
expect(page).not_to have_content(user.name)
expect(page).not_to have_content(user.username)
end
step 'gitlab user "Mike"' do step 'gitlab user "Mike"' do
create(:user, name: "Mike") create(:user, name: "Mike")
end end
...@@ -113,7 +44,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps ...@@ -113,7 +44,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
project.team << [user, :reporter] project.team << [user, :reporter]
end end
step 'I click link "Import team from another project"' do step 'I click link "Import team from another project"' do
page.within '.users-project-form' do page.within '.users-project-form' do
click_link "Import" click_link "Import"
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment