Commit d3e81673 authored by Stan Hu's avatar Stan Hu

Merge branch 'master' into sh-headless-chrome-support

parents 767dffcc 441ba5f3
...@@ -226,6 +226,7 @@ update-tests-metadata: ...@@ -226,6 +226,7 @@ update-tests-metadata:
flaky-examples-check: flaky-examples-check:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
image: ruby:2.3-alpine image: ruby:2.3-alpine
services: [] services: []
before_script: [] before_script: []
......
...@@ -2,6 +2,27 @@ ...@@ -2,6 +2,27 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 9.4.5 (2017-08-14)
- Fix deletion of deploy keys linked to other projects. !13162
- Allow any logged in users to read_users_list even if it's restricted. !13201
- Make Delete Merged Branches handle wildcard protected branches correctly. !13251
- Fix an order of operations for CI connection error message in merge request widget. !13252
- Fix pipeline_schedules pages when active schedule has an abnormal state. !13286
- Add missing validation error for username change with container registry tags. !13356
- Fix destroy of case-insensitive conflicting redirects. !13357
- Project pending delete no longer return 500 error in admins projects view. !13389
- Fix search box losing focus when typing.
- Use jQuery to control scroll behavior in job log for cross browser consistency.
- Use project_ref_path to create the link to a branch to fix links that 404.
- improve file upload/replace experience.
- fix jump to next discussion button.
- Fixes new issue button for failed job returning 404.
- Fix links to group milestones from issue and merge request sidebar.
- Fixed sign-in restrictions buttons not toggling active state.
- Fix Mattermost integration.
- Change project FK migration to skip existing FKs.
## 9.4.4 (2017-08-09) ## 9.4.4 (2017-08-09)
- Remove hidden symlinks from project import files. - Remove hidden symlinks from project import files.
......
...@@ -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'
...@@ -324,6 +324,7 @@ group :development, :test do ...@@ -324,6 +324,7 @@ group :development, :test do
gem 'spinach-rerun-reporter', '~> 0.0.2' gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5' gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3' gem 'rspec-set', '~> 0.1.3'
gem 'rspec-parameterized'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0' gem 'minitest', '~> 5.7.0'
......
...@@ -2,6 +2,7 @@ GEM ...@@ -2,6 +2,7 @@ GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
RedCloth (4.3.2) RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.2) ace-rails-ap (4.1.2)
actionmailer (4.2.8) actionmailer (4.2.8)
actionpack (= 4.2.8) actionpack (= 4.2.8)
...@@ -41,6 +42,9 @@ GEM ...@@ -41,6 +42,9 @@ GEM
tzinfo (~> 1.1) tzinfo (~> 1.1)
acts-as-taggable-on (4.0.0) acts-as-taggable-on (4.0.0)
activerecord (>= 4.0) activerecord (>= 4.0)
adamantium (0.2.0)
ice_nine (~> 0.11.0)
memoizable (~> 0.4.0)
addressable (2.3.8) addressable (2.3.8)
after_commit_queue (1.3.0) after_commit_queue (1.3.0)
activerecord (>= 3.0) activerecord (>= 3.0)
...@@ -125,6 +129,9 @@ GEM ...@@ -125,6 +129,9 @@ GEM
coercible (1.0.0) coercible (1.0.0)
descendants_tracker (~> 0.0.1) descendants_tracker (~> 0.0.1)
colorize (0.7.7) colorize (0.7.7)
concord (0.1.5)
adamantium (~> 0.2.0)
equalizer (~> 0.0.9)
concurrent-ruby (1.0.5) concurrent-ruby (1.0.5)
concurrent-ruby-ext (1.0.5) concurrent-ruby-ext (1.0.5)
concurrent-ruby (= 1.0.5) concurrent-ruby (= 1.0.5)
...@@ -420,9 +427,18 @@ GEM ...@@ -420,9 +427,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
...@@ -462,6 +478,8 @@ GEM ...@@ -462,6 +478,8 @@ GEM
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
mail_room (0.9.1) mail_room (0.9.1)
memoist (0.15.0) memoist (0.15.0)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2) method_source (0.8.2)
mime-types (2.99.3) mime-types (2.99.3)
mimemagic (0.3.0) mimemagic (0.3.0)
...@@ -598,6 +616,11 @@ GEM ...@@ -598,6 +616,11 @@ GEM
premailer-rails (1.9.7) premailer-rails (1.9.7)
actionmailer (>= 3, < 6) actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
proc_to_ast (0.1.0)
coderay
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.7.0.beta11) prometheus-client-mmap (0.7.0.beta11)
mmap2 (~> 2.2, >= 2.2.7) mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4) pry (0.10.4)
...@@ -706,6 +729,10 @@ GEM ...@@ -706,6 +729,10 @@ GEM
chunky_png chunky_png
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
rqrcode (>= 0.4.2) rqrcode (>= 0.4.2)
rspec (3.6.0)
rspec-core (~> 3.6.0)
rspec-expectations (~> 3.6.0)
rspec-mocks (~> 3.6.0)
rspec-core (3.6.0) rspec-core (3.6.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.6.0)
rspec-expectations (3.6.0) rspec-expectations (3.6.0)
...@@ -714,6 +741,12 @@ GEM ...@@ -714,6 +741,12 @@ GEM
rspec-mocks (3.6.0) rspec-mocks (3.6.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.6.0)
rspec-parameterized (0.4.0)
binding_of_caller
parser
proc_to_ast
rspec (>= 2.13, < 4)
unparser
rspec-rails (3.6.0) rspec-rails (3.6.0)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
...@@ -883,6 +916,14 @@ GEM ...@@ -883,6 +916,14 @@ GEM
get_process_mem (~> 0) get_process_mem (~> 0)
unicorn (>= 4, < 6) unicorn (>= 4, < 6)
uniform_notifier (1.10.0) uniform_notifier (1.10.0)
unparser (0.2.6)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
diff-lcs (~> 1.3)
equalizer (~> 0.0.9)
parser (>= 2.3.1.2, < 2.5)
procto (~> 0.0.2)
url_safe_base64 (0.2.2) url_safe_base64 (0.2.2)
validates_hostname (1.0.6) validates_hostname (1.0.6)
activerecord (>= 3.0) activerecord (>= 3.0)
...@@ -1008,7 +1049,7 @@ DEPENDENCIES ...@@ -1008,7 +1049,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)
...@@ -1081,6 +1122,7 @@ DEPENDENCIES ...@@ -1081,6 +1122,7 @@ DEPENDENCIES
responders (~> 2.0) responders (~> 2.0)
rouge (~> 2.0) rouge (~> 2.0)
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.6.0) rspec-rails (~> 3.6.0)
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3) rspec-set (~> 0.1.3)
......
...@@ -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({
......
...@@ -347,6 +347,9 @@ import initChangesDropdown from './init_changes_dropdown'; ...@@ -347,6 +347,9 @@ import initChangesDropdown from './init_changes_dropdown';
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();
......
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import bp from './breakpoints'; import bp from './breakpoints';
const HIDE_INTERVAL_TIMEOUT = 300;
const IS_OVER_CLASS = 'is-over';
const IS_ABOVE_CLASS = 'is-above';
const IS_SHOWING_FLY_OUT_CLASS = 'is-showing-fly-out';
let currentOpenMenu = null;
let menuCornerLocs;
let timeoutId;
export const mousePos = [];
export const setOpenMenu = (menu = null) => { currentOpenMenu = menu; };
export const slope = (a, b) => (b.y - a.y) / (b.x - a.x);
export const canShowActiveSubItems = (el) => { export const canShowActiveSubItems = (el) => {
const isHiddenByMedia = bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md'; const isHiddenByMedia = bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md';
...@@ -10,8 +24,28 @@ export const canShowActiveSubItems = (el) => { ...@@ -10,8 +24,28 @@ export const canShowActiveSubItems = (el) => {
return true; return true;
}; };
export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg'; export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg';
export const getHideSubItemsInterval = () => {
if (!currentOpenMenu) return 0;
const currentMousePos = mousePos[mousePos.length - 1];
const prevMousePos = mousePos[0];
const currentMousePosY = currentMousePos.y;
const [menuTop, menuBottom] = menuCornerLocs;
if (currentMousePosY < menuTop.y ||
currentMousePosY > menuBottom.y) return 0;
if (slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) &&
slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)) {
return HIDE_INTERVAL_TIMEOUT;
}
return 0;
};
export const calculateTop = (boundingRect, outerHeight) => { export const calculateTop = (boundingRect, outerHeight) => {
const windowHeight = window.innerHeight; const windowHeight = window.innerHeight;
const bottomOverflow = windowHeight - (boundingRect.top + outerHeight); const bottomOverflow = windowHeight - (boundingRect.top + outerHeight);
...@@ -20,45 +54,118 @@ export const calculateTop = (boundingRect, outerHeight) => { ...@@ -20,45 +54,118 @@ export const calculateTop = (boundingRect, outerHeight) => {
boundingRect.top; boundingRect.top;
}; };
export const showSubLevelItems = (el) => { export const hideMenu = (el) => {
const subItems = el.querySelector('.sidebar-sub-level-items'); if (!el) return;
if (!subItems || !canShowSubItems() || !canShowActiveSubItems(el)) return; const parentEl = el.parentNode;
subItems.style.display = 'block'; el.style.display = ''; // eslint-disable-line no-param-reassign
el.classList.add('is-showing-fly-out'); el.style.transform = ''; // eslint-disable-line no-param-reassign
el.classList.add('is-over'); el.classList.remove(IS_ABOVE_CLASS);
parentEl.classList.remove(IS_OVER_CLASS);
parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS);
setOpenMenu();
};
export const moveSubItemsToPosition = (el, subItems) => {
const boundingRect = el.getBoundingClientRect(); const boundingRect = el.getBoundingClientRect();
const top = calculateTop(boundingRect, subItems.offsetHeight); const top = calculateTop(boundingRect, subItems.offsetHeight);
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)}px, 0)`; // eslint-disable-line no-param-reassign
const subItemsRect = subItems.getBoundingClientRect();
menuCornerLocs = [
{
x: subItemsRect.left, // left position of the sub items
y: subItemsRect.top, // top position of the sub items
},
{
x: subItemsRect.left, // left position of the sub items
y: subItemsRect.top + subItemsRect.height, // bottom position of the sub items
},
];
if (isAbove) { if (isAbove) {
subItems.classList.add('is-above'); subItems.classList.add(IS_ABOVE_CLASS);
} }
}; };
export const hideSubLevelItems = (el) => { export const showSubLevelItems = (el) => {
const subItems = el.querySelector('.sidebar-sub-level-items');
if (!canShowSubItems() || !canShowActiveSubItems(el)) return;
el.classList.add(IS_OVER_CLASS);
if (!subItems) return;
subItems.style.display = 'block';
el.classList.add(IS_SHOWING_FLY_OUT_CLASS);
setOpenMenu(subItems);
moveSubItemsToPosition(el, subItems);
};
export const mouseEnterTopItems = (el) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
if (currentOpenMenu) hideMenu(currentOpenMenu);
showSubLevelItems(el);
}, getHideSubItemsInterval());
};
export const mouseLeaveTopItem = (el) => {
const subItems = el.querySelector('.sidebar-sub-level-items'); const subItems = el.querySelector('.sidebar-sub-level-items');
if (!subItems || !canShowSubItems() || !canShowActiveSubItems(el)) return; if (!canShowSubItems() || !canShowActiveSubItems(el) ||
(subItems && subItems === currentOpenMenu)) return;
el.classList.remove(IS_OVER_CLASS);
};
export const documentMouseMove = (e) => {
mousePos.push({
x: e.clientX,
y: e.clientY,
});
el.classList.remove('is-showing-fly-out'); if (mousePos.length > 6) mousePos.shift();
el.classList.remove('is-over');
subItems.style.display = '';
subItems.style.transform = '';
subItems.classList.remove('is-above');
}; };
export default () => { export default () => {
const items = [...document.querySelectorAll('.sidebar-top-level-items > li')] const sidebar = document.querySelector('.sidebar-top-level-items');
.filter(el => el.querySelector('.sidebar-sub-level-items'));
if (!sidebar) return;
const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')];
sidebar.addEventListener('mouseleave', () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
if (currentOpenMenu) hideMenu(currentOpenMenu);
}, getHideSubItemsInterval());
});
items.forEach((el) => { items.forEach((el) => {
el.addEventListener('mouseenter', e => showSubLevelItems(e.currentTarget)); const subItems = el.querySelector('.sidebar-sub-level-items');
el.addEventListener('mouseleave', e => hideSubLevelItems(e.currentTarget));
if (subItems) {
subItems.addEventListener('mouseleave', () => {
clearTimeout(timeoutId);
hideMenu(currentOpenMenu);
});
}
el.addEventListener('mouseenter', e => mouseEnterTopItems(e.currentTarget));
el.addEventListener('mouseleave', e => mouseLeaveTopItem(e.currentTarget));
}); });
document.addEventListener('mousemove', documentMouseMove);
}; };
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 Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../../vue_shared/translate';
import illustrationSvg from '../icons/intro_illustration.svg';
Vue.use(Translate);
const cookieKey = 'pipeline_schedules_callout_dismissed';
export default {
name: 'PipelineSchedulesCallout',
data() {
return {
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
illustrationSvg,
calloutDismissed: Cookies.get(cookieKey) === 'true',
};
},
methods: {
dismissCallout() {
this.calloutDismissed = true;
Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
},
},
template: `
<div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout">
<div class="bordered-box landing content-block">
<button
id="dismiss-callout-btn"
class="btn btn-default close"
@click="dismissCallout">
<i class="fa fa-times"></i>
</button>
<div class="svg-container" v-html="illustrationSvg"></div>
<div class="user-callout-copy">
<h4>{{ __('Scheduling Pipelines') }}</h4>
<p>
{{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }}
</p>
<p> {{ __('Learn more in the') }}
<a
:href="docsUrl"
target="_blank"
rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period -->
</p>
</div>
</div>
</div>
`,
};
<script>
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../../vue_shared/translate';
import illustrationSvg from '../icons/intro_illustration.svg';
Vue.use(Translate);
const cookieKey = 'pipeline_schedules_callout_dismissed';
export default {
name: 'PipelineSchedulesCallout',
data() {
return {
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
calloutDismissed: Cookies.get(cookieKey) === 'true',
};
},
methods: {
dismissCallout() {
this.calloutDismissed = true;
Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
},
},
created() {
this.illustrationSvg = illustrationSvg;
},
};
</script>
<template>
<div
v-if="!calloutDismissed"
class="pipeline-schedules-user-callout user-callout">
<div class="bordered-box landing content-block">
<button
id="dismiss-callout-btn"
class="btn btn-default close"
@click="dismissCallout">
<i
aria-hidden="true"
class="fa fa-times">
</i>
</button>
<div class="svg-container" v-html="illustrationSvg"></div>
<div class="user-callout-copy">
<h4>{{ __('Scheduling Pipelines') }}</h4>
<p>
{{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }}
</p>
<p> {{ __('Learn more in the') }}
<a
:href="docsUrl"
target="_blank"
rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period -->
</p>
</div>
</div>
</div>
</template>
import Vue from 'vue'; import Vue from 'vue';
import PipelineSchedulesCallout from './components/pipeline_schedules_callout'; import PipelineSchedulesCallout from './components/pipeline_schedules_callout.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipeline-schedules-callout', el: '#pipeline-schedules-callout',
......
...@@ -48,6 +48,27 @@ ...@@ -48,6 +48,27 @@
return `${this.job.name} - ${this.job.status.label}`; return `${this.job.name} - ${this.job.status.label}`;
}, },
}, },
methods: {
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
.on('click', (e) => {
e.stopPropagation();
});
},
},
mounted() {
this.stopDropdownClickPropagation();
},
}; };
</script> </script>
<template> <template>
......
...@@ -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()}`);
} }
} }
} }
......
...@@ -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"
class="panel-right"
:class="{'edit-mode': editMode}">
<repo-tabs/> <repo-tabs/>
<component :is="currentBlobView" class="blob-viewer-container"></component> <component
:is="currentBlobView"
class="blob-viewer-container"/>
<repo-file-buttons/> <repo-file-buttons/>
</div> </div>
<repo-commit-section/> <repo-commit-section/>
<popup-dialog <popup-dialog
v-show="dialog.open"
:primary-button-label="__('Discard changes')" :primary-button-label="__('Discard changes')"
:open="dialog.open"
kind="warning" kind="warning"
:title="__('Are you sure?')" :title="__('Are you sure?')"
:body="__('Are you sure you want to discard your changes?')" :body="__('Are you sure you want to discard your changes?')"
@toggle="dialogToggled" @toggle="toggleDialogOpen"
@submit="dialogSubmitted" @submit="dialogSubmitted"
/> />
</div> </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); })
.catch(Helper.loadingError);
},
methods: {
setupEditor() {
this.showHide(); this.showHide();
const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
this.monacoInstance.setModel(newModel); Helper.setMonacoModelFromLanguage();
}).catch(Helper.loadingError);
}, },
methods: {
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;
}
},
},
watch: { Helper.monacoInstance.setPosition({
activeLine() {
this.monacoInstance.setPosition({
lineNumber: this.activeLine, lineNumber: this.activeLine,
column: 1, column: 1,
}); });
}
}, },
activeFileLabel() {
this.showHide();
}, },
watch: {
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() { blobRaw() {
this.showHide(); if (Helper.monacoInstance && !this.isTree) {
}, this.setupEditor();
}
openedFiles() {
this.showHide();
}, },
binary() {
this.showHide();
}, },
computed: {
blobRaw() { shouldHideEditor() {
this.showHide(); return !this.openedFiles.length || (this.binary && !this.activeFile.raw);
if (this.isTree) return;
this.monacoInstance.setModel(null);
const languages = this.monaco.languages.getLanguages();
const languageID = Helper.getLanguageIDForFile(this.activeFile, languages);
const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
this.monacoInstance.setModel(newModel);
}, },
}, },
}; };
...@@ -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">
<td class="hidden-sm hidden-xs">
<div class="commit-message"> <div class="commit-message">
<a :href="file.lastCommitUrl">{{file.lastCommitMessage}}</a> <a @click.stop :href="file.lastCommitUrl">
{{file.lastCommitMessage}}
</a>
</div> </div>
</td> </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
class="commit-update"
:title="tooltipTitle(file.lastCommitUpdate)">
{{timeFormated(file.lastCommitUpdate)}}
</span>
</td> </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">
<a
:href="activeFile.blame_path"
class="btn btn-default blame">
Blame
</a>
<a
:href="activeFile.commits_path"
class="btn btn-default history">
History
</a>
<a
:href="activeFile.permalink"
class="btn btn-default permalink">
Permalink
</a>
</div> </div>
<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
v-if="showGhostLines"
class="loading-file">
<td> <td>
<div class="animation-container animation-container-small"> <div
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> class="animation-container animation-container-small">
<div
v-for="n in 6"
:key="n"
:class="lineOfCode(n)">
</div>
</div> </div>
</td> </td>
<td v-if="!isMini" class="hidden-sm hidden-xs"> <td
v-if="!isMini"
class="hidden-sm hidden-xs">
<div class="animation-container"> <div class="animation-container">
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> <div
v-for="n in 6"
:key="n"
:class="lineOfCode(n)">
</div>
</div> </div>
</td> </td>
<td v-if="!isMini" class="hidden-xs"> <td
v-if="!isMini"
class="hidden-xs">
<div class="animation-container animation-container-small"> <div class="animation-container animation-container-small">
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> <div
v-for="n in 6"
:key="n"
:class="lineOfCode(n)">
</div>
</div> </div>
</td> </td>
</tr> </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) => {
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'); 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 This issue is 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>
......
<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>
......
...@@ -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);
}
}
...@@ -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;
......
...@@ -403,6 +403,7 @@ header.navbar-gitlab-new { ...@@ -403,6 +403,7 @@ header.navbar-gitlab-new {
} }
.breadcrumbs-extra { .breadcrumbs-extra {
display: flex;
flex: 0 0 auto; flex: 0 0 auto;
margin-left: auto; margin-left: auto;
} }
......
...@@ -250,32 +250,13 @@ $new-sidebar-collapsed-width: 50px; ...@@ -250,32 +250,13 @@ $new-sidebar-collapsed-width: 50px;
position: absolute; position: absolute;
top: -30px; top: -30px;
bottom: -30px; bottom: -30px;
left: 0; left: -10px;
right: -30px; right: -30px;
z-index: -1; z-index: -1;
} }
&::after {
content: "";
position: absolute;
top: 44px;
left: -30px;
right: 35px;
bottom: 0;
height: 100%;
max-height: 150px;
z-index: -1;
transform: skew(33deg);
}
&.is-above { &.is-above {
margin-top: 1px; margin-top: 1px;
&::after {
top: auto;
bottom: 44px;
transform: skew(-30deg);
}
} }
> .active { > .active {
...@@ -322,8 +303,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -322,8 +303,7 @@ $new-sidebar-collapsed-width: 50px;
} }
} }
&:not(.active):hover > a, &.active > a:hover,
> a:hover,
&.is-over > a { &.is-over > a {
background-color: $white-light; background-color: $white-light;
} }
......
...@@ -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;
......
...@@ -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;
} }
......
...@@ -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 {
margin-left: 0;
} }
}
.note-actions-item {
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 {
......
.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;
......
...@@ -6,6 +6,13 @@ module CycleAnalyticsParams ...@@ -6,6 +6,13 @@ module CycleAnalyticsParams
end end
def start_date(params) def start_date(params)
params[:start_date] == '30' ? 30.days.ago : 90.days.ago case params[:start_date]
when '7'
7.days.ago
when '30'
30.days.ago
else
90.days.ago
end
end end
end end
...@@ -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
...@@ -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
......
...@@ -212,7 +212,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -212,7 +212,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def create_merge_request def create_merge_request
result = MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
if result[:status] == :success if result[:status] == :success
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request]) render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
......
...@@ -59,7 +59,7 @@ module GroupsHelper ...@@ -59,7 +59,7 @@ module GroupsHelper
end end
def remove_group_message(group) def remove_group_message(group)
_("You are going to remove %{group_name}.\nRemoved groups CANNOT be restored!\nAre you ABSOLUTELY sure?") % _("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
{ group_name: group.name } { group_name: group.name }
end end
......
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
...@@ -80,7 +80,7 @@ module ProjectsHelper ...@@ -80,7 +80,7 @@ module ProjectsHelper
end end
def remove_project_message(project) def remove_project_message(project)
_("You are going to remove %{project_name_with_namespace}.\nRemoved project CANNOT be restored!\nAre you ABSOLUTELY sure?") % _("You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
{ project_name_with_namespace: project.name_with_namespace } { project_name_with_namespace: project.name_with_namespace }
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
......
...@@ -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,
......
...@@ -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) .where(requested_at: nil)
.reorder(nil)
end
# Like active, but without invites. For when a User is required.
scope :active_without_invites, -> do
left_join_users
.where(users: { state: 'active' })
.where(requested_at: nil)
.reorder(nil)
end end
scope :invite, -> { where.not(invite_token: nil) } scope :invite, -> { where.not(invite_token: nil) }
...@@ -276,6 +287,13 @@ class Member < ActiveRecord::Base ...@@ -276,6 +287,13 @@ class Member < ActiveRecord::Base
@notification_setting ||= user.notification_settings_for(source) @notification_setting ||= user.notification_settings_for(source)
end end
def notifiable?(type, opts = {})
# always notify when there isn't a user yet
return true if user.blank?
NotificationRecipientService.notifiable?(user, type, notifiable_options.merge(opts))
end
private private
def send_invite def send_invite
...@@ -332,4 +350,8 @@ class Member < ActiveRecord::Base ...@@ -332,4 +350,8 @@ class Member < ActiveRecord::Base
def notification_service def notification_service
NotificationService.new NotificationService.new
end end
def notifiable_options
{}
end
end end
...@@ -30,6 +30,10 @@ class GroupMember < Member ...@@ -30,6 +30,10 @@ class GroupMember < Member
'Group' 'Group'
end end
def notifiable_options
{ group: group }
end
private private
def send_invite def send_invite
......
...@@ -87,6 +87,10 @@ class ProjectMember < Member ...@@ -87,6 +87,10 @@ class ProjectMember < Member
project.owner == user project.owner == user
end end
def notifiable_options
{ project: project }
end
private private
def delete_member_todos def delete_member_todos
......
...@@ -443,7 +443,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -443,7 +443,8 @@ class MergeRequest < ActiveRecord::Base
end end
def reload_diff_if_branch_changed def reload_diff_if_branch_changed
if source_branch_changed? || target_branch_changed? if (source_branch_changed? || target_branch_changed?) &&
(source_branch_head && target_branch_head)
reload_diff reload_diff
end end
end end
...@@ -792,11 +793,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -792,11 +793,7 @@ class MergeRequest < ActiveRecord::Base
end end
def fetch_ref def fetch_ref
target_project.repository.fetch_ref( write_ref
source_project.repository.path_to_repo,
"refs/heads/#{source_branch}",
ref_path
)
update_column(:ref_fetched, true) update_column(:ref_fetched, true)
end end
...@@ -939,4 +936,17 @@ class MergeRequest < ActiveRecord::Base ...@@ -939,4 +936,17 @@ class MergeRequest < ActiveRecord::Base
true true
end end
private
def write_ref
target_project.repository.with_repo_branch_commit(
source_project.repository, source_branch) do |commit|
if commit
target_project.repository.write_ref(ref_path, commit.sha)
else
raise Rugged::ReferenceError, 'source repository is empty'
end
end
end
end end
...@@ -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
......
...@@ -5,14 +5,22 @@ class NotificationRecipient ...@@ -5,14 +5,22 @@ class NotificationRecipient
custom_action: nil, custom_action: nil,
target: nil, target: nil,
acting_user: nil, acting_user: nil,
project: nil project: nil,
group: nil,
skip_read_ability: false
) )
unless NotificationSetting.levels.key?(type) || type == :subscription
raise ArgumentError, "invalid type: #{type.inspect}"
end
@custom_action = custom_action @custom_action = custom_action
@acting_user = acting_user @acting_user = acting_user
@target = target @target = target
@project = project || @target&.project @project = project || default_project
@group = group || @project&.group
@user = user @user = user
@type = type @type = type
@skip_read_ability = skip_read_ability
end end
def notification_setting def notification_setting
...@@ -77,6 +85,8 @@ class NotificationRecipient ...@@ -77,6 +85,8 @@ class NotificationRecipient
def has_access? def has_access?
DeclarativePolicy.subject_scope do DeclarativePolicy.subject_scope do
return false unless user.can?(:receive_notifications) return false unless user.can?(:receive_notifications)
return true if @skip_read_ability
return false if @project && !user.can?(:read_project, @project) return false if @project && !user.can?(:read_project, @project)
return true unless read_ability return true unless read_ability
...@@ -96,6 +106,7 @@ class NotificationRecipient ...@@ -96,6 +106,7 @@ class NotificationRecipient
private private
def read_ability def read_ability
return nil if @skip_read_ability
return @read_ability if instance_variable_defined?(:@read_ability) return @read_ability if instance_variable_defined?(:@read_ability)
@read_ability = @read_ability =
...@@ -111,12 +122,18 @@ class NotificationRecipient ...@@ -111,12 +122,18 @@ class NotificationRecipient
end end
end end
def default_project
return nil if @target.nil?
return @target if @target.is_a?(Project)
return @target.project if @target.respond_to?(:project)
end
def find_notification_setting def find_notification_setting
project_setting = @project && user.notification_settings_for(@project) project_setting = @project && user.notification_settings_for(@project)
return project_setting unless project_setting.nil? || project_setting.global? return project_setting unless project_setting.nil? || project_setting.global?
group_setting = @project&.group && user.notification_settings_for(@project.group) group_setting = @group && user.notification_settings_for(@group)
return group_setting unless group_setting.nil? || group_setting.global? return group_setting unless group_setting.nil? || group_setting.global?
......
...@@ -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
...@@ -1048,9 +1047,7 @@ class Project < ActiveRecord::Base ...@@ -1048,9 +1047,7 @@ class Project < ActiveRecord::Base
def change_head(branch) def change_head(branch)
if repository.branch_exists?(branch) if repository.branch_exists?(branch)
repository.before_change_head repository.before_change_head
repository.rugged.references.create('HEAD', repository.write_ref('HEAD', "refs/heads/#{branch}")
"refs/heads/#{branch}",
force: true)
repository.copy_gitattributes(branch) repository.copy_gitattributes(branch)
repository.after_change_head repository.after_change_head
reload_default_branch reload_default_branch
...@@ -1398,6 +1395,10 @@ class Project < ActiveRecord::Base ...@@ -1398,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)
......
...@@ -224,7 +224,7 @@ class Repository ...@@ -224,7 +224,7 @@ class Repository
# This will still fail if the file is corrupted (e.g. 0 bytes) # This will still fail if the file is corrupted (e.g. 0 bytes)
begin begin
rugged.references.create(keep_around_ref_name(sha), sha, force: true) write_ref(keep_around_ref_name(sha), sha)
rescue Rugged::ReferenceError => ex rescue Rugged::ReferenceError => ex
Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}" Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
rescue Rugged::OSError => ex rescue Rugged::OSError => ex
...@@ -237,6 +237,10 @@ class Repository ...@@ -237,6 +237,10 @@ class Repository
ref_exists?(keep_around_ref_name(sha)) ref_exists?(keep_around_ref_name(sha))
end end
def write_ref(ref_path, sha)
rugged.references.create(ref_path, sha, force: true)
end
def diverging_commit_counts(branch) def diverging_commit_counts(branch)
root_ref_hash = raw_repository.rev_parse_target(root_ref).oid root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
cache.fetch(:"diverging_commit_counts_#{branch.name}") do cache.fetch(:"diverging_commit_counts_#{branch.name}") do
...@@ -985,12 +989,10 @@ class Repository ...@@ -985,12 +989,10 @@ class Repository
if start_repository == self if start_repository == self
start_branch_name start_branch_name
else else
tmp_ref = "refs/tmp/#{SecureRandom.hex}/head" tmp_ref = fetch_ref(
fetch_ref(
start_repository.path_to_repo, start_repository.path_to_repo,
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
tmp_ref "refs/tmp/#{SecureRandom.hex}/head"
) )
start_repository.commit(start_branch_name).sha start_repository.commit(start_branch_name).sha
...@@ -1021,7 +1023,12 @@ class Repository ...@@ -1021,7 +1023,12 @@ class Repository
def fetch_ref(source_path, source_ref, target_ref) def fetch_ref(source_path, source_ref, target_ref)
args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
run_git(args) message, status = run_git(args)
# Make sure ref was created, and raise Rugged::ReferenceError when not
raise Rugged::ReferenceError, message if status != 0
target_ref
end end
def create_ref(ref, ref_path) def create_ref(ref, ref_path)
......
...@@ -726,9 +726,9 @@ class User < ActiveRecord::Base ...@@ -726,9 +726,9 @@ class User < ActiveRecord::Base
end end
def sanitize_attrs def sanitize_attrs
%w[username skype linkedin twitter].each do |attr| %i[skype linkedin twitter].each do |attr|
value = public_send(attr) # rubocop:disable GitlabSecurity/PublicSend value = self[attr]
public_send("#{attr}=", Sanitize.clean(value)) if value.present? # rubocop:disable GitlabSecurity/PublicSend self[attr] = Sanitize.clean(value) if value.present?
end end
end end
...@@ -1069,6 +1069,7 @@ class User < ActiveRecord::Base ...@@ -1069,6 +1069,7 @@ class User < ActiveRecord::Base
# Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
def send_devise_notification(notification, *args) def send_devise_notification(notification, *args)
return true unless can?(:receive_notifications)
devise_mailer.send(notification, self, *args).deliver_later devise_mailer.send(notification, self, *args).deliver_later
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
...@@ -10,9 +10,11 @@ class NotificationService ...@@ -10,9 +10,11 @@ class NotificationService
# only if ssh key is not deploy key # only if ssh key is not deploy key
# #
# This is security email so it will be sent # This is security email so it will be sent
# even if user disabled notifications # even if user disabled notifications. However,
# it won't be sent to internal users like the
# ghost user or the EE support bot.
def new_key(key) def new_key(key)
if key.user if key.user&.can?(:receive_notifications)
mailer.new_ssh_key_email(key.id).deliver_later mailer.new_ssh_key_email(key.id).deliver_later
end end
end end
...@@ -22,14 +24,14 @@ class NotificationService ...@@ -22,14 +24,14 @@ class NotificationService
# This is a security email so it will be sent even if the user user disabled # This is a security email so it will be sent even if the user user disabled
# notifications # notifications
def new_gpg_key(gpg_key) def new_gpg_key(gpg_key)
if gpg_key.user if gpg_key.user&.can?(:receive_notifications)
mailer.new_gpg_key_email(gpg_key.id).deliver_later mailer.new_gpg_key_email(gpg_key.id).deliver_later
end end
end end
# Always notify user about email added to profile # Always notify user about email added to profile
def new_email(email) def new_email(email)
if email.user if email.user&.can?(:receive_notifications)
mailer.new_email_email(email.id).deliver_later mailer.new_email_email(email.id).deliver_later
end end
end end
...@@ -185,6 +187,8 @@ class NotificationService ...@@ -185,6 +187,8 @@ class NotificationService
# Notify new user with email after creation # Notify new user with email after creation
def new_user(user, token = nil) def new_user(user, token = nil)
return true unless notifiable?(user, :mention)
# Don't email omniauth created users # Don't email omniauth created users
mailer.new_user_email(user.id, token).deliver_later unless user.identities.any? mailer.new_user_email(user.id, token).deliver_later unless user.identities.any?
end end
...@@ -206,19 +210,27 @@ class NotificationService ...@@ -206,19 +210,27 @@ class NotificationService
# Members # Members
def new_access_request(member) def new_access_request(member)
return true unless member.notifiable?(:subscription)
mailer.member_access_requested_email(member.real_source_type, member.id).deliver_later mailer.member_access_requested_email(member.real_source_type, member.id).deliver_later
end end
def decline_access_request(member) def decline_access_request(member)
return true unless member.notifiable?(:subscription)
mailer.member_access_denied_email(member.real_source_type, member.source_id, member.user_id).deliver_later mailer.member_access_denied_email(member.real_source_type, member.source_id, member.user_id).deliver_later
end end
# Project invite # Project invite
def invite_project_member(project_member, token) def invite_project_member(project_member, token)
return true unless project_member.notifiable?(:subscription)
mailer.member_invited_email(project_member.real_source_type, project_member.id, token).deliver_later mailer.member_invited_email(project_member.real_source_type, project_member.id, token).deliver_later
end end
def accept_project_invite(project_member) def accept_project_invite(project_member)
return true unless project_member.notifiable?(:subscription)
mailer.member_invite_accepted_email(project_member.real_source_type, project_member.id).deliver_later mailer.member_invite_accepted_email(project_member.real_source_type, project_member.id).deliver_later
end end
...@@ -232,10 +244,14 @@ class NotificationService ...@@ -232,10 +244,14 @@ class NotificationService
end end
def new_project_member(project_member) def new_project_member(project_member)
return true unless project_member.notifiable?(:mention, skip_read_ability: true)
mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
end end
def update_project_member(project_member) def update_project_member(project_member)
return true unless project_member.notifiable?(:mention)
mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
end end
...@@ -249,6 +265,9 @@ class NotificationService ...@@ -249,6 +265,9 @@ class NotificationService
end end
def decline_group_invite(group_member) def decline_group_invite(group_member)
# always send this one, since it's a response to the user's own
# action
mailer.member_invite_declined_email( mailer.member_invite_declined_email(
group_member.real_source_type, group_member.real_source_type,
group_member.group.id, group_member.group.id,
...@@ -258,15 +277,19 @@ class NotificationService ...@@ -258,15 +277,19 @@ class NotificationService
end end
def new_group_member(group_member) def new_group_member(group_member)
return true unless group_member.notifiable?(:mention)
mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
end end
def update_group_member(group_member) def update_group_member(group_member)
return true unless group_member.notifiable?(:mention)
mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
end end
def project_was_moved(project, old_path_with_namespace) def project_was_moved(project, old_path_with_namespace)
recipients = NotificationRecipientService.notifiable_users(project.team.members, :mention, project: project) recipients = notifiable_users(project.team.members, :mention, project: project)
recipients.each do |recipient| recipients.each do |recipient|
mailer.project_was_moved_email( mailer.project_was_moved_email(
...@@ -288,10 +311,14 @@ class NotificationService ...@@ -288,10 +311,14 @@ class NotificationService
end end
def project_exported(project, current_user) def project_exported(project, current_user)
return true unless notifiable?(current_user, :mention, project: project)
mailer.project_was_exported_email(current_user, project).deliver_later mailer.project_was_exported_email(current_user, project).deliver_later
end end
def project_not_exported(project, current_user, errors) def project_not_exported(project, current_user, errors)
return true unless notifiable?(current_user, :mention, project: project)
mailer.project_was_not_exported_email(current_user, project, errors).deliver_later mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
end end
...@@ -300,7 +327,7 @@ class NotificationService ...@@ -300,7 +327,7 @@ class NotificationService
return unless mailer.respond_to?(email_template) return unless mailer.respond_to?(email_template)
recipients ||= NotificationRecipientService.notifiable_users( recipients ||= notifiable_users(
[pipeline.user], :watch, [pipeline.user], :watch,
custom_action: :"#{pipeline.status}_pipeline", custom_action: :"#{pipeline.status}_pipeline",
target: pipeline target: pipeline
...@@ -369,7 +396,7 @@ class NotificationService ...@@ -369,7 +396,7 @@ class NotificationService
def relabeled_resource_email(target, labels, current_user, method) def relabeled_resource_email(target, labels, current_user, method)
recipients = labels.flat_map { |l| l.subscribers(target.project) } recipients = labels.flat_map { |l| l.subscribers(target.project) }
recipients = NotificationRecipientService.notifiable_users( recipients = notifiable_users(
recipients, :subscription, recipients, :subscription,
target: target, target: target,
acting_user: current_user acting_user: current_user
...@@ -401,4 +428,14 @@ class NotificationService ...@@ -401,4 +428,14 @@ class NotificationService
object.previous_changes[attribute].first object.previous_changes[attribute].first
end end
end end
private
def notifiable?(*args)
NotificationRecipientService.notifiable?(*args)
end
def notifiable_users(*args)
NotificationRecipientService.notifiable_users(*args)
end
end end
...@@ -128,6 +128,8 @@ module Projects ...@@ -128,6 +128,8 @@ module Projects
project.repository.before_delete project.repository.before_delete
Repository.new(wiki_path, project, disk_path: repo_path).before_delete Repository.new(wiki_path, project, disk_path: repo_path).before_delete
Projects::ForksCountService.new(project).delete_cache
end end
end end
end end
...@@ -21,11 +21,17 @@ module Projects ...@@ -21,11 +21,17 @@ module Projects
builds_access_level = @project.project_feature.builds_access_level builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level) new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
refresh_forks_count
new_project new_project
end end
private private
def refresh_forks_count
Projects::ForksCountService.new(@project).refresh_cache
end
def allowed_visibility_level def allowed_visibility_level
project_level = @project.visibility_level project_level = @project.visibility_level
......
module Projects
# Service class for getting and caching the number of forks of a project.
class ForksCountService
def initialize(project)
@project = project
end
def count
Rails.cache.fetch(cache_key) { uncached_count }
end
def refresh_cache
Rails.cache.write(cache_key, uncached_count)
end
def delete_cache
Rails.cache.delete(cache_key)
end
private
def uncached_count
@project.forks.count
end
def cache_key
['projects', @project.id, 'forks_count']
end
end
end
...@@ -13,7 +13,13 @@ module Projects ...@@ -13,7 +13,13 @@ module Projects
::MergeRequests::CloseService.new(@project, @current_user).execute(mr) ::MergeRequests::CloseService.new(@project, @current_user).execute(mr)
end end
refresh_forks_count(@project.forked_from_project)
@project.forked_project_link.destroy @project.forked_project_link.destroy
end end
def refresh_forks_count(project)
Projects::ForksCountService.new(project).refresh_cache
end
end end
end end
...@@ -4,7 +4,7 @@ class PersonalFileUploader < FileUploader ...@@ -4,7 +4,7 @@ class PersonalFileUploader < FileUploader
end end
def self.base_dir def self.base_dir
File.join(root_dir, 'system') File.join(root_dir, '-', 'system')
end end
private private
......
.gl-pagination
%ul.pagination.clearfix
- if previous_path
%li.prev
= link_to(t('views.pagination.previous'), previous_path, rel: 'prev')
- if next_path
%li.next
= link_to(t('views.pagination.next'), next_path, rel: 'next')
- if commit.has_signature? - if commit.has_signature?
%button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } } %button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
%i.fa.fa-spinner.fa-spin
...@@ -39,6 +39,9 @@ ...@@ -39,6 +39,9 @@
%span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }} %span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }}
%i.fa.fa-chevron-down %i.fa.fa-chevron-down
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
%li
%a{ "href" => "#", "data-value" => "7" }
{{ n__('Last %d day', 'Last %d days', 7) }}
%li %li
%a{ "href" => "#", "data-value" => "30" } %a{ "href" => "#", "data-value" => "30" }
{{ n__('Last %d day', 'Last %d days', 30) }} {{ n__('Last %d day', 'Last %d days', 30) }}
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed{ class: ("diff-files-changed-merge-request" if merge_request) } .content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed{ class: ("diff-files-changed-merge-request" if merge_request) }
.files-changed-inner .files-changed-inner
.inline-parallel-buttons .inline-parallel-buttons.hidden-xs.hidden-sm
- if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? } - if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? }
= link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default' = link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
- if show_whitespace_toggle - if show_whitespace_toggle
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
%strong.cgreen #{sum_added_lines} additions %strong.cgreen #{sum_added_lines} additions
and and
%strong.cred #{sum_removed_lines} deletions %strong.cred #{sum_removed_lines} deletions
.diff-stats-additions-deletions-collapsed.pull-right{ "aria-hidden": "true", "aria-describedby": "diff-stats" } .diff-stats-additions-deletions-collapsed.pull-right.hidden-xs.hidden-sm{ "aria-hidden": "true", "aria-describedby": "diff-stats" }
%strong.cgreen< %strong.cgreen<
+#{sum_added_lines} +#{sum_added_lines}
%strong.cred< %strong.cred<
......
= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do = link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss') = icon('rss')
- if @can_bulk_update - if @can_bulk_update
= button_tag "Edit Issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
= link_to "New issue", new_project_issue_path(@project, = link_to "New issue", new_project_issue_path(@project,
issue: { assignee_id: issues_finder.assignee.try(:id), issue: { assignee_id: issues_finder.assignee.try(:id),
milestone_id: issues_finder.milestones.first.try(:id) }), milestone_id: issues_finder.milestones.first.try(:id) }),
......
- if @can_bulk_update - if @can_bulk_update
= button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle" = button_tag "Edit merge requests", class: "btn append-right-10 js-bulk-update-toggle"
- if merge_project - if merge_project
= link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do = link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do
New merge request New merge request
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
"inline-template" => true, "inline-template" => true,
"ref" => "note_#{note.id}" } "ref" => "note_#{note.id}" }
.note-actions-item
%button.note-action-button.line-resolve-btn{ type: "button", %button.note-action-button.line-resolve-btn{ type: "button",
class: ("is-disabled" unless can_resolve), class: ("is-disabled" unless can_resolve),
":class" => "{ 'is-active': isResolved }", ":class" => "{ 'is-active': isResolved }",
...@@ -31,10 +32,17 @@ ...@@ -31,10 +32,17 @@
- if current_user - if current_user
- if note.emoji_awardable? - if note.emoji_awardable?
- user_authored = note.user_authored?(current_user) - user_authored = note.user_authored?(current_user)
= link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do .note-actions-item
= button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip btn btn-transparent", data: { position: 'right', container: 'body' } do
= icon('spinner spin') = icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
- if note_editable
.note-actions-item
= button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do
%span.link-highlight
= custom_icon('icon_pencil')
= render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
- is_current_user = current_user == note.author - is_current_user = current_user == note.author
- if note_editable || !is_current_user - if note_editable || !is_current_user
.dropdown.more-actions .dropdown.more-actions.note-actions-item
= button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do
= icon('ellipsis-v', class: 'icon') %span.icon
= custom_icon('ellipsis_v')
%ul.dropdown-menu.more-actions-dropdown.dropdown-open-left %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
- if note_editable
%li
= button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent'
%li.divider
- unless is_current_user - unless is_current_user
%li %li
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
.tree-ref-holder .tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path = render 'shared/ref_switcher', destination: 'tree', path: @path
- if show_new_repo? - if show_new_repo?
.tree-ref-target-holder.js-tree-ref-target-holder
= icon('long-arrow-right', title: 'to target branch') = icon('long-arrow-right', title: 'to target branch')
= render 'shared/target_switcher', destination: 'tree', path: @path = render 'shared/target_switcher', destination: 'tree', path: @path
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
- @options && @options.each do |key, value| - @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil = hidden_field_tag key, value, id: nil
.dropdown .dropdown
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" } = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" }
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
= dropdown_title _("Switch branch/tag") = dropdown_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags") = dropdown_filter _("Search branches and tags")
......
- dropdown_toggle_text = @ref || @project.default_branch - dropdown_toggle_text = @ref || @project.default_branch
= form_tag nil, method: :get, class: "project-refs-target-form" do = form_tag nil, method: :get, style: { display: 'none' }, class: "project-refs-target-form" do
= hidden_field_tag :destination, destination = hidden_field_tag :destination, destination
- if defined?(path) - if defined?(path)
= hidden_field_tag :path, path = hidden_field_tag :path, path
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 1600"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></svg>
...@@ -23,6 +23,6 @@ ...@@ -23,6 +23,6 @@
= icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon') = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
%strong= pluralize(@private_forks_count, 'private fork') %strong= pluralize(@private_forks_count, 'private fork')
%span &nbsp;you have no access to. %span &nbsp;you have no access to.
= paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages = paginate_collection(projects, remote: remote)
- else - else
.nothing-here-block No projects found .nothing-here-block No projects found
#repo{ data: { url: content_url, project_name: project.name, refs_url: refs_project_path(project, format: :json), project_url: project_path(project), project_id: project.id, can_commit: (!!can_push_branch?(project, @ref)).to_s } } #repo{ data: { url: content_url,
%repo project_name: project.name,
refs_url: refs_project_path(project, format: :json),
project_url: project_path(project),
project_id: project.id,
can_commit: (!!can_push_branch?(project, @ref)).to_s,
on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } }
- if current_user - if current_user
- if note.emoji_awardable? - if note.emoji_awardable?
- user_authored = note.user_authored?(current_user) - user_authored = note.user_authored?(current_user)
.note-actions-item
= link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do
= icon('spinner spin') = icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
- if note_editable
.note-actions-item
= button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do
%span.link-highlight
= custom_icon('icon_pencil')
= render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
---
title: disabling notifications globally now properly turns off group/project added
emails
merge_request: 13325
author: @jneen
type: fixed
---
title: Improves performance of vue code by using vue files and moving svg out of data
function in pipeline schedule callout
merge_request:
author:
type: other
---
title: Use jQuery to control scroll behavior in job log for cross browser consistency
merge_request:
author:
--- ---
title: improve file upload/replace experience title: move edit comment button outside of dropdown
merge_request: merge_request:
author: author:
---
title: Fix timeouts when creating projects in groups with many members
merge_request: 13508
author:
type: fixed
---
title: Project pending delete no longer return 500 error in admins projects view
merge_request: 13389
author:
---
title: Allow any logged in users to read_users_list even if it's restricted
merge_request: 13201
author:
---
title: Fixes new issue button for failed job returning 404
merge_request:
author:
---
title: Prevents jobs dropdown from closing in pipeline graph
merge_request:
author:
type: fixed
---
title: Don't rename namespace called system when upgrading from 9.1.x to 9.5
merge_request: 13228
author:
---
title: Fix inconsistent spacing for edit buttons on issues and merge request page
merge_request:
author:
type: fixed
---
title: Fix edit merge request and issues button inconsistent letter casing
merge_request:
author:
type: fixed
---
title: Fix links to group milestones from issue and merge request sidebar
merge_request:
author:
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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