diff options
Diffstat (limited to 'app')
100 files changed, 1367 insertions, 1111 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 76b724e1bcb..56f91e95bb9 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -97,7 +97,6 @@ const Api = { }, commitMultiple(id, data, callback) { - // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions const url = Api.buildUrl(Api.commitPath) .replace(':id', id); return $.ajax({ diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index cbc3ad23990..32cb42c8b10 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -15,6 +15,10 @@ export const setOpenMenu = (menu = null) => { currentOpenMenu = menu; }; export const slope = (a, b) => (b.y - a.y) / (b.x - a.x); +let headerHeight = 50; + +export const getHeaderHeight = () => headerHeight; + export const canShowActiveSubItems = (el) => { const isHiddenByMedia = bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md'; @@ -74,7 +78,7 @@ export const moveSubItemsToPosition = (el, subItems) => { const isAbove = top < boundingRect.top; subItems.classList.add('fly-out-list'); - subItems.style.transform = `translate3d(0, ${Math.floor(top)}px, 0)`; // eslint-disable-line no-param-reassign + subItems.style.transform = `translate3d(0, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign const subItemsRect = subItems.getBoundingClientRect(); @@ -153,6 +157,8 @@ export default () => { }, getHideSubItemsInterval()); }); + headerHeight = document.querySelector('.nav-sidebar').offsetTop; + items.forEach((el) => { const subItems = el.querySelector('.sidebar-sub-level-items'); diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js index 1c379e9bb67..7ac9dcd1112 100644 --- a/app/assets/javascripts/gpg_badges.js +++ b/app/assets/javascripts/gpg_badges.js @@ -1,12 +1,14 @@ export default class GpgBadges { static fetch() { + const badges = $('.js-loading-gpg-badge'); const form = $('.commits-search-form'); + badges.html('<i class="fa fa-spinner fa-spin"></i>'); + $.get({ url: form.data('signatures-path'), data: form.serialize(), }).done((response) => { - const badges = $('.js-loading-gpg-badge'); response.signatures.forEach((signature) => { badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); }); diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js deleted file mode 100644 index c827b7402dc..00000000000 --- a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js +++ /dev/null @@ -1,50 +0,0 @@ -import Vue from 'vue'; -import Cookies from 'js-cookie'; -import Translate from '../../vue_shared/translate'; -import illustrationSvg from '../icons/intro_illustration.svg'; - -Vue.use(Translate); - -const cookieKey = 'pipeline_schedules_callout_dismissed'; - -export default { - name: 'PipelineSchedulesCallout', - data() { - return { - docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl, - illustrationSvg, - calloutDismissed: Cookies.get(cookieKey) === 'true', - }; - }, - methods: { - dismissCallout() { - this.calloutDismissed = true; - Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 }); - }, - }, - template: ` - <div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout"> - <div class="bordered-box landing content-block"> - <button - id="dismiss-callout-btn" - class="btn btn-default close" - @click="dismissCallout"> - <i class="fa fa-times"></i> - </button> - <div class="svg-container" v-html="illustrationSvg"></div> - <div class="user-callout-copy"> - <h4>{{ __('Scheduling Pipelines') }}</h4> - <p> - {{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }} - </p> - <p> {{ __('Learn more in the') }} - <a - :href="docsUrl" - target="_blank" - rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period --> - </p> - </div> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue new file mode 100644 index 00000000000..6e0bc2d697a --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue @@ -0,0 +1,59 @@ +<script> + import Vue from 'vue'; + import Cookies from 'js-cookie'; + import Translate from '../../vue_shared/translate'; + import illustrationSvg from '../icons/intro_illustration.svg'; + + Vue.use(Translate); + + const cookieKey = 'pipeline_schedules_callout_dismissed'; + + export default { + name: 'PipelineSchedulesCallout', + data() { + return { + docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl, + calloutDismissed: Cookies.get(cookieKey) === 'true', + }; + }, + methods: { + dismissCallout() { + this.calloutDismissed = true; + Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 }); + }, + }, + created() { + this.illustrationSvg = illustrationSvg; + }, + }; +</script> +<template> + <div + v-if="!calloutDismissed" + class="pipeline-schedules-user-callout user-callout"> + <div class="bordered-box landing content-block"> + <button + id="dismiss-callout-btn" + class="btn btn-default close" + @click="dismissCallout"> + <i + aria-hidden="true" + class="fa fa-times"> + </i> + </button> + <div class="svg-container" v-html="illustrationSvg"></div> + <div class="user-callout-copy"> + <h4>{{ __('Scheduling Pipelines') }}</h4> + <p> + {{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }} + </p> + <p> {{ __('Learn more in the') }} + <a + :href="docsUrl" + target="_blank" + rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period --> + </p> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js index 6584549ad06..a6c945e22b0 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js +++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import PipelineSchedulesCallout from './components/pipeline_schedules_callout'; +import PipelineSchedulesCallout from './components/pipeline_schedules_callout.vue'; document.addEventListener('DOMContentLoaded', () => new Vue({ el: '#pipeline-schedules-callout', diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 1c2100a1c25..d7e3ab42f00 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -126,11 +126,11 @@ import Cookies from 'js-cookie'; var $form = $dropdown.closest('form'); var $visit = $dropdown.data('visit'); - var shouldVisit = typeof $visit === 'undefined' ? true : $visit; + var shouldVisit = $visit ? true : $visit; var action = $form.attr('action'); var divider = action.indexOf('?') === -1 ? '?' : '&'; if (shouldVisit) { - gl.utils.visitUrl(action + '' + divider + '' + $form.serialize()); + gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`); } } } diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue index 703da749ad3..3d5e01c8ec0 100644 --- a/app/assets/javascripts/repo/components/repo.vue +++ b/app/assets/javascripts/repo/components/repo.vue @@ -14,13 +14,13 @@ export default { data: () => Store, mixins: [RepoMixin], components: { - 'repo-sidebar': RepoSidebar, - 'repo-tabs': RepoTabs, - 'repo-file-buttons': RepoFileButtons, + RepoSidebar, + RepoTabs, + RepoFileButtons, 'repo-editor': MonacoLoaderHelper.repoEditorLoader, - 'repo-commit-section': RepoCommitSection, - 'popup-dialog': PopupDialog, - 'repo-preview': RepoPreview, + RepoCommitSection, + PopupDialog, + RepoPreview, }, mounted() { @@ -28,12 +28,12 @@ export default { }, methods: { - dialogToggled(toggle) { + toggleDialogOpen(toggle) { this.dialog.open = toggle; }, dialogSubmitted(status) { - this.dialog.open = false; + this.toggleDialogOpen(false); this.dialog.status = status; }, @@ -43,21 +43,25 @@ export default { </script> <template> -<div class="repository-view tree-content-holder"> - <repo-sidebar/><div class="panel-right" :class="{'edit-mode': editMode}"> - <repo-tabs/> - <component :is="currentBlobView" class="blob-viewer-container"></component> - <repo-file-buttons/> + <div class="repository-view tree-content-holder"> + <repo-sidebar/><div v-if="isMini" + class="panel-right" + :class="{'edit-mode': editMode}"> + <repo-tabs/> + <component + :is="currentBlobView" + class="blob-viewer-container"/> + <repo-file-buttons/> + </div> + <repo-commit-section/> + <popup-dialog + v-show="dialog.open" + :primary-button-label="__('Discard changes')" + kind="warning" + :title="__('Are you sure?')" + :body="__('Are you sure you want to discard your changes?')" + @toggle="toggleDialogOpen" + @submit="dialogSubmitted" + /> </div> - <repo-commit-section/> - <popup-dialog - :primary-button-label="__('Discard changes')" - :open="dialog.open" - kind="warning" - :title="__('Are you sure?')" - :body="__('Are you sure you want to discard your changes?')" - @toggle="dialogToggled" - @submit="dialogSubmitted" - /> -</div> </template> diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue index bd83f80c928..5ec4a9b6593 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -2,18 +2,20 @@ /* global Flash */ import Store from '../stores/repo_store'; import RepoMixin from '../mixins/repo_mixin'; -import Helper from '../helpers/repo_helper'; import Service from '../services/repo_service'; -const RepoCommitSection = { +export default { data: () => Store, mixins: [RepoMixin], computed: { + showCommitable() { + return this.isCommitable && this.changedFiles.length; + }, + branchPaths() { - const branch = Helper.getBranch(); - return this.changedFiles.map(f => Helper.getFilePathFromFullPath(f.url, branch)); + return this.changedFiles.map(f => f.path); }, cantCommitYet() { @@ -28,11 +30,10 @@ const RepoCommitSection = { methods: { makeCommit() { // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions - const branch = Helper.getBranch(); const commitMessage = this.commitMessage; const actions = this.changedFiles.map(f => ({ action: 'update', - file_path: Helper.getFilePathFromFullPath(f.url, branch), + file_path: f.path, content: f.newContent, })); const payload = { @@ -47,51 +48,80 @@ const RepoCommitSection = { resetCommitState() { this.submitCommitsLoading = false; this.changedFiles = []; - this.openedFiles = []; this.commitMessage = ''; this.editMode = false; - $('html, body').animate({ scrollTop: 0 }, 'fast'); + window.scrollTo(0, 0); }, }, }; - -export default RepoCommitSection; </script> <template> -<div id="commit-area" v-if="isCommitable && changedFiles.length" > - <form class="form-horizontal"> +<div + v-if="showCommitable" + id="commit-area"> + <form + class="form-horizontal" + @submit.prevent="makeCommit"> <fieldset> <div class="form-group"> - <label class="col-md-4 control-label staged-files">Staged files ({{changedFiles.length}})</label> - <div class="col-md-4"> + <label class="col-md-4 control-label staged-files"> + Staged files ({{changedFiles.length}}) + </label> + <div class="col-md-6"> <ul class="list-unstyled changed-files"> - <li v-for="file in branchPaths" :key="file.id"> - <span class="help-block">{{file}}</span> + <li + v-for="branchPath in branchPaths" + :key="branchPath"> + <span class="help-block"> + {{branchPath}} + </span> </li> </ul> </div> </div> - <!-- Textarea - --> <div class="form-group"> - <label class="col-md-4 control-label" for="commit-message">Commit message</label> - <div class="col-md-4"> - <textarea class="form-control" id="commit-message" name="commit-message" v-model="commitMessage"></textarea> + <label + class="col-md-4 control-label" + for="commit-message"> + Commit message + </label> + <div class="col-md-6"> + <textarea + id="commit-message" + class="form-control" + name="commit-message" + v-model="commitMessage"> + </textarea> </div> </div> - <!-- Button Drop Down - --> <div class="form-group target-branch"> - <label class="col-md-4 control-label" for="target-branch">Target branch</label> - <div class="col-md-4"> - <span class="help-block">{{targetBranch}}</span> + <label + class="col-md-4 control-label" + for="target-branch"> + Target branch + </label> + <div class="col-md-6"> + <span class="help-block"> + {{targetBranch}} + </span> </div> </div> - <div class="col-md-offset-4 col-md-4"> - <button type="submit" :disabled="cantCommitYet" class="btn btn-success submit-commit" @click.prevent="makeCommit"> - <i class="fa fa-spinner fa-spin" v-if="submitCommitsLoading"></i> - <span class="commit-summary">Commit {{changedFiles.length}} {{filePluralize}}</span> + <div class="col-md-offset-4 col-md-6"> + <button + ref="submitCommit" + type="submit" + :disabled="cantCommitYet" + class="btn btn-success"> + <i + v-if="submitCommitsLoading" + class="fa fa-spinner fa-spin" + aria-hidden="true" + aria-label="loading"> + </i> + <span class="commit-summary"> + Commit {{changedFiles.length}} {{filePluralize}} + </span> </button> </div> </fieldset> diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue index f47b6c33fa2..29b76975561 100644 --- a/app/assets/javascripts/repo/components/repo_edit_button.vue +++ b/app/assets/javascripts/repo/components/repo_edit_button.vue @@ -10,12 +10,15 @@ export default { return this.editMode ? this.__('Cancel edit') : this.__('Edit'); }, - buttonIcon() { - return this.editMode ? [] : ['fa', 'fa-pencil']; + showButton() { + return this.isCommitable && + !this.activeFile.render_error && + !this.binary && + this.openedFiles.length; }, }, methods: { - editClicked() { + editCancelClicked() { if (this.changedFiles.length) { this.dialog.open = true; return; @@ -23,25 +26,33 @@ export default { this.editMode = !this.editMode; Store.toggleBlobView(); }, + toggleProjectRefsForm() { + $('.project-refs-form').toggleClass('disabled', this.editMode); + $('.js-tree-ref-target-holder').toggle(this.editMode); + }, }, watch: { editMode() { - if (this.editMode) { - $('.project-refs-form').addClass('disabled'); - $('.js-tree-ref-target-holder').show(); - } else { - $('.project-refs-form').removeClass('disabled'); - $('.js-tree-ref-target-holder').hide(); - } + this.toggleProjectRefsForm(); }, }, }; </script> <template> -<button class="btn btn-default" @click.prevent="editClicked" v-cloak v-if="isCommitable && !activeFile.render_error" :disabled="binary"> - <i :class="buttonIcon"></i> - <span>{{buttonLabel}}</span> +<button + v-if="showButton" + class="btn btn-default" + type="button" + @click.prevent="editCancelClicked"> + <i + v-if="!editMode" + class="fa fa-pencil" + aria-hidden="true"> + </i> + <span> + {{buttonLabel}} + </span> </button> </template> diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index fd1a21e15b4..96d6a75bb61 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -8,38 +8,39 @@ const RepoEditor = { data: () => Store, destroyed() { - // this.monacoInstance.getModels().forEach((m) => { - // m.dispose(); - // }); - this.monacoInstance.destroy(); + if (Helper.monacoInstance) { + Helper.monacoInstance.destroy(); + } }, mounted() { Service.getRaw(this.activeFile.raw_path) - .then((rawResponse) => { - Store.blobRaw = rawResponse.data; - Helper.findOpenedFileFromActive().plain = rawResponse.data; + .then((rawResponse) => { + Store.blobRaw = rawResponse.data; + Store.activeFile.plain = rawResponse.data; - const monacoInstance = this.monaco.editor.create(this.$el, { - model: null, - readOnly: false, - contextmenu: false, - }); + const monacoInstance = Helper.monaco.editor.create(this.$el, { + model: null, + readOnly: false, + contextmenu: false, + }); - Store.monacoInstance = monacoInstance; + Helper.monacoInstance = monacoInstance; - this.addMonacoEvents(); + this.addMonacoEvents(); - const languages = this.monaco.languages.getLanguages(); - const languageID = Helper.getLanguageIDForFile(this.activeFile, languages); - this.showHide(); - const newModel = this.monaco.editor.createModel(this.blobRaw, languageID); - - this.monacoInstance.setModel(newModel); - }).catch(Helper.loadingError); + this.setupEditor(); + }) + .catch(Helper.loadingError); }, methods: { + setupEditor() { + this.showHide(); + + Helper.setMonacoModelFromLanguage(); + }, + showHide() { if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) { this.$el.style.display = 'none'; @@ -49,41 +50,36 @@ const RepoEditor = { }, addMonacoEvents() { - this.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp); - this.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this)); + Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp); + Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this)); }, onMonacoEditorKeysPressed() { - Store.setActiveFileContents(this.monacoInstance.getValue()); + Store.setActiveFileContents(Helper.monacoInstance.getValue()); }, onMonacoEditorMouseUp(e) { + if (!e.target.position) return; const lineNumber = e.target.position.lineNumber; - if (e.target.element.className === 'line-numbers') { + if (e.target.element.classList.contains('line-numbers')) { location.hash = `L${lineNumber}`; Store.activeLine = lineNumber; + + Helper.monacoInstance.setPosition({ + lineNumber: this.activeLine, + column: 1, + }); } }, }, watch: { - activeLine() { - this.monacoInstance.setPosition({ - lineNumber: this.activeLine, - column: 1, - }); - }, - - activeFileLabel() { - this.showHide(); - }, - dialog: { handler(obj) { const newObj = obj; if (newObj.status) { newObj.status = false; - this.openedFiles.map((file) => { + this.openedFiles = this.openedFiles.map((file) => { const f = file; if (f.active) { this.blobRaw = f.plain; @@ -94,35 +90,21 @@ const RepoEditor = { return f; }); this.editMode = false; + Store.toggleBlobView(); } }, deep: true, }, - isTree() { - this.showHide(); - }, - - openedFiles() { - this.showHide(); - }, - - binary() { - this.showHide(); - }, - blobRaw() { - this.showHide(); - - if (this.isTree) return; - - this.monacoInstance.setModel(null); - - const languages = this.monaco.languages.getLanguages(); - const languageID = Helper.getLanguageIDForFile(this.activeFile, languages); - const newModel = this.monaco.editor.createModel(this.blobRaw, languageID); - - this.monacoInstance.setModel(newModel); + if (Helper.monacoInstance && !this.isTree) { + this.setupEditor(); + } + }, + }, + computed: { + shouldHideEditor() { + return !this.openedFiles.length || (this.binary && !this.activeFile.raw); }, }, }; @@ -131,5 +113,5 @@ export default RepoEditor; </script> <template> -<div id="ide"></div> +<div id="ide" v-if='!shouldHideEditor'></div> </template> diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue index f604bc22a26..20ebf840774 100644 --- a/app/assets/javascripts/repo/components/repo_file.vue +++ b/app/assets/javascripts/repo/components/repo_file.vue @@ -33,6 +33,26 @@ const RepoFile = { canShowFile() { return !this.loading.tree || this.hasFiles; }, + + fileIcon() { + const classObj = { + 'fa-spinner fa-spin': this.file.loading, + [this.file.icon]: !this.file.loading, + }; + return classObj; + }, + + fileIndentation() { + return { + 'margin-left': `${this.file.level * 10}px`, + }; + }, + + activeFileClass() { + return { + active: this.activeFile.url === this.file.url, + }; + }, }, methods: { @@ -46,21 +66,42 @@ export default RepoFile; </script> <template> -<tr class="file" v-if="canShowFile" :class="{'active': activeFile.url === file.url}"> - <td @click.prevent="linkClicked(file)"> - <i class="fa file-icon" v-if="!file.loading" :class="file.icon" :style="{'margin-left': file.level * 10 + 'px'}"></i> - <i class="fa fa-spinner fa-spin" v-if="file.loading" :style="{'margin-left': file.level * 10 + 'px'}"></i> - <a :href="file.url" class="repo-file-name" :title="file.url">{{file.name}}</a> +<tr + v-if="canShowFile" + class="file" + :class="activeFileClass" + @click.prevent="linkClicked(file)"> + <td> + <i + class="fa fa-fw file-icon" + :class="fileIcon" + :style="fileIndentation" + aria-label="file icon"> + </i> + <a + :href="file.url" + class="repo-file-name" + :title="file.url"> + {{file.name}} + </a> </td> - <td v-if="!isMini" class="hidden-sm hidden-xs"> - <div class="commit-message"> - <a :href="file.lastCommitUrl">{{file.lastCommitMessage}}</a> - </div> - </td> + <template v-if="!isMini"> + <td class="hidden-sm hidden-xs"> + <div class="commit-message"> + <a @click.stop :href="file.lastCommitUrl"> + {{file.lastCommitMessage}} + </a> + </div> + </td> - <td v-if="!isMini" class="hidden-xs"> - <span class="commit-update" :title="tooltipTitle(file.lastCommitUpdate)">{{timeFormated(file.lastCommitUpdate)}}</span> - </td> + <td class="hidden-xs"> + <span + class="commit-update" + :title="tooltipTitle(file.lastCommitUpdate)"> + {{timeFormated(file.lastCommitUpdate)}} + </span> + </td> + </template> </tr> </template> diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue index 628d02ca704..e43ef366f47 100644 --- a/app/assets/javascripts/repo/components/repo_file_buttons.vue +++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue @@ -15,7 +15,7 @@ const RepoFileButtons = { }, canPreview() { - return Helper.isKindaBinary(); + return Helper.isRenderable(); }, }, @@ -28,15 +28,42 @@ export default RepoFileButtons; </script> <template> -<div id="repo-file-buttons" v-if="isMini"> - <a :href="activeFile.raw_path" target="_blank" class="btn btn-default raw" rel="noopener noreferrer">{{rawDownloadButtonLabel}}</a> + <div id="repo-file-buttons"> + <a + :href="activeFile.raw_path" + target="_blank" + class="btn btn-default raw" + rel="noopener noreferrer"> + {{rawDownloadButtonLabel}} + </a> - <div class="btn-group" role="group" aria-label="File actions"> - <a :href="activeFile.blame_path" class="btn btn-default blame">Blame</a> - <a :href="activeFile.commits_path" class="btn btn-default history">History</a> - <a :href="activeFile.permalink" class="btn btn-default permalink">Permalink</a> - </div> + <div + class="btn-group" + role="group" + aria-label="File actions"> + <a + :href="activeFile.blame_path" + class="btn btn-default blame"> + Blame + </a> + <a + :href="activeFile.commits_path" + class="btn btn-default history"> + History + </a> + <a + :href="activeFile.permalink" + class="btn btn-default permalink"> + Permalink + </a> + </div> - <a href="#" v-if="canPreview" @click.prevent="rawPreviewToggle" class="btn btn-default preview">{{activeFileLabel}}</a> -</div> + <a + v-if="canPreview" + href="#" + @click.prevent="rawPreviewToggle" + class="btn btn-default preview"> + {{activeFileLabel}} + </a> + </div> </template> diff --git a/app/assets/javascripts/repo/components/repo_file_options.vue b/app/assets/javascripts/repo/components/repo_file_options.vue index ba53ce0eecc..6a15755f029 100644 --- a/app/assets/javascripts/repo/components/repo_file_options.vue +++ b/app/assets/javascripts/repo/components/repo_file_options.vue @@ -17,7 +17,7 @@ export default RepoFileOptions; </script> <template> -<tr v-if="isMini" class="repo-file-options"> + <tr v-if="isMini" class="repo-file-options"> <td> <span class="title">{{projectName}}</span> </td> diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/repo/components/repo_loading_file.vue index 38e9f16d041..bc8c64c8362 100644 --- a/app/assets/javascripts/repo/components/repo_loading_file.vue +++ b/app/assets/javascripts/repo/components/repo_loading_file.vue @@ -18,9 +18,15 @@ const RepoLoadingFile = { }, }, + computed: { + showGhostLines() { + return this.loading.tree && !this.hasFiles; + }, + }, + methods: { lineOfCode(n) { - return `line-of-code-${n}`; + return `skeleton-line-${n}`; }, }, }; @@ -29,23 +35,42 @@ export default RepoLoadingFile; </script> <template> -<tr v-if="loading.tree && !hasFiles" class="loading-file"> - <td> - <div class="animation-container animation-container-small"> - <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> - </div> - </td> + <tr + v-if="showGhostLines" + class="loading-file"> + <td> + <div + class="animation-container animation-container-small"> + <div + v-for="n in 6" + :key="n" + :class="lineOfCode(n)"> + </div> + </div> + </td> - <td v-if="!isMini" class="hidden-sm hidden-xs"> - <div class="animation-container"> - <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> - </div> - </td> + <td + v-if="!isMini" + class="hidden-sm hidden-xs"> + <div class="animation-container"> + <div + v-for="n in 6" + :key="n" + :class="lineOfCode(n)"> + </div> + </div> + </td> - <td v-if="!isMini" class="hidden-xs"> - <div class="animation-container animation-container-small"> - <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> - </div> - </td> -</tr> + <td + v-if="!isMini" + class="hidden-xs"> + <div class="animation-container animation-container-small"> + <div + v-for="n in 6" + :key="n" + :class="lineOfCode(n)"> + </div> + </div> + </td> + </tr> </template> diff --git a/app/assets/javascripts/repo/components/repo_prev_directory.vue b/app/assets/javascripts/repo/components/repo_prev_directory.vue index 6a0d684052f..bbdbdc61e38 100644 --- a/app/assets/javascripts/repo/components/repo_prev_directory.vue +++ b/app/assets/javascripts/repo/components/repo_prev_directory.vue @@ -1,4 +1,6 @@ <script> +import RepoMixin from '../mixins/repo_mixin'; + const RepoPreviousDirectory = { props: { prevUrl: { @@ -7,6 +9,14 @@ const RepoPreviousDirectory = { }, }, + mixins: [RepoMixin], + + computed: { + colSpanCondition() { + return this.isMini ? undefined : 3; + }, + }, + methods: { linkClicked(file) { this.$emit('linkclicked', file); @@ -19,8 +29,10 @@ export default RepoPreviousDirectory; <template> <tr class="prev-directory"> - <td colspan="3"> - <a :href="prevUrl" @click.prevent="linkClicked(prevUrl)">..</a> + <td + :colspan="colSpanCondition" + @click.prevent="linkClicked(prevUrl)"> + <a :href="prevUrl">..</a> </td> </tr> </template> diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue index 0d4f8c6635e..72b40288566 100644 --- a/app/assets/javascripts/repo/components/repo_sidebar.vue +++ b/app/assets/javascripts/repo/components/repo_sidebar.vue @@ -8,7 +8,7 @@ import RepoFile from './repo_file.vue'; import RepoLoadingFile from './repo_loading_file.vue'; import RepoMixin from '../mixins/repo_mixin'; -const RepoSidebar = { +export default { mixins: [RepoMixin], components: { 'repo-file-options': RepoFileOptions, @@ -35,7 +35,7 @@ const RepoSidebar = { fileClicked(clickedFile) { let file = clickedFile; - + if (file.loading) return; file.loading = true; if (file.type === 'tree' && file.opened) { file = Store.removeChildFilesOfTree(file); @@ -59,12 +59,10 @@ const RepoSidebar = { }, }, }; - -export default RepoSidebar; </script> <template> -<div id="sidebar" :class="{'sidebar-mini' : isMini}" v-cloak> +<div id="sidebar" :class="{'sidebar-mini' : isMini}"> <table class="table"> <thead v-if="!isMini"> <tr> diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue index fc66a8ea953..0d0c34ec741 100644 --- a/app/assets/javascripts/repo/components/repo_tab.vue +++ b/app/assets/javascripts/repo/components/repo_tab.vue @@ -18,8 +18,8 @@ const RepoTab = { }, changedClass() { const tabChangedObj = { - 'fa-times': !this.tab.changed, - 'fa-circle': this.tab.changed, + 'fa-times close-icon': !this.tab.changed, + 'fa-circle unsaved-icon': this.tab.changed, }; return tabChangedObj; }, @@ -28,9 +28,9 @@ const RepoTab = { methods: { tabClicked: Store.setActiveFiles, - xClicked(file) { + closeTab(file) { if (file.changed) return; - this.$emit('xclicked', file); + this.$emit('tabclosed', file); }, }, }; @@ -39,11 +39,11 @@ export default RepoTab; </script> <template> -<li> +<li @click="tabClicked(tab)"> <a href="#0" class="close" - @click.prevent="xClicked(tab)" + @click.stop.prevent="closeTab(tab)" :aria-label="closeLabel"> <i class="fa" diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/repo/components/repo_tabs.vue index bbd60d9d793..9c5bfc5d0cf 100644 --- a/app/assets/javascripts/repo/components/repo_tabs.vue +++ b/app/assets/javascripts/repo/components/repo_tabs.vue @@ -13,7 +13,7 @@ const RepoTabs = { data: () => Store, methods: { - xClicked(file) { + tabClosed(file) { Store.removeFromOpenedFiles(file); }, }, @@ -23,10 +23,14 @@ export default RepoTabs; </script> <template> -<ul - v-if="isMini" - id="tabs"> - <repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/> +<ul id="tabs"> + <repo-tab + v-for="tab in openedFiles" + :key="tab.id" + :tab="tab" + :class="{'active' : tab.active}" + @tabclosed="tabClosed" + /> <li class="tabs-divider" /> </ul> </template> diff --git a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js index c1a0e80f8f3..f8729bbf585 100644 --- a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js +++ b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js @@ -1,13 +1,14 @@ /* global monaco */ import RepoEditor from '../components/repo_editor.vue'; import Store from '../stores/repo_store'; +import Helper from '../helpers/repo_helper'; import monacoLoader from '../monaco_loader'; function repoEditorLoader() { Store.monacoLoading = true; return new Promise((resolve, reject) => { monacoLoader(['vs/editor/editor.main'], () => { - Store.monaco = monaco; + Helper.monaco = monaco; Store.monacoLoading = false; resolve(RepoEditor); }, () => { diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js index 17aaa0e1584..2bd8d7eea65 100644 --- a/app/assets/javascripts/repo/helpers/repo_helper.js +++ b/app/assets/javascripts/repo/helpers/repo_helper.js @@ -4,6 +4,8 @@ import Store from '../stores/repo_store'; import '../../flash'; const RepoHelper = { + monacoInstance: null, + getDefaultActiveFile() { return { active: true, @@ -37,10 +39,6 @@ const RepoHelper = { return fileName.split('.').pop(); }, - getBranch() { - return $('button.dropdown-menu-toggle').attr('data-ref'); - }, - getLanguageIDForFile(file, langs) { const ext = RepoHelper.getFileExtension(file.name); const foundLang = RepoHelper.findLanguage(ext, langs); @@ -48,8 +46,12 @@ const RepoHelper = { return foundLang ? foundLang.id : 'plaintext'; }, - getFilePathFromFullPath(fullPath, branch) { - return fullPath.split(`${Store.projectUrl}/blob/${branch}`)[1]; + setMonacoModelFromLanguage() { + RepoHelper.monacoInstance.setModel(null); + const languages = RepoHelper.monaco.languages.getLanguages(); + const languageID = RepoHelper.getLanguageIDForFile(Store.activeFile, languages); + const newModel = RepoHelper.monaco.editor.createModel(Store.blobRaw, languageID); + RepoHelper.monacoInstance.setModel(newModel); }, findLanguage(ext, langs) { @@ -62,11 +64,11 @@ const RepoHelper = { file.opened = true; file.icon = 'fa-folder-open'; - RepoHelper.toURL(file.url, file.name); + RepoHelper.updateHistoryEntry(file.url, file.name); return file; }, - isKindaBinary() { + isRenderable() { const okExts = ['md', 'svg']; return okExts.indexOf(Store.activeFile.extension) > -1; }, @@ -80,22 +82,8 @@ const RepoHelper = { .catch(RepoHelper.loadingError); }, - toggleFakeTab(loading, file) { - if (loading) return Store.addPlaceholderFile(); - return Store.removeFromOpenedFiles(file); - }, - - setLoading(loading, file) { - if (Service.url.indexOf('blob') > -1) { - Store.loading.blob = loading; - return RepoHelper.toggleFakeTab(loading, file); - } - - if (Service.url.indexOf('tree') > -1) Store.loading.tree = loading; - - return undefined; - }, - + // when you open a directory you need to put the directory files under + // the directory... This will merge the list of the current directory and the new list. getNewMergedList(inDirectory, currentList, newList) { const newListSorted = newList.sort(this.compareFilesCaseInsensitive); if (!inDirectory) return newListSorted; @@ -104,6 +92,9 @@ const RepoHelper = { return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile); }, + // within the get new merged list this does the merging of the current list of files + // and the new list of files. The files are never "in" another directory they just + // appear like they are because of the margin. mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) { newList.reverse().forEach((newFile) => { const fileIndex = indexOfFile + 1; @@ -141,11 +132,9 @@ const RepoHelper = { getContent(treeOrFile) { let file = treeOrFile; - // const loadingData = RepoHelper.setLoading(true); return Service.getContent() .then((response) => { const data = response.data; - // RepoHelper.setLoading(false, loadingData); Store.isTree = RepoHelper.isTree(data); if (!Store.isTree) { if (!file) file = data; @@ -246,37 +235,19 @@ const RepoHelper = { }, dataToListOfFiles(data) { - const a = []; - - // push in blobs - data.blobs.forEach((blob) => { - a.push(RepoHelper.serializeBlob(blob)); - }); - - data.trees.forEach((tree) => { - a.push(RepoHelper.serializeTree(tree)); - }); - - data.submodules.forEach((submodule) => { - a.push(RepoHelper.serializeSubmodule(submodule)); - }); - - return a; + const { blobs, trees, submodules } = data; + return [ + ...blobs.map(blob => RepoHelper.serializeBlob(blob)), + ...trees.map(tree => RepoHelper.serializeTree(tree)), + ...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)), + ]; }, genKey() { return RepoHelper.Time.now().toFixed(3); }, - getStateKey() { - return RepoHelper.key; - }, - - setStateKey(key) { - RepoHelper.key = key; - }, - - toURL(url, title) { + updateHistoryEntry(url, title) { const history = window.history; RepoHelper.key = RepoHelper.genKey(); @@ -293,7 +264,7 @@ const RepoHelper = { }, loadingError() { - Flash('Unable to load the file at this time.'); + Flash('Unable to load this content at this time.'); }, }; diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js index 3e37da1726e..6c1d468e937 100644 --- a/app/assets/javascripts/repo/index.js +++ b/app/assets/javascripts/repo/index.js @@ -33,6 +33,8 @@ function setInitialStore(data) { Store.projectId = data.projectId; Store.projectName = data.projectName; Store.projectUrl = data.projectUrl; + Store.canCommit = data.canCommit; + Store.onTopOfBranch = data.onTopOfBranch; Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref'); Store.checkIsCommitable(); } @@ -43,6 +45,9 @@ function initRepo(el) { components: { repo: Repo, }, + render(createElement) { + return createElement('repo'); + }, }); } diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js index 17578f3bbf3..3cf204e6ec8 100644 --- a/app/assets/javascripts/repo/services/repo_service.js +++ b/app/assets/javascripts/repo/services/repo_service.js @@ -13,14 +13,6 @@ const RepoService = { }, richExtensionRegExp: /md/, - checkCurrentBranchIsCommitable() { - const url = Store.service.refsUrl; - return axios.get(url, { params: { - ref: Store.currentBranch, - search: Store.currentBranch, - } }); - }, - getRaw(url) { return axios.get(url, { // Stop Axios from parsing a JSON file into a JS object @@ -75,7 +67,11 @@ const RepoService = { commitFiles(payload, cb) { Api.commitMultiple(Store.projectId, payload, (data) => { - Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice'); + if (data.short_id && data.stats) { + Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice'); + } else { + Flash(data.message); + } cb(); }); }, diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js index bb605540aad..1c0df528aea 100644 --- a/app/assets/javascripts/repo/stores/repo_store.js +++ b/app/assets/javascripts/repo/stores/repo_store.js @@ -5,8 +5,9 @@ import Service from '../services/repo_service'; const RepoStore = { monaco: {}, monacoLoading: false, - monacoInstance: {}, service: '', + canCommit: false, + onTopOfBranch: false, editMode: false, isTree: false, isRoot: false, @@ -52,14 +53,7 @@ const RepoStore = { // mutations checkIsCommitable() { - RepoStore.service.checkCurrentBranchIsCommitable() - .then((data) => { - // you shouldn't be able to make commits on commits or tags. - const { Branches, Commits, Tags } = data.data; - if (Branches && Branches.length) RepoStore.isCommitable = true; - if (Commits && Commits.length) RepoStore.isCommitable = false; - if (Tags && Tags.length) RepoStore.isCommitable = false; - }).catch(() => Flash('Failed to check if branch can be committed to.')); + RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit; }, addFilesToDirectory(inDirectory, currentList, newList) { @@ -90,7 +84,7 @@ const RepoStore = { }).catch(Helper.loadingError); } - if (!file.loading) Helper.toURL(file.url, file.name); + if (!file.loading) Helper.updateHistoryEntry(file.url, file.name); RepoStore.binary = file.binary; }, @@ -117,15 +111,15 @@ const RepoStore = { removeChildFilesOfTree(tree) { let foundTree = false; const treeToClose = tree; - let wereDone = false; + let canStopSearching = false; RepoStore.files = RepoStore.files.filter((file) => { const isItTheTreeWeWant = file.url === treeToClose.url; // if it's the next tree if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) { - wereDone = true; + canStopSearching = true; return true; } - if (wereDone) return true; + if (canStopSearching) return true; if (isItTheTreeWeWant) foundTree = true; @@ -142,8 +136,8 @@ const RepoStore = { if (file.type === 'tree') return; let foundIndex; RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => { - if (openedFile.url === file.url) foundIndex = i; - return openedFile.url !== file.url; + if (openedFile.path === file.path) foundIndex = i; + return openedFile.path !== file.path; }); // now activate the right tab based on what you closed. @@ -157,36 +151,16 @@ const RepoStore = { return; } - if (foundIndex) { - if (foundIndex > 0) { - RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]); - } + if (foundIndex && foundIndex > 0) { + RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]); } }, - addPlaceholderFile() { - const randomURL = Helper.Time.now(); - const newFakeFile = { - active: false, - binary: true, - type: 'blob', - loading: true, - mime_type: 'loading', - name: 'loading', - url: randomURL, - fake: true, - }; - - RepoStore.openedFiles.push(newFakeFile); - - return newFakeFile; - }, - addToOpenedFiles(file) { const openFile = file; const openedFilesAlreadyExists = RepoStore.openedFiles - .some(openedFile => openedFile.url === openFile.url); + .some(openedFile => openedFile.path === openFile.path); if (openedFilesAlreadyExists) return; diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index 422c02c7b7e..cfacba09fad 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -71,7 +71,7 @@ export default { /> <div v-if="!isConfidential" class="no-value confidential-value"> <i class="fa fa-eye is-not-confidential"></i> - None + This issue is not confidential </div> <div v-else class="value confidential-value hide-collapsed"> <i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i> diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue index 7d339c0e753..994b33bc1c9 100644 --- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue +++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue @@ -1,31 +1,37 @@ <script> -const PopupDialog = { +export default { name: 'popup-dialog', props: { - open: Boolean, - title: String, - body: String, + title: { + type: String, + required: true, + }, + body: { + type: String, + required: true, + }, kind: { type: String, + required: false, default: 'primary', }, closeButtonLabel: { type: String, + required: false, default: 'Cancel', }, primaryButtonLabel: { type: String, - default: 'Save changes', + required: true, }, }, computed: { - typeOfClass() { - const className = `btn-${this.kind}`; - const returnObj = {}; - returnObj[className] = true; - return returnObj; + btnKindClass() { + return { + [`btn-${this.kind}`]: true, + }; }, }, @@ -33,33 +39,45 @@ const PopupDialog = { close() { this.$emit('toggle', false); }, - - yesClick() { - this.$emit('submit', true); - }, - - noClick() { - this.$emit('submit', false); + emitSubmit(status) { + this.$emit('submit', status); }, }, }; - -export default PopupDialog; </script> + <template> -<div class="modal popup-dialog" tabindex="-1" v-show="open" role="dialog"> +<div + class="modal popup-dialog" + role="dialog" + tabindex="-1"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> - <button type="button" class="close" @click="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> + <button type="button" + class="close" + @click="close" + aria-label="Close"> + <span aria-hidden="true">×</span> + </button> <h4 class="modal-title">{{this.title}}</h4> </div> <div class="modal-body"> <p>{{this.body}}</p> </div> <div class="modal-footer"> - <button type="button" class="btn btn-default" data-dismiss="modal" @click="noClick">{{closeButtonLabel}}</button> - <button type="button" class="btn" :class="typeOfClass" @click="yesClick">{{primaryButtonLabel}}</button> + <button + type="button" + class="btn btn-default" + @click="emitSubmit(false)"> + {{closeButtonLabel}} + </button> + <button type="button" + class="btn" + :class="btnKindClass" + @click="emitSubmit(true)"> + {{primaryButtonLabel}} + </button> </div> </div> </div> diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 3cd7f81da47..667b73e150d 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -187,3 +187,81 @@ a { .fade-in-full { animation: fadeInFull $fade-in-duration 1; } + + +.animation-container { + background: $repo-editor-grey; + height: 40px; + overflow: hidden; + position: relative; + + &.animation-container-small { + height: 12px; + } + + &::before { + animation-duration: 1s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: blockTextShine; + animation-timing-function: linear; + background-image: $repo-editor-linear-gradient; + background-repeat: no-repeat; + background-size: 800px 45px; + content: ' '; + display: block; + height: 100%; + position: relative; + } + + div { + background: $white-light; + height: 6px; + left: 0; + position: absolute; + right: 0; + } + + .skeleton-line-1 { + left: 0; + top: 8px; + } + + .skeleton-line-2 { + left: 150px; + top: 0; + height: 10px; + } + + .skeleton-line-3 { + left: 0; + top: 23px; + } + + .skeleton-line-4 { + left: 0; + top: 38px; + } + + .skeleton-line-5 { + left: 200px; + top: 28px; + height: 10px; + } + + .skeleton-line-6 { + top: 14px; + left: 230px; + height: 10px; + } +} + +@keyframes blockTextShine { + 0% { + transform: translateX(-468px); + } + + 100% { + transform: translateX(468px); + } +} diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index bd0367f86dd..bd521028c44 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -117,10 +117,6 @@ body { margin-top: $header-height + $performance-bar-height; } -[v-cloak] { - display: none; -} - .vertical-center { min-height: 100vh; display: flex; diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index fcd4c72b430..e3920b5d3d9 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -204,6 +204,16 @@ } } + div.avatar { + display: inline-flex; + justify-content: center; + align-items: center; + + .center { + line-height: 14px; + } + } + strong { color: $gl-text-color; } diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index 795ee91af8b..3e2f23e6b2a 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -403,6 +403,7 @@ header.navbar-gitlab-new { } .breadcrumbs-extra { + display: flex; flex: 0 0 auto; margin-left: auto; } diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index faedd207e01..d078c8b956b 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -97,9 +97,9 @@ $new-sidebar-collapsed-width: 50px; top: $header-height; bottom: 0; left: 0; - overflow: auto; background-color: $gray-normal; box-shadow: inset -2px 0 0 $border-color; + transform: translate3d(0, 0, 0); &.sidebar-icons-only { width: $new-sidebar-collapsed-width; @@ -176,6 +176,12 @@ $new-sidebar-collapsed-width: 50px; } } +.nav-sidebar-inner-scroll { + height: 100%; + width: 100%; + overflow: auto; +} + .with-performance-bar .nav-sidebar { top: $header-height + $performance-bar-height; } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index cd9f2d787c5..46fbfe5f91e 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -286,6 +286,10 @@ .gpg-status-box { + &:empty { + display: none; + } + &.valid { @include green-status-color; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 215bedc04fd..913a1a95dca 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -560,9 +560,13 @@ } .diff-files-changed { + .inline-parallel-buttons { + position: relative; + z-index: 1; + } + .commit-stat-summary { @include new-style-dropdown; - z-index: -1; @media (min-width: $screen-sm-min) { margin-left: -$gl-padding; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index d14b976374c..87eaf27663f 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -8,13 +8,13 @@ .is-confidential { color: $orange-600; background-color: $orange-50; - border-radius: 3px; + border-radius: $border-radius-default; padding: 5px; margin: 0 3px 0 -4px; } .is-not-confidential { - border-radius: 3px; + border-radius: $border-radius-default; padding: 5px; margin: 0 3px 0 -4px; } diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index ad17078c98a..b3527fe8cd9 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -1,6 +1,6 @@ .fade-enter-active, .fade-leave-active { - transition: opacity .5s; + transition: opacity $sidebar-transition-duration; } .monaco-loader { @@ -28,11 +28,6 @@ .project-refs-form, .project-refs-target-form { display: inline-block; - - &.disabled { - opacity: 0.5; - pointer-events: none; - } } .fade-enter, @@ -90,7 +85,7 @@ } .blob-viewer-container { - height: calc(100vh - 63px); + height: calc(100vh - 62px); overflow: auto; } @@ -114,6 +109,7 @@ border-right: 1px solid $white-dark; border-bottom: 1px solid $white-dark; white-space: nowrap; + cursor: pointer; &.remove { animation: swipeRightDissapear ease-in 0.1s; @@ -133,10 +129,10 @@ a { @include str-truncated(100px); color: $black; - display: inline-block; width: 100px; text-align: center; vertical-align: middle; + text-decoration: none; &.close { width: auto; @@ -146,15 +142,15 @@ } } - i.fa.fa-times, - i.fa.fa-circle { + .close-icon, + .unsaved-icon { float: right; margin-top: 3px; margin-left: 15px; color: $gray-darkest; } - i.fa.fa-circle { + .unsaved-icon { color: $brand-success; } @@ -204,7 +200,7 @@ background: $gray-light; padding: 20px; - span.help-block { + .help-block { padding-top: 7px; margin-top: 0; } @@ -232,6 +228,7 @@ vertical-align: top; width: 20%; border-right: 1px solid $white-normal; + min-height: 475px; height: calc(100vh + 20px); overflow: auto; } @@ -261,7 +258,6 @@ text-transform: uppercase; font-weight: bold; color: $gray-darkest; - width: 185px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -270,7 +266,7 @@ } } - .fa { + .file-icon { margin-right: 5px; } @@ -280,118 +276,22 @@ } a { + @include str-truncated(250px); color: $almost-black; display: inline-block; vertical-align: middle; } - - ul { - list-style-type: none; - padding: 0; - - li { - border-bottom: 1px solid $border-gray-normal; - padding: 10px 20px; - - a { - color: $almost-black; - } - - .fa { - font-size: $code_font_size; - margin-right: 5px; - } - } - } - } - -} - -.animation-container { - background: $repo-editor-grey; - height: 40px; - overflow: hidden; - position: relative; - - &.animation-container-small { - height: 12px; - } - - &::before { - animation-duration: 1s; - animation-fill-mode: forwards; - animation-iteration-count: infinite; - animation-name: blockTextShine; - animation-timing-function: linear; - background-image: $repo-editor-linear-gradient; - background-repeat: no-repeat; - background-size: 800px 45px; - content: ' '; - display: block; - height: 100%; - position: relative; - } - - div { - background: $white-light; - height: 6px; - left: 0; - position: absolute; - right: 0; - } - - .line-of-code-1 { - left: 0; - top: 8px; - } - - .line-of-code-2 { - left: 150px; - top: 0; - height: 10px; - } - - .line-of-code-3 { - left: 0; - top: 23px; - } - - .line-of-code-4 { - left: 0; - top: 38px; - } - - .line-of-code-5 { - left: 200px; - top: 28px; - height: 10px; - } - - .line-of-code-6 { - top: 14px; - left: 230px; - height: 10px; } } .render-error { - min-height: calc(100vh - 63px); + min-height: calc(100vh - 62px); p { width: 100%; } } -@keyframes blockTextShine { - 0% { - transform: translateX(-468px); - } - - 100% { - transform: translateX(468px); - } -} - @keyframes swipeRightAppear { 0% { transform: scaleX(0.00); diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 0c3b68a7ac3..4079072a930 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -10,7 +10,7 @@ module IssuableActions def destroy issuable.destroy destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym - TodoService.new.public_send(destroy_method, issuable, current_user) + TodoService.new.public_send(destroy_method, issuable, current_user) # rubocop:disable GitlabSecurity/PublicSend name = issuable.human_class_name flash[:notice] = "The #{name} was successfully deleted." diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index c0ac47e363d..96ce686c989 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -34,7 +34,7 @@ class Groups::ApplicationController < ApplicationController def build_canonical_path(group) params[:group_id] = group.to_param - + url_for(params) end end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index baa6645e5ce..ab18d86dcae 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -64,7 +64,7 @@ class Import::GithubController < Import::BaseController end def import_enabled? - __send__("#{provider}_import_enabled?") + __send__("#{provider}_import_enabled?") # rubocop:disable GitlabSecurity/PublicSend end def new_import_url diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index b4213574561..7444826a5d1 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -142,13 +142,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def oauth @oauth ||= request.env['omniauth.auth'] end - + def fail_login error_message = @user.errors.full_messages.to_sentence return redirect_to omniauth_error_path(oauth['provider'], error: error_message) end - + def fail_ldap_login flash[:alert] = 'Access denied for your LDAP account.' diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index a2e8c10857d..2b8f3977e6e 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -198,6 +198,10 @@ class Projects::BlobController < Projects::ApplicationController json = blob_json(@blob) return render_404 unless json + path_segments = @path.split('/') + path_segments.pop + tree_path = path_segments.join('/') + render json: json.merge( path: blob.path, name: blob.name, @@ -212,6 +216,7 @@ class Projects::BlobController < Projects::ApplicationController raw_path: project_raw_path(project, @id), blame_path: project_blame_path(project, @id), commits_path: project_commits_path(project, @id), + tree_path: project_tree_path(project, File.join(@ref, tree_path)), permalink: project_blob_path(project, File.join(@commit.id, @path)) ) end diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb index b69d46f2c41..26f3c114108 100644 --- a/app/controllers/projects/cycle_analytics/events_controller.rb +++ b/app/controllers/projects/cycle_analytics/events_controller.rb @@ -2,7 +2,7 @@ module Projects module CycleAnalytics class EventsController < Projects::ApplicationController include CycleAnalyticsParams - + before_action :authorize_read_cycle_analytics! before_action :authorize_read_build!, only: [:test, :staging] before_action :authorize_read_issue!, only: [:issue, :production] @@ -11,33 +11,33 @@ module Projects def issue render_events(cycle_analytics[:issue].events) end - + def plan render_events(cycle_analytics[:plan].events) end - + def code render_events(cycle_analytics[:code].events) end - + def test options(events_params)[:branch] = events_params[:branch_name] - + render_events(cycle_analytics[:test].events) end - + def review render_events(cycle_analytics[:review].events) end - + def staging render_events(cycle_analytics[:staging].events) end - + def production render_events(cycle_analytics[:production].events) end - + private def render_events(events) @@ -46,14 +46,14 @@ module Projects format.json { render json: { events: events } } end end - + def cycle_analytics @cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params)) end - + def events_params return {} unless params[:events].present? - + params[:events].permit(:start_date, :branch_name) end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 4de814d0ca8..2a3b73577a5 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -218,8 +218,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo if can?(current_user, :read_environment, environment) && environment.has_metrics? metrics_project_environment_deployment_path(environment.project, environment, deployment) end - - metrics_monitoring_url = + + metrics_monitoring_url = if can?(current_user, :read_environment, environment) environment_metrics_path(environment) end diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index dc882b17143..16a74f82d3f 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -89,7 +89,7 @@ class UploadsController < ApplicationController @uploader.retrieve_from_store!(params[:filename]) else - @uploader = @model.send(upload_mount) + @uploader = @model.public_send(upload_mount) # rubocop:disable GitlabSecurity/PublicSend redirect_to @uploader.url unless @uploader.file_storage? end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 69220a1c0f6..72e26b64e60 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -128,10 +128,10 @@ module CommitsHelper # avatar: true will prepend the avatar image # size: size of the avatar image in px def commit_person_link(commit, options = {}) - user = commit.send(options[:source]) + user = commit.public_send(options[:source]) # rubocop:disable GitlabSecurity/PublicSend - source_name = clean(commit.send "#{options[:source]}_name".to_sym) - source_email = clean(commit.send "#{options[:source]}_email".to_sym) + source_name = clean(commit.public_send(:"#{options[:source]}_name")) # rubocop:disable GitlabSecurity/PublicSend + source_email = clean(commit.public_send(:"#{options[:source]}_email")) # rubocop:disable GitlabSecurity/PublicSend person_name = user.try(:name) || source_name diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index a57b5a8fea5..a18ebfb6030 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -5,7 +5,7 @@ module ImportHelper end def provider_project_link(provider, path_with_namespace) - url = __send__("#{provider}_project_url", path_with_namespace) + url = __send__("#{provider}_project_url", path_with_namespace) # rubocop:disable GitlabSecurity/PublicSend link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer' end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 70ea35fab1e..197c90c4081 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -174,7 +174,14 @@ module IssuablesHelper end def assigned_issuables_count(issuable_type) - current_user.public_send("assigned_open_#{issuable_type}_count") + case issuable_type + when :issues + current_user.assigned_open_issues_count + when :merge_requests + current_user.assigned_open_merge_requests_count + else + raise ArgumentError, "invalid issuable `#{issuable_type}`" + end end def issuable_filter_params @@ -298,10 +305,6 @@ module IssuablesHelper cookies[:collapsed_gutter] == 'true' end - def base_issuable_scope(issuable) - issuable.project.send(issuable.class.table_name).send(issuable_state_scope(issuable)) - end - def issuable_state_scope(issuable) if issuable.respond_to?(:merged?) && issuable.merged? :merged diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index f8860bfee99..86666022a2a 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -32,7 +32,18 @@ module MilestonesHelper end def milestone_issues_by_label_count(milestone, label, state:) - milestone.issues.with_label(label.title).send(state).size + issues = milestone.issues.with_label(label.title) + issues = + case state + when :opened + issues.opened + when :closed + issues.closed + else + raise ArgumentError, "invalid milestone state `#{state}`" + end + + issues.size end # Returns count of milestones for different states diff --git a/app/helpers/pipeline_schedules_helper.rb b/app/helpers/pipeline_schedules_helper.rb index fee1edc2a1b..6edaf78de1b 100644 --- a/app/helpers/pipeline_schedules_helper.rb +++ b/app/helpers/pipeline_schedules_helper.rb @@ -1,10 +1,10 @@ module PipelineSchedulesHelper def timezone_data - ActiveSupport::TimeZone.all.map do |timezone| - { - name: timezone.name, - offset: timezone.utc_offset, - identifier: timezone.tzinfo.identifier + ActiveSupport::TimeZone.all.map do |timezone| + { + name: timezone.name, + offset: timezone.utc_offset, + identifier: timezone.tzinfo.identifier } end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 09cfd06dad3..bee4950e414 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -149,15 +149,16 @@ module ProjectsHelper # Don't show option "everyone with access" if project is private options = project_feature_options + level = @project.project_feature.public_send(field) # rubocop:disable GitlabSecurity/PublicSend + if @project.private? - level = @project.project_feature.send(field) disabled_option = ProjectFeature::ENABLED highest_available_option = ProjectFeature::PRIVATE if level == disabled_option end options = options_for_select( options.invert, - selected: highest_available_option || @project.project_feature.public_send(field), + selected: highest_available_option || level, disabled: disabled_option ) @@ -234,6 +235,8 @@ module ProjectsHelper # If no limit is applied we'll just issue a COUNT since the result set could # be too large to load into memory. def any_projects?(projects) + return projects.any? if projects.is_a?(Array) + if projects.limit_value projects.to_a.any? else @@ -486,7 +489,7 @@ module ProjectsHelper end def filename_path(project, filename) - if project && blob = project.repository.send(filename) + if project && blob = project.repository.public_send(filename) # rubocop:disable GitlabSecurity/PublicSend project_blob_path( project, tree_join(project.default_branch, blob.name) diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index 7b617b359ea..d76c61c369f 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -11,11 +11,11 @@ module Emails @member_source_type = member_source_type @member_id = member_id - admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email) + admins = member_source.members.owners_and_masters.pluck(:notification_email) # A project in a group can have no explicit owners/masters, in that case # we fallbacks to the group's owners/masters. if admins.empty? && member_source.respond_to?(:group) && member_source.group - admins = member_source.group.members.owners_and_masters.includes(:user).pluck(:notification_email) + admins = member_source.group.members.owners_and_masters.pluck(:notification_email) end mail(to: admins, diff --git a/app/models/blob_viewer/notebook.rb b/app/models/blob_viewer/notebook.rb index 8632b8a9885..e00b47e6c17 100644 --- a/app/models/blob_viewer/notebook.rb +++ b/app/models/blob_viewer/notebook.rb @@ -2,7 +2,7 @@ module BlobViewer class Notebook < Base include Rich include ClientSide - + self.partial_name = 'notebook' self.extensions = %w(ipynb) self.binary = false diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 8be2dee6479..4692fb5644a 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -194,10 +194,7 @@ module Ci # * Maximum length is 63 bytes # * First/Last Character is not a hyphen def ref_slug - ref.to_s - .downcase - .gsub(/[^a-z0-9]/, '-')[0..62] - .gsub(/(\A-+|-+\z)/, '') + Gitlab::Utils.slugify(ref.to_s) end # Variables whose value does not depend on environment diff --git a/app/models/commit.rb b/app/models/commit.rb index 638fddc5d3d..5ca2f150247 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -200,7 +200,7 @@ class Commit end def method_missing(m, *args, &block) - @raw.send(m, *args, &block) + @raw.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end def respond_to_missing?(method, include_private = false) diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 48547a938fc..193e459977a 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -78,7 +78,7 @@ module CacheMarkdownField def cached_html_up_to_date?(markdown_field) html_field = cached_markdown_fields.html_field(markdown_field) - cached = cached_html_for(markdown_field).present? && __send__(markdown_field).present? + cached = cached_html_for(markdown_field).present? && __send__(markdown_field).present? # rubocop:disable GitlabSecurity/PublicSend return false unless cached markdown_changed = attribute_changed?(markdown_field) || false @@ -93,14 +93,14 @@ module CacheMarkdownField end def attribute_invalidated?(attr) - __send__("#{attr}_invalidated?") + __send__("#{attr}_invalidated?") # rubocop:disable GitlabSecurity/PublicSend end def cached_html_for(markdown_field) raise ArgumentError.new("Unknown field: #{field}") unless cached_markdown_fields.markdown_fields.include?(markdown_field) - __send__(cached_markdown_fields.html_field(markdown_field)) + __send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend end included do diff --git a/app/models/concerns/internal_id.rb b/app/models/concerns/internal_id.rb index 67a0adfcd56..a3d0ac8d862 100644 --- a/app/models/concerns/internal_id.rb +++ b/app/models/concerns/internal_id.rb @@ -9,7 +9,7 @@ module InternalId def set_iid if iid.blank? parent = project || group - records = parent.send(self.class.name.tableize) + records = parent.public_send(self.class.name.tableize) # rubocop:disable GitlabSecurity/PublicSend records = records.with_deleted if self.paranoid? max_iid = records.maximum(:iid) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index c034bf9cbc0..1db6b2d2fa2 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -56,7 +56,7 @@ module Mentionable end self.class.mentionable_attrs.each do |attr, options| - text = __send__(attr) + text = __send__(attr) # rubocop:disable GitlabSecurity/PublicSend options = options.merge( cache_key: [self, attr], author: author, @@ -100,7 +100,7 @@ module Mentionable end self.class.mentionable_attrs.any? do |attr, _| - __send__(attr) =~ reference_pattern + __send__(attr) =~ reference_pattern # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 4865c0a14b1..ce69fd34ac5 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -82,7 +82,7 @@ module Participable if attr.respond_to?(:call) source.instance_exec(current_user, ext, &attr) else - process << source.__send__(attr) + process << source.__send__(attr) # rubocop:disable GitlabSecurity/PublicSend end end when Enumerable, ActiveRecord::Relation diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 60734bc6660..cb59b4da3d7 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -32,6 +32,6 @@ module ProjectFeaturesCompatibility build_project_feature unless project_feature access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED - project_feature.send(:write_attribute, field, access_level) + project_feature.__send__(:write_attribute, field, access_level) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb index ae8486bd9ac..b37b9bfbdac 100644 --- a/app/models/deploy_keys_project.rb +++ b/app/models/deploy_keys_project.rb @@ -12,7 +12,7 @@ class DeployKeysProject < ActiveRecord::Base def destroy_orphaned_deploy_key return unless self.deploy_key.destroyed_when_orphaned? && self.deploy_key.orphaned? - + self.deploy_key.destroy end end diff --git a/app/models/group.rb b/app/models/group.rb index bd5735ed82e..2816a68257c 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -212,21 +212,39 @@ class Group < Namespace end def user_ids_for_project_authorizations - users_with_parents.pluck(:id) + members_with_parents.pluck(:user_id) end def members_with_parents - GroupMember.active.where(source_id: ancestors.pluck(:id).push(id)).where.not(user_id: nil) + # Avoids an unnecessary SELECT when the group has no parents + source_ids = + if parent_id + self_and_ancestors.reorder(nil).select(:id) + else + id + end + + GroupMember + .active_without_invites + .where(source_id: source_ids) + end + + def members_with_descendants + GroupMember + .active_without_invites + .where(source_id: self_and_descendants.reorder(nil).select(:id)) end def users_with_parents - User.where(id: members_with_parents.select(:user_id)) + User + .where(id: members_with_parents.select(:user_id)) + .reorder(nil) end def users_with_descendants - members_with_descendants = GroupMember.non_request.where(source_id: descendants.pluck(:id).push(id)) - - User.where(id: members_with_descendants.select(:user_id)) + User + .where(id: members_with_descendants.select(:user_id)) + .reorder(nil) end def max_member_access_for_user(user) diff --git a/app/models/member.rb b/app/models/member.rb index b26b5017183..ee2cb13697b 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -41,9 +41,20 @@ class Member < ActiveRecord::Base is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil)) user_is_active = User.arel_table[:state].eq(:active) - includes(:user).references(:users) - .where(is_external_invite.or(user_is_active)) + user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_active) + + left_join_users + .where(user_ok) + .where(requested_at: nil) + .reorder(nil) + end + + # Like active, but without invites. For when a User is required. + scope :active_without_invites, -> do + left_join_users + .where(users: { state: 'active' }) .where(requested_at: nil) + .reorder(nil) end scope :invite, -> { where.not(invite_token: nil) } diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 6073fb94a3f..e7bc1d1b080 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -156,6 +156,14 @@ class Namespace < ActiveRecord::Base .base_and_ancestors end + def self_and_ancestors + return self.class.where(id: id) unless parent_id + + Gitlab::GroupHierarchy + .new(self.class.where(id: id)) + .base_and_ancestors + end + # Returns all the descendants of the current namespace. def descendants Gitlab::GroupHierarchy @@ -163,6 +171,12 @@ class Namespace < ActiveRecord::Base .base_and_descendants end + def self_and_descendants + Gitlab::GroupHierarchy + .new(self.class.where(id: id)) + .base_and_descendants + end + def user_ids_for_project_authorizations [owner_id] end diff --git a/app/models/network/commit.rb b/app/models/network/commit.rb index 8417f200e36..9357e55b419 100644 --- a/app/models/network/commit.rb +++ b/app/models/network/commit.rb @@ -12,7 +12,7 @@ module Network end def method_missing(m, *args, &block) - @commit.send(m, *args, &block) + @commit.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end def space diff --git a/app/models/project.rb b/app/models/project.rb index 7cdd00bc17b..22b347cc8f9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -196,7 +196,6 @@ class Project < ActiveRecord::Base accepts_nested_attributes_for :import_data delegate :name, to: :owner, allow_nil: true, prefix: true - delegate :count, to: :forks, prefix: true delegate :members, to: :team, prefix: true delegate :add_user, :add_users, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team @@ -921,14 +920,14 @@ class Project < ActiveRecord::Base end def execute_hooks(data, hooks_scope = :push_hooks) - hooks.send(hooks_scope).each do |hook| + hooks.public_send(hooks_scope).each do |hook| # rubocop:disable GitlabSecurity/PublicSend hook.async_execute(data, hooks_scope.to_s) end end def execute_services(data, hooks_scope = :push_hooks) # Call only service hooks that are active for this scope - services.send(hooks_scope).each do |service| + services.public_send(hooks_scope).each do |service| # rubocop:disable GitlabSecurity/PublicSend service.async_execute(data) end end @@ -1283,12 +1282,16 @@ class Project < ActiveRecord::Base status.zero? end + def full_path_slug + Gitlab::Utils.slugify(full_path.to_s) + end + def predefined_variables [ { key: 'CI_PROJECT_ID', value: id.to_s, public: true }, { key: 'CI_PROJECT_NAME', value: path, public: true }, { key: 'CI_PROJECT_PATH', value: full_path, public: true }, - { key: 'CI_PROJECT_PATH_SLUG', value: full_path.parameterize, public: true }, + { key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug, public: true }, { key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: web_url, public: true } ] @@ -1396,6 +1399,10 @@ class Project < ActiveRecord::Base # @deprecated cannot remove yet because it has an index with its name in elasticsearch alias_method :path_with_namespace, :full_path + def forks_count + Projects::ForksCountService.new(self).count + end + private def cross_namespace_reference?(from) diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index 6d1a321f651..7b15a5dd04d 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -115,7 +115,7 @@ class ChatNotificationService < Service def get_channel_field(event) field_name = event_channel_name(event) - self.public_send(field_name) + self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend end def build_event_channels diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index e3906943ecd..f422e0ea036 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -53,7 +53,7 @@ class HipchatService < Service return unless supported_events.include?(data[:object_kind]) message = create_message(data) return unless message.present? - gate[room].send('GitLab', message, message_options(data)) + gate[room].send('GitLab', message, message_options(data)) # rubocop:disable GitlabSecurity/PublicSend end def test(data) diff --git a/app/models/protectable_dropdown.rb b/app/models/protectable_dropdown.rb index 122fbce257d..c96edc5a259 100644 --- a/app/models/protectable_dropdown.rb +++ b/app/models/protectable_dropdown.rb @@ -1,5 +1,9 @@ class ProtectableDropdown + REF_TYPES = %i[branches tags].freeze + def initialize(project, ref_type) + raise ArgumentError, "invalid ref type `#{ref_type}`" unless ref_type.in?(REF_TYPES) + @project = project @ref_type = ref_type end @@ -16,7 +20,7 @@ class ProtectableDropdown private def refs - @project.repository.public_send(@ref_type) + @project.repository.public_send(@ref_type) # rubocop:disable GitlabSecurity/PublicSend end def ref_names @@ -24,7 +28,7 @@ class ProtectableDropdown end def protections - @project.public_send("protected_#{@ref_type}") + @project.public_send("protected_#{@ref_type}") # rubocop:disable GitlabSecurity/PublicSend end def non_wildcard_protected_ref_names diff --git a/app/models/redirect_route.rb b/app/models/redirect_route.rb index 090fbd61e6f..31de204d824 100644 --- a/app/models/redirect_route.rb +++ b/app/models/redirect_route.rb @@ -14,7 +14,7 @@ class RedirectRoute < ActiveRecord::Base else 'redirect_routes.path = ? OR redirect_routes.path LIKE ?' end - + where(wheres, path, "#{sanitize_sql_like(path)}/%") end end diff --git a/app/models/repository.rb b/app/models/repository.rb index a761302b06b..c1e4fcf94a4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -48,7 +48,9 @@ class Repository alias_method(original, name) define_method(name) do - cache_method_output(name, fallback: fallback, memoize_only: memoize_only) { __send__(original) } + cache_method_output(name, fallback: fallback, memoize_only: memoize_only) do + __send__(original) # rubocop:disable GitlabSecurity/PublicSend + end end end @@ -443,9 +445,9 @@ class Repository def method_missing(m, *args, &block) if m == :lookup && !block_given? lookup_cache[m] ||= {} - lookup_cache[m][args.join(":")] ||= raw_repository.send(m, *args, &block) + lookup_cache[m][args.join(":")] ||= raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend else - raw_repository.send(m, *args, &block) + raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end end @@ -776,7 +778,7 @@ class Repository end actions.each do |options| - index.public_send(options.delete(:action), options) + index.public_send(options.delete(:action), options) # rubocop:disable GitlabSecurity/PublicSend end options = { diff --git a/app/models/user.rb b/app/models/user.rb index 2b25736bb26..0e2654ff757 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1070,7 +1070,7 @@ class User < ActiveRecord::Base # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration def send_devise_notification(notification, *args) return true unless can?(:receive_notifications) - devise_mailer.send(notification, self, *args).deliver_later + devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend end # This works around a bug in Devise 4.2.0 that erroneously causes a user to diff --git a/app/serializers/project_entity.rb b/app/serializers/project_entity.rb index dc283ba3e7a..b3e5fd21e97 100644 --- a/app/serializers/project_entity.rb +++ b/app/serializers/project_entity.rb @@ -1,6 +1,6 @@ class ProjectEntity < Grape::Entity include RequestAwareEntity - + expose :id expose :name diff --git a/app/serializers/tree_root_entity.rb b/app/serializers/tree_root_entity.rb index 23b65aa4a4c..69702ae1493 100644 --- a/app/serializers/tree_root_entity.rb +++ b/app/serializers/tree_root_entity.rb @@ -1,8 +1,21 @@ # TODO: Inherit from TreeEntity, when `Tree` implements `id` and `name` like `Gitlab::Git::Tree`. class TreeRootEntity < Grape::Entity + include RequestAwareEntity + expose :path - + expose :trees, using: TreeEntity expose :blobs, using: BlobEntity expose :submodules, using: SubmoduleEntity + + expose :parent_tree_url do |tree| + path = tree.path.sub(%r{\A/}, '') + next unless path.present? + + path_segments = path.split('/') + path_segments.pop + parent_tree_path = path_segments.join('/') + + project_tree_path(request.project, File.join(request.ref, parent_tree_path)) + end end diff --git a/app/services/akismet_service.rb b/app/services/akismet_service.rb index 8e11a2a36a7..59153cbbc0a 100644 --- a/app/services/akismet_service.rb +++ b/app/services/akismet_service.rb @@ -58,7 +58,7 @@ class AkismetService } begin - akismet_client.public_send(type, options[:ip_address], options[:user_agent], params) + akismet_client.public_send(type, options[:ip_address], options[:user_agent], params) # rubocop:disable GitlabSecurity/PublicSend true rescue => e Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!") diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index 6372e5755db..ea3b8d66ed9 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -23,7 +23,7 @@ module Ci end attributes = CLONE_ACCESSORS.map do |attribute| - [attribute, build.send(attribute)] + [attribute, build.public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend end attributes.push([:user, current_user]) diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index a48d6a976f0..85c2fcf9ea6 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -11,6 +11,7 @@ module Commits def commit_change(action) raise NotImplementedError unless repository.respond_to?(action) + # rubocop:disable GitlabSecurity/PublicSend repository.public_send( action, current_user, diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index b84a6fd2b7d..4a4f2b91182 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -338,7 +338,7 @@ class IssuableBaseService < BaseService def invalidate_cache_counts(issuable, users: [], skip_project_cache: false) users.each do |user| - user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") + user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend end unless skip_project_cache diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 2e089149ca8..46c505baf8b 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -31,7 +31,7 @@ module Members source.members.find_by(condition) || source.requesters.find_by!(condition) else - source.public_send(scope).find_by!(condition) + source.public_send(scope).find_by!(condition) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index fa0c0b7175c..194413bf321 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -25,7 +25,6 @@ module MergeRequests end def after_create(issuable) - event_service.open_mr(issuable, current_user) todo_service.new_merge_request(issuable, current_user) issuable.cache_merge_request_closes_issues!(current_user) update_merge_requests_head_pipeline(issuable) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 4267879b03d..e2a80db06a6 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -1,3 +1,5 @@ +# rubocop:disable GitlabSecurity/PublicSend + # NotificationService class # # Used for notifying users with emails about different events diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 11ad4838471..54eb75ab9bf 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -128,6 +128,8 @@ module Projects project.repository.before_delete Repository.new(wiki_path, project, disk_path: repo_path).before_delete + + Projects::ForksCountService.new(project).delete_cache end end end diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index a2b23ea6171..ad67e68a86a 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -21,11 +21,17 @@ module Projects builds_access_level = @project.project_feature.builds_access_level new_project.project_feature.update_attributes(builds_access_level: builds_access_level) + refresh_forks_count + new_project end private + def refresh_forks_count + Projects::ForksCountService.new(@project).refresh_cache + end + def allowed_visibility_level project_level = @project.visibility_level diff --git a/app/services/projects/forks_count_service.rb b/app/services/projects/forks_count_service.rb new file mode 100644 index 00000000000..e2e2b1da91d --- /dev/null +++ b/app/services/projects/forks_count_service.rb @@ -0,0 +1,30 @@ +module Projects + # Service class for getting and caching the number of forks of a project. + class ForksCountService + def initialize(project) + @project = project + end + + def count + Rails.cache.fetch(cache_key) { uncached_count } + end + + def refresh_cache + Rails.cache.write(cache_key, uncached_count) + end + + def delete_cache + Rails.cache.delete(cache_key) + end + + private + + def uncached_count + @project.forks.count + end + + def cache_key + ['projects', @project.id, 'forks_count'] + end + end +end diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb index f385e426827..f30b40423c8 100644 --- a/app/services/projects/unlink_fork_service.rb +++ b/app/services/projects/unlink_fork_service.rb @@ -13,7 +13,13 @@ module Projects ::MergeRequests::CloseService.new(@project, @current_user).execute(mr) end + refresh_forks_count(@project.forked_from_project) + @project.forked_project_link.destroy end + + def refresh_forks_count(project) + Projects::ForksCountService.new(project).refresh_cache + end end end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index cbcd4478af6..a1c2f8d0180 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -4,7 +4,7 @@ class SystemHooksService end def execute_hooks(data, hooks_scope = :all) - SystemHook.public_send(hooks_scope).find_each do |hook| + SystemHook.public_send(hooks_scope).find_each do |hook| # rubocop:disable GitlabSecurity/PublicSend hook.async_execute(data, 'system_hooks') end end diff --git a/app/services/test_hooks/base_service.rb b/app/services/test_hooks/base_service.rb index 74ba814afff..4abd2c44b2f 100644 --- a/app/services/test_hooks/base_service.rb +++ b/app/services/test_hooks/base_service.rb @@ -18,7 +18,7 @@ module TestHooks end error_message = catch(:validation_error) do - sample_data = self.__send__(trigger_data_method) + sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend return hook.execute(sample_data, trigger) end diff --git a/app/views/layouts/nav/_new_admin_sidebar.html.haml b/app/views/layouts/nav/_new_admin_sidebar.html.haml index 0b4a9d92bea..3cbcd841aff 100644 --- a/app/views/layouts/nav/_new_admin_sidebar.html.haml +++ b/app/views/layouts/nav/_new_admin_sidebar.html.haml @@ -1,150 +1,151 @@ .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - .context-header - = link_to admin_root_path, title: 'Admin Overview' do - .avatar-container.s40.settings-avatar - = icon('wrench') - .project-title Admin Area - %ul.sidebar-top-level-items - = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do - = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do - .nav-icon-container - = custom_icon('overview') - %span.nav-item-name - Overview - - %ul.sidebar-sub-level-items - = nav_link(controller: :dashboard, html_options: {class: 'home'}) do - = link_to admin_root_path, title: 'Overview' do - %span - Dashboard - = nav_link(controller: [:admin, :projects]) do - = link_to admin_projects_path, title: 'Projects' do - %span - Projects - = nav_link(controller: :users) do - = link_to admin_users_path, title: 'Users' do - %span - Users - = nav_link(controller: :groups) do - = link_to admin_groups_path, title: 'Groups' do - %span - Groups - = nav_link path: 'jobs#index' do - = link_to admin_jobs_path, title: 'Jobs' do - %span - Jobs - = nav_link path: ['runners#index', 'runners#show'] do - = link_to admin_runners_path, title: 'Runners' do - %span - Runners - = nav_link path: 'cohorts#index' do - = link_to admin_cohorts_path, title: 'Cohorts' do - %span - Cohorts + .nav-sidebar-inner-scroll + .context-header + = link_to admin_root_path, title: 'Admin Overview' do + .avatar-container.s40.settings-avatar + = icon('wrench') + .project-title Admin Area + %ul.sidebar-top-level-items + = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do + = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do + .nav-icon-container + = custom_icon('overview') + %span.nav-item-name + Overview - = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do - = link_to admin_conversational_development_index_path, title: 'Monitoring' do - .nav-icon-container - = custom_icon('monitoring') - %span.nav-item-name - Monitoring + %ul.sidebar-sub-level-items + = nav_link(controller: :dashboard, html_options: {class: 'home'}) do + = link_to admin_root_path, title: 'Overview' do + %span + Dashboard + = nav_link(controller: [:admin, :projects]) do + = link_to admin_projects_path, title: 'Projects' do + %span + Projects + = nav_link(controller: :users) do + = link_to admin_users_path, title: 'Users' do + %span + Users + = nav_link(controller: :groups) do + = link_to admin_groups_path, title: 'Groups' do + %span + Groups + = nav_link path: 'jobs#index' do + = link_to admin_jobs_path, title: 'Jobs' do + %span + Jobs + = nav_link path: ['runners#index', 'runners#show'] do + = link_to admin_runners_path, title: 'Runners' do + %span + Runners + = nav_link path: 'cohorts#index' do + = link_to admin_cohorts_path, title: 'Cohorts' do + %span + Cohorts - %ul.sidebar-sub-level-items - = nav_link(controller: :conversational_development_index) do - = link_to admin_conversational_development_index_path, title: 'ConvDev Index' do - %span - ConvDev Index - = nav_link(controller: :system_info) do - = link_to admin_system_info_path, title: 'System Info' do - %span - System Info - = nav_link(controller: :background_jobs) do - = link_to admin_background_jobs_path, title: 'Background Jobs' do - %span - Background Jobs - = nav_link(controller: :logs) do - = link_to admin_logs_path, title: 'Logs' do - %span - Logs - = nav_link(controller: :health_check) do - = link_to admin_health_check_path, title: 'Health Check' do - %span - Health Check - = nav_link(controller: :requests_profiles) do - = link_to admin_requests_profiles_path, title: 'Requests Profiles' do - %span - Requests Profiles + = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do + = link_to admin_conversational_development_index_path, title: 'Monitoring' do + .nav-icon-container + = custom_icon('monitoring') + %span.nav-item-name + Monitoring - = nav_link(controller: :broadcast_messages) do - = link_to admin_broadcast_messages_path, title: 'Messages' do - .nav-icon-container - = custom_icon('messages') - %span.nav-item-name - Messages - = nav_link(controller: [:hooks, :hook_logs]) do - = link_to admin_hooks_path, title: 'Hooks' do - .nav-icon-container - = custom_icon('system_hooks') - %span.nav-item-name - System Hooks + %ul.sidebar-sub-level-items + = nav_link(controller: :conversational_development_index) do + = link_to admin_conversational_development_index_path, title: 'ConvDev Index' do + %span + ConvDev Index + = nav_link(controller: :system_info) do + = link_to admin_system_info_path, title: 'System Info' do + %span + System Info + = nav_link(controller: :background_jobs) do + = link_to admin_background_jobs_path, title: 'Background Jobs' do + %span + Background Jobs + = nav_link(controller: :logs) do + = link_to admin_logs_path, title: 'Logs' do + %span + Logs + = nav_link(controller: :health_check) do + = link_to admin_health_check_path, title: 'Health Check' do + %span + Health Check + = nav_link(controller: :requests_profiles) do + = link_to admin_requests_profiles_path, title: 'Requests Profiles' do + %span + Requests Profiles - = nav_link(controller: :applications) do - = link_to admin_applications_path, title: 'Applications' do - .nav-icon-container - = custom_icon('applications') - %span.nav-item-name - Applications + = nav_link(controller: :broadcast_messages) do + = link_to admin_broadcast_messages_path, title: 'Messages' do + .nav-icon-container + = custom_icon('messages') + %span.nav-item-name + Messages + = nav_link(controller: [:hooks, :hook_logs]) do + = link_to admin_hooks_path, title: 'Hooks' do + .nav-icon-container + = custom_icon('system_hooks') + %span.nav-item-name + System Hooks - = nav_link(controller: :abuse_reports) do - = link_to admin_abuse_reports_path, title: "Abuse Reports" do - .nav-icon-container - = custom_icon('abuse_reports') - %span.nav-item-name - Abuse Reports - %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) + = nav_link(controller: :applications) do + = link_to admin_applications_path, title: 'Applications' do + .nav-icon-container + = custom_icon('applications') + %span.nav-item-name + Applications - - if akismet_enabled? - = nav_link(controller: :spam_logs) do - = link_to admin_spam_logs_path, title: "Spam Logs" do + = nav_link(controller: :abuse_reports) do + = link_to admin_abuse_reports_path, title: "Abuse Reports" do .nav-icon-container - = custom_icon('spam_logs') + = custom_icon('abuse_reports') %span.nav-item-name - Spam Logs + Abuse Reports + %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) - = nav_link(controller: :deploy_keys) do - = link_to admin_deploy_keys_path, title: 'Deploy Keys' do - .nav-icon-container - = custom_icon('key') - %span.nav-item-name - Deploy Keys + - if akismet_enabled? + = nav_link(controller: :spam_logs) do + = link_to admin_spam_logs_path, title: "Spam Logs" do + .nav-icon-container + = custom_icon('spam_logs') + %span.nav-item-name + Spam Logs - = nav_link(controller: :services) do - = link_to admin_application_settings_services_path, title: 'Service Templates' do - .nav-icon-container - = custom_icon('service_templates') - %span.nav-item-name - Service Templates + = nav_link(controller: :deploy_keys) do + = link_to admin_deploy_keys_path, title: 'Deploy Keys' do + .nav-icon-container + = custom_icon('key') + %span.nav-item-name + Deploy Keys - = nav_link(controller: :labels) do - = link_to admin_labels_path, title: 'Labels' do - .nav-icon-container - = custom_icon('labels') - %span.nav-item-name - Labels + = nav_link(controller: :services) do + = link_to admin_application_settings_services_path, title: 'Service Templates' do + .nav-icon-container + = custom_icon('service_templates') + %span.nav-item-name + Service Templates - = nav_link(controller: :appearances) do - = link_to admin_appearances_path, title: 'Appearances' do - .nav-icon-container - = custom_icon('appearance') - %span.nav-item-name - Appearance + = nav_link(controller: :labels) do + = link_to admin_labels_path, title: 'Labels' do + .nav-icon-container + = custom_icon('labels') + %span.nav-item-name + Labels - %li.divider - = nav_link(controller: :application_settings) do - = link_to admin_application_settings_path, title: 'Settings' do - .nav-icon-container - = custom_icon('settings') - %span.nav-item-name - Settings + = nav_link(controller: :appearances) do + = link_to admin_appearances_path, title: 'Appearances' do + .nav-icon-container + = custom_icon('appearance') + %span.nav-item-name + Appearance + + %li.divider + = nav_link(controller: :application_settings) do + = link_to admin_application_settings_path, title: 'Settings' do + .nav-icon-container + = custom_icon('settings') + %span.nav-item-name + Settings - = render 'shared/sidebar_toggle_button' + = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/_new_group_sidebar.html.haml b/app/views/layouts/nav/_new_group_sidebar.html.haml index c7dabbd8237..ed5793f09fe 100644 --- a/app/views/layouts/nav/_new_group_sidebar.html.haml +++ b/app/views/layouts/nav/_new_group_sidebar.html.haml @@ -1,89 +1,90 @@ .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - .context-header - = link_to group_path(@group), title: @group.name do - .avatar-container.s40.group-avatar - = image_tag group_icon(@group), class: "avatar s40 avatar-tile" - .group-title - = @group.name - %ul.sidebar-top-level-items - = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to group_path(@group), title: 'Group overview' do - .nav-icon-container - = custom_icon('project') - %span.nav-item-name - Overview - - %ul.sidebar-sub-level-items - = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to group_path(@group), title: 'Group details' do - %span - Details - - = nav_link(path: 'groups#activity') do - = link_to activity_group_path(@group), title: 'Activity' do - %span - Activity - - = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do - = link_to issues_group_path(@group), title: 'Issues' do - .nav-icon-container - = custom_icon('issues') - %span.nav-item-name - - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - Issues - %span.badge.count= number_with_delimiter(issues.count) - - %ul.sidebar-sub-level-items - = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do - = link_to issues_group_path(@group), title: 'List' do - %span - List + .nav-sidebar-inner-scroll + .context-header + = link_to group_path(@group), title: @group.name do + .avatar-container.s40.group-avatar + = image_tag group_icon(@group), class: "avatar s40 avatar-tile" + .group-title + = @group.name + %ul.sidebar-top-level-items + = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do + = link_to group_path(@group), title: 'Group overview' do + .nav-icon-container + = custom_icon('project') + %span.nav-item-name + Overview - = nav_link(path: 'labels#index') do - = link_to group_labels_path(@group), title: 'Labels' do - %span - Labels + %ul.sidebar-sub-level-items + = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do + = link_to group_path(@group), title: 'Group details' do + %span + Details - = nav_link(path: 'milestones#index') do - = link_to group_milestones_path(@group), title: 'Milestones' do - %span - Milestones + = nav_link(path: 'groups#activity') do + = link_to activity_group_path(@group), title: 'Activity' do + %span + Activity - = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group), title: 'Merge Requests' do - .nav-icon-container - = custom_icon('mr_bold') - %span.nav-item-name - - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute - Merge Requests - %span.badge.count= number_with_delimiter(merge_requests.count) - = nav_link(path: 'group_members#index') do - = link_to group_group_members_path(@group), title: 'Members' do - .nav-icon-container - = custom_icon('members') - %span.nav-item-name - Members - - if current_user && can?(current_user, :admin_group, @group) - = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do - = link_to edit_group_path(@group), title: 'Settings' do + = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do + = link_to issues_group_path(@group), title: 'Issues' do .nav-icon-container - = custom_icon('settings') + = custom_icon('issues') %span.nav-item-name - Settings + - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute + Issues + %span.badge.count= number_with_delimiter(issues.count) + %ul.sidebar-sub-level-items - = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group), title: 'General' do + = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do + = link_to issues_group_path(@group), title: 'List' do %span - General + List - = nav_link(path: 'groups#projects') do - = link_to projects_group_path(@group), title: 'Projects' do + = nav_link(path: 'labels#index') do + = link_to group_labels_path(@group), title: 'Labels' do %span - Projects + Labels - = nav_link(controller: :ci_cd) do - = link_to group_settings_ci_cd_path(@group), title: 'CI / CD' do + = nav_link(path: 'milestones#index') do + = link_to group_milestones_path(@group), title: 'Milestones' do %span - CI / CD + Milestones + + = nav_link(path: 'groups#merge_requests') do + = link_to merge_requests_group_path(@group), title: 'Merge Requests' do + .nav-icon-container + = custom_icon('mr_bold') + %span.nav-item-name + - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute + Merge Requests + %span.badge.count= number_with_delimiter(merge_requests.count) + = nav_link(path: 'group_members#index') do + = link_to group_group_members_path(@group), title: 'Members' do + .nav-icon-container + = custom_icon('members') + %span.nav-item-name + Members + - if current_user && can?(current_user, :admin_group, @group) + = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do + = link_to edit_group_path(@group), title: 'Settings' do + .nav-icon-container + = custom_icon('settings') + %span.nav-item-name + Settings + %ul.sidebar-sub-level-items + = nav_link(path: 'groups#edit') do + = link_to edit_group_path(@group), title: 'General' do + %span + General + + = nav_link(path: 'groups#projects') do + = link_to projects_group_path(@group), title: 'Projects' do + %span + Projects + + = nav_link(controller: :ci_cd) do + = link_to group_settings_ci_cd_path(@group), title: 'CI / CD' do + %span + CI / CD - = render 'shared/sidebar_toggle_button' + = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/_new_profile_sidebar.html.haml b/app/views/layouts/nav/_new_profile_sidebar.html.haml index edae009a28e..4234df56d1d 100644 --- a/app/views/layouts/nav/_new_profile_sidebar.html.haml +++ b/app/views/layouts/nav/_new_profile_sidebar.html.haml @@ -1,84 +1,85 @@ .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - .context-header - = link_to profile_path, title: 'Profile Settings' do - .avatar-container.s40.settings-avatar - = icon('user') - .project-title User Settings - %ul.sidebar-top-level-items - = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do + .nav-sidebar-inner-scroll + .context-header = link_to profile_path, title: 'Profile Settings' do - .nav-icon-container - = custom_icon('profile') - %span.nav-item-name - Profile - = nav_link(controller: [:accounts, :two_factor_auths]) do - = link_to profile_account_path, title: 'Account' do - .nav-icon-container - = custom_icon('account') - %span.nav-item-name - Account - - if current_application_settings.user_oauth_applications? - = nav_link(controller: 'oauth/applications') do - = link_to applications_profile_path, title: 'Applications' do + .avatar-container.s40.settings-avatar + = icon('user') + .project-title User Settings + %ul.sidebar-top-level-items + = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do + = link_to profile_path, title: 'Profile Settings' do .nav-icon-container - = custom_icon('applications') + = custom_icon('profile') %span.nav-item-name - Applications - = nav_link(controller: :chat_names) do - = link_to profile_chat_names_path, title: 'Chat' do - .nav-icon-container - = custom_icon('chat') - %span.nav-item-name - Chat - = nav_link(controller: :personal_access_tokens) do - = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do - .nav-icon-container - = custom_icon('access_tokens') - %span.nav-item-name - Access Tokens - = nav_link(controller: :emails) do - = link_to profile_emails_path, title: 'Emails' do - .nav-icon-container - = custom_icon('emails') - %span.nav-item-name - Emails - - unless current_user.ldap_user? - = nav_link(controller: :passwords) do - = link_to edit_profile_password_path, title: 'Password' do + Profile + = nav_link(controller: [:accounts, :two_factor_auths]) do + = link_to profile_account_path, title: 'Account' do .nav-icon-container - = custom_icon('lock') + = custom_icon('account') %span.nav-item-name - Password - = nav_link(controller: :notifications) do - = link_to profile_notifications_path, title: 'Notifications' do - .nav-icon-container - = custom_icon('notifications') - %span.nav-item-name - Notifications + Account + - if current_application_settings.user_oauth_applications? + = nav_link(controller: 'oauth/applications') do + = link_to applications_profile_path, title: 'Applications' do + .nav-icon-container + = custom_icon('applications') + %span.nav-item-name + Applications + = nav_link(controller: :chat_names) do + = link_to profile_chat_names_path, title: 'Chat' do + .nav-icon-container + = custom_icon('chat') + %span.nav-item-name + Chat + = nav_link(controller: :personal_access_tokens) do + = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do + .nav-icon-container + = custom_icon('access_tokens') + %span.nav-item-name + Access Tokens + = nav_link(controller: :emails) do + = link_to profile_emails_path, title: 'Emails' do + .nav-icon-container + = custom_icon('emails') + %span.nav-item-name + Emails + - unless current_user.ldap_user? + = nav_link(controller: :passwords) do + = link_to edit_profile_password_path, title: 'Password' do + .nav-icon-container + = custom_icon('lock') + %span.nav-item-name + Password + = nav_link(controller: :notifications) do + = link_to profile_notifications_path, title: 'Notifications' do + .nav-icon-container + = custom_icon('notifications') + %span.nav-item-name + Notifications - = nav_link(controller: :keys) do - = link_to profile_keys_path, title: 'SSH Keys' do - .nav-icon-container - = custom_icon('key') - %span.nav-item-name - SSH Keys - = nav_link(controller: :gpg_keys) do - = link_to profile_gpg_keys_path, title: 'GPG Keys' do - .nav-icon-container - = custom_icon('key_2') - %span.nav-item-name - GPG Keys - = nav_link(controller: :preferences) do - = link_to profile_preferences_path, title: 'Preferences' do - .nav-icon-container - = custom_icon('preferences') - %span.nav-item-name - Preferences - = nav_link(path: 'profiles#audit_log') do - = link_to audit_log_profile_path, title: 'Authentication log' do - .nav-icon-container - = custom_icon('authentication_log') - %span.nav-item-name - Authentication log + = nav_link(controller: :keys) do + = link_to profile_keys_path, title: 'SSH Keys' do + .nav-icon-container + = custom_icon('key') + %span.nav-item-name + SSH Keys + = nav_link(controller: :gpg_keys) do + = link_to profile_gpg_keys_path, title: 'GPG Keys' do + .nav-icon-container + = custom_icon('key_2') + %span.nav-item-name + GPG Keys + = nav_link(controller: :preferences) do + = link_to profile_preferences_path, title: 'Preferences' do + .nav-icon-container + = custom_icon('preferences') + %span.nav-item-name + Preferences + = nav_link(path: 'profiles#audit_log') do + = link_to audit_log_profile_path, title: 'Authentication log' do + .nav-icon-container + = custom_icon('authentication_log') + %span.nav-item-name + Authentication log - = render 'shared/sidebar_toggle_button' + = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml index e0477c29ebe..0ef81375c3a 100644 --- a/app/views/layouts/nav/_new_project_sidebar.html.haml +++ b/app/views/layouts/nav/_new_project_sidebar.html.haml @@ -1,261 +1,262 @@ .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - - can_edit = can?(current_user, :admin_project, @project) - .context-header - = link_to project_path(@project), title: @project.name do - .avatar-container.s40.project-avatar - = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile') - .project-title - = @project.name - %ul.sidebar-top-level-items - = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do - = link_to project_path(@project), title: 'Project overview', class: 'shortcuts-project' do - .nav-icon-container - = custom_icon('project') - %span.nav-item-name - Overview - - %ul.sidebar-sub-level-items - = nav_link(path: 'projects#show') do - = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do - %span= _('Details') - - = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do - %span= _('Activity') - - - if can?(current_user, :read_cycle_analytics, @project) - = nav_link(path: 'cycle_analytics#show') do - = link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do - %span= _('Cycle Analytics') - - - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do - = link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do + .nav-sidebar-inner-scroll + - can_edit = can?(current_user, :admin_project, @project) + .context-header + = link_to project_path(@project), title: @project.name do + .avatar-container.s40.project-avatar + = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile') + .project-title + = @project.name + %ul.sidebar-top-level-items + = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do + = link_to project_path(@project), title: 'Project overview', class: 'shortcuts-project' do .nav-icon-container - = custom_icon('doc_text') + = custom_icon('project') %span.nav-item-name - Repository + Overview %ul.sidebar-sub-level-items - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do - = link_to project_tree_path(@project) do - #{ _('Files') } - - = nav_link(controller: [:commit, :commits]) do - = link_to project_commits_path(@project, current_ref) do - #{ _('Commits') } - - = nav_link(html_options: {class: branches_tab_class}) do - = link_to project_branches_path(@project) do - #{ _('Branches') } - - = nav_link(controller: [:tags, :releases]) do - = link_to project_tags_path(@project) do - #{ _('Tags') } - - = nav_link(path: 'graphs#show') do - = link_to project_graph_path(@project, current_ref) do - #{ _('Contributors') } - - = nav_link(controller: %w(network)) do - = link_to project_network_path(@project, current_ref) do - #{ s_('ProjectNetworkGraph|Graph') } - - = nav_link(controller: :compare) do - = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do - #{ _('Compare') } - - = nav_link(path: 'graphs#charts') do - = link_to charts_project_graph_path(@project, current_ref) do - #{ _('Charts') } - - - if project_nav_tab? :container_registry - = nav_link(controller: %w[projects/registry/repositories]) do - = link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do - .nav-icon-container - = custom_icon('container_registry') - %span.nav-item-name - Registry - - - if project_nav_tab? :issues - = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do - = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do - .nav-icon-container - = custom_icon('issues') - %span.nav-item-name - Issues - - if @project.issues_enabled? - %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) - - %ul.sidebar-sub-level-items - = nav_link(controller: :issues) do - = link_to project_issues_path(@project), title: 'Issues' do - %span - List - - = nav_link(controller: :boards) do - = link_to project_boards_path(@project), title: 'Board' do - %span - Board - - = nav_link(controller: :labels) do - = link_to project_labels_path(@project), title: 'Labels' do - %span - Labels - - = nav_link(controller: :milestones) do - = link_to project_milestones_path(@project), title: 'Milestones' do - %span - Milestones - - - if project_nav_tab? :merge_requests - = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do - = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do - .nav-icon-container - = custom_icon('mr_bold') - %span.nav-item-name - Merge Requests - %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) - - - if project_nav_tab? :pipelines - = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do - = link_to project_pipelines_path(@project), title: 'CI / CD', class: 'shortcuts-pipelines' do - .nav-icon-container - = custom_icon('pipeline') - %span.nav-item-name - CI / CD - - %ul.sidebar-sub-level-items - - if project_nav_tab? :pipelines - = nav_link(path: ['pipelines#index', 'pipelines#show']) do - = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do - %span - Pipelines - - - if project_nav_tab? :builds - = nav_link(controller: [:jobs, :artifacts]) do - = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do + = nav_link(path: 'projects#show') do + = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do + %span= _('Details') + + = nav_link(path: 'projects#activity') do + = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do + %span= _('Activity') + + - if can?(current_user, :read_cycle_analytics, @project) + = nav_link(path: 'cycle_analytics#show') do + = link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do + %span= _('Cycle Analytics') + + - if project_nav_tab? :files + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do + = link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do + .nav-icon-container + = custom_icon('doc_text') + %span.nav-item-name + Repository + + %ul.sidebar-sub-level-items + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_tree_path(@project) do + #{ _('Files') } + + = nav_link(controller: [:commit, :commits]) do + = link_to project_commits_path(@project, current_ref) do + #{ _('Commits') } + + = nav_link(html_options: {class: branches_tab_class}) do + = link_to project_branches_path(@project) do + #{ _('Branches') } + + = nav_link(controller: [:tags, :releases]) do + = link_to project_tags_path(@project) do + #{ _('Tags') } + + = nav_link(path: 'graphs#show') do + = link_to project_graph_path(@project, current_ref) do + #{ _('Contributors') } + + = nav_link(controller: %w(network)) do + = link_to project_network_path(@project, current_ref) do + #{ s_('ProjectNetworkGraph|Graph') } + + = nav_link(controller: :compare) do + = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do + #{ _('Compare') } + + = nav_link(path: 'graphs#charts') do + = link_to charts_project_graph_path(@project, current_ref) do + #{ _('Charts') } + + - if project_nav_tab? :container_registry + = nav_link(controller: %w[projects/registry/repositories]) do + = link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do + .nav-icon-container + = custom_icon('container_registry') + %span.nav-item-name + Registry + + - if project_nav_tab? :issues + = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do + = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do + .nav-icon-container + = custom_icon('issues') + %span.nav-item-name + Issues + - if @project.issues_enabled? + %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) + + %ul.sidebar-sub-level-items + = nav_link(controller: :issues) do + = link_to project_issues_path(@project), title: 'Issues' do %span - Jobs + List - - if project_nav_tab? :pipelines - = nav_link(controller: :pipeline_schedules) do - = link_to pipeline_schedules_path(@project), title: 'Schedules', class: 'shortcuts-builds' do + = nav_link(controller: :boards) do + = link_to project_boards_path(@project), title: 'Board' do %span - Schedules + Board - - if project_nav_tab? :environments - = nav_link(controller: :environments) do - = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do + = nav_link(controller: :labels) do + = link_to project_labels_path(@project), title: 'Labels' do %span - Environments + Labels - - if @project.feature_available?(:builds, current_user) && !@project.empty_repo? - = nav_link(path: 'pipelines#charts') do - = link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do + = nav_link(controller: :milestones) do + = link_to project_milestones_path(@project), title: 'Milestones' do %span - Charts + Milestones + + - if project_nav_tab? :merge_requests + = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do + = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do + .nav-icon-container + = custom_icon('mr_bold') + %span.nav-item-name + Merge Requests + %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) + + - if project_nav_tab? :pipelines + = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do + = link_to project_pipelines_path(@project), title: 'CI / CD', class: 'shortcuts-pipelines' do + .nav-icon-container + = custom_icon('pipeline') + %span.nav-item-name + CI / CD + + %ul.sidebar-sub-level-items + - if project_nav_tab? :pipelines + = nav_link(path: ['pipelines#index', 'pipelines#show']) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + %span + Pipelines - - if project_nav_tab? :wiki - = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do - .nav-icon-container - = custom_icon('wiki') - %span.nav-item-name - Wiki + - if project_nav_tab? :builds + = nav_link(controller: [:jobs, :artifacts]) do + = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do + %span + Jobs - - if project_nav_tab? :snippets - = nav_link(controller: :snippets) do - = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do - .nav-icon-container - = custom_icon('snippets') - %span.nav-item-name - Snippets + - if project_nav_tab? :pipelines + = nav_link(controller: :pipeline_schedules) do + = link_to pipeline_schedules_path(@project), title: 'Schedules', class: 'shortcuts-builds' do + %span + Schedules - - if project_nav_tab? :settings - = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do - = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do - .nav-icon-container - = custom_icon('settings') - %span.nav-item-name - Settings + - if project_nav_tab? :environments + = nav_link(controller: :environments) do + = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do + %span + Environments - %ul.sidebar-sub-level-items - - can_edit = can?(current_user, :admin_project, @project) - - if can_edit - = nav_link(path: %w[projects#edit]) do - = link_to edit_project_path(@project), title: 'General' do - %span - General - = nav_link(controller: :project_members) do - = link_to project_project_members_path(@project), title: 'Members' do - %span - Members - - if can_edit - = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do - = link_to project_settings_integrations_path(@project), title: 'Integrations' do - %span - Integrations - = nav_link(controller: :repository) do - = link_to project_settings_repository_path(@project), title: 'Repository' do + - if @project.feature_available?(:builds, current_user) && !@project.empty_repo? + = nav_link(path: 'pipelines#charts') do + = link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do + %span + Charts + + - if project_nav_tab? :wiki + = nav_link(controller: :wikis) do + = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do + .nav-icon-container + = custom_icon('wiki') + %span.nav-item-name + Wiki + + - if project_nav_tab? :snippets + = nav_link(controller: :snippets) do + = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do + .nav-icon-container + = custom_icon('snippets') + %span.nav-item-name + Snippets + + - if project_nav_tab? :settings + = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do + = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do + .nav-icon-container + = custom_icon('settings') + %span.nav-item-name + Settings + + %ul.sidebar-sub-level-items + - can_edit = can?(current_user, :admin_project, @project) + - if can_edit + = nav_link(path: %w[projects#edit]) do + = link_to edit_project_path(@project), title: 'General' do + %span + General + = nav_link(controller: :project_members) do + = link_to project_project_members_path(@project), title: 'Members' do %span - Repository - - if @project.feature_available?(:builds, current_user) - = nav_link(controller: :ci_cd) do - = link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do + Members + - if can_edit + = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do + = link_to project_settings_integrations_path(@project), title: 'Integrations' do %span - CI / CD - - if Gitlab.config.pages.enabled - = nav_link(controller: :pages) do - = link_to project_pages_path(@project), title: 'Pages' do + Integrations + = nav_link(controller: :repository) do + = link_to project_settings_repository_path(@project), title: 'Repository' do %span - Pages - - - else - = nav_link(path: %w[members#show]) do - = link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do - .nav-icon-container - = custom_icon('members') - %span.nav-item-name - Members - - = render 'shared/sidebar_toggle_button' - - -# Shortcut to Project > Activity - %li.hidden - = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do - %span - Activity - - -# Shortcut to Repository > Graph (formerly, Network) - - if project_nav_tab? :network + Repository + - if @project.feature_available?(:builds, current_user) + = nav_link(controller: :ci_cd) do + = link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do + %span + CI / CD + - if Gitlab.config.pages.enabled + = nav_link(controller: :pages) do + = link_to project_pages_path(@project), title: 'Pages' do + %span + Pages + + - else + = nav_link(path: %w[members#show]) do + = link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do + .nav-icon-container + = custom_icon('members') + %span.nav-item-name + Members + + = render 'shared/sidebar_toggle_button' + + -# Shortcut to Project > Activity %li.hidden - = link_to project_network_path(@project, current_ref), title: 'Network', class: 'shortcuts-network' do - Graph - - -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs") - - unless @project.empty_repo? - %li.hidden - = link_to charts_project_graph_path(@project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do - Charts - - -# Shortcut to Issues > New Issue - %li.hidden - = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do - Create a new issue - - -# Shortcut to Pipelines > Jobs - - if project_nav_tab? :builds + = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do + %span + Activity + + -# Shortcut to Repository > Graph (formerly, Network) + - if project_nav_tab? :network + %li.hidden + = link_to project_network_path(@project, current_ref), title: 'Network', class: 'shortcuts-network' do + Graph + + -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs") + - unless @project.empty_repo? + %li.hidden + = link_to charts_project_graph_path(@project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do + Charts + + -# Shortcut to Issues > New Issue %li.hidden - = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do - Jobs - - -# Shortcut to commits page - - if project_nav_tab? :commits + = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do + Create a new issue + + -# Shortcut to Pipelines > Jobs + - if project_nav_tab? :builds + %li.hidden + = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do + Jobs + + -# Shortcut to commits page + - if project_nav_tab? :commits + %li.hidden + = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do + Commits + + -# Shortcut to issue boards %li.hidden - = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do - Commits - - -# Shortcut to issue boards - %li.hidden - = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards' + = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards' diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml index 22674b671c9..83821326aec 100644 --- a/app/views/projects/commit/_ajax_signature.html.haml +++ b/app/views/projects/commit/_ajax_signature.html.haml @@ -1,3 +1,2 @@ - if commit.has_signature? %button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } } - %i.fa.fa-spinner.fa-spin diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 178ab3df2e5..376f672f424 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -6,7 +6,7 @@ .content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed{ class: ("diff-files-changed-merge-request" if merge_request) } .files-changed-inner - .inline-parallel-buttons + .inline-parallel-buttons.hidden-xs.hidden-sm - if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? } = link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default' - if show_whitespace_toggle diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index efc0ea31917..02fd54c97fb 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -10,7 +10,7 @@ %strong.cgreen #{sum_added_lines} additions and %strong.cred #{sum_removed_lines} deletions - .diff-stats-additions-deletions-collapsed.pull-right{ "aria-hidden": "true", "aria-describedby": "diff-stats" } + .diff-stats-additions-deletions-collapsed.pull-right.hidden-xs.hidden-sm{ "aria-hidden": "true", "aria-describedby": "diff-stats" } %strong.cgreen< +#{sum_added_lines} %strong.cred< diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml index 756faf4625e..13809da6523 100644 --- a/app/views/projects/issues/_nav_btns.html.haml +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -1,7 +1,7 @@ = link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do = icon('rss') - if @can_bulk_update - = button_tag "Edit Issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" + = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" = link_to "New issue", new_project_issue_path(@project, issue: { assignee_id: issues_finder.assignee.try(:id), milestone_id: issues_finder.milestones.first.try(:id) }), diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml index e92f2712347..e73dab8ad4a 100644 --- a/app/views/projects/merge_requests/_nav_btns.html.haml +++ b/app/views/projects/merge_requests/_nav_btns.html.haml @@ -1,5 +1,5 @@ - if @can_bulk_update - = button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle" + = button_tag "Edit merge requests", class: "btn append-right-10 js-bulk-update-toggle" - if merge_project = link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do New merge request diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 4498c8f8349..7ad743b3b81 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -6,7 +6,7 @@ - @options && @options.each do |key, value| = hidden_field_tag key, value, id: nil .dropdown - = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" } + = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" } .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } = dropdown_title _("Switch branch/tag") = dropdown_filter _("Search branches and tags") diff --git a/app/views/shared/_target_switcher.html.haml b/app/views/shared/_target_switcher.html.haml index 3672b552f10..9236868652f 100644 --- a/app/views/shared/_target_switcher.html.haml +++ b/app/views/shared/_target_switcher.html.haml @@ -1,5 +1,5 @@ - dropdown_toggle_text = @ref || @project.default_branch -= form_tag nil, method: :get, class: "project-refs-target-form" do += form_tag nil, method: :get, style: { display: 'none' }, class: "project-refs-target-form" do = hidden_field_tag :destination, destination - if defined?(path) = hidden_field_tag :path, path diff --git a/app/views/shared/icons/_node_express.svg b/app/views/shared/icons/_express.svg index f2c94319f19..f2c94319f19 100644 --- a/app/views/shared/icons/_node_express.svg +++ b/app/views/shared/icons/_express.svg diff --git a/app/views/shared/icons/_java_spring.svg b/app/views/shared/icons/_spring.svg index 508349aa456..508349aa456 100644 --- a/app/views/shared/icons/_java_spring.svg +++ b/app/views/shared/icons/_spring.svg diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml index 0fc40cf0801..87fa2007d16 100644 --- a/app/views/shared/repo/_repo.html.haml +++ b/app/views/shared/repo/_repo.html.haml @@ -1,2 +1,7 @@ -#repo{ data: { url: content_url, project_name: project.name, refs_url: refs_project_path(project, format: :json), project_url: project_path(project), project_id: project.id, can_commit: (!!can_push_branch?(project, @ref)).to_s } } - %repo +#repo{ data: { url: content_url, + project_name: project.name, + refs_url: refs_project_path(project, format: :json), + project_url: project_path(project), + project_id: project.id, + can_commit: (!!can_push_branch?(project, @ref)).to_s, + on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } } diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb index 964287a1793..0ec871e00e1 100644 --- a/app/workers/gitlab_shell_worker.rb +++ b/app/workers/gitlab_shell_worker.rb @@ -4,6 +4,6 @@ class GitlabShellWorker include DedicatedSidekiqQueue def perform(action, *arg) - gitlab_shell.send(action, *arg) + gitlab_shell.__send__(action, *arg) # rubocop:disable GitlabSecurity/PublicSend end end |