Commit cfe4c4d9 authored by Constance Okoghenun's avatar Constance Okoghenun

Resolved conflicts in dispatcher.js

parents e7c8f8fb 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"
......
...@@ -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].
......
...@@ -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,7 +24,7 @@ import './components/board'; ...@@ -22,7 +24,7 @@ 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 () => { export default () => {
const $boardApp = document.getElementById('board-app'); const $boardApp = document.getElementById('board-app');
......
...@@ -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,112 +44,42 @@ var Dispatcher; ...@@ -66,112 +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 '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> <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: {
......
...@@ -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 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);
......
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',
......
...@@ -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 {
......
...@@ -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
...@@ -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 }
......
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
......
...@@ -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
......
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?) }
......
...@@ -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
......
...@@ -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,
......
...@@ -23,9 +23,6 @@ ...@@ -23,9 +23,6 @@
#js-vue-mr-widget.mr-widget #js-vue-mr-widget.mr-widget
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'vue_merge_request_widget'
.content-block.content-block-small.emoji-list-container .content-block.content-block-small.emoji-list-container
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true = render 'award_emoji/awards_block', awardable: @merge_request, inline: true
......
- breadcrumb_title "Graph" - breadcrumb_title "Graph"
- page_title "Graph", @ref - page_title "Graph", @ref
- content_for :page_specific_javascripts do
= webpack_bundle_tag('network')
= render "head" = render "head"
%div{ class: container_class } %div{ class: container_class }
.project-network .project-network
......
...@@ -12,6 +12,3 @@ ...@@ -12,6 +12,3 @@
"has-ci" => @repository.gitlab_ci_yml, "has-ci" => @repository.gitlab_ci_yml,
"ci-lint-path" => ci_lint_path, "ci-lint-path" => ci_lint_path,
"reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } } "reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } }
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('pipelines')
:plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}');
$("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@project_member)}"));
%h4
= s_('PrometheusService|Auto configuration')
- if service.manual_configuration?
.well
= s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below')
- else
.container-fluid
.row
- if service.prometheus_installed?
.col-sm-2
.svg-container
= image_tag 'illustrations/monitoring/getting_started.svg'
.col-sm-10
%p.text-success.prepend-top-default
= s_('PrometheusService|Prometheus is being automatically managed on your clusters')
= link_to s_('PrometheusService|Manage clusters'), project_clusters_path(project), class: 'btn'
- else
.col-sm-2
= image_tag 'illustrations/monitoring/loading.svg'
.col-sm-10
%p.prepend-top-default
= s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments')
= link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(project), class: 'btn btn-success'
%hr
%h4 - if @project
= s_('PrometheusService|Auto configuration') = render 'projects/services/prometheus/configuration_banner', project: @project, service: @service
- if @service.manual_configuration?
.well
= s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below')
- else
.container-fluid
.row
- if @service.prometheus_installed?
.col-sm-2
.svg-container
= image_tag 'illustrations/monitoring/getting_started.svg'
.col-sm-10
%p.text-success.prepend-top-default
= s_('PrometheusService|Prometheus is being automatically managed on your clusters')
= link_to s_('PrometheusService|Manage clusters'), project_clusters_path(@project), class: 'btn'
- else
.col-sm-2
= image_tag 'illustrations/monitoring/loading.svg'
.col-sm-10
%p.prepend-top-default
= s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments')
= link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(@project), class: 'btn btn-success'
%hr
%h4.append-bottom-default %h4.append-bottom-default
= s_('PrometheusService|Manual configuration') = s_('PrometheusService|Manual configuration')
......
- todo = issuable_todo(issuable) - todo = issuable_todo(issuable)
- 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('sidebar')
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } %aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } } .issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } }
......
- member = local_assigns.fetch(:member)
:plain
var $listItem = $('#{escape_javascript(render('shared/members/member', member: member))}');
$("##{dom_id(member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(member)}"));
...@@ -5,7 +5,7 @@ class RemoveExpiredMembersWorker ...@@ -5,7 +5,7 @@ class RemoveExpiredMembersWorker
def perform def perform
Member.expired.find_each do |member| Member.expired.find_each do |member|
begin begin
Members::AuthorizedDestroyService.new(member).execute Members::DestroyService.new.execute(member, skip_authorization: true)
rescue => ex rescue => ex
logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}") logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
end end
......
---
title: Clear the Labels dropdown search filter after a selection is made
merge_request: 17393
author: Andrew Torres
type: changed
---
title: Keep link when redacting unauthorized object links
merge_request:
author:
type: fixed
---
title: Enables eslint in codeclimate job
merge_request: 17392
author:
type: other
---
title: Allow Prometheus application to be installed from Cluster applications
merge_request: 17372
author:
type: fixed
---
title: Remove duplicated error message on duplicate variable validation
merge_request: 17135
author:
type: fixed
---
title: Fixes gpg popover layout
merge_request: 17323
author:
type: fixed
---
title: Ensure group issues and merge requests pages show results from subgroups when
there are no results from the current group
merge_request: 17312
author:
type: fixed
---
title: Fixes Prometheus admin configuration page
merge_request: 17377
author:
type: fixed
---
title: Add catch-up background migration to migrate pipeline stages
merge_request: 15741
author:
type: performance
...@@ -21,63 +21,49 @@ var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false'; ...@@ -21,63 +21,49 @@ var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
var WEBPACK_REPORT = process.env.WEBPACK_REPORT; var WEBPACK_REPORT = process.env.WEBPACK_REPORT;
var NO_COMPRESSION = process.env.NO_COMPRESSION; var NO_COMPRESSION = process.env.NO_COMPRESSION;
// generate automatic entry points var autoEntriesCount = 0;
var autoEntries = {}; var watchAutoEntries = [];
var pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') });
function generateEntries() {
// filter out entries currently imported dynamically in dispatcher.js // generate automatic entry points
var dispatcher = fs.readFileSync(path.join(ROOT_PATH, 'app/assets/javascripts/dispatcher.js')).toString(); var autoEntries = {};
var dispatcherChunks = dispatcher.match(/(?!import\(')\.\/pages\/[^']+/g); var pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') });
watchAutoEntries = [
function generateAutoEntries(path, prefix = '.') { path.join(ROOT_PATH, 'app/assets/javascripts/pages/'),
];
function generateAutoEntries(path, prefix = '.') {
const chunkPath = path.replace(/\/index\.js$/, ''); const chunkPath = path.replace(/\/index\.js$/, '');
if (!dispatcherChunks.includes(`${prefix}/${chunkPath}`)) {
const chunkName = chunkPath.replace(/\//g, '.'); const chunkName = chunkPath.replace(/\//g, '.');
autoEntries[chunkName] = `${prefix}/${path}`; autoEntries[chunkName] = `${prefix}/${path}`;
} }
}
pageEntries.forEach(( path ) => generateAutoEntries(path)); pageEntries.forEach(( path ) => generateAutoEntries(path));
// report our auto-generated bundle count autoEntriesCount = Object.keys(autoEntries).length;
var autoEntriesCount = Object.keys(autoEntries).length;
console.log(`${autoEntriesCount} entries from '/pages' automatically added to webpack output.`);
var config = { const manualEntries = {
// because sqljs requires fs.
node: {
fs: "empty"
},
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: {
balsamiq_viewer: './blob/balsamiq_viewer.js', balsamiq_viewer: './blob/balsamiq_viewer.js',
common: './commons/index.js', common: './commons/index.js',
common_vue: './vue_shared/vue_resource_interceptor.js', common_vue: './vue_shared/vue_resource_interceptor.js',
cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js', cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
commit_pipelines: './commit/pipelines/pipelines_bundle.js',
diff_notes: './diff_notes/diff_notes_bundle.js',
environments: './environments/environments_bundle.js', environments: './environments/environments_bundle.js',
environments_folder: './environments/folder/environments_folder_bundle.js',
filtered_search: './filtered_search/filtered_search_bundle.js', filtered_search: './filtered_search/filtered_search_bundle.js',
help: './help/help.js', help: './help/help.js',
merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js', merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
monitoring: './monitoring/monitoring_bundle.js', monitoring: './monitoring/monitoring_bundle.js',
network: './network/network_bundle.js',
notebook_viewer: './blob/notebook_viewer.js', notebook_viewer: './blob/notebook_viewer.js',
pdf_viewer: './blob/pdf_viewer.js', pdf_viewer: './blob/pdf_viewer.js',
pipelines: './pipelines/pipelines_bundle.js',
pipelines_details: './pipelines/pipeline_details_bundle.js', pipelines_details: './pipelines/pipeline_details_bundle.js',
project_import_gl: './projects/project_import_gitlab_project.js', project_import_gl: './projects/project_import_gitlab_project.js',
protected_branches: './protected_branches', protected_branches: './protected_branches',
protected_tags: './protected_tags', protected_tags: './protected_tags',
registry_list: './registry/index.js', registry_list: './registry/index.js',
sidebar: './sidebar/sidebar_bundle.js',
snippet: './snippet/snippet_bundle.js', snippet: './snippet/snippet_bundle.js',
sketch_viewer: './blob/sketch_viewer.js', sketch_viewer: './blob/sketch_viewer.js',
stl_viewer: './blob/stl_viewer.js', stl_viewer: './blob/stl_viewer.js',
terminal: './terminal/terminal_bundle.js', terminal: './terminal/terminal_bundle.js',
ui_development_kit: './ui_development_kit.js', ui_development_kit: './ui_development_kit.js',
vue_merge_request_widget: './vue_merge_request_widget/index.js',
two_factor_auth: './two_factor_auth.js', two_factor_auth: './two_factor_auth.js',
...@@ -90,7 +76,15 @@ var config = { ...@@ -90,7 +76,15 @@ var config = {
test: './test.js', test: './test.js',
u2f: ['vendor/u2f'], u2f: ['vendor/u2f'],
webpack_runtime: './webpack.js', webpack_runtime: './webpack.js',
}, };
return Object.assign(manualEntries, autoEntries);
}
var config = {
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: generateEntries,
output: { output: {
path: path.join(ROOT_PATH, 'public/assets/webpack'), path: path.join(ROOT_PATH, 'public/assets/webpack'),
...@@ -243,12 +237,9 @@ var config = { ...@@ -243,12 +237,9 @@ var config = {
name: 'common_vue', name: 'common_vue',
chunks: [ chunks: [
'boards', 'boards',
'commit_pipelines',
'cycle_analytics', 'cycle_analytics',
'deploy_keys', 'deploy_keys',
'diff_notes',
'environments', 'environments',
'environments_folder',
'filtered_search', 'filtered_search',
'groups', 'groups',
'merge_conflicts', 'merge_conflicts',
...@@ -308,11 +299,15 @@ var config = { ...@@ -308,11 +299,15 @@ var config = {
'images': path.join(ROOT_PATH, 'app/assets/images'), 'images': path.join(ROOT_PATH, 'app/assets/images'),
'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
'vue$': 'vue/dist/vue.esm.js', 'vue$': 'vue/dist/vue.esm.js',
'spec': path.join(ROOT_PATH, 'spec/javascripts'),
} }
} },
}
config.entry = Object.assign({}, autoEntries, config.entry); // sqljs requires fs
node: {
fs: 'empty',
},
};
if (IS_PRODUCTION) { if (IS_PRODUCTION) {
config.devtool = 'source-map'; config.devtool = 'source-map';
...@@ -349,7 +344,24 @@ if (IS_DEV_SERVER) { ...@@ -349,7 +344,24 @@ if (IS_DEV_SERVER) {
}; };
config.plugins.push( config.plugins.push(
// watch node_modules for changes if we encounter a missing module compile error // watch node_modules for changes if we encounter a missing module compile error
new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules')) new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules')),
// watch for changes to our automatic entry point modules
{
apply(compiler) {
compiler.plugin('emit', (compilation, callback) => {
compilation.contextDependencies = [
...compilation.contextDependencies,
...watchAutoEntries,
];
// report our auto-generated bundle count
console.log(`${autoEntriesCount} entries from '/pages' automatically added to webpack output.`);
callback();
})
},
},
); );
if (DEV_SERVER_LIVERELOAD) { if (DEV_SERVER_LIVERELOAD) {
config.plugins.push(new webpack.HotModuleReplacementPlugin()); config.plugins.push(new webpack.HotModuleReplacementPlugin());
......
class AddTmpPartialNullIndexToBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
add_concurrent_index(:ci_builds, :id, where: 'stage_id IS NULL',
name: 'tmp_id_partial_null_index')
end
def down
remove_concurrent_index_by_name(:ci_builds, 'tmp_id_partial_null_index')
end
end
class ScheduleBuildStageMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'MigrateBuildStage'.freeze
BATCH_SIZE = 500
disable_ddl_transaction!
class Build < ActiveRecord::Base
include EachBatch
self.table_name = 'ci_builds'
end
def up
disable_statement_timeout
Build.where('stage_id IS NULL').tap do |relation|
queue_background_migration_jobs_by_range_at_intervals(relation,
MIGRATION,
5.minutes,
batch_size: BATCH_SIZE)
end
end
def down
# noop
end
end
class RemoveTmpPartialNullIndexFromBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
remove_concurrent_index_by_name(:ci_builds, 'tmp_id_partial_null_index')
end
def down
add_concurrent_index(:ci_builds, :id, where: 'stage_id IS NULL',
name: 'tmp_id_partial_null_index')
end
end
...@@ -53,7 +53,10 @@ module API ...@@ -53,7 +53,10 @@ module API
put ':id/access_requests/:user_id/approve' do put ':id/access_requests/:user_id/approve' do
source = find_source(source_type, params[:id]) source = find_source(source_type, params[:id])
member = ::Members::ApproveAccessRequestService.new(source, current_user, declared_params).execute access_requester = source.requesters.find_by!(user_id: params[:user_id])
member = ::Members::ApproveAccessRequestService
.new(current_user, declared_params)
.execute(access_requester)
status :created status :created
present member, with: Entities::Member present member, with: Entities::Member
...@@ -70,8 +73,7 @@ module API ...@@ -70,8 +73,7 @@ module API
member = source.requesters.find_by!(user_id: params[:user_id]) member = source.requesters.find_by!(user_id: params[:user_id])
destroy_conditionally!(member) do destroy_conditionally!(member) do
::Members::DestroyService.new(source, current_user, params) ::Members::DestroyService.new(current_user).execute(member)
.execute(:requesters)
end end
end end
end end
......
...@@ -81,12 +81,16 @@ module API ...@@ -81,12 +81,16 @@ module API
source = find_source(source_type, params.delete(:id)) source = find_source(source_type, params.delete(:id))
authorize_admin_source!(source_type, source) authorize_admin_source!(source_type, source)
member = source.members.find_by!(user_id: params.delete(:user_id)) member = source.members.find_by!(user_id: params[:user_id])
updated_member =
::Members::UpdateService
.new(current_user, declared_params(include_missing: false))
.execute(member)
if member.update_attributes(declared_params(include_missing: false)) if updated_member.valid?
present member, with: Entities::Member present updated_member, with: Entities::Member
else else
render_validation_error!(member) render_validation_error!(updated_member)
end end
end end
...@@ -99,7 +103,7 @@ module API ...@@ -99,7 +103,7 @@ module API
member = source.members.find_by!(user_id: params[:user_id]) member = source.members.find_by!(user_id: params[:user_id])
destroy_conditionally!(member) do destroy_conditionally!(member) do
::Members::DestroyService.new(source, current_user, declared_params).execute ::Members::DestroyService.new(current_user).execute(member)
end end
end end
end end
......
# frozen_string_literal: true
module API module API
class Services < Grape::API class Services < Grape::API
chat_notification_settings = [ CHAT_NOTIFICATION_SETTINGS = [
{ {
required: true, required: true,
name: :webhook, name: :webhook,
...@@ -19,9 +20,9 @@ module API ...@@ -19,9 +20,9 @@ module API
type: String, type: String,
desc: 'The default chat channel' desc: 'The default chat channel'
} }
] ].freeze
chat_notification_flags = [ CHAT_NOTIFICATION_FLAGS = [
{ {
required: false, required: false,
name: :notify_only_broken_pipelines, name: :notify_only_broken_pipelines,
...@@ -34,9 +35,9 @@ module API ...@@ -34,9 +35,9 @@ module API
type: Boolean, type: Boolean,
desc: 'Send notifications only for the default branch' desc: 'Send notifications only for the default branch'
} }
] ].freeze
chat_notification_channels = [ CHAT_NOTIFICATION_CHANNELS = [
{ {
required: false, required: false,
name: :push_channel, name: :push_channel,
...@@ -85,9 +86,9 @@ module API ...@@ -85,9 +86,9 @@ module API
type: String, type: String,
desc: 'The name of the channel to receive wiki_page_events notifications' desc: 'The name of the channel to receive wiki_page_events notifications'
} }
] ].freeze
chat_notification_events = [ CHAT_NOTIFICATION_EVENTS = [
{ {
required: false, required: false,
name: :push_events, name: :push_events,
...@@ -136,7 +137,7 @@ module API ...@@ -136,7 +137,7 @@ module API
type: Boolean, type: Boolean,
desc: 'Enable notifications for wiki_page_events' desc: 'Enable notifications for wiki_page_events'
} }
] ].freeze
services = { services = {
'asana' => [ 'asana' => [
...@@ -627,10 +628,10 @@ module API ...@@ -627,10 +628,10 @@ module API
} }
], ],
'slack' => [ 'slack' => [
chat_notification_settings, CHAT_NOTIFICATION_SETTINGS,
chat_notification_flags, CHAT_NOTIFICATION_FLAGS,
chat_notification_channels, CHAT_NOTIFICATION_CHANNELS,
chat_notification_events CHAT_NOTIFICATION_EVENTS
].flatten, ].flatten,
'microsoft-teams' => [ 'microsoft-teams' => [
{ {
...@@ -641,10 +642,10 @@ module API ...@@ -641,10 +642,10 @@ module API
} }
], ],
'mattermost' => [ 'mattermost' => [
chat_notification_settings, CHAT_NOTIFICATION_SETTINGS,
chat_notification_flags, CHAT_NOTIFICATION_FLAGS,
chat_notification_channels, CHAT_NOTIFICATION_CHANNELS,
chat_notification_events CHAT_NOTIFICATION_EVENTS
].flatten, ].flatten,
'teamcity' => [ 'teamcity' => [
{ {
...@@ -724,7 +725,22 @@ module API ...@@ -724,7 +725,22 @@ module API
] ]
end end
trigger_services = { SERVICES = services.freeze
SERVICE_CLASSES = service_classes.freeze
SERVICE_CLASSES.each do |service|
event_names = service.try(:event_names) || next
event_names.each do |event_name|
SERVICES[service.to_param.tr("_", "-")] << {
required: false,
name: event_name.to_sym,
type: String,
desc: ServicesHelper.service_event_description(event_name)
}
end
end
TRIGGER_SERVICES = {
'mattermost-slash-commands' => [ 'mattermost-slash-commands' => [
{ {
name: :token, name: :token,
...@@ -756,22 +772,9 @@ module API ...@@ -756,22 +772,9 @@ module API
end end
end end
services.each do |service_slug, settings| SERVICES.each do |service_slug, settings|
desc "Set #{service_slug} service for project" desc "Set #{service_slug} service for project"
params do params do
service_classes.each do |service|
event_names = service.try(:event_names) || next
event_names.each do |event_name|
services[service.to_param.tr("_", "-")] << {
required: false,
name: event_name.to_sym,
type: String,
desc: ServicesHelper.service_event_description(event_name)
}
end
end
services.freeze
settings.each do |setting| settings.each do |setting|
if setting[:required] if setting[:required]
requires setting[:name], type: setting[:type], desc: setting[:desc] requires setting[:name], type: setting[:type], desc: setting[:desc]
...@@ -794,7 +797,7 @@ module API ...@@ -794,7 +797,7 @@ module API
desc "Delete a service for project" desc "Delete a service for project"
params do params do
requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' requires :service_slug, type: String, values: SERVICES.keys, desc: 'The name of the service'
end end
delete ":id/services/:service_slug" do delete ":id/services/:service_slug" do
service = user_project.find_or_initialize_service(params[:service_slug].underscore) service = user_project.find_or_initialize_service(params[:service_slug].underscore)
...@@ -814,7 +817,7 @@ module API ...@@ -814,7 +817,7 @@ module API
success Entities::ProjectService success Entities::ProjectService
end end
params do params do
requires :service_slug, type: String, values: services.keys, desc: 'The name of the service' requires :service_slug, type: String, values: SERVICES.keys, desc: 'The name of the service'
end end
get ":id/services/:service_slug" do get ":id/services/:service_slug" do
service = user_project.find_or_initialize_service(params[:service_slug].underscore) service = user_project.find_or_initialize_service(params[:service_slug].underscore)
...@@ -822,7 +825,7 @@ module API ...@@ -822,7 +825,7 @@ module API
end end
end end
trigger_services.each do |service_slug, settings| TRIGGER_SERVICES.each do |service_slug, settings|
helpers do helpers do
def slash_command_service(project, service_slug, params) def slash_command_service(project, service_slug, params)
project.services.active.where(template: false).find do |service| project.services.active.where(template: false).find do |service|
......
...@@ -124,7 +124,7 @@ module API ...@@ -124,7 +124,7 @@ module API
status(200 ) status(200 )
{ message: "Access revoked", id: params[:user_id].to_i } { message: "Access revoked", id: params[:user_id].to_i }
else else
::Members::DestroyService.new(source, current_user, declared_params).execute ::Members::DestroyService.new(current_user).execute(member)
present member, with: ::API::Entities::Member present member, with: ::API::Entities::Member
end end
......
...@@ -174,7 +174,9 @@ module Banzai ...@@ -174,7 +174,9 @@ module Banzai
title = object_link_title(object) title = object_link_title(object)
klass = reference_class(object_sym) klass = reference_class(object_sym)
data = data_attributes_for(link_content || match, parent, object, link: !!link_content) data = data_attributes_for(link_content || match, parent, object,
link_content: !!link_content,
link_reference: link_reference)
url = url =
if matches.names.include?("url") && matches[:url] if matches.names.include?("url") && matches[:url]
...@@ -194,10 +196,11 @@ module Banzai ...@@ -194,10 +196,11 @@ module Banzai
end end
end end
def data_attributes_for(text, project, object, link: false) def data_attributes_for(text, project, object, link_content: false, link_reference: false)
data_attribute( data_attribute(
original: text, original: text,
link: link, link: link_content,
link_reference: link_reference,
project: project.id, project: project.id,
object_sym => object.id object_sym => object.id
) )
......
...@@ -42,16 +42,33 @@ module Banzai ...@@ -42,16 +42,33 @@ module Banzai
next if visible.include?(node) next if visible.include?(node)
doc_data[:visible_reference_count] -= 1 doc_data[:visible_reference_count] -= 1
# The reference should be replaced by the original link's content, redacted_content = redacted_node_content(node)
# which is not always the same as the rendered one. node.replace(redacted_content)
content = node.attr('data-original') || node.inner_html
node.replace(content)
end end
end end
metadata metadata
end end
# Return redacted content of given node as either the original link (<a> tag),
# the original content (text), or the inner HTML of the node.
#
def redacted_node_content(node)
original_content = node.attr('data-original')
link_reference = node.attr('data-link-reference')
# Build the raw <a> tag just with a link as href and content if
# it's originally a link pattern. We shouldn't return a plain text href.
original_link =
if link_reference == 'true' && href = original_content
%(<a href="#{href}">#{href}</a>)
end
# The reference should be replaced by the original link's content,
# which is not always the same as the rendered one.
original_link || original_content || node.inner_html
end
def redact_cross_project_references(documents) def redact_cross_project_references(documents)
extractor = Banzai::IssuableExtractor.new(project, user) extractor = Banzai::IssuableExtractor.new(project, user)
issuables = extractor.extract(documents) issuables = extractor.extract(documents)
......
# frozen_string_literal: true
# rubocop:disable Metrics/AbcSize
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class MigrateBuildStage
module Migratable
class Stage < ActiveRecord::Base
self.table_name = 'ci_stages'
end
class Build < ActiveRecord::Base
self.table_name = 'ci_builds'
def ensure_stage!(attempts: 2)
find_stage || create_stage!
rescue ActiveRecord::RecordNotUnique
retry if (attempts -= 1) > 0
raise
end
def find_stage
Stage.find_by(name: self.stage || 'test',
pipeline_id: self.commit_id,
project_id: self.project_id)
end
def create_stage!
Stage.create!(name: self.stage || 'test',
pipeline_id: self.commit_id,
project_id: self.project_id)
end
end
end
def perform(start_id, stop_id)
stages = Migratable::Build.where('stage_id IS NULL')
.where('id BETWEEN ? AND ?', start_id, stop_id)
.map { |build| build.ensure_stage! }
.compact.map(&:id)
MigrateBuildStageIdReference.new.perform(start_id, stop_id)
MigrateStageStatus.new.perform(stages.min, stages.max)
end
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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