Commit f024f7ef authored by Gabriel Mazetto's avatar Gabriel Mazetto

Merge branch '9-5-stable' into security-9-5

parents 7014a737 77bfdacd
...@@ -354,7 +354,7 @@ ee_compat_check: ...@@ -354,7 +354,7 @@ ee_compat_check:
except: except:
- master - master
- tags - tags
- /^[\d-]+-stable(-ee)?$/ - /^[\d-]+-stable(-ee)?/
allow_failure: yes allow_failure: yes
cache: cache:
key: "ee_compat_check_repo" key: "ee_compat_check_repo"
......
This diff is collapsed.
...@@ -84,7 +84,7 @@ gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' ...@@ -84,7 +84,7 @@ gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
gem 'hashie-forbidden_attributes' gem 'hashie-forbidden_attributes'
# Pagination # Pagination
gem 'kaminari', '~> 0.17.0' gem 'kaminari', '~> 1.0'
# HAML # HAML
gem 'hamlit', '~> 2.6.1' gem 'hamlit', '~> 2.6.1'
......
...@@ -419,9 +419,18 @@ GEM ...@@ -419,9 +419,18 @@ GEM
json-schema (2.6.2) json-schema (2.6.2)
addressable (~> 2.3.8) addressable (~> 2.3.8)
jwt (1.5.6) jwt (1.5.6)
kaminari (0.17.0) kaminari (1.0.1)
actionpack (>= 3.0.0) activesupport (>= 4.1.0)
activesupport (>= 3.0.0) kaminari-actionview (= 1.0.1)
kaminari-activerecord (= 1.0.1)
kaminari-core (= 1.0.1)
kaminari-actionview (1.0.1)
actionview
kaminari-core (= 1.0.1)
kaminari-activerecord (1.0.1)
activerecord
kaminari-core (= 1.0.1)
kaminari-core (1.0.1)
kgio (2.10.0) kgio (2.10.0)
knapsack (1.11.0) knapsack (1.11.0)
rake rake
...@@ -1009,7 +1018,7 @@ DEPENDENCIES ...@@ -1009,7 +1018,7 @@ DEPENDENCIES
jquery-rails (~> 4.1.0) jquery-rails (~> 4.1.0)
json-schema (~> 2.6.2) json-schema (~> 2.6.2)
jwt (~> 1.5.6) jwt (~> 1.5.6)
kaminari (~> 0.17.0) kaminari (~> 1.0)
knapsack (~> 1.11.0) knapsack (~> 1.11.0)
kubeclient (~> 2.2.0) kubeclient (~> 2.2.0)
letter_opener_web (~> 1.3.0) letter_opener_web (~> 1.3.0)
......
...@@ -97,7 +97,6 @@ const Api = { ...@@ -97,7 +97,6 @@ const Api = {
}, },
commitMultiple(id, data, callback) { 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) const url = Api.buildUrl(Api.commitPath)
.replace(':id', id); .replace(':id', id);
return $.ajax({ return $.ajax({
......
/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */ /* eslint-disable func-names, object-shorthand, prefer-arrow-callback */
/* global Dropzone */ /* global Dropzone */
import '../lib/utils/url_utility';
import { HIDDEN_CLASS } from '../lib/utils/constants';
function toggleLoading($el, $icon, loading) {
if (loading) {
$el.disable();
$icon.removeClass(HIDDEN_CLASS);
} else {
$el.enable();
$icon.addClass(HIDDEN_CLASS);
}
}
export default class BlobFileDropzone { export default class BlobFileDropzone {
constructor(form, method) { constructor(form, method) {
const formDropzone = form.find('.dropzone'); const formDropzone = form.find('.dropzone');
const submitButton = form.find('#submit-all');
const submitButtonLoadingIcon = submitButton.find('.js-loading-icon');
const dropzoneMessage = form.find('.dz-message');
Dropzone.autoDiscover = false; Dropzone.autoDiscover = false;
const dropzone = formDropzone.dropzone({ const dropzone = formDropzone.dropzone({
...@@ -26,12 +41,20 @@ export default class BlobFileDropzone { ...@@ -26,12 +41,20 @@ export default class BlobFileDropzone {
}, },
init: function () { init: function () {
this.on('addedfile', function () { this.on('addedfile', function () {
toggleLoading(submitButton, submitButtonLoadingIcon, false);
dropzoneMessage.addClass(HIDDEN_CLASS);
$('.dropzone-alerts').html('').hide(); $('.dropzone-alerts').html('').hide();
}); });
this.on('removedfile', function () {
toggleLoading(submitButton, submitButtonLoadingIcon, false);
dropzoneMessage.removeClass(HIDDEN_CLASS);
});
this.on('success', function (header, response) { this.on('success', function (header, response) {
window.location.href = response.filePath; $('#modal-upload-blob').modal('hide');
window.gl.utils.visitUrl(response.filePath);
}); });
this.on('maxfilesexceeded', function (file) { this.on('maxfilesexceeded', function (file) {
dropzoneMessage.addClass(HIDDEN_CLASS);
this.removeFile(file); this.removeFile(file);
}); });
this.on('sending', function (file, xhr, formData) { this.on('sending', function (file, xhr, formData) {
...@@ -48,14 +71,15 @@ export default class BlobFileDropzone { ...@@ -48,14 +71,15 @@ export default class BlobFileDropzone {
}, },
}); });
const submitButton = form.find('#submit-all')[0]; submitButton.on('click', (e) => {
submitButton.addEventListener('click', function (e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (dropzone[0].dropzone.getQueuedFiles().length === 0) { if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
alert('Please select a file'); alert('Please select a file');
return false;
} }
toggleLoading(submitButton, submitButtonLoadingIcon, true);
dropzone[0].dropzone.processQueue(); dropzone[0].dropzone.processQueue();
return false; return false;
}); });
......
/* global ListIssue */ /* global ListIssue */
/* global bp */
import Vue from 'vue'; import Vue from 'vue';
import bp from '../../../breakpoints';
const ModalStore = gl.issueBoards.ModalStore; const ModalStore = gl.issueBoards.ModalStore;
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, no-return-assign, new-parens, no-param-reassign, max-len */ export const breakpoints = {
lg: 1200,
md: 992,
sm: 768,
xs: 0,
};
var Breakpoints = (function() { const BreakpointInstance = {
var BreakpointInstance, instance; windowWidth: () => window.innerWidth,
getBreakpointSize() {
const windowWidth = this.windowWidth();
function Breakpoints() {} const breakpoint = Object.keys(breakpoints).find(key => windowWidth > breakpoints[key]);
instance = null; return breakpoint;
},
};
BreakpointInstance = (function() { export default BreakpointInstance;
var BREAKPOINTS;
BREAKPOINTS = ["xs", "sm", "md", "lg"];
function BreakpointInstance() {
this.setup();
}
BreakpointInstance.prototype.setup = function() {
var allDeviceSelector, els;
allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
return ".device-" + breakpoint;
});
if ($(allDeviceSelector.join(",")).length) {
return;
}
// Create all the elements
els = $.map(BREAKPOINTS, function(breakpoint) {
return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>";
});
return $("body").append(els.join(''));
};
BreakpointInstance.prototype.visibleDevice = function() {
var allDeviceSelector;
allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
return ".device-" + breakpoint;
});
return $(allDeviceSelector.join(",")).filter(":visible");
};
BreakpointInstance.prototype.getBreakpointSize = function() {
var $visibleDevice;
$visibleDevice = this.visibleDevice;
// TODO: Consider refactoring in light of turbolinks removal.
// the page refreshed via turbolinks
if (!$visibleDevice().length) {
this.setup();
}
$visibleDevice = this.visibleDevice();
return $visibleDevice.attr("class").split("visible-")[1];
};
return BreakpointInstance;
})();
Breakpoints.get = function() {
return instance != null ? instance : instance = new BreakpointInstance;
};
return Breakpoints;
})();
$(() => { window.bp = Breakpoints.get(); });
window.Breakpoints = Breakpoints;
/* eslint-disable func-names, wrap-iife, no-use-before-define, /* eslint-disable func-names, wrap-iife, no-use-before-define,
consistent-return, prefer-rest-params */ consistent-return, prefer-rest-params */
/* global Breakpoints */
import _ from 'underscore'; import _ from 'underscore';
import bp from './breakpoints';
import { bytesToKiB } from './lib/utils/number_utils'; import { bytesToKiB } from './lib/utils/number_utils';
window.Build = (function () { window.Build = (function () {
...@@ -34,8 +33,6 @@ window.Build = (function () { ...@@ -34,8 +33,6 @@ window.Build = (function () {
this.$scrollBottomBtn = $('.js-scroll-down'); this.$scrollBottomBtn = $('.js-scroll-down');
clearTimeout(Build.timeout); clearTimeout(Build.timeout);
// Init breakpoint checker
this.bp = Breakpoints.get();
this.initSidebar(); this.initSidebar();
this.populateJobs(this.buildStage); this.populateJobs(this.buildStage);
...@@ -230,7 +227,7 @@ window.Build = (function () { ...@@ -230,7 +227,7 @@ window.Build = (function () {
}; };
Build.prototype.shouldHideSidebarForViewport = function () { Build.prototype.shouldHideSidebarForViewport = function () {
const bootstrapBreakpoint = this.bp.getBreakpointSize(); const bootstrapBreakpoint = bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
}; };
......
...@@ -17,7 +17,7 @@ window.CommitsList = (function() { ...@@ -17,7 +17,7 @@ window.CommitsList = (function() {
} }
}); });
Pager.init(limit, false, false, this.processCommits); Pager.init(parseInt(limit, 10), false, false, this.processCommits);
this.content = $("#commits-list"); this.content = $("#commits-list");
this.searchField = $("#commits-search"); this.searchField = $("#commits-search");
......
...@@ -42,6 +42,10 @@ $(() => { ...@@ -42,6 +42,10 @@ $(() => {
$components.each(function () { $components.each(function () {
const $this = $(this); const $this = $(this);
const noteId = $this.attr(':note-id'); const noteId = $this.attr(':note-id');
const discussionId = $this.attr(':discussion-id');
if ($this.is('comment-and-resolve-btn') && !discussionId) return;
const tmp = Vue.extend({ const tmp = Vue.extend({
template: $this.get(0).outerHTML template: $this.get(0).outerHTML
}); });
......
...@@ -76,6 +76,7 @@ import initLegacyFilters from './init_legacy_filters'; ...@@ -76,6 +76,7 @@ import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar'; import initIssuableSidebar from './init_issuable_sidebar';
import GpgBadges from './gpg_badges'; import GpgBadges from './gpg_badges';
import UserFeatureHelper from './helpers/user_feature_helper'; import UserFeatureHelper from './helpers/user_feature_helper';
import initChangesDropdown from './init_changes_dropdown';
(function() { (function() {
var Dispatcher; var Dispatcher;
...@@ -228,6 +229,7 @@ import UserFeatureHelper from './helpers/user_feature_helper'; ...@@ -228,6 +229,7 @@ import UserFeatureHelper from './helpers/user_feature_helper';
break; break;
case 'projects:compare:show': case 'projects:compare:show':
new gl.Diff(); new gl.Diff();
initChangesDropdown();
break; break;
case 'projects:branches:new': case 'projects:branches:new':
case 'projects:branches:create': case 'projects:branches:create':
...@@ -320,6 +322,7 @@ import UserFeatureHelper from './helpers/user_feature_helper'; ...@@ -320,6 +322,7 @@ import UserFeatureHelper from './helpers/user_feature_helper';
container: '.js-commit-pipeline-graph', container: '.js-commit-pipeline-graph',
}).bindEvents(); }).bindEvents();
initNotes(); initNotes();
initChangesDropdown();
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
break; break;
case 'projects:commit:pipelines': case 'projects:commit:pipelines':
...@@ -344,6 +347,9 @@ import UserFeatureHelper from './helpers/user_feature_helper'; ...@@ -344,6 +347,9 @@ import UserFeatureHelper from './helpers/user_feature_helper';
if ($('#tree-slider').length) new TreeView(); if ($('#tree-slider').length) new TreeView();
if ($('.blob-viewer').length) new BlobViewer(); if ($('.blob-viewer').length) new BlobViewer();
if ($('.project-show-activity').length) new gl.Activities(); if ($('.project-show-activity').length) new gl.Activities();
$('#tree-slider').waitForImages(function() {
gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
});
break; break;
case 'projects:edit': case 'projects:edit':
setupProjectEdit(); setupProjectEdit();
...@@ -638,7 +644,7 @@ import UserFeatureHelper from './helpers/user_feature_helper'; ...@@ -638,7 +644,7 @@ import UserFeatureHelper from './helpers/user_feature_helper';
return Dispatcher; return Dispatcher;
})(); })();
$(function() { $(window).on('load', function() {
new Dispatcher(); new Dispatcher();
}); });
}).call(window); }).call(window);
/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */ /* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */
/* global dateFormat */ /* global dateFormat */
/* global Pikaday */
import Pikaday from 'pikaday';
import DateFix from './lib/utils/datefix'; import DateFix from './lib/utils/datefix';
class DueDateSelect { class DueDateSelect {
......
/* global bp */ import bp from './breakpoints';
import Cookies from 'js-cookie';
import './breakpoints';
export const canShowActiveSubItems = (el) => { let headerHeight = 50;
const isHiddenByMedia = bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md'; let sidebar;
export const setSidebar = (el) => { sidebar = el; };
if (el.classList.contains('active') && !isHiddenByMedia) { export const getHeaderHeight = () => headerHeight;
return Cookies.get('sidebar_collapsed') === 'true';
export const canShowActiveSubItems = (el) => {
if (el.classList.contains('active') && (sidebar && !sidebar.classList.contains('sidebar-icons-only'))) {
return false;
} }
return true; return true;
...@@ -35,7 +38,7 @@ export const showSubLevelItems = (el) => { ...@@ -35,7 +38,7 @@ export const showSubLevelItems = (el) => {
const isAbove = top < boundingRect.top; const isAbove = top < boundingRect.top;
subItems.classList.add('fly-out-list'); subItems.classList.add('fly-out-list');
subItems.style.transform = `translate3d(0, ${Math.floor(top)}px, 0)`; subItems.style.transform = `translate3d(0, ${Math.floor(top) - headerHeight}px, 0)`;
if (isAbove) { if (isAbove) {
subItems.classList.add('is-above'); subItems.classList.add('is-above');
...@@ -49,7 +52,8 @@ export const hideSubLevelItems = (el) => { ...@@ -49,7 +52,8 @@ export const hideSubLevelItems = (el) => {
el.classList.remove('is-showing-fly-out'); el.classList.remove('is-showing-fly-out');
el.classList.remove('is-over'); el.classList.remove('is-over');
subItems.style.display = 'none'; subItems.style.display = '';
subItems.style.transform = '';
subItems.classList.remove('is-above'); subItems.classList.remove('is-above');
}; };
...@@ -57,8 +61,14 @@ export default () => { ...@@ -57,8 +61,14 @@ export default () => {
const items = [...document.querySelectorAll('.sidebar-top-level-items > li')] const items = [...document.querySelectorAll('.sidebar-top-level-items > li')]
.filter(el => el.querySelector('.sidebar-sub-level-items')); .filter(el => el.querySelector('.sidebar-sub-level-items'));
items.forEach((el) => { sidebar = document.querySelector('.nav-sidebar');
el.addEventListener('mouseenter', e => showSubLevelItems(e.currentTarget));
el.addEventListener('mouseleave', e => hideSubLevelItems(e.currentTarget)); if (sidebar) {
}); headerHeight = sidebar.offsetTop;
items.forEach((el) => {
el.addEventListener('mouseenter', e => showSubLevelItems(e.currentTarget));
el.addEventListener('mouseleave', e => hideSubLevelItems(e.currentTarget));
});
}
}; };
export default class GpgBadges { export default class GpgBadges {
static fetch() { static fetch() {
const badges = $('.js-loading-gpg-badge');
const form = $('.commits-search-form'); const form = $('.commits-search-form');
badges.html('<i class="fa fa-spinner fa-spin"></i>');
$.get({ $.get({
url: form.data('signatures-path'), url: form.data('signatures-path'),
data: form.serialize(), data: form.serialize(),
}).done((response) => { }).done((response) => {
const badges = $('.js-loading-gpg-badge');
response.signatures.forEach((signature) => { response.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
}); });
......
import stickyMonitor from './lib/utils/sticky';
export default () => {
stickyMonitor(document.querySelector('.js-diff-files-changed'));
$('.js-diff-stats-dropdown').glDropdown({
filterable: true,
remoteFilter: false,
});
};
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
/* global bp */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import bp from './breakpoints';
import UsersSelect from './users_select'; import UsersSelect from './users_select';
const PARTICIPANTS_ROW_COUNT = 7; const PARTICIPANTS_ROW_COUNT = 7;
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
/* global GitLab */ /* global GitLab */
/* global Autosave */ /* global Autosave */
/* global dateFormat */ /* global dateFormat */
/* global Pikaday */
import Pikaday from 'pikaday';
import UsersSelect from './users_select'; import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete'; import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode'; import ZenMode from './zen_mode';
......
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
label: 'New issue', label: 'New issue',
path: this.job.new_issue_path, path: this.job.new_issue_path,
cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block', cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
type: 'ujs-link', type: 'link',
}); });
} }
......
/* eslint-disable import/prefer-default-export */ /* eslint-disable import/prefer-default-export */
export const BYTES_IN_KIB = 1024; export const BYTES_IN_KIB = 1024;
export const HIDDEN_CLASS = 'hidden';
export const isSticky = (el, scrollY, stickyTop) => { export const isSticky = (el, scrollY, stickyTop) => {
const top = el.offsetTop - scrollY; const top = el.offsetTop - scrollY;
if (top === stickyTop) { if (top <= stickyTop) {
el.classList.add('is-stuck'); el.classList.add('is-stuck');
} else { } else {
el.classList.remove('is-stuck'); el.classList.remove('is-stuck');
......
/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */ /* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */
/* global bp */
/* global Flash */ /* global Flash */
/* global ConfirmDangerModal */ /* global ConfirmDangerModal */
/* global Aside */ /* global Aside */
...@@ -7,7 +6,6 @@ ...@@ -7,7 +6,6 @@
import jQuery from 'jquery'; import jQuery from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import Pikaday from 'pikaday';
import Dropzone from 'dropzone'; import Dropzone from 'dropzone';
import Sortable from 'vendor/Sortable'; import Sortable from 'vendor/Sortable';
...@@ -20,7 +18,6 @@ import 'vendor/fuzzaldrin-plus'; ...@@ -20,7 +18,6 @@ import 'vendor/fuzzaldrin-plus';
window.jQuery = jQuery; window.jQuery = jQuery;
window.$ = jQuery; window.$ = jQuery;
window._ = _; window._ = _;
window.Pikaday = Pikaday;
window.Dropzone = Dropzone; window.Dropzone = Dropzone;
window.Sortable = Sortable; window.Sortable = Sortable;
...@@ -68,7 +65,7 @@ import './api'; ...@@ -68,7 +65,7 @@ import './api';
import './aside'; import './aside';
import './autosave'; import './autosave';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import './breakpoints'; import bp from './breakpoints';
import './broadcast_message'; import './broadcast_message';
import './build'; import './build';
import './build_artifacts'; import './build_artifacts';
...@@ -135,8 +132,9 @@ import './project_select'; ...@@ -135,8 +132,9 @@ import './project_select';
import './project_show'; import './project_show';
import './project_variables'; import './project_variables';
import './projects_list'; import './projects_list';
import './render_gfm'; import './syntax_highlight';
import './render_math'; import './render_math';
import './render_gfm';
import './right_sidebar'; import './right_sidebar';
import './search'; import './search';
import './search_autocomplete'; import './search_autocomplete';
...@@ -144,7 +142,6 @@ import './smart_interval'; ...@@ -144,7 +142,6 @@ import './smart_interval';
import './star'; import './star';
import './subscription'; import './subscription';
import './subscription_select'; import './subscription_select';
import './syntax_highlight';
import './dispatcher'; import './dispatcher';
......
/* global Pikaday */
/* global dateFormat */ /* global dateFormat */
import Pikaday from 'pikaday';
(() => { (() => {
// Add datepickers to all `js-access-expiration-date` elements. If those elements are // Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling // children of an element with the `clearable-input` class, and have a sibling
......
/* eslint-disable no-new, class-methods-use-this */ /* eslint-disable no-new, class-methods-use-this */
/* global Breakpoints */
/* global Flash */ /* global Flash */
/* global notes */ /* global notes */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import './breakpoints';
import './flash'; import './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion'; import BlobForkSuggestion from './blob/blob_fork_suggestion';
import stickyMonitor from './lib/utils/sticky'; import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
/* eslint-disable max-len */ /* eslint-disable max-len */
// MergeRequestTabs // MergeRequestTabs
...@@ -134,7 +133,7 @@ import stickyMonitor from './lib/utils/sticky'; ...@@ -134,7 +133,7 @@ import stickyMonitor from './lib/utils/sticky';
this.destroyPipelinesView(); this.destroyPipelinesView();
} else if (this.isDiffAction(action)) { } else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href')); this.loadDiff($target.attr('href'));
if (Breakpoints.get().getBreakpointSize() !== 'lg') { if (bp.getBreakpointSize() !== 'lg') {
this.shrinkView(); this.shrinkView();
} }
if (this.diffViewType() === 'parallel') { if (this.diffViewType() === 'parallel') {
...@@ -145,7 +144,7 @@ import stickyMonitor from './lib/utils/sticky'; ...@@ -145,7 +144,7 @@ import stickyMonitor from './lib/utils/sticky';
this.resetViewContainer(); this.resetViewContainer();
this.mountPipelinesView(); this.mountPipelinesView();
} else { } else {
if (Breakpoints.get().getBreakpointSize() !== 'xs') { if (bp.getBreakpointSize() !== 'xs') {
this.expandView(); this.expandView();
} }
this.resetViewContainer(); this.resetViewContainer();
...@@ -267,9 +266,7 @@ import stickyMonitor from './lib/utils/sticky'; ...@@ -267,9 +266,7 @@ import stickyMonitor from './lib/utils/sticky';
const $container = $('#diffs'); const $container = $('#diffs');
$container.html(data.html); $container.html(data.html);
this.initChangesDropdown(); initChangesDropdown();
stickyMonitor(document.querySelector('.js-diff-files-changed'));
if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
...@@ -319,13 +316,6 @@ import stickyMonitor from './lib/utils/sticky'; ...@@ -319,13 +316,6 @@ import stickyMonitor from './lib/utils/sticky';
}); });
} }
initChangesDropdown() {
$('.js-diff-stats-dropdown').glDropdown({
filterable: true,
remoteFilter: false,
});
}
// Show or hide the loading spinner // Show or hide the loading spinner
// //
// status - Boolean, true to show, false to hide // status - Boolean, true to show, false to hide
...@@ -401,7 +391,7 @@ import stickyMonitor from './lib/utils/sticky'; ...@@ -401,7 +391,7 @@ import stickyMonitor from './lib/utils/sticky';
// Screen space on small screens is usually very sparse // Screen space on small screens is usually very sparse
// So we dont affix the tabs on these // So we dont affix the tabs on these
if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return;
/** /**
If the browser does not support position sticky, it returns the position as static. If the browser does not support position sticky, it returns the position as static.
......
<script> <script>
/* global Breakpoints */
import d3 from 'd3'; import d3 from 'd3';
import monitoringLegends from './monitoring_legends.vue'; import monitoringLegends from './monitoring_legends.vue';
import monitoringFlag from './monitoring_flag.vue'; import monitoringFlag from './monitoring_flag.vue';
...@@ -8,6 +7,7 @@ ...@@ -8,6 +7,7 @@
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import measurements from '../utils/measurements'; import measurements from '../utils/measurements';
import { formatRelevantDigits } from '../../lib/utils/number_utils'; import { formatRelevantDigits } from '../../lib/utils/number_utils';
import bp from '../../breakpoints';
const bisectDate = d3.bisector(d => d.time).left; const bisectDate = d3.bisector(d => d.time).left;
...@@ -42,7 +42,6 @@ ...@@ -42,7 +42,6 @@
yScale: {}, yScale: {},
margin: {}, margin: {},
data: [], data: [],
breakpointHandler: Breakpoints.get(),
unitOfDisplay: '', unitOfDisplay: '',
areaColorRgb: '#8fbce8', areaColorRgb: '#8fbce8',
lineColorRgb: '#1f78d1', lineColorRgb: '#1f78d1',
...@@ -96,7 +95,7 @@ ...@@ -96,7 +95,7 @@
methods: { methods: {
draw() { draw() {
const breakpointSize = this.breakpointHandler.getBreakpointSize(); const breakpointSize = bp.getBreakpointSize();
const query = this.columnData.queries[0]; const query = this.columnData.queries[0];
this.margin = measurements.large.margin; this.margin = measurements.large.margin;
if (breakpointSize === 'xs' || breakpointSize === 'sm') { if (breakpointSize === 'xs' || breakpointSize === 'sm') {
......
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import _ from 'underscore'; import _ from 'underscore';
/* global bp */ import bp from './breakpoints';
import './breakpoints';
export default class NewNavSidebar { export default class NewNavSidebar {
constructor() { constructor() {
...@@ -44,10 +43,12 @@ export default class NewNavSidebar { ...@@ -44,10 +43,12 @@ export default class NewNavSidebar {
} }
toggleCollapsedSidebar(collapsed) { toggleCollapsedSidebar(collapsed) {
this.$sidebar.toggleClass('sidebar-icons-only', collapsed); const breakpoint = bp.getBreakpointSize();
if (this.$sidebar.length) { if (this.$sidebar.length) {
this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
this.$page.toggleClass('page-with-new-sidebar', !collapsed); this.$page.toggleClass('page-with-new-sidebar', !collapsed);
this.$page.toggleClass('page-with-icon-sidebar', collapsed); this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
} }
NewNavSidebar.setCollapsedCookie(collapsed); NewNavSidebar.setCollapsedCookie(collapsed);
} }
......
...@@ -126,11 +126,11 @@ import Cookies from 'js-cookie'; ...@@ -126,11 +126,11 @@ import Cookies from 'js-cookie';
var $form = $dropdown.closest('form'); var $form = $dropdown.closest('form');
var $visit = $dropdown.data('visit'); var $visit = $dropdown.data('visit');
var shouldVisit = typeof $visit === 'undefined' ? true : $visit; var shouldVisit = $visit ? true : $visit;
var action = $form.attr('action'); var action = $form.attr('action');
var divider = action.indexOf('?') === -1 ? '?' : '&'; var divider = action.indexOf('?') === -1 ? '?' : '&';
if (shouldVisit) { if (shouldVisit) {
gl.utils.visitUrl(action + '' + divider + '' + $form.serialize()); gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
} }
} }
} }
......
...@@ -36,7 +36,7 @@ const bindEvents = () => { ...@@ -36,7 +36,7 @@ const bindEvents = () => {
$('.how_to_import_link').on('click', (e) => { $('.how_to_import_link').on('click', (e) => {
e.preventDefault(); e.preventDefault();
$('.how_to_import_link').next('.modal').show(); $(e.currentTarget).next('.modal').show();
}); });
$('.modal-header .close').on('click', () => { $('.modal-header .close').on('click', () => {
......
...@@ -11,7 +11,5 @@ ...@@ -11,7 +11,5 @@
return this; return this;
}; };
$(document).on('ready load', function() { $(() => $('body').renderGFM());
return $('body').renderGFM();
});
}).call(window); }).call(window);
...@@ -14,13 +14,13 @@ export default { ...@@ -14,13 +14,13 @@ export default {
data: () => Store, data: () => Store,
mixins: [RepoMixin], mixins: [RepoMixin],
components: { components: {
'repo-sidebar': RepoSidebar, RepoSidebar,
'repo-tabs': RepoTabs, RepoTabs,
'repo-file-buttons': RepoFileButtons, RepoFileButtons,
'repo-editor': MonacoLoaderHelper.repoEditorLoader, 'repo-editor': MonacoLoaderHelper.repoEditorLoader,
'repo-commit-section': RepoCommitSection, RepoCommitSection,
'popup-dialog': PopupDialog, PopupDialog,
'repo-preview': RepoPreview, RepoPreview,
}, },
mounted() { mounted() {
...@@ -28,12 +28,12 @@ export default { ...@@ -28,12 +28,12 @@ export default {
}, },
methods: { methods: {
dialogToggled(toggle) { toggleDialogOpen(toggle) {
this.dialog.open = toggle; this.dialog.open = toggle;
}, },
dialogSubmitted(status) { dialogSubmitted(status) {
this.dialog.open = false; this.toggleDialogOpen(false);
this.dialog.status = status; this.dialog.status = status;
}, },
...@@ -43,21 +43,25 @@ export default { ...@@ -43,21 +43,25 @@ export default {
</script> </script>
<template> <template>
<div class="repository-view tree-content-holder"> <div class="repository-view tree-content-holder">
<repo-sidebar/><div class="panel-right" :class="{'edit-mode': editMode}"> <repo-sidebar/><div v-if="isMini"
<repo-tabs/> class="panel-right"
<component :is="currentBlobView" class="blob-viewer-container"></component> :class="{'edit-mode': editMode}">
<repo-file-buttons/> <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> </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> </template>
...@@ -2,18 +2,20 @@ ...@@ -2,18 +2,20 @@
/* global Flash */ /* global Flash */
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
const RepoCommitSection = { export default {
data: () => Store, data: () => Store,
mixins: [RepoMixin], mixins: [RepoMixin],
computed: { computed: {
showCommitable() {
return this.isCommitable && this.changedFiles.length;
},
branchPaths() { branchPaths() {
const branch = Helper.getBranch(); return this.changedFiles.map(f => f.path);
return this.changedFiles.map(f => Helper.getFilePathFromFullPath(f.url, branch));
}, },
cantCommitYet() { cantCommitYet() {
...@@ -28,11 +30,10 @@ const RepoCommitSection = { ...@@ -28,11 +30,10 @@ const RepoCommitSection = {
methods: { methods: {
makeCommit() { makeCommit() {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions // 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 commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({ const actions = this.changedFiles.map(f => ({
action: 'update', action: 'update',
file_path: Helper.getFilePathFromFullPath(f.url, branch), file_path: f.path,
content: f.newContent, content: f.newContent,
})); }));
const payload = { const payload = {
...@@ -47,51 +48,80 @@ const RepoCommitSection = { ...@@ -47,51 +48,80 @@ const RepoCommitSection = {
resetCommitState() { resetCommitState() {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
this.changedFiles = []; this.changedFiles = [];
this.openedFiles = [];
this.commitMessage = ''; this.commitMessage = '';
this.editMode = false; this.editMode = false;
$('html, body').animate({ scrollTop: 0 }, 'fast'); window.scrollTo(0, 0);
}, },
}, },
}; };
export default RepoCommitSection;
</script> </script>
<template> <template>
<div id="commit-area" v-if="isCommitable && changedFiles.length" > <div
<form class="form-horizontal"> v-if="showCommitable"
id="commit-area">
<form
class="form-horizontal"
@submit.prevent="makeCommit">
<fieldset> <fieldset>
<div class="form-group"> <div class="form-group">
<label class="col-md-4 control-label staged-files">Staged files ({{changedFiles.length}})</label> <label class="col-md-4 control-label staged-files">
<div class="col-md-4"> Staged files ({{changedFiles.length}})
</label>
<div class="col-md-6">
<ul class="list-unstyled changed-files"> <ul class="list-unstyled changed-files">
<li v-for="file in branchPaths" :key="file.id"> <li
<span class="help-block">{{file}}</span> v-for="branchPath in branchPaths"
:key="branchPath">
<span class="help-block">
{{branchPath}}
</span>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<!-- Textarea
-->
<div class="form-group"> <div class="form-group">
<label class="col-md-4 control-label" for="commit-message">Commit message</label> <label
<div class="col-md-4"> class="col-md-4 control-label"
<textarea class="form-control" id="commit-message" name="commit-message" v-model="commitMessage"></textarea> 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>
</div> </div>
<!-- Button Drop Down
-->
<div class="form-group target-branch"> <div class="form-group target-branch">
<label class="col-md-4 control-label" for="target-branch">Target branch</label> <label
<div class="col-md-4"> class="col-md-4 control-label"
<span class="help-block">{{targetBranch}}</span> for="target-branch">
Target branch
</label>
<div class="col-md-6">
<span class="help-block">
{{targetBranch}}
</span>
</div> </div>
</div> </div>
<div class="col-md-offset-4 col-md-4"> <div class="col-md-offset-4 col-md-6">
<button type="submit" :disabled="cantCommitYet" class="btn btn-success submit-commit" @click.prevent="makeCommit"> <button
<i class="fa fa-spinner fa-spin" v-if="submitCommitsLoading"></i> ref="submitCommit"
<span class="commit-summary">Commit {{changedFiles.length}} {{filePluralize}}</span> 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> </button>
</div> </div>
</fieldset> </fieldset>
......
...@@ -10,12 +10,15 @@ export default { ...@@ -10,12 +10,15 @@ export default {
return this.editMode ? this.__('Cancel edit') : this.__('Edit'); return this.editMode ? this.__('Cancel edit') : this.__('Edit');
}, },
buttonIcon() { showButton() {
return this.editMode ? [] : ['fa', 'fa-pencil']; return this.isCommitable &&
!this.activeFile.render_error &&
!this.binary &&
this.openedFiles.length;
}, },
}, },
methods: { methods: {
editClicked() { editCancelClicked() {
if (this.changedFiles.length) { if (this.changedFiles.length) {
this.dialog.open = true; this.dialog.open = true;
return; return;
...@@ -23,27 +26,33 @@ export default { ...@@ -23,27 +26,33 @@ export default {
this.editMode = !this.editMode; this.editMode = !this.editMode;
Store.toggleBlobView(); Store.toggleBlobView();
}, },
toggleProjectRefsForm() {
$('.project-refs-form').toggleClass('disabled', this.editMode);
$('.js-tree-ref-target-holder').toggle(this.editMode);
},
}, },
watch: { watch: {
editMode() { editMode() {
if (this.editMode) { this.toggleProjectRefsForm();
$('.project-refs-form').addClass('disabled');
$('.fa-long-arrow-right').show();
$('.project-refs-target-form').show();
} else {
$('.project-refs-form').removeClass('disabled');
$('.fa-long-arrow-right').hide();
$('.project-refs-target-form').hide();
}
}, },
}, },
}; };
</script> </script>
<template> <template>
<button class="btn btn-default" @click.prevent="editClicked" v-cloak v-if="isCommitable && !activeFile.render_error" :disabled="binary"> <button
<i :class="buttonIcon"></i> v-if="showButton"
<span>{{buttonLabel}}</span> 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> </button>
</template> </template>
...@@ -8,38 +8,39 @@ const RepoEditor = { ...@@ -8,38 +8,39 @@ const RepoEditor = {
data: () => Store, data: () => Store,
destroyed() { destroyed() {
// this.monacoInstance.getModels().forEach((m) => { if (Helper.monacoInstance) {
// m.dispose(); Helper.monacoInstance.destroy();
// }); }
this.monacoInstance.destroy();
}, },
mounted() { mounted() {
Service.getRaw(this.activeFile.raw_path) Service.getRaw(this.activeFile.raw_path)
.then((rawResponse) => { .then((rawResponse) => {
Store.blobRaw = rawResponse.data; Store.blobRaw = rawResponse.data;
Helper.findOpenedFileFromActive().plain = rawResponse.data; Store.activeFile.plain = rawResponse.data;
const monacoInstance = this.monaco.editor.create(this.$el, { const monacoInstance = Helper.monaco.editor.create(this.$el, {
model: null, model: null,
readOnly: false, readOnly: false,
contextmenu: false, contextmenu: false,
}); });
Store.monacoInstance = monacoInstance; Helper.monacoInstance = monacoInstance;
this.addMonacoEvents(); this.addMonacoEvents();
const languages = this.monaco.languages.getLanguages(); this.setupEditor();
const languageID = Helper.getLanguageIDForFile(this.activeFile, languages); })
this.showHide(); .catch(Helper.loadingError);
const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
this.monacoInstance.setModel(newModel);
}).catch(Helper.loadingError);
}, },
methods: { methods: {
setupEditor() {
this.showHide();
Helper.setMonacoModelFromLanguage();
},
showHide() { showHide() {
if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) { if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
this.$el.style.display = 'none'; this.$el.style.display = 'none';
...@@ -49,41 +50,36 @@ const RepoEditor = { ...@@ -49,41 +50,36 @@ const RepoEditor = {
}, },
addMonacoEvents() { addMonacoEvents() {
this.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp); Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
this.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this)); Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
}, },
onMonacoEditorKeysPressed() { onMonacoEditorKeysPressed() {
Store.setActiveFileContents(this.monacoInstance.getValue()); Store.setActiveFileContents(Helper.monacoInstance.getValue());
}, },
onMonacoEditorMouseUp(e) { onMonacoEditorMouseUp(e) {
if (!e.target.position) return;
const lineNumber = e.target.position.lineNumber; 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}`; location.hash = `L${lineNumber}`;
Store.activeLine = lineNumber; Store.activeLine = lineNumber;
Helper.monacoInstance.setPosition({
lineNumber: this.activeLine,
column: 1,
});
} }
}, },
}, },
watch: { watch: {
activeLine() {
this.monacoInstance.setPosition({
lineNumber: this.activeLine,
column: 1,
});
},
activeFileLabel() {
this.showHide();
},
dialog: { dialog: {
handler(obj) { handler(obj) {
const newObj = obj; const newObj = obj;
if (newObj.status) { if (newObj.status) {
newObj.status = false; newObj.status = false;
this.openedFiles.map((file) => { this.openedFiles = this.openedFiles.map((file) => {
const f = file; const f = file;
if (f.active) { if (f.active) {
this.blobRaw = f.plain; this.blobRaw = f.plain;
...@@ -94,35 +90,21 @@ const RepoEditor = { ...@@ -94,35 +90,21 @@ const RepoEditor = {
return f; return f;
}); });
this.editMode = false; this.editMode = false;
Store.toggleBlobView();
} }
}, },
deep: true, deep: true,
}, },
isTree() {
this.showHide();
},
openedFiles() {
this.showHide();
},
binary() {
this.showHide();
},
blobRaw() { blobRaw() {
this.showHide(); if (Helper.monacoInstance && !this.isTree) {
this.setupEditor();
if (this.isTree) return; }
},
this.monacoInstance.setModel(null); },
computed: {
const languages = this.monaco.languages.getLanguages(); shouldHideEditor() {
const languageID = Helper.getLanguageIDForFile(this.activeFile, languages); return !this.openedFiles.length || (this.binary && !this.activeFile.raw);
const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
this.monacoInstance.setModel(newModel);
}, },
}, },
}; };
...@@ -131,5 +113,5 @@ export default RepoEditor; ...@@ -131,5 +113,5 @@ export default RepoEditor;
</script> </script>
<template> <template>
<div id="ide"></div> <div id="ide" v-if='!shouldHideEditor'></div>
</template> </template>
...@@ -33,6 +33,26 @@ const RepoFile = { ...@@ -33,6 +33,26 @@ const RepoFile = {
canShowFile() { canShowFile() {
return !this.loading.tree || this.hasFiles; 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: { methods: {
...@@ -46,21 +66,42 @@ export default RepoFile; ...@@ -46,21 +66,42 @@ export default RepoFile;
</script> </script>
<template> <template>
<tr class="file" v-if="canShowFile" :class="{'active': activeFile.url === file.url}"> <tr
<td @click.prevent="linkClicked(file)"> v-if="canShowFile"
<i class="fa file-icon" v-if="!file.loading" :class="file.icon" :style="{'margin-left': file.level * 10 + 'px'}"></i> class="file"
<i class="fa fa-spinner fa-spin" v-if="file.loading" :style="{'margin-left': file.level * 10 + 'px'}"></i> :class="activeFileClass"
<a :href="file.url" class="repo-file-name" :title="file.url">{{file.name}}</a> @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>
<td v-if="!isMini" class="hidden-sm hidden-xs"> <template v-if="!isMini">
<div class="commit-message"> <td class="hidden-sm hidden-xs">
<a :href="file.lastCommitUrl">{{file.lastCommitMessage}}</a> <div class="commit-message">
</div> <a @click.stop :href="file.lastCommitUrl">
</td> {{file.lastCommitMessage}}
</a>
</div>
</td>
<td v-if="!isMini" class="hidden-xs"> <td class="hidden-xs">
<span class="commit-update" :title="tooltipTitle(file.lastCommitUpdate)">{{timeFormated(file.lastCommitUpdate)}}</span> <span
</td> class="commit-update"
:title="tooltipTitle(file.lastCommitUpdate)">
{{timeFormated(file.lastCommitUpdate)}}
</span>
</td>
</template>
</tr> </tr>
</template> </template>
...@@ -15,7 +15,7 @@ const RepoFileButtons = { ...@@ -15,7 +15,7 @@ const RepoFileButtons = {
}, },
canPreview() { canPreview() {
return Helper.isKindaBinary(); return Helper.isRenderable();
}, },
}, },
...@@ -28,15 +28,42 @@ export default RepoFileButtons; ...@@ -28,15 +28,42 @@ export default RepoFileButtons;
</script> </script>
<template> <template>
<div id="repo-file-buttons" v-if="isMini"> <div id="repo-file-buttons">
<a :href="activeFile.raw_path" target="_blank" class="btn btn-default raw" rel="noopener noreferrer">{{rawDownloadButtonLabel}}</a> <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"> <div
<a :href="activeFile.blame_path" class="btn btn-default blame">Blame</a> class="btn-group"
<a :href="activeFile.commits_path" class="btn btn-default history">History</a> role="group"
<a :href="activeFile.permalink" class="btn btn-default permalink">Permalink</a> aria-label="File actions">
</div> <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> <a
</div> v-if="canPreview"
href="#"
@click.prevent="rawPreviewToggle"
class="btn btn-default preview">
{{activeFileLabel}}
</a>
</div>
</template> </template>
...@@ -17,7 +17,7 @@ export default RepoFileOptions; ...@@ -17,7 +17,7 @@ export default RepoFileOptions;
</script> </script>
<template> <template>
<tr v-if="isMini" class="repo-file-options"> <tr v-if="isMini" class="repo-file-options">
<td> <td>
<span class="title">{{projectName}}</span> <span class="title">{{projectName}}</span>
</td> </td>
......
...@@ -18,9 +18,15 @@ const RepoLoadingFile = { ...@@ -18,9 +18,15 @@ const RepoLoadingFile = {
}, },
}, },
computed: {
showGhostLines() {
return this.loading.tree && !this.hasFiles;
},
},
methods: { methods: {
lineOfCode(n) { lineOfCode(n) {
return `line-of-code-${n}`; return `skeleton-line-${n}`;
}, },
}, },
}; };
...@@ -29,23 +35,42 @@ export default RepoLoadingFile; ...@@ -29,23 +35,42 @@ export default RepoLoadingFile;
</script> </script>
<template> <template>
<tr v-if="loading.tree && !hasFiles" class="loading-file"> <tr
<td> v-if="showGhostLines"
<div class="animation-container animation-container-small"> class="loading-file">
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> <td>
</div> <div
</td> 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"> <td
<div class="animation-container"> v-if="!isMini"
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> class="hidden-sm hidden-xs">
</div> <div class="animation-container">
</td> <div
v-for="n in 6"
:key="n"
:class="lineOfCode(n)">
</div>
</div>
</td>
<td v-if="!isMini" class="hidden-xs"> <td
<div class="animation-container animation-container-small"> v-if="!isMini"
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> class="hidden-xs">
</div> <div class="animation-container animation-container-small">
</td> <div
</tr> v-for="n in 6"
:key="n"
:class="lineOfCode(n)">
</div>
</div>
</td>
</tr>
</template> </template>
<script> <script>
import RepoMixin from '../mixins/repo_mixin';
const RepoPreviousDirectory = { const RepoPreviousDirectory = {
props: { props: {
prevUrl: { prevUrl: {
...@@ -7,6 +9,14 @@ const RepoPreviousDirectory = { ...@@ -7,6 +9,14 @@ const RepoPreviousDirectory = {
}, },
}, },
mixins: [RepoMixin],
computed: {
colSpanCondition() {
return this.isMini ? undefined : 3;
},
},
methods: { methods: {
linkClicked(file) { linkClicked(file) {
this.$emit('linkclicked', file); this.$emit('linkclicked', file);
...@@ -19,8 +29,10 @@ export default RepoPreviousDirectory; ...@@ -19,8 +29,10 @@ export default RepoPreviousDirectory;
<template> <template>
<tr class="prev-directory"> <tr class="prev-directory">
<td colspan="3"> <td
<a :href="prevUrl" @click.prevent="linkClicked(prevUrl)">..</a> :colspan="colSpanCondition"
@click.prevent="linkClicked(prevUrl)">
<a :href="prevUrl">..</a>
</td> </td>
</tr> </tr>
</template> </template>
...@@ -4,7 +4,7 @@ import Store from '../stores/repo_store'; ...@@ -4,7 +4,7 @@ import Store from '../stores/repo_store';
export default { export default {
data: () => Store, data: () => Store,
mounted() { mounted() {
$(this.$el).find('.file-content').syntaxHighlight(); this.highlightFile();
}, },
computed: { computed: {
html() { html() {
...@@ -12,10 +12,16 @@ export default { ...@@ -12,10 +12,16 @@ export default {
}, },
}, },
methods: {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
},
watch: { watch: {
html() { html() {
this.$nextTick(() => { this.$nextTick(() => {
$(this.$el).find('.file-content').syntaxHighlight(); this.highlightFile();
}); });
}, },
}, },
...@@ -24,9 +30,23 @@ export default { ...@@ -24,9 +30,23 @@ export default {
<template> <template>
<div> <div>
<div v-if="!activeFile.render_error" v-html="activeFile.html"></div> <div
<div v-if="activeFile.render_error" class="vertical-center render-error"> v-if="!activeFile.render_error"
<p class="text-center">The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.</p> v-html="activeFile.html">
</div>
<div
v-else-if="activeFile.tooLarge"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.
</p>
</div>
<div
v-else
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because a rendering error occured. You can <a :href="activeFile.raw_path">download</a> it instead.
</p>
</div> </div>
</div> </div>
</template> </template>
...@@ -8,7 +8,7 @@ import RepoFile from './repo_file.vue'; ...@@ -8,7 +8,7 @@ import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue'; import RepoLoadingFile from './repo_loading_file.vue';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
const RepoSidebar = { export default {
mixins: [RepoMixin], mixins: [RepoMixin],
components: { components: {
'repo-file-options': RepoFileOptions, 'repo-file-options': RepoFileOptions,
...@@ -33,40 +33,36 @@ const RepoSidebar = { ...@@ -33,40 +33,36 @@ const RepoSidebar = {
}); });
}, },
linkClicked(clickedFile) { fileClicked(clickedFile) {
let url = '';
let file = clickedFile; let file = clickedFile;
if (typeof file === 'object') { if (file.loading) return;
file.loading = true; file.loading = true;
if (file.type === 'tree' && file.opened) { if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file); file = Store.removeChildFilesOfTree(file);
file.loading = false; file.loading = false;
} else { } else {
url = file.url; Service.url = file.url;
Service.url = url; Helper.getContent(file)
// I need to refactor this to do the `then` here. .then(() => {
// Not a callback. For now this is good enough.
// it works.
Helper.getContent(file, () => {
file.loading = false; file.loading = false;
Helper.scrollTabsRight(); Helper.scrollTabsRight();
}); })
} .catch(Helper.loadingError);
} else if (typeof file === 'string') {
// go back
url = file;
Service.url = url;
Helper.getContent(null, () => Helper.scrollTabsRight());
} }
}, },
goToPreviousDirectoryClicked(prevURL) {
Service.url = prevURL;
Helper.getContent(null)
.then(() => Helper.scrollTabsRight())
.catch(Helper.loadingError);
},
}, },
}; };
export default RepoSidebar;
</script> </script>
<template> <template>
<div id="sidebar" :class="{'sidebar-mini' : isMini}" v-cloak> <div id="sidebar" :class="{'sidebar-mini' : isMini}">
<table class="table"> <table class="table">
<thead v-if="!isMini"> <thead v-if="!isMini">
<tr> <tr>
...@@ -82,7 +78,7 @@ export default RepoSidebar; ...@@ -82,7 +78,7 @@ export default RepoSidebar;
<repo-previous-directory <repo-previous-directory
v-if="isRoot" v-if="isRoot"
:prev-url="prevURL" :prev-url="prevURL"
@linkclicked="linkClicked(prevURL)"/> @linkclicked="goToPreviousDirectoryClicked(prevURL)"/>
<repo-loading-file <repo-loading-file
v-for="n in 5" v-for="n in 5"
:key="n" :key="n"
...@@ -94,7 +90,7 @@ export default RepoSidebar; ...@@ -94,7 +90,7 @@ export default RepoSidebar;
:key="file.id" :key="file.id"
:file="file" :file="file"
:is-mini="isMini" :is-mini="isMini"
@linkclicked="linkClicked(file)" @linkclicked="fileClicked(file)"
:is-tree="isTree" :is-tree="isTree"
:has-files="!!files.length" :has-files="!!files.length"
:active-file="activeFile"/> :active-file="activeFile"/>
......
...@@ -10,10 +10,16 @@ const RepoTab = { ...@@ -10,10 +10,16 @@ const RepoTab = {
}, },
computed: { computed: {
closeLabel() {
if (this.tab.changed) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
changedClass() { changedClass() {
const tabChangedObj = { const tabChangedObj = {
'fa-times': !this.tab.changed, 'fa-times close-icon': !this.tab.changed,
'fa-circle': this.tab.changed, 'fa-circle unsaved-icon': this.tab.changed,
}; };
return tabChangedObj; return tabChangedObj;
}, },
...@@ -22,9 +28,9 @@ const RepoTab = { ...@@ -22,9 +28,9 @@ const RepoTab = {
methods: { methods: {
tabClicked: Store.setActiveFiles, tabClicked: Store.setActiveFiles,
xClicked(file) { closeTab(file) {
if (file.changed) return; if (file.changed) return;
this.$emit('xclicked', file); this.$emit('tabclosed', file);
}, },
}, },
}; };
...@@ -33,13 +39,25 @@ export default RepoTab; ...@@ -33,13 +39,25 @@ export default RepoTab;
</script> </script>
<template> <template>
<li> <li @click="tabClicked(tab)">
<a href="#" class="close" @click.prevent="xClicked(tab)" v-if="!tab.loading"> <a
<i class="fa" :class="changedClass"></i> href="#0"
class="close"
@click.stop.prevent="closeTab(tab)"
:aria-label="closeLabel">
<i
class="fa"
:class="changedClass"
aria-hidden="true">
</i>
</a> </a>
<a href="#" class="repo-tab" v-if="!tab.loading" :title="tab.url" @click.prevent="tabClicked(tab)">{{tab.name}}</a> <a
href="#"
<i v-if="tab.loading" class="fa fa-spinner fa-spin"></i> class="repo-tab"
:title="tab.url"
@click.prevent="tabClicked(tab)">
{{tab.name}}
</a>
</li> </li>
</template> </template>
<script> <script>
import Vue from 'vue';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
...@@ -14,30 +13,24 @@ const RepoTabs = { ...@@ -14,30 +13,24 @@ const RepoTabs = {
data: () => Store, data: () => Store,
methods: { methods: {
isOverflow() { tabClosed(file) {
return this.$el.scrollWidth > this.$el.offsetWidth;
},
xClicked(file) {
Store.removeFromOpenedFiles(file); Store.removeFromOpenedFiles(file);
}, },
}, },
watch: {
openedFiles() {
Vue.nextTick(() => {
this.tabsOverflow = this.isOverflow();
});
},
},
}; };
export default RepoTabs; export default RepoTabs;
</script> </script>
<template> <template>
<ul id="tabs" v-if="isMini" v-cloak :class="{'overflown': tabsOverflow}"> <ul id="tabs">
<repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/> <repo-tab
v-for="tab in openedFiles"
:key="tab.id"
:tab="tab"
:class="{'active' : tab.active}"
@tabclosed="tabClosed"
/>
<li class="tabs-divider" /> <li class="tabs-divider" />
</ul> </ul>
</template> </template>
/* global monaco */ /* global monaco */
import RepoEditor from '../components/repo_editor.vue'; import RepoEditor from '../components/repo_editor.vue';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
function repoEditorLoader() { function repoEditorLoader() {
Store.monacoLoading = true; Store.monacoLoading = true;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
monacoLoader(['vs/editor/editor.main'], () => { monacoLoader(['vs/editor/editor.main'], () => {
Store.monaco = monaco; Helper.monaco = monaco;
Store.monacoLoading = false; Store.monacoLoading = false;
resolve(RepoEditor); resolve(RepoEditor);
}, reject); }, () => {
Store.monacoLoading = false;
reject();
});
}); });
} }
......
...@@ -4,6 +4,8 @@ import Store from '../stores/repo_store'; ...@@ -4,6 +4,8 @@ import Store from '../stores/repo_store';
import '../../flash'; import '../../flash';
const RepoHelper = { const RepoHelper = {
monacoInstance: null,
getDefaultActiveFile() { getDefaultActiveFile() {
return { return {
active: true, active: true,
...@@ -33,19 +35,23 @@ const RepoHelper = { ...@@ -33,19 +35,23 @@ const RepoHelper = {
? window.performance ? window.performance
: Date, : Date,
getBranch() { getFileExtension(fileName) {
return $('button.dropdown-menu-toggle').attr('data-ref'); return fileName.split('.').pop();
}, },
getLanguageIDForFile(file, langs) { getLanguageIDForFile(file, langs) {
const ext = file.name.split('.').pop(); const ext = RepoHelper.getFileExtension(file.name);
const foundLang = RepoHelper.findLanguage(ext, langs); const foundLang = RepoHelper.findLanguage(ext, langs);
return foundLang ? foundLang.id : 'plaintext'; return foundLang ? foundLang.id : 'plaintext';
}, },
getFilePathFromFullPath(fullPath, branch) { setMonacoModelFromLanguage() {
return fullPath.split(`${Store.projectUrl}/blob/${branch}`)[1]; 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) { findLanguage(ext, langs) {
...@@ -58,11 +64,11 @@ const RepoHelper = { ...@@ -58,11 +64,11 @@ const RepoHelper = {
file.opened = true; file.opened = true;
file.icon = 'fa-folder-open'; file.icon = 'fa-folder-open';
RepoHelper.toURL(file.url, file.name); RepoHelper.updateHistoryEntry(file.url, file.name);
return file; return file;
}, },
isKindaBinary() { isRenderable() {
const okExts = ['md', 'svg']; const okExts = ['md', 'svg'];
return okExts.indexOf(Store.activeFile.extension) > -1; return okExts.indexOf(Store.activeFile.extension) > -1;
}, },
...@@ -76,22 +82,8 @@ const RepoHelper = { ...@@ -76,22 +82,8 @@ const RepoHelper = {
.catch(RepoHelper.loadingError); .catch(RepoHelper.loadingError);
}, },
toggleFakeTab(loading, file) { // when you open a directory you need to put the directory files under
if (loading) return Store.addPlaceholderFile(); // the directory... This will merge the list of the current directory and the new list.
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;
},
getNewMergedList(inDirectory, currentList, newList) { getNewMergedList(inDirectory, currentList, newList) {
const newListSorted = newList.sort(this.compareFilesCaseInsensitive); const newListSorted = newList.sort(this.compareFilesCaseInsensitive);
if (!inDirectory) return newListSorted; if (!inDirectory) return newListSorted;
...@@ -100,6 +92,9 @@ const RepoHelper = { ...@@ -100,6 +92,9 @@ const RepoHelper = {
return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile); 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) { mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) {
newList.reverse().forEach((newFile) => { newList.reverse().forEach((newFile) => {
const fileIndex = indexOfFile + 1; const fileIndex = indexOfFile + 1;
...@@ -135,21 +130,17 @@ const RepoHelper = { ...@@ -135,21 +130,17 @@ const RepoHelper = {
return isRoot; return isRoot;
}, },
getContent(treeOrFile, cb) { getContent(treeOrFile) {
let file = treeOrFile; let file = treeOrFile;
// const loadingData = RepoHelper.setLoading(true);
return Service.getContent() return Service.getContent()
.then((response) => { .then((response) => {
const data = response.data; const data = response.data;
// RepoHelper.setLoading(false, loadingData);
if (cb) cb();
Store.isTree = RepoHelper.isTree(data); Store.isTree = RepoHelper.isTree(data);
if (!Store.isTree) { if (!Store.isTree) {
if (!file) file = data; if (!file) file = data;
Store.binary = data.binary; Store.binary = data.binary;
if (data.binary) { if (data.binary) {
Store.binaryMimeType = data.mime_type;
// file might be undefined // file might be undefined
RepoHelper.setBinaryDataAsBase64(data); RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview(); Store.setViewToPreview();
...@@ -188,9 +179,8 @@ const RepoHelper = { ...@@ -188,9 +179,8 @@ const RepoHelper = {
setFile(data, file) { setFile(data, file) {
const newFile = data; const newFile = data;
newFile.url = file.url || location.pathname;
newFile.url = file.url; newFile.url = file.url;
if (newFile.render_error === 'too_large') { if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
newFile.tooLarge = true; newFile.tooLarge = true;
} }
newFile.newContent = ''; newFile.newContent = '';
...@@ -199,10 +189,6 @@ const RepoHelper = { ...@@ -199,10 +189,6 @@ const RepoHelper = {
Store.setActiveFiles(newFile); Store.setActiveFiles(newFile);
}, },
toFA(icon) {
return `fa-${icon}`;
},
serializeBlob(blob) { serializeBlob(blob) {
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob); const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
simpleBlob.lastCommitMessage = blob.last_commit.message; simpleBlob.lastCommitMessage = blob.last_commit.message;
...@@ -226,7 +212,7 @@ const RepoHelper = { ...@@ -226,7 +212,7 @@ const RepoHelper = {
type, type,
name, name,
url, url,
icon: RepoHelper.toFA(icon), icon: `fa-${icon}`,
level: 0, level: 0,
loading: false, loading: false,
}; };
...@@ -244,42 +230,24 @@ const RepoHelper = { ...@@ -244,42 +230,24 @@ const RepoHelper = {
setTimeout(() => { setTimeout(() => {
const tabs = document.getElementById('tabs'); const tabs = document.getElementById('tabs');
if (!tabs) return; if (!tabs) return;
tabs.scrollLeft = 12000; tabs.scrollLeft = tabs.scrollWidth;
}, 200); }, 200);
}, },
dataToListOfFiles(data) { dataToListOfFiles(data) {
const a = []; const { blobs, trees, submodules } = data;
return [
// push in blobs ...blobs.map(blob => RepoHelper.serializeBlob(blob)),
data.blobs.forEach((blob) => { ...trees.map(tree => RepoHelper.serializeTree(tree)),
a.push(RepoHelper.serializeBlob(blob)); ...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)),
}); ];
data.trees.forEach((tree) => {
a.push(RepoHelper.serializeTree(tree));
});
data.submodules.forEach((submodule) => {
a.push(RepoHelper.serializeSubmodule(submodule));
});
return a;
}, },
genKey() { genKey() {
return RepoHelper.Time.now().toFixed(3); return RepoHelper.Time.now().toFixed(3);
}, },
getStateKey() { updateHistoryEntry(url, title) {
return RepoHelper.key;
},
setStateKey(key) {
RepoHelper.key = key;
},
toURL(url, title) {
const history = window.history; const history = window.history;
RepoHelper.key = RepoHelper.genKey(); RepoHelper.key = RepoHelper.genKey();
...@@ -296,7 +264,7 @@ const RepoHelper = { ...@@ -296,7 +264,7 @@ const RepoHelper = {
}, },
loadingError() { loadingError() {
Flash('Unable to load the file at this time.'); Flash('Unable to load this content at this time.');
}, },
}; };
......
...@@ -7,8 +7,7 @@ import RepoEditButton from './components/repo_edit_button.vue'; ...@@ -7,8 +7,7 @@ import RepoEditButton from './components/repo_edit_button.vue';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
function initDropdowns() { function initDropdowns() {
$('.project-refs-target-form').hide(); $('.js-tree-ref-target-holder').hide();
$('.fa-long-arrow-right').hide();
} }
function addEventsForNonVueEls() { function addEventsForNonVueEls() {
...@@ -34,6 +33,8 @@ function setInitialStore(data) { ...@@ -34,6 +33,8 @@ function setInitialStore(data) {
Store.projectId = data.projectId; Store.projectId = data.projectId;
Store.projectName = data.projectName; Store.projectName = data.projectName;
Store.projectUrl = data.projectUrl; Store.projectUrl = data.projectUrl;
Store.canCommit = data.canCommit;
Store.onTopOfBranch = data.onTopOfBranch;
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref'); Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
Store.checkIsCommitable(); Store.checkIsCommitable();
} }
...@@ -44,6 +45,9 @@ function initRepo(el) { ...@@ -44,6 +45,9 @@ function initRepo(el) {
components: { components: {
repo: Repo, repo: Repo,
}, },
render(createElement) {
return createElement('repo');
},
}); });
} }
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import axios from 'axios'; import axios from 'axios';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import Api from '../../api'; import Api from '../../api';
import Helper from '../helpers/repo_helper';
const RepoService = { const RepoService = {
url: '', url: '',
...@@ -12,16 +13,9 @@ const RepoService = { ...@@ -12,16 +13,9 @@ const RepoService = {
}, },
richExtensionRegExp: /md/, richExtensionRegExp: /md/,
checkCurrentBranchIsCommitable() {
const url = Store.service.refsUrl;
return axios.get(url, { params: {
ref: Store.currentBranch,
search: Store.currentBranch,
} });
},
getRaw(url) { getRaw(url) {
return axios.get(url, { return axios.get(url, {
// Stop Axios from parsing a JSON file into a JS object
transformResponse: [res => res], transformResponse: [res => res],
}); });
}, },
...@@ -36,7 +30,7 @@ const RepoService = { ...@@ -36,7 +30,7 @@ const RepoService = {
}, },
urlIsRichBlob(url = this.url) { urlIsRichBlob(url = this.url) {
const extension = url.split('.').pop(); const extension = Helper.getFileExtension(url);
return this.richExtensionRegExp.test(extension); return this.richExtensionRegExp.test(extension);
}, },
...@@ -73,7 +67,11 @@ const RepoService = { ...@@ -73,7 +67,11 @@ const RepoService = {
commitFiles(payload, cb) { commitFiles(payload, cb) {
Api.commitMultiple(Store.projectId, payload, (data) => { 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(); cb();
}); });
}, },
......
...@@ -3,13 +3,11 @@ import Helper from '../helpers/repo_helper'; ...@@ -3,13 +3,11 @@ import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
const RepoStore = { const RepoStore = {
ideEl: {},
monaco: {}, monaco: {},
monacoLoading: false, monacoLoading: false,
monacoInstance: {},
service: '', service: '',
editor: '', canCommit: false,
sidebar: '', onTopOfBranch: false,
editMode: false, editMode: false,
isTree: false, isTree: false,
isRoot: false, isRoot: false,
...@@ -17,19 +15,10 @@ const RepoStore = { ...@@ -17,19 +15,10 @@ const RepoStore = {
projectId: '', projectId: '',
projectName: '', projectName: '',
projectUrl: '', projectUrl: '',
trees: [],
blobs: [],
submodules: [],
blobRaw: '', blobRaw: '',
blobRendered: '',
currentBlobView: 'repo-preview', currentBlobView: 'repo-preview',
openedFiles: [], openedFiles: [],
tabSize: 100,
defaultTabSize: 100,
minTabSize: 30,
tabsOverflow: 41,
submitCommitsLoading: false, submitCommitsLoading: false,
binaryLoaded: false,
dialog: { dialog: {
open: false, open: false,
title: '', title: '',
...@@ -45,9 +34,6 @@ const RepoStore = { ...@@ -45,9 +34,6 @@ const RepoStore = {
currentBranch: '', currentBranch: '',
targetBranch: 'new-branch', targetBranch: 'new-branch',
commitMessage: '', commitMessage: '',
binaryMimeType: '',
// scroll bar space for windows
scrollWidth: 0,
binaryTypes: { binaryTypes: {
png: false, png: false,
md: false, md: false,
...@@ -58,7 +44,6 @@ const RepoStore = { ...@@ -58,7 +44,6 @@ const RepoStore = {
tree: false, tree: false,
blob: false, blob: false,
}, },
readOnly: true,
resetBinaryTypes() { resetBinaryTypes() {
Object.keys(RepoStore.binaryTypes).forEach((key) => { Object.keys(RepoStore.binaryTypes).forEach((key) => {
...@@ -68,14 +53,7 @@ const RepoStore = { ...@@ -68,14 +53,7 @@ const RepoStore = {
// mutations // mutations
checkIsCommitable() { checkIsCommitable() {
RepoStore.service.checkCurrentBranchIsCommitable() RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
.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.'));
}, },
addFilesToDirectory(inDirectory, currentList, newList) { addFilesToDirectory(inDirectory, currentList, newList) {
...@@ -96,7 +74,6 @@ const RepoStore = { ...@@ -96,7 +74,6 @@ const RepoStore = {
if (file.binary) { if (file.binary) {
RepoStore.blobRaw = file.base64; RepoStore.blobRaw = file.base64;
RepoStore.binaryMimeType = file.mime_type;
} else if (file.newContent || file.plain) { } else if (file.newContent || file.plain) {
RepoStore.blobRaw = file.newContent || file.plain; RepoStore.blobRaw = file.newContent || file.plain;
} else { } else {
...@@ -107,7 +84,7 @@ const RepoStore = { ...@@ -107,7 +84,7 @@ const RepoStore = {
}).catch(Helper.loadingError); }).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; RepoStore.binary = file.binary;
}, },
...@@ -134,15 +111,15 @@ const RepoStore = { ...@@ -134,15 +111,15 @@ const RepoStore = {
removeChildFilesOfTree(tree) { removeChildFilesOfTree(tree) {
let foundTree = false; let foundTree = false;
const treeToClose = tree; const treeToClose = tree;
let wereDone = false; let canStopSearching = false;
RepoStore.files = RepoStore.files.filter((file) => { RepoStore.files = RepoStore.files.filter((file) => {
const isItTheTreeWeWant = file.url === treeToClose.url; const isItTheTreeWeWant = file.url === treeToClose.url;
// if it's the next tree // if it's the next tree
if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) { if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) {
wereDone = true; canStopSearching = true;
return true; return true;
} }
if (wereDone) return true; if (canStopSearching) return true;
if (isItTheTreeWeWant) foundTree = true; if (isItTheTreeWeWant) foundTree = true;
...@@ -159,8 +136,8 @@ const RepoStore = { ...@@ -159,8 +136,8 @@ const RepoStore = {
if (file.type === 'tree') return; if (file.type === 'tree') return;
let foundIndex; let foundIndex;
RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => { RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => {
if (openedFile.url === file.url) foundIndex = i; if (openedFile.path === file.path) foundIndex = i;
return openedFile.url !== file.url; return openedFile.path !== file.path;
}); });
// now activate the right tab based on what you closed. // now activate the right tab based on what you closed.
...@@ -174,36 +151,16 @@ const RepoStore = { ...@@ -174,36 +151,16 @@ const RepoStore = {
return; return;
} }
if (foundIndex) { if (foundIndex && foundIndex > 0) {
if (foundIndex > 0) { RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]);
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) { addToOpenedFiles(file) {
const openFile = file; const openFile = file;
const openedFilesAlreadyExists = RepoStore.openedFiles const openedFilesAlreadyExists = RepoStore.openedFiles
.some(openedFile => openedFile.url === openFile.url); .some(openedFile => openedFile.path === openFile.path);
if (openedFilesAlreadyExists) return; if (openedFilesAlreadyExists) return;
...@@ -238,4 +195,5 @@ const RepoStore = { ...@@ -238,4 +195,5 @@ const RepoStore = {
return RepoStore.currentBlobView === 'repo-preview'; return RepoStore.currentBlobView === 'repo-preview';
}, },
}; };
export default RepoStore; export default RepoStore;
...@@ -71,7 +71,7 @@ export default { ...@@ -71,7 +71,7 @@ export default {
/> />
<div v-if="!isConfidential" class="no-value confidential-value"> <div v-if="!isConfidential" class="no-value confidential-value">
<i class="fa fa-eye is-not-confidential"></i> <i class="fa fa-eye is-not-confidential"></i>
None Not confidential
</div> </div>
<div v-else class="value confidential-value hide-collapsed"> <div v-else class="value confidential-value hide-collapsed">
<i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i> <i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i>
......
...@@ -120,7 +120,7 @@ export default { ...@@ -120,7 +120,7 @@ export default {
</a> </a>
<a <a
v-if="action.type === 'ujs-link'" v-else-if="action.type === 'ujs-link'"
:href="action.path" :href="action.path"
data-method="post" data-method="post"
rel="nofollow" rel="nofollow"
...@@ -129,7 +129,7 @@ export default { ...@@ -129,7 +129,7 @@ export default {
</a> </a>
<button <button
v-else="action.type === 'button'" v-else-if="action.type === 'button'"
@click="onClickAction(action)" @click="onClickAction(action)"
:disabled="action.isLoading" :disabled="action.isLoading"
:class="action.cssClass" :class="action.cssClass"
......
<script> <script>
const PopupDialog = { export default {
name: 'popup-dialog', name: 'popup-dialog',
props: { props: {
open: Boolean, title: {
title: String, type: String,
body: String, required: true,
},
body: {
type: String,
required: true,
},
kind: { kind: {
type: String, type: String,
required: false,
default: 'primary', default: 'primary',
}, },
closeButtonLabel: { closeButtonLabel: {
type: String, type: String,
required: false,
default: 'Cancel', default: 'Cancel',
}, },
primaryButtonLabel: { primaryButtonLabel: {
type: String, type: String,
default: 'Save changes', required: true,
}, },
}, },
computed: { computed: {
typeOfClass() { btnKindClass() {
const className = `btn-${this.kind}`; return {
const returnObj = {}; [`btn-${this.kind}`]: true,
returnObj[className] = true; };
return returnObj;
}, },
}, },
...@@ -33,33 +39,45 @@ const PopupDialog = { ...@@ -33,33 +39,45 @@ const PopupDialog = {
close() { close() {
this.$emit('toggle', false); this.$emit('toggle', false);
}, },
emitSubmit(status) {
yesClick() { this.$emit('submit', status);
this.$emit('submit', true);
},
noClick() {
this.$emit('submit', false);
}, },
}, },
}; };
export default PopupDialog;
</script> </script>
<template> <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-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" @click="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button type="button"
class="close"
@click="close"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">{{this.title}}</h4> <h4 class="modal-title">{{this.title}}</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>{{this.body}}</p> <p>{{this.body}}</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal" @click="noClick">{{closeButtonLabel}}</button> <button
<button type="button" class="btn" :class="typeOfClass" @click="yesClick">{{primaryButtonLabel}}</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> </div>
</div> </div>
......
/* global Breakpoints */ import bp from './breakpoints';
import './breakpoints';
export default class Wikis { export default class Wikis {
constructor() { constructor() {
this.bp = Breakpoints.get();
this.sidebarEl = document.querySelector('.js-wiki-sidebar'); this.sidebarEl = document.querySelector('.js-wiki-sidebar');
this.sidebarExpanded = false; this.sidebarExpanded = false;
...@@ -41,15 +38,15 @@ export default class Wikis { ...@@ -41,15 +38,15 @@ export default class Wikis {
this.renderSidebar(); this.renderSidebar();
} }
sidebarCanCollapse() { static sidebarCanCollapse() {
const bootstrapBreakpoint = this.bp.getBreakpointSize(); const bootstrapBreakpoint = bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
} }
renderSidebar() { renderSidebar() {
if (!this.sidebarEl) return; if (!this.sidebarEl) return;
const { classList } = this.sidebarEl; const { classList } = this.sidebarEl;
if (this.sidebarExpanded || !this.sidebarCanCollapse()) { if (this.sidebarExpanded || !Wikis.sidebarCanCollapse()) {
if (!classList.contains('right-sidebar-expanded')) { if (!classList.contains('right-sidebar-expanded')) {
classList.remove('right-sidebar-collapsed'); classList.remove('right-sidebar-collapsed');
classList.add('right-sidebar-expanded'); classList.add('right-sidebar-expanded');
......
...@@ -187,3 +187,81 @@ a { ...@@ -187,3 +187,81 @@ a {
.fade-in-full { .fade-in-full {
animation: fadeInFull $fade-in-duration 1; 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);
}
}
...@@ -372,6 +372,10 @@ table { ...@@ -372,6 +372,10 @@ table {
background: $gl-success !important; background: $gl-success !important;
} }
.dz-message {
margin: 0;
}
.space-right { .space-right {
margin-right: 10px; margin-right: 10px;
} }
......
...@@ -26,7 +26,7 @@ header { ...@@ -26,7 +26,7 @@ header {
&.navbar-gitlab { &.navbar-gitlab {
padding: 0 16px; padding: 0 16px;
z-index: 2000; z-index: 1000;
margin-bottom: 0; margin-bottom: 0;
min-height: $header-height; min-height: $header-height;
background-color: $gray-light; background-color: $gray-light;
......
...@@ -117,10 +117,6 @@ body { ...@@ -117,10 +117,6 @@ body {
margin-top: $header-height + $performance-bar-height; margin-top: $header-height + $performance-bar-height;
} }
[v-cloak] {
display: none;
}
.vertical-center { .vertical-center {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
......
...@@ -204,6 +204,16 @@ ...@@ -204,6 +204,16 @@
} }
} }
div.avatar {
display: inline-flex;
justify-content: center;
align-items: center;
.center {
line-height: 14px;
}
}
strong { strong {
color: $gl-text-color; color: $gl-text-color;
} }
......
...@@ -162,3 +162,5 @@ $pre-color: $gl-text-color !default; ...@@ -162,3 +162,5 @@ $pre-color: $gl-text-color !default;
$pre-border-color: $border-color; $pre-border-color: $border-color;
$table-bg-accent: $gray-light; $table-bg-accent: $gray-light;
$zindex-popover: 900;
...@@ -18,7 +18,8 @@ ...@@ -18,7 +18,8 @@
background-color: $gray-lightest; background-color: $gray-lightest;
} }
img.js-lazy-loaded { img.js-lazy-loaded,
img.emoji {
min-width: inherit; min-width: inherit;
min-height: inherit; min-height: inherit;
background-color: inherit; background-color: inherit;
......
...@@ -97,18 +97,30 @@ $new-sidebar-collapsed-width: 50px; ...@@ -97,18 +97,30 @@ $new-sidebar-collapsed-width: 50px;
top: $header-height; top: $header-height;
bottom: 0; bottom: 0;
left: 0; left: 0;
overflow: auto;
background-color: $gray-normal; background-color: $gray-normal;
box-shadow: inset -2px 0 0 $border-color; box-shadow: inset -2px 0 0 $border-color;
transform: translate3d(0, 0, 0);
&.sidebar-icons-only { &.sidebar-icons-only {
width: $new-sidebar-collapsed-width; width: $new-sidebar-collapsed-width;
overflow-x: hidden;
.nav-sidebar-inner-scroll {
overflow-x: hidden;
}
.nav-item-name,
.badge, .badge,
.project-title { .project-title {
display: none; display: none;
} }
.nav-item-name {
display: none;
}
.sidebar-top-level-items > li > a {
min-height: 44px;
}
} }
&.nav-sidebar-expanded { &.nav-sidebar-expanded {
...@@ -172,6 +184,12 @@ $new-sidebar-collapsed-width: 50px; ...@@ -172,6 +184,12 @@ $new-sidebar-collapsed-width: 50px;
} }
} }
.nav-sidebar-inner-scroll {
height: 100%;
width: 100%;
overflow: auto;
}
.with-performance-bar .nav-sidebar { .with-performance-bar .nav-sidebar {
top: $header-height + $performance-bar-height; top: $header-height + $performance-bar-height;
} }
...@@ -182,7 +200,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -182,7 +200,7 @@ $new-sidebar-collapsed-width: 50px;
> li { > li {
a { a {
padding: 8px 16px 8px 50px; padding: 8px 16px 8px 40px;
&:hover, &:hover,
&:focus { &:focus {
...@@ -215,6 +233,10 @@ $new-sidebar-collapsed-width: 50px; ...@@ -215,6 +233,10 @@ $new-sidebar-collapsed-width: 50px;
&:hover { &:hover {
color: $gl-text-color; color: $gl-text-color;
svg {
fill: $gl-text-color;
}
} }
} }
...@@ -301,6 +323,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -301,6 +323,7 @@ $new-sidebar-collapsed-width: 50px;
> a { > a {
margin-left: 4px; margin-left: 4px;
padding-left: 12px;
} }
.badge { .badge {
...@@ -361,7 +384,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -361,7 +384,7 @@ $new-sidebar-collapsed-width: 50px;
.sidebar-icons-only { .sidebar-icons-only {
.context-header { .context-header {
height: 60px; height: 61px;
a { a {
padding: 10px 4px; padding: 10px 4px;
......
...@@ -286,6 +286,10 @@ ...@@ -286,6 +286,10 @@
.gpg-status-box { .gpg-status-box {
&:empty {
display: none;
}
&.valid { &.valid {
@include green-status-color; @include green-status-color;
} }
......
...@@ -560,9 +560,13 @@ ...@@ -560,9 +560,13 @@
} }
.diff-files-changed { .diff-files-changed {
.inline-parallel-buttons {
position: relative;
z-index: 1;
}
.commit-stat-summary { .commit-stat-summary {
@include new-style-dropdown; @include new-style-dropdown;
z-index: -1;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
margin-left: -$gl-padding; margin-left: -$gl-padding;
...@@ -574,10 +578,14 @@ ...@@ -574,10 +578,14 @@
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
position: -webkit-sticky; position: -webkit-sticky;
position: sticky; position: sticky;
top: 84px; top: 34px;
background-color: $white-light; background-color: $white-light;
z-index: 190; z-index: 190;
&.diff-files-changed-merge-request {
top: 84px;
}
+ .files, + .files,
+ .alert { + .alert {
margin-top: 1px; margin-top: 1px;
......
...@@ -8,13 +8,13 @@ ...@@ -8,13 +8,13 @@
.is-confidential { .is-confidential {
color: $orange-600; color: $orange-600;
background-color: $orange-50; background-color: $orange-50;
border-radius: 3px; border-radius: $border-radius-default;
padding: 5px; padding: 5px;
margin: 0 3px 0 -4px; margin: 0 3px 0 -4px;
} }
.is-not-confidential { .is-not-confidential {
border-radius: 3px; border-radius: $border-radius-default;
padding: 5px; padding: 5px;
margin: 0 3px 0 -4px; margin: 0 3px 0 -4px;
} }
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
.commit-box, .commit-box,
.info-well, .info-well,
.commit-ci-menu, .commit-ci-menu,
.files-changed, .files-changed-inner,
.limited-header-width, .limited-header-width,
.limited-width-notes { .limited-width-notes {
@extend .fixed-width-container; @extend .fixed-width-container;
...@@ -81,6 +81,7 @@ ...@@ -81,6 +81,7 @@
border: 1px solid $white-normal; border: 1px solid $white-normal;
padding: 5px; padding: 5px;
max-height: calc(100vh - 100px); max-height: calc(100vh - 100px);
max-width: 100%;
} }
.emoji-block { .emoji-block {
...@@ -259,7 +260,7 @@ ...@@ -259,7 +260,7 @@
padding-top: 10px; padding-top: 10px;
} }
&:not(.issue-boards-sidebar):not([data-signed-in]) { &:not(.issue-boards-sidebar):not([data-signed-in]):not([data-always-show-toggle]) {
.issuable-sidebar-header { .issuable-sidebar-header {
display: none; display: none;
} }
......
...@@ -108,6 +108,7 @@ ...@@ -108,6 +108,7 @@
background-color: $orange-50; background-color: $orange-50;
border-radius: $border-radius-default $border-radius-default 0 0; border-radius: $border-radius-default $border-radius-default 0 0;
border: 1px solid $border-gray-normal; border: 1px solid $border-gray-normal;
border-bottom: none;
padding: 3px 12px; padding: 3px 12px;
margin: auto; margin: auto;
align-items: center; align-items: center;
...@@ -132,22 +133,9 @@ ...@@ -132,22 +133,9 @@
} }
} }
.not-confidential { .confidential-issue-warning + .md-area {
padding: 0; border-top-left-radius: 0;
border-top: none; border-top-right-radius: 0;
}
.right-sidebar-expanded {
.md-area {
border-radius: 0;
border-top: none;
}
}
.right-sidebar-collapsed {
.confidential-issue-warning {
border-bottom: none;
}
} }
.discussion-form { .discussion-form {
......
...@@ -453,7 +453,10 @@ ul.notes { ...@@ -453,7 +453,10 @@ ul.notes {
} }
.note-actions { .note-actions {
align-self: flex-start;
flex-shrink: 0; flex-shrink: 0;
display: inline-flex;
align-items: center;
// For PhantomJS that does not support flex // For PhantomJS that does not support flex
float: right; float: right;
margin-left: 10px; margin-left: 10px;
...@@ -463,18 +466,12 @@ ul.notes { ...@@ -463,18 +466,12 @@ ul.notes {
float: none; float: none;
margin-left: 0; margin-left: 0;
} }
.note-action-button {
margin-left: 8px;
}
.more-actions-toggle {
margin-left: 2px;
}
} }
.more-actions { .more-actions {
display: inline-block; float: right; // phantomjs fallback
display: flex;
align-items: flex-end;
.tooltip { .tooltip {
white-space: nowrap; white-space: nowrap;
...@@ -482,16 +479,10 @@ ul.notes { ...@@ -482,16 +479,10 @@ ul.notes {
} }
.more-actions-toggle { .more-actions-toggle {
padding: 0;
&:hover .icon, &:hover .icon,
&:focus .icon { &:focus .icon {
color: $blue-600; color: $blue-600;
} }
.icon {
padding: 0 6px;
}
} }
.more-actions-dropdown { .more-actions-dropdown {
...@@ -519,28 +510,42 @@ ul.notes { ...@@ -519,28 +510,42 @@ ul.notes {
@include notes-media('max', $screen-md-max) { @include notes-media('max', $screen-md-max) {
float: none; float: none;
margin-left: 0; margin-left: 0;
}
}
.note-action-button { .note-actions-item {
margin-left: 0; margin-left: 15px;
} display: flex;
align-items: center;
&.more-actions {
// compensate for narrow icon
margin-left: 10px;
} }
} }
.note-action-button { .note-action-button {
display: inline; line-height: 1;
line-height: 20px; padding: 0;
min-width: 16px;
color: $gray-darkest;
.fa { .fa {
color: $gray-darkest;
position: relative; position: relative;
font-size: 17px; font-size: 16px;
} }
svg { svg {
height: 16px; height: 16px;
width: 16px; width: 16px;
fill: $gray-darkest; top: 0;
vertical-align: text-top; vertical-align: text-top;
path {
fill: currentColor;
}
} }
.award-control-icon-positive, .award-control-icon-positive,
...@@ -613,10 +618,7 @@ ul.notes { ...@@ -613,10 +618,7 @@ ul.notes {
.note-role { .note-role {
position: relative; position: relative;
top: -2px; padding: 0 7px;
display: inline-block;
padding-left: 7px;
padding-right: 7px;
color: $notes-role-color; color: $notes-role-color;
font-size: 12px; font-size: 12px;
line-height: 20px; line-height: 20px;
......
...@@ -824,6 +824,7 @@ button.mini-pipeline-graph-dropdown-toggle { ...@@ -824,6 +824,7 @@ button.mini-pipeline-graph-dropdown-toggle {
* Top arrow in the dropdown in the mini pipeline graph * Top arrow in the dropdown in the mini pipeline graph
*/ */
.mini-pipeline-graph-dropdown-menu { .mini-pipeline-graph-dropdown-menu {
z-index: 200;
&::before, &::before,
&::after { &::after {
......
...@@ -566,14 +566,14 @@ a.deploy-project-label { ...@@ -566,14 +566,14 @@ a.deploy-project-label {
&::before { &::before {
content: "OR"; content: "OR";
position: absolute; position: absolute;
left: 0; left: -10px;
top: 40%; top: 50%;
z-index: 10; z-index: 10;
padding: 8px 0; padding: 8px 0;
text-align: center; text-align: center;
background-color: $white-light; background-color: $white-light;
color: $gl-text-color-tertiary; color: $gl-text-color-tertiary;
transform: translateX(-50%); transform: translateY(-50%);
font-size: 12px; font-size: 12px;
font-weight: bold; font-weight: bold;
line-height: 20px; line-height: 20px;
...@@ -581,8 +581,8 @@ a.deploy-project-label { ...@@ -581,8 +581,8 @@ a.deploy-project-label {
// Mobile // Mobile
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
left: 50%; left: 50%;
top: 10px; top: 0;
transform: translateY(-50%); transform: translateX(-50%);
padding: 0 8px; padding: 0 8px;
} }
} }
......
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
transition: opacity .5s; transition: opacity $sidebar-transition-duration;
} }
.monaco-loader { .monaco-loader {
...@@ -28,11 +28,6 @@ ...@@ -28,11 +28,6 @@
.project-refs-form, .project-refs-form,
.project-refs-target-form { .project-refs-target-form {
display: inline-block; display: inline-block;
&.disabled {
opacity: 0.5;
pointer-events: none;
}
} }
.fade-enter, .fade-enter,
...@@ -90,7 +85,7 @@ ...@@ -90,7 +85,7 @@
} }
.blob-viewer-container { .blob-viewer-container {
height: calc(100vh - 63px); height: calc(100vh - 62px);
overflow: auto; overflow: auto;
} }
...@@ -114,6 +109,7 @@ ...@@ -114,6 +109,7 @@
border-right: 1px solid $white-dark; border-right: 1px solid $white-dark;
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
white-space: nowrap; white-space: nowrap;
cursor: pointer;
&.remove { &.remove {
animation: swipeRightDissapear ease-in 0.1s; animation: swipeRightDissapear ease-in 0.1s;
...@@ -133,10 +129,10 @@ ...@@ -133,10 +129,10 @@
a { a {
@include str-truncated(100px); @include str-truncated(100px);
color: $black; color: $black;
display: inline-block;
width: 100px; width: 100px;
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
text-decoration: none;
&.close { &.close {
width: auto; width: auto;
...@@ -146,15 +142,15 @@ ...@@ -146,15 +142,15 @@
} }
} }
i.fa.fa-times, .close-icon,
i.fa.fa-circle { .unsaved-icon {
float: right; float: right;
margin-top: 3px; margin-top: 3px;
margin-left: 15px; margin-left: 15px;
color: $gray-darkest; color: $gray-darkest;
} }
i.fa.fa-circle { .unsaved-icon {
color: $brand-success; color: $brand-success;
} }
...@@ -204,7 +200,7 @@ ...@@ -204,7 +200,7 @@
background: $gray-light; background: $gray-light;
padding: 20px; padding: 20px;
span.help-block { .help-block {
padding-top: 7px; padding-top: 7px;
margin-top: 0; margin-top: 0;
} }
...@@ -232,6 +228,7 @@ ...@@ -232,6 +228,7 @@
vertical-align: top; vertical-align: top;
width: 20%; width: 20%;
border-right: 1px solid $white-normal; border-right: 1px solid $white-normal;
min-height: 475px;
height: calc(100vh + 20px); height: calc(100vh + 20px);
overflow: auto; overflow: auto;
} }
...@@ -261,7 +258,6 @@ ...@@ -261,7 +258,6 @@
text-transform: uppercase; text-transform: uppercase;
font-weight: bold; font-weight: bold;
color: $gray-darkest; color: $gray-darkest;
width: 185px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
...@@ -270,7 +266,7 @@ ...@@ -270,7 +266,7 @@
} }
} }
.fa { .file-icon {
margin-right: 5px; margin-right: 5px;
} }
...@@ -280,118 +276,22 @@ ...@@ -280,118 +276,22 @@
} }
a { a {
@include str-truncated(250px);
color: $almost-black; color: $almost-black;
display: inline-block; display: inline-block;
vertical-align: middle; 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 { .render-error {
min-height: calc(100vh - 63px); min-height: calc(100vh - 62px);
p { p {
width: 100%; width: 100%;
} }
} }
@keyframes blockTextShine {
0% {
transform: translateX(-468px);
}
100% {
transform: translateX(468px);
}
}
@keyframes swipeRightAppear { @keyframes swipeRightAppear {
0% { 0% {
transform: scaleX(0.00); transform: scaleX(0.00);
......
...@@ -29,6 +29,10 @@ ...@@ -29,6 +29,10 @@
margin-right: 15px; margin-right: 15px;
} }
.tree-ref-target-holder {
display: inline-block;
}
.repo-breadcrumb { .repo-breadcrumb {
li:last-of-type { li:last-of-type {
position: relative; position: relative;
...@@ -216,6 +220,9 @@ ...@@ -216,6 +220,9 @@
} }
.blob-upload-dropzone-previews { .blob-upload-dropzone-previews {
display: flex;
justify-content: center;
align-items: center;
text-align: center; text-align: center;
border: 2px; border: 2px;
border-style: dashed; border-style: dashed;
......
...@@ -45,7 +45,7 @@ class Admin::AppearancesController < Admin::ApplicationController ...@@ -45,7 +45,7 @@ class Admin::AppearancesController < Admin::ApplicationController
# Use callbacks to share common setup or constraints between actions. # Use callbacks to share common setup or constraints between actions.
def set_appearance def set_appearance
@appearance = Appearance.last || Appearance.new @appearance = Appearance.current || Appearance.new
end end
# Only allow a trusted parameter "white list" through. # Only allow a trusted parameter "white list" through.
......
...@@ -52,8 +52,10 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -52,8 +52,10 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end end
def load_events def load_events
@events = Event.in_projects(load_projects(params.merge(non_public: true))) projects = load_projects(params.merge(non_public: true))
@events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0) @events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
end end
end end
...@@ -29,9 +29,9 @@ class DashboardController < Dashboard::ApplicationController ...@@ -29,9 +29,9 @@ class DashboardController < Dashboard::ApplicationController
current_user.authorized_projects current_user.authorized_projects
end end
@events = Event.in_projects(projects) @events = EventCollection
@events = @event_filter.apply_filter(@events).with_associations .new(projects, offset: params[:offset].to_i, filter: @event_filter)
@events = @events.limit(20).offset(params[:offset] || 0) .to_a
end end
def set_show_full_reference def set_show_full_reference
......
...@@ -6,7 +6,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -6,7 +6,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def index def index
params[:sort] ||= 'latest_activity_desc' params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort] @sort = params[:sort]
@projects = load_projects.page(params[:page]) @projects = load_projects
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending def trending
params[:trending] = true params[:trending] = true
@sort = params[:sort] @sort = params[:sort]
@projects = load_projects.page(params[:page]) @projects = load_projects
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -34,7 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -34,7 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end end
def starred def starred
@projects = load_projects.reorder('star_count DESC').page(params[:page]) @projects = load_projects.reorder('star_count DESC')
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -50,6 +50,9 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -50,6 +50,9 @@ class Explore::ProjectsController < Explore::ApplicationController
def load_projects def load_projects
ProjectsFinder.new(current_user: current_user, params: params) ProjectsFinder.new(current_user: current_user, params: params)
.execute.includes(:route, namespace: :route) .execute
.includes(:route, namespace: :route)
.page(params[:page])
.without_count
end end
end end
...@@ -160,9 +160,9 @@ class GroupsController < Groups::ApplicationController ...@@ -160,9 +160,9 @@ class GroupsController < Groups::ApplicationController
end end
def load_events def load_events
@events = Event.in_projects(@projects) @events = EventCollection
@events = event_filter.apply_filter(@events).with_associations .new(@projects, offset: params[:offset].to_i, filter: event_filter)
@events = @events.limit(20).offset(params[:offset] || 0) .to_a
end end
def user_actions def user_actions
......
...@@ -198,6 +198,10 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -198,6 +198,10 @@ class Projects::BlobController < Projects::ApplicationController
json = blob_json(@blob) json = blob_json(@blob)
return render_404 unless json return render_404 unless json
path_segments = @path.split('/')
path_segments.pop
tree_path = path_segments.join('/')
render json: json.merge( render json: json.merge(
path: blob.path, path: blob.path,
name: blob.name, name: blob.name,
...@@ -212,6 +216,7 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -212,6 +216,7 @@ class Projects::BlobController < Projects::ApplicationController
raw_path: project_raw_path(project, @id), raw_path: project_raw_path(project, @id),
blame_path: project_blame_path(project, @id), blame_path: project_blame_path(project, @id),
commits_path: project_commits_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)) permalink: project_blob_path(project, File.join(@commit.id, @path))
) )
end end
......
...@@ -301,10 +301,11 @@ class ProjectsController < Projects::ApplicationController ...@@ -301,10 +301,11 @@ class ProjectsController < Projects::ApplicationController
end end
def load_events def load_events
@events = @project.events.recent projects = Project.where(id: @project.id)
@events = event_filter.apply_filter(@events).with_associations
limit = (params[:limit] || 20).to_i @events = EventCollection
@events = @events.limit(limit).offset(params[:offset] || 0) .new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
end end
def project_params def project_params
......
...@@ -18,7 +18,7 @@ class Admin::ProjectsFinder ...@@ -18,7 +18,7 @@ class Admin::ProjectsFinder
end end
def execute def execute
items = Project.with_statistics items = Project.without_deleted.with_statistics
items = items.in_namespace(namespace_id) if namespace_id.present? items = items.in_namespace(namespace_id) if namespace_id.present?
items = items.where(visibility_level: visibility_level) if visibility_level.present? items = items.where(visibility_level: visibility_level) if visibility_level.present?
items = items.with_push if with_push.present? items = items.with_push if with_push.present?
......
...@@ -20,7 +20,7 @@ module AppearancesHelper ...@@ -20,7 +20,7 @@ module AppearancesHelper
end end
def brand_item def brand_item
@appearance ||= Appearance.first @appearance ||= Appearance.current
end end
def brand_header_logo def brand_header_logo
......
module PaginationHelper
def paginate_collection(collection, remote: nil)
if collection.is_a?(Kaminari::PaginatableWithoutCount)
paginate_without_count(collection)
elsif collection.respond_to?(:total_pages)
paginate_with_count(collection, remote: remote)
end
end
def paginate_without_count(collection)
render(
'kaminari/gitlab/without_count',
previous_path: path_to_prev_page(collection),
next_path: path_to_next_page(collection)
)
end
def paginate_with_count(collection, remote: nil)
paginate(collection, remote: remote, theme: 'gitlab')
end
end
...@@ -234,6 +234,8 @@ module ProjectsHelper ...@@ -234,6 +234,8 @@ module ProjectsHelper
# If no limit is applied we'll just issue a COUNT since the result set could # If no limit is applied we'll just issue a COUNT since the result set could
# be too large to load into memory. # be too large to load into memory.
def any_projects?(projects) def any_projects?(projects)
return projects.any? if projects.is_a?(Array)
if projects.limit_value if projects.limit_value
projects.to_a.any? projects.to_a.any?
else else
......
...@@ -2,7 +2,7 @@ module VersionCheckHelper ...@@ -2,7 +2,7 @@ module VersionCheckHelper
def version_status_badge def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled if Rails.env.production? && current_application_settings.version_check_enabled
image_url = VersionCheck.new.url image_url = VersionCheck.new.url
image_tag image_url, class: 'js-version-status-badge', lazy: false image_tag image_url, class: 'js-version-status-badge'
end end
end end
end end
...@@ -11,11 +11,11 @@ module Emails ...@@ -11,11 +11,11 @@ module Emails
@member_source_type = member_source_type @member_source_type = member_source_type
@member_id = member_id @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 # A project in a group can have no explicit owners/masters, in that case
# we fallbacks to the group's owners/masters. # we fallbacks to the group's owners/masters.
if admins.empty? && member_source.respond_to?(:group) && member_source.group 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 end
mail(to: admins, mail(to: admins,
......
...@@ -8,7 +8,27 @@ class Appearance < ActiveRecord::Base ...@@ -8,7 +8,27 @@ class Appearance < ActiveRecord::Base
validates :logo, file_size: { maximum: 1.megabyte } validates :logo, file_size: { maximum: 1.megabyte }
validates :header_logo, file_size: { maximum: 1.megabyte } validates :header_logo, file_size: { maximum: 1.megabyte }
validate :single_appearance_row, on: :create
mount_uploader :logo, AttachmentUploader mount_uploader :logo, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader mount_uploader :header_logo, AttachmentUploader
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
CACHE_KEY = 'current_appearance'.freeze
after_commit :flush_redis_cache
def self.current
Rails.cache.fetch(CACHE_KEY) { first }
end
def flush_redis_cache
Rails.cache.delete(CACHE_KEY)
end
def single_appearance_row
if self.class.any?
errors.add(:single_appearance_row, 'Only 1 appearances row can exist')
end
end
end end
...@@ -14,9 +14,15 @@ class BroadcastMessage < ActiveRecord::Base ...@@ -14,9 +14,15 @@ class BroadcastMessage < ActiveRecord::Base
default_value_for :color, '#E75E40' default_value_for :color, '#E75E40'
default_value_for :font, '#FFFFFF' default_value_for :font, '#FFFFFF'
CACHE_KEY = 'broadcast_message_current'.freeze
after_commit :flush_redis_cache
def self.current def self.current
Rails.cache.fetch("broadcast_message_current", expires_in: 1.minute) do Rails.cache.fetch(CACHE_KEY) do
where('ends_at > :now AND starts_at <= :now', now: Time.zone.now).order([:created_at, :id]).to_a where('ends_at > :now AND starts_at <= :now', now: Time.zone.now)
.reorder(id: :asc)
.to_a
end end
end end
...@@ -31,4 +37,8 @@ class BroadcastMessage < ActiveRecord::Base ...@@ -31,4 +37,8 @@ class BroadcastMessage < ActiveRecord::Base
def ended? def ended?
ends_at < Time.zone.now ends_at < Time.zone.now
end end
def flush_redis_cache
Rails.cache.delete(CACHE_KEY)
end
end end
...@@ -392,6 +392,6 @@ class Commit ...@@ -392,6 +392,6 @@ class Commit
end end
def gpg_commit def gpg_commit
@gpg_commit ||= Gitlab::Gpg::Commit.new(self) @gpg_commit ||= Gitlab::Gpg::Commit.for_commit(self)
end end
end end
...@@ -48,6 +48,7 @@ class Event < ActiveRecord::Base ...@@ -48,6 +48,7 @@ class Event < ActiveRecord::Base
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :project belongs_to :project
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_one :push_event_payload, foreign_key: :event_id
# For Hash only # For Hash only
serialize :data # rubocop:disable Cop/ActiveRecordSerialize serialize :data # rubocop:disable Cop/ActiveRecordSerialize
...@@ -55,19 +56,55 @@ class Event < ActiveRecord::Base ...@@ -55,19 +56,55 @@ class Event < ActiveRecord::Base
# Callbacks # Callbacks
after_create :reset_project_activity after_create :reset_project_activity
after_create :set_last_repository_updated_at, if: :push? after_create :set_last_repository_updated_at, if: :push?
after_create :replicate_event_for_push_events_migration
# Scopes # Scopes
scope :recent, -> { reorder(id: :desc) } scope :recent, -> { reorder(id: :desc) }
scope :code_push, -> { where(action: PUSHED) } scope :code_push, -> { where(action: PUSHED) }
scope :in_projects, ->(projects) do scope :in_projects, -> (projects) do
where(project_id: projects.pluck(:id)).recent sub_query = projects
.except(:order)
.select(1)
.where('projects.id = events.project_id')
where('EXISTS (?)', sub_query).recent
end
scope :with_associations, -> do
# We're using preload for "push_event_payload" as otherwise the association
# is not always available (depending on the query being built).
includes(:author, :project, project: :namespace)
.preload(:target, :push_event_payload)
end end
scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) }
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) } scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
self.inheritance_column = 'action'
class << self class << self
def model_name
ActiveModel::Name.new(self, nil, 'event')
end
def find_sti_class(action)
if action.to_i == PUSHED
PushEvent
else
Event
end
end
def subclass_from_attributes(attrs)
# Without this Rails will keep calling this method on the returned class,
# resulting in an infinite loop.
return unless self == Event
action = attrs.with_indifferent_access[inheritance_column].to_i
PushEvent if action == PUSHED
end
# Update Gitlab::ContributionsCalendar#activity_dates if this changes # Update Gitlab::ContributionsCalendar#activity_dates if this changes
def contributions def contributions
where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)", where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)",
...@@ -290,6 +327,16 @@ class Event < ActiveRecord::Base ...@@ -290,6 +327,16 @@ class Event < ActiveRecord::Base
@commits ||= (data[:commits] || []).reverse @commits ||= (data[:commits] || []).reverse
end end
def commit_title
commit = commits.last
commit[:message] if commit
end
def commit_id
commit_to || commit_from
end
def commits_count def commits_count
data[:total_commits_count] || commits.count || 0 data[:total_commits_count] || commits.count || 0
end end
...@@ -385,6 +432,22 @@ class Event < ActiveRecord::Base ...@@ -385,6 +432,22 @@ class Event < ActiveRecord::Base
user ? author_id == user.id : false user ? author_id == user.id : false
end end
# We're manually replicating data into the new table since database triggers
# are not dumped to db/schema.rb. This could mean that a new installation
# would not have the triggers in place, thus losing events data in GitLab
# 10.0.
def replicate_event_for_push_events_migration
new_attributes = attributes.with_indifferent_access.except(:title, :data)
EventForMigration.create!(new_attributes)
end
def to_partial_path
# We are intentionally using `Event` rather than `self.class` so that
# subclasses also use the `Event` implementation.
Event._to_partial_path
end
private private
def recent_update? def recent_update?
......
# A collection of events to display in an event list.
#
# An EventCollection is meant to be used for displaying events to a user (e.g.
# in a controller), it's not suitable for building queries that are used for
# building other queries.
class EventCollection
# To prevent users from putting too much pressure on the database by cycling
# through thousands of events we put a limit on the number of pages.
MAX_PAGE = 10
# projects - An ActiveRecord::Relation object that returns the projects for
# which to retrieve events.
# filter - An EventFilter instance to use for filtering events.
def initialize(projects, limit: 20, offset: 0, filter: nil)
@projects = projects
@limit = limit
@offset = offset
@filter = filter
end
# Returns an Array containing the events.
def to_a
return [] if current_page > MAX_PAGE
relation = if Gitlab::Database.join_lateral_supported?
relation_with_join_lateral
else
relation_without_join_lateral
end
relation.with_associations.to_a
end
private
# Returns the events relation to use when JOIN LATERAL is not supported.
#
# This relation simply gets all the events for all authorized projects, then
# limits that set.
def relation_without_join_lateral
events = filtered_events.in_projects(projects)
paginate_events(events)
end
# Returns the events relation to use when JOIN LATERAL is supported.
#
# This relation is built using JOIN LATERAL, producing faster queries than a
# regular LIMIT + OFFSET approach.
def relation_with_join_lateral
projects_for_lateral = projects.select(:id).to_sql
lateral = filtered_events
.limit(limit_for_join_lateral)
.where('events.project_id = projects_for_lateral.id')
.to_sql
# The outer query does not need to re-apply the filters since the JOIN
# LATERAL body already takes care of this.
outer = base_relation
.from("(#{projects_for_lateral}) projects_for_lateral")
.joins("JOIN LATERAL (#{lateral}) AS #{Event.table_name} ON true")
paginate_events(outer)
end
def filtered_events
@filter ? @filter.apply_filter(base_relation) : base_relation
end
def paginate_events(events)
events.limit(@limit).offset(@offset)
end
def base_relation
# We want to have absolute control over the event queries being built, thus
# we're explicitly opting out of any default scopes that may be set.
Event.unscoped.recent
end
def limit_for_join_lateral
# Applying the OFFSET on the inside of a JOIN LATERAL leads to incorrect
# results. To work around this we need to increase the inner limit for every
# page.
#
# This means that on page 1 we use LIMIT 20, and an outer OFFSET of 0. On
# page 2 we use LIMIT 40 and an outer OFFSET of 20.
@limit + @offset
end
def current_page
(@offset / @limit) + 1
end
def projects
@projects.except(:order)
end
end
# This model is used to replicate events between the old "events" table and the
# new "events_for_migration" table that will replace "events" in GitLab 10.0.
class EventForMigration < ActiveRecord::Base
self.table_name = 'events_for_migration'
end
...@@ -18,4 +18,8 @@ class GpgSignature < ActiveRecord::Base ...@@ -18,4 +18,8 @@ class GpgSignature < ActiveRecord::Base
def commit def commit
project.commit(commit_sha) project.commit(commit_sha)
end end
def gpg_commit
Gitlab::Gpg::Commit.new(project, commit_sha)
end
end end
...@@ -212,21 +212,39 @@ class Group < Namespace ...@@ -212,21 +212,39 @@ class Group < Namespace
end end
def user_ids_for_project_authorizations def user_ids_for_project_authorizations
users_with_parents.pluck(:id) members_with_parents.pluck(:user_id)
end end
def members_with_parents 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 end
def users_with_parents 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 end
def users_with_descendants 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 end
def max_member_access_for_user(user) def max_member_access_for_user(user)
......
...@@ -41,9 +41,20 @@ class Member < ActiveRecord::Base ...@@ -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)) 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) user_is_active = User.arel_table[:state].eq(:active)
includes(:user).references(:users) user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_active)
.where(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) .where(requested_at: nil)
.reorder(nil)
end end
scope :invite, -> { where.not(invite_token: nil) } scope :invite, -> { where.not(invite_token: nil) }
......
...@@ -156,6 +156,14 @@ class Namespace < ActiveRecord::Base ...@@ -156,6 +156,14 @@ class Namespace < ActiveRecord::Base
.base_and_ancestors .base_and_ancestors
end 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. # Returns all the descendants of the current namespace.
def descendants def descendants
Gitlab::GroupHierarchy Gitlab::GroupHierarchy
...@@ -163,6 +171,12 @@ class Namespace < ActiveRecord::Base ...@@ -163,6 +171,12 @@ class Namespace < ActiveRecord::Base
.base_and_descendants .base_and_descendants
end end
def self_and_descendants
Gitlab::GroupHierarchy
.new(self.class.where(id: id))
.base_and_descendants
end
def user_ids_for_project_authorizations def user_ids_for_project_authorizations
[owner_id] [owner_id]
end end
......
...@@ -60,7 +60,7 @@ class Project < ActiveRecord::Base ...@@ -60,7 +60,7 @@ class Project < ActiveRecord::Base
end end
before_destroy :remove_private_deploy_keys before_destroy :remove_private_deploy_keys
after_destroy :remove_pages after_destroy -> { run_after_commit { remove_pages } }
# update visibility_level of forks # update visibility_level of forks
after_update :update_forks_visibility_level after_update :update_forks_visibility_level
...@@ -196,7 +196,6 @@ class Project < ActiveRecord::Base ...@@ -196,7 +196,6 @@ class Project < ActiveRecord::Base
accepts_nested_attributes_for :import_data accepts_nested_attributes_for :import_data
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :count, to: :forks, prefix: true
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
...@@ -1222,6 +1221,9 @@ class Project < ActiveRecord::Base ...@@ -1222,6 +1221,9 @@ class Project < ActiveRecord::Base
# TODO: what to do here when not using Legacy Storage? Do we still need to rename and delay removal? # TODO: what to do here when not using Legacy Storage? Do we still need to rename and delay removal?
def remove_pages def remove_pages
# Projects with a missing namespace cannot have their pages removed
return unless namespace
::Projects::UpdatePagesConfigurationService.new(self).execute ::Projects::UpdatePagesConfigurationService.new(self).execute
# 1. We rename pages to temporary directory # 1. We rename pages to temporary directory
...@@ -1393,6 +1395,10 @@ class Project < ActiveRecord::Base ...@@ -1393,6 +1395,10 @@ class Project < ActiveRecord::Base
# @deprecated cannot remove yet because it has an index with its name in elasticsearch # @deprecated cannot remove yet because it has an index with its name in elasticsearch
alias_method :path_with_namespace, :full_path alias_method :path_with_namespace, :full_path
def forks_count
Projects::ForksCountService.new(self).count
end
private private
def cross_namespace_reference?(from) def cross_namespace_reference?(from)
......
class PushEvent < Event
# This validation exists so we can't accidentally use PushEvent with a
# different "action" value.
validate :validate_push_action
# Authors are required as they're used to display who pushed data.
#
# We're just validating the presence of the ID here as foreign key constraints
# should ensure the ID points to a valid user.
validates :author_id, presence: true
# The project is required to build links to commits, commit ranges, etc.
#
# We're just validating the presence of the ID here as foreign key constraints
# should ensure the ID points to a valid project.
validates :project_id, presence: true
# The "data" field must not be set for push events since it's not used and a
# waste of space.
validates :data, absence: true
# These fields are also not used for push events, thus storing them would be a
# waste.
validates :target_id, absence: true
validates :target_type, absence: true
def self.sti_name
PUSHED
end
def push?
true
end
def push_with_commits?
!!(commit_from && commit_to)
end
def tag?
return super unless push_event_payload
push_event_payload.tag?
end
def branch?
return super unless push_event_payload
push_event_payload.branch?
end
def valid_push?
return super unless push_event_payload
push_event_payload.ref.present?
end
def new_ref?
return super unless push_event_payload
push_event_payload.created?
end
def rm_ref?
return super unless push_event_payload
push_event_payload.removed?
end
def commit_from
return super unless push_event_payload
push_event_payload.commit_from
end
def commit_to
return super unless push_event_payload
push_event_payload.commit_to
end
def ref_name
return super unless push_event_payload
push_event_payload.ref
end
def ref_type
return super unless push_event_payload
push_event_payload.ref_type
end
def branch_name
return super unless push_event_payload
ref_name
end
def tag_name
return super unless push_event_payload
ref_name
end
def commit_title
return super unless push_event_payload
push_event_payload.commit_title
end
def commit_id
commit_to || commit_from
end
def commits_count
return super unless push_event_payload
push_event_payload.commit_count
end
def validate_push_action
return if action == PUSHED
errors.add(:action, "the action #{action.inspect} is not valid")
end
end
class PushEventPayload < ActiveRecord::Base
include ShaAttribute
belongs_to :event, inverse_of: :push_event_payload
validates :event_id, :commit_count, :action, :ref_type, presence: true
validates :commit_title, length: { maximum: 70 }
sha_attribute :commit_from
sha_attribute :commit_to
enum action: {
created: 0,
removed: 1,
pushed: 2
}
enum ref_type: {
branch: 0,
tag: 1
}
end
...@@ -8,5 +8,13 @@ class RedirectRoute < ActiveRecord::Base ...@@ -8,5 +8,13 @@ class RedirectRoute < ActiveRecord::Base
presence: true, presence: true,
uniqueness: { case_sensitive: false } uniqueness: { case_sensitive: false }
scope :matching_path_and_descendants, -> (path) { where('redirect_routes.path = ? OR redirect_routes.path LIKE ?', path, "#{sanitize_sql_like(path)}/%") } scope :matching_path_and_descendants, -> (path) do
wheres = if Gitlab::Database.postgresql?
'LOWER(redirect_routes.path) = LOWER(?) OR LOWER(redirect_routes.path) LIKE LOWER(?)'
else
'redirect_routes.path = ? OR redirect_routes.path LIKE ?'
end
where(wheres, path, "#{sanitize_sql_like(path)}/%")
end
end end
...@@ -825,7 +825,7 @@ class User < ActiveRecord::Base ...@@ -825,7 +825,7 @@ class User < ActiveRecord::Base
{ {
name: name, name: name,
username: username, username: username,
avatar_url: avatar_url avatar_url: avatar_url(only_path: false)
} }
end end
......
# TODO: Inherit from TreeEntity, when `Tree` implements `id` and `name` like `Gitlab::Git::Tree`. # TODO: Inherit from TreeEntity, when `Tree` implements `id` and `name` like `Gitlab::Git::Tree`.
class TreeRootEntity < Grape::Entity class TreeRootEntity < Grape::Entity
include RequestAwareEntity
expose :path expose :path
expose :trees, using: TreeEntity expose :trees, using: TreeEntity
expose :blobs, using: BlobEntity expose :blobs, using: BlobEntity
expose :submodules, using: SubmoduleEntity 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 end
...@@ -85,13 +85,13 @@ module Ci ...@@ -85,13 +85,13 @@ module Ci
end end
def register_failure def register_failure
failed_attempt_counter.increase failed_attempt_counter.increment
attempt_counter.increase attempt_counter.increment
end end
def register_success(job) def register_success(job)
job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at) job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at)
attempt_counter.increase attempt_counter.increment
end end
def failed_attempt_counter def failed_attempt_counter
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment