Merge branch 'master' into sh-headless-chrome-support
This commit is contained in:
commit
d3e8167382
|
@ -226,6 +226,7 @@ update-tests-metadata:
|
|||
|
||||
flaky-examples-check:
|
||||
<<: *dedicated-runner
|
||||
<<: *except-docs
|
||||
image: ruby:2.3-alpine
|
||||
services: []
|
||||
before_script: []
|
||||
|
|
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -2,6 +2,27 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
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)
|
||||
|
||||
- Remove hidden symlinks from project import files.
|
||||
|
|
3
Gemfile
3
Gemfile
|
@ -84,7 +84,7 @@ gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
|
|||
gem 'hashie-forbidden_attributes'
|
||||
|
||||
# Pagination
|
||||
gem 'kaminari', '~> 0.17.0'
|
||||
gem 'kaminari', '~> 1.0'
|
||||
|
||||
# HAML
|
||||
gem 'hamlit', '~> 2.6.1'
|
||||
|
@ -324,6 +324,7 @@ group :development, :test do
|
|||
gem 'spinach-rerun-reporter', '~> 0.0.2'
|
||||
gem 'rspec_profiling', '~> 0.0.5'
|
||||
gem 'rspec-set', '~> 0.1.3'
|
||||
gem 'rspec-parameterized'
|
||||
|
||||
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
|
||||
gem 'minitest', '~> 5.7.0'
|
||||
|
|
50
Gemfile.lock
50
Gemfile.lock
|
@ -2,6 +2,7 @@ GEM
|
|||
remote: https://rubygems.org/
|
||||
specs:
|
||||
RedCloth (4.3.2)
|
||||
abstract_type (0.0.7)
|
||||
ace-rails-ap (4.1.2)
|
||||
actionmailer (4.2.8)
|
||||
actionpack (= 4.2.8)
|
||||
|
@ -41,6 +42,9 @@ GEM
|
|||
tzinfo (~> 1.1)
|
||||
acts-as-taggable-on (4.0.0)
|
||||
activerecord (>= 4.0)
|
||||
adamantium (0.2.0)
|
||||
ice_nine (~> 0.11.0)
|
||||
memoizable (~> 0.4.0)
|
||||
addressable (2.3.8)
|
||||
after_commit_queue (1.3.0)
|
||||
activerecord (>= 3.0)
|
||||
|
@ -125,6 +129,9 @@ GEM
|
|||
coercible (1.0.0)
|
||||
descendants_tracker (~> 0.0.1)
|
||||
colorize (0.7.7)
|
||||
concord (0.1.5)
|
||||
adamantium (~> 0.2.0)
|
||||
equalizer (~> 0.0.9)
|
||||
concurrent-ruby (1.0.5)
|
||||
concurrent-ruby-ext (1.0.5)
|
||||
concurrent-ruby (= 1.0.5)
|
||||
|
@ -420,9 +427,18 @@ GEM
|
|||
json-schema (2.6.2)
|
||||
addressable (~> 2.3.8)
|
||||
jwt (1.5.6)
|
||||
kaminari (0.17.0)
|
||||
actionpack (>= 3.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
kaminari (1.0.1)
|
||||
activesupport (>= 4.1.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)
|
||||
knapsack (1.11.0)
|
||||
rake
|
||||
|
@ -462,6 +478,8 @@ GEM
|
|||
mime-types (>= 1.16, < 4)
|
||||
mail_room (0.9.1)
|
||||
memoist (0.15.0)
|
||||
memoizable (0.4.2)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
method_source (0.8.2)
|
||||
mime-types (2.99.3)
|
||||
mimemagic (0.3.0)
|
||||
|
@ -598,6 +616,11 @@ GEM
|
|||
premailer-rails (1.9.7)
|
||||
actionmailer (>= 3, < 6)
|
||||
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)
|
||||
mmap2 (~> 2.2, >= 2.2.7)
|
||||
pry (0.10.4)
|
||||
|
@ -706,6 +729,10 @@ GEM
|
|||
chunky_png
|
||||
rqrcode-rails3 (0.1.7)
|
||||
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-support (~> 3.6.0)
|
||||
rspec-expectations (3.6.0)
|
||||
|
@ -714,6 +741,12 @@ GEM
|
|||
rspec-mocks (3.6.0)
|
||||
diff-lcs (>= 1.2.0, < 2.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)
|
||||
actionpack (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
|
@ -883,6 +916,14 @@ GEM
|
|||
get_process_mem (~> 0)
|
||||
unicorn (>= 4, < 6)
|
||||
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)
|
||||
validates_hostname (1.0.6)
|
||||
activerecord (>= 3.0)
|
||||
|
@ -1008,7 +1049,7 @@ DEPENDENCIES
|
|||
jquery-rails (~> 4.1.0)
|
||||
json-schema (~> 2.6.2)
|
||||
jwt (~> 1.5.6)
|
||||
kaminari (~> 0.17.0)
|
||||
kaminari (~> 1.0)
|
||||
knapsack (~> 1.11.0)
|
||||
kubeclient (~> 2.2.0)
|
||||
letter_opener_web (~> 1.3.0)
|
||||
|
@ -1081,6 +1122,7 @@ DEPENDENCIES
|
|||
responders (~> 2.0)
|
||||
rouge (~> 2.0)
|
||||
rqrcode-rails3 (~> 0.1.7)
|
||||
rspec-parameterized
|
||||
rspec-rails (~> 3.6.0)
|
||||
rspec-retry (~> 0.4.5)
|
||||
rspec-set (~> 0.1.3)
|
||||
|
|
|
@ -97,7 +97,6 @@ const Api = {
|
|||
},
|
||||
|
||||
commitMultiple(id, data, callback) {
|
||||
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
|
||||
const url = Api.buildUrl(Api.commitPath)
|
||||
.replace(':id', id);
|
||||
return $.ajax({
|
||||
|
|
|
@ -347,6 +347,9 @@ import initChangesDropdown from './init_changes_dropdown';
|
|||
if ($('#tree-slider').length) new TreeView();
|
||||
if ($('.blob-viewer').length) new BlobViewer();
|
||||
if ($('.project-show-activity').length) new gl.Activities();
|
||||
$('#tree-slider').waitForImages(function() {
|
||||
gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
|
||||
});
|
||||
break;
|
||||
case 'projects:edit':
|
||||
setupProjectEdit();
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
import Cookies from 'js-cookie';
|
||||
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) => {
|
||||
const isHiddenByMedia = bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md';
|
||||
|
||||
|
@ -10,8 +24,28 @@ export const canShowActiveSubItems = (el) => {
|
|||
|
||||
return true;
|
||||
};
|
||||
|
||||
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) => {
|
||||
const windowHeight = window.innerHeight;
|
||||
const bottomOverflow = windowHeight - (boundingRect.top + outerHeight);
|
||||
|
@ -20,45 +54,118 @@ export const calculateTop = (boundingRect, outerHeight) => {
|
|||
boundingRect.top;
|
||||
};
|
||||
|
||||
export const showSubLevelItems = (el) => {
|
||||
const subItems = el.querySelector('.sidebar-sub-level-items');
|
||||
export const hideMenu = (el) => {
|
||||
if (!el) return;
|
||||
|
||||
if (!subItems || !canShowSubItems() || !canShowActiveSubItems(el)) return;
|
||||
const parentEl = el.parentNode;
|
||||
|
||||
subItems.style.display = 'block';
|
||||
el.classList.add('is-showing-fly-out');
|
||||
el.classList.add('is-over');
|
||||
el.style.display = ''; // eslint-disable-line no-param-reassign
|
||||
el.style.transform = ''; // eslint-disable-line no-param-reassign
|
||||
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 top = calculateTop(boundingRect, subItems.offsetHeight);
|
||||
const isAbove = top < boundingRect.top;
|
||||
|
||||
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) {
|
||||
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 (!subItems || !canShowSubItems() || !canShowActiveSubItems(el)) return;
|
||||
if (!canShowSubItems() || !canShowActiveSubItems(el)) return;
|
||||
|
||||
el.classList.remove('is-showing-fly-out');
|
||||
el.classList.remove('is-over');
|
||||
subItems.style.display = '';
|
||||
subItems.style.transform = '';
|
||||
subItems.classList.remove('is-above');
|
||||
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');
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
if (mousePos.length > 6) mousePos.shift();
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const items = [...document.querySelectorAll('.sidebar-top-level-items > li')]
|
||||
.filter(el => el.querySelector('.sidebar-sub-level-items'));
|
||||
const sidebar = document.querySelector('.sidebar-top-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) => {
|
||||
el.addEventListener('mouseenter', e => showSubLevelItems(e.currentTarget));
|
||||
el.addEventListener('mouseleave', e => hideSubLevelItems(e.currentTarget));
|
||||
const subItems = el.querySelector('.sidebar-sub-level-items');
|
||||
|
||||
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);
|
||||
};
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
export default class GpgBadges {
|
||||
static fetch() {
|
||||
const badges = $('.js-loading-gpg-badge');
|
||||
const form = $('.commits-search-form');
|
||||
|
||||
badges.html('<i class="fa fa-spinner fa-spin"></i>');
|
||||
|
||||
$.get({
|
||||
url: form.data('signatures-path'),
|
||||
data: form.serialize(),
|
||||
}).done((response) => {
|
||||
const badges = $('.js-loading-gpg-badge');
|
||||
response.signatures.forEach((signature) => {
|
||||
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
|
||||
});
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import Cookies from 'js-cookie';
|
||||
import Translate from '../../vue_shared/translate';
|
||||
import illustrationSvg from '../icons/intro_illustration.svg';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
const cookieKey = 'pipeline_schedules_callout_dismissed';
|
||||
|
||||
export default {
|
||||
name: 'PipelineSchedulesCallout',
|
||||
data() {
|
||||
return {
|
||||
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
|
||||
illustrationSvg,
|
||||
calloutDismissed: Cookies.get(cookieKey) === 'true',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
dismissCallout() {
|
||||
this.calloutDismissed = true;
|
||||
Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout">
|
||||
<div class="bordered-box landing content-block">
|
||||
<button
|
||||
id="dismiss-callout-btn"
|
||||
class="btn btn-default close"
|
||||
@click="dismissCallout">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
<div class="svg-container" v-html="illustrationSvg"></div>
|
||||
<div class="user-callout-copy">
|
||||
<h4>{{ __('Scheduling Pipelines') }}</h4>
|
||||
<p>
|
||||
{{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }}
|
||||
</p>
|
||||
<p> {{ __('Learn more in the') }}
|
||||
<a
|
||||
:href="docsUrl"
|
||||
target="_blank"
|
||||
rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period -->
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
|
@ -0,0 +1,59 @@
|
|||
<script>
|
||||
import Vue from 'vue';
|
||||
import Cookies from 'js-cookie';
|
||||
import Translate from '../../vue_shared/translate';
|
||||
import illustrationSvg from '../icons/intro_illustration.svg';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
const cookieKey = 'pipeline_schedules_callout_dismissed';
|
||||
|
||||
export default {
|
||||
name: 'PipelineSchedulesCallout',
|
||||
data() {
|
||||
return {
|
||||
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
|
||||
calloutDismissed: Cookies.get(cookieKey) === 'true',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
dismissCallout() {
|
||||
this.calloutDismissed = true;
|
||||
Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.illustrationSvg = illustrationSvg;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
v-if="!calloutDismissed"
|
||||
class="pipeline-schedules-user-callout user-callout">
|
||||
<div class="bordered-box landing content-block">
|
||||
<button
|
||||
id="dismiss-callout-btn"
|
||||
class="btn btn-default close"
|
||||
@click="dismissCallout">
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-times">
|
||||
</i>
|
||||
</button>
|
||||
<div class="svg-container" v-html="illustrationSvg"></div>
|
||||
<div class="user-callout-copy">
|
||||
<h4>{{ __('Scheduling Pipelines') }}</h4>
|
||||
<p>
|
||||
{{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }}
|
||||
</p>
|
||||
<p> {{ __('Learn more in the') }}
|
||||
<a
|
||||
:href="docsUrl"
|
||||
target="_blank"
|
||||
rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period -->
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,5 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import PipelineSchedulesCallout from './components/pipeline_schedules_callout';
|
||||
import PipelineSchedulesCallout from './components/pipeline_schedules_callout.vue';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => new Vue({
|
||||
el: '#pipeline-schedules-callout',
|
||||
|
|
|
@ -48,6 +48,27 @@
|
|||
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>
|
||||
<template>
|
||||
|
|
|
@ -126,11 +126,11 @@ import Cookies from 'js-cookie';
|
|||
var $form = $dropdown.closest('form');
|
||||
|
||||
var $visit = $dropdown.data('visit');
|
||||
var shouldVisit = typeof $visit === 'undefined' ? true : $visit;
|
||||
var shouldVisit = $visit ? true : $visit;
|
||||
var action = $form.attr('action');
|
||||
var divider = action.indexOf('?') === -1 ? '?' : '&';
|
||||
if (shouldVisit) {
|
||||
gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
|
||||
gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,13 +14,13 @@ export default {
|
|||
data: () => Store,
|
||||
mixins: [RepoMixin],
|
||||
components: {
|
||||
'repo-sidebar': RepoSidebar,
|
||||
'repo-tabs': RepoTabs,
|
||||
'repo-file-buttons': RepoFileButtons,
|
||||
RepoSidebar,
|
||||
RepoTabs,
|
||||
RepoFileButtons,
|
||||
'repo-editor': MonacoLoaderHelper.repoEditorLoader,
|
||||
'repo-commit-section': RepoCommitSection,
|
||||
'popup-dialog': PopupDialog,
|
||||
'repo-preview': RepoPreview,
|
||||
RepoCommitSection,
|
||||
PopupDialog,
|
||||
RepoPreview,
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
@ -28,12 +28,12 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
dialogToggled(toggle) {
|
||||
toggleDialogOpen(toggle) {
|
||||
this.dialog.open = toggle;
|
||||
},
|
||||
|
||||
dialogSubmitted(status) {
|
||||
this.dialog.open = false;
|
||||
this.toggleDialogOpen(false);
|
||||
this.dialog.status = status;
|
||||
},
|
||||
|
||||
|
@ -43,21 +43,25 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="repository-view tree-content-holder">
|
||||
<repo-sidebar/><div class="panel-right" :class="{'edit-mode': editMode}">
|
||||
<repo-tabs/>
|
||||
<component :is="currentBlobView" class="blob-viewer-container"></component>
|
||||
<repo-file-buttons/>
|
||||
<div class="repository-view tree-content-holder">
|
||||
<repo-sidebar/><div v-if="isMini"
|
||||
class="panel-right"
|
||||
:class="{'edit-mode': editMode}">
|
||||
<repo-tabs/>
|
||||
<component
|
||||
:is="currentBlobView"
|
||||
class="blob-viewer-container"/>
|
||||
<repo-file-buttons/>
|
||||
</div>
|
||||
<repo-commit-section/>
|
||||
<popup-dialog
|
||||
v-show="dialog.open"
|
||||
:primary-button-label="__('Discard changes')"
|
||||
kind="warning"
|
||||
:title="__('Are you sure?')"
|
||||
:body="__('Are you sure you want to discard your changes?')"
|
||||
@toggle="toggleDialogOpen"
|
||||
@submit="dialogSubmitted"
|
||||
/>
|
||||
</div>
|
||||
<repo-commit-section/>
|
||||
<popup-dialog
|
||||
:primary-button-label="__('Discard changes')"
|
||||
:open="dialog.open"
|
||||
kind="warning"
|
||||
:title="__('Are you sure?')"
|
||||
:body="__('Are you sure you want to discard your changes?')"
|
||||
@toggle="dialogToggled"
|
||||
@submit="dialogSubmitted"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -2,18 +2,20 @@
|
|||
/* global Flash */
|
||||
import Store from '../stores/repo_store';
|
||||
import RepoMixin from '../mixins/repo_mixin';
|
||||
import Helper from '../helpers/repo_helper';
|
||||
import Service from '../services/repo_service';
|
||||
|
||||
const RepoCommitSection = {
|
||||
export default {
|
||||
data: () => Store,
|
||||
|
||||
mixins: [RepoMixin],
|
||||
|
||||
computed: {
|
||||
showCommitable() {
|
||||
return this.isCommitable && this.changedFiles.length;
|
||||
},
|
||||
|
||||
branchPaths() {
|
||||
const branch = Helper.getBranch();
|
||||
return this.changedFiles.map(f => Helper.getFilePathFromFullPath(f.url, branch));
|
||||
return this.changedFiles.map(f => f.path);
|
||||
},
|
||||
|
||||
cantCommitYet() {
|
||||
|
@ -28,11 +30,10 @@ const RepoCommitSection = {
|
|||
methods: {
|
||||
makeCommit() {
|
||||
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
|
||||
const branch = Helper.getBranch();
|
||||
const commitMessage = this.commitMessage;
|
||||
const actions = this.changedFiles.map(f => ({
|
||||
action: 'update',
|
||||
file_path: Helper.getFilePathFromFullPath(f.url, branch),
|
||||
file_path: f.path,
|
||||
content: f.newContent,
|
||||
}));
|
||||
const payload = {
|
||||
|
@ -47,51 +48,80 @@ const RepoCommitSection = {
|
|||
resetCommitState() {
|
||||
this.submitCommitsLoading = false;
|
||||
this.changedFiles = [];
|
||||
this.openedFiles = [];
|
||||
this.commitMessage = '';
|
||||
this.editMode = false;
|
||||
$('html, body').animate({ scrollTop: 0 }, 'fast');
|
||||
window.scrollTo(0, 0);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default RepoCommitSection;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="commit-area" v-if="isCommitable && changedFiles.length" >
|
||||
<form class="form-horizontal">
|
||||
<div
|
||||
v-if="showCommitable"
|
||||
id="commit-area">
|
||||
<form
|
||||
class="form-horizontal"
|
||||
@submit.prevent="makeCommit">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label staged-files">Staged files ({{changedFiles.length}})</label>
|
||||
<div class="col-md-4">
|
||||
<label class="col-md-4 control-label staged-files">
|
||||
Staged files ({{changedFiles.length}})
|
||||
</label>
|
||||
<div class="col-md-6">
|
||||
<ul class="list-unstyled changed-files">
|
||||
<li v-for="file in branchPaths" :key="file.id">
|
||||
<span class="help-block">{{file}}</span>
|
||||
<li
|
||||
v-for="branchPath in branchPaths"
|
||||
:key="branchPath">
|
||||
<span class="help-block">
|
||||
{{branchPath}}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Textarea
|
||||
-->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label" for="commit-message">Commit message</label>
|
||||
<div class="col-md-4">
|
||||
<textarea class="form-control" id="commit-message" name="commit-message" v-model="commitMessage"></textarea>
|
||||
<label
|
||||
class="col-md-4 control-label"
|
||||
for="commit-message">
|
||||
Commit message
|
||||
</label>
|
||||
<div class="col-md-6">
|
||||
<textarea
|
||||
id="commit-message"
|
||||
class="form-control"
|
||||
name="commit-message"
|
||||
v-model="commitMessage">
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Button Drop Down
|
||||
-->
|
||||
<div class="form-group target-branch">
|
||||
<label class="col-md-4 control-label" for="target-branch">Target branch</label>
|
||||
<div class="col-md-4">
|
||||
<span class="help-block">{{targetBranch}}</span>
|
||||
<label
|
||||
class="col-md-4 control-label"
|
||||
for="target-branch">
|
||||
Target branch
|
||||
</label>
|
||||
<div class="col-md-6">
|
||||
<span class="help-block">
|
||||
{{targetBranch}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-offset-4 col-md-4">
|
||||
<button type="submit" :disabled="cantCommitYet" class="btn btn-success submit-commit" @click.prevent="makeCommit">
|
||||
<i class="fa fa-spinner fa-spin" v-if="submitCommitsLoading"></i>
|
||||
<span class="commit-summary">Commit {{changedFiles.length}} {{filePluralize}}</span>
|
||||
<div class="col-md-offset-4 col-md-6">
|
||||
<button
|
||||
ref="submitCommit"
|
||||
type="submit"
|
||||
:disabled="cantCommitYet"
|
||||
class="btn btn-success">
|
||||
<i
|
||||
v-if="submitCommitsLoading"
|
||||
class="fa fa-spinner fa-spin"
|
||||
aria-hidden="true"
|
||||
aria-label="loading">
|
||||
</i>
|
||||
<span class="commit-summary">
|
||||
Commit {{changedFiles.length}} {{filePluralize}}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
|
@ -10,12 +10,15 @@ export default {
|
|||
return this.editMode ? this.__('Cancel edit') : this.__('Edit');
|
||||
},
|
||||
|
||||
buttonIcon() {
|
||||
return this.editMode ? [] : ['fa', 'fa-pencil'];
|
||||
showButton() {
|
||||
return this.isCommitable &&
|
||||
!this.activeFile.render_error &&
|
||||
!this.binary &&
|
||||
this.openedFiles.length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
editClicked() {
|
||||
editCancelClicked() {
|
||||
if (this.changedFiles.length) {
|
||||
this.dialog.open = true;
|
||||
return;
|
||||
|
@ -23,27 +26,33 @@ export default {
|
|||
this.editMode = !this.editMode;
|
||||
Store.toggleBlobView();
|
||||
},
|
||||
toggleProjectRefsForm() {
|
||||
$('.project-refs-form').toggleClass('disabled', this.editMode);
|
||||
$('.js-tree-ref-target-holder').toggle(this.editMode);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
editMode() {
|
||||
if (this.editMode) {
|
||||
$('.project-refs-form').addClass('disabled');
|
||||
$('.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();
|
||||
}
|
||||
this.toggleProjectRefsForm();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="btn btn-default" @click.prevent="editClicked" v-cloak v-if="isCommitable && !activeFile.render_error" :disabled="binary">
|
||||
<i :class="buttonIcon"></i>
|
||||
<span>{{buttonLabel}}</span>
|
||||
<button
|
||||
v-if="showButton"
|
||||
class="btn btn-default"
|
||||
type="button"
|
||||
@click.prevent="editCancelClicked">
|
||||
<i
|
||||
v-if="!editMode"
|
||||
class="fa fa-pencil"
|
||||
aria-hidden="true">
|
||||
</i>
|
||||
<span>
|
||||
{{buttonLabel}}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
|
|
@ -8,38 +8,39 @@ const RepoEditor = {
|
|||
data: () => Store,
|
||||
|
||||
destroyed() {
|
||||
// this.monacoInstance.getModels().forEach((m) => {
|
||||
// m.dispose();
|
||||
// });
|
||||
this.monacoInstance.destroy();
|
||||
if (Helper.monacoInstance) {
|
||||
Helper.monacoInstance.destroy();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
Service.getRaw(this.activeFile.raw_path)
|
||||
.then((rawResponse) => {
|
||||
Store.blobRaw = rawResponse.data;
|
||||
Helper.findOpenedFileFromActive().plain = rawResponse.data;
|
||||
.then((rawResponse) => {
|
||||
Store.blobRaw = rawResponse.data;
|
||||
Store.activeFile.plain = rawResponse.data;
|
||||
|
||||
const monacoInstance = this.monaco.editor.create(this.$el, {
|
||||
model: null,
|
||||
readOnly: false,
|
||||
contextmenu: false,
|
||||
});
|
||||
const monacoInstance = Helper.monaco.editor.create(this.$el, {
|
||||
model: null,
|
||||
readOnly: false,
|
||||
contextmenu: false,
|
||||
});
|
||||
|
||||
Store.monacoInstance = monacoInstance;
|
||||
Helper.monacoInstance = monacoInstance;
|
||||
|
||||
this.addMonacoEvents();
|
||||
this.addMonacoEvents();
|
||||
|
||||
const languages = this.monaco.languages.getLanguages();
|
||||
const languageID = Helper.getLanguageIDForFile(this.activeFile, languages);
|
||||
this.showHide();
|
||||
const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
|
||||
|
||||
this.monacoInstance.setModel(newModel);
|
||||
}).catch(Helper.loadingError);
|
||||
this.setupEditor();
|
||||
})
|
||||
.catch(Helper.loadingError);
|
||||
},
|
||||
|
||||
methods: {
|
||||
setupEditor() {
|
||||
this.showHide();
|
||||
|
||||
Helper.setMonacoModelFromLanguage();
|
||||
},
|
||||
|
||||
showHide() {
|
||||
if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
|
||||
this.$el.style.display = 'none';
|
||||
|
@ -49,41 +50,36 @@ const RepoEditor = {
|
|||
},
|
||||
|
||||
addMonacoEvents() {
|
||||
this.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
|
||||
this.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
|
||||
Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
|
||||
Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
|
||||
},
|
||||
|
||||
onMonacoEditorKeysPressed() {
|
||||
Store.setActiveFileContents(this.monacoInstance.getValue());
|
||||
Store.setActiveFileContents(Helper.monacoInstance.getValue());
|
||||
},
|
||||
|
||||
onMonacoEditorMouseUp(e) {
|
||||
if (!e.target.position) return;
|
||||
const lineNumber = e.target.position.lineNumber;
|
||||
if (e.target.element.className === 'line-numbers') {
|
||||
if (e.target.element.classList.contains('line-numbers')) {
|
||||
location.hash = `L${lineNumber}`;
|
||||
Store.activeLine = lineNumber;
|
||||
|
||||
Helper.monacoInstance.setPosition({
|
||||
lineNumber: this.activeLine,
|
||||
column: 1,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
activeLine() {
|
||||
this.monacoInstance.setPosition({
|
||||
lineNumber: this.activeLine,
|
||||
column: 1,
|
||||
});
|
||||
},
|
||||
|
||||
activeFileLabel() {
|
||||
this.showHide();
|
||||
},
|
||||
|
||||
dialog: {
|
||||
handler(obj) {
|
||||
const newObj = obj;
|
||||
if (newObj.status) {
|
||||
newObj.status = false;
|
||||
this.openedFiles.map((file) => {
|
||||
this.openedFiles = this.openedFiles.map((file) => {
|
||||
const f = file;
|
||||
if (f.active) {
|
||||
this.blobRaw = f.plain;
|
||||
|
@ -94,35 +90,21 @@ const RepoEditor = {
|
|||
return f;
|
||||
});
|
||||
this.editMode = false;
|
||||
Store.toggleBlobView();
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
|
||||
isTree() {
|
||||
this.showHide();
|
||||
},
|
||||
|
||||
openedFiles() {
|
||||
this.showHide();
|
||||
},
|
||||
|
||||
binary() {
|
||||
this.showHide();
|
||||
},
|
||||
|
||||
blobRaw() {
|
||||
this.showHide();
|
||||
|
||||
if (this.isTree) return;
|
||||
|
||||
this.monacoInstance.setModel(null);
|
||||
|
||||
const languages = this.monaco.languages.getLanguages();
|
||||
const languageID = Helper.getLanguageIDForFile(this.activeFile, languages);
|
||||
const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
|
||||
|
||||
this.monacoInstance.setModel(newModel);
|
||||
if (Helper.monacoInstance && !this.isTree) {
|
||||
this.setupEditor();
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
shouldHideEditor() {
|
||||
return !this.openedFiles.length || (this.binary && !this.activeFile.raw);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -131,5 +113,5 @@ export default RepoEditor;
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div id="ide"></div>
|
||||
<div id="ide" v-if='!shouldHideEditor'></div>
|
||||
</template>
|
||||
|
|
|
@ -33,6 +33,26 @@ const RepoFile = {
|
|||
canShowFile() {
|
||||
return !this.loading.tree || this.hasFiles;
|
||||
},
|
||||
|
||||
fileIcon() {
|
||||
const classObj = {
|
||||
'fa-spinner fa-spin': this.file.loading,
|
||||
[this.file.icon]: !this.file.loading,
|
||||
};
|
||||
return classObj;
|
||||
},
|
||||
|
||||
fileIndentation() {
|
||||
return {
|
||||
'margin-left': `${this.file.level * 10}px`,
|
||||
};
|
||||
},
|
||||
|
||||
activeFileClass() {
|
||||
return {
|
||||
active: this.activeFile.url === this.file.url,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -46,21 +66,42 @@ export default RepoFile;
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<tr class="file" v-if="canShowFile" :class="{'active': activeFile.url === file.url}">
|
||||
<td @click.prevent="linkClicked(file)">
|
||||
<i class="fa file-icon" v-if="!file.loading" :class="file.icon" :style="{'margin-left': file.level * 10 + 'px'}"></i>
|
||||
<i class="fa fa-spinner fa-spin" v-if="file.loading" :style="{'margin-left': file.level * 10 + 'px'}"></i>
|
||||
<a :href="file.url" class="repo-file-name" :title="file.url">{{file.name}}</a>
|
||||
<tr
|
||||
v-if="canShowFile"
|
||||
class="file"
|
||||
:class="activeFileClass"
|
||||
@click.prevent="linkClicked(file)">
|
||||
<td>
|
||||
<i
|
||||
class="fa fa-fw file-icon"
|
||||
:class="fileIcon"
|
||||
:style="fileIndentation"
|
||||
aria-label="file icon">
|
||||
</i>
|
||||
<a
|
||||
:href="file.url"
|
||||
class="repo-file-name"
|
||||
:title="file.url">
|
||||
{{file.name}}
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<td v-if="!isMini" class="hidden-sm hidden-xs">
|
||||
<div class="commit-message">
|
||||
<a :href="file.lastCommitUrl">{{file.lastCommitMessage}}</a>
|
||||
</div>
|
||||
</td>
|
||||
<template v-if="!isMini">
|
||||
<td class="hidden-sm hidden-xs">
|
||||
<div class="commit-message">
|
||||
<a @click.stop :href="file.lastCommitUrl">
|
||||
{{file.lastCommitMessage}}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td v-if="!isMini" class="hidden-xs">
|
||||
<span class="commit-update" :title="tooltipTitle(file.lastCommitUpdate)">{{timeFormated(file.lastCommitUpdate)}}</span>
|
||||
</td>
|
||||
<td class="hidden-xs">
|
||||
<span
|
||||
class="commit-update"
|
||||
:title="tooltipTitle(file.lastCommitUpdate)">
|
||||
{{timeFormated(file.lastCommitUpdate)}}
|
||||
</span>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
||||
|
|
|
@ -15,7 +15,7 @@ const RepoFileButtons = {
|
|||
},
|
||||
|
||||
canPreview() {
|
||||
return Helper.isKindaBinary();
|
||||
return Helper.isRenderable();
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -28,15 +28,42 @@ export default RepoFileButtons;
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div id="repo-file-buttons" v-if="isMini">
|
||||
<a :href="activeFile.raw_path" target="_blank" class="btn btn-default raw" rel="noopener noreferrer">{{rawDownloadButtonLabel}}</a>
|
||||
<div id="repo-file-buttons">
|
||||
<a
|
||||
:href="activeFile.raw_path"
|
||||
target="_blank"
|
||||
class="btn btn-default raw"
|
||||
rel="noopener noreferrer">
|
||||
{{rawDownloadButtonLabel}}
|
||||
</a>
|
||||
|
||||
<div class="btn-group" role="group" aria-label="File actions">
|
||||
<a :href="activeFile.blame_path" class="btn btn-default blame">Blame</a>
|
||||
<a :href="activeFile.commits_path" class="btn btn-default history">History</a>
|
||||
<a :href="activeFile.permalink" class="btn btn-default permalink">Permalink</a>
|
||||
<div
|
||||
class="btn-group"
|
||||
role="group"
|
||||
aria-label="File actions">
|
||||
<a
|
||||
:href="activeFile.blame_path"
|
||||
class="btn btn-default blame">
|
||||
Blame
|
||||
</a>
|
||||
<a
|
||||
:href="activeFile.commits_path"
|
||||
class="btn btn-default history">
|
||||
History
|
||||
</a>
|
||||
<a
|
||||
:href="activeFile.permalink"
|
||||
class="btn btn-default permalink">
|
||||
Permalink
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a
|
||||
v-if="canPreview"
|
||||
href="#"
|
||||
@click.prevent="rawPreviewToggle"
|
||||
class="btn btn-default preview">
|
||||
{{activeFileLabel}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a href="#" v-if="canPreview" @click.prevent="rawPreviewToggle" class="btn btn-default preview">{{activeFileLabel}}</a>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -17,7 +17,7 @@ export default RepoFileOptions;
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<tr v-if="isMini" class="repo-file-options">
|
||||
<tr v-if="isMini" class="repo-file-options">
|
||||
<td>
|
||||
<span class="title">{{projectName}}</span>
|
||||
</td>
|
||||
|
|
|
@ -18,9 +18,15 @@ const RepoLoadingFile = {
|
|||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
showGhostLines() {
|
||||
return this.loading.tree && !this.hasFiles;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
lineOfCode(n) {
|
||||
return `line-of-code-${n}`;
|
||||
return `skeleton-line-${n}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -29,23 +35,42 @@ export default RepoLoadingFile;
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<tr v-if="loading.tree && !hasFiles" class="loading-file">
|
||||
<td>
|
||||
<div class="animation-container animation-container-small">
|
||||
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
|
||||
</div>
|
||||
</td>
|
||||
<tr
|
||||
v-if="showGhostLines"
|
||||
class="loading-file">
|
||||
<td>
|
||||
<div
|
||||
class="animation-container animation-container-small">
|
||||
<div
|
||||
v-for="n in 6"
|
||||
:key="n"
|
||||
:class="lineOfCode(n)">
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td v-if="!isMini" class="hidden-sm hidden-xs">
|
||||
<div class="animation-container">
|
||||
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
v-if="!isMini"
|
||||
class="hidden-sm hidden-xs">
|
||||
<div class="animation-container">
|
||||
<div
|
||||
v-for="n in 6"
|
||||
:key="n"
|
||||
:class="lineOfCode(n)">
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td v-if="!isMini" class="hidden-xs">
|
||||
<div class="animation-container animation-container-small">
|
||||
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<td
|
||||
v-if="!isMini"
|
||||
class="hidden-xs">
|
||||
<div class="animation-container animation-container-small">
|
||||
<div
|
||||
v-for="n in 6"
|
||||
:key="n"
|
||||
:class="lineOfCode(n)">
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
<script>
|
||||
import RepoMixin from '../mixins/repo_mixin';
|
||||
|
||||
const RepoPreviousDirectory = {
|
||||
props: {
|
||||
prevUrl: {
|
||||
|
@ -7,6 +9,14 @@ const RepoPreviousDirectory = {
|
|||
},
|
||||
},
|
||||
|
||||
mixins: [RepoMixin],
|
||||
|
||||
computed: {
|
||||
colSpanCondition() {
|
||||
return this.isMini ? undefined : 3;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
linkClicked(file) {
|
||||
this.$emit('linkclicked', file);
|
||||
|
@ -19,8 +29,10 @@ export default RepoPreviousDirectory;
|
|||
|
||||
<template>
|
||||
<tr class="prev-directory">
|
||||
<td colspan="3">
|
||||
<a :href="prevUrl" @click.prevent="linkClicked(prevUrl)">..</a>
|
||||
<td
|
||||
:colspan="colSpanCondition"
|
||||
@click.prevent="linkClicked(prevUrl)">
|
||||
<a :href="prevUrl">..</a>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
|
|
@ -4,7 +4,7 @@ import Store from '../stores/repo_store';
|
|||
export default {
|
||||
data: () => Store,
|
||||
mounted() {
|
||||
$(this.$el).find('.file-content').syntaxHighlight();
|
||||
this.highlightFile();
|
||||
},
|
||||
computed: {
|
||||
html() {
|
||||
|
@ -12,10 +12,16 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
highlightFile() {
|
||||
$(this.$el).find('.file-content').syntaxHighlight();
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
html() {
|
||||
this.$nextTick(() => {
|
||||
$(this.$el).find('.file-content').syntaxHighlight();
|
||||
this.highlightFile();
|
||||
});
|
||||
},
|
||||
},
|
||||
|
@ -24,9 +30,23 @@ export default {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!activeFile.render_error" v-html="activeFile.html"></div>
|
||||
<div v-if="activeFile.render_error" 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
|
||||
v-if="!activeFile.render_error"
|
||||
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>
|
||||
</template>
|
||||
|
|
|
@ -8,7 +8,7 @@ import RepoFile from './repo_file.vue';
|
|||
import RepoLoadingFile from './repo_loading_file.vue';
|
||||
import RepoMixin from '../mixins/repo_mixin';
|
||||
|
||||
const RepoSidebar = {
|
||||
export default {
|
||||
mixins: [RepoMixin],
|
||||
components: {
|
||||
'repo-file-options': RepoFileOptions,
|
||||
|
@ -33,40 +33,36 @@ const RepoSidebar = {
|
|||
});
|
||||
},
|
||||
|
||||
linkClicked(clickedFile) {
|
||||
let url = '';
|
||||
fileClicked(clickedFile) {
|
||||
let file = clickedFile;
|
||||
if (typeof file === 'object') {
|
||||
file.loading = true;
|
||||
if (file.type === 'tree' && file.opened) {
|
||||
file = Store.removeChildFilesOfTree(file);
|
||||
file.loading = false;
|
||||
} else {
|
||||
url = file.url;
|
||||
Service.url = url;
|
||||
// I need to refactor this to do the `then` here.
|
||||
// Not a callback. For now this is good enough.
|
||||
// it works.
|
||||
Helper.getContent(file, () => {
|
||||
if (file.loading) return;
|
||||
file.loading = true;
|
||||
if (file.type === 'tree' && file.opened) {
|
||||
file = Store.removeChildFilesOfTree(file);
|
||||
file.loading = false;
|
||||
} else {
|
||||
Service.url = file.url;
|
||||
Helper.getContent(file)
|
||||
.then(() => {
|
||||
file.loading = false;
|
||||
Helper.scrollTabsRight();
|
||||
});
|
||||
}
|
||||
} else if (typeof file === 'string') {
|
||||
// go back
|
||||
url = file;
|
||||
Service.url = url;
|
||||
Helper.getContent(null, () => Helper.scrollTabsRight());
|
||||
})
|
||||
.catch(Helper.loadingError);
|
||||
}
|
||||
},
|
||||
|
||||
goToPreviousDirectoryClicked(prevURL) {
|
||||
Service.url = prevURL;
|
||||
Helper.getContent(null)
|
||||
.then(() => Helper.scrollTabsRight())
|
||||
.catch(Helper.loadingError);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default RepoSidebar;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="sidebar" :class="{'sidebar-mini' : isMini}" v-cloak>
|
||||
<div id="sidebar" :class="{'sidebar-mini' : isMini}">
|
||||
<table class="table">
|
||||
<thead v-if="!isMini">
|
||||
<tr>
|
||||
|
@ -82,7 +78,7 @@ export default RepoSidebar;
|
|||
<repo-previous-directory
|
||||
v-if="isRoot"
|
||||
:prev-url="prevURL"
|
||||
@linkclicked="linkClicked(prevURL)"/>
|
||||
@linkclicked="goToPreviousDirectoryClicked(prevURL)"/>
|
||||
<repo-loading-file
|
||||
v-for="n in 5"
|
||||
:key="n"
|
||||
|
@ -94,7 +90,7 @@ export default RepoSidebar;
|
|||
:key="file.id"
|
||||
:file="file"
|
||||
:is-mini="isMini"
|
||||
@linkclicked="linkClicked(file)"
|
||||
@linkclicked="fileClicked(file)"
|
||||
:is-tree="isTree"
|
||||
:has-files="!!files.length"
|
||||
:active-file="activeFile"/>
|
||||
|
|
|
@ -10,10 +10,16 @@ const RepoTab = {
|
|||
},
|
||||
|
||||
computed: {
|
||||
closeLabel() {
|
||||
if (this.tab.changed) {
|
||||
return `${this.tab.name} changed`;
|
||||
}
|
||||
return `Close ${this.tab.name}`;
|
||||
},
|
||||
changedClass() {
|
||||
const tabChangedObj = {
|
||||
'fa-times': !this.tab.changed,
|
||||
'fa-circle': this.tab.changed,
|
||||
'fa-times close-icon': !this.tab.changed,
|
||||
'fa-circle unsaved-icon': this.tab.changed,
|
||||
};
|
||||
return tabChangedObj;
|
||||
},
|
||||
|
@ -22,9 +28,9 @@ const RepoTab = {
|
|||
methods: {
|
||||
tabClicked: Store.setActiveFiles,
|
||||
|
||||
xClicked(file) {
|
||||
closeTab(file) {
|
||||
if (file.changed) return;
|
||||
this.$emit('xclicked', file);
|
||||
this.$emit('tabclosed', file);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -33,13 +39,25 @@ export default RepoTab;
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<li>
|
||||
<a href="#" class="close" @click.prevent="xClicked(tab)" v-if="!tab.loading">
|
||||
<i class="fa" :class="changedClass"></i>
|
||||
<li @click="tabClicked(tab)">
|
||||
<a
|
||||
href="#0"
|
||||
class="close"
|
||||
@click.stop.prevent="closeTab(tab)"
|
||||
:aria-label="closeLabel">
|
||||
<i
|
||||
class="fa"
|
||||
:class="changedClass"
|
||||
aria-hidden="true">
|
||||
</i>
|
||||
</a>
|
||||
|
||||
<a href="#" class="repo-tab" v-if="!tab.loading" :title="tab.url" @click.prevent="tabClicked(tab)">{{tab.name}}</a>
|
||||
|
||||
<i v-if="tab.loading" class="fa fa-spinner fa-spin"></i>
|
||||
<a
|
||||
href="#"
|
||||
class="repo-tab"
|
||||
:title="tab.url"
|
||||
@click.prevent="tabClicked(tab)">
|
||||
{{tab.name}}
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script>
|
||||
import Vue from 'vue';
|
||||
import Store from '../stores/repo_store';
|
||||
import RepoTab from './repo_tab.vue';
|
||||
import RepoMixin from '../mixins/repo_mixin';
|
||||
|
@ -14,30 +13,24 @@ const RepoTabs = {
|
|||
data: () => Store,
|
||||
|
||||
methods: {
|
||||
isOverflow() {
|
||||
return this.$el.scrollWidth > this.$el.offsetWidth;
|
||||
},
|
||||
|
||||
xClicked(file) {
|
||||
tabClosed(file) {
|
||||
Store.removeFromOpenedFiles(file);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
openedFiles() {
|
||||
Vue.nextTick(() => {
|
||||
this.tabsOverflow = this.isOverflow();
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default RepoTabs;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul id="tabs" v-if="isMini" v-cloak :class="{'overflown': tabsOverflow}">
|
||||
<repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/>
|
||||
<ul id="tabs">
|
||||
<repo-tab
|
||||
v-for="tab in openedFiles"
|
||||
:key="tab.id"
|
||||
:tab="tab"
|
||||
:class="{'active' : tab.active}"
|
||||
@tabclosed="tabClosed"
|
||||
/>
|
||||
<li class="tabs-divider" />
|
||||
</ul>
|
||||
</template>
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
/* global monaco */
|
||||
import RepoEditor from '../components/repo_editor.vue';
|
||||
import Store from '../stores/repo_store';
|
||||
import Helper from '../helpers/repo_helper';
|
||||
import monacoLoader from '../monaco_loader';
|
||||
|
||||
function repoEditorLoader() {
|
||||
Store.monacoLoading = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
monacoLoader(['vs/editor/editor.main'], () => {
|
||||
Store.monaco = monaco;
|
||||
Helper.monaco = monaco;
|
||||
Store.monacoLoading = false;
|
||||
resolve(RepoEditor);
|
||||
}, reject);
|
||||
}, () => {
|
||||
Store.monacoLoading = false;
|
||||
reject();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ import Store from '../stores/repo_store';
|
|||
import '../../flash';
|
||||
|
||||
const RepoHelper = {
|
||||
monacoInstance: null,
|
||||
|
||||
getDefaultActiveFile() {
|
||||
return {
|
||||
active: true,
|
||||
|
@ -33,19 +35,23 @@ const RepoHelper = {
|
|||
? window.performance
|
||||
: Date,
|
||||
|
||||
getBranch() {
|
||||
return $('button.dropdown-menu-toggle').attr('data-ref');
|
||||
getFileExtension(fileName) {
|
||||
return fileName.split('.').pop();
|
||||
},
|
||||
|
||||
getLanguageIDForFile(file, langs) {
|
||||
const ext = file.name.split('.').pop();
|
||||
const ext = RepoHelper.getFileExtension(file.name);
|
||||
const foundLang = RepoHelper.findLanguage(ext, langs);
|
||||
|
||||
return foundLang ? foundLang.id : 'plaintext';
|
||||
},
|
||||
|
||||
getFilePathFromFullPath(fullPath, branch) {
|
||||
return fullPath.split(`${Store.projectUrl}/blob/${branch}`)[1];
|
||||
setMonacoModelFromLanguage() {
|
||||
RepoHelper.monacoInstance.setModel(null);
|
||||
const languages = RepoHelper.monaco.languages.getLanguages();
|
||||
const languageID = RepoHelper.getLanguageIDForFile(Store.activeFile, languages);
|
||||
const newModel = RepoHelper.monaco.editor.createModel(Store.blobRaw, languageID);
|
||||
RepoHelper.monacoInstance.setModel(newModel);
|
||||
},
|
||||
|
||||
findLanguage(ext, langs) {
|
||||
|
@ -58,11 +64,11 @@ const RepoHelper = {
|
|||
|
||||
file.opened = true;
|
||||
file.icon = 'fa-folder-open';
|
||||
RepoHelper.toURL(file.url, file.name);
|
||||
RepoHelper.updateHistoryEntry(file.url, file.name);
|
||||
return file;
|
||||
},
|
||||
|
||||
isKindaBinary() {
|
||||
isRenderable() {
|
||||
const okExts = ['md', 'svg'];
|
||||
return okExts.indexOf(Store.activeFile.extension) > -1;
|
||||
},
|
||||
|
@ -76,22 +82,8 @@ const RepoHelper = {
|
|||
.catch(RepoHelper.loadingError);
|
||||
},
|
||||
|
||||
toggleFakeTab(loading, file) {
|
||||
if (loading) return Store.addPlaceholderFile();
|
||||
return Store.removeFromOpenedFiles(file);
|
||||
},
|
||||
|
||||
setLoading(loading, file) {
|
||||
if (Service.url.indexOf('blob') > -1) {
|
||||
Store.loading.blob = loading;
|
||||
return RepoHelper.toggleFakeTab(loading, file);
|
||||
}
|
||||
|
||||
if (Service.url.indexOf('tree') > -1) Store.loading.tree = loading;
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
// when you open a directory you need to put the directory files under
|
||||
// the directory... This will merge the list of the current directory and the new list.
|
||||
getNewMergedList(inDirectory, currentList, newList) {
|
||||
const newListSorted = newList.sort(this.compareFilesCaseInsensitive);
|
||||
if (!inDirectory) return newListSorted;
|
||||
|
@ -100,6 +92,9 @@ const RepoHelper = {
|
|||
return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile);
|
||||
},
|
||||
|
||||
// within the get new merged list this does the merging of the current list of files
|
||||
// and the new list of files. The files are never "in" another directory they just
|
||||
// appear like they are because of the margin.
|
||||
mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) {
|
||||
newList.reverse().forEach((newFile) => {
|
||||
const fileIndex = indexOfFile + 1;
|
||||
|
@ -135,21 +130,17 @@ const RepoHelper = {
|
|||
return isRoot;
|
||||
},
|
||||
|
||||
getContent(treeOrFile, cb) {
|
||||
getContent(treeOrFile) {
|
||||
let file = treeOrFile;
|
||||
// const loadingData = RepoHelper.setLoading(true);
|
||||
return Service.getContent()
|
||||
.then((response) => {
|
||||
const data = response.data;
|
||||
// RepoHelper.setLoading(false, loadingData);
|
||||
if (cb) cb();
|
||||
Store.isTree = RepoHelper.isTree(data);
|
||||
if (!Store.isTree) {
|
||||
if (!file) file = data;
|
||||
Store.binary = data.binary;
|
||||
|
||||
if (data.binary) {
|
||||
Store.binaryMimeType = data.mime_type;
|
||||
// file might be undefined
|
||||
RepoHelper.setBinaryDataAsBase64(data);
|
||||
Store.setViewToPreview();
|
||||
|
@ -188,9 +179,8 @@ const RepoHelper = {
|
|||
setFile(data, file) {
|
||||
const newFile = data;
|
||||
|
||||
newFile.url = file.url || location.pathname;
|
||||
newFile.url = file.url;
|
||||
if (newFile.render_error === 'too_large') {
|
||||
if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
|
||||
newFile.tooLarge = true;
|
||||
}
|
||||
newFile.newContent = '';
|
||||
|
@ -199,10 +189,6 @@ const RepoHelper = {
|
|||
Store.setActiveFiles(newFile);
|
||||
},
|
||||
|
||||
toFA(icon) {
|
||||
return `fa-${icon}`;
|
||||
},
|
||||
|
||||
serializeBlob(blob) {
|
||||
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
|
||||
simpleBlob.lastCommitMessage = blob.last_commit.message;
|
||||
|
@ -226,7 +212,7 @@ const RepoHelper = {
|
|||
type,
|
||||
name,
|
||||
url,
|
||||
icon: RepoHelper.toFA(icon),
|
||||
icon: `fa-${icon}`,
|
||||
level: 0,
|
||||
loading: false,
|
||||
};
|
||||
|
@ -244,42 +230,24 @@ const RepoHelper = {
|
|||
setTimeout(() => {
|
||||
const tabs = document.getElementById('tabs');
|
||||
if (!tabs) return;
|
||||
tabs.scrollLeft = 12000;
|
||||
tabs.scrollLeft = tabs.scrollWidth;
|
||||
}, 200);
|
||||
},
|
||||
|
||||
dataToListOfFiles(data) {
|
||||
const a = [];
|
||||
|
||||
// push in blobs
|
||||
data.blobs.forEach((blob) => {
|
||||
a.push(RepoHelper.serializeBlob(blob));
|
||||
});
|
||||
|
||||
data.trees.forEach((tree) => {
|
||||
a.push(RepoHelper.serializeTree(tree));
|
||||
});
|
||||
|
||||
data.submodules.forEach((submodule) => {
|
||||
a.push(RepoHelper.serializeSubmodule(submodule));
|
||||
});
|
||||
|
||||
return a;
|
||||
const { blobs, trees, submodules } = data;
|
||||
return [
|
||||
...blobs.map(blob => RepoHelper.serializeBlob(blob)),
|
||||
...trees.map(tree => RepoHelper.serializeTree(tree)),
|
||||
...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)),
|
||||
];
|
||||
},
|
||||
|
||||
genKey() {
|
||||
return RepoHelper.Time.now().toFixed(3);
|
||||
},
|
||||
|
||||
getStateKey() {
|
||||
return RepoHelper.key;
|
||||
},
|
||||
|
||||
setStateKey(key) {
|
||||
RepoHelper.key = key;
|
||||
},
|
||||
|
||||
toURL(url, title) {
|
||||
updateHistoryEntry(url, title) {
|
||||
const history = window.history;
|
||||
|
||||
RepoHelper.key = RepoHelper.genKey();
|
||||
|
@ -296,7 +264,7 @@ const RepoHelper = {
|
|||
},
|
||||
|
||||
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';
|
|||
import Translate from '../vue_shared/translate';
|
||||
|
||||
function initDropdowns() {
|
||||
$('.project-refs-target-form').hide();
|
||||
$('.fa-long-arrow-right').hide();
|
||||
$('.js-tree-ref-target-holder').hide();
|
||||
}
|
||||
|
||||
function addEventsForNonVueEls() {
|
||||
|
@ -34,6 +33,8 @@ function setInitialStore(data) {
|
|||
Store.projectId = data.projectId;
|
||||
Store.projectName = data.projectName;
|
||||
Store.projectUrl = data.projectUrl;
|
||||
Store.canCommit = data.canCommit;
|
||||
Store.onTopOfBranch = data.onTopOfBranch;
|
||||
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
|
||||
Store.checkIsCommitable();
|
||||
}
|
||||
|
@ -44,6 +45,9 @@ function initRepo(el) {
|
|||
components: {
|
||||
repo: Repo,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('repo');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import axios from 'axios';
|
||||
import Store from '../stores/repo_store';
|
||||
import Api from '../../api';
|
||||
import Helper from '../helpers/repo_helper';
|
||||
|
||||
const RepoService = {
|
||||
url: '',
|
||||
|
@ -12,16 +13,9 @@ const RepoService = {
|
|||
},
|
||||
richExtensionRegExp: /md/,
|
||||
|
||||
checkCurrentBranchIsCommitable() {
|
||||
const url = Store.service.refsUrl;
|
||||
return axios.get(url, { params: {
|
||||
ref: Store.currentBranch,
|
||||
search: Store.currentBranch,
|
||||
} });
|
||||
},
|
||||
|
||||
getRaw(url) {
|
||||
return axios.get(url, {
|
||||
// Stop Axios from parsing a JSON file into a JS object
|
||||
transformResponse: [res => res],
|
||||
});
|
||||
},
|
||||
|
@ -36,7 +30,7 @@ const RepoService = {
|
|||
},
|
||||
|
||||
urlIsRichBlob(url = this.url) {
|
||||
const extension = url.split('.').pop();
|
||||
const extension = Helper.getFileExtension(url);
|
||||
|
||||
return this.richExtensionRegExp.test(extension);
|
||||
},
|
||||
|
@ -73,7 +67,11 @@ const RepoService = {
|
|||
|
||||
commitFiles(payload, cb) {
|
||||
Api.commitMultiple(Store.projectId, payload, (data) => {
|
||||
Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
|
||||
if (data.short_id && data.stats) {
|
||||
Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
|
||||
} else {
|
||||
Flash(data.message);
|
||||
}
|
||||
cb();
|
||||
});
|
||||
},
|
||||
|
|
|
@ -3,13 +3,11 @@ import Helper from '../helpers/repo_helper';
|
|||
import Service from '../services/repo_service';
|
||||
|
||||
const RepoStore = {
|
||||
ideEl: {},
|
||||
monaco: {},
|
||||
monacoLoading: false,
|
||||
monacoInstance: {},
|
||||
service: '',
|
||||
editor: '',
|
||||
sidebar: '',
|
||||
canCommit: false,
|
||||
onTopOfBranch: false,
|
||||
editMode: false,
|
||||
isTree: false,
|
||||
isRoot: false,
|
||||
|
@ -17,19 +15,10 @@ const RepoStore = {
|
|||
projectId: '',
|
||||
projectName: '',
|
||||
projectUrl: '',
|
||||
trees: [],
|
||||
blobs: [],
|
||||
submodules: [],
|
||||
blobRaw: '',
|
||||
blobRendered: '',
|
||||
currentBlobView: 'repo-preview',
|
||||
openedFiles: [],
|
||||
tabSize: 100,
|
||||
defaultTabSize: 100,
|
||||
minTabSize: 30,
|
||||
tabsOverflow: 41,
|
||||
submitCommitsLoading: false,
|
||||
binaryLoaded: false,
|
||||
dialog: {
|
||||
open: false,
|
||||
title: '',
|
||||
|
@ -45,9 +34,6 @@ const RepoStore = {
|
|||
currentBranch: '',
|
||||
targetBranch: 'new-branch',
|
||||
commitMessage: '',
|
||||
binaryMimeType: '',
|
||||
// scroll bar space for windows
|
||||
scrollWidth: 0,
|
||||
binaryTypes: {
|
||||
png: false,
|
||||
md: false,
|
||||
|
@ -58,7 +44,6 @@ const RepoStore = {
|
|||
tree: false,
|
||||
blob: false,
|
||||
},
|
||||
readOnly: true,
|
||||
|
||||
resetBinaryTypes() {
|
||||
Object.keys(RepoStore.binaryTypes).forEach((key) => {
|
||||
|
@ -68,14 +53,7 @@ const RepoStore = {
|
|||
|
||||
// mutations
|
||||
checkIsCommitable() {
|
||||
RepoStore.service.checkCurrentBranchIsCommitable()
|
||||
.then((data) => {
|
||||
// you shouldn't be able to make commits on commits or tags.
|
||||
const { Branches, Commits, Tags } = data.data;
|
||||
if (Branches && Branches.length) RepoStore.isCommitable = true;
|
||||
if (Commits && Commits.length) RepoStore.isCommitable = false;
|
||||
if (Tags && Tags.length) RepoStore.isCommitable = false;
|
||||
}).catch(() => Flash('Failed to check if branch can be committed to.'));
|
||||
RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
|
||||
},
|
||||
|
||||
addFilesToDirectory(inDirectory, currentList, newList) {
|
||||
|
@ -96,7 +74,6 @@ const RepoStore = {
|
|||
|
||||
if (file.binary) {
|
||||
RepoStore.blobRaw = file.base64;
|
||||
RepoStore.binaryMimeType = file.mime_type;
|
||||
} else if (file.newContent || file.plain) {
|
||||
RepoStore.blobRaw = file.newContent || file.plain;
|
||||
} else {
|
||||
|
@ -107,7 +84,7 @@ const RepoStore = {
|
|||
}).catch(Helper.loadingError);
|
||||
}
|
||||
|
||||
if (!file.loading) Helper.toURL(file.url, file.name);
|
||||
if (!file.loading) Helper.updateHistoryEntry(file.url, file.name);
|
||||
RepoStore.binary = file.binary;
|
||||
},
|
||||
|
||||
|
@ -134,15 +111,15 @@ const RepoStore = {
|
|||
removeChildFilesOfTree(tree) {
|
||||
let foundTree = false;
|
||||
const treeToClose = tree;
|
||||
let wereDone = false;
|
||||
let canStopSearching = false;
|
||||
RepoStore.files = RepoStore.files.filter((file) => {
|
||||
const isItTheTreeWeWant = file.url === treeToClose.url;
|
||||
// if it's the next tree
|
||||
if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) {
|
||||
wereDone = true;
|
||||
canStopSearching = true;
|
||||
return true;
|
||||
}
|
||||
if (wereDone) return true;
|
||||
if (canStopSearching) return true;
|
||||
|
||||
if (isItTheTreeWeWant) foundTree = true;
|
||||
|
||||
|
@ -159,8 +136,8 @@ const RepoStore = {
|
|||
if (file.type === 'tree') return;
|
||||
let foundIndex;
|
||||
RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => {
|
||||
if (openedFile.url === file.url) foundIndex = i;
|
||||
return openedFile.url !== file.url;
|
||||
if (openedFile.path === file.path) foundIndex = i;
|
||||
return openedFile.path !== file.path;
|
||||
});
|
||||
|
||||
// now activate the right tab based on what you closed.
|
||||
|
@ -174,36 +151,16 @@ const RepoStore = {
|
|||
return;
|
||||
}
|
||||
|
||||
if (foundIndex) {
|
||||
if (foundIndex > 0) {
|
||||
RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]);
|
||||
}
|
||||
if (foundIndex && foundIndex > 0) {
|
||||
RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]);
|
||||
}
|
||||
},
|
||||
|
||||
addPlaceholderFile() {
|
||||
const randomURL = Helper.Time.now();
|
||||
const newFakeFile = {
|
||||
active: false,
|
||||
binary: true,
|
||||
type: 'blob',
|
||||
loading: true,
|
||||
mime_type: 'loading',
|
||||
name: 'loading',
|
||||
url: randomURL,
|
||||
fake: true,
|
||||
};
|
||||
|
||||
RepoStore.openedFiles.push(newFakeFile);
|
||||
|
||||
return newFakeFile;
|
||||
},
|
||||
|
||||
addToOpenedFiles(file) {
|
||||
const openFile = file;
|
||||
|
||||
const openedFilesAlreadyExists = RepoStore.openedFiles
|
||||
.some(openedFile => openedFile.url === openFile.url);
|
||||
.some(openedFile => openedFile.path === openFile.path);
|
||||
|
||||
if (openedFilesAlreadyExists) return;
|
||||
|
||||
|
@ -238,4 +195,5 @@ const RepoStore = {
|
|||
return RepoStore.currentBlobView === 'repo-preview';
|
||||
},
|
||||
};
|
||||
|
||||
export default RepoStore;
|
||||
|
|
|
@ -71,7 +71,7 @@ export default {
|
|||
/>
|
||||
<div v-if="!isConfidential" class="no-value confidential-value">
|
||||
<i class="fa fa-eye is-not-confidential"></i>
|
||||
None
|
||||
This issue is not confidential
|
||||
</div>
|
||||
<div v-else class="value confidential-value hide-collapsed">
|
||||
<i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i>
|
||||
|
|
|
@ -1,31 +1,37 @@
|
|||
<script>
|
||||
const PopupDialog = {
|
||||
export default {
|
||||
name: 'popup-dialog',
|
||||
|
||||
props: {
|
||||
open: Boolean,
|
||||
title: String,
|
||||
body: String,
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
body: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
kind: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'primary',
|
||||
},
|
||||
closeButtonLabel: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'Cancel',
|
||||
},
|
||||
primaryButtonLabel: {
|
||||
type: String,
|
||||
default: 'Save changes',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
typeOfClass() {
|
||||
const className = `btn-${this.kind}`;
|
||||
const returnObj = {};
|
||||
returnObj[className] = true;
|
||||
return returnObj;
|
||||
btnKindClass() {
|
||||
return {
|
||||
[`btn-${this.kind}`]: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -33,33 +39,45 @@ const PopupDialog = {
|
|||
close() {
|
||||
this.$emit('toggle', false);
|
||||
},
|
||||
|
||||
yesClick() {
|
||||
this.$emit('submit', true);
|
||||
},
|
||||
|
||||
noClick() {
|
||||
this.$emit('submit', false);
|
||||
emitSubmit(status) {
|
||||
this.$emit('submit', status);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default PopupDialog;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="modal popup-dialog" tabindex="-1" v-show="open" role="dialog">
|
||||
<div
|
||||
class="modal popup-dialog"
|
||||
role="dialog"
|
||||
tabindex="-1">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" @click="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<button type="button"
|
||||
class="close"
|
||||
@click="close"
|
||||
aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">{{this.title}}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{this.body}}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" @click="noClick">{{closeButtonLabel}}</button>
|
||||
<button type="button" class="btn" :class="typeOfClass" @click="yesClick">{{primaryButtonLabel}}</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
@click="emitSubmit(false)">
|
||||
{{closeButtonLabel}}
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn"
|
||||
:class="btnKindClass"
|
||||
@click="emitSubmit(true)">
|
||||
{{primaryButtonLabel}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -187,3 +187,81 @@ a {
|
|||
.fade-in-full {
|
||||
animation: fadeInFull $fade-in-duration 1;
|
||||
}
|
||||
|
||||
|
||||
.animation-container {
|
||||
background: $repo-editor-grey;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&.animation-container-small {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
animation-duration: 1s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: blockTextShine;
|
||||
animation-timing-function: linear;
|
||||
background-image: $repo-editor-linear-gradient;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 800px 45px;
|
||||
content: ' ';
|
||||
display: block;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div {
|
||||
background: $white-light;
|
||||
height: 6px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.skeleton-line-1 {
|
||||
left: 0;
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
.skeleton-line-2 {
|
||||
left: 150px;
|
||||
top: 0;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.skeleton-line-3 {
|
||||
left: 0;
|
||||
top: 23px;
|
||||
}
|
||||
|
||||
.skeleton-line-4 {
|
||||
left: 0;
|
||||
top: 38px;
|
||||
}
|
||||
|
||||
.skeleton-line-5 {
|
||||
left: 200px;
|
||||
top: 28px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.skeleton-line-6 {
|
||||
top: 14px;
|
||||
left: 230px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blockTextShine {
|
||||
0% {
|
||||
transform: translateX(-468px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(468px);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,10 +117,6 @@ body {
|
|||
margin-top: $header-height + $performance-bar-height;
|
||||
}
|
||||
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vertical-center {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
|
|
|
@ -403,6 +403,7 @@ header.navbar-gitlab-new {
|
|||
}
|
||||
|
||||
.breadcrumbs-extra {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
|
|
@ -250,32 +250,13 @@ $new-sidebar-collapsed-width: 50px;
|
|||
position: absolute;
|
||||
top: -30px;
|
||||
bottom: -30px;
|
||||
left: 0;
|
||||
left: -10px;
|
||||
right: -30px;
|
||||
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 {
|
||||
margin-top: 1px;
|
||||
|
||||
&::after {
|
||||
top: auto;
|
||||
bottom: 44px;
|
||||
transform: skew(-30deg);
|
||||
}
|
||||
}
|
||||
|
||||
> .active {
|
||||
|
@ -322,8 +303,7 @@ $new-sidebar-collapsed-width: 50px;
|
|||
}
|
||||
}
|
||||
|
||||
&:not(.active):hover > a,
|
||||
> a:hover,
|
||||
&.active > a:hover,
|
||||
&.is-over > a {
|
||||
background-color: $white-light;
|
||||
}
|
||||
|
|
|
@ -286,6 +286,10 @@
|
|||
|
||||
|
||||
.gpg-status-box {
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.valid {
|
||||
@include green-status-color;
|
||||
}
|
||||
|
|
|
@ -560,9 +560,13 @@
|
|||
}
|
||||
|
||||
.diff-files-changed {
|
||||
.inline-parallel-buttons {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.commit-stat-summary {
|
||||
@include new-style-dropdown;
|
||||
z-index: -1;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
margin-left: -$gl-padding;
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
.is-confidential {
|
||||
color: $orange-600;
|
||||
background-color: $orange-50;
|
||||
border-radius: 3px;
|
||||
border-radius: $border-radius-default;
|
||||
padding: 5px;
|
||||
margin: 0 3px 0 -4px;
|
||||
}
|
||||
|
||||
.is-not-confidential {
|
||||
border-radius: 3px;
|
||||
border-radius: $border-radius-default;
|
||||
padding: 5px;
|
||||
margin: 0 3px 0 -4px;
|
||||
}
|
||||
|
|
|
@ -453,7 +453,10 @@ ul.notes {
|
|||
}
|
||||
|
||||
.note-actions {
|
||||
align-self: flex-start;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
// For PhantomJS that does not support flex
|
||||
float: right;
|
||||
margin-left: 10px;
|
||||
|
@ -463,18 +466,12 @@ ul.notes {
|
|||
float: none;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.note-action-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.more-actions-toggle {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.more-actions {
|
||||
display: inline-block;
|
||||
float: right; // phantomjs fallback
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
.tooltip {
|
||||
white-space: nowrap;
|
||||
|
@ -482,16 +479,10 @@ ul.notes {
|
|||
}
|
||||
|
||||
.more-actions-toggle {
|
||||
padding: 0;
|
||||
|
||||
&:hover .icon,
|
||||
&:focus .icon {
|
||||
color: $blue-600;
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.more-actions-dropdown {
|
||||
|
@ -519,28 +510,42 @@ ul.notes {
|
|||
@include notes-media('max', $screen-md-max) {
|
||||
float: none;
|
||||
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 {
|
||||
display: inline;
|
||||
line-height: 20px;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
min-width: 16px;
|
||||
color: $gray-darkest;
|
||||
|
||||
.fa {
|
||||
color: $gray-darkest;
|
||||
position: relative;
|
||||
font-size: 17px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
svg {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
fill: $gray-darkest;
|
||||
top: 0;
|
||||
vertical-align: text-top;
|
||||
|
||||
path {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.award-control-icon-positive,
|
||||
|
@ -613,10 +618,7 @@ ul.notes {
|
|||
|
||||
.note-role {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
display: inline-block;
|
||||
padding-left: 7px;
|
||||
padding-right: 7px;
|
||||
padding: 0 7px;
|
||||
color: $notes-role-color;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
|
|
|
@ -824,6 +824,7 @@ button.mini-pipeline-graph-dropdown-toggle {
|
|||
* Top arrow in the dropdown in the mini pipeline graph
|
||||
*/
|
||||
.mini-pipeline-graph-dropdown-menu {
|
||||
z-index: 200;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity .5s;
|
||||
transition: opacity $sidebar-transition-duration;
|
||||
}
|
||||
|
||||
.monaco-loader {
|
||||
|
@ -28,11 +28,6 @@
|
|||
.project-refs-form,
|
||||
.project-refs-target-form {
|
||||
display: inline-block;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
|
@ -90,7 +85,7 @@
|
|||
}
|
||||
|
||||
.blob-viewer-container {
|
||||
height: calc(100vh - 63px);
|
||||
height: calc(100vh - 62px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
@ -114,6 +109,7 @@
|
|||
border-right: 1px solid $white-dark;
|
||||
border-bottom: 1px solid $white-dark;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
&.remove {
|
||||
animation: swipeRightDissapear ease-in 0.1s;
|
||||
|
@ -133,10 +129,10 @@
|
|||
a {
|
||||
@include str-truncated(100px);
|
||||
color: $black;
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
text-decoration: none;
|
||||
|
||||
&.close {
|
||||
width: auto;
|
||||
|
@ -146,15 +142,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
i.fa.fa-times,
|
||||
i.fa.fa-circle {
|
||||
.close-icon,
|
||||
.unsaved-icon {
|
||||
float: right;
|
||||
margin-top: 3px;
|
||||
margin-left: 15px;
|
||||
color: $gray-darkest;
|
||||
}
|
||||
|
||||
i.fa.fa-circle {
|
||||
.unsaved-icon {
|
||||
color: $brand-success;
|
||||
}
|
||||
|
||||
|
@ -204,7 +200,7 @@
|
|||
background: $gray-light;
|
||||
padding: 20px;
|
||||
|
||||
span.help-block {
|
||||
.help-block {
|
||||
padding-top: 7px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
@ -232,6 +228,7 @@
|
|||
vertical-align: top;
|
||||
width: 20%;
|
||||
border-right: 1px solid $white-normal;
|
||||
min-height: 475px;
|
||||
height: calc(100vh + 20px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
@ -261,7 +258,6 @@
|
|||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
color: $gray-darkest;
|
||||
width: 185px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -270,7 +266,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.fa {
|
||||
.file-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
|
@ -280,118 +276,22 @@
|
|||
}
|
||||
|
||||
a {
|
||||
@include str-truncated(250px);
|
||||
color: $almost-black;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid $border-gray-normal;
|
||||
padding: 10px 20px;
|
||||
|
||||
a {
|
||||
color: $almost-black;
|
||||
}
|
||||
|
||||
.fa {
|
||||
font-size: $code_font_size;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.animation-container {
|
||||
background: $repo-editor-grey;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&.animation-container-small {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
animation-duration: 1s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: blockTextShine;
|
||||
animation-timing-function: linear;
|
||||
background-image: $repo-editor-linear-gradient;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 800px 45px;
|
||||
content: ' ';
|
||||
display: block;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div {
|
||||
background: $white-light;
|
||||
height: 6px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.line-of-code-1 {
|
||||
left: 0;
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
.line-of-code-2 {
|
||||
left: 150px;
|
||||
top: 0;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.line-of-code-3 {
|
||||
left: 0;
|
||||
top: 23px;
|
||||
}
|
||||
|
||||
.line-of-code-4 {
|
||||
left: 0;
|
||||
top: 38px;
|
||||
}
|
||||
|
||||
.line-of-code-5 {
|
||||
left: 200px;
|
||||
top: 28px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.line-of-code-6 {
|
||||
top: 14px;
|
||||
left: 230px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.render-error {
|
||||
min-height: calc(100vh - 63px);
|
||||
min-height: calc(100vh - 62px);
|
||||
|
||||
p {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blockTextShine {
|
||||
0% {
|
||||
transform: translateX(-468px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(468px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes swipeRightAppear {
|
||||
0% {
|
||||
transform: scaleX(0.00);
|
||||
|
|
|
@ -29,6 +29,10 @@
|
|||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.tree-ref-target-holder {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.repo-breadcrumb {
|
||||
li:last-of-type {
|
||||
position: relative;
|
||||
|
|
|
@ -6,6 +6,13 @@ module CycleAnalyticsParams
|
|||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -6,7 +6,7 @@ class Explore::ProjectsController < Explore::ApplicationController
|
|||
def index
|
||||
params[:sort] ||= 'latest_activity_desc'
|
||||
@sort = params[:sort]
|
||||
@projects = load_projects.page(params[:page])
|
||||
@projects = load_projects
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
|
|||
def trending
|
||||
params[:trending] = true
|
||||
@sort = params[:sort]
|
||||
@projects = load_projects.page(params[:page])
|
||||
@projects = load_projects
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
@ -34,7 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController
|
|||
end
|
||||
|
||||
def starred
|
||||
@projects = load_projects.reorder('star_count DESC').page(params[:page])
|
||||
@projects = load_projects.reorder('star_count DESC')
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
@ -50,6 +50,9 @@ class Explore::ProjectsController < Explore::ApplicationController
|
|||
|
||||
def load_projects
|
||||
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
|
||||
|
|
|
@ -198,6 +198,10 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
json = blob_json(@blob)
|
||||
return render_404 unless json
|
||||
|
||||
path_segments = @path.split('/')
|
||||
path_segments.pop
|
||||
tree_path = path_segments.join('/')
|
||||
|
||||
render json: json.merge(
|
||||
path: blob.path,
|
||||
name: blob.name,
|
||||
|
@ -212,6 +216,7 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
raw_path: project_raw_path(project, @id),
|
||||
blame_path: project_blame_path(project, @id),
|
||||
commits_path: project_commits_path(project, @id),
|
||||
tree_path: project_tree_path(project, File.join(@ref, tree_path)),
|
||||
permalink: project_blob_path(project, File.join(@commit.id, @path))
|
||||
)
|
||||
end
|
||||
|
|
|
@ -212,7 +212,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
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
|
||||
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
|
||||
|
|
|
@ -59,7 +59,7 @@ module GroupsHelper
|
|||
end
|
||||
|
||||
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 }
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
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
|
|||
end
|
||||
|
||||
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 }
|
||||
end
|
||||
|
||||
|
@ -234,6 +234,8 @@ module ProjectsHelper
|
|||
# If no limit is applied we'll just issue a COUNT since the result set could
|
||||
# be too large to load into memory.
|
||||
def any_projects?(projects)
|
||||
return projects.any? if projects.is_a?(Array)
|
||||
|
||||
if projects.limit_value
|
||||
projects.to_a.any?
|
||||
else
|
||||
|
|
|
@ -11,11 +11,11 @@ module Emails
|
|||
@member_source_type = member_source_type
|
||||
@member_id = member_id
|
||||
|
||||
admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email)
|
||||
admins = member_source.members.owners_and_masters.pluck(:notification_email)
|
||||
# A project in a group can have no explicit owners/masters, in that case
|
||||
# we fallbacks to the group's owners/masters.
|
||||
if admins.empty? && member_source.respond_to?(:group) && member_source.group
|
||||
admins = member_source.group.members.owners_and_masters.includes(:user).pluck(:notification_email)
|
||||
admins = member_source.group.members.owners_and_masters.pluck(:notification_email)
|
||||
end
|
||||
|
||||
mail(to: admins,
|
||||
|
|
|
@ -212,21 +212,39 @@ class Group < Namespace
|
|||
end
|
||||
|
||||
def user_ids_for_project_authorizations
|
||||
users_with_parents.pluck(:id)
|
||||
members_with_parents.pluck(:user_id)
|
||||
end
|
||||
|
||||
def members_with_parents
|
||||
GroupMember.active.where(source_id: ancestors.pluck(:id).push(id)).where.not(user_id: nil)
|
||||
# Avoids an unnecessary SELECT when the group has no parents
|
||||
source_ids =
|
||||
if parent_id
|
||||
self_and_ancestors.reorder(nil).select(:id)
|
||||
else
|
||||
id
|
||||
end
|
||||
|
||||
GroupMember
|
||||
.active_without_invites
|
||||
.where(source_id: source_ids)
|
||||
end
|
||||
|
||||
def members_with_descendants
|
||||
GroupMember
|
||||
.active_without_invites
|
||||
.where(source_id: self_and_descendants.reorder(nil).select(:id))
|
||||
end
|
||||
|
||||
def users_with_parents
|
||||
User.where(id: members_with_parents.select(:user_id))
|
||||
User
|
||||
.where(id: members_with_parents.select(:user_id))
|
||||
.reorder(nil)
|
||||
end
|
||||
|
||||
def users_with_descendants
|
||||
members_with_descendants = GroupMember.non_request.where(source_id: descendants.pluck(:id).push(id))
|
||||
|
||||
User.where(id: members_with_descendants.select(:user_id))
|
||||
User
|
||||
.where(id: members_with_descendants.select(:user_id))
|
||||
.reorder(nil)
|
||||
end
|
||||
|
||||
def max_member_access_for_user(user)
|
||||
|
|
|
@ -41,9 +41,20 @@ class Member < ActiveRecord::Base
|
|||
is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil))
|
||||
user_is_active = User.arel_table[:state].eq(:active)
|
||||
|
||||
includes(:user).references(:users)
|
||||
.where(is_external_invite.or(user_is_active))
|
||||
user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_active)
|
||||
|
||||
left_join_users
|
||||
.where(user_ok)
|
||||
.where(requested_at: nil)
|
||||
.reorder(nil)
|
||||
end
|
||||
|
||||
# Like active, but without invites. For when a User is required.
|
||||
scope :active_without_invites, -> do
|
||||
left_join_users
|
||||
.where(users: { state: 'active' })
|
||||
.where(requested_at: nil)
|
||||
.reorder(nil)
|
||||
end
|
||||
|
||||
scope :invite, -> { where.not(invite_token: nil) }
|
||||
|
@ -276,6 +287,13 @@ class Member < ActiveRecord::Base
|
|||
@notification_setting ||= user.notification_settings_for(source)
|
||||
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
|
||||
|
||||
def send_invite
|
||||
|
@ -332,4 +350,8 @@ class Member < ActiveRecord::Base
|
|||
def notification_service
|
||||
NotificationService.new
|
||||
end
|
||||
|
||||
def notifiable_options
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,6 +30,10 @@ class GroupMember < Member
|
|||
'Group'
|
||||
end
|
||||
|
||||
def notifiable_options
|
||||
{ group: group }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_invite
|
||||
|
|
|
@ -87,6 +87,10 @@ class ProjectMember < Member
|
|||
project.owner == user
|
||||
end
|
||||
|
||||
def notifiable_options
|
||||
{ project: project }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def delete_member_todos
|
||||
|
|
|
@ -443,7 +443,8 @@ class MergeRequest < ActiveRecord::Base
|
|||
end
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
@ -792,11 +793,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def fetch_ref
|
||||
target_project.repository.fetch_ref(
|
||||
source_project.repository.path_to_repo,
|
||||
"refs/heads/#{source_branch}",
|
||||
ref_path
|
||||
)
|
||||
write_ref
|
||||
update_column(:ref_fetched, true)
|
||||
end
|
||||
|
||||
|
@ -939,4 +936,17 @@ class MergeRequest < ActiveRecord::Base
|
|||
|
||||
true
|
||||
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
|
||||
|
|
|
@ -156,6 +156,14 @@ class Namespace < ActiveRecord::Base
|
|||
.base_and_ancestors
|
||||
end
|
||||
|
||||
def self_and_ancestors
|
||||
return self.class.where(id: id) unless parent_id
|
||||
|
||||
Gitlab::GroupHierarchy
|
||||
.new(self.class.where(id: id))
|
||||
.base_and_ancestors
|
||||
end
|
||||
|
||||
# Returns all the descendants of the current namespace.
|
||||
def descendants
|
||||
Gitlab::GroupHierarchy
|
||||
|
@ -163,6 +171,12 @@ class Namespace < ActiveRecord::Base
|
|||
.base_and_descendants
|
||||
end
|
||||
|
||||
def self_and_descendants
|
||||
Gitlab::GroupHierarchy
|
||||
.new(self.class.where(id: id))
|
||||
.base_and_descendants
|
||||
end
|
||||
|
||||
def user_ids_for_project_authorizations
|
||||
[owner_id]
|
||||
end
|
||||
|
|
|
@ -5,14 +5,22 @@ class NotificationRecipient
|
|||
custom_action: nil,
|
||||
target: 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
|
||||
@acting_user = acting_user
|
||||
@target = target
|
||||
@project = project || @target&.project
|
||||
@project = project || default_project
|
||||
@group = group || @project&.group
|
||||
@user = user
|
||||
@type = type
|
||||
@skip_read_ability = skip_read_ability
|
||||
end
|
||||
|
||||
def notification_setting
|
||||
|
@ -77,6 +85,8 @@ class NotificationRecipient
|
|||
def has_access?
|
||||
DeclarativePolicy.subject_scope do
|
||||
return false unless user.can?(:receive_notifications)
|
||||
return true if @skip_read_ability
|
||||
|
||||
return false if @project && !user.can?(:read_project, @project)
|
||||
|
||||
return true unless read_ability
|
||||
|
@ -96,6 +106,7 @@ class NotificationRecipient
|
|||
private
|
||||
|
||||
def read_ability
|
||||
return nil if @skip_read_ability
|
||||
return @read_ability if instance_variable_defined?(:@read_ability)
|
||||
|
||||
@read_ability =
|
||||
|
@ -111,12 +122,18 @@ class NotificationRecipient
|
|||
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
|
||||
project_setting = @project && user.notification_settings_for(@project)
|
||||
|
||||
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?
|
||||
|
||||
|
|
|
@ -196,7 +196,6 @@ class Project < ActiveRecord::Base
|
|||
accepts_nested_attributes_for :import_data
|
||||
|
||||
delegate :name, to: :owner, allow_nil: true, prefix: true
|
||||
delegate :count, to: :forks, prefix: true
|
||||
delegate :members, to: :team, prefix: true
|
||||
delegate :add_user, :add_users, to: :team
|
||||
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
|
||||
|
@ -1048,9 +1047,7 @@ class Project < ActiveRecord::Base
|
|||
def change_head(branch)
|
||||
if repository.branch_exists?(branch)
|
||||
repository.before_change_head
|
||||
repository.rugged.references.create('HEAD',
|
||||
"refs/heads/#{branch}",
|
||||
force: true)
|
||||
repository.write_ref('HEAD', "refs/heads/#{branch}")
|
||||
repository.copy_gitattributes(branch)
|
||||
repository.after_change_head
|
||||
reload_default_branch
|
||||
|
@ -1398,6 +1395,10 @@ class Project < ActiveRecord::Base
|
|||
# @deprecated cannot remove yet because it has an index with its name in elasticsearch
|
||||
alias_method :path_with_namespace, :full_path
|
||||
|
||||
def forks_count
|
||||
Projects::ForksCountService.new(self).count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cross_namespace_reference?(from)
|
||||
|
|
|
@ -224,7 +224,7 @@ class Repository
|
|||
|
||||
# This will still fail if the file is corrupted (e.g. 0 bytes)
|
||||
begin
|
||||
rugged.references.create(keep_around_ref_name(sha), sha, force: true)
|
||||
write_ref(keep_around_ref_name(sha), sha)
|
||||
rescue Rugged::ReferenceError => ex
|
||||
Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
|
||||
rescue Rugged::OSError => ex
|
||||
|
@ -237,6 +237,10 @@ class Repository
|
|||
ref_exists?(keep_around_ref_name(sha))
|
||||
end
|
||||
|
||||
def write_ref(ref_path, sha)
|
||||
rugged.references.create(ref_path, sha, force: true)
|
||||
end
|
||||
|
||||
def diverging_commit_counts(branch)
|
||||
root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
|
||||
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
|
||||
|
@ -985,12 +989,10 @@ class Repository
|
|||
if start_repository == self
|
||||
start_branch_name
|
||||
else
|
||||
tmp_ref = "refs/tmp/#{SecureRandom.hex}/head"
|
||||
|
||||
fetch_ref(
|
||||
tmp_ref = fetch_ref(
|
||||
start_repository.path_to_repo,
|
||||
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
|
||||
tmp_ref
|
||||
"refs/tmp/#{SecureRandom.hex}/head"
|
||||
)
|
||||
|
||||
start_repository.commit(start_branch_name).sha
|
||||
|
@ -1021,7 +1023,12 @@ class Repository
|
|||
|
||||
def fetch_ref(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
|
||||
|
||||
def create_ref(ref, ref_path)
|
||||
|
|
|
@ -726,9 +726,9 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def sanitize_attrs
|
||||
%w[username skype linkedin twitter].each do |attr|
|
||||
value = public_send(attr) # rubocop:disable GitlabSecurity/PublicSend
|
||||
public_send("#{attr}=", Sanitize.clean(value)) if value.present? # rubocop:disable GitlabSecurity/PublicSend
|
||||
%i[skype linkedin twitter].each do |attr|
|
||||
value = self[attr]
|
||||
self[attr] = Sanitize.clean(value) if value.present?
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1069,6 +1069,7 @@ class User < ActiveRecord::Base
|
|||
|
||||
# Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
|
||||
def send_devise_notification(notification, *args)
|
||||
return true unless can?(:receive_notifications)
|
||||
devise_mailer.send(notification, self, *args).deliver_later
|
||||
end
|
||||
|
||||
|
|
|
@ -1,8 +1,21 @@
|
|||
# TODO: Inherit from TreeEntity, when `Tree` implements `id` and `name` like `Gitlab::Git::Tree`.
|
||||
class TreeRootEntity < Grape::Entity
|
||||
include RequestAwareEntity
|
||||
|
||||
expose :path
|
||||
|
||||
|
||||
expose :trees, using: TreeEntity
|
||||
expose :blobs, using: BlobEntity
|
||||
expose :submodules, using: SubmoduleEntity
|
||||
|
||||
expose :parent_tree_url do |tree|
|
||||
path = tree.path.sub(%r{\A/}, '')
|
||||
next unless path.present?
|
||||
|
||||
path_segments = path.split('/')
|
||||
path_segments.pop
|
||||
parent_tree_path = path_segments.join('/')
|
||||
|
||||
project_tree_path(request.project, File.join(request.ref, parent_tree_path))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,9 +10,11 @@ class NotificationService
|
|||
# only if ssh key is not deploy key
|
||||
#
|
||||
# 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)
|
||||
if key.user
|
||||
if key.user&.can?(:receive_notifications)
|
||||
mailer.new_ssh_key_email(key.id).deliver_later
|
||||
end
|
||||
end
|
||||
|
@ -22,14 +24,14 @@ class NotificationService
|
|||
# This is a security email so it will be sent even if the user user disabled
|
||||
# notifications
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
# Always notify user about email added to profile
|
||||
def new_email(email)
|
||||
if email.user
|
||||
if email.user&.can?(:receive_notifications)
|
||||
mailer.new_email_email(email.id).deliver_later
|
||||
end
|
||||
end
|
||||
|
@ -185,6 +187,8 @@ class NotificationService
|
|||
|
||||
# Notify new user with email after creation
|
||||
def new_user(user, token = nil)
|
||||
return true unless notifiable?(user, :mention)
|
||||
|
||||
# Don't email omniauth created users
|
||||
mailer.new_user_email(user.id, token).deliver_later unless user.identities.any?
|
||||
end
|
||||
|
@ -206,19 +210,27 @@ class NotificationService
|
|||
|
||||
# Members
|
||||
def new_access_request(member)
|
||||
return true unless member.notifiable?(:subscription)
|
||||
|
||||
mailer.member_access_requested_email(member.real_source_type, member.id).deliver_later
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
# Project invite
|
||||
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
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
@ -232,10 +244,14 @@ class NotificationService
|
|||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
@ -249,6 +265,9 @@ class NotificationService
|
|||
end
|
||||
|
||||
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(
|
||||
group_member.real_source_type,
|
||||
group_member.group.id,
|
||||
|
@ -258,15 +277,19 @@ class NotificationService
|
|||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
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|
|
||||
mailer.project_was_moved_email(
|
||||
|
@ -288,10 +311,14 @@ class NotificationService
|
|||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
@ -300,7 +327,7 @@ class NotificationService
|
|||
|
||||
return unless mailer.respond_to?(email_template)
|
||||
|
||||
recipients ||= NotificationRecipientService.notifiable_users(
|
||||
recipients ||= notifiable_users(
|
||||
[pipeline.user], :watch,
|
||||
custom_action: :"#{pipeline.status}_pipeline",
|
||||
target: pipeline
|
||||
|
@ -369,7 +396,7 @@ class NotificationService
|
|||
|
||||
def relabeled_resource_email(target, labels, current_user, method)
|
||||
recipients = labels.flat_map { |l| l.subscribers(target.project) }
|
||||
recipients = NotificationRecipientService.notifiable_users(
|
||||
recipients = notifiable_users(
|
||||
recipients, :subscription,
|
||||
target: target,
|
||||
acting_user: current_user
|
||||
|
@ -401,4 +428,14 @@ class NotificationService
|
|||
object.previous_changes[attribute].first
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notifiable?(*args)
|
||||
NotificationRecipientService.notifiable?(*args)
|
||||
end
|
||||
|
||||
def notifiable_users(*args)
|
||||
NotificationRecipientService.notifiable_users(*args)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -128,6 +128,8 @@ module Projects
|
|||
project.repository.before_delete
|
||||
|
||||
Repository.new(wiki_path, project, disk_path: repo_path).before_delete
|
||||
|
||||
Projects::ForksCountService.new(project).delete_cache
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,11 +21,17 @@ module Projects
|
|||
builds_access_level = @project.project_feature.builds_access_level
|
||||
new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
|
||||
|
||||
refresh_forks_count
|
||||
|
||||
new_project
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def refresh_forks_count
|
||||
Projects::ForksCountService.new(@project).refresh_cache
|
||||
end
|
||||
|
||||
def allowed_visibility_level
|
||||
project_level = @project.visibility_level
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
module Projects
|
||||
# Service class for getting and caching the number of forks of a project.
|
||||
class ForksCountService
|
||||
def initialize(project)
|
||||
@project = project
|
||||
end
|
||||
|
||||
def count
|
||||
Rails.cache.fetch(cache_key) { uncached_count }
|
||||
end
|
||||
|
||||
def refresh_cache
|
||||
Rails.cache.write(cache_key, uncached_count)
|
||||
end
|
||||
|
||||
def delete_cache
|
||||
Rails.cache.delete(cache_key)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def uncached_count
|
||||
@project.forks.count
|
||||
end
|
||||
|
||||
def cache_key
|
||||
['projects', @project.id, 'forks_count']
|
||||
end
|
||||
end
|
||||
end
|
|
@ -13,7 +13,13 @@ module Projects
|
|||
::MergeRequests::CloseService.new(@project, @current_user).execute(mr)
|
||||
end
|
||||
|
||||
refresh_forks_count(@project.forked_from_project)
|
||||
|
||||
@project.forked_project_link.destroy
|
||||
end
|
||||
|
||||
def refresh_forks_count(project)
|
||||
Projects::ForksCountService.new(project).refresh_cache
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ class PersonalFileUploader < FileUploader
|
|||
end
|
||||
|
||||
def self.base_dir
|
||||
File.join(root_dir, 'system')
|
||||
File.join(root_dir, '-', 'system')
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
.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')
|
|
@ -1,3 +1,2 @@
|
|||
- if commit.has_signature?
|
||||
%button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
|
||||
%i.fa.fa-spinner.fa-spin
|
||||
|
|
|
@ -39,6 +39,9 @@
|
|||
%span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }}
|
||||
%i.fa.fa-chevron-down
|
||||
%ul.dropdown-menu.dropdown-menu-align-right
|
||||
%li
|
||||
%a{ "href" => "#", "data-value" => "7" }
|
||||
{{ n__('Last %d day', 'Last %d days', 7) }}
|
||||
%li
|
||||
%a{ "href" => "#", "data-value" => "30" }
|
||||
{{ n__('Last %d day', 'Last %d days', 30) }}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed{ class: ("diff-files-changed-merge-request" if merge_request) }
|
||||
.files-changed-inner
|
||||
.inline-parallel-buttons
|
||||
.inline-parallel-buttons.hidden-xs.hidden-sm
|
||||
- if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? }
|
||||
= link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
|
||||
- if show_whitespace_toggle
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
%strong.cgreen #{sum_added_lines} additions
|
||||
and
|
||||
%strong.cred #{sum_removed_lines} deletions
|
||||
.diff-stats-additions-deletions-collapsed.pull-right{ "aria-hidden": "true", "aria-describedby": "diff-stats" }
|
||||
.diff-stats-additions-deletions-collapsed.pull-right.hidden-xs.hidden-sm{ "aria-hidden": "true", "aria-describedby": "diff-stats" }
|
||||
%strong.cgreen<
|
||||
+#{sum_added_lines}
|
||||
%strong.cred<
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
|
||||
= icon('rss')
|
||||
- if @can_bulk_update
|
||||
= button_tag "Edit Issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
|
||||
= button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
|
||||
= link_to "New issue", new_project_issue_path(@project,
|
||||
issue: { assignee_id: issues_finder.assignee.try(:id),
|
||||
milestone_id: issues_finder.milestones.first.try(:id) }),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- if @can_bulk_update
|
||||
= button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle"
|
||||
= button_tag "Edit merge requests", class: "btn append-right-10 js-bulk-update-toggle"
|
||||
- if merge_project
|
||||
= link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do
|
||||
New merge request
|
||||
|
|
|
@ -17,24 +17,32 @@
|
|||
"inline-template" => true,
|
||||
"ref" => "note_#{note.id}" }
|
||||
|
||||
%button.note-action-button.line-resolve-btn{ type: "button",
|
||||
class: ("is-disabled" unless can_resolve),
|
||||
":class" => "{ 'is-active': isResolved }",
|
||||
":aria-label" => "buttonText",
|
||||
"@click" => "resolve",
|
||||
":title" => "buttonText",
|
||||
":ref" => "'button'" }
|
||||
.note-actions-item
|
||||
%button.note-action-button.line-resolve-btn{ type: "button",
|
||||
class: ("is-disabled" unless can_resolve),
|
||||
":class" => "{ 'is-active': isResolved }",
|
||||
":aria-label" => "buttonText",
|
||||
"@click" => "resolve",
|
||||
":title" => "buttonText",
|
||||
":ref" => "'button'" }
|
||||
|
||||
= icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
|
||||
%div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg'
|
||||
= icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
|
||||
%div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg'
|
||||
|
||||
- if current_user
|
||||
- if note.emoji_awardable?
|
||||
- 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
|
||||
= icon('spinner spin')
|
||||
%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-super-positive' }= custom_icon('emoji_smile')
|
||||
.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')
|
||||
%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-super-positive' }= custom_icon('emoji_smile')
|
||||
|
||||
= render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
|
||||
- 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
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
- is_current_user = current_user == note.author
|
||||
|
||||
- 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
|
||||
= icon('ellipsis-v', class: 'icon')
|
||||
%span.icon
|
||||
= custom_icon('ellipsis_v')
|
||||
%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
|
||||
%li
|
||||
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
.tree-ref-holder
|
||||
= render 'shared/ref_switcher', destination: 'tree', path: @path
|
||||
- if show_new_repo?
|
||||
= icon('long-arrow-right', title: 'to target branch')
|
||||
= render 'shared/target_switcher', destination: 'tree', path: @path
|
||||
.tree-ref-target-holder.js-tree-ref-target-holder
|
||||
= icon('long-arrow-right', title: 'to target branch')
|
||||
= render 'shared/target_switcher', destination: 'tree', path: @path
|
||||
|
||||
- unless show_new_repo?
|
||||
= render 'projects/tree/old_tree_header'
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
- @options && @options.each do |key, value|
|
||||
= hidden_field_tag key, value, id: nil
|
||||
.dropdown
|
||||
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" }
|
||||
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" }
|
||||
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
|
||||
= dropdown_title _("Switch branch/tag")
|
||||
= dropdown_filter _("Search branches and tags")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- dropdown_toggle_text = @ref || @project.default_branch
|
||||
= form_tag nil, method: :get, class: "project-refs-target-form" do
|
||||
= form_tag nil, method: :get, style: { display: 'none' }, class: "project-refs-target-form" do
|
||||
= hidden_field_tag :destination, destination
|
||||
- if defined?(path)
|
||||
= hidden_field_tag :path, path
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<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>
|
After Width: | Height: | Size: 370 B |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
@ -23,6 +23,6 @@
|
|||
= icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
|
||||
%strong= pluralize(@private_forks_count, 'private fork')
|
||||
%span you have no access to.
|
||||
= paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages
|
||||
= paginate_collection(projects, remote: remote)
|
||||
- else
|
||||
.nothing-here-block No projects found
|
||||
|
|
|
@ -1,2 +1,7 @@
|
|||
#repo{ data: { url: content_url, project_name: project.name, refs_url: refs_project_path(project, format: :json), project_url: project_path(project), project_id: project.id, can_commit: (!!can_push_branch?(project, @ref)).to_s } }
|
||||
%repo
|
||||
#repo{ data: { url: content_url,
|
||||
project_name: project.name,
|
||||
refs_url: refs_project_path(project, format: :json),
|
||||
project_url: project_path(project),
|
||||
project_id: project.id,
|
||||
can_commit: (!!can_push_branch?(project, @ref)).to_s,
|
||||
on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } }
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
- if current_user
|
||||
- if note.emoji_awardable?
|
||||
- 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
|
||||
= icon('spinner spin')
|
||||
%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-super-positive' }= custom_icon('emoji_smile')
|
||||
.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
|
||||
= icon('spinner spin')
|
||||
%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-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
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: disabling notifications globally now properly turns off group/project added
|
||||
emails
|
||||
merge_request: 13325
|
||||
author: @jneen
|
||||
type: fixed
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
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
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
title: Use jQuery to control scroll behavior in job log for cross browser consistency
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: move edit comment button outside of dropdown
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix timeouts when creating projects in groups with many members
|
||||
merge_request: 13508
|
||||
author:
|
||||
type: fixed
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
title: improve file upload/replace experience
|
||||
merge_request:
|
||||
author:
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
title: fix jump to next discussion button
|
||||
merge_request:
|
||||
author:
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
title: Project pending delete no longer return 500 error in admins projects view
|
||||
merge_request: 13389
|
||||
author:
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
title: Allow any logged in users to read_users_list even if it's restricted
|
||||
merge_request: 13201
|
||||
author:
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
title: Fixes new issue button for failed job returning 404
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Prevents jobs dropdown from closing in pipeline graph
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Don't rename namespace called system when upgrading from 9.1.x to 9.5
|
||||
merge_request: 13228
|
||||
author:
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix inconsistent spacing for edit buttons on issues and merge request page
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue