Commit f20ee984 authored by Clement Ho's avatar Clement Ho

Merge branch 'master' into 'pipelines_show_refactor'

# Conflicts:
#   config/webpack.config.js
parents 124ce3b6 d4867c51
...@@ -11,8 +11,8 @@ engines: ...@@ -11,8 +11,8 @@ engines:
exclude_paths: exclude_paths:
- "lib/api/v3/*" - "lib/api/v3/*"
eslint: eslint:
# eslint-plugin-vue is locked to version 2 in codeclimate, we need version 4 enabled: true
enabled: false channel: "eslint-4"
rubocop: rubocop:
enabled: true enabled: true
channel: "gitlab-rubocop-0-52-1" channel: "gitlab-rubocop-0-52-1"
......
...@@ -2,6 +2,27 @@ ...@@ -2,6 +2,27 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.5.2 (2018-02-25)
### Fixed (7 changes)
- Fix single digit value clipping for stacked progress bar. !17217
- Fix issue with cache key being empty when variable used as the key. !17260
- Enable Legacy Authorization by default on Cluster creations. !17302
- Allow branch names to be named the same as the sha it points to.
- Fix 500 error when loading an invalid upload URL.
- Don't attempt to update user tracked fields if database is in read-only.
- Prevent MR Widget error when no CI configured.
### Performance (5 changes)
- Improve query performance for snippets dashboard. !17088
- Only check LFS integrity for first ref in a push to avoid timeout. !17098
- Improve query performance of MembersFinder. !17190
- Increase feature flag cache TTL to one hour.
- Improve performance of searching for and autocompleting of users.
## 10.5.1 (2018-02-22) ## 10.5.1 (2018-02-22)
- No changes. - No changes.
......
...@@ -397,9 +397,9 @@ For issues related to the open source stewardship of GitLab, ...@@ -397,9 +397,9 @@ For issues related to the open source stewardship of GitLab,
there is the ~"stewardship" label. there is the ~"stewardship" label.
This label is to be used for issues in which the stewardship of GitLab This label is to be used for issues in which the stewardship of GitLab
is a topic of discussion. For instance if GitLab Inc. is planning to remove is a topic of discussion. For instance if GitLab Inc. is planning to add
features from GitLab CE to make exclusive in GitLab EE, related issues features from GitLab EE to GitLab CE, related issues would be labelled with
would be labelled with ~"stewardship". ~"stewardship".
A recent example of this was the issue for A recent example of this was the issue for
[bringing the time tracking API to GitLab CE][time-tracking-issue]. [bringing the time tracking API to GitLab CE][time-tracking-issue].
......
...@@ -411,7 +411,7 @@ group :ed25519 do ...@@ -411,7 +411,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.84.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.85.0', require: 'gitaly'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1' gem 'google-protobuf', '= 3.5.1'
......
...@@ -285,7 +285,7 @@ GEM ...@@ -285,7 +285,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.84.0) gitaly-proto (0.85.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -1057,7 +1057,7 @@ DEPENDENCIES ...@@ -1057,7 +1057,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.84.0) gitaly-proto (~> 0.85.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2) gitlab-markup (~> 1.6.2)
......
<script> <script>
import Sortable from 'vendor/Sortable'; import Sortable from 'vendor/Sortable';
import boardNewIssue from './board_new_issue'; import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue'; import boardCard from './board_card.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
......
/* global ListIssue */ <script>
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import ListIssue from '../models/issue';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
...@@ -17,6 +18,9 @@ export default { ...@@ -17,6 +18,9 @@ export default {
error: false, error: false,
}; };
}, },
mounted() {
this.$refs.input.focus();
},
methods: { methods: {
submit(e) { submit(e) {
e.preventDefault(); e.preventDefault();
...@@ -59,42 +63,51 @@ export default { ...@@ -59,42 +63,51 @@ export default {
eventHub.$emit(`hide-issue-form-${this.list.id}`); eventHub.$emit(`hide-issue-form-${this.list.id}`);
}, },
}, },
mounted() { };
this.$refs.input.focus(); </script>
},
template: ` <template>
<div class="card board-new-issue-form"> <div class="card board-new-issue-form">
<form @submit="submit($event)"> <form @submit="submit($event)">
<div class="flash-container" <div
v-if="error"> class="flash-container"
v-if="error"
>
<div class="flash-alert"> <div class="flash-alert">
An error occurred. Please try again. An error occurred. Please try again.
</div> </div>
</div> </div>
<label class="label-light" <label
:for="list.id + '-title'"> class="label-light"
:for="list.id + '-title'"
>
Title Title
</label> </label>
<input class="form-control" <input
class="form-control"
type="text" type="text"
v-model="title" v-model="title"
ref="input" ref="input"
autocomplete="off" autocomplete="off"
:id="list.id + '-title'" /> :id="list.id + '-title'"
/>
<div class="clearfix prepend-top-10"> <div class="clearfix prepend-top-10">
<button class="btn btn-success pull-left" <button
class="btn btn-success pull-left"
type="submit" type="submit"
:disabled="title === ''" :disabled="title === ''"
ref="submit-button"> ref="submit-button"
>
Submit issue Submit issue
</button> </button>
<button class="btn btn-default pull-right" <button
class="btn btn-default pull-right"
type="button" type="button"
@click="cancel"> @click="cancel"
>
Cancel Cancel
</button> </button>
</div> </div>
</form> </form>
</div> </div>
`, </template>
};
...@@ -2,11 +2,13 @@ ...@@ -2,11 +2,13 @@
import _ from 'underscore'; import _ from 'underscore';
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../flash';
import { __ } from '../locale'; import Flash from '~/flash';
import { __ } from '~/locale';
import FilteredSearchBoards from './filtered_search_boards'; import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub'; import eventHub from './eventhub';
import sidebarEventHub from '../sidebar/event_hub'; import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import/first
import './models/issue'; import './models/issue';
import './models/label'; import './models/label';
import './models/list'; import './models/list';
...@@ -22,9 +24,9 @@ import './components/board'; ...@@ -22,9 +24,9 @@ import './components/board';
import './components/board_sidebar'; import './components/board_sidebar';
import './components/new_list_dropdown'; import './components/new_list_dropdown';
import './components/modal/index'; import './components/modal/index';
import '../vue_shared/vue_resource_interceptor'; import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/first
$(() => { export default () => {
const $boardApp = document.getElementById('board-app'); const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
const ModalStore = gl.issueBoards.ModalStore; const ModalStore = gl.issueBoards.ModalStore;
...@@ -236,4 +238,4 @@ $(() => { ...@@ -236,4 +238,4 @@ $(() => {
</div> </div>
`, `,
}); });
}); };
...@@ -110,3 +110,5 @@ class ListIssue { ...@@ -110,3 +110,5 @@ class ListIssue {
} }
window.ListIssue = ListIssue; window.ListIssue = ListIssue;
export default ListIssue;
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/* global List */ /* global List */
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { getUrlParamsArray } from '../../lib/utils/common_utils'; import { getUrlParamsArray } from '~/lib/utils/common_utils';
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {}; window.gl.issueBoards = window.gl.issueBoards || {};
......
...@@ -15,7 +15,7 @@ const CommitPipelinesTable = Vue.extend(commitPipelinesTable); ...@@ -15,7 +15,7 @@ const CommitPipelinesTable = Vue.extend(commitPipelinesTable);
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.CommitPipelinesTable = CommitPipelinesTable; window.gl.CommitPipelinesTable = CommitPipelinesTable;
document.addEventListener('DOMContentLoaded', () => { export default () => {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
if (pipelineTableViewEl) { if (pipelineTableViewEl) {
...@@ -43,4 +43,4 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -43,4 +43,4 @@ document.addEventListener('DOMContentLoaded', () => {
pipelineTableViewEl.appendChild(table.$el); pipelineTableViewEl.appendChild(table.$el);
} }
} }
}); };
...@@ -6,43 +6,21 @@ import GlFieldErrors from './gl_field_errors'; ...@@ -6,43 +6,21 @@ import GlFieldErrors from './gl_field_errors';
import Shortcuts from './shortcuts'; import Shortcuts from './shortcuts';
import SearchAutocomplete from './search_autocomplete'; import SearchAutocomplete from './search_autocomplete';
var Dispatcher; function initSearch() {
// Only when search form is present
(function() { if ($('.search').length) {
Dispatcher = (function() { return new SearchAutocomplete();
function Dispatcher() {
this.initSearch();
this.initFieldErrors();
this.initPageScripts();
}
Dispatcher.prototype.initPageScripts = function() {
var path, shortcut_handler;
const page = $('body').attr('data-page');
if (!page) {
return false;
} }
}
const fail = () => Flash('Error loading dynamic module'); function initFieldErrors() {
const callDefault = m => m.default(); $('.gl-show-field-errors').each((i, form) => {
new GlFieldErrors(form);
path = page.split(':');
shortcut_handler = null;
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
gfm.setup($(el), {
emojis: true,
members: enableGFM,
issues: enableGFM,
milestones: enableGFM,
mergeRequests: enableGFM,
labels: enableGFM,
});
}); });
}
const shortcutHandlerPages = [ function initPageShortcuts(page) {
const pagesWithCustomShortcuts = [
'projects:activity', 'projects:activity',
'projects:artifacts:browse', 'projects:artifacts:browse',
'projects:artifacts:file', 'projects:artifacts:file',
...@@ -66,117 +44,42 @@ var Dispatcher; ...@@ -66,117 +44,42 @@ var Dispatcher;
'groups:show', 'groups:show',
]; ];
if (shortcutHandlerPages.indexOf(page) !== -1) { if (pagesWithCustomShortcuts.indexOf(page) === -1) {
shortcut_handler = true;
}
switch (path[0]) {
case 'admin':
switch (path[1]) {
case 'broadcast_messages':
import('./pages/admin/broadcast_messages')
.then(callDefault)
.catch(fail);
break;
case 'cohorts':
import('./pages/admin/cohorts')
.then(callDefault)
.catch(fail);
break;
case 'groups':
switch (path[2]) {
case 'show':
import('./pages/admin/groups/show')
.then(callDefault)
.catch(fail);
break;
}
break;
case 'projects':
import('./pages/admin/projects')
.then(callDefault)
.catch(fail);
break;
case 'labels':
switch (path[2]) {
case 'new':
import('./pages/admin/labels/new')
.then(callDefault)
.catch(fail);
break;
case 'edit':
import('./pages/admin/labels/edit')
.then(callDefault)
.catch(fail);
break;
}
case 'abuse_reports':
import('./pages/admin/abuse_reports')
.then(callDefault)
.catch(fail);
break;
}
break;
case 'profiles':
import('./pages/profiles/index')
.then(callDefault)
.catch(fail);
break;
case 'projects':
import('./pages/projects')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
switch (path[1]) {
case 'compare':
import('./pages/projects/compare')
.then(callDefault)
.catch(fail);
break;
case 'create':
case 'new':
import('./pages/projects/new')
.then(callDefault)
.catch(fail);
break;
case 'wikis':
import('./pages/projects/wikis')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
}
break;
}
// If we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) {
new Shortcuts(); new Shortcuts();
} }
}
function initGFMInput() {
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
gfm.setup($(el), {
emojis: true,
members: enableGFM,
issues: enableGFM,
milestones: enableGFM,
mergeRequests: enableGFM,
labels: enableGFM,
});
});
}
function initPerformanceBar() {
if (document.querySelector('#peek')) { if (document.querySelector('#peek')) {
import('./performance_bar') import('./performance_bar')
.then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap .then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap
.catch(fail); .catch(() => Flash('Error loading performance bar module'));
} }
}; }
Dispatcher.prototype.initSearch = function() {
// Only when search form is present
if ($('.search').length) {
return new SearchAutocomplete();
}
};
Dispatcher.prototype.initFieldErrors = function() {
$('.gl-show-field-errors').each((i, form) => {
new GlFieldErrors(form);
});
};
return Dispatcher; export default () => {
})(); initSearch();
})(); initFieldErrors();
export default function initDispatcher() { const page = $('body').attr('data-page');
return new Dispatcher(); if (page) {
} initPageShortcuts(page);
initGFMInput();
initPerformanceBar();
}
};
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
/** /**
* Render environments table. * Render environments table.
*/ */
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import environmentItem from './environment_item.vue'; import environmentItem from './environment_item.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default { export default {
components: { components: {
......
...@@ -5,7 +5,7 @@ import Translate from '../../vue_shared/translate'; ...@@ -5,7 +5,7 @@ import Translate from '../../vue_shared/translate';
Vue.use(Translate); Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({ export default () => new Vue({
el: '#environments-folder-list-view', el: '#environments-folder-list-view',
components: { components: {
environmentsFolderApp, environmentsFolderApp,
...@@ -32,4 +32,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -32,4 +32,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
}, },
}); });
}, },
})); });
<script>
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import FilteredSearchTokenizer from '../filtered_search_tokenizer'; import FilteredSearchTokenizer from '../filtered_search_tokenizer';
export default { export default {
name: 'RecentSearchesDropdownContent', name: 'RecentSearchesDropdownContent',
props: { props: {
items: { items: {
type: Array, type: Array,
...@@ -19,7 +19,6 @@ export default { ...@@ -19,7 +19,6 @@ export default {
required: true, required: true,
}, },
}, },
computed: { computed: {
processedItems() { processedItems() {
return this.items.map((item) => { return this.items.map((item) => {
...@@ -42,7 +41,6 @@ export default { ...@@ -42,7 +41,6 @@ export default {
return this.items.length > 0; return this.items.length > 0;
}, },
}, },
methods: { methods: {
onItemActivated(text) { onItemActivated(text) {
eventHub.$emit('recentSearchesItemSelected', text); eventHub.$emit('recentSearchesItemSelected', text);
...@@ -54,8 +52,9 @@ export default { ...@@ -54,8 +52,9 @@ export default {
eventHub.$emit('requestClearRecentSearches'); eventHub.$emit('requestClearRecentSearches');
}, },
}, },
};
template: ` </script>
<template>
<div> <div>
<div <div
v-if="!isLocalStorageAvailable" v-if="!isLocalStorageAvailable"
...@@ -65,16 +64,20 @@ export default { ...@@ -65,16 +64,20 @@ export default {
<ul v-else-if="hasItems"> <ul v-else-if="hasItems">
<li <li
v-for="(item, index) in processedItems" v-for="(item, index) in processedItems"
:key="index"> :key="`processed-items-${index}`"
>
<button <button
type="button" type="button"
class="filtered-search-history-dropdown-item" class="filtered-search-history-dropdown-item"
@click="onItemActivated(item.text)"> @click="onItemActivated(item.text)">
<span> <span>
<span <span
v-for="(token, tokenIndex) in item.tokens" class="filtered-search-history-dropdown-token"
class="filtered-search-history-dropdown-token"> v-for="(token, index) in item.tokens"
<span class="name">{{ token.prefix }}</span><span class="value">{{ token.suffix }}</span> :key="`dropdown-token-${index}`"
>
<span class="name">{{ token.prefix }}</span>
<span class="value">{{ token.suffix }}</span>
</span> </span>
</span> </span>
<span class="filtered-search-history-dropdown-search-token"> <span class="filtered-search-history-dropdown-search-token">
...@@ -98,5 +101,4 @@ export default { ...@@ -98,5 +101,4 @@ export default {
You don't have any recent searches You don't have any recent searches
</div> </div>
</div> </div>
`, </template>
};
import Vue from 'vue'; import Vue from 'vue';
import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content'; import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content.vue';
import eventHub from './event_hub'; import eventHub from './event_hub';
class RecentSearchesRoot { class RecentSearchesRoot {
...@@ -33,7 +33,7 @@ class RecentSearchesRoot { ...@@ -33,7 +33,7 @@ class RecentSearchesRoot {
this.vm = new Vue({ this.vm = new Vue({
el: this.wrapperElement, el: this.wrapperElement,
components: { components: {
'recent-searches-dropdown-content': RecentSearchesDropdownContent, RecentSearchesDropdownContent,
}, },
data() { return state; }, data() { return state; },
template: ` template: `
......
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago'; import timeAgoMixin from '~/vue_shared/mixins/timeago';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import fileIcon from '~/vue_shared/components/file_icon.vue';
import newDropdown from './new_dropdown/index.vue'; import newDropdown from './new_dropdown/index.vue';
import fileIcon from '../../vue_shared/components/file_icon.vue';
export default { export default {
components: { components: {
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import fileIcon from '../../vue_shared/components/file_icon.vue'; import fileIcon from '~/vue_shared/components/file_icon.vue';
export default { export default {
components: { components: {
......
...@@ -213,7 +213,7 @@ export default class LabelsSelect { ...@@ -213,7 +213,7 @@ export default class LabelsSelect {
} }
} }
if (label.duplicate) { if (label.duplicate) {
color = gl.DropdownUtils.duplicateLabelColor(label.color); color = DropdownUtils.duplicateLabelColor(label.color);
} }
else { else {
if (label.color != null) { if (label.color != null) {
...@@ -242,10 +242,16 @@ export default class LabelsSelect { ...@@ -242,10 +242,16 @@ export default class LabelsSelect {
filterable: true, filterable: true,
selected: $dropdown.data('selected') || [], selected: $dropdown.data('selected') || [],
toggleLabel: function(selected, el) { toggleLabel: function(selected, el) {
var $dropdownParent = $dropdown.parent();
var $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
var isSelected = el !== null ? el.hasClass('is-active') : false; var isSelected = el !== null ? el.hasClass('is-active') : false;
var title = selected.title; var title = selected.title;
var selectedLabels = this.selected; var selectedLabels = this.selected;
if ($dropdownInputField.length && $dropdownInputField.val().length) {
$dropdownParent.find('.dropdown-input-clear').trigger('click');
}
if (selected.id === 0) { if (selected.id === 0) {
this.selected = []; this.selected = [];
return 'No Label'; return 'No Label';
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */
import ShortcutsNetwork from '../shortcuts_network';
import Network from './network';
$(function() {
if (!$(".network-graph").length) return;
var network_graph;
network_graph = new Network({
url: $(".network-graph").attr('data-url'),
commit_url: $(".network-graph").attr('data-commit-url'),
ref: $(".network-graph").attr('data-ref'),
commit_id: $(".network-graph").attr('data-commit-id')
});
return new ShortcutsNetwork(network_graph.branch_graph);
});
import AbuseReports from './abuse_reports'; import AbuseReports from './abuse_reports';
export default () => new AbuseReports(); document.addEventListener('DOMContentLoaded', () => new AbuseReports());
...@@ -3,7 +3,7 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -3,7 +3,7 @@ import axios from '~/lib/utils/axios_utils';
import flash from '~/flash'; import flash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
export default function initBroadcastMessagesForm() { export default () => {
$('input#broadcast_message_color').on('input', function onMessageColorInput() { $('input#broadcast_message_color').on('input', function onMessageColorInput() {
const previewColor = $(this).val(); const previewColor = $(this).val();
$('div.broadcast-message-preview').css('background-color', previewColor); $('div.broadcast-message-preview').css('background-color', previewColor);
...@@ -32,4 +32,4 @@ export default function initBroadcastMessagesForm() { ...@@ -32,4 +32,4 @@ export default function initBroadcastMessagesForm() {
.catch(() => flash(__('An error occurred while rendering preview broadcast message'))); .catch(() => flash(__('An error occurred while rendering preview broadcast message')));
} }
}, 250)); }, 250));
} };
import initBroadcastMessagesForm from './broadcast_message'; import initBroadcastMessagesForm from './broadcast_message';
export default () => initBroadcastMessagesForm(); document.addEventListener('DOMContentLoaded', initBroadcastMessagesForm);
import initUsagePing from './usage_ping'; import initUsagePing from './usage_ping';
export default () => initUsagePing(); document.addEventListener('DOMContentLoaded', initUsagePing);
import UsersSelect from '../../../../users_select'; import UsersSelect from '../../../../users_select';
export default () => new UsersSelect(); document.addEventListener('DOMContentLoaded', () => new UsersSelect());
import Labels from '../../../../labels'; import Labels from '../../../../labels';
export default () => new Labels(); document.addEventListener('DOMContentLoaded', () => new Labels());
import Labels from '../../../../labels'; import Labels from '../../../../labels';
export default () => new Labels(); document.addEventListener('DOMContentLoaded', () => new Labels());
import ProjectsList from '../../../projects_list'; import ProjectsList from '../../../projects_list';
import NamespaceSelect from '../../../namespace_select'; import NamespaceSelect from '../../../namespace_select';
export default () => { document.addEventListener('DOMContentLoaded', () => {
new ProjectsList(); // eslint-disable-line no-new new ProjectsList(); // eslint-disable-line no-new
document.querySelectorAll('.js-namespace-select') document.querySelectorAll('.js-namespace-select')
.forEach(dropdown => new NamespaceSelect({ dropdown })); .forEach(dropdown => new NamespaceSelect({ dropdown }));
}; });
import NotificationsForm from '../../../notifications_form'; import NotificationsForm from '../../../notifications_form';
import notificationsDropdown from '../../../notifications_dropdown'; import notificationsDropdown from '../../../notifications_dropdown';
export default () => { document.addEventListener('DOMContentLoaded', () => {
new NotificationsForm(); // eslint-disable-line no-new new NotificationsForm(); // eslint-disable-line no-new
notificationsDropdown(); notificationsDropdown();
}; });
import UsersSelect from '~/users_select'; import UsersSelect from '~/users_select';
import ShortcutsNavigation from '~/shortcuts_navigation'; import ShortcutsNavigation from '~/shortcuts_navigation';
import initBoards from '~/boards';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new UsersSelect(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
initBoards();
}); });
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new MiniPipelineGraph({ new MiniPipelineGraph({
container: '.js-commit-pipeline-graph', container: '.js-commit-pipeline-graph',
}).bindEvents(); }).bindEvents();
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
initPipelines();
}); });
...@@ -5,6 +5,7 @@ import ShortcutsNavigation from '~/shortcuts_navigation'; ...@@ -5,6 +5,7 @@ import ShortcutsNavigation from '~/shortcuts_navigation';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import initNotes from '~/init_notes'; import initNotes from '~/init_notes';
import initChangesDropdown from '~/init_changes_dropdown'; import initChangesDropdown from '~/init_changes_dropdown';
import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import { fetchCommitMergeRequests } from '~/commit_merge_requests'; import { fetchCommitMergeRequests } from '~/commit_merge_requests';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
...@@ -19,4 +20,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -19,4 +20,5 @@ document.addEventListener('DOMContentLoaded', () => {
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop); initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop);
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
fetchCommitMergeRequests(); fetchCommitMergeRequests();
initDiffNotes();
}); });
import initCompareAutocomplete from '~/compare_autocomplete'; import initCompareAutocomplete from '~/compare_autocomplete';
export default () => { document.addEventListener('DOMContentLoaded', initCompareAutocomplete);
initCompareAutocomplete();
};
import initEnvironmentsFolderBundle from '~/environments/folder/environments_folder_bundle';
document.addEventListener('DOMContentLoaded', initEnvironmentsFolderBundle);
import Project from './project'; import Project from './project';
import ShortcutsNavigation from '../../shortcuts_navigation'; import ShortcutsNavigation from '../../shortcuts_navigation';
export default () => { document.addEventListener('DOMContentLoaded', () => {
new Project(); // eslint-disable-line no-new new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
}; });
import initIssuableSidebar from '~/init_issuable_sidebar'; import initIssuableSidebar from '~/init_issuable_sidebar';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import Issue from '~/issue'; import Issue from '~/issue';
import ShortcutsIssuable from '~/shortcuts_issuable'; import ShortcutsIssuable from '~/shortcuts_issuable';
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
...@@ -10,4 +11,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -10,4 +11,5 @@ document.addEventListener('DOMContentLoaded', () => {
new ShortcutsIssuable(); // eslint-disable-line no-new new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar(); initIssuableSidebar();
initSidebarBundle();
}); });
import initSidebarBundle from '~/sidebar/sidebar_bundle';
document.addEventListener('DOMContentLoaded', initSidebarBundle);
import initSidebarBundle from '~/sidebar/sidebar_bundle';
document.addEventListener('DOMContentLoaded', initSidebarBundle);
import Compare from '~/compare'; import Compare from '~/compare';
import MergeRequest from '~/merge_request'; import MergeRequest from '~/merge_request';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
...@@ -14,5 +15,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -14,5 +15,6 @@ document.addEventListener('DOMContentLoaded', () => {
new MergeRequest({ // eslint-disable-line no-new new MergeRequest({ // eslint-disable-line no-new
action: mrNewSubmitNode.dataset.mrSubmitAction, action: mrNewSubmitNode.dataset.mrSubmitAction,
}); });
initPipelines();
} }
}); });
...@@ -3,18 +3,23 @@ import ZenMode from '~/zen_mode'; ...@@ -3,18 +3,23 @@ import ZenMode from '~/zen_mode';
import initNotes from '~/init_notes'; import initNotes from '~/init_notes';
import initIssuableSidebar from '~/init_issuable_sidebar'; import initIssuableSidebar from '~/init_issuable_sidebar';
import initDiffNotes from '~/diff_notes/diff_notes_bundle'; import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import ShortcutsIssuable from '~/shortcuts_issuable'; import ShortcutsIssuable from '~/shortcuts_issuable';
import Diff from '~/diff'; import Diff from '~/diff';
import { handleLocationHash } from '~/lib/utils/common_utils'; import { handleLocationHash } from '~/lib/utils/common_utils';
import howToMerge from '~/how_to_merge'; import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initWidget from '../../../../vue_merge_request_widget';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new new Diff(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar(); initIssuableSidebar();
initSidebarBundle();
initNotes(); initNotes();
initDiffNotes(); initDiffNotes();
initPipelines();
const mrShowNode = document.querySelector('.merge-request'); const mrShowNode = document.querySelector('.merge-request');
...@@ -25,4 +30,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -25,4 +30,5 @@ document.addEventListener('DOMContentLoaded', () => {
new ShortcutsIssuable(true); // eslint-disable-line no-new new ShortcutsIssuable(true); // eslint-disable-line no-new
handleLocationHash(); handleLocationHash();
howToMerge(); howToMerge();
initWidget();
}); });
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */
import BranchGraph from './branch_graph'; import BranchGraph from '../../../network/branch_graph';
export default (function() { export default (function() {
function Network(opts) { function Network(opts) {
......
import ShortcutsNetwork from '../../../../shortcuts_network';
import Network from '../network';
document.addEventListener('DOMContentLoaded', () => {
if (!$('.network-graph').length) return;
const networkGraph = new Network({
url: $('.network-graph').attr('data-url'),
commit_url: $('.network-graph').attr('data-commit-url'),
ref: $('.network-graph').attr('data-ref'),
commit_id: $('.network-graph').attr('data-commit-id'),
});
// eslint-disable-next-line no-new
new ShortcutsNetwork(networkGraph.branch_graph);
});
...@@ -2,8 +2,8 @@ import ProjectNew from '../shared/project_new'; ...@@ -2,8 +2,8 @@ import ProjectNew from '../shared/project_new';
import initProjectVisibilitySelector from '../../../project_visibility'; import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new'; import initProjectNew from '../../../projects/project_new';
export default () => { document.addEventListener('DOMContentLoaded', () => {
new ProjectNew(); // eslint-disable-line no-new new ProjectNew(); // eslint-disable-line no-new
initProjectVisibilitySelector(); initProjectVisibilitySelector();
initProjectNew.bindEvents(); initProjectNew.bindEvents();
}; });
import Vue from 'vue'; import Vue from 'vue';
import PipelinesStore from './stores/pipelines_store'; import PipelinesStore from '../../../../pipelines/stores/pipelines_store';
import pipelinesComponent from './components/pipelines.vue'; import pipelinesComponent from '../../../../pipelines/components/pipelines.vue';
import Translate from '../vue_shared/translate'; import Translate from '../../../../vue_shared/translate';
Vue.use(Translate); Vue.use(Translate);
......
...@@ -3,9 +3,9 @@ import ShortcutsWiki from '../../../shortcuts_wiki'; ...@@ -3,9 +3,9 @@ import ShortcutsWiki from '../../../shortcuts_wiki';
import ZenMode from '../../../zen_mode'; import ZenMode from '../../../zen_mode';
import GLForm from '../../../gl_form'; import GLForm from '../../../gl_form';
export default () => { document.addEventListener('DOMContentLoaded', () => {
new Wikis(); // eslint-disable-line no-new new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
new GLForm($('.wiki-form'), true); // eslint-disable-line no-new new GLForm($('.wiki-form'), true); // eslint-disable-line no-new
}; });
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import Vue from 'vue'; import Vue from 'vue';
import VueResource from 'vue-resource'; import VueResource from 'vue-resource';
import '../../vue_shared/vue_resource_interceptor';
Vue.use(VueResource); Vue.use(VueResource);
......
...@@ -9,13 +9,12 @@ export default class ShortcutsIssuable extends Shortcuts { ...@@ -9,13 +9,12 @@ export default class ShortcutsIssuable extends Shortcuts {
super(); super();
this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form'); this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form');
this.editBtn = document.querySelector('.js-issuable-edit');
Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee')); Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee'));
Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone')); Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone'));
Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels')); Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels'));
Mousetrap.bind('r', this.replyWithSelectedText.bind(this)); Mousetrap.bind('r', this.replyWithSelectedText.bind(this));
Mousetrap.bind('e', this.editIssue.bind(this)); Mousetrap.bind('e', ShortcutsIssuable.editIssue);
if (isMergeRequest) { if (isMergeRequest) {
this.enabledHelp.push('.hidden-shortcut.merge_requests'); this.enabledHelp.push('.hidden-shortcut.merge_requests');
...@@ -58,10 +57,10 @@ export default class ShortcutsIssuable extends Shortcuts { ...@@ -58,10 +57,10 @@ export default class ShortcutsIssuable extends Shortcuts {
return false; return false;
} }
editIssue() { static editIssue() {
// Need to click the element as on issues, editing is inline // Need to click the element as on issues, editing is inline
// on merge request, editing is on a different page // on merge request, editing is on a different page
this.editBtn.click(); document.querySelector('.js-issuable-edit').click();
return false; return false;
} }
......
import Mediator from './sidebar_mediator'; import Mediator from './sidebar_mediator';
import { mountSidebar, getSidebarOptions } from './mount_sidebar'; import { mountSidebar, getSidebarOptions } from './mount_sidebar';
function domContentLoaded() { export default () => {
const mediator = new Mediator(getSidebarOptions()); const mediator = new Mediator(getSidebarOptions());
mediator.fetch(); mediator.fetch();
mountSidebar(mediator); mountSidebar(mediator);
} };
document.addEventListener('DOMContentLoaded', domContentLoaded);
export default domContentLoaded;
<script> <script>
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
import pipelineStage from '../../pipelines/components/stage.vue'; import pipelineStage from '~/pipelines/components/stage.vue';
import ciIcon from '../../vue_shared/components/ci_icon.vue'; import ciIcon from '~/vue_shared/components/ci_icon.vue';
import icon from '../../vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
export default { export default {
name: 'MRWidgetPipeline', name: 'MRWidgetPipeline',
......
...@@ -227,7 +227,8 @@ export default { ...@@ -227,7 +227,8 @@ export default {
@click="handleMergeButtonClick()" @click="handleMergeButtonClick()"
:disabled="isMergeButtonDisabled" :disabled="isMergeButtonDisabled"
:class="mergeButtonClass" :class="mergeButtonClass"
type="button"> type="button"
class="qa-merge-button">
<i <i
v-if="isMakingRequest" v-if="isMakingRequest"
class="fa fa-spinner fa-spin" class="fa fa-spinner fa-spin"
......
...@@ -111,7 +111,7 @@ js-toggle-container accept-action media space-children" ...@@ -111,7 +111,7 @@ js-toggle-container accept-action media space-children"
> >
<button <button
type="button" type="button"
class="btn btn-sm btn-reopen btn-success" class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button"
:disabled="isMakingRequest" :disabled="isMakingRequest"
@click="rebase" @click="rebase"
> >
......
...@@ -6,7 +6,7 @@ import Translate from '../vue_shared/translate'; ...@@ -6,7 +6,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate); Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => { export default () => {
gl.mrWidgetData.gitlabLogo = gon.gitlab_logo; gl.mrWidgetData.gitlabLogo = gon.gitlab_logo;
const vm = new Vue(mrWidgetOptions); const vm = new Vue(mrWidgetOptions);
...@@ -14,4 +14,4 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -14,4 +14,4 @@ document.addEventListener('DOMContentLoaded', () => {
window.gl.mrWidget = { window.gl.mrWidget = {
checkStatus: vm.checkStatus, checkStatus: vm.checkStatus,
}; };
}); };
...@@ -196,17 +196,9 @@ ...@@ -196,17 +196,9 @@
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
font-size: 0; font-size: 0;
div {
display: inline;
}
.fa-spinner { .fa-spinner {
font-size: 12px; font-size: 12px;
} }
span {
font-size: 6px;
}
} }
.ci-status-link { .ci-status-link {
......
...@@ -13,6 +13,16 @@ ...@@ -13,6 +13,16 @@
display: inline-block; display: inline-block;
} }
.issuable-meta {
.author_link {
display: inline-block;
}
.issuable-comments {
height: 18px;
}
}
.icon-merge-request-unmerged { .icon-merge-request-unmerged {
height: 13px; height: 13px;
margin-bottom: 3px; margin-bottom: 3px;
......
...@@ -48,7 +48,7 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -48,7 +48,7 @@ class Admin::GroupsController < Admin::ApplicationController
def members_update def members_update
member_params = params.permit(:user_ids, :access_level, :expires_at) member_params = params.permit(:user_ids, :access_level, :expires_at)
result = Members::CreateService.new(@group, current_user, member_params.merge(limit: -1)).execute result = Members::CreateService.new(current_user, member_params.merge(limit: -1)).execute(@group)
if result[:status] == :success if result[:status] == :success
redirect_to [:admin, @group], notice: 'Users were successfully added.' redirect_to [:admin, @group], notice: 'Users were successfully added.'
......
...@@ -3,20 +3,31 @@ module MembershipActions ...@@ -3,20 +3,31 @@ module MembershipActions
def create def create
create_params = params.permit(:user_ids, :access_level, :expires_at) create_params = params.permit(:user_ids, :access_level, :expires_at)
result = Members::CreateService.new(membershipable, current_user, create_params).execute result = Members::CreateService.new(current_user, create_params).execute(membershipable)
redirect_url = members_page_url
if result[:status] == :success if result[:status] == :success
redirect_to redirect_url, notice: 'Users were successfully added.' redirect_to members_page_url, notice: 'Users were successfully added.'
else else
redirect_to redirect_url, alert: result[:message] redirect_to members_page_url, alert: result[:message]
end
end
def update
update_params = params.require(root_params_key).permit(:access_level, :expires_at)
member = membershipable.members_and_requesters.find(params[:id])
member = Members::UpdateService
.new(current_user, update_params)
.execute(member)
.present(current_user: current_user)
respond_to do |format|
format.js { render 'shared/members/update', locals: { member: member } }
end end
end end
def destroy def destroy
Members::DestroyService.new(membershipable, current_user, params) member = membershipable.members_and_requesters.find(params[:id])
.execute(:all) Members::DestroyService.new(current_user).execute(member)
respond_to do |format| respond_to do |format|
format.html do format.html do
...@@ -36,14 +47,17 @@ module MembershipActions ...@@ -36,14 +47,17 @@ module MembershipActions
end end
def approve_access_request def approve_access_request
Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute access_requester = membershipable.requesters.find(params[:id])
Members::ApproveAccessRequestService
.new(current_user, params)
.execute(access_requester)
redirect_to members_page_url redirect_to members_page_url
end end
def leave def leave
member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id) member = membershipable.members_and_requesters.find_by!(user_id: current_user.id)
.execute(:all) Members::DestroyService.new(current_user).execute(member)
notice = notice =
if member.request? if member.request?
...@@ -62,17 +76,43 @@ module MembershipActions ...@@ -62,17 +76,43 @@ module MembershipActions
end end
end end
def resend_invite
member = membershipable.members.find(params[:id])
if member.invite?
member.resend_invite
redirect_to members_page_url, notice: 'The invitation was successfully resent.'
else
redirect_to members_page_url, alert: 'The invitation has already been accepted.'
end
end
protected protected
def membershipable def membershipable
raise NotImplementedError raise NotImplementedError
end end
def root_params_key
case membershipable
when Namespace
:group_member
when Project
:project_member
else
raise "Unknown membershipable type: #{membershipable}!"
end
end
def members_page_url def members_page_url
if membershipable.is_a?(Project) case membershipable
when Namespace
polymorphic_url([membershipable, :members])
when Project
project_project_members_path(membershipable) project_project_members_path(membershipable)
else else
polymorphic_url([membershipable, :members]) raise "Unknown membershipable type: #{membershipable}!"
end end
end end
......
...@@ -18,10 +18,6 @@ class Groups::ApplicationController < ApplicationController ...@@ -18,10 +18,6 @@ class Groups::ApplicationController < ApplicationController
@projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute @projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute
end end
def group_merge_requests
@group_merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id).execute
end
def authorize_admin_group! def authorize_admin_group!
unless can?(current_user, :admin_group, group) unless can?(current_user, :admin_group, group)
return render_404 return render_404
......
...@@ -27,35 +27,6 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -27,35 +27,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
@group_member = @group.group_members.new @group_member = @group.group_members.new
end end
def update
@group_member = @group.members_and_requesters.find(params[:id])
.present(current_user: current_user)
return render_403 unless can?(current_user, :update_group_member, @group_member)
@group_member.update_attributes(member_params)
end
def resend_invite
redirect_path = group_group_members_path(@group)
@group_member = @group.group_members.find(params[:id])
if @group_member.invite?
@group_member.resend_invite
redirect_to redirect_path, notice: 'The invitation was successfully resent.'
else
redirect_to redirect_path, alert: 'The invitation has already been accepted.'
end
end
protected
def member_params
params.require(:group_member).permit(:access_level, :user_id, :expires_at)
end
# MembershipActions concern # MembershipActions concern
alias_method :membershipable, :group alias_method :membershipable, :group
end end
...@@ -14,7 +14,6 @@ class GroupsController < Groups::ApplicationController ...@@ -14,7 +14,6 @@ class GroupsController < Groups::ApplicationController
before_action :authorize_create_group!, only: [:new] before_action :authorize_create_group!, only: [:new]
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests] before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
before_action :group_merge_requests, only: [:merge_requests]
before_action :event_filter, only: [:activity] before_action :event_filter, only: [:activity]
before_action :user_actions, only: [:show, :subgroups] before_action :user_actions, only: [:show, :subgroups]
......
...@@ -26,29 +26,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -26,29 +26,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_member = @project.project_members.new @project_member = @project.project_members.new
end end
def update
@project_member = @project.members_and_requesters.find(params[:id])
.present(current_user: current_user)
return render_403 unless can?(current_user, :update_project_member, @project_member)
@project_member.update_attributes(member_params)
end
def resend_invite
redirect_path = project_project_members_path(@project)
@project_member = @project.project_members.find(params[:id])
if @project_member.invite?
@project_member.resend_invite
redirect_to redirect_path, notice: 'The invitation was successfully resent.'
else
redirect_to redirect_path, alert: 'The invitation has already been accepted.'
end
end
def import def import
@projects = current_user.authorized_projects.order_id_desc @projects = current_user.authorized_projects.order_id_desc
end end
...@@ -67,12 +44,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -67,12 +44,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
notice: notice) notice: notice)
end end
protected
def member_params
params.require(:project_member).permit(:user_id, :access_level, :expires_at)
end
# MembershipActions concern # MembershipActions concern
alias_method :membershipable, :project alias_method :membershipable, :project
end end
...@@ -39,7 +39,7 @@ class LabelsFinder < UnionFinder ...@@ -39,7 +39,7 @@ class LabelsFinder < UnionFinder
end end
end end
elsif only_group_labels? elsif only_group_labels?
label_ids << Label.where(group_id: group.id) label_ids << Label.where(group_id: group_ids)
else else
label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id)) label_ids << Label.where(project_id: projects.select(:id))
...@@ -59,10 +59,11 @@ class LabelsFinder < UnionFinder ...@@ -59,10 +59,11 @@ class LabelsFinder < UnionFinder
items.where(title: title) items.where(title: title)
end end
def group def group_ids
strong_memoize(:group) do strong_memoize(:group_ids) do
group = Group.find(params[:group_id]) group = Group.find(params[:group_id])
authorized_to_read_labels?(group) && group groups = params[:include_ancestor_groups].present? ? group.self_and_ancestors : [group]
groups_user_can_read_labels(groups).map(&:id)
end end
end end
...@@ -120,4 +121,10 @@ class LabelsFinder < UnionFinder ...@@ -120,4 +121,10 @@ class LabelsFinder < UnionFinder
Ability.allowed?(current_user, :read_label, label_parent) Ability.allowed?(current_user, :read_label, label_parent)
end end
def groups_user_can_read_labels(groups)
DeclarativePolicy.user_scope do
groups.select { |group| authorized_to_read_labels?(group) }
end
end
end end
...@@ -16,71 +16,38 @@ module BlobHelper ...@@ -16,71 +16,38 @@ module BlobHelper
options[:link_opts]) options[:link_opts])
end end
def edit_blob_link(project = @project, ref = @ref, path = @path, options = {})
blob = options.delete(:blob)
blob ||= project.repository.blob_at(ref, path) rescue nil
return unless blob && blob.readable_text?
common_classes = "btn js-edit-blob #{options[:extra_class]}"
if !on_top_of_branch?(project, ref)
button_tag 'Edit', class: "#{common_classes} disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
# This condition applies to anonymous or users who can edit directly
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
link_to 'Edit', edit_blob_path(project, ref, path, options), class: "#{common_classes} btn-sm"
elsif current_user && can?(current_user, :fork_project, project)
continue_params = {
to: edit_blob_path(project, ref, path, options),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
fork_path = project_forks_path(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
def ide_edit_path(project = @project, ref = @ref, path = @path, options = {}) def ide_edit_path(project = @project, ref = @ref, path = @path, options = {})
"#{ide_path}/project#{edit_blob_path(project, ref, path, options)}" "#{ide_path}/project#{edit_blob_path(project, ref, path, options)}"
end end
def ide_edit_text def edit_blob_button(project = @project, ref = @ref, path = @path, options = {})
"#{_('Web IDE')}" return unless blob = readable_blob(options, path, project, ref)
end
def ide_blob_link(project = @project, ref = @ref, path = @path, options = {}) common_classes = "btn js-edit-blob #{options[:extra_class]}"
return unless show_new_ide?
blob = options.delete(:blob) edit_button_tag(blob,
blob ||= project.repository.blob_at(ref, path) rescue nil common_classes,
_('Edit'),
edit_blob_path(project, ref, path, options),
project,
ref)
end
return unless blob && blob.readable_text? def ide_edit_button(project = @project, ref = @ref, path = @path, options = {})
return unless show_new_ide?
return unless blob = readable_blob(options, path, project, ref)
common_classes = "btn js-edit-ide #{options[:extra_class]}" common_classes = "btn js-edit-ide #{options[:extra_class]}"
if !on_top_of_branch?(project, ref) edit_button_tag(blob,
button_tag ide_edit_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' } common_classes,
# This condition applies to anonymous or users who can edit directly _('Web IDE'),
elsif current_user && can_modify_blob?(blob, project, ref) ide_edit_path(project, ref, path, options),
link_to ide_edit_text, ide_edit_path(project, ref, path, options), class: "#{common_classes} btn-sm" project,
elsif current_user && can?(current_user, :fork_project, project) ref)
continue_params = {
to: ide_edit_path(project, ref, path, options),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params)
button_tag ide_edit_text,
class: common_classes,
data: { fork_path: fork_path }
end
end end
def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:) def modify_file_button(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
return unless current_user return unless current_user
blob = project.repository.blob_at(ref, path) rescue nil blob = project.repository.blob_at(ref, path) rescue nil
...@@ -96,21 +63,12 @@ module BlobHelper ...@@ -96,21 +63,12 @@ module BlobHelper
elsif can_modify_blob?(blob, project, ref) elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "#{common_classes}", '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 = { edit_fork_button_tag(common_classes, project, label, edit_modify_file_fork_params(action), action)
to: request.fullpath,
notice: edit_in_new_fork_notice + " Try to #{action} this file again.",
notice_now: edit_in_new_fork_notice_now
}
fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: continue_params)
button_tag label,
class: "#{common_classes} js-edit-blob-link-fork-toggler",
data: { action: action, fork_path: fork_path }
end end
end end
def replace_blob_link(project = @project, ref = @ref, path = @path) def replace_blob_link(project = @project, ref = @ref, path = @path)
modify_file_link( modify_file_button(
project, project,
ref, ref,
path, path,
...@@ -122,7 +80,7 @@ module BlobHelper ...@@ -122,7 +80,7 @@ module BlobHelper
end end
def delete_blob_link(project = @project, ref = @ref, path = @path) def delete_blob_link(project = @project, ref = @ref, path = @path)
modify_file_link( modify_file_button(
project, project,
ref, ref,
path, path,
...@@ -332,4 +290,55 @@ module BlobHelper ...@@ -332,4 +290,55 @@ module BlobHelper
options options
end end
def readable_blob(options, path, project, ref)
blob = options.delete(:blob)
blob ||= project.repository.blob_at(ref, path) rescue nil
blob if blob&.readable_text?
end
def edit_blob_fork_params(path)
{
to: path,
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
end
def edit_modify_file_fork_params(action)
{
to: request.fullpath,
notice: edit_in_new_fork_notice_action(action),
notice_now: edit_in_new_fork_notice_now
}
end
def edit_fork_button_tag(common_classes, project, label, params, action = 'edit')
fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: params)
button_tag label,
class: "#{common_classes} js-edit-blob-link-fork-toggler",
data: { action: action, fork_path: fork_path }
end
def edit_disabled_button_tag(button_text, common_classes)
button_tag(button_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' })
end
def edit_link_tag(link_text, edit_path, common_classes)
link_to link_text, edit_path, class: "#{common_classes} btn-sm"
end
def edit_button_tag(blob, common_classes, text, edit_path, project, ref)
if !on_top_of_branch?(project, ref)
edit_disabled_button_tag(text, common_classes)
# This condition only applies to users who are logged in
# Web IDE (Beta) requires the user to have this feature enabled
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
edit_link_tag(text, edit_path, common_classes)
elsif current_user && can?(current_user, :fork_project, project)
edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path))
end
end
end end
...@@ -19,6 +19,20 @@ module GroupsHelper ...@@ -19,6 +19,20 @@ module GroupsHelper
can?(current_user, :change_share_with_group_lock, group) can?(current_user, :change_share_with_group_lock, group)
end end
def group_issues_count(state:)
IssuesFinder
.new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
.execute
.count
end
def group_merge_requests_count(state:)
MergeRequestsFinder
.new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
.execute
.count
end
def group_icon(group, options = {}) def group_icon(group, options = {})
img_path = group_icon_url(group, options) img_path = group_icon_url(group, options)
image_tag img_path, options image_tag img_path, options
...@@ -77,10 +91,6 @@ module GroupsHelper ...@@ -77,10 +91,6 @@ module GroupsHelper
end end
end end
def group_issues(group)
IssuesFinder.new(current_user, group_id: group.id).execute
end
def remove_group_message(group) def remove_group_message(group)
_("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") % _("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
{ group_name: group.name } { group_name: group.name }
......
...@@ -234,7 +234,7 @@ module IssuablesHelper ...@@ -234,7 +234,7 @@ module IssuablesHelper
data.merge!(updated_at_by(issuable)) data.merge!(updated_at_by(issuable))
data.to_json data
end end
def updated_at_by(issuable) def updated_at_by(issuable)
......
...@@ -83,6 +83,10 @@ module TreeHelper ...@@ -83,6 +83,10 @@ module TreeHelper
" A fork of this project has been created that you can make changes in, so you can submit a merge request." " A fork of this project has been created that you can make changes in, so you can submit a merge request."
end end
def edit_in_new_fork_notice_action(action)
edit_in_new_fork_notice + " Try to #{action} this file again."
end
def commit_in_fork_help def commit_in_fork_help
"A new branch will be created in your fork and a new merge request will be started." "A new branch will be created in your fork and a new merge request will be started."
end end
......
class ChatName < ActiveRecord::Base class ChatName < ActiveRecord::Base
LAST_USED_AT_INTERVAL = 1.hour
belongs_to :service belongs_to :service
belongs_to :user belongs_to :user
...@@ -9,4 +11,23 @@ class ChatName < ActiveRecord::Base ...@@ -9,4 +11,23 @@ class ChatName < ActiveRecord::Base
validates :user_id, uniqueness: { scope: [:service_id] } validates :user_id, uniqueness: { scope: [:service_id] }
validates :chat_id, uniqueness: { scope: [:service_id, :team_id] } validates :chat_id, uniqueness: { scope: [:service_id, :team_id] }
# Updates the "last_used_timestamp" but only if it wasn't already updated
# recently.
#
# The throttling this method uses is put in place to ensure that high chat
# traffic doesn't result in many UPDATE queries being performed.
def update_last_used_at
return unless update_last_used_at?
obtained = Gitlab::ExclusiveLease
.new("chat_name/last_used_at/#{id}", timeout: LAST_USED_AT_INTERVAL.to_i)
.try_obtain
touch(:last_used_at) if obtained
end
def update_last_used_at?
last_used_at.nil? || last_used_at > LAST_USED_AT_INTERVAL.ago
end
end end
...@@ -6,7 +6,10 @@ module Ci ...@@ -6,7 +6,10 @@ module Ci
belongs_to :group belongs_to :group
validates :key, uniqueness: { scope: :group_id } validates :key, uniqueness: {
scope: :group_id,
message: "(%{value}) has already been taken"
}
scope :unprotected, -> { where(protected: false) } scope :unprotected, -> { where(protected: false) }
end end
......
...@@ -6,7 +6,10 @@ module Ci ...@@ -6,7 +6,10 @@ module Ci
belongs_to :project belongs_to :project
validates :key, uniqueness: { scope: [:project_id, :environment_scope] } validates :key, uniqueness: {
scope: [:project_id, :environment_scope],
message: "(%{value}) has already been taken"
}
scope :unprotected, -> { where(protected: false) } scope :unprotected, -> { where(protected: false) }
end end
......
...@@ -8,6 +8,6 @@ module AccessRequestable ...@@ -8,6 +8,6 @@ module AccessRequestable
extend ActiveSupport::Concern extend ActiveSupport::Concern
def request_access(user) def request_access(user)
Members::RequestAccessService.new(self, user).execute Members::RequestAccessService.new(user).execute(self)
end end
end end
...@@ -128,7 +128,7 @@ class Member < ActiveRecord::Base ...@@ -128,7 +128,7 @@ class Member < ActiveRecord::Base
find_by(invite_token: invite_token) find_by(invite_token: invite_token)
end end
def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil) def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil, ldap: false)
# `user` can be either a User object, User ID or an email to be invited # `user` can be either a User object, User ID or an email to be invited
member = retrieve_member(source, user, existing_members) member = retrieve_member(source, user, existing_members)
access_level = retrieve_access_level(access_level) access_level = retrieve_access_level(access_level)
...@@ -143,11 +143,13 @@ class Member < ActiveRecord::Base ...@@ -143,11 +143,13 @@ class Member < ActiveRecord::Base
if member.request? if member.request?
::Members::ApproveAccessRequestService.new( ::Members::ApproveAccessRequestService.new(
source,
current_user, current_user,
id: member.id,
access_level: access_level access_level: access_level
).execute ).execute(
member,
skip_authorization: ldap,
skip_log_audit_event: ldap
)
else else
member.save member.save
end end
......
...@@ -30,10 +30,10 @@ class SlashCommandsService < Service ...@@ -30,10 +30,10 @@ class SlashCommandsService < Service
def trigger(params) def trigger(params)
return unless valid_token?(params[:token]) return unless valid_token?(params[:token])
user = find_chat_user(params) chat_user = find_chat_user(params)
if user if chat_user&.user
Gitlab::SlashCommands::Command.new(project, user, params).execute Gitlab::SlashCommands::Command.new(project, chat_user, params).execute
else else
url = authorize_chat_name_url(params) url = authorize_chat_name_url(params)
Gitlab::SlashCommands::Presenters::Access.new(url).authorize Gitlab::SlashCommands::Presenters::Access.new(url).authorize
......
...@@ -9,10 +9,9 @@ class Tree ...@@ -9,10 +9,9 @@ class Tree
@repository = repository @repository = repository
@sha = sha @sha = sha
@path = path @path = path
@recursive = recursive
git_repo = @repository.raw_repository git_repo = @repository.raw_repository
@entries = get_entries(git_repo, @sha, @path, recursive: @recursive) @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive)
end end
def readme def readme
...@@ -58,21 +57,4 @@ class Tree ...@@ -58,21 +57,4 @@ class Tree
def sorted_entries def sorted_entries
trees + blobs + submodules trees + blobs + submodules
end end
private
def get_entries(git_repo, sha, path, recursive: false)
current_path_entries = Gitlab::Git::Tree.where(git_repo, sha, path)
ordered_entries = []
current_path_entries.each do |entry|
ordered_entries << entry
if recursive && entry.dir?
ordered_entries.concat(get_entries(git_repo, sha, entry.path, recursive: true))
end
end
ordered_entries
end
end end
...@@ -431,7 +431,7 @@ class User < ActiveRecord::Base ...@@ -431,7 +431,7 @@ class User < ActiveRecord::Base
end end
def self.non_internal def self.non_internal
where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)]) where(internal_attributes.map { |attr| "#{attr} IS NOT TRUE" }.join(" AND "))
end end
# #
......
...@@ -9,8 +9,8 @@ module ChatNames ...@@ -9,8 +9,8 @@ module ChatNames
chat_name = find_chat_name chat_name = find_chat_name
return unless chat_name return unless chat_name
chat_name.touch(:last_used_at) chat_name.update_last_used_at
chat_name.user chat_name
end end
private private
......
...@@ -4,13 +4,33 @@ module Ci ...@@ -4,13 +4,33 @@ module Ci
return if job.job_artifacts_trace return if job.job_artifacts_trace
job.trace.read do |stream| job.trace.read do |stream|
if stream.file? break unless stream.file?
clone_file!(stream.path, JobArtifactUploader.workhorse_upload_path) do |clone_path|
create_job_trace!(job, clone_path)
FileUtils.rm(stream.path)
end
end
end
private
def create_job_trace!(job, path)
File.open(path) do |stream|
job.create_job_artifacts_trace!( job.create_job_artifacts_trace!(
project: job.project, project: job.project,
file_type: :trace, file_type: :trace,
file: stream) file: stream)
end end
end end
def clone_file!(src_path, temp_dir)
FileUtils.mkdir_p(temp_dir)
Dir.mktmpdir('tmp-trace', temp_dir) do |dir_path|
temp_path = File.join(dir_path, "job.log")
FileUtils.copy(src_path, temp_path)
yield(temp_path)
end
end end
end end
end end
...@@ -77,8 +77,12 @@ class IssuableBaseService < BaseService ...@@ -77,8 +77,12 @@ class IssuableBaseService < BaseService
return unless labels return unless labels
params[:label_ids] = labels.split(",").map do |label_name| params[:label_ids] = labels.split(",").map do |label_name|
service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip) label = Labels::FindOrCreateService.new(
label = service.execute current_user,
parent,
title: label_name.strip,
available_labels: available_labels
).execute
label.try(:id) label.try(:id)
end.compact end.compact
...@@ -102,7 +106,7 @@ class IssuableBaseService < BaseService ...@@ -102,7 +106,7 @@ class IssuableBaseService < BaseService
end end
def available_labels def available_labels
LabelsFinder.new(current_user, project_id: @project.id).execute @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end end
def merge_quick_actions_into_params!(issuable) def merge_quick_actions_into_params!(issuable)
...@@ -303,4 +307,8 @@ class IssuableBaseService < BaseService ...@@ -303,4 +307,8 @@ class IssuableBaseService < BaseService
def update_project_counter_caches?(issuable) def update_project_counter_caches?(issuable)
issuable.state_changed? issuable.state_changed?
end end
def parent
project
end
end end
module Labels module Labels
class FindOrCreateService class FindOrCreateService
def initialize(current_user, project, params = {}) def initialize(current_user, parent, params = {})
@current_user = current_user @current_user = current_user
@project = project @parent = parent
@available_labels = params.delete(:available_labels)
@params = params.dup.with_indifferent_access @params = params.dup.with_indifferent_access
end end
...@@ -13,12 +14,13 @@ module Labels ...@@ -13,12 +14,13 @@ module Labels
private private
attr_reader :current_user, :project, :params, :skip_authorization attr_reader :current_user, :parent, :params, :skip_authorization
def available_labels def available_labels
@available_labels ||= LabelsFinder.new( @available_labels ||= LabelsFinder.new(
current_user, current_user,
project_id: project.id "#{parent_type}_id".to_sym => parent.id,
only_group_labels: parent_is_group?
).execute(skip_authorization: skip_authorization) ).execute(skip_authorization: skip_authorization)
end end
...@@ -27,8 +29,8 @@ module Labels ...@@ -27,8 +29,8 @@ module Labels
def find_or_create_label def find_or_create_label
new_label = available_labels.find_by(title: title) new_label = available_labels.find_by(title: title)
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project)) if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent))
new_label = Labels::CreateService.new(params).execute(project: project) new_label = Labels::CreateService.new(params).execute(parent_type.to_sym => parent)
end end
new_label new_label
...@@ -37,5 +39,13 @@ module Labels ...@@ -37,5 +39,13 @@ module Labels
def title def title
params[:title] || params[:name] params[:title] || params[:name]
end end
def parent_type
parent.model_name.param_key
end
def parent_is_group?
parent_type == "group"
end
end end
end end
module Members module Members
class ApproveAccessRequestService < BaseService class ApproveAccessRequestService < Members::BaseService
include MembersHelper def execute(access_requester, skip_authorization: false, skip_log_audit_event: false)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_update_access_requester?(access_requester)
attr_accessor :source
# source - The source object that respond to `#requesters` (i.g. project or group)
# current_user - The user that performs the access request approval
# params - A hash of parameters
# :user_id - User ID used to retrieve the access requester
# :id - Member ID used to retrieve the access requester
# :access_level - Optional access level set when the request is accepted
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user
@params = params.slice(:user_id, :id, :access_level)
end
# opts - A hash of options
# :force - Bypass permission check: current_user can be nil in that case
def execute(opts = {})
condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
access_requester = source.requesters.find_by!(condition)
raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester, opts)
access_requester.access_level = params[:access_level] if params[:access_level] access_requester.access_level = params[:access_level] if params[:access_level]
access_requester.accept_request access_requester.accept_request
after_execute(member: access_requester, skip_log_audit_event: skip_log_audit_event)
access_requester access_requester
end end
private private
def can_update_access_requester?(access_requester, opts = {}) def can_update_access_requester?(access_requester)
access_requester && (
opts[:force] ||
can?(current_user, update_member_permission(access_requester), access_requester) can?(current_user, update_member_permission(access_requester), access_requester)
)
end
def update_member_permission(member)
case member
when GroupMember
:update_group_member
when ProjectMember
:update_project_member
end
end end
end end
end end
module Members
class AuthorizedDestroyService < BaseService
attr_accessor :member, :user
def initialize(member, user = nil)
@member, @user = member, user
end
def execute
return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
Member.transaction do
unassign_issues_and_merge_requests(member) unless member.invite?
member.notification_setting&.destroy
member.destroy
end
if member.request? && member.user != user
notification_service.decline_access_request(member)
end
member
end
private
def unassign_issues_and_merge_requests(member)
if member.is_a?(GroupMember)
issues = Issue.unscoped.select(1)
.joins(:project)
.where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id)
# DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
IssueAssignee.unscoped
.where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues)
.delete_all
MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id)
.execute
.update_all(assignee_id: nil)
else
project = member.source
# SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X
issues = Issue.unscoped.select(1)
.where('issues.id = issue_assignees.issue_id')
.where(project_id: project.id)
# DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
IssueAssignee.unscoped
.where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues)
.delete_all
project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
end
member.user.invalidate_cache_counts
end
end
end
module Members
class BaseService < ::BaseService
# current_user - The user that performs the action
# params - A hash of parameters
def initialize(current_user = nil, params = {})
@current_user = current_user
@params = params
end
def after_execute(args)
# overriden in EE::Members modules
end
private
def update_member_permission(member)
case member
when GroupMember
:update_group_member
when ProjectMember
:update_project_member
else
raise "Unknown member type: #{member}!"
end
end
def override_member_permission(member)
case member
when GroupMember
:override_group_member
when ProjectMember
:override_project_member
else
raise "Unknown member type: #{member}!"
end
end
def action_member_permission(action, member)
case action
when :update
update_member_permission(member)
when :override
override_member_permission(member)
else
raise "Unknown action '#{action}' on #{member}!"
end
end
end
end
module Members module Members
class CreateService < BaseService class CreateService < Members::BaseService
DEFAULT_LIMIT = 100 DEFAULT_LIMIT = 100
def initialize(source, current_user, params = {}) def execute(source)
@source = source
@current_user = current_user
@params = params
@error = nil
end
def execute
return error('No users specified.') if params[:user_ids].blank? return error('No users specified.') if params[:user_ids].blank?
user_ids = params[:user_ids].split(',').uniq user_ids = params[:user_ids].split(',').uniq
...@@ -17,13 +10,15 @@ module Members ...@@ -17,13 +10,15 @@ module Members
return error("Too many users specified (limit is #{user_limit})") if return error("Too many users specified (limit is #{user_limit})") if
user_limit && user_ids.size > user_limit user_limit && user_ids.size > user_limit
@source.add_users( members = source.add_users(
user_ids, user_ids,
params[:access_level], params[:access_level],
expires_at: params[:expires_at], expires_at: params[:expires_at],
current_user: current_user current_user: current_user
) )
members.each { |member| after_execute(member: member) }
success success
end end
......
module Members module Members
class DestroyService < BaseService class DestroyService < Members::BaseService
include MembersHelper def execute(member, skip_authorization: false)
raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member)
attr_accessor :source return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
ALLOWED_SCOPES = %i[members requesters all].freeze Member.transaction do
unassign_issues_and_merge_requests(member) unless member.invite?
member.notification_setting&.destroy
def initialize(source, current_user, params = {}) member.destroy
@source = source
@current_user = current_user
@params = params
end end
def execute(scope = :members) if member.request? && member.user != current_user
raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope) notification_service.decline_access_request(member)
end
member = find_member!(scope)
raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member) after_execute(member: member)
AuthorizedDestroyService.new(member, current_user).execute member
end end
private private
def find_member!(scope)
condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
case scope
when :all
source.members.find_by(condition) ||
source.requesters.find_by!(condition)
else
source.public_send(scope).find_by!(condition) # rubocop:disable GitlabSecurity/PublicSend
end
end
def can_destroy_member?(member) def can_destroy_member?(member)
member && can?(current_user, destroy_member_permission(member), member) can?(current_user, destroy_member_permission(member), member)
end end
def destroy_member_permission(member) def destroy_member_permission(member)
...@@ -45,7 +33,42 @@ module Members ...@@ -45,7 +33,42 @@ module Members
:destroy_group_member :destroy_group_member
when ProjectMember when ProjectMember
:destroy_project_member :destroy_project_member
else
raise "Unknown member type: #{member}!"
end end
end end
def unassign_issues_and_merge_requests(member)
if member.is_a?(GroupMember)
issues = Issue.unscoped.select(1)
.joins(:project)
.where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id)
# DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
IssueAssignee.unscoped
.where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues)
.delete_all
MergeRequestsFinder.new(current_user, group_id: member.source_id, assignee_id: member.user_id)
.execute
.update_all(assignee_id: nil)
else
project = member.source
# SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X
issues = Issue.unscoped.select(1)
.where('issues.id = issue_assignees.issue_id')
.where(project_id: project.id)
# DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
IssueAssignee.unscoped
.where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues)
.delete_all
project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
end
member.user.invalidate_cache_counts
end
end end
end end
module Members module Members
class RequestAccessService < BaseService class RequestAccessService < Members::BaseService
attr_accessor :source def execute(source)
def initialize(source, current_user)
@source = source
@current_user = current_user
end
def execute
raise Gitlab::Access::AccessDeniedError unless can_request_access?(source) raise Gitlab::Access::AccessDeniedError unless can_request_access?(source)
source.members.create( source.members.create(
...@@ -19,7 +12,7 @@ module Members ...@@ -19,7 +12,7 @@ module Members
private private
def can_request_access?(source) def can_request_access?(source)
source && can?(current_user, :request_access, source) can?(current_user, :request_access, source)
end end
end end
end end
module Members
class UpdateService < Members::BaseService
# returns the updated member
def execute(member, permission: :update)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, action_member_permission(permission, member), member)
old_access_level = member.human_access
if member.update_attributes(params)
after_execute(action: permission, old_access_level: old_access_level, member: member)
end
member
end
end
end
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
# - Use `validates :xxx, uniqueness: { scope: :xxx_id }` in a child model # - Use `validates :xxx, uniqueness: { scope: :xxx_id }` in a child model
class VariableDuplicatesValidator < ActiveModel::EachValidator class VariableDuplicatesValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
return if record.errors.include?(:"#{attribute}.key")
if options[:scope] if options[:scope]
scoped = value.group_by do |variable| scoped = value.group_by do |variable|
Array(options[:scope]).map { |attr| variable.send(attr) } # rubocop:disable GitlabSecurity/PublicSend Array(options[:scope]).map { |attr| variable.send(attr) } # rubocop:disable GitlabSecurity/PublicSend
......
:plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}');
$("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@group_member)}"));
- page_title "Issues" - page_title "Issues"
- group_issues_exists = group_issues(@group).exists?
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues") = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues")
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
- if group_issues_exists - if group_issues_count(state: 'all').zero?
= render 'shared/empty_states/issues', project_select_button: true
- else
.top-area .top-area
= render 'shared/issuable/nav', type: :issues = render 'shared/issuable/nav', type: :issues
.nav-controls .nav-controls
...@@ -19,5 +20,3 @@ ...@@ -19,5 +20,3 @@
= render 'shared/issuable/search_bar', type: :issues = render 'shared/issuable/search_bar', type: :issues
= render 'shared/issues' = render 'shared/issues'
- else
= render 'shared/empty_states/issues', project_select_button: true
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
- if @group_merge_requests.empty? - if group_merge_requests_count(state: 'all').zero?
= render 'shared/empty_states/merge_requests', project_select_button: true = render 'shared/empty_states/merge_requests', project_select_button: true
- else - else
.top-area .top-area
......
- issues_count = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute.count - issues_count = group_issues_count(state: 'opened')
- merge_requests_count = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute.count - merge_requests_count = group_merge_requests_count(state: 'opened')
- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index'] - issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index']
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) } .nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.radio .radio
= label_tag :project_merge_method_ff do = label_tag :project_merge_method_ff do
= form.radio_button :merge_method, :ff, class: "js-merge-method-radio" = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff"
%strong Fast-forward merge %strong Fast-forward merge
%br %br
%span.descr %span.descr
......
...@@ -11,8 +11,8 @@ ...@@ -11,8 +11,8 @@
= view_on_environment_button(@commit.sha, @path, @environment) if @environment = view_on_environment_button(@commit.sha, @path, @environment) if @environment
.btn-group{ role: "group" }< .btn-group{ role: "group" }<
= edit_blob_link = edit_blob_button
= ide_blob_link = ide_edit_button
- if current_user - if current_user
= replace_blob_link = replace_blob_link
= delete_blob_link = delete_blob_link
......
...@@ -27,6 +27,3 @@ ...@@ -27,6 +27,3 @@
- unless can?(current_user, :push_code, @project) - unless can?(current_user, :push_code, @project)
.inline.prepend-left-10 .inline.prepend-left-10
= commit_in_fork_help = commit_in_fork_help
- content_for :page_specific_javascripts do
= webpack_bundle_tag('blob')
...@@ -9,4 +9,3 @@ ...@@ -9,4 +9,3 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue') = webpack_bundle_tag('common_vue')
= webpack_bundle_tag('commit_pipelines')
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
- page_description @commit.description - page_description @commit.description
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue') = webpack_bundle_tag('common_vue')
= webpack_bundle_tag('diff_notes')
.container-fluid{ class: [limited_container_width, container_class] } .container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box" = render "commit_box"
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
.avatar-cell.hidden-xs .avatar-cell.hidden-xs
= author_avatar(commit, size: 36) = author_avatar(commit, size: 36)
.commit-detail .commit-detail.flex-list
.commit-content .commit-content
= link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title") = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title")
%span.commit-row-message.visible-xs-inline %span.commit-row-message.visible-xs-inline
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
\ \
- if editable_diff?(diff_file) - if editable_diff?(diff_file)
- link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {} - link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {}
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, = edit_blob_button(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
blob: blob, link_opts: link_opts) blob: blob, link_opts: link_opts)
- if image_diff && image_replaced - if image_diff && image_replaced
......
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
.settings-content .settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f| = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f|
= render 'merge_request_settings', form: f = render 'merge_request_settings', form: f
= f.submit 'Save changes', class: "btn btn-save" = f.submit 'Save changes', class: "btn btn-save qa-save-merge-request-changes"
= render 'export', project: @project = render 'export', project: @project
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue') = webpack_bundle_tag('common_vue')
= webpack_bundle_tag("environments_folder")
#environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json), #environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json),
"folder-name" => @folder, "folder-name" => @folder,
......
...@@ -55,7 +55,7 @@ ...@@ -55,7 +55,7 @@
.issue-details.issuable-details .issue-details.issuable-details
.detail-page-description.content-block .detail-page-description.content-block
%script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue) %script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue).to_json
#js-issuable-app #js-issuable-app
%h2.title= markdown_field(@issue, :title) %h2.title= markdown_field(@issue, :title)
- if @issue.description.present? - if @issue.description.present?
......
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.
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.
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.
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