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:
exclude_paths:
- "lib/api/v3/*"
eslint:
# eslint-plugin-vue is locked to version 2 in codeclimate, we need version 4
enabled: false
enabled: true
channel: "eslint-4"
rubocop:
enabled: true
channel: "gitlab-rubocop-0-52-1"
......
......@@ -2,6 +2,27 @@
documentation](doc/development/changelog.md) for instructions on adding your own
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)
- No changes.
......
......@@ -397,9 +397,9 @@ For issues related to the open source stewardship of GitLab,
there is the ~"stewardship" label.
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
features from GitLab CE to make exclusive in GitLab EE, related issues
would be labelled with ~"stewardship".
is a topic of discussion. For instance if GitLab Inc. is planning to add
features from GitLab EE to GitLab CE, related issues would be labelled with
~"stewardship".
A recent example of this was the issue for
[bringing the time tracking API to GitLab CE][time-tracking-issue].
......
......@@ -411,7 +411,7 @@ group :ed25519 do
end
# 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
gem 'google-protobuf', '= 3.5.1'
......
......@@ -285,7 +285,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.84.0)
gitaly-proto (0.85.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (5.3.3)
......@@ -1057,7 +1057,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.84.0)
gitaly-proto (~> 0.85.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
......
<script>
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 eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
......
/* global ListIssue */
<script>
import eventHub from '../eventhub';
import ListIssue from '../models/issue';
const Store = gl.issueBoards.BoardsStore;
......@@ -17,6 +18,9 @@ export default {
error: false,
};
},
mounted() {
this.$refs.input.focus();
},
methods: {
submit(e) {
e.preventDefault();
......@@ -59,42 +63,51 @@ export default {
eventHub.$emit(`hide-issue-form-${this.list.id}`);
},
},
mounted() {
this.$refs.input.focus();
},
template: `
};
</script>
<template>
<div class="card board-new-issue-form">
<form @submit="submit($event)">
<div class="flash-container"
v-if="error">
<div
class="flash-container"
v-if="error"
>
<div class="flash-alert">
An error occurred. Please try again.
</div>
</div>
<label class="label-light"
:for="list.id + '-title'">
<label
class="label-light"
:for="list.id + '-title'"
>
Title
</label>
<input class="form-control"
<input
class="form-control"
type="text"
v-model="title"
ref="input"
autocomplete="off"
:id="list.id + '-title'" />
:id="list.id + '-title'"
/>
<div class="clearfix prepend-top-10">
<button class="btn btn-success pull-left"
<button
class="btn btn-success pull-left"
type="submit"
:disabled="title === ''"
ref="submit-button">
ref="submit-button"
>
Submit issue
</button>
<button class="btn btn-default pull-right"
<button
class="btn btn-default pull-right"
type="button"
@click="cancel">
@click="cancel"
>
Cancel
</button>
</div>
</form>
</div>
`,
};
</template>
......@@ -2,11 +2,13 @@
import _ from 'underscore';
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 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/label';
import './models/list';
......@@ -22,9 +24,9 @@ import './components/board';
import './components/board_sidebar';
import './components/new_list_dropdown';
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 Store = gl.issueBoards.BoardsStore;
const ModalStore = gl.issueBoards.ModalStore;
......@@ -236,4 +238,4 @@ $(() => {
</div>
`,
});
});
};
......@@ -110,3 +110,5 @@ class ListIssue {
}
window.ListIssue = ListIssue;
export default ListIssue;
......@@ -2,7 +2,7 @@
/* global List */
import _ from 'underscore';
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.issueBoards = window.gl.issueBoards || {};
......
......@@ -15,7 +15,7 @@ const CommitPipelinesTable = Vue.extend(commitPipelinesTable);
window.gl = window.gl || {};
window.gl.CommitPipelinesTable = CommitPipelinesTable;
document.addEventListener('DOMContentLoaded', () => {
export default () => {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
if (pipelineTableViewEl) {
......@@ -43,4 +43,4 @@ document.addEventListener('DOMContentLoaded', () => {
pipelineTableViewEl.appendChild(table.$el);
}
}
});
};
......@@ -6,43 +6,21 @@ import GlFieldErrors from './gl_field_errors';
import Shortcuts from './shortcuts';
import SearchAutocomplete from './search_autocomplete';
var Dispatcher;
(function() {
Dispatcher = (function() {
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;
function initSearch() {
// Only when search form is present
if ($('.search').length) {
return new SearchAutocomplete();
}
}
const fail = () => Flash('Error loading dynamic module');
const callDefault = m => m.default();
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,
});
function initFieldErrors() {
$('.gl-show-field-errors').each((i, form) => {
new GlFieldErrors(form);
});
}
const shortcutHandlerPages = [
function initPageShortcuts(page) {
const pagesWithCustomShortcuts = [
'projects:activity',
'projects:artifacts:browse',
'projects:artifacts:file',
......@@ -66,117 +44,42 @@ var Dispatcher;
'groups:show',
];
if (shortcutHandlerPages.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) {
if (pagesWithCustomShortcuts.indexOf(page) === -1) {
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')) {
import('./performance_bar')
.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() {
return new Dispatcher();
}
const page = $('body').attr('data-page');
if (page) {
initPageShortcuts(page);
initGFMInput();
initPerformanceBar();
}
};
......@@ -2,8 +2,8 @@
/**
* Render environments table.
*/
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import environmentItem from './environment_item.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {
......
......@@ -5,7 +5,7 @@ import Translate from '../../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({
export default () => new Vue({
el: '#environments-folder-list-view',
components: {
environmentsFolderApp,
......@@ -32,4 +32,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
}));
});
<script>
import eventHub from '../event_hub';
import FilteredSearchTokenizer from '../filtered_search_tokenizer';
export default {
name: 'RecentSearchesDropdownContent',
props: {
items: {
type: Array,
......@@ -19,7 +19,6 @@ export default {
required: true,
},
},
computed: {
processedItems() {
return this.items.map((item) => {
......@@ -42,7 +41,6 @@ export default {
return this.items.length > 0;
},
},
methods: {
onItemActivated(text) {
eventHub.$emit('recentSearchesItemSelected', text);
......@@ -54,8 +52,9 @@ export default {
eventHub.$emit('requestClearRecentSearches');
},
},
template: `
};
</script>
<template>
<div>
<div
v-if="!isLocalStorageAvailable"
......@@ -65,16 +64,20 @@ export default {
<ul v-else-if="hasItems">
<li
v-for="(item, index) in processedItems"
:key="index">
:key="`processed-items-${index}`"
>
<button
type="button"
class="filtered-search-history-dropdown-item"
@click="onItemActivated(item.text)">
<span>
<span
v-for="(token, tokenIndex) in item.tokens"
class="filtered-search-history-dropdown-token">
<span class="name">{{ token.prefix }}</span><span class="value">{{ token.suffix }}</span>
class="filtered-search-history-dropdown-token"
v-for="(token, index) in item.tokens"
:key="`dropdown-token-${index}`"
>
<span class="name">{{ token.prefix }}</span>
<span class="value">{{ token.suffix }}</span>
</span>
</span>
<span class="filtered-search-history-dropdown-search-token">
......@@ -98,5 +101,4 @@ export default {
You don't have any recent searches
</div>
</div>
`,
};
</template>
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';
class RecentSearchesRoot {
......@@ -33,7 +33,7 @@ class RecentSearchesRoot {
this.vm = new Vue({
el: this.wrapperElement,
components: {
'recent-searches-dropdown-content': RecentSearchesDropdownContent,
RecentSearchesDropdownContent,
},
data() { return state; },
template: `
......
<script>
import { mapState } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
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 fileIcon from '../../vue_shared/components/file_icon.vue';
export default {
components: {
......
<script>
import { mapActions } from 'vuex';
import fileIcon from '../../vue_shared/components/file_icon.vue';
import fileIcon from '~/vue_shared/components/file_icon.vue';
export default {
components: {
......
......@@ -213,7 +213,7 @@ export default class LabelsSelect {
}
}
if (label.duplicate) {
color = gl.DropdownUtils.duplicateLabelColor(label.color);
color = DropdownUtils.duplicateLabelColor(label.color);
}
else {
if (label.color != null) {
......@@ -242,10 +242,16 @@ export default class LabelsSelect {
filterable: true,
selected: $dropdown.data('selected') || [],
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 title = selected.title;
var selectedLabels = this.selected;
if ($dropdownInputField.length && $dropdownInputField.val().length) {
$dropdownParent.find('.dropdown-input-clear').trigger('click');
}
if (selected.id === 0) {
this.selected = [];
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';
export default () => new AbuseReports();
document.addEventListener('DOMContentLoaded', () => new AbuseReports());
......@@ -3,7 +3,7 @@ import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
export default function initBroadcastMessagesForm() {
export default () => {
$('input#broadcast_message_color').on('input', function onMessageColorInput() {
const previewColor = $(this).val();
$('div.broadcast-message-preview').css('background-color', previewColor);
......@@ -32,4 +32,4 @@ export default function initBroadcastMessagesForm() {
.catch(() => flash(__('An error occurred while rendering preview broadcast message')));
}
}, 250));
}
};
import initBroadcastMessagesForm from './broadcast_message';
export default () => initBroadcastMessagesForm();
document.addEventListener('DOMContentLoaded', initBroadcastMessagesForm);
import initUsagePing from './usage_ping';
export default () => initUsagePing();
document.addEventListener('DOMContentLoaded', initUsagePing);
import UsersSelect from '../../../../users_select';
export default () => new UsersSelect();
document.addEventListener('DOMContentLoaded', () => new UsersSelect());
import Labels from '../../../../labels';
export default () => new Labels();
document.addEventListener('DOMContentLoaded', () => new Labels());
import Labels from '../../../../labels';
export default () => new Labels();
document.addEventListener('DOMContentLoaded', () => new Labels());
import ProjectsList from '../../../projects_list';
import NamespaceSelect from '../../../namespace_select';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new ProjectsList(); // eslint-disable-line no-new
document.querySelectorAll('.js-namespace-select')
.forEach(dropdown => new NamespaceSelect({ dropdown }));
};
});
import NotificationsForm from '../../../notifications_form';
import notificationsDropdown from '../../../notifications_dropdown';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new NotificationsForm(); // eslint-disable-line no-new
notificationsDropdown();
};
});
import UsersSelect from '~/users_select';
import ShortcutsNavigation from '~/shortcuts_navigation';
import initBoards from '~/boards';
document.addEventListener('DOMContentLoaded', () => {
new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
initBoards();
});
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
document.addEventListener('DOMContentLoaded', () => {
new MiniPipelineGraph({
container: '.js-commit-pipeline-graph',
}).bindEvents();
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
initPipelines();
});
......@@ -5,6 +5,7 @@ import ShortcutsNavigation from '~/shortcuts_navigation';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import initNotes from '~/init_notes';
import initChangesDropdown from '~/init_changes_dropdown';
import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import { fetchCommitMergeRequests } from '~/commit_merge_requests';
document.addEventListener('DOMContentLoaded', () => {
......@@ -19,4 +20,5 @@ document.addEventListener('DOMContentLoaded', () => {
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop);
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
fetchCommitMergeRequests();
initDiffNotes();
});
import initCompareAutocomplete from '~/compare_autocomplete';
export default () => {
initCompareAutocomplete();
};
document.addEventListener('DOMContentLoaded', initCompareAutocomplete);
import initEnvironmentsFolderBundle from '~/environments/folder/environments_folder_bundle';
document.addEventListener('DOMContentLoaded', initEnvironmentsFolderBundle);
import Project from './project';
import ShortcutsNavigation from '../../shortcuts_navigation';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
};
});
import initIssuableSidebar from '~/init_issuable_sidebar';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import Issue from '~/issue';
import ShortcutsIssuable from '~/shortcuts_issuable';
import ZenMode from '~/zen_mode';
......@@ -10,4 +11,5 @@ document.addEventListener('DOMContentLoaded', () => {
new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
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 MergeRequest from '~/merge_request';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
document.addEventListener('DOMContentLoaded', () => {
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
......@@ -14,5 +15,6 @@ document.addEventListener('DOMContentLoaded', () => {
new MergeRequest({ // eslint-disable-line no-new
action: mrNewSubmitNode.dataset.mrSubmitAction,
});
initPipelines();
}
});
......@@ -3,18 +3,23 @@ import ZenMode from '~/zen_mode';
import initNotes from '~/init_notes';
import initIssuableSidebar from '~/init_issuable_sidebar';
import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import ShortcutsIssuable from '~/shortcuts_issuable';
import Diff from '~/diff';
import { handleLocationHash } from '~/lib/utils/common_utils';
import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initWidget from '../../../../vue_merge_request_widget';
document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar();
initSidebarBundle();
initNotes();
initDiffNotes();
initPipelines();
const mrShowNode = document.querySelector('.merge-request');
......@@ -25,4 +30,5 @@ document.addEventListener('DOMContentLoaded', () => {
new ShortcutsIssuable(true); // eslint-disable-line no-new
handleLocationHash();
howToMerge();
initWidget();
});
/* 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() {
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';
import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new ProjectNew(); // eslint-disable-line no-new
initProjectVisibilitySelector();
initProjectNew.bindEvents();
};
});
import Vue from 'vue';
import PipelinesStore from './stores/pipelines_store';
import pipelinesComponent from './components/pipelines.vue';
import Translate from '../vue_shared/translate';
import PipelinesStore from '../../../../pipelines/stores/pipelines_store';
import pipelinesComponent from '../../../../pipelines/components/pipelines.vue';
import Translate from '../../../../vue_shared/translate';
Vue.use(Translate);
......
......@@ -3,9 +3,9 @@ import ShortcutsWiki from '../../../shortcuts_wiki';
import ZenMode from '../../../zen_mode';
import GLForm from '../../../gl_form';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.wiki-form'), true); // eslint-disable-line no-new
};
});
/* eslint-disable class-methods-use-this */
import Vue from 'vue';
import VueResource from 'vue-resource';
import '../../vue_shared/vue_resource_interceptor';
Vue.use(VueResource);
......
......@@ -9,13 +9,12 @@ export default class ShortcutsIssuable extends Shortcuts {
super();
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('m', () => ShortcutsIssuable.openSidebarDropdown('milestone'));
Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels'));
Mousetrap.bind('r', this.replyWithSelectedText.bind(this));
Mousetrap.bind('e', this.editIssue.bind(this));
Mousetrap.bind('e', ShortcutsIssuable.editIssue);
if (isMergeRequest) {
this.enabledHelp.push('.hidden-shortcut.merge_requests');
......@@ -58,10 +57,10 @@ export default class ShortcutsIssuable extends Shortcuts {
return false;
}
editIssue() {
static editIssue() {
// Need to click the element as on issues, editing is inline
// on merge request, editing is on a different page
this.editBtn.click();
document.querySelector('.js-issuable-edit').click();
return false;
}
......
import Mediator from './sidebar_mediator';
import { mountSidebar, getSidebarOptions } from './mount_sidebar';
function domContentLoaded() {
export default () => {
const mediator = new Mediator(getSidebarOptions());
mediator.fetch();
mountSidebar(mediator);
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
export default domContentLoaded;
};
<script>
/* eslint-disable vue/require-default-prop */
import pipelineStage from '../../pipelines/components/stage.vue';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
import icon from '../../vue_shared/components/icon.vue';
import pipelineStage from '~/pipelines/components/stage.vue';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
import icon from '~/vue_shared/components/icon.vue';
export default {
name: 'MRWidgetPipeline',
......
......@@ -227,7 +227,8 @@ export default {
@click="handleMergeButtonClick()"
:disabled="isMergeButtonDisabled"
:class="mergeButtonClass"
type="button">
type="button"
class="qa-merge-button">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
......
......@@ -111,7 +111,7 @@ js-toggle-container accept-action media space-children"
>
<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"
@click="rebase"
>
......
......@@ -6,7 +6,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
export default () => {
gl.mrWidgetData.gitlabLogo = gon.gitlab_logo;
const vm = new Vue(mrWidgetOptions);
......@@ -14,4 +14,4 @@ document.addEventListener('DOMContentLoaded', () => {
window.gl.mrWidget = {
checkStatus: vm.checkStatus,
};
});
};
......@@ -196,17 +196,9 @@
@media (min-width: $screen-sm-min) {
font-size: 0;
div {
display: inline;
}
.fa-spinner {
font-size: 12px;
}
span {
font-size: 6px;
}
}
.ci-status-link {
......
......@@ -13,6 +13,16 @@
display: inline-block;
}
.issuable-meta {
.author_link {
display: inline-block;
}
.issuable-comments {
height: 18px;
}
}
.icon-merge-request-unmerged {
height: 13px;
margin-bottom: 3px;
......
......@@ -48,7 +48,7 @@ class Admin::GroupsController < Admin::ApplicationController
def members_update
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
redirect_to [:admin, @group], notice: 'Users were successfully added.'
......
......@@ -3,20 +3,31 @@ module MembershipActions
def create
create_params = params.permit(:user_ids, :access_level, :expires_at)
result = Members::CreateService.new(membershipable, current_user, create_params).execute
redirect_url = members_page_url
result = Members::CreateService.new(current_user, create_params).execute(membershipable)
if result[:status] == :success
redirect_to redirect_url, notice: 'Users were successfully added.'
redirect_to members_page_url, notice: 'Users were successfully added.'
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
def destroy
Members::DestroyService.new(membershipable, current_user, params)
.execute(:all)
member = membershipable.members_and_requesters.find(params[:id])
Members::DestroyService.new(current_user).execute(member)
respond_to do |format|
format.html do
......@@ -36,14 +47,17 @@ module MembershipActions
end
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
end
def leave
member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id)
.execute(:all)
member = membershipable.members_and_requesters.find_by!(user_id: current_user.id)
Members::DestroyService.new(current_user).execute(member)
notice =
if member.request?
......@@ -62,17 +76,43 @@ module MembershipActions
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
def membershipable
raise NotImplementedError
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
if membershipable.is_a?(Project)
case membershipable
when Namespace
polymorphic_url([membershipable, :members])
when Project
project_project_members_path(membershipable)
else
polymorphic_url([membershipable, :members])
raise "Unknown membershipable type: #{membershipable}!"
end
end
......
......@@ -18,10 +18,6 @@ class Groups::ApplicationController < ApplicationController
@projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute
end
def group_merge_requests
@group_merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id).execute
end
def authorize_admin_group!
unless can?(current_user, :admin_group, group)
return render_404
......
......@@ -27,35 +27,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
@group_member = @group.group_members.new
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
alias_method :membershipable, :group
end
......@@ -14,7 +14,6 @@ class GroupsController < Groups::ApplicationController
before_action :authorize_create_group!, only: [:new]
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 :user_actions, only: [:show, :subgroups]
......
......@@ -26,29 +26,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_member = @project.project_members.new
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
@projects = current_user.authorized_projects.order_id_desc
end
......@@ -67,12 +44,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
notice: notice)
end
protected
def member_params
params.require(:project_member).permit(:user_id, :access_level, :expires_at)
end
# MembershipActions concern
alias_method :membershipable, :project
end
......@@ -39,7 +39,7 @@ class LabelsFinder < UnionFinder
end
end
elsif only_group_labels?
label_ids << Label.where(group_id: group.id)
label_ids << Label.where(group_id: group_ids)
else
label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id))
......@@ -59,10 +59,11 @@ class LabelsFinder < UnionFinder
items.where(title: title)
end
def group
strong_memoize(:group) do
def group_ids
strong_memoize(:group_ids) do
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
......@@ -120,4 +121,10 @@ class LabelsFinder < UnionFinder
Ability.allowed?(current_user, :read_label, label_parent)
end
def groups_user_can_read_labels(groups)
DeclarativePolicy.user_scope do
groups.select { |group| authorized_to_read_labels?(group) }
end
end
end
......@@ -16,71 +16,38 @@ module BlobHelper
options[:link_opts])
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 = {})
"#{ide_path}/project#{edit_blob_path(project, ref, path, options)}"
end
def ide_edit_text
"#{_('Web IDE')}"
end
def edit_blob_button(project = @project, ref = @ref, path = @path, options = {})
return unless blob = readable_blob(options, path, project, ref)
def ide_blob_link(project = @project, ref = @ref, path = @path, options = {})
return unless show_new_ide?
common_classes = "btn js-edit-blob #{options[:extra_class]}"
blob = options.delete(:blob)
blob ||= project.repository.blob_at(ref, path) rescue nil
edit_button_tag(blob,
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]}"
if !on_top_of_branch?(project, ref)
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' }
# This condition applies to anonymous or users who can edit directly
elsif current_user && can_modify_blob?(blob, project, ref)
link_to ide_edit_text, ide_edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
elsif current_user && can?(current_user, :fork_project, project)
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
edit_button_tag(blob,
common_classes,
_('Web IDE'),
ide_edit_path(project, ref, path, options),
project,
ref)
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
blob = project.repository.blob_at(ref, path) rescue nil
......@@ -96,21 +63,12 @@ module BlobHelper
elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
elsif can?(current_user, :fork_project, project)
continue_params = {
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 }
edit_fork_button_tag(common_classes, project, label, edit_modify_file_fork_params(action), action)
end
end
def replace_blob_link(project = @project, ref = @ref, path = @path)
modify_file_link(
modify_file_button(
project,
ref,
path,
......@@ -122,7 +80,7 @@ module BlobHelper
end
def delete_blob_link(project = @project, ref = @ref, path = @path)
modify_file_link(
modify_file_button(
project,
ref,
path,
......@@ -332,4 +290,55 @@ module BlobHelper
options
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
......@@ -19,6 +19,20 @@ module GroupsHelper
can?(current_user, :change_share_with_group_lock, group)
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 = {})
img_path = group_icon_url(group, options)
image_tag img_path, options
......@@ -77,10 +91,6 @@ module GroupsHelper
end
end
def group_issues(group)
IssuesFinder.new(current_user, group_id: group.id).execute
end
def remove_group_message(group)
_("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
{ group_name: group.name }
......
......@@ -234,7 +234,7 @@ module IssuablesHelper
data.merge!(updated_at_by(issuable))
data.to_json
data
end
def updated_at_by(issuable)
......
......@@ -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."
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
"A new branch will be created in your fork and a new merge request will be started."
end
......
class ChatName < ActiveRecord::Base
LAST_USED_AT_INTERVAL = 1.hour
belongs_to :service
belongs_to :user
......@@ -9,4 +11,23 @@ class ChatName < ActiveRecord::Base
validates :user_id, uniqueness: { scope: [:service_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
......@@ -6,7 +6,10 @@ module Ci
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) }
end
......
......@@ -6,7 +6,10 @@ module Ci
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) }
end
......
......@@ -8,6 +8,6 @@ module AccessRequestable
extend ActiveSupport::Concern
def request_access(user)
Members::RequestAccessService.new(self, user).execute
Members::RequestAccessService.new(user).execute(self)
end
end
......@@ -128,7 +128,7 @@ class Member < ActiveRecord::Base
find_by(invite_token: invite_token)
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
member = retrieve_member(source, user, existing_members)
access_level = retrieve_access_level(access_level)
......@@ -143,11 +143,13 @@ class Member < ActiveRecord::Base
if member.request?
::Members::ApproveAccessRequestService.new(
source,
current_user,
id: member.id,
access_level: access_level
).execute
).execute(
member,
skip_authorization: ldap,
skip_log_audit_event: ldap
)
else
member.save
end
......
......@@ -30,10 +30,10 @@ class SlashCommandsService < Service
def trigger(params)
return unless valid_token?(params[:token])
user = find_chat_user(params)
chat_user = find_chat_user(params)
if user
Gitlab::SlashCommands::Command.new(project, user, params).execute
if chat_user&.user
Gitlab::SlashCommands::Command.new(project, chat_user, params).execute
else
url = authorize_chat_name_url(params)
Gitlab::SlashCommands::Presenters::Access.new(url).authorize
......
......@@ -9,10 +9,9 @@ class Tree
@repository = repository
@sha = sha
@path = path
@recursive = recursive
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
def readme
......@@ -58,21 +57,4 @@ class Tree
def sorted_entries
trees + blobs + submodules
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
......@@ -431,7 +431,7 @@ class User < ActiveRecord::Base
end
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
#
......
......@@ -9,8 +9,8 @@ module ChatNames
chat_name = find_chat_name
return unless chat_name
chat_name.touch(:last_used_at)
chat_name.user
chat_name.update_last_used_at
chat_name
end
private
......
......@@ -4,13 +4,33 @@ module Ci
return if job.job_artifacts_trace
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!(
project: job.project,
file_type: :trace,
file: stream)
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
......@@ -77,8 +77,12 @@ class IssuableBaseService < BaseService
return unless labels
params[:label_ids] = labels.split(",").map do |label_name|
service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip)
label = service.execute
label = Labels::FindOrCreateService.new(
current_user,
parent,
title: label_name.strip,
available_labels: available_labels
).execute
label.try(:id)
end.compact
......@@ -102,7 +106,7 @@ class IssuableBaseService < BaseService
end
def available_labels
LabelsFinder.new(current_user, project_id: @project.id).execute
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end
def merge_quick_actions_into_params!(issuable)
......@@ -303,4 +307,8 @@ class IssuableBaseService < BaseService
def update_project_counter_caches?(issuable)
issuable.state_changed?
end
def parent
project
end
end
module Labels
class FindOrCreateService
def initialize(current_user, project, params = {})
def initialize(current_user, parent, params = {})
@current_user = current_user
@project = project
@parent = parent
@available_labels = params.delete(:available_labels)
@params = params.dup.with_indifferent_access
end
......@@ -13,12 +14,13 @@ module Labels
private
attr_reader :current_user, :project, :params, :skip_authorization
attr_reader :current_user, :parent, :params, :skip_authorization
def available_labels
@available_labels ||= LabelsFinder.new(
current_user,
project_id: project.id
"#{parent_type}_id".to_sym => parent.id,
only_group_labels: parent_is_group?
).execute(skip_authorization: skip_authorization)
end
......@@ -27,8 +29,8 @@ module Labels
def find_or_create_label
new_label = available_labels.find_by(title: title)
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project))
new_label = Labels::CreateService.new(params).execute(project: project)
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent))
new_label = Labels::CreateService.new(params).execute(parent_type.to_sym => parent)
end
new_label
......@@ -37,5 +39,13 @@ module Labels
def title
params[:title] || params[:name]
end
def parent_type
parent.model_name.param_key
end
def parent_is_group?
parent_type == "group"
end
end
end
module Members
class ApproveAccessRequestService < BaseService
include MembersHelper
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)
class ApproveAccessRequestService < Members::BaseService
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)
access_requester.access_level = params[:access_level] if params[:access_level]
access_requester.accept_request
after_execute(member: access_requester, skip_log_audit_event: skip_log_audit_event)
access_requester
end
private
def can_update_access_requester?(access_requester, opts = {})
access_requester && (
opts[:force] ||
def can_update_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
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
class CreateService < BaseService
class CreateService < Members::BaseService
DEFAULT_LIMIT = 100
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user
@params = params
@error = nil
end
def execute
def execute(source)
return error('No users specified.') if params[:user_ids].blank?
user_ids = params[:user_ids].split(',').uniq
......@@ -17,13 +10,15 @@ module Members
return error("Too many users specified (limit is #{user_limit})") if
user_limit && user_ids.size > user_limit
@source.add_users(
members = source.add_users(
user_ids,
params[:access_level],
expires_at: params[:expires_at],
current_user: current_user
)
members.each { |member| after_execute(member: member) }
success
end
......
module Members
class DestroyService < BaseService
include MembersHelper
class DestroyService < Members::BaseService
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 = {})
@source = source
@current_user = current_user
@params = params
member.destroy
end
def execute(scope = :members)
raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope)
member = find_member!(scope)
if member.request? && member.user != current_user
notification_service.decline_access_request(member)
end
raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member)
after_execute(member: member)
AuthorizedDestroyService.new(member, current_user).execute
member
end
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)
member && can?(current_user, destroy_member_permission(member), member)
can?(current_user, destroy_member_permission(member), member)
end
def destroy_member_permission(member)
......@@ -45,7 +33,42 @@ module Members
:destroy_group_member
when ProjectMember
:destroy_project_member
else
raise "Unknown member type: #{member}!"
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
module Members
class RequestAccessService < BaseService
attr_accessor :source
def initialize(source, current_user)
@source = source
@current_user = current_user
end
def execute
class RequestAccessService < Members::BaseService
def execute(source)
raise Gitlab::Access::AccessDeniedError unless can_request_access?(source)
source.members.create(
......@@ -19,7 +12,7 @@ module Members
private
def can_request_access?(source)
source && can?(current_user, :request_access, source)
can?(current_user, :request_access, source)
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 @@
# - Use `validates :xxx, uniqueness: { scope: :xxx_id }` in a child model
class VariableDuplicatesValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if record.errors.include?(:"#{attribute}.key")
if options[:scope]
scoped = value.group_by do |variable|
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"
- group_issues_exists = group_issues(@group).exists?
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues")
- content_for :page_specific_javascripts do
= 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
= render 'shared/issuable/nav', type: :issues
.nav-controls
......@@ -19,5 +20,3 @@
= render 'shared/issuable/search_bar', type: :issues
= render 'shared/issues'
- else
= render 'shared/empty_states/issues', project_select_button: true
......@@ -3,7 +3,7 @@
- content_for :page_specific_javascripts do
= 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
- else
.top-area
......
- issues_count = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute.count
- merge_requests_count = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute.count
- issues_count = group_issues_count(state: 'opened')
- merge_requests_count = group_merge_requests_count(state: 'opened')
- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index']
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
......
......@@ -3,7 +3,7 @@
.radio
= 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
%br
%span.descr
......
......@@ -11,8 +11,8 @@
= view_on_environment_button(@commit.sha, @path, @environment) if @environment
.btn-group{ role: "group" }<
= edit_blob_link
= ide_blob_link
= edit_blob_button
= ide_edit_button
- if current_user
= replace_blob_link
= delete_blob_link
......
......@@ -27,6 +27,3 @@
- unless can?(current_user, :push_code, @project)
.inline.prepend-left-10
= commit_in_fork_help
- content_for :page_specific_javascripts do
= webpack_bundle_tag('blob')
......@@ -9,4 +9,3 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('commit_pipelines')
......@@ -8,7 +8,6 @@
- page_description @commit.description
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('diff_notes')
.container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box"
......
......@@ -20,7 +20,7 @@
.avatar-cell.hidden-xs
= author_avatar(commit, size: 36)
.commit-detail
.commit-detail.flex-list
.commit-content
= link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title")
%span.commit-row-message.visible-xs-inline
......
......@@ -17,7 +17,7 @@
\
- if editable_diff?(diff_file)
- 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)
- if image_diff && image_replaced
......
......@@ -85,7 +85,7 @@
.settings-content
= 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
= 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
......
......@@ -3,7 +3,6 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag("environments_folder")
#environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json),
"folder-name" => @folder,
......
......@@ -55,7 +55,7 @@
.issue-details.issuable-details
.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
%h2.title= markdown_field(@issue, :title)
- 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