Commit 39bb3720 authored by DJ Mountney's avatar DJ Mountney

Merge remote-tracking branch 'origin/master' into dev-master

parents f7f92836 166b4575
...@@ -618,21 +618,26 @@ karma: ...@@ -618,21 +618,26 @@ karma:
codequality: codequality:
<<: *dedicated-no-docs-no-db-pull-cache-job <<: *dedicated-no-docs-no-db-pull-cache-job
image: docker:latest image: docker:stable
allow_failure: true
# gitlab-org runners set `privileged: false` but we need to have it set to true
# since we're using Docker in Docker
tags: []
before_script: [] before_script: []
services: services:
- docker:dind - docker:dind
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
DOCKER_DRIVER: overlay2 DOCKER_DRIVER: overlay2
CODECLIMATE_FORMAT: json
cache: {} cache: {}
dependencies: [] dependencies: []
script: script:
- apk update && apk add jq # Get the custom rubocop codeclimate image (https://gitlab.com/gitlab-org/codeclimate-rubocop/wikis/home)
- ./scripts/codequality analyze -f json > raw_codeclimate.json || true - docker pull dev.gitlab.org:5005/gitlab/gitlab-build-images:gitlab-codeclimate-rubocop-0-52-1
# The following line keeps only the fields used in the MR widget, reducing the JSON artifact size - docker tag dev.gitlab.org:5005/gitlab/gitlab-build-images:gitlab-codeclimate-rubocop-0-52-1 codeclimate/codeclimate-rubocop:gitlab-codeclimate-rubocop-0-52-1
- jq -c 'map({check_name,description,fingerprint,location})' raw_codeclimate.json > codeclimate.json # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- docker run --env SOURCE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
artifacts: artifacts:
paths: [codeclimate.json] paths: [codeclimate.json]
expire_in: 1 week expire_in: 1 week
......
...@@ -33,13 +33,16 @@ When removing columns, tables, indexes or other structures: ...@@ -33,13 +33,16 @@ When removing columns, tables, indexes or other structures:
## General Checklist ## General Checklist
- [ ] [Changelog entry](https://docs.gitlab.com/ce/development/changelog.html) added, if necessary - [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary
- [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) - [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/doc_styleguide.html)
- [ ] API support added - [ ] API support added
- [ ] Tests added for this feature/bug - [ ] Tests added for this feature/bug
- Review - Review
- [ ] Has been reviewed by Backend - [ ] Has been reviewed by Backend
- [ ] Has been reviewed by Database - [ ] Has been reviewed by Database
- [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - [ ] Conform by the [merge request performance guides](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html)
- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) - [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides)
- [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) - [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
- [ ] Internationalization required/considered
- [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan
- [ ] End-to-end tests pass (`package-qa` manual pipeline job)
...@@ -199,7 +199,6 @@ entry. ...@@ -199,7 +199,6 @@ entry.
- Enable privileged mode for GitLab Runner. !17528 - Enable privileged mode for GitLab Runner. !17528
- Expose GITLAB_FEATURES as CI/CD variable (fixes #40994). - Expose GITLAB_FEATURES as CI/CD variable (fixes #40994).
- Upgrade GitLab Workhorse to 4.0.0. - Upgrade GitLab Workhorse to 4.0.0.
- Allow CI/CD Jobs being grouped on version strings.
- Add discussions API for Issues and Snippets. - Add discussions API for Issues and Snippets.
- Add one group board to Libre. - Add one group board to Libre.
- Add support for filtering by source and target branch to merge requests API. - Add support for filtering by source and target branch to merge requests API.
......
...@@ -97,7 +97,7 @@ GEM ...@@ -97,7 +97,7 @@ GEM
autoprefixer-rails (>= 5.2.1) autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4) sass (>= 3.3.4)
bootstrap_form (2.7.0) bootstrap_form (2.7.0)
brakeman (3.6.2) brakeman (4.2.1)
browser (2.5.3) browser (2.5.3)
builder (3.2.3) builder (3.2.3)
bullet (5.5.1) bullet (5.5.1)
...@@ -400,7 +400,7 @@ GEM ...@@ -400,7 +400,7 @@ GEM
hipchat (1.5.4) hipchat (1.5.4)
httparty httparty
mimemagic mimemagic
html-pipeline (2.6.0) html-pipeline (2.7.1)
activesupport (>= 2) activesupport (>= 2)
nokogiri (>= 1.4) nokogiri (>= 1.4)
html2text (0.2.1) html2text (0.2.1)
...@@ -1013,7 +1013,7 @@ DEPENDENCIES ...@@ -1013,7 +1013,7 @@ DEPENDENCIES
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0) bootstrap-sass (~> 3.3.0)
bootstrap_form (~> 2.7.0) bootstrap_form (~> 2.7.0)
brakeman (~> 3.6.0) brakeman (~> 4.2)
browser (~> 2.2) browser (~> 2.2)
bullet (~> 5.5.0) bullet (~> 5.5.0)
bundler-audit (~> 0.5.0) bundler-audit (~> 0.5.0)
...@@ -1084,7 +1084,7 @@ DEPENDENCIES ...@@ -1084,7 +1084,7 @@ DEPENDENCIES
hashie-forbidden_attributes hashie-forbidden_attributes
health_check (~> 2.6.0) health_check (~> 2.6.0)
hipchat (~> 1.5.0) hipchat (~> 1.5.0)
html-pipeline (~> 2.6.0) html-pipeline (~> 2.7.1)
html2text html2text
httparty (~> 0.13.3) httparty (~> 0.13.3)
influxdb (~> 0.2) influxdb (~> 0.2)
......
...@@ -4,7 +4,7 @@ import $ from 'jquery'; ...@@ -4,7 +4,7 @@ import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { __ } from './locale'; import { __ } from './locale';
import { isInIssuePage, isInMRPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils'; import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils';
import flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
...@@ -300,7 +300,7 @@ class AwardsHandler { ...@@ -300,7 +300,7 @@ class AwardsHandler {
} }
isInVueNoteablePage() { isInVueNoteablePage() {
return isInIssuePage() || this.isVueMRDiscussions(); return isInIssuePage() || isInEpicPage() || this.isVueMRDiscussions();
} }
getVotesBlock() { getVotesBlock() {
......
...@@ -26,8 +26,8 @@ export default class FilteredSearchDropdownManager { ...@@ -26,8 +26,8 @@ export default class FilteredSearchDropdownManager {
this.filteredSearchInput = this.container.querySelector('.filtered-search'); this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.page = page; this.page = page;
this.groupsOnly = isGroup; this.groupsOnly = isGroup;
this.groupAncestor = isGroupAncestor; this.includeAncestorGroups = isGroupAncestor;
this.isGroupDecendent = isGroupDecendent; this.includeDescendantGroups = isGroupDecendent;
this.setupMapping(); this.setupMapping();
...@@ -108,7 +108,19 @@ export default class FilteredSearchDropdownManager { ...@@ -108,7 +108,19 @@ export default class FilteredSearchDropdownManager {
} }
getLabelsEndpoint() { getLabelsEndpoint() {
const endpoint = `${this.baseEndpoint}/labels.json`; let endpoint = `${this.baseEndpoint}/labels.json?`;
if (this.groupsOnly) {
endpoint = `${endpoint}only_group_labels=true&`;
}
if (this.includeAncestorGroups) {
endpoint = `${endpoint}include_ancestor_groups=true&`;
}
if (this.includeDescendantGroups) {
endpoint = `${endpoint}include_descendant_groups=true`;
}
return endpoint; return endpoint;
} }
......
...@@ -21,7 +21,7 @@ export default class FilteredSearchManager { ...@@ -21,7 +21,7 @@ export default class FilteredSearchManager {
constructor({ constructor({
page, page,
isGroup = false, isGroup = false,
isGroupAncestor = false, isGroupAncestor = true,
isGroupDecendent = false, isGroupDecendent = false,
filteredSearchTokenKeys = FilteredSearchTokenKeys, filteredSearchTokenKeys = FilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters', stateFiltersSelector = '.issues-state-filters',
...@@ -86,6 +86,7 @@ export default class FilteredSearchManager { ...@@ -86,6 +86,7 @@ export default class FilteredSearchManager {
page: this.page, page: this.page,
isGroup: this.isGroup, isGroup: this.isGroup,
isGroupAncestor: this.isGroupAncestor, isGroupAncestor: this.isGroupAncestor,
isGroupDecendent: this.isGroupDecendent,
filteredSearchTokenKeys: this.filteredSearchTokenKeys, filteredSearchTokenKeys: this.filteredSearchTokenKeys,
}); });
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import router from '../../ide_router';
export default { export default {
components: { components: {
icon, Icon,
}, },
props: { props: {
file: { file: {
...@@ -22,17 +21,16 @@ ...@@ -22,17 +21,16 @@
}, },
}, },
methods: { methods: {
...mapActions([ ...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
'discardFileChanges',
'updateViewer',
]),
openFileInEditor(file) { openFileInEditor(file) {
return this.openPendingTab(file).then(changeViewer => {
if (changeViewer) {
this.updateViewer('diff'); this.updateViewer('diff');
}
router.push(`/project${file.url}`); });
}, },
}, },
}; };
</script> </script>
<template> <template>
......
...@@ -60,6 +60,7 @@ export default { ...@@ -60,6 +60,7 @@ export default {
v-if="activeFile" v-if="activeFile"
> >
<repo-tabs <repo-tabs
:active-file="activeFile"
:files="openFiles" :files="openFiles"
:viewer="viewer" :viewer="viewer"
:has-changes="hasChanges" :has-changes="hasChanges"
......
...@@ -21,7 +21,8 @@ export default { ...@@ -21,7 +21,8 @@ export default {
}, },
watch: { watch: {
file(oldVal, newVal) { file(oldVal, newVal) {
if (newVal.path !== this.file.path) { // Compare key to allow for files opened in review mode to be cached differently
if (newVal.key !== this.file.key) {
this.initMonaco(); this.initMonaco();
} }
}, },
...@@ -70,7 +71,7 @@ export default { ...@@ -70,7 +71,7 @@ export default {
}) })
.then(() => { .then(() => {
const viewerPromise = this.delayViewerUpdated const viewerPromise = this.delayViewerUpdated
? this.updateViewer('editor') ? this.updateViewer(this.file.pending ? 'diff' : 'editor')
: Promise.resolve(); : Promise.resolve();
return viewerPromise; return viewerPromise;
......
...@@ -62,11 +62,7 @@ export default { ...@@ -62,11 +62,7 @@ export default {
this.toggleTreeOpen(this.file.path); this.toggleTreeOpen(this.file.path);
} }
const delayPromise = this.file.changed return this.updateDelayViewerUpdated(true).then(() => {
? Promise.resolve()
: this.updateDelayViewerUpdated(true);
return delayPromise.then(() => {
router.push(`/project${this.file.url}`); router.push(`/project${this.file.url}`);
}); });
}, },
......
<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';
import icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import fileStatusIcon from './repo_file_status_icon.vue'; import FileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue'; import ChangedFileIcon from './changed_file_icon.vue';
export default { export default {
components: { components: {
fileStatusIcon, FileStatusIcon,
fileIcon, FileIcon,
icon, Icon,
changedFileIcon, ChangedFileIcon,
}, },
props: { props: {
tab: { tab: {
...@@ -37,11 +37,15 @@ ...@@ -37,11 +37,15 @@
}, },
methods: { methods: {
...mapActions([ ...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']),
'closeFile',
]),
clickFile(tab) { clickFile(tab) {
this.updateDelayViewerUpdated(true);
if (tab.pending) {
this.openPendingTab(tab);
} else {
this.$router.push(`/project${tab.url}`); this.$router.push(`/project${tab.url}`);
}
}, },
mouseOverTab() { mouseOverTab() {
if (this.tab.changed) { if (this.tab.changed) {
...@@ -54,7 +58,7 @@ ...@@ -54,7 +58,7 @@
} }
}, },
}, },
}; };
</script> </script>
<template> <template>
...@@ -66,7 +70,7 @@ ...@@ -66,7 +70,7 @@
<button <button
type="button" type="button"
class="multi-file-tab-close" class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab.path)" @click.stop.prevent="closeFile(tab)"
:aria-label="closeLabel" :aria-label="closeLabel"
> >
<icon <icon
...@@ -82,7 +86,9 @@ ...@@ -82,7 +86,9 @@
<div <div
class="multi-file-tab" class="multi-file-tab"
:class="{active : tab.active }" :class="{
active: tab.active
}"
:title="tab.url" :title="tab.url"
> >
<file-icon <file-icon
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue'; import EditorMode from './editor_mode_dropdown.vue';
import router from '../ide_router';
export default { export default {
components: { components: {
...@@ -9,6 +10,10 @@ export default { ...@@ -9,6 +10,10 @@ export default {
EditorMode, EditorMode,
}, },
props: { props: {
activeFile: {
type: Object,
required: true,
},
files: { files: {
type: Array, type: Array,
required: true, required: true,
...@@ -38,7 +43,18 @@ export default { ...@@ -38,7 +43,18 @@ export default {
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth; this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
}, },
methods: { methods: {
...mapActions(['updateViewer']), ...mapActions(['updateViewer', 'removePendingTab']),
openFileViewer(viewer) {
this.updateViewer(viewer);
if (this.activeFile.pending) {
return this.removePendingTab(this.activeFile).then(() => {
router.push(`/project${this.activeFile.url}`);
});
}
return null;
},
}, },
}; };
</script> </script>
...@@ -60,7 +76,7 @@ export default { ...@@ -60,7 +76,7 @@ export default {
:show-shadow="showShadow" :show-shadow="showShadow"
:has-changes="hasChanges" :has-changes="hasChanges"
:merge-request-id="mergeRequestId" :merge-request-id="mergeRequestId"
@click="updateViewer" @click="openFileViewer"
/> />
</div> </div>
</template> </template>
...@@ -77,7 +77,11 @@ router.beforeEach((to, from, next) => { ...@@ -77,7 +77,11 @@ router.beforeEach((to, from, next) => {
if (to.params[0]) { if (to.params[0]) {
const path = const path =
to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0]; to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
const treeEntry = store.state.entries[path]; const treeEntryKey = Object.keys(store.state.entries).find(
key => key === path && !store.state.entries[key].pending,
);
const treeEntry = store.state.entries[treeEntryKey];
if (treeEntry) { if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry); store.dispatch('handleTreeEntryAction', treeEntry);
} }
......
...@@ -13,12 +13,12 @@ export default class Model { ...@@ -13,12 +13,12 @@ export default class Model {
(this.originalModel = this.monaco.editor.createModel( (this.originalModel = this.monaco.editor.createModel(
this.file.raw, this.file.raw,
undefined, undefined,
new this.monaco.Uri(null, null, `original/${this.file.path}`), new this.monaco.Uri(null, null, `original/${this.file.key}`),
)), )),
(this.model = this.monaco.editor.createModel( (this.model = this.monaco.editor.createModel(
this.content, this.content,
undefined, undefined,
new this.monaco.Uri(null, null, this.file.path), new this.monaco.Uri(null, null, this.file.key),
)), )),
); );
if (this.file.mrChange) { if (this.file.mrChange) {
...@@ -36,7 +36,7 @@ export default class Model { ...@@ -36,7 +36,7 @@ export default class Model {
this.updateContent = this.updateContent.bind(this); this.updateContent = this.updateContent.bind(this);
this.dispose = this.dispose.bind(this); this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose); eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose);
eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent); eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
} }
...@@ -53,7 +53,7 @@ export default class Model { ...@@ -53,7 +53,7 @@ export default class Model {
} }
get path() { get path() {
return this.file.path; return this.file.key;
} }
getModel() { getModel() {
...@@ -88,7 +88,7 @@ export default class Model { ...@@ -88,7 +88,7 @@ export default class Model {
this.disposable.dispose(); this.disposable.dispose();
this.events.clear(); this.events.clear();
eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose); eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent); eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
} }
} }
...@@ -9,17 +9,17 @@ export default class ModelManager { ...@@ -9,17 +9,17 @@ export default class ModelManager {
this.models = new Map(); this.models = new Map();
} }
hasCachedModel(path) { hasCachedModel(key) {
return this.models.has(path); return this.models.has(key);
} }
getModel(path) { getModel(key) {
return this.models.get(path); return this.models.get(key);
} }
addModel(file) { addModel(file) {
if (this.hasCachedModel(file.path)) { if (this.hasCachedModel(file.key)) {
return this.getModel(file.path); return this.getModel(file.key);
} }
const model = new Model(this.monaco, file); const model = new Model(this.monaco, file);
...@@ -27,7 +27,7 @@ export default class ModelManager { ...@@ -27,7 +27,7 @@ export default class ModelManager {
this.disposable.add(model); this.disposable.add(model);
eventHub.$on( eventHub.$on(
`editor.update.model.dispose.${file.path}`, `editor.update.model.dispose.${file.key}`,
this.removeCachedModel.bind(this, file), this.removeCachedModel.bind(this, file),
); );
...@@ -35,12 +35,9 @@ export default class ModelManager { ...@@ -35,12 +35,9 @@ export default class ModelManager {
} }
removeCachedModel(file) { removeCachedModel(file) {
this.models.delete(file.path); this.models.delete(file.key);
eventHub.$off( eventHub.$off(`editor.update.model.dispose.${file.key}`, this.removeCachedModel);
`editor.update.model.dispose.${file.path}`,
this.removeCachedModel,
);
} }
dispose() { dispose() {
......
...@@ -21,7 +21,7 @@ export const discardAllChanges = ({ state, commit, dispatch }) => { ...@@ -21,7 +21,7 @@ export const discardAllChanges = ({ state, commit, dispatch }) => {
}; };
export const closeAllFiles = ({ state, dispatch }) => { export const closeAllFiles = ({ state, dispatch }) => {
state.openFiles.forEach(file => dispatch('closeFile', file.path)); state.openFiles.forEach(file => dispatch('closeFile', file));
}; };
export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => { export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
......
...@@ -6,24 +6,34 @@ import * as types from '../mutation_types'; ...@@ -6,24 +6,34 @@ import * as types from '../mutation_types';
import router from '../../ide_router'; import router from '../../ide_router';
import { setPageTitle } from '../utils'; import { setPageTitle } from '../utils';
export const closeFile = ({ commit, state, getters, dispatch }, path) => { export const closeFile = ({ commit, state, dispatch }, file) => {
const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path); const path = file.path;
const file = state.entries[path]; const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key);
const fileWasActive = file.active; const fileWasActive = file.active;
if (file.pending) {
commit(types.REMOVE_PENDING_TAB, file);
} else {
commit(types.TOGGLE_FILE_OPEN, path); commit(types.TOGGLE_FILE_OPEN, path);
commit(types.SET_FILE_ACTIVE, { path, active: false }); commit(types.SET_FILE_ACTIVE, { path, active: false });
}
if (state.openFiles.length > 0 && fileWasActive) { if (state.openFiles.length > 0 && fileWasActive) {
const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1; const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
const nextFileToOpen = state.entries[state.openFiles[nextIndexToOpen].path]; const nextFileToOpen = state.openFiles[nextIndexToOpen];
if (nextFileToOpen.pending) {
dispatch('updateViewer', 'diff');
dispatch('openPendingTab', nextFileToOpen);
} else {
dispatch('updateDelayViewerUpdated', true);
router.push(`/project${nextFileToOpen.url}`); router.push(`/project${nextFileToOpen.url}`);
}
} else if (!state.openFiles.length) { } else if (!state.openFiles.length) {
router.push(`/project/${file.projectId}/tree/${file.branchId}/`); router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
} }
eventHub.$emit(`editor.update.model.dispose.${file.path}`); eventHub.$emit(`editor.update.model.dispose.${file.key}`);
}; };
export const setFileActive = ({ commit, state, getters, dispatch }, path) => { export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
...@@ -151,3 +161,23 @@ export const discardFileChanges = ({ state, commit }, path) => { ...@@ -151,3 +161,23 @@ export const discardFileChanges = ({ state, commit }, path) => {
eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw); eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
}; };
export const openPendingTab = ({ commit, getters, dispatch, state }, file) => {
if (getters.activeFile && getters.activeFile.path === file.path && state.viewer === 'diff') {
return false;
}
commit(types.ADD_PENDING_TAB, { file });
dispatch('scrollToTab');
router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`);
return true;
};
export const removePendingTab = ({ commit }, file) => {
commit(types.REMOVE_PENDING_TAB, file);
eventHub.$emit(`editor.update.model.dispose.${file.key}`);
};
...@@ -49,3 +49,6 @@ export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY'; ...@@ -49,3 +49,6 @@ export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE'; export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
export const UPDATE_VIEWER = 'UPDATE_VIEWER'; export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE'; export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
...@@ -5,6 +5,14 @@ export default { ...@@ -5,6 +5,14 @@ export default {
Object.assign(state.entries[path], { Object.assign(state.entries[path], {
active, active,
}); });
if (active && !state.entries[path].pending) {
Object.assign(state, {
openFiles: state.openFiles.map(f =>
Object.assign(f, { active: f.pending ? false : f.active }),
),
});
}
}, },
[types.TOGGLE_FILE_OPEN](state, path) { [types.TOGGLE_FILE_OPEN](state, path) {
Object.assign(state.entries[path], { Object.assign(state.entries[path], {
...@@ -12,10 +20,14 @@ export default { ...@@ -12,10 +20,14 @@ export default {
}); });
if (state.entries[path].opened) { if (state.entries[path].opened) {
state.openFiles.push(state.entries[path]); Object.assign(state, {
openFiles: state.openFiles.filter(f => f.path !== path).concat(state.entries[path]),
});
} else { } else {
const file = state.entries[path];
Object.assign(state, { Object.assign(state, {
openFiles: state.openFiles.filter(f => f.path !== path), openFiles: state.openFiles.filter(f => f.key !== file.key),
}); });
} }
}, },
...@@ -92,4 +104,37 @@ export default { ...@@ -92,4 +104,37 @@ export default {
changed, changed,
}); });
}, },
[types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
const pendingTab = state.openFiles.find(f => f.path === file.path && f.pending);
let openFiles = state.openFiles.map(f =>
Object.assign(f, { active: f.path === file.path, opened: false }),
);
if (!pendingTab) {
const openFile = openFiles.find(f => f.path === file.path);
openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => {
if (!f) return acc;
if (f.path === file.path) {
return acc.concat({
...f,
active: true,
pending: true,
opened: true,
key: `${keyPrefix}-${f.key}`,
});
}
return acc.concat(f);
}, []);
}
Object.assign(state, { openFiles });
},
[types.REMOVE_PENDING_TAB](state, file) {
Object.assign(state, {
openFiles: state.openFiles.filter(f => f.key !== file.key),
});
},
}; };
export const dataStructure = () => ({ export const dataStructure = () => ({
id: '', id: '',
// Key will contain a mixture of ID and path
// it can also contain a prefix `pending-` for files opened in review mode
key: '', key: '',
type: '', type: '',
projectId: '', projectId: '',
......
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
return `#${this.job.runner.id}`; return `#${this.job.runner.id}`;
}, },
hasTimeout() { hasTimeout() {
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== ''; return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
}, },
timeout() { timeout() {
if (this.job.metadata == null) { if (this.job.metadata == null) {
......
...@@ -33,6 +33,7 @@ export const checkPageAndAction = (page, action) => { ...@@ -33,6 +33,7 @@ export const checkPageAndAction = (page, action) => {
export const isInIssuePage = () => checkPageAndAction('issues', 'show'); export const isInIssuePage = () => checkPageAndAction('issues', 'show');
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show'); export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
export const isInEpicPage = () => checkPageAndAction('epics', 'show');
export const isInNoteablePage = () => isInIssuePage() || isInMRPage(); export const isInNoteablePage = () => isInIssuePage() || isInMRPage();
export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions'); export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions');
......
...@@ -99,6 +99,10 @@ export default { ...@@ -99,6 +99,10 @@ export default {
'js-note-target-reopen': !this.isOpen, 'js-note-target-reopen': !this.isOpen,
}; };
}, },
supportQuickActions() {
// Disable quick actions support for Epics
return this.noteableType !== constants.EPIC_NOTEABLE_TYPE;
},
markdownDocsPath() { markdownDocsPath() {
return this.getNotesData.markdownDocsPath; return this.getNotesData.markdownDocsPath;
}, },
...@@ -355,7 +359,7 @@ Please check your network connection and try again.`; ...@@ -355,7 +359,7 @@ Please check your network connection and try again.`;
name="note[note]" name="note[note]"
class="note-textarea js-vue-comment-form class="note-textarea js-vue-comment-form
js-gfm-input js-autosize markdown-area js-vue-textarea" js-gfm-input js-autosize markdown-area js-vue-textarea"
data-supports-quick-actions="true" :data-supports-quick-actions="supportQuickActions"
aria-label="Description" aria-label="Description"
v-model="note" v-model="note"
ref="textarea" ref="textarea"
......
...@@ -50,7 +50,11 @@ export default { ...@@ -50,7 +50,11 @@ export default {
...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']), ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
noteableType() { noteableType() {
// FIXME -- @fatihacet Get this from JSON data. // FIXME -- @fatihacet Get this from JSON data.
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants; const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
if (this.noteableData.noteableType === EPIC_NOTEABLE_TYPE) {
return EPIC_NOTEABLE_TYPE;
}
return this.noteableData.merge_params return this.noteableData.merge_params
? MERGE_REQUEST_NOTEABLE_TYPE ? MERGE_REQUEST_NOTEABLE_TYPE
......
...@@ -10,6 +10,7 @@ export const CLOSED = 'closed'; ...@@ -10,6 +10,7 @@ export const CLOSED = 'closed';
export const EMOJI_THUMBSUP = 'thumbsup'; export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown'; export const EMOJI_THUMBSDOWN = 'thumbsdown';
export const ISSUE_NOTEABLE_TYPE = 'issue'; export const ISSUE_NOTEABLE_TYPE = 'issue';
export const EPIC_NOTEABLE_TYPE = 'epic';
export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request'; export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete'; export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post'; export const RESOLVE_NOTE_METHOD_NAME = 'post';
...@@ -12,8 +12,11 @@ document.addEventListener( ...@@ -12,8 +12,11 @@ document.addEventListener(
data() { data() {
const notesDataset = document.getElementById('js-vue-notes').dataset; const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData); const parsedUserData = JSON.parse(notesDataset.currentUserData);
const noteableData = JSON.parse(notesDataset.noteableData);
let currentUserData = {}; let currentUserData = {};
noteableData.noteableType = notesDataset.noteableType;
if (parsedUserData) { if (parsedUserData) {
currentUserData = { currentUserData = {
id: parsedUserData.id, id: parsedUserData.id,
...@@ -25,7 +28,7 @@ document.addEventListener( ...@@ -25,7 +28,7 @@ document.addEventListener(
} }
return { return {
noteableData: JSON.parse(notesDataset.noteableData), noteableData,
currentUserData, currentUserData,
notesData: JSON.parse(notesDataset.notesData), notesData: JSON.parse(notesDataset.notesData),
}; };
......
...@@ -14,6 +14,8 @@ export default { ...@@ -14,6 +14,8 @@ export default {
return constants.MERGE_REQUEST_NOTEABLE_TYPE; return constants.MERGE_REQUEST_NOTEABLE_TYPE;
case 'Issue': case 'Issue':
return constants.ISSUE_NOTEABLE_TYPE; return constants.ISSUE_NOTEABLE_TYPE;
case 'Epic':
return constants.EPIC_NOTEABLE_TYPE;
default: default:
return ''; return '';
} }
......
...@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants'; ...@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({ initFilteredSearch({
page: FILTERED_SEARCH.ISSUES, page: FILTERED_SEARCH.ISSUES,
isGroupDecendent: true,
}); });
projectSelect(); projectSelect();
}); });
...@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants'; ...@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({ initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS, page: FILTERED_SEARCH.MERGE_REQUESTS,
isGroupDecendent: true,
}); });
projectSelect(); projectSelect();
}); });
...@@ -706,8 +706,8 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -706,8 +706,8 @@ button.mini-pipeline-graph-dropdown-toggle {
// dropdown content for big and mini pipeline // dropdown content for big and mini pipeline
.big-pipeline-graph-dropdown-menu, .big-pipeline-graph-dropdown-menu,
.mini-pipeline-graph-dropdown-menu { .mini-pipeline-graph-dropdown-menu {
width: 195px; width: 240px;
max-width: 195px; max-width: 240px;
.scrollable-menu { .scrollable-menu {
padding: 0; padding: 0;
...@@ -750,7 +750,7 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -750,7 +750,7 @@ button.mini-pipeline-graph-dropdown-toggle {
height: #{$ci-action-icon-size - 6}; height: #{$ci-action-icon-size - 6};
left: -3px; left: -3px;
position: relative; position: relative;
top: -2px; top: -1px;
&.icon-action-stop, &.icon-action-stop,
&.icon-action-cancel { &.icon-action-cancel {
...@@ -931,13 +931,11 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -931,13 +931,11 @@ button.mini-pipeline-graph-dropdown-toggle {
*/ */
&.dropdown-menu { &.dropdown-menu {
transform: translate(-80%, 0); transform: translate(-80%, 0);
min-width: 150px;
@media(min-width: $screen-md-min) { @media(min-width: $screen-md-min) {
transform: translate(-50%, 0); transform: translate(-50%, 0);
right: auto; right: auto;
left: 50%; left: 50%;
min-width: 240px;
} }
} }
} }
......
...@@ -5,7 +5,7 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -5,7 +5,7 @@ class Admin::GroupsController < Admin::ApplicationController
def index def index
@groups = Group.with_statistics.with_route @groups = Group.with_statistics.with_route
@groups = @groups.sort(@sort = params[:sort]) @groups = @groups.sort_by_attribute(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present? @groups = @groups.search(params[:name]) if params[:name].present?
@groups = @groups.page(params[:page]) @groups = @groups.page(params[:page])
end end
......
...@@ -4,7 +4,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -4,7 +4,7 @@ class Admin::UsersController < Admin::ApplicationController
def index def index
@users = User.order_name_asc.filter(params[:filter]) @users = User.order_name_asc.filter(params[:filter])
@users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present? @users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present?
@users = @users.sort(@sort = params[:sort]) @users = @users.sort_by_attribute(@sort = params[:sort])
@users = @users.page(params[:page]) @users = @users.page(params[:page])
end end
......
...@@ -229,10 +229,6 @@ class ApplicationController < ActionController::Base ...@@ -229,10 +229,6 @@ class ApplicationController < ActionController::Base
@event_filter ||= EventFilter.new(filters) @event_filter ||= EventFilter.new(filters)
end end
def gitlab_ldap_access(&block)
Gitlab::Auth::LDAP::Access.open { |access| yield(access) }
end
# JSON for infinite scroll via Pager object # JSON for infinite scroll via Pager object
def pager_json(partial, count, locals = {}) def pager_json(partial, count, locals = {})
html = render_to_string( html = render_to_string(
......
...@@ -14,7 +14,7 @@ module GroupTree ...@@ -14,7 +14,7 @@ module GroupTree
end end
@groups = @groups.with_selects_for_list(archived: params[:archived]) @groups = @groups.with_selects_for_list(archived: params[:archived])
.sort(@sort = params[:sort]) .sort_by_attribute(@sort = params[:sort])
.page(params[:page]) .page(params[:page])
respond_to do |format| respond_to do |format|
......
...@@ -88,11 +88,15 @@ module IssuableActions ...@@ -88,11 +88,15 @@ module IssuableActions
discussions = Discussion.build_collection(notes, issuable) discussions = Discussion.build_collection(notes, issuable)
render json: DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user).represent(discussions, context: self) render json: discussion_serializer.represent(discussions, context: self)
end end
private private
def discussion_serializer
DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user, note_entity: ProjectNoteEntity)
end
def recaptcha_check_if_spammable(should_redirect = true, &block) def recaptcha_check_if_spammable(should_redirect = true, &block)
return yield unless issuable.is_a? Spammable return yield unless issuable.is_a? Spammable
......
...@@ -212,7 +212,7 @@ module NotesActions ...@@ -212,7 +212,7 @@ module NotesActions
end end
def note_serializer def note_serializer
NoteSerializer.new(project: project, noteable: noteable, current_user: current_user) ProjectNoteSerializer.new(project: project, noteable: noteable, current_user: current_user)
end end
def note_project def note_project
......
...@@ -17,7 +17,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -17,7 +17,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
@members = GroupMembersFinder.new(@group).execute @members = GroupMembersFinder.new(@group).execute
@members = @members.non_invite unless can?(current_user, :admin_group, @group) @members = @members.non_invite unless can?(current_user, :admin_group, @group)
@members = @members.search(params[:search]) if params[:search].present? @members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort(@sort) @members = @members.sort_by_attribute(@sort)
@members = @members.page(params[:page]).per(50) @members = @members.page(params[:page]).per(50)
@members = present_members(@members.includes(:user)) @members = present_members(@members.includes(:user))
......
...@@ -51,7 +51,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -51,7 +51,7 @@ class ProfilesController < Profiles::ApplicationController
end end
def update_username def update_username
result = Users::UpdateService.new(current_user, user: @user, username: user_params[:username]).execute result = Users::UpdateService.new(current_user, user: @user, username: username_param).execute
options = if result[:status] == :success options = if result[:status] == :success
{ notice: "Username successfully changed" } { notice: "Username successfully changed" }
...@@ -72,6 +72,10 @@ class ProfilesController < Profiles::ApplicationController ...@@ -72,6 +72,10 @@ class ProfilesController < Profiles::ApplicationController
return render_404 unless @user.can_change_username? return render_404 unless @user.can_change_username?
end end
def username_param
@username_param ||= user_params.require(:username)
end
def user_params def user_params
@user_params ||= params.require(:user).permit( @user_params ||= params.require(:user).permit(
:avatar, :avatar,
......
...@@ -43,7 +43,7 @@ class Projects::DiscussionsController < Projects::ApplicationController ...@@ -43,7 +43,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
def render_json_with_discussions_serializer def render_json_with_discussions_serializer
render json: render json:
DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user) DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user, note_entity: ProjectNoteEntity)
.represent(discussion, context: self) .represent(discussion, context: self)
end end
......
...@@ -150,7 +150,8 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -150,7 +150,8 @@ class Projects::LabelsController < Projects::ApplicationController
end end
def find_labels def find_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute @available_labels ||=
LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: params[:include_ancestor_groups]).execute
end end
def authorize_admin_labels! def authorize_admin_labels!
......
...@@ -14,7 +14,7 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -14,7 +14,7 @@ class Projects::MilestonesController < Projects::ApplicationController
def index def index
@sort = params[:sort] || 'due_date_asc' @sort = params[:sort] || 'due_date_asc'
@milestones = milestones.sort(@sort) @milestones = milestones.sort_by_attribute(@sort)
respond_to do |format| respond_to do |format|
format.html do format.html do
......
...@@ -21,7 +21,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -21,7 +21,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) @group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end end
@project_members = present_members(@project_members.sort(@sort).page(params[:page])) @project_members = present_members(@project_members.sort_by_attribute(@sort).page(params[:page]))
@requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user)) @requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user))
@project_member = @project.project_members.new @project_member = @project.project_members.new
end end
......
...@@ -16,6 +16,10 @@ module Projects ...@@ -16,6 +16,10 @@ module Projects
@protected_tags = @project.protected_tags.order(:name).page(params[:page]) @protected_tags = @project.protected_tags.order(:name).page(params[:page])
@protected_branch = @project.protected_branches.new @protected_branch = @project.protected_branches.new
@protected_tag = @project.protected_tags.new @protected_tag = @project.protected_tags.new
@protected_branches_count = @protected_branches.reduce(0) { |sum, branch| sum + branch.matching(@project.repository.branches).size }
@protected_tags_count = @protected_tags.reduce(0) { |sum, tag| sum + tag.matching(@project.repository.tags).size }
load_gon_index load_gon_index
end end
......
...@@ -62,6 +62,6 @@ class Admin::ProjectsFinder ...@@ -62,6 +62,6 @@ class Admin::ProjectsFinder
def sort(items) def sort(items)
sort = params.fetch(:sort) { 'latest_activity_desc' } sort = params.fetch(:sort) { 'latest_activity_desc' }
items.sort(sort) items.sort_by_attribute(sort)
end end
end end
...@@ -337,7 +337,7 @@ class IssuableFinder ...@@ -337,7 +337,7 @@ class IssuableFinder
def sort(items) def sort(items)
# Ensure we always have an explicit sort order (instead of inheriting # Ensure we always have an explicit sort order (instead of inheriting
# multiple orders when combining ActiveRecord::Relation objects). # multiple orders when combining ActiveRecord::Relation objects).
params[:sort] ? items.sort(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc) params[:sort] ? items.sort_by_attribute(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc)
end end
def by_assignee(items) def by_assignee(items)
......
...@@ -28,9 +28,10 @@ class LabelsFinder < UnionFinder ...@@ -28,9 +28,10 @@ class LabelsFinder < UnionFinder
if project if project
if project.group.present? if project.group.present?
labels_table = Label.arel_table labels_table = Label.arel_table
group_ids = group_ids_for(project.group)
label_ids << Label.where( label_ids << Label.where(
labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].eq(project.group.id)).or( labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].in(group_ids)).or(
labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id)) labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id))
) )
) )
...@@ -38,11 +39,14 @@ class LabelsFinder < UnionFinder ...@@ -38,11 +39,14 @@ class LabelsFinder < UnionFinder
label_ids << project.labels label_ids << project.labels
end end
end end
elsif only_group_labels?
label_ids << Label.where(group_id: group_ids)
else else
if group?
group = Group.find(params[:group_id])
label_ids << Label.where(group_id: group_ids_for(group))
end
label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id)) label_ids << Label.where(project_id: projects.select(:id)) unless only_group_labels?
end end
label_ids label_ids
...@@ -59,22 +63,33 @@ class LabelsFinder < UnionFinder ...@@ -59,22 +63,33 @@ class LabelsFinder < UnionFinder
items.where(title: title) items.where(title: title)
end end
def group_ids # Gets redacted array of group ids
# which can include the ancestors and descendants of the requested group.
def group_ids_for(group)
strong_memoize(:group_ids) do strong_memoize(:group_ids) do
groups_user_can_read_labels(groups_to_include).map(&:id) groups = groups_to_include(group)
groups_user_can_read_labels(groups).map(&:id)
end end
end end
def groups_to_include def groups_to_include(group)
group = Group.find(params[:group_id])
groups = [group] groups = [group]
groups += group.ancestors if params[:include_ancestor_groups].present? groups += group.ancestors if include_ancestor_groups?
groups += group.descendants if params[:include_descendant_groups].present? groups += group.descendants if include_descendant_groups?
groups groups
end end
def include_ancestor_groups?
params[:include_ancestor_groups]
end
def include_descendant_groups?
params[:include_descendant_groups]
end
def group? def group?
params[:group_id].present? params[:group_id].present?
end end
......
...@@ -124,7 +124,7 @@ class ProjectsFinder < UnionFinder ...@@ -124,7 +124,7 @@ class ProjectsFinder < UnionFinder
end end
def sort(items) def sort(items)
params[:sort].present? ? items.sort(params[:sort]) : items.order_id_desc params[:sort].present? ? items.sort_by_attribute(params[:sort]) : items.order_id_desc
end end
def by_archived(projects) def by_archived(projects)
......
...@@ -119,7 +119,7 @@ class TodosFinder ...@@ -119,7 +119,7 @@ class TodosFinder
end end
def sort(items) def sort(items)
params[:sort] ? items.sort(params[:sort]) : items.order_id_desc params[:sort] ? items.sort_by_attribute(params[:sort]) : items.order_id_desc
end end
def by_action(items) def by_action(items)
......
...@@ -53,10 +53,12 @@ module BoardsHelper ...@@ -53,10 +53,12 @@ module BoardsHelper
end end
def board_list_data def board_list_data
include_descendant_groups = @group&.present?
{ {
toggle: "dropdown", toggle: "dropdown",
list_labels_path: labels_filter_path(true), list_labels_path: labels_filter_path(true, include_ancestor_groups: true),
labels: labels_filter_path(true), labels: labels_filter_path(true, include_descendant_groups: include_descendant_groups),
labels_endpoint: @labels_endpoint, labels_endpoint: @labels_endpoint,
namespace_path: @namespace_path, namespace_path: @namespace_path,
project_path: @project&.path, project_path: @project&.path,
......
...@@ -129,13 +129,17 @@ module LabelsHelper ...@@ -129,13 +129,17 @@ module LabelsHelper
end end
end end
def labels_filter_path(only_group_labels = false) def labels_filter_path(only_group_labels = false, include_ancestor_groups: true, include_descendant_groups: false)
project = @target_project || @project project = @target_project || @project
options = {}
options[:include_ancestor_groups] = include_ancestor_groups if include_ancestor_groups
options[:include_descendant_groups] = include_descendant_groups if include_descendant_groups
if project if project
project_labels_path(project, :json) project_labels_path(project, :json, options)
elsif @group elsif @group
options = { only_group_labels: only_group_labels } if only_group_labels options[:only_group_labels] = only_group_labels if only_group_labels
group_labels_path(@group, :json, options) group_labels_path(@group, :json, options)
else else
dashboard_labels_path(:json) dashboard_labels_path(:json)
......
...@@ -151,16 +151,17 @@ module NotesHelper ...@@ -151,16 +151,17 @@ module NotesHelper
} }
end end
def notes_data(issuable) def discussions_path(issuable)
discussions_path =
if issuable.is_a?(Issue) if issuable.is_a?(Issue)
discussions_project_issue_path(@project, issuable, format: :json) discussions_project_issue_path(@project, issuable, format: :json)
else else
discussions_project_merge_request_path(@project, issuable, format: :json) discussions_project_merge_request_path(@project, issuable, format: :json)
end end
end
def notes_data(issuable)
{ {
discussionsPath: discussions_path, discussionsPath: discussions_path(issuable),
registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'),
newSessionPath: new_session_path(:user, redirect_to_referer: 'yes'), newSessionPath: new_session_path(:user, redirect_to_referer: 'yes'),
markdownDocsPath: help_page_path('user/markdown'), markdownDocsPath: help_page_path('user/markdown'),
...@@ -170,7 +171,6 @@ module NotesHelper ...@@ -170,7 +171,6 @@ module NotesHelper
notesPath: notes_url, notesPath: notes_url,
totalNotes: issuable.discussions.length, totalNotes: issuable.discussions.length,
lastFetchedAt: Time.now.to_i lastFetchedAt: Time.now.to_i
}.to_json }.to_json
end end
......
...@@ -123,7 +123,7 @@ module TreeHelper ...@@ -123,7 +123,7 @@ module TreeHelper
# returns the relative path of the first subdir that doesn't have only one directory descendant # returns the relative path of the first subdir that doesn't have only one directory descendant
def flatten_tree(root_path, tree) def flatten_tree(root_path, tree)
return tree.flat_path.sub(%r{\A#{root_path}/}, '') if tree.flat_path.present? return tree.flat_path.sub(%r{\A#{Regexp.escape(root_path)}/}, '') if tree.flat_path.present?
subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path) subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path)
if subtree.count == 1 && subtree.first.dir? if subtree.count == 1 && subtree.first.dir?
......
...@@ -90,6 +90,7 @@ module Ci ...@@ -90,6 +90,7 @@ module Ci
before_save :ensure_token before_save :ensure_token
before_destroy { unscoped_project } before_destroy { unscoped_project }
before_create :ensure_metadata
after_create unless: :importing? do |build| after_create unless: :importing? do |build|
run_after_commit { BuildHooksWorker.perform_async(build.id) } run_after_commit { BuildHooksWorker.perform_async(build.id) }
end end
......
...@@ -32,7 +32,8 @@ class Commit ...@@ -32,7 +32,8 @@ class Commit
COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze
def banzai_render_context(field) def banzai_render_context(field)
context = { pipeline: :single_line, project: self.project } pipeline = field == :description ? :commit_description : :single_line
context = { pipeline: pipeline, project: self.project }
context[:author] = self.author if self.author context[:author] = self.author if self.author
context context
......
...@@ -141,7 +141,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -141,7 +141,7 @@ class CommitStatus < ActiveRecord::Base
end end
def group_name def group_name
name.to_s.gsub(%r{\d+[\.\s:/\\]+\d+\s*}, '').strip name.to_s.gsub(%r{\d+[\s:/\\]+\d+\s*}, '').strip
end end
def failed_but_allowed? def failed_but_allowed?
......
...@@ -137,7 +137,7 @@ module Issuable ...@@ -137,7 +137,7 @@ module Issuable
fuzzy_search(query, [:title, :description]) fuzzy_search(query, [:title, :description])
end end
def sort(method, excluded_labels: []) def sort_by_attribute(method, excluded_labels: [])
sorted = sorted =
case method.to_s case method.to_s
when 'downvotes_desc' then order_downvotes_desc when 'downvotes_desc' then order_downvotes_desc
......
...@@ -45,11 +45,11 @@ module Milestoneish ...@@ -45,11 +45,11 @@ module Milestoneish
end end
def sorted_issues(user) def sorted_issues(user)
issues_visible_to_user(user).preload_associations.sort('label_priority') issues_visible_to_user(user).preload_associations.sort_by_attribute('label_priority')
end end
def sorted_merge_requests def sorted_merge_requests
merge_requests.sort('label_priority') merge_requests.sort_by_attribute('label_priority')
end end
def upcoming? def upcoming?
......
...@@ -53,7 +53,7 @@ class Group < Namespace ...@@ -53,7 +53,7 @@ class Group < Namespace
Gitlab::Database.postgresql? Gitlab::Database.postgresql?
end end
def sort(method) def sort_by_attribute(method)
if method == 'storage_size_desc' if method == 'storage_size_desc'
# storage_size is a virtual column so we need to # storage_size is a virtual column so we need to
# pass a string to avoid AR adding the table name # pass a string to avoid AR adding the table name
......
...@@ -116,7 +116,7 @@ class Issue < ActiveRecord::Base ...@@ -116,7 +116,7 @@ class Issue < ActiveRecord::Base
'project_id' 'project_id'
end end
def self.sort(method, excluded_labels: []) def self.sort_by_attribute(method, excluded_labels: [])
case method.to_s case method.to_s
when 'due_date' then order_due_date_asc when 'due_date' then order_due_date_asc
when 'due_date_asc' then order_due_date_asc when 'due_date_asc' then order_due_date_asc
......
...@@ -96,7 +96,7 @@ class Member < ActiveRecord::Base ...@@ -96,7 +96,7 @@ class Member < ActiveRecord::Base
joins(:user).merge(User.search(query)) joins(:user).merge(User.search(query))
end end
def sort(method) def sort_by_attribute(method)
case method.to_s case method.to_s
when 'access_level_asc' then reorder(access_level: :asc) when 'access_level_asc' then reorder(access_level: :asc)
when 'access_level_desc' then reorder(access_level: :desc) when 'access_level_desc' then reorder(access_level: :desc)
......
...@@ -138,7 +138,7 @@ class Milestone < ActiveRecord::Base ...@@ -138,7 +138,7 @@ class Milestone < ActiveRecord::Base
User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq
end end
def self.sort(method) def self.sort_by_attribute(method)
case method.to_s case method.to_s
when 'due_date_asc' when 'due_date_asc'
reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC'))
......
...@@ -379,12 +379,15 @@ class Note < ActiveRecord::Base ...@@ -379,12 +379,15 @@ class Note < ActiveRecord::Base
def expire_etag_cache def expire_etag_cache
return unless noteable&.discussions_rendered_on_frontend? return unless noteable&.discussions_rendered_on_frontend?
key = Gitlab::Routing.url_helpers.project_noteable_notes_path( Gitlab::EtagCaching::Store.new.touch(etag_key)
end
def etag_key
Gitlab::Routing.url_helpers.project_noteable_notes_path(
project, project,
target_type: noteable_type.underscore, target_type: noteable_type.underscore,
target_id: noteable_id target_id: noteable_id
) )
Gitlab::EtagCaching::Store.new.touch(key)
end end
def touch(*args) def touch(*args)
......
...@@ -436,7 +436,7 @@ class Project < ActiveRecord::Base ...@@ -436,7 +436,7 @@ class Project < ActiveRecord::Base
Gitlab::VisibilityLevel.options Gitlab::VisibilityLevel.options
end end
def sort(method) def sort_by_attribute(method)
case method.to_s case method.to_s
when 'storage_size_desc' when 'storage_size_desc'
# storage_size is a joined column so we need to # storage_size is a joined column so we need to
...@@ -566,9 +566,7 @@ class Project < ActiveRecord::Base ...@@ -566,9 +566,7 @@ class Project < ActiveRecord::Base
def add_import_job def add_import_job
job_id = job_id =
if forked? if forked?
RepositoryForkWorker.perform_async(id, RepositoryForkWorker.perform_async(id)
forked_from_project.repository_storage_path,
forked_from_project.disk_path)
elsif gitlab_project_import? elsif gitlab_project_import?
# Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-ce/issues/26189 is solved. # Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-ce/issues/26189 is solved.
RepositoryImportWorker.set(retry: false).perform_async(self.id) RepositoryImportWorker.set(retry: false).perform_async(self.id)
......
...@@ -50,7 +50,7 @@ class Todo < ActiveRecord::Base ...@@ -50,7 +50,7 @@ class Todo < ActiveRecord::Base
# Priority sorting isn't displayed in the dropdown, because we don't show # Priority sorting isn't displayed in the dropdown, because we don't show
# milestones, but still show something if the user has a URL with that # milestones, but still show something if the user has a URL with that
# selected. # selected.
def sort(method) def sort_by_attribute(method)
sorted = sorted =
case method.to_s case method.to_s
when 'priority', 'label_priority' then order_by_labels_priority when 'priority', 'label_priority' then order_by_labels_priority
......
...@@ -256,7 +256,7 @@ class User < ActiveRecord::Base ...@@ -256,7 +256,7 @@ class User < ActiveRecord::Base
end end
end end
def sort(method) def sort_by_attribute(method)
order_method = method || 'id_desc' order_method = method || 'id_desc'
case order_method.to_s case order_method.to_s
......
class BuildMetadataEntity < Grape::Entity class BuildMetadataEntity < Grape::Entity
expose :timeout_human_readable do |metadata| expose :timeout_human_readable
metadata.timeout_human_readable unless metadata.timeout.nil?
end
expose :timeout_source do |metadata| expose :timeout_source do |metadata|
metadata.present.timeout_source metadata.present.timeout_source
end end
......
...@@ -4,7 +4,9 @@ class DiscussionEntity < Grape::Entity ...@@ -4,7 +4,9 @@ class DiscussionEntity < Grape::Entity
expose :id, :reply_id expose :id, :reply_id
expose :expanded?, as: :expanded expose :expanded?, as: :expanded
expose :notes, using: NoteEntity expose :notes do |discussion, opts|
request.note_entity.represent(discussion.notes, opts)
end
expose :individual_note?, as: :individual_note expose :individual_note?, as: :individual_note
expose :resolvable?, as: :resolvable expose :resolvable?, as: :resolvable
...@@ -12,7 +14,7 @@ class DiscussionEntity < Grape::Entity ...@@ -12,7 +14,7 @@ class DiscussionEntity < Grape::Entity
expose :resolve_path, if: -> (d, _) { d.resolvable? } do |discussion| expose :resolve_path, if: -> (d, _) { d.resolvable? } do |discussion|
resolve_project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion.id) resolve_project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion.id)
end end
expose :resolve_with_issue_path do |discussion| expose :resolve_with_issue_path, if: -> (d, _) { d.resolvable? } do |discussion|
new_project_issue_path(discussion.project, merge_request_to_resolve_discussions_of: discussion.noteable.iid, discussion_to_resolve: discussion.id) new_project_issue_path(discussion.project, merge_request_to_resolve_discussions_of: discussion.noteable.iid, discussion_to_resolve: discussion.id)
end end
......
...@@ -5,10 +5,6 @@ class NoteEntity < API::Entities::Note ...@@ -5,10 +5,6 @@ class NoteEntity < API::Entities::Note
expose :author, using: NoteUserEntity expose :author, using: NoteUserEntity
expose :human_access do |note|
note.project.team.human_max_access(note.author_id)
end
unexpose :note, as: :body unexpose :note, as: :body
expose :note expose :note
...@@ -37,36 +33,10 @@ class NoteEntity < API::Entities::Note ...@@ -37,36 +33,10 @@ class NoteEntity < API::Entities::Note
expose :emoji_awardable?, as: :emoji_awardable expose :emoji_awardable?, as: :emoji_awardable
expose :award_emoji, if: -> (note, _) { note.emoji_awardable? }, using: AwardEmojiEntity expose :award_emoji, if: -> (note, _) { note.emoji_awardable? }, using: AwardEmojiEntity
expose :toggle_award_path, if: -> (note, _) { note.emoji_awardable? } do |note|
if note.for_personal_snippet?
toggle_award_emoji_snippet_note_path(note.noteable, note)
else
toggle_award_emoji_project_note_path(note.project, note.id)
end
end
expose :report_abuse_path do |note| expose :report_abuse_path do |note|
new_abuse_report_path(user_id: note.author.id, ref_url: Gitlab::UrlBuilder.build(note)) new_abuse_report_path(user_id: note.author.id, ref_url: Gitlab::UrlBuilder.build(note))
end end
expose :path do |note|
if note.for_personal_snippet?
snippet_note_path(note.noteable, note)
else
project_note_path(note.project, note)
end
end
expose :resolve_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
resolve_project_merge_request_discussion_path(note.project, note.noteable, note.discussion_id)
end
expose :resolve_with_issue_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
new_project_issue_path(note.project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id)
end
expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? } expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? }
expose :delete_attachment_path, if: -> (note, _) { note.attachment? } do |note|
delete_attachment_project_note_path(note.project, note)
end
end end
class NoteSerializer < BaseSerializer
entity NoteEntity
end
class ProjectNoteEntity < NoteEntity
expose :human_access do |note|
note.project.team.human_max_access(note.author_id)
end
expose :toggle_award_path, if: -> (note, _) { note.emoji_awardable? } do |note|
toggle_award_emoji_project_note_path(note.project, note.id)
end
expose :path do |note|
project_note_path(note.project, note)
end
expose :resolve_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
resolve_project_merge_request_discussion_path(note.project, note.noteable, note.discussion_id)
end
expose :resolve_with_issue_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
new_project_issue_path(note.project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id)
end
expose :delete_attachment_path, if: -> (note, _) { note.attachment? } do |note|
delete_attachment_project_note_path(note.project, note)
end
end
class ProjectNoteSerializer < BaseSerializer
entity ProjectNoteEntity
end
...@@ -12,11 +12,15 @@ module Boards ...@@ -12,11 +12,15 @@ module Boards
private private
def available_labels_for(board) def available_labels_for(board)
options = { include_ancestor_groups: true }
if board.group_board? if board.group_board?
parent.labels options.merge!(group_id: parent.id, only_group_labels: true)
else else
LabelsFinder.new(current_user, project_id: parent.id).execute options[:project_id] = parent.id
end end
LabelsFinder.new(current_user, options).execute
end end
def next_position(board) def next_position(board)
......
...@@ -106,7 +106,7 @@ class IssuableBaseService < BaseService ...@@ -106,7 +106,7 @@ class IssuableBaseService < BaseService
end end
def available_labels def available_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: true).execute
end end
def handle_quick_actions_on_create(issuable) def handle_quick_actions_on_create(issuable)
......
...@@ -21,7 +21,8 @@ module Projects ...@@ -21,7 +21,8 @@ module Projects
end end
def labels(target = nil) def labels(target = nil)
labels = LabelsFinder.new(current_user, project_id: project.id).execute.select([:color, :title]) labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true)
.execute.select([:color, :title])
return labels unless target&.respond_to?(:labels) return labels unless target&.respond_to?(:labels)
......
...@@ -61,7 +61,7 @@ module Projects ...@@ -61,7 +61,7 @@ module Projects
project.ensure_repository project.ensure_repository
project.repository.fetch_as_mirror(project.import_url, refmap: refmap) project.repository.fetch_as_mirror(project.import_url, refmap: refmap)
else else
gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, project.import_url) gitlab_shell.import_repository(project.repository_storage, project.disk_path, project.import_url)
end end
rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e
# Expire cache to prevent scenarios such as: # Expire cache to prevent scenarios such as:
......
...@@ -200,7 +200,7 @@ module QuickActions ...@@ -200,7 +200,7 @@ module QuickActions
end end
params '~label1 ~"label 2"' params '~label1 ~"label 2"'
condition do condition do
available_labels = LabelsFinder.new(current_user, project_id: project.id).execute available_labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true).execute
current_user.can?(:"admin_#{issuable.to_ability_name}", project) && current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
available_labels.any? available_labels.any?
...@@ -562,7 +562,7 @@ module QuickActions ...@@ -562,7 +562,7 @@ module QuickActions
def find_labels(labels_param) def find_labels(labels_param)
extract_references(labels_param, :label) | extract_references(labels_param, :label) |
LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split, include_ancestor_groups: true).execute
end end
def find_label_references(labels_param) def find_label_references(labels_param)
...@@ -593,6 +593,7 @@ module QuickActions ...@@ -593,6 +593,7 @@ module QuickActions
def extract_references(arg, type) def extract_references(arg, type)
ext = Gitlab::ReferenceExtractor.new(project, current_user) ext = Gitlab::ReferenceExtractor.new(project, current_user)
ext.analyze(arg, author: current_user) ext.analyze(arg, author: current_user)
ext.references(type) ext.references(type)
......
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
= f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :admin_notification_email, class: 'form-control'
.help-block
Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :email_author_in_body do
= f.check_box :email_author_in_body
Include author name in notification email body
.help-block
Some email servers do not support overriding the email sender name.
Enable this option to include the name of the author of the issue,
merge request or comment in the email body instead.
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :html_emails_enabled do
= f.check_box :html_emails_enabled
Enable HTML emails
.help-block
By default GitLab sends emails in HTML and plain text formats so mail
clients can choose what format to use. Disable this option if you only
want to send emails in plain text format.
= f.submit 'Save changes', class: "btn btn-success"
This diff is collapsed.
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
= f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :gitaly_timeout_default, class: 'form-control'
.help-block
Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
for git fetch/push operations or Sidekiq jobs.
.form-group
= f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :gitaly_timeout_fast, class: 'form-control'
.help-block
Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
can help maintain the stability of the GitLab instance.
.form-group
= f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :gitaly_timeout_medium, class: 'form-control'
.help-block
Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :throttle_unauthenticated_enabled do
= f.check_box :throttle_unauthenticated_enabled
Enable unauthenticated request rate limit
%span.help-block
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
= f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control'
.form-group
= f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control'
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :throttle_authenticated_api_enabled do
= f.check_box :throttle_authenticated_api_enabled
Enable authenticated API request rate limit
%span.help-block
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
= f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control'
.form-group
= f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control'
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :throttle_authenticated_web_enabled do
= f.check_box :throttle_authenticated_web_enabled
Enable authenticated web request rate limit
%span.help-block
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
= f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control'
.form-group
= f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :koding_enabled do
= f.check_box :koding_enabled
Enable Koding
.help-block
Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again.
.form-group
= f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
.help-block
Koding has integration enabled out of the box for the
%strong gitlab
team, and you need to provide that team's URL here. Learn more in the
= succeed "." do
= link_to "Koding administration documentation", help_page_path("administration/integration/koding")
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :sentry_enabled do
= f.check_box :sentry_enabled
Enable Sentry
.help-block
%p This setting requires a restart to take effect.
Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
%a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com
.form-group
= f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :sentry_dsn, class: 'form-control'
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :clientside_sentry_enabled do
= f.check_box :clientside_sentry_enabled
Enable Clientside Sentry
.help-block
Sentry can also be used for reporting and logging clientside exceptions.
%a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/
.form-group
= f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :clientside_sentry_dsn, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :allow_local_requests_from_hooks_and_services do
= f.check_box :allow_local_requests_from_hooks_and_services
Allow requests to the local network from hooks and services
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :authorized_keys_enabled do
= f.check_box :authorized_keys_enabled
Write to "authorized_keys" file
.help-block
By default, we write to the "authorized_keys" file to support Git
over SSH without additional configuration. GitLab can be optimized
to authenticate SSH keys via the database file. Only uncheck this
if you have configured your OpenSSH server to use the
AuthorizedKeysCommand. Click on the help icon for more details.
= link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup')
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :plantuml_enabled do
= f.check_box :plantuml_enabled
Enable PlantUML
.form-group
= f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
.help-block
Allow rendering of
= link_to "PlantUML", "http://plantuml.com"
diagrams in Asciidoc documents using an external PlantUML service.
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
= f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :polling_interval_multiplier, class: 'form-control'
.help-block
Change this value to influence how frequently the GitLab UI polls for updates.
If you set the value to 2 all polling intervals are multiplied
by 2, which means that polling happens half as frequently.
The multiplier can also have a decimal value.
The default value (1) is a reasonable choice for the majority of GitLab
installations. Set to 0 to completely disable polling.
= link_to icon('question-circle'), help_page_path('administration/polling')
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
= f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :container_registry_token_expire_delay, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.sub-section
%h4 Repository checks
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :repository_checks_enabled do
= f.check_box :repository_checks_enabled
Enable Repository Checks
.help-block
GitLab will periodically run
%a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck'
in all project and wiki repositories to look for silent disk corruption issues.
.form-group
.col-sm-offset-2.col-sm-10
= link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove"
.help-block
If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
.sub-section
%h4 Housekeeping
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :housekeeping_enabled do
= f.check_box :housekeeping_enabled
Enable automatic repository housekeeping (git repack, git gc)
.help-block
If you keep automatic housekeeping disabled for a long time Git
repository access on your GitLab server will become slower and your
repositories will use more disk space. We recommend to always leave
this enabled.
.checkbox
= f.label :housekeeping_bitmaps_enabled do
= f.check_box :housekeeping_bitmaps_enabled
Enable Git pack file bitmap creation
.help-block
Creating pack file bitmaps makes housekeeping take a little longer but
bitmaps should accelerate 'git clone' performance.
.form-group
= f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :housekeeping_incremental_repack_period, class: 'form-control'
.help-block
Number of Git pushes after which an incremental 'git repack' is run.
.form-group
= f.label :housekeeping_full_repack_period, 'Full repack period', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :housekeeping_full_repack_period, class: 'form-control'
.help-block
Number of Git pushes after which a full 'git repack' is run.
.form-group
= f.label :housekeeping_gc_period, 'Git GC period', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :housekeeping_gc_period, class: 'form-control'
.help-block
Number of Git pushes after which 'git gc' is run.
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.sub-section
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :hashed_storage_enabled do
= f.check_box :hashed_storage_enabled
Create new projects using hashed storage paths
.help-block
Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents
repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance.
%em (EXPERIMENTAL)
.form-group
= f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2'
.col-sm-10
= f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages),
{include_hidden: false}, multiple: true, class: 'form-control'
.help-block
Manage repository storage paths. Learn more in the
= succeed "." do
= link_to "repository storages documentation", help_page_path("administration/repository_storages")
.sub-section
%h4 Circuit breaker
.form-group
= f.label :circuitbreaker_check_interval, _('Check interval'), class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_check_interval, class: 'form-control'
.help-block
= circuitbreaker_check_interval_help_text
.form-group
= f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_access_retries, class: 'form-control'
.help-block
= circuitbreaker_access_retries_help_text
.form-group
= f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_storage_timeout, class: 'form-control'
.help-block
= circuitbreaker_storage_timeout_help_text
.form-group
= f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control'
.help-block
= circuitbreaker_failure_count_help_text
.form-group
= f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_failure_reset_time, class: 'form-control'
.help-block
= circuitbreaker_failure_reset_time_help_text
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
= f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :terminal_max_session_time, class: 'form-control'
.help-block
Maximum time for web terminal websocket connection (in seconds).
0 for unlimited.
= f.submit 'Save changes', class: "btn btn-success"
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :version_check_enabled do
= f.check_box :version_check_enabled
Enable version check
.help-block
GitLab will inform you if a new version is available.
= link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")
about what information is shared with GitLab Inc.
.form-group
.col-sm-offset-2.col-sm-10
- can_be_configured = @application_setting.usage_ping_can_be_configured?
.checkbox
= f.label :usage_ping_enabled do
= f.check_box :usage_ping_enabled, disabled: !can_be_configured
Enable usage ping
.help-block
- if can_be_configured
To help improve GitLab and its user experience, GitLab will
periodically collect usage information.
= link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
about what information is shared with GitLab Inc. Visit
= link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping')
to see the JSON payload sent.
- else
The usage ping is disabled, and cannot be configured through this
form. For more information, see the documentation on
= succeed '.' do
= link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
= f.submit 'Save changes', class: "btn btn-success"
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
%button.btn.js-settings-toggle{ type: 'button' } %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
= _('Auto DevOps, runners amd job artifacts') = _('Auto DevOps, runners and job artifacts')
.settings-content .settings-content
= render 'ci_cd' = render 'ci_cd'
...@@ -102,7 +102,7 @@ ...@@ -102,7 +102,7 @@
.settings-content .settings-content
= render 'prometheus' = render 'prometheus'
%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded) } %section.settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
= _('Profiling - Performance bar') = _('Profiling - Performance bar')
...@@ -136,5 +136,169 @@ ...@@ -136,5 +136,169 @@
.settings-content .settings-content
= render 'spam' = render 'spam'
.prepend-top-20 %section.settings.as-abuse.no-animate#js-abuse-settings{ class: ('expanded' if expanded) }
= render 'form' .settings-header
%h4
= _('Abuse reports')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Set notification email for abuse reports.')
.settings-content
= render 'abuse'
%section.settings.as-logging.no-animate#js-logging-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Error Reporting and Logging')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Enable Sentry for error reporting and logging.')
.settings-content
= render 'logging'
%section.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Repository storage')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Configure storage path and circuit breaker settings.')
.settings-content
= render 'repository_storage'
%section.settings.as-repository-check.no-animate#js-repository-check-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Repository maintenance')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Configure automatic git checks and housekeeping on repositories.')
.settings-content
= render 'repository_check'
- if Gitlab.config.registry.enabled
%section.settings.as-registry.no-animate#js-registry-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Container Registry')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Various container registry settings.')
.settings-content
= render 'registry'
- if koding_enabled?
%section.settings.as-koding.no-animate#js-koding-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Koding')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Online IDE integration settings.')
.settings-content
= render 'koding'
%section.settings.as-plantuml.no-animate#js-plantuml-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('PlantUML')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Allow rendering of PlantUML diagrams in Asciidoc documents.')
.settings-content
= render 'plantuml'
%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded) }
.settings-header#usage-statistics
%h4
= _('Usage statistics')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Enable or disable version check and usage ping.')
.settings-content
= render 'usage'
%section.settings.as-email.no-animate#js-email-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Email')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Various email settings.')
.settings-content
= render 'email'
%section.settings.as-gitaly.no-animate#js-gitaly-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Gitaly')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Configure Gitaly timeouts.')
.settings-content
= render 'gitaly'
%section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Web terminal')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Set max session time for web terminal.')
.settings-content
= render 'terminal'
%section.settings.as-realtime.no-animate#js-realtime-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Real-time features')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Change this value to influence how frequently the GitLab UI polls for updates.')
.settings-content
= render 'realtime'
%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Performance optimization')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Various settings that affect GitLab performance.')
.settings-content
= render 'performance'
%section.settings.as-ip-limits.no-animate#js-ip-limits-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('User and IP Rate Limits')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Configure limits for web and API requests.')
.settings-content
= render 'ip_limits'
%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Outbound requests')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Allow requests to the local network from hooks and services.')
.settings-content
= render 'outbound'
...@@ -49,10 +49,10 @@ ...@@ -49,10 +49,10 @@
.commit-box{ data: { project_path: project_path(@project) } } .commit-box{ data: { project_path: project_path(@project) } }
%h3.commit-title %h3.commit-title
= markdown(@commit.title, pipeline: :single_line, author: @commit.author) = markdown_field(@commit, :title)
- if @commit.description.present? - if @commit.description.present?
%pre.commit-description %pre.commit-description
= preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author)) = preserve(markdown_field(@commit, :description))
.info-well .info-well
.well-segment.branch-info .well-segment.branch-info
......
...@@ -10,5 +10,5 @@ xml.entry do ...@@ -10,5 +10,5 @@ xml.entry do
xml.email commit.author_email xml.email commit.author_email
end end
xml.summary markdown(commit.description, pipeline: :single_line), type: 'html' xml.summary markdown_field(commit, :description), type: 'html'
end end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- if @protected_branches.empty? - if @protected_branches.empty?
.panel-heading .panel-heading
%h3.panel-title %h3.panel-title
Protected branch (#{@protected_branches.size}) Protected branch (#{@protected_branches_count})
%p.settings-message.text-center %p.settings-message.text-center
There are currently no protected branches, protect a branch with the form above. There are currently no protected branches, protect a branch with the form above.
- else - else
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
%col %col
%thead %thead
%tr %tr
%th Protected branch (#{@protected_branches.size}) %th Protected branch (#{@protected_branches_count})
%th Last commit %th Last commit
%th Allowed to merge %th Allowed to merge
%th Allowed to push %th Allowed to push
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- if @protected_tags.empty? - if @protected_tags.empty?
.panel-heading .panel-heading
%h3.panel-title %h3.panel-title
Protected tag (#{@protected_tags.size}) Protected tag (#{@protected_tags_count})
%p.settings-message.text-center %p.settings-message.text-center
There are currently no protected tags, protect a tag with the form above. There are currently no protected tags, protect a tag with the form above.
- else - else
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
%col %col
%thead %thead
%tr %tr
%th Protected tag (#{@protected_tags.size}) %th Protected tag (#{@protected_tags_count})
%th Last commit %th Last commit
%th Allowed to create %th Allowed to create
- if can_admin_project - if can_admin_project
......
...@@ -107,7 +107,7 @@ ...@@ -107,7 +107,7 @@
- selected_labels.each do |label| - selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown .dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (project_labels_path(@project, :json) if @project) } } %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path(false) if @project) } }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels") = multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true') = icon('chevron-down', 'aria-hidden': 'true')
......
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.
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