diff options
Diffstat (limited to 'app/assets/javascripts')
51 files changed, 1041 insertions, 350 deletions
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index 1c64ccf536f..b5500ac116f 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -8,6 +8,7 @@ import BlobFileDropzone from '../blob/blob_file_dropzone'; $(() => { const editBlobForm = $('.js-edit-blob-form'); const uploadBlobForm = $('.js-upload-blob-form'); + const deleteBlobForm = $('.js-delete-blob-form'); if (editBlobForm.length) { const urlRoot = editBlobForm.data('relative-url-root'); @@ -30,4 +31,8 @@ $(() => { '.btn-upload-file', ); } + + if (deleteBlobForm.length) { + new NewCommitForm(deleteBlobForm); + } }); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 1dfa064acfd..b3d3bbcf84f 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -64,7 +64,7 @@ window.Build = (function () { $(window) .off('scroll') .on('scroll', () => { - const contentHeight = this.$buildTraceOutput.prop('scrollHeight'); + const contentHeight = this.$buildTraceOutput.height(); if (contentHeight > this.windowSize) { // means the user did not scroll, the content was updated. this.windowSize = contentHeight; @@ -105,16 +105,17 @@ window.Build = (function () { }; Build.prototype.canScroll = function () { - return document.body.scrollHeight > window.innerHeight; + return $(document).height() > $(window).height(); }; Build.prototype.toggleScroll = function () { - const currentPosition = document.body.scrollTop; - const windowHeight = window.innerHeight; + const currentPosition = $(document).scrollTop(); + const scrollHeight = $(document).height(); + const windowHeight = $(window).height(); if (this.canScroll()) { if (currentPosition > 0 && - (document.body.scrollHeight - currentPosition !== windowHeight)) { + (scrollHeight - currentPosition !== windowHeight)) { // User is in the middle of the log this.toggleDisableButton(this.$scrollTopBtn, false); @@ -124,7 +125,7 @@ window.Build = (function () { this.toggleDisableButton(this.$scrollTopBtn, true); this.toggleDisableButton(this.$scrollBottomBtn, false); - } else if (document.body.scrollHeight - currentPosition === windowHeight) { + } else if (scrollHeight - currentPosition === windowHeight) { // User is at the bottom of the build log. this.toggleDisableButton(this.$scrollTopBtn, false); @@ -137,7 +138,7 @@ window.Build = (function () { }; Build.prototype.scrollDown = function () { - document.body.scrollTop = document.body.scrollHeight; + $(document).scrollTop($(document).height()); }; Build.prototype.scrollToBottom = function () { @@ -147,7 +148,7 @@ window.Build = (function () { }; Build.prototype.scrollToTop = function () { - document.body.scrollTop = 0; + $(document).scrollTop(0); this.hasBeenScrolled = true; this.toggleScroll(); }; @@ -178,7 +179,7 @@ window.Build = (function () { this.state = log.state; } - this.windowSize = this.$buildTraceOutput.prop('scrollHeight'); + this.windowSize = this.$buildTraceOutput.height(); if (log.append) { this.$buildTraceOutput.append(log.html); diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js index 36bfe457be9..510bedbf641 100644 --- a/app/assets/javascripts/commons/bootstrap.js +++ b/app/assets/javascripts/commons/bootstrap.js @@ -8,6 +8,7 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/modal'; import 'bootstrap-sass/assets/javascripts/bootstrap/tab'; import 'bootstrap-sass/assets/javascripts/bootstrap/transition'; import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip'; +import 'bootstrap-sass/assets/javascripts/bootstrap/popover'; // custom jQuery functions $.fn.extend({ diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js index ba9d9a3e1f7..54257531284 100644 --- a/app/assets/javascripts/copy_as_gfm.js +++ b/app/assets/javascripts/copy_as_gfm.js @@ -1,6 +1,7 @@ /* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ import './lib/utils/common_utils'; +import { placeholderImage } from './lazy_loader'; const gfmRules = { // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert @@ -56,6 +57,11 @@ const gfmRules = { return text; }, }, + ImageLazyLoadFilter: { + 'img'(el, text) { + return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; + }, + }, VideoLinkFilter: { '.video-container'(el) { const videoEl = el.querySelector('video'); @@ -163,7 +169,9 @@ const gfmRules = { return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); }, 'img'(el) { - return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; + const imageSrc = el.src; + const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : (el.dataset.src || ''); + return `![${el.getAttribute('alt')}](${imageUrl})`; }, 'a.anchor'(el, text) { // Don't render a Markdown link for the anchor link inside a heading diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index ffe97c071ba..178e72a1127 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -8,6 +8,8 @@ /* global LabelsSelect */ /* global MilestoneSelect */ /* global Commit */ +/* global CommitsList */ +/* global NewBranchForm */ /* global NotificationsForm */ /* global NotificationsDropdown */ /* global GroupAvatar */ @@ -18,13 +20,20 @@ /* global Search */ /* global Admin */ /* global NamespaceSelects */ +/* global NewCommitForm */ +/* global NewBranchForm */ /* global Project */ /* global ProjectAvatar */ +/* global MergeRequest */ +/* global Compare */ /* global CompareAutocomplete */ +/* global ProjectFindFile */ /* global ProjectNew */ /* global ProjectShow */ +/* global ProjectImport */ /* global Labels */ /* global Shortcuts */ +/* global ShortcutsFindFile */ /* global Sidebar */ /* global ShortcutsWiki */ @@ -62,6 +71,10 @@ import initSettingsPanels from './settings_panels'; import initExperimentalFlags from './experimental_flags'; import OAuthRememberMe from './oauth_remember_me'; import PerformanceBar from './performance_bar'; +import initNotes from './init_notes'; +import initLegacyFilters from './init_legacy_filters'; +import initIssuableSidebar from './init_issuable_sidebar'; +import GpgBadges from './gpg_badges'; (function() { var Dispatcher; @@ -126,6 +139,8 @@ import PerformanceBar from './performance_bar'; .init(); } + const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); + switch (page) { case 'profiles:preferences:show': initExperimentalFlags(); @@ -142,7 +157,7 @@ import PerformanceBar from './performance_bar'; break; case 'projects:merge_requests:index': case 'projects:issues:index': - if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) { + if (filteredSearchEnabled) { const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests'); filteredSearchManager.setup(); } @@ -156,6 +171,8 @@ import PerformanceBar from './performance_bar'; new Issue(); shortcut_handler = new ShortcutsIssuable(); new ZenMode(); + initIssuableSidebar(); + initNotes(); break; case 'dashboard:milestones:index': new ProjectSelect(); @@ -166,9 +183,17 @@ import PerformanceBar from './performance_bar'; new Milestone(); new Sidebar(); break; - case 'groups:issues': + case 'dashboard:issues': + case 'dashboard:merge_requests': case 'groups:merge_requests': - new UsersSelect(); + new ProjectSelect(); + initLegacyFilters(); + break; + case 'groups:issues': + if (filteredSearchEnabled) { + const filteredSearchManager = new gl.FilteredSearchManager('issues'); + filteredSearchManager.setup(); + } new ProjectSelect(); break; case 'dashboard:todos:index': @@ -184,7 +209,6 @@ import PerformanceBar from './performance_bar'; break; case 'explore:groups:index': new GroupsList(); - const landingElement = document.querySelector('.js-explore-groups-landing'); if (!landingElement) break; const exploreGroupsLanding = new Landing( @@ -207,6 +231,10 @@ import PerformanceBar from './performance_bar'; case 'projects:compare:show': new gl.Diff(); break; + case 'projects:branches:new': + case 'projects:branches:create': + new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)); + break; case 'projects:branches:index': gl.AjaxLoadingSpinner.init(); new DeleteModal(); @@ -221,6 +249,19 @@ import PerformanceBar from './performance_bar'; new gl.IssuableTemplateSelectors(); break; case 'projects:merge_requests:creations:new': + const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); + if (mrNewCompareNode) { + new Compare({ + targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl, + sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl, + targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl, + }); + } else { + const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit'); + new MergeRequest({ + action: mrNewSubmitNode.dataset.mrSubmitAction, + }); + } case 'projects:merge_requests:creations:diffs': case 'projects:merge_requests:edit': new gl.Diff(); @@ -235,7 +276,10 @@ import PerformanceBar from './performance_bar'; case 'projects:tags:new': new ZenMode(); new gl.GLForm($('.tag-form'), true); - new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs); + new RefSelectDropdown($('.js-branch-select')); + break; + case 'projects:snippets:show': + initNotes(); break; case 'projects:snippets:new': case 'projects:snippets:edit': @@ -257,15 +301,18 @@ import PerformanceBar from './performance_bar'; new gl.Diff(); shortcut_handler = new ShortcutsIssuable(true); new ZenMode(); + + initIssuableSidebar(); + initNotes(); + + const mrShowNode = document.querySelector('.merge-request'); + window.mergeRequest = new MergeRequest({ + action: mrShowNode.dataset.mrAction, + }); break; case 'dashboard:activity': new gl.Activities(); break; - case 'dashboard:issues': - case 'dashboard:merge_requests': - new ProjectSelect(); - new UsersSelect(); - break; case 'projects:commit:show': new Commit(); new gl.Diff(); @@ -274,16 +321,25 @@ import PerformanceBar from './performance_bar'; new MiniPipelineGraph({ container: '.js-commit-pipeline-graph', }).bindEvents(); + initNotes(); + $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); break; case 'projects:commit:pipelines': new MiniPipelineGraph({ container: '.js-commit-pipeline-graph', }).bindEvents(); + $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); break; - case 'projects:commits:show': case 'projects:activity': + new gl.Activities(); shortcut_handler = new ShortcutsNavigation(); break; + case 'projects:commits:show': + CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit); + new gl.Activities(); + shortcut_handler = new ShortcutsNavigation(); + GpgBadges.fetch(); + break; case 'projects:show': shortcut_handler = new ShortcutsNavigation(); new NotificationsForm(); @@ -297,6 +353,12 @@ import PerformanceBar from './performance_bar'; case 'projects:edit': setupProjectEdit(); break; + case 'projects:imports:show': + new ProjectImport(); + break; + case 'projects:pipelines:new': + new NewBranchForm($('.js-new-pipeline-form')); + break; case 'projects:pipelines:builds': case 'projects:pipelines:failures': case 'projects:pipelines:show': @@ -350,8 +412,19 @@ import PerformanceBar from './performance_bar'; shortcut_handler = new ShortcutsNavigation(); new TreeView(); new BlobViewer(); + new NewCommitForm($('.js-create-dir-form')); + $('#tree-slider').waitForImages(function() { + gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); + }); break; case 'projects:find_file:show': + const findElement = document.querySelector('.js-file-finder'); + const projectFindFile = new ProjectFindFile($(".file-finder-holder"), { + url: findElement.dataset.fileFindUrl, + treeUrl: findElement.dataset.findTreeUrl, + blobUrlTemplate: findElement.dataset.blobUrlTemplate, + }); + new ShortcutsFindFile(projectFindFile); shortcut_handler = true; break; case 'projects:blob:show': @@ -367,10 +440,20 @@ import PerformanceBar from './performance_bar'; case 'projects:labels:edit': new Labels(); break; + case 'groups:labels:index': case 'projects:labels:index': if ($('.prioritized-labels').length) { new gl.LabelManager(); } + $('.label-subscription').each((i, el) => { + const $el = $(el); + + if ($el.find('.dropdown-group-label').length) { + new gl.GroupLabelSubscription($el); + } else { + new gl.ProjectLabelSubscription($el); + } + }); break; case 'projects:network:show': // Ensure we don't create a particular shortcut handler here. This is @@ -415,10 +498,15 @@ import PerformanceBar from './performance_bar'; case 'snippets:show': new LineHighlighter(); new BlobViewer(); + initNotes(); break; case 'import:fogbugz:new_user_map': new UsersSelect(); break; + case 'profiles:personal_access_tokens:index': + case 'admin:impersonation_tokens:index': + new gl.DueDateSelectors(); + break; } switch (path.first()) { case 'sessions': @@ -492,6 +580,7 @@ import PerformanceBar from './performance_bar'; shortcut_handler = new ShortcutsWiki(); new ZenMode(); new gl.GLForm($('.wiki-form'), true); + new Sidebar(); break; case 'snippets': shortcut_handler = new ShortcutsNavigation(); @@ -515,6 +604,13 @@ import PerformanceBar from './performance_bar'; case 'protected_branches': shortcut_handler = new ShortcutsNavigation(); } + break; + case 'users': + const action = path[1]; + import(/* webpackChunkName: 'user_profile' */ './users') + .then(user => user.default(action)) + .catch(() => {}); + break; } // If we haven't installed a custom shortcut handler, install the default one if (!shortcut_handler) { diff --git a/app/assets/javascripts/droplab/plugins/ajax.js b/app/assets/javascripts/droplab/plugins/ajax.js index c0da5866139..267b53fa4f2 100644 --- a/app/assets/javascripts/droplab/plugins/ajax.js +++ b/app/assets/javascripts/droplab/plugins/ajax.js @@ -11,6 +11,16 @@ const Ajax = { if (!self.destroyed) self.hook.list[config.method].call(self.hook.list, data); }, + preprocessing: function preprocessing(config, data) { + let results = data; + + if (config.preprocessing && !data.preprocessed) { + results = config.preprocessing(data); + AjaxCache.override(config.endpoint, results); + } + + return results; + }, init: function init(hook) { var self = this; self.destroyed = false; @@ -31,7 +41,8 @@ const Ajax = { dynamicList.outerHTML = loadingTemplate.outerHTML; } - AjaxCache.retrieve(config.endpoint) + return AjaxCache.retrieve(config.endpoint) + .then(self.preprocessing.bind(null, config)) .then((data) => self._loadData(data, config, self)) .catch(config.onError); }, diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js index 2615d626c4c..0bc4b6f22a9 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js @@ -6,7 +6,7 @@ import './filtered_search_dropdown'; class DropdownNonUser extends gl.FilteredSearchDropdown { constructor(options = {}) { - const { input, endpoint, symbol } = options; + const { input, endpoint, symbol, preprocessing } = options; super(options); this.symbol = symbol; this.config = { @@ -14,6 +14,7 @@ class DropdownNonUser extends gl.FilteredSearchDropdown { endpoint, method: 'setData', loadingTemplate: this.loadingTemplate, + preprocessing, onError() { /* eslint-disable no-new */ new Flash('An error occured fetching the dropdown data.'); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index ef8fe071012..9c7a4d9f6ad 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -50,6 +50,66 @@ class DropdownUtils { return updatedItem; } + static mergeDuplicateLabels(dataMap, newLabel) { + const updatedMap = dataMap; + const key = newLabel.title; + + const hasKeyProperty = Object.prototype.hasOwnProperty.call(updatedMap, key); + + if (!hasKeyProperty) { + updatedMap[key] = newLabel; + } else { + const existing = updatedMap[key]; + + if (!existing.multipleColors) { + existing.multipleColors = [existing.color]; + } + + existing.multipleColors.push(newLabel.color); + } + + return updatedMap; + } + + static duplicateLabelColor(labelColors) { + const colors = labelColors; + const spacing = 100 / colors.length; + + // Reduce the colors to 4 + colors.length = Math.min(colors.length, 4); + + const color = colors.map((c, i) => { + const percentFirst = Math.floor(spacing * i); + const percentSecond = Math.floor(spacing * (i + 1)); + return `${c} ${percentFirst}%, ${c} ${percentSecond}%`; + }).join(', '); + + return `linear-gradient(${color})`; + } + + static duplicateLabelPreprocessing(data) { + const results = []; + const dataMap = {}; + + data.forEach(DropdownUtils.mergeDuplicateLabels.bind(null, dataMap)); + + Object.keys(dataMap) + .forEach((key) => { + const label = dataMap[key]; + + if (label.multipleColors) { + label.color = DropdownUtils.duplicateLabelColor(label.multipleColors); + label.text_color = '#000000'; + } + + results.push(label); + }); + + results.preprocessed = true; + + return results; + } + static filterHint(config, item) { const { input, allowedKeys } = config; const updatedItem = item; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index 61cef435209..47cecd5b5f7 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -54,6 +54,7 @@ class FilteredSearchDropdownManager { extraArguments: { endpoint: `${this.baseEndpoint}/labels.json`, symbol: '~', + preprocessing: gl.DropdownUtils.duplicateLabelPreprocessing, }, element: this.container.querySelector('#js-dropdown-label'), }, diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 7872e9e68ad..3ce8b8607ad 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -20,13 +20,13 @@ class FilteredSearchManager { allowedKeys: this.filteredSearchTokenKeys.getKeys(), }); this.searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown'); - const projectPath = this.searchHistoryDropdownElement ? - this.searchHistoryDropdownElement.dataset.projectFullPath : 'project'; + const fullPath = this.searchHistoryDropdownElement ? + this.searchHistoryDropdownElement.dataset.fullPath : 'project'; let recentSearchesPagePrefix = 'issue-recent-searches'; if (this.page === 'merge_requests') { recentSearchesPagePrefix = 'merge-request-recent-searches'; } - const recentSearchesKey = `${projectPath}-${recentSearchesPagePrefix}`; + const recentSearchesKey = `${fullPath}-${recentSearchesPagePrefix}`; this.recentSearchesService = new RecentSearchesService(recentSearchesKey); } diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index e9278140af0..243ee4d723a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -58,29 +58,54 @@ class FilteredSearchVisualTokens { `; } + static setTokenStyle(tokenContainer, backgroundColor, textColor) { + const token = tokenContainer; + + // Labels with linear gradient should not override default background color + if (backgroundColor.indexOf('linear-gradient') === -1) { + token.style.backgroundColor = backgroundColor; + } + + token.style.color = textColor; + + if (textColor === '#FFFFFF') { + const removeToken = token.querySelector('.remove-token'); + removeToken.classList.add('inverted'); + } + + return token; + } + + static preprocessLabel(labelsEndpoint, labels) { + let processed = labels; + + if (!labels.preprocessed) { + processed = gl.DropdownUtils.duplicateLabelPreprocessing(labels); + AjaxCache.override(labelsEndpoint, processed); + processed.preprocessed = true; + } + + return processed; + } + static updateLabelTokenColor(tokenValueContainer, tokenValue) { const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search'); const baseEndpoint = filteredSearchInput.dataset.baseEndpoint; const labelsEndpoint = `${baseEndpoint}/labels.json`; return AjaxCache.retrieve(labelsEndpoint) - .then((labels) => { - const matchingLabel = (labels || []).find(label => `~${gl.DropdownUtils.getEscapedText(label.title)}` === tokenValue); - - if (!matchingLabel) { - return; - } + .then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint)) + .then((labels) => { + const matchingLabel = (labels || []).find(label => `~${gl.DropdownUtils.getEscapedText(label.title)}` === tokenValue); - const tokenValueStyle = tokenValueContainer.style; - tokenValueStyle.backgroundColor = matchingLabel.color; - tokenValueStyle.color = matchingLabel.text_color; + if (!matchingLabel) { + return; + } - if (matchingLabel.text_color === '#FFFFFF') { - const removeToken = tokenValueContainer.querySelector('.remove-token'); - removeToken.classList.add('inverted'); - } - }) - .catch(() => new Flash('An error occurred while fetching label colors.')); + FilteredSearchVisualTokens + .setTokenStyle(tokenValueContainer, matchingLabel.color, matchingLabel.text_color); + }) + .catch(() => new Flash('An error occurred while fetching label colors.')); } static updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 3babe273100..9475498e176 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -730,10 +730,10 @@ GitLabDropdown = (function() { GitLabDropdown.prototype.focusTextInput = function(triggerFocus = false) { if (this.options.filterable) { - $(':focus').blur(); - this.dropdown.one('transitionend', () => { - this.filterInput.focus(); + if (this.dropdown.is('.open')) { + this.filterInput.focus(); + } }); if (triggerFocus) { diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js new file mode 100644 index 00000000000..1c379e9bb67 --- /dev/null +++ b/app/assets/javascripts/gpg_badges.js @@ -0,0 +1,15 @@ +export default class GpgBadges { + static fetch() { + const form = $('.commits-search-form'); + + $.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/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index a433c7ba8f0..534bc535bb6 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,6 +1,4 @@ import Chart from 'vendor/Chart'; -import ContributorsStatGraph from './stat_graph_contributors'; // export to global scope window.Chart = Chart; -window.ContributorsStatGraph = ContributorsStatGraph; diff --git a/app/assets/javascripts/graphs/graphs_charts.js b/app/assets/javascripts/graphs/graphs_charts.js new file mode 100644 index 00000000000..279ffef770f --- /dev/null +++ b/app/assets/javascripts/graphs/graphs_charts.js @@ -0,0 +1,63 @@ +import Chart from 'vendor/Chart'; + +document.addEventListener('DOMContentLoaded', () => { + const projectChartData = JSON.parse(document.getElementById('projectChartData').innerHTML); + + const responsiveChart = (selector, data) => { + const options = { + scaleOverlay: true, + responsive: true, + pointHitDetectionRadius: 2, + maintainAspectRatio: false, + }; + // get selector by context + const ctx = selector.get(0).getContext('2d'); + // pointing parent container to make chart.js inherit its width + const container = $(selector).parent(); + const generateChart = () => { + selector.attr('width', $(container).width()); + if (window.innerWidth < 768) { + // Scale fonts if window width lower than 768px (iPad portrait) + options.scaleFontSize = 8; + } + return new Chart(ctx).Bar(data, options); + }; + // enabling auto-resizing + $(window).resize(generateChart); + return generateChart(); + }; + + const chartData = (keys, values) => { + const data = { + labels: keys, + datasets: [{ + fillColor: 'rgba(220,220,220,0.5)', + strokeColor: 'rgba(220,220,220,1)', + barStrokeWidth: 1, + barValueSpacing: 1, + barDatasetSpacing: 1, + data: values, + }], + }; + return data; + }; + + const hourData = chartData(projectChartData.hour.keys, projectChartData.hour.values); + responsiveChart($('#hour-chart'), hourData); + + const dayData = chartData(projectChartData.weekDays.keys, projectChartData.weekDays.values); + responsiveChart($('#weekday-chart'), dayData); + + const monthData = chartData(projectChartData.month.keys, projectChartData.month.values); + responsiveChart($('#month-chart'), monthData); + + const data = projectChartData.languages; + const ctx = $('#languages-chart').get(0).getContext('2d'); + const options = { + scaleOverlay: true, + responsive: true, + maintainAspectRatio: false, + }; + + new Chart(ctx).Pie(data, options); +}); diff --git a/app/assets/javascripts/graphs/graphs_show.js b/app/assets/javascripts/graphs/graphs_show.js new file mode 100644 index 00000000000..36bad6db3e1 --- /dev/null +++ b/app/assets/javascripts/graphs/graphs_show.js @@ -0,0 +1,21 @@ +import ContributorsStatGraph from './stat_graph_contributors'; + +document.addEventListener('DOMContentLoaded', () => { + $.ajax({ + type: 'GET', + url: document.querySelector('.js-graphs-show').dataset.projectGraphPath, + dataType: 'json', + success(data) { + const graph = new ContributorsStatGraph(); + graph.init(data); + + $('#brush_change').change(() => { + graph.change_date_header(); + graph.redraw_authors(); + }); + + $('.stat-graph').fadeIn(); + $('.loading-graph').hide(); + }, + }); +}); diff --git a/app/assets/javascripts/groups/components/group_identicon.vue b/app/assets/javascripts/groups/components/group_identicon.vue new file mode 100644 index 00000000000..0edd820743f --- /dev/null +++ b/app/assets/javascripts/groups/components/group_identicon.vue @@ -0,0 +1,45 @@ +<script> +export default { + props: { + entityId: { + type: Number, + required: true, + }, + entityName: { + type: String, + required: true, + }, + }, + computed: { + /** + * This method is based on app/helpers/application_helper.rb#project_identicon + */ + identiconStyles() { + const allowedColors = [ + '#FFEBEE', + '#F3E5F5', + '#E8EAF6', + '#E3F2FD', + '#E0F2F1', + '#FBE9E7', + '#EEEEEE', + ]; + + const backgroundColor = allowedColors[this.entityId % 7]; + + return `background-color: ${backgroundColor}; color: #555;`; + }, + identiconTitle() { + return this.entityName.charAt(0).toUpperCase(); + }, + }, +}; +</script> + +<template> + <div + class="avatar s40 identicon" + :style="identiconStyles"> + {{identiconTitle}} + </div> +</template> diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index b1db34b9c50..cb133cf7535 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -1,7 +1,11 @@ <script> import eventHub from '../event_hub'; +import groupIdenticon from './group_identicon.vue'; export default { + components: { + groupIdenticon, + }, props: { group: { type: Object, @@ -92,6 +96,9 @@ export default { hasGroups() { return Object.keys(this.group.subGroups).length > 0; }, + hasAvatar() { + return this.group.avatarUrl && this.group.avatarUrl.indexOf('/assets/no_group_avatar') === -1; + }, }, }; </script> @@ -194,9 +201,15 @@ export default { <a :href="group.groupPath"> <img + v-if="hasAvatar" class="avatar s40" :src="group.avatarUrl" /> + <group-identicon + v-else + :entity-id=group.id + :entity-name="group.name" + /> </a> </div> <div diff --git a/app/assets/javascripts/how_to_merge.js b/app/assets/javascripts/how_to_merge.js new file mode 100644 index 00000000000..19f4a946f73 --- /dev/null +++ b/app/assets/javascripts/how_to_merge.js @@ -0,0 +1,12 @@ +document.addEventListener('DOMContentLoaded', () => { + const modal = $('#modal_merge_info').modal({ + modal: true, + show: false, + }); + $('.how_to_merge_link').on('click', () => { + modal.show(); + }); + $('.modal-header .close').on('click', () => { + modal.hide(); + }); +}); diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js new file mode 100644 index 00000000000..29e3d2ea94e --- /dev/null +++ b/app/assets/javascripts/init_issuable_sidebar.js @@ -0,0 +1,18 @@ +/* eslint-disable no-new */ +/* global MilestoneSelect */ +/* global LabelsSelect */ +/* global IssuableContext */ +/* global Sidebar */ + +export default () => { + const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); + + new MilestoneSelect({ + full_path: sidebarOptions.fullPath, + }); + new LabelsSelect(); + new IssuableContext(sidebarOptions.currentUser); + gl.Subscription.bindAll('.subscription'); + new gl.DueDateSelectors(); + window.sidebar = new Sidebar(); +}; diff --git a/app/assets/javascripts/init_legacy_filters.js b/app/assets/javascripts/init_legacy_filters.js new file mode 100644 index 00000000000..1211c2c802c --- /dev/null +++ b/app/assets/javascripts/init_legacy_filters.js @@ -0,0 +1,15 @@ +/* eslint-disable no-new */ +/* global LabelsSelect */ +/* global MilestoneSelect */ +/* global IssueStatusSelect */ +/* global SubscriptionSelect */ + +import UsersSelect from './users_select'; + +export default () => { + new UsersSelect(); + new LabelsSelect(); + new MilestoneSelect(); + new IssueStatusSelect(); + new SubscriptionSelect(); +}; diff --git a/app/assets/javascripts/init_notes.js b/app/assets/javascripts/init_notes.js new file mode 100644 index 00000000000..3a8b4360cb6 --- /dev/null +++ b/app/assets/javascripts/init_notes.js @@ -0,0 +1,14 @@ +/* global Notes */ + +export default () => { + const dataEl = document.querySelector('.js-notes-data'); + const { + notesUrl, + notesIds, + now, + diffView, + autocomplete, + } = JSON.parse(dataEl.innerHTML); + + window.notes = new Notes(notesUrl, notesIds, now, diffView, autocomplete); +}; diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js index ddd3a6aab99..cf1e6a14725 100644 --- a/app/assets/javascripts/integrations/integration_settings_form.js +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -102,7 +102,7 @@ export default class IntegrationSettingsForm { }) .done((res) => { if (res.error) { - new Flash(res.message, null, null, { + new Flash(`${res.message} ${res.service_response}`, null, null, { title: 'Save anyway', clickHandler: (e) => { e.preventDefault(); diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index a4d7bf096ef..26392db4b5b 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -4,6 +4,8 @@ import Cookies from 'js-cookie'; import UsersSelect from './users_select'; +const PARTICIPANTS_ROW_COUNT = 7; + (function() { this.IssuableContext = (function() { function IssuableContext(currentUser) { @@ -50,11 +52,9 @@ import UsersSelect from './users_select'; } IssuableContext.prototype.initParticipants = function() { - var _this; - _this = this; $(document).on("click", ".js-participants-more", this.toggleHiddenParticipants); return $(".js-participants-author").each(function(i) { - if (i >= _this.PARTICIPANTS_ROW_COUNT) { + if (i >= PARTICIPANTS_ROW_COUNT) { return $(this).addClass("js-participants-hidden").hide(); } }); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 8d7d3d73571..f0e02ca0fb2 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -3,6 +3,7 @@ /* global ListLabel */ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; +import DropdownUtils from './filtered_search/dropdown_utils'; (function() { this.LabelsSelect = (function() { @@ -218,18 +219,7 @@ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; } } if (label.duplicate) { - spacing = 100 / label.color.length; - // Reduce the colors to 4 - label.color = label.color.filter(function(color, i) { - return i < 4; - }); - color = _.map(label.color, function(color, i) { - var percentFirst, percentSecond; - percentFirst = Math.floor(spacing * i); - percentSecond = Math.floor(spacing * (i + 1)); - return color + " " + percentFirst + "%," + color + " " + percentSecond + "% "; - }).join(','); - color = "linear-gradient(" + color + ")"; + color = gl.DropdownUtils.duplicateLabelColor(label.color); } else { if (label.color != null) { diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js new file mode 100644 index 00000000000..3d64b121fa7 --- /dev/null +++ b/app/assets/javascripts/lazy_loader.js @@ -0,0 +1,76 @@ +/* eslint-disable one-export, one-var, one-var-declaration-per-line */ + +import _ from 'underscore'; + +export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; +const SCROLL_THRESHOLD = 300; + +export default class LazyLoader { + constructor(options = {}) { + this.lazyImages = []; + this.observerNode = options.observerNode || '#content-body'; + + const throttledScrollCheck = _.throttle(() => this.scrollCheck(), 300); + const debouncedElementsInView = _.debounce(() => this.checkElementsInView(), 300); + + window.addEventListener('scroll', throttledScrollCheck); + window.addEventListener('resize', debouncedElementsInView); + + const scrollContainer = options.scrollContainer || window; + scrollContainer.addEventListener('load', () => this.loadCheck()); + } + searchLazyImages() { + this.lazyImages = [].slice.call(document.querySelectorAll('.lazy')); + this.checkElementsInView(); + } + startContentObserver() { + const contentNode = document.querySelector(this.observerNode) || document.querySelector('body'); + + if (contentNode) { + const observer = new MutationObserver(() => this.searchLazyImages()); + + observer.observe(contentNode, { + childList: true, + subtree: true, + }); + } + } + loadCheck() { + this.searchLazyImages(); + this.startContentObserver(); + } + scrollCheck() { + requestAnimationFrame(() => this.checkElementsInView()); + } + checkElementsInView() { + const scrollTop = pageYOffset; + const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD; + let imgBoundRect, imgTop, imgBound; + + // Loading Images which are in the current viewport or close to them + this.lazyImages = this.lazyImages.filter((selectedImage) => { + if (selectedImage.getAttribute('data-src')) { + imgBoundRect = selectedImage.getBoundingClientRect(); + + imgTop = scrollTop + imgBoundRect.top; + imgBound = imgTop + imgBoundRect.height; + + if (scrollTop < imgBound && visHeight > imgTop) { + LazyLoader.loadImage(selectedImage); + return false; + } + + return true; + } + return false; + }); + } + static loadImage(img) { + if (img.getAttribute('data-src')) { + img.setAttribute('src', img.getAttribute('data-src')); + img.removeAttribute('data-src'); + img.classList.remove('lazy'); + img.classList.add('js-lazy-loaded'); + } + } +} diff --git a/app/assets/javascripts/lib/utils/ajax_cache.js b/app/assets/javascripts/lib/utils/ajax_cache.js index 7477b5a5214..629d8f44e18 100644 --- a/app/assets/javascripts/lib/utils/ajax_cache.js +++ b/app/assets/javascripts/lib/utils/ajax_cache.js @@ -6,6 +6,10 @@ class AjaxCache extends Cache { this.pendingRequests = { }; } + override(endpoint, data) { + this.internalStorage[endpoint] = data; + } + retrieve(endpoint, forceRetrieve) { if (this.hasData(endpoint) && !forceRetrieve) { return Promise.resolve(this.get(endpoint)); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 26c67fb721c..cd45091c211 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -109,6 +109,7 @@ import './label_manager'; import './labels'; import './labels_select'; import './layout_nav'; +import LazyLoader from './lazy_loader'; import './line_highlighter'; import './logo'; import './member_expiration_date'; @@ -144,7 +145,6 @@ import './right_sidebar'; import './search'; import './search_autocomplete'; import './smart_interval'; -import './snippets_list'; import './star'; import './subscription'; import './subscription_select'; @@ -158,6 +158,8 @@ document.addEventListener('beforeunload', function () { $(document).off('scroll'); // Close any open tooltips $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); + // Close any open popover + $('[data-toggle="popover"]').popover('destroy'); }); window.addEventListener('hashchange', gl.utils.handleLocationHash); @@ -166,6 +168,11 @@ window.addEventListener('load', function onLoad() { gl.utils.handleLocationHash(); }, false); +gl.lazyLoader = new LazyLoader({ + scrollContainer: window, + observerNode: '#content-body' +}); + $(function () { var $body = $('body'); var $document = $(document); @@ -241,6 +248,11 @@ $(function () { return $(el).data('placement') || 'bottom'; } }); + // Initialize popovers + $body.popover({ + selector: '[data-toggle="popover"]', + trigger: 'focus' + }); $('.trigger-submit').on('change', function () { return $(this).parents('form').submit(); // Form submitter @@ -284,13 +296,7 @@ $(function () { return $container.remove(); // Commit show suppressed diff }); - $('.navbar-toggle').on('click', function () { - $('.header-content .title, .header-content .navbar-sub-nav').toggle(); - $('.header-content .header-logo').toggle(); - $('.header-content .navbar-collapse').toggle(); - $('.js-navbar-toggle-left, .js-navbar-toggle-right, .title-container').toggle(); - return $('.navbar-toggle').toggleClass('active'); - }); + $('.navbar-toggle').on('click', () => $('.header-content').toggleClass('menu-expanded')); // Show/hide comments on diff $body.on('click', '.js-toggle-diff-comments', function (e) { var $this = $(this); @@ -347,4 +353,14 @@ $(function () { gl.utils.renderTimeago(); $(document).trigger('init.scrolling-tabs'); + + $('form.filter-form').on('submit', function (event) { + const link = document.createElement('a'); + link.href = this.action; + + const action = `${this.action}${link.search === '' ? '?' : '&'}`; + + event.preventDefault(); + gl.utils.visitUrl(`${action}${$(this).serialize()}`); + }); }); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js index c4e379a4a0b..8be7314ded8 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js @@ -175,7 +175,7 @@ import Cookies from 'js-cookie'; getConflictsCountText() { const count = this.getConflictsCount(); - const text = count ? 'conflicts' : 'conflict'; + const text = count > 1 ? 'conflicts' : 'conflict'; return `${count} ${text}`; }, diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index 17030c3e4d3..d74cf5328ad 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -2,6 +2,7 @@ /* global Flash */ import Vue from 'vue'; +import initIssuableSidebar from '../init_issuable_sidebar'; import './merge_conflict_store'; import './merge_conflict_service'; import './mixins/line_conflict_utils'; @@ -19,6 +20,8 @@ $(() => { resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath }); + initIssuableSidebar(); + gl.MergeConflictsResolverApp = new Vue({ el: '#conflicts', data: mergeConflictsStore.state, diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 9d481d7c003..6756ab0b3aa 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -8,7 +8,7 @@ var _this, $els; if (currentProject != null) { _this = this; - this.currentProject = JSON.parse(currentProject); + this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject; } $els = $(els); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index b2c503d1656..dfa07a2def4 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -529,6 +529,7 @@ export default class Notes { form.find('#note_line_code').remove(); form.find('#note_position').remove(); form.find('#note_type').val(''); + form.find('#note_project_id').remove(); form.find('#in_reply_to_discussion_id').remove(); form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove(); this.parentTimeline = form.parents('.timeline'); @@ -556,6 +557,7 @@ export default class Notes { form.find('#note_noteable_id').val(), form.find('#note_commit_id').val(), form.find('#note_type').val(), + form.find('#note_project_id').val(), form.find('#in_reply_to_discussion_id').val(), // LegacyDiffNote @@ -848,6 +850,8 @@ export default class Notes { form.find('#in_reply_to_discussion_id').val(discussionID); } + form.find('#note_project_id').val(dataHolder.data('discussionProjectId')); + form.attr('data-line-code', dataHolder.data('lineCode')); form.find('#line_type').val(dataHolder.data('lineType')); diff --git a/app/assets/javascripts/pipelines/pipelines_charts.js b/app/assets/javascripts/pipelines/pipelines_charts.js new file mode 100644 index 00000000000..001faf4be33 --- /dev/null +++ b/app/assets/javascripts/pipelines/pipelines_charts.js @@ -0,0 +1,38 @@ +import Chart from 'vendor/Chart'; + +document.addEventListener('DOMContentLoaded', () => { + const chartData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML); + const buildChart = (chartScope) => { + const data = { + labels: chartScope.labels, + datasets: [{ + fillColor: '#7f8fa4', + strokeColor: '#7f8fa4', + pointColor: '#7f8fa4', + pointStrokeColor: '#EEE', + data: chartScope.totalValues, + }, + { + fillColor: '#44aa22', + strokeColor: '#44aa22', + pointColor: '#44aa22', + pointStrokeColor: '#fff', + data: chartScope.successValues, + }, + ], + }; + const ctx = $(`#${chartScope.scope}Chart`).get(0).getContext('2d'); + const options = { + scaleOverlay: true, + responsive: true, + maintainAspectRatio: false, + }; + if (window.innerWidth < 768) { + // Scale fonts if window width lower than 768px (iPad portrait) + options.scaleFontSize = 8; + } + new Chart(ctx).Line(data, options); + }; + + chartData.forEach(scope => buildChart(scope)); +}); diff --git a/app/assets/javascripts/pipelines/pipelines_times.js b/app/assets/javascripts/pipelines/pipelines_times.js new file mode 100644 index 00000000000..b5e7a0e53d9 --- /dev/null +++ b/app/assets/javascripts/pipelines/pipelines_times.js @@ -0,0 +1,27 @@ +import Chart from 'vendor/Chart'; + +document.addEventListener('DOMContentLoaded', () => { + const chartData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML); + const data = { + labels: chartData.labels, + datasets: [{ + fillColor: 'rgba(220,220,220,0.5)', + strokeColor: 'rgba(220,220,220,1)', + barStrokeWidth: 1, + barValueSpacing: 1, + barDatasetSpacing: 1, + data: chartData.values, + }], + }; + const ctx = $('#build_timesChart').get(0).getContext('2d'); + const options = { + scaleOverlay: true, + responsive: true, + maintainAspectRatio: false, + }; + if (window.innerWidth < 768) { + // Scale fonts if window width lower than 768px (iPad portrait) + options.scaleFontSize = 8; + } + new Chart(ctx).Bar(data, options); +}); diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index cf1566eeb87..f32b2413725 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -1,6 +1,6 @@ /* eslint-disable no-useless-escape, max-len, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */ -import 'vendor/cropper'; +import 'cropper'; ((global) => { // Matches everything but the file name diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 738e710deb9..a3f7d69b98d 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -6,21 +6,22 @@ import Cookies from 'js-cookie'; (function() { this.Project = (function() { function Project() { - $('ul.clone-options-dropdown a').click(function() { - var url; - if ($(this).hasClass('active')) { - return; - } - $('.active').not($(this)).removeClass('active'); - $(this).toggleClass('active'); - url = $("#project_clone").val(); - $('#project_clone').val(url); + const $cloneOptions = $('ul.clone-options-dropdown'); + const $projectCloneField = $('#project_clone'); + const $cloneBtnText = $('a.clone-dropdown-btn span'); + + $('a', $cloneOptions).on('click', (e) => { + const $this = $(e.currentTarget); + const url = $this.attr('href'); + + e.preventDefault(); + + $('.active', $cloneOptions).not($this).removeClass('active'); + $this.toggleClass('active'); + $projectCloneField.val(url); + $cloneBtnText.text($this.text()); + return $('.clone').text(url); - // Git protocol switcher - // Remove the active class for all buttons (ssh, http, kerberos if shown) - // Add the active class for the clicked button - // Update the input field - // Update the command line instructions }); // Ref switcher this.initRefSwitcher(); diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js new file mode 100644 index 00000000000..1dc1dbf356d --- /dev/null +++ b/app/assets/javascripts/projects/project_new.js @@ -0,0 +1,85 @@ +let hasUserDefinedProjectPath = false; + +const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => { + if ($projectImportUrl.attr('disabled') || hasUserDefinedProjectPath) { + return; + } + + let importUrl = $projectImportUrl.val().trim(); + if (importUrl.length === 0) { + return; + } + + /* + \/?: remove trailing slash + (\.git\/?)?: remove trailing .git (with optional trailing slash) + (\?.*)?: remove query string + (#.*)?: remove fragment identifier + */ + importUrl = importUrl.replace(/\/?(\.git\/?)?(\?.*)?(#.*)?$/, ''); + + // extract everything after the last slash + const pathMatch = /\/([^/]+)$/.exec(importUrl); + if (pathMatch) { + $projectPath.val(pathMatch[1]); + } +}; + +const bindEvents = () => { + const $newProjectForm = $('#new_project'); + const importBtnTooltip = 'Please enter a valid project name.'; + const $importBtnWrapper = $('.import_gitlab_project'); + const $projectImportUrl = $('#project_import_url'); + const $projectPath = $('#project_path'); + + if ($newProjectForm.length !== 1) { + return; + } + + $('.how_to_import_link').on('click', (e) => { + e.preventDefault(); + $('.how_to_import_link').next('.modal').show(); + }); + + $('.modal-header .close').on('click', () => { + $('.modal').hide(); + }); + + $('.btn_import_gitlab_project').on('click', () => { + const importHref = $('a.btn_import_gitlab_project').attr('href'); + $('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`); + }); + + $('.btn_import_gitlab_project').attr('disabled', !$projectPath.val().trim().length); + $importBtnWrapper.attr('title', importBtnTooltip); + + $newProjectForm.on('submit', () => { + $projectPath.val($projectPath.val().trim()); + }); + + $projectPath.on('keyup', () => { + hasUserDefinedProjectPath = $projectPath.val().trim().length > 0; + if (hasUserDefinedProjectPath) { + $('.btn_import_gitlab_project').attr('disabled', false); + $importBtnWrapper.attr('title', ''); + $importBtnWrapper.removeClass('has-tooltip'); + } else { + $('.btn_import_gitlab_project').attr('disabled', true); + $importBtnWrapper.addClass('has-tooltip'); + } + }); + + $projectImportUrl.disable(); + $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl, $projectPath)); + + $('.import_git').on('click', () => { + $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled')); + }); +}; + +document.addEventListener('DOMContentLoaded', bindEvents); + +export default { + bindEvents, + deriveProjectPathFromUrl, +}; diff --git a/app/assets/javascripts/ref_select_dropdown.js b/app/assets/javascripts/ref_select_dropdown.js index 215cd6fbdfd..65e4101352c 100644 --- a/app/assets/javascripts/ref_select_dropdown.js +++ b/app/assets/javascripts/ref_select_dropdown.js @@ -1,7 +1,8 @@ class RefSelectDropdown { constructor($dropdownButton, availableRefs) { + const availableRefsValue = availableRefs || JSON.parse(document.getElementById('availableRefs').innerHTML); $dropdownButton.glDropdown({ - data: availableRefs, + data: availableRefsValue, filterable: true, filterByText: true, remote: false, diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js index 2b02af87d8a..a9df66748c5 100644 --- a/app/assets/javascripts/sidebar/sidebar_bundle.js +++ b/app/assets/javascripts/sidebar/sidebar_bundle.js @@ -5,7 +5,8 @@ import sidebarAssignees from './components/assignees/sidebar_assignees'; import Mediator from './sidebar_mediator'; function domContentLoaded() { - const mediator = new Mediator(gl.sidebarOptions); + const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); + const mediator = new Mediator(sidebarOptions); mediator.fetch(); const sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees'); diff --git a/app/assets/javascripts/snippets_list.js b/app/assets/javascripts/snippets_list.js deleted file mode 100644 index 3b6d999b1c3..00000000000 --- a/app/assets/javascripts/snippets_list.js +++ /dev/null @@ -1,9 +0,0 @@ -function SnippetsList() { - const $holder = $('.snippets-list-holder'); - - $holder.find('.pagination').on('ajax:success', (e, data) => { - $holder.replaceWith(data.html); - }); -} - -window.gl.SnippetsList = SnippetsList; diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index 6d38124f1c1..3a06b477d7c 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -1,6 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */ /* global Flash */ +import { __, s__ } from './locale'; + export default class Star { constructor() { $('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) { @@ -11,10 +13,10 @@ export default class Star { toggleStar = function(isStarred) { $this.parent().find('.star-count').text(data.star_count); if (isStarred) { - $starSpan.removeClass('starred').text('Star'); + $starSpan.removeClass('starred').text(s__('StarProject|Star')); $starIcon.removeClass('fa-star').addClass('fa-star-o'); } else { - $starSpan.addClass('starred').text('Unstar'); + $starSpan.addClass('starred').text(__('Unstar')); $starIcon.removeClass('fa-star-o').addClass('fa-star'); } }; diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js index cd305631c10..bba8b5abbb4 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js @@ -37,10 +37,6 @@ export default class Todos { this.initFilterDropdown($('.js-type-search'), 'type'); this.initFilterDropdown($('.js-action-search'), 'action_id'); - $('form.filter-form').on('submit', function applyFilters(event) { - event.preventDefault(); - gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`); - }); return new UsersSelect(); } diff --git a/app/assets/javascripts/two_factor_auth.js b/app/assets/javascripts/two_factor_auth.js new file mode 100644 index 00000000000..d26f61562a5 --- /dev/null +++ b/app/assets/javascripts/two_factor_auth.js @@ -0,0 +1,13 @@ +/* global U2FRegister */ +document.addEventListener('DOMContentLoaded', () => { + const twoFactorNode = document.querySelector('.js-two-factor-auth'); + const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true'; + if (skippable) { + const button = `<a class="btn btn-xs btn-warning pull-right" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>`; + const flashAlert = document.querySelector('.flash-alert .container-fluid'); + if (flashAlert) flashAlert.insertAdjacentHTML('beforeend', button); + } + + const u2fRegister = new U2FRegister($('#js-register-u2f'), gon.u2f); + u2fRegister.start(); +}); diff --git a/app/assets/javascripts/ui_development_kit.js b/app/assets/javascripts/ui_development_kit.js new file mode 100644 index 00000000000..f503076715c --- /dev/null +++ b/app/assets/javascripts/ui_development_kit.js @@ -0,0 +1,22 @@ +import Api from './api'; + +document.addEventListener('DOMContentLoaded', () => { + $('#js-project-dropdown').glDropdown({ + data: (term, callback) => { + Api.projects(term, { + order_by: 'last_activity_at', + }, (data) => { + callback(data); + }); + }, + text: project => (project.name_with_namespace || project.name), + selectable: true, + fieldName: 'author_id', + filterable: true, + search: { + fields: ['name_with_namespace'], + }, + id: data => data.id, + isSelected: data => (data.id === 2), + }); +}); diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/users/activity_calendar.js index b7f50cfd083..f091e319f44 100644 --- a/app/assets/javascripts/users/activity_calendar.js +++ b/app/assets/javascripts/users/activity_calendar.js @@ -1,10 +1,28 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, camelcase, vars-on-top, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, max-len, class-methods-use-this */ - import d3 from 'd3'; +const LOADING_HTML = ` + <div class="text-center"> + <i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i> + </div> +`; + +function formatTooltipText({ date, count }) { + const dateObject = new Date(date); + const dateDayName = gl.utils.getDayName(dateObject); + const dateText = dateObject.format('mmm d, yyyy'); + + let contribText = 'No contributions'; + if (count > 0) { + contribText = `${count} contribution${count > 1 ? 's' : ''}`; + } + return `${contribText}<br />${dateDayName} ${dateText}`; +} + +const initColorKey = () => d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]); + export default class ActivityCalendar { - constructor(timestamps, calendar_activities_path) { - this.calendar_activities_path = calendar_activities_path; + constructor(container, timestamps, calendarActivitiesPath) { + this.calendarActivitiesPath = calendarActivitiesPath; this.clickDay = this.clickDay.bind(this); this.currentSelectedDate = ''; this.daySpace = 1; @@ -12,25 +30,26 @@ export default class ActivityCalendar { this.daySizeWithSpace = this.daySize + (this.daySpace * 2); this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; this.months = []; + // Loop through the timestamps to create a group of objects // The group of objects will be grouped based on the day of the week they are this.timestampsTmp = []; - var group = 0; + let group = 0; - var today = new Date(); + const today = new Date(); today.setHours(0, 0, 0, 0, 0); - var oneYearAgo = new Date(today); + const oneYearAgo = new Date(today); oneYearAgo.setFullYear(today.getFullYear() - 1); - var days = gl.utils.getDayDifference(oneYearAgo, today); + const days = gl.utils.getDayDifference(oneYearAgo, today); - for (var i = 0; i <= days; i += 1) { - var date = new Date(oneYearAgo); + for (let i = 0; i <= days; i += 1) { + const date = new Date(oneYearAgo); date.setDate(date.getDate() + i); - var day = date.getDay(); - var count = timestamps[date.format('yyyy-mm-dd')]; + const day = date.getDay(); + const count = timestamps[date.format('yyyy-mm-dd')] || 0; // Create a new group array if this is the first day of the week // or if is first object @@ -39,129 +58,119 @@ export default class ActivityCalendar { group += 1; } - var innerArray = this.timestampsTmp[group - 1]; // Push to the inner array the values that will be used to render map - innerArray.push({ - count: count || 0, - date: date, - day: day - }); + const innerArray = this.timestampsTmp[group - 1]; + innerArray.push({ count, date, day }); } // Init color functions - this.colorKey = this.initColorKey(); + this.colorKey = initColorKey(); this.color = this.initColor(); + // Init the svg element - this.renderSvg(group); + this.svg = this.renderSvg(container, group); this.renderDays(); this.renderMonths(); this.renderDayTitles(); this.renderKey(); - this.initTooltips(); + + // Init tooltips + $(`${container} .js-tooltip`).tooltip({ html: true }); } // Add extra padding for the last month label if it is also the last column getExtraWidthPadding(group) { - var extraWidthPadding = 0; - var lastColMonth = this.timestampsTmp[group - 1][0].date.getMonth(); - var secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth(); + let extraWidthPadding = 0; + const lastColMonth = this.timestampsTmp[group - 1][0].date.getMonth(); + const secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth(); - if (lastColMonth != secondLastColMonth) { + if (lastColMonth !== secondLastColMonth) { extraWidthPadding = 3; } return extraWidthPadding; } - renderSvg(group) { - var width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group); - return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', width).attr('height', 167).attr('class', 'contrib-calendar'); + renderSvg(container, group) { + const width = ((group + 1) * this.daySizeWithSpace) + this.getExtraWidthPadding(group); + return d3.select(container) + .append('svg') + .attr('width', width) + .attr('height', 167) + .attr('class', 'contrib-calendar'); } renderDays() { - return this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g').attr('transform', (function(_this) { - return function(group, i) { - _.each(group, function(stamp, a) { - var lastMonth, lastMonthX, month, x; + this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g') + .attr('transform', (group, i) => { + _.each(group, (stamp, a) => { if (a === 0 && stamp.day === 0) { - month = stamp.date.getMonth(); - x = (_this.daySizeWithSpace * i + 1) + _this.daySizeWithSpace; - lastMonth = _.last(_this.months); - if (lastMonth != null) { - lastMonthX = lastMonth.x; - } - if (lastMonth == null) { - return _this.months.push({ - month: month, - x: x - }); - } else if (month !== lastMonth.month && x - _this.daySizeWithSpace !== lastMonthX) { - return _this.months.push({ - month: month, - x: x - }); + const month = stamp.date.getMonth(); + const x = (this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace; + const lastMonth = _.last(this.months); + if ( + lastMonth == null || + (month !== lastMonth.month && x - this.daySizeWithSpace !== lastMonth.x) + ) { + this.months.push({ month, x }); } } }); - return "translate(" + ((_this.daySizeWithSpace * i + 1) + _this.daySizeWithSpace) + ", 18)"; - }; - })(this)).selectAll('rect').data(function(stamp) { - return stamp; - }).enter().append('rect').attr('x', '0').attr('y', (function(_this) { - return function(stamp, i) { - return _this.daySizeWithSpace * stamp.day; - }; - })(this)).attr('width', this.daySize).attr('height', this.daySize).attr('title', (function(_this) { - return function(stamp) { - var contribText, date, dateText; - date = new Date(stamp.date); - contribText = 'No contributions'; - if (stamp.count > 0) { - contribText = stamp.count + " contribution" + (stamp.count > 1 ? 's' : ''); - } - dateText = date.format('mmm d, yyyy'); - return contribText + "<br />" + (gl.utils.getDayName(date)) + " " + dateText; - }; - })(this)).attr('class', 'user-contrib-cell js-tooltip').attr('fill', (function(_this) { - return function(stamp) { - if (stamp.count !== 0) { - return _this.color(Math.min(stamp.count, 40)); - } else { - return '#ededed'; - } - }; - })(this)).attr('data-container', 'body').on('click', this.clickDay); + return `translate(${(this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace}, 18)`; + }) + .selectAll('rect') + .data(stamp => stamp) + .enter() + .append('rect') + .attr('x', '0') + .attr('y', stamp => this.daySizeWithSpace * stamp.day) + .attr('width', this.daySize) + .attr('height', this.daySize) + .attr('fill', stamp => ( + stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed' + )) + .attr('title', stamp => formatTooltipText(stamp)) + .attr('class', 'user-contrib-cell js-tooltip') + .attr('data-container', 'body') + .on('click', this.clickDay); } renderDayTitles() { - var days; - days = [ + const days = [ { text: 'M', - y: 29 + (this.daySizeWithSpace * 1) + y: 29 + (this.daySizeWithSpace * 1), }, { text: 'W', - y: 29 + (this.daySizeWithSpace * 3) + y: 29 + (this.daySizeWithSpace * 3), }, { text: 'F', - y: 29 + (this.daySizeWithSpace * 5) - } + y: 29 + (this.daySizeWithSpace * 5), + }, ]; - return this.svg.append('g').selectAll('text').data(days).enter().append('text').attr('text-anchor', 'middle').attr('x', 8).attr('y', function(day) { - return day.y; - }).text(function(day) { - return day.text; - }).attr('class', 'user-contrib-text'); + this.svg.append('g') + .selectAll('text') + .data(days) + .enter() + .append('text') + .attr('text-anchor', 'middle') + .attr('x', 8) + .attr('y', day => day.y) + .text(day => day.text) + .attr('class', 'user-contrib-text'); } renderMonths() { - return this.svg.append('g').attr('direction', 'ltr').selectAll('text').data(this.months).enter().append('text').attr('x', function(date) { - return date.x; - }).attr('y', 10).attr('class', 'user-contrib-text').text((function(_this) { - return function(date) { - return _this.monthNames[date.month]; - }; - })(this)); + this.svg.append('g') + .attr('direction', 'ltr') + .selectAll('text') + .data(this.months) + .enter() + .append('text') + .attr('x', date => date.x) + .attr('y', 10) + .attr('class', 'user-contrib-text') + .text(date => this.monthNames[date.month]); } renderKey() { @@ -169,7 +178,7 @@ export default class ActivityCalendar { const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; this.svg.append('g') - .attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`) + .attr('transform', `translate(18, ${(this.daySizeWithSpace * 8) + 16})`) .selectAll('rect') .data(keyColors) .enter() @@ -185,43 +194,31 @@ export default class ActivityCalendar { } initColor() { - var colorRange; - colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; + const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; return d3.scale.threshold().domain([0, 10, 20, 30]).range(colorRange); } - initColorKey() { - return d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]); - } - clickDay(stamp) { - var formatted_date; if (this.currentSelectedDate !== stamp.date) { this.currentSelectedDate = stamp.date; - formatted_date = this.currentSelectedDate.getFullYear() + "-" + (this.currentSelectedDate.getMonth() + 1) + "-" + this.currentSelectedDate.getDate(); - return $.ajax({ - url: this.calendar_activities_path, - data: { - date: formatted_date - }, + + const date = [ + this.currentSelectedDate.getFullYear(), + this.currentSelectedDate.getMonth() + 1, + this.currentSelectedDate.getDate(), + ].join('-'); + + $.ajax({ + url: this.calendarActivitiesPath, + data: { date }, cache: false, dataType: 'html', - beforeSend: function() { - return $('.user-calendar-activities').html('<div class="text-center"><i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i></div>'); - }, - success: function(data) { - return $('.user-calendar-activities').html(data); - } + beforeSend: () => $('.user-calendar-activities').html(LOADING_HTML), + success: data => $('.user-calendar-activities').html(data), }); } else { this.currentSelectedDate = ''; - return $('.user-calendar-activities').html(''); + $('.user-calendar-activities').html(''); } } - - initTooltips() { - return $('.js-contrib-calendar .js-tooltip').tooltip({ - html: true - }); - } } diff --git a/app/assets/javascripts/users/index.js b/app/assets/javascripts/users/index.js index ecd8e09161e..33a83f8dae5 100644 --- a/app/assets/javascripts/users/index.js +++ b/app/assets/javascripts/users/index.js @@ -1,7 +1,19 @@ -import ActivityCalendar from './activity_calendar'; -import User from './user'; +import Cookies from 'js-cookie'; +import UserTabs from './user_tabs'; -// use legacy exports until embedded javascript is refactored -window.Calendar = ActivityCalendar; -window.gl = window.gl || {}; -window.gl.User = User; +export default function initUserProfile(action) { + // place profile avatars to top + $('.profile-groups-avatars').tooltip({ + placement: 'top', + }); + + // eslint-disable-next-line no-new + new UserTabs({ parentEl: '.user-profile', action }); + + // hide project limit message + $('.hide-project-limit-message').on('click', (e) => { + e.preventDefault(); + Cookies.set('hide_project_limit_message', 'false'); + $(this).parents('.project-limit-message').remove(); + }); +} diff --git a/app/assets/javascripts/users/user.js b/app/assets/javascripts/users/user.js deleted file mode 100644 index 0b0a3e1afb4..00000000000 --- a/app/assets/javascripts/users/user.js +++ /dev/null @@ -1,34 +0,0 @@ -/* eslint-disable class-methods-use-this */ - -import Cookies from 'js-cookie'; -import UserTabs from './user_tabs'; - -export default class User { - constructor({ action }) { - this.action = action; - this.placeProfileAvatarsToTop(); - this.initTabs(); - this.hideProjectLimitMessage(); - } - - placeProfileAvatarsToTop() { - $('.profile-groups-avatars').tooltip({ - placement: 'top', - }); - } - - initTabs() { - return new UserTabs({ - parentEl: '.user-profile', - action: this.action, - }); - } - - hideProjectLimitMessage() { - $('.hide-project-limit-message').on('click', (e) => { - e.preventDefault(); - Cookies.set('hide_project_limit_message', 'false'); - $(this).parents('.project-limit-message').remove(); - }); - } -} diff --git a/app/assets/javascripts/users/user_tabs.js b/app/assets/javascripts/users/user_tabs.js index f8e23c8624d..5fe6603ce7b 100644 --- a/app/assets/javascripts/users/user_tabs.js +++ b/app/assets/javascripts/users/user_tabs.js @@ -1,72 +1,76 @@ -/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, no-param-reassign, class-methods-use-this */ - -/* -UserTabs - -Handles persisting and restoring the current tab selection and lazily-loading -content on the Users#show page. - -### Example Markup - - <ul class="nav-links"> - <li class="activity-tab active"> - <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username"> - Activity - </a> - </li> - <li class="groups-tab"> - <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups"> - Groups - </a> - </li> - <li class="contributed-tab"> - <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed"> - Contributed projects - </a> - </li> - <li class="projects-tab"> - <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects"> - Personal projects - </a> - </li> - <li class="snippets-tab"> - <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets"> - </a> - </li> - </ul> - - <div class="tab-content"> - <div class="tab-pane" id="activity"> - Activity Content - </div> - <div class="tab-pane" id="groups"> - Groups Content - </div> - <div class="tab-pane" id="contributed"> - Contributed projects content - </div> - <div class="tab-pane" id="projects"> - Projects content - </div> - <div class="tab-pane" id="snippets"> - Snippets content - </div> +import ActivityCalendar from './activity_calendar'; + +/** + * UserTabs + * + * Handles persisting and restoring the current tab selection and lazily-loading + * content on the Users#show page. + * + * ### Example Markup + * + * <ul class="nav-links"> + * <li class="activity-tab active"> + * <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username"> + * Activity + * </a> + * </li> + * <li class="groups-tab"> + * <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups"> + * Groups + * </a> + * </li> + * <li class="contributed-tab"> + * ... + * </li> + * <li class="projects-tab"> + * ... + * </li> + * <li class="snippets-tab"> + * ... + * </li> + * </ul> + * + * <div class="tab-content"> + * <div class="tab-pane" id="activity"> + * Activity Content + * </div> + * <div class="tab-pane" id="groups"> + * Groups Content + * </div> + * <div class="tab-pane" id="contributed"> + * Contributed projects content + * </div> + * <div class="tab-pane" id="projects"> + * Projects content + * </div> + * <div class="tab-pane" id="snippets"> + * Snippets content + * </div> + * </div> + * + * <div class="loading-status"> + * <div class="loading"> + * Loading Animation + * </div> + * </div> + */ + +const CALENDAR_TEMPLATE = ` + <div class="clearfix calendar"> + <div class="js-contrib-calendar"></div> + <div class="calendar-hint"> + Summary of issues, merge requests, push events, and comments + </div> </div> - - <div class="loading-status"> - <div class="loading"> - Loading Animation - </div> - </div> -*/ +`; export default class UserTabs { - constructor ({ defaultAction, action, parentEl }) { + constructor({ defaultAction, action, parentEl }) { this.loaded = {}; this.defaultAction = defaultAction || 'activity'; this.action = action || this.defaultAction; this.$parentEl = $(parentEl) || $(document); - this._location = window.location; + this.windowLocation = window.location; this.$parentEl.find('.nav-links a') .each((i, navLink) => { this.loaded[$(navLink).attr('data-action')] = false; @@ -82,12 +86,10 @@ export default class UserTabs { } bindEvents() { - this.changeProjectsPageWrapper = this.changeProjectsPage.bind(this); - - this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]') - .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event)); - - this.$parentEl.on('click', '.gl-pagination a', this.changeProjectsPageWrapper); + this.$parentEl + .off('shown.bs.tab', '.nav-links a[data-toggle="tab"]') + .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event)) + .on('click', '.gl-pagination a', event => this.changeProjectsPage(event)); } changeProjectsPage(e) { @@ -122,7 +124,7 @@ export default class UserTabs { const loadableActions = ['groups', 'contributed', 'projects', 'snippets']; if (loadableActions.indexOf(action) > -1) { - return this.loadTab(action, endpoint); + this.loadTab(action, endpoint); } } @@ -131,25 +133,38 @@ export default class UserTabs { beforeSend: () => this.toggleLoading(true), complete: () => this.toggleLoading(false), dataType: 'json', - type: 'GET', url: endpoint, success: (data) => { const tabSelector = `div#${action}`; this.$parentEl.find(tabSelector).html(data.html); this.loaded[action] = true; - return gl.utils.localTimeAgo($('.js-timeago', tabSelector)); - } + gl.utils.localTimeAgo($('.js-timeago', tabSelector)); + }, }); } loadActivities() { - if (this.loaded['activity']) { + if (this.loaded.activity) { return; } const $calendarWrap = this.$parentEl.find('.user-calendar'); - $calendarWrap.load($calendarWrap.data('href')); + const calendarPath = $calendarWrap.data('calendarPath'); + const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath'); + + $.ajax({ + dataType: 'json', + url: calendarPath, + success: (activityData) => { + $calendarWrap.html(CALENDAR_TEMPLATE); + + // eslint-disable-next-line no-new + new ActivityCalendar('.js-contrib-calendar', activityData, calendarActivitiesPath); + }, + }); + + // eslint-disable-next-line no-new new gl.Activities(); - return this.loaded['activity'] = true; + this.loaded.activity = true; } toggleLoading(status) { @@ -158,13 +173,13 @@ export default class UserTabs { } setCurrentAction(source) { - let new_state = source; - new_state = new_state.replace(/\/+$/, ''); - new_state += this._location.search + this._location.hash; + let newState = source; + newState = newState.replace(/\/+$/, ''); + newState += this.windowLocation.search + this.windowLocation.hash; history.replaceState({ - url: new_state - }, document.title, new_state); - return new_state; + url: newState, + }, document.title, newState); + return newState; } getCurrentAction() { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js index e8e22ad93a5..744a1cd24fa 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js @@ -108,7 +108,8 @@ export default { </div> <mr-widget-memory-usage v-if="deployment.metrics_url" - :metricsUrl="deployment.metrics_url" + :metrics-url="deployment.metrics_url" + :metrics-monitoring-url="deployment.metrics_monitoring_url" /> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js index 76cb71b6c12..534e2a88eff 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js @@ -7,7 +7,14 @@ import MRWidgetService from '../services/mr_widget_service'; export default { name: 'MemoryUsage', props: { - metricsUrl: { type: String, required: true }, + metricsUrl: { + type: String, + required: true, + }, + metricsMonitoringUrl: { + type: String, + required: true, + }, }, data() { return { @@ -124,7 +131,7 @@ export default { <p v-if="shouldShowMemoryGraph" class="usage-info js-usage-info"> - Memory usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB + <a :href="metricsMonitoringUrl">Memory</a> usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB </p> <p v-if="shouldShowLoadFailure" diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 72a13108404..fddafb0ddfa 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -65,7 +65,7 @@ export default class MergeRequestStore { this.mergeCheckPath = data.merge_check_path; this.mergeActionsContentPath = data.commit_change_content_path; this.isRemovingSourceBranch = this.isRemovingSourceBranch || false; - this.isOpen = data.state === 'opened' || data.state === 'reopened' || false; + this.isOpen = data.state === 'opened'; this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false; this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false; this.canMerge = !!data.merge_path; |