Commit e243dade authored by Micaël Bergeron's avatar Micaël Bergeron

Merge remote-tracking branch 'origin/master' into ee-40781-os-to-ce

parents c34a1332 9e9748e6
......@@ -22,8 +22,8 @@ gem 'faraday', '~> 0.12'
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
gem 'doorkeeper-openid_connect', '~> 1.2.0'
gem 'doorkeeper', '~> 4.3'
gem 'doorkeeper-openid_connect', '~> 1.3'
gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.9'
......@@ -34,7 +34,7 @@ gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.5.2'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2'
gem 'omniauth-saml', '~> 1.10.0'
gem 'omniauth-saml', '~> 1.10'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
......@@ -116,7 +116,7 @@ gem 'fog-rackspace', '~> 0.1.1'
gem 'fog-aliyun', '~> 0.2.0'
# for Google storage
gem 'google-api-client', '~> 0.19'
gem 'google-api-client', '~> 0.19.8'
# for aws storage
gem 'unf', '~> 0.1.4'
......@@ -245,9 +245,6 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.5'
# Faster JSON
gem 'oj', '~> 2.17.4'
# Faster blank
gem 'fast_blank'
......@@ -291,7 +288,6 @@ gem 'batch-loader', '~> 1.2.1'
# Perf bar
gem 'peek', '~> 1.0.1'
gem 'peek-gc', '~> 0.0.2'
gem 'peek-host', '~> 1.0.0'
gem 'peek-mysql2', '~> 1.1.0', group: :mysql
gem 'peek-performance_bar', '~> 1.3.0'
gem 'peek-pg', '~> 1.3.0', group: :postgres
......
......@@ -47,6 +47,7 @@ GEM
memoizable (~> 0.4.0)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
aes_key_wrap (1.0.1)
akismet (2.0.0)
allocations (1.0.5)
arel (6.0.4)
......@@ -94,7 +95,7 @@ GEM
coderay (>= 1.0.0)
erubis (>= 2.6.6)
rack (>= 0.9.0)
bindata (2.4.1)
bindata (2.4.3)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
blankslate (2.1.2.4)
......@@ -184,10 +185,10 @@ GEM
docile (1.1.5)
domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.6)
doorkeeper (4.3.1)
railties (>= 4.2)
doorkeeper-openid_connect (1.2.0)
doorkeeper (~> 4.0)
doorkeeper-openid_connect (1.3.0)
doorkeeper (~> 4.3)
json-jwt (~> 1.6)
dropzonejs-rails (0.7.2)
rails (> 3.1)
......@@ -457,10 +458,10 @@ GEM
jmespath (1.3.1)
jquery-atwho-rails (1.3.2)
json (1.8.6)
json-jwt (1.7.2)
json-jwt (1.9.2)
activesupport
aes_key_wrap
bindata
multi_json (>= 1.3)
securecompare
url_safe_base64
json-schema (2.8.0)
......@@ -553,7 +554,6 @@ GEM
rack (>= 1.2, < 3)
octokit (4.8.0)
sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.5)
omniauth (1.4.3)
hashie (>= 1.2, < 4)
rack (>= 1.6.2, < 3)
......@@ -623,8 +623,6 @@ GEM
railties (>= 4.0.0)
peek-gc (0.0.2)
peek
peek-host (1.0.0)
peek
peek-mysql2 (1.1.0)
atomic (>= 1.0.0)
mysql2
......@@ -688,7 +686,7 @@ GEM
httpclient (>= 2.4)
multi_json (>= 1.3.6)
rack (>= 1.1)
rack-protection (1.5.3)
rack-protection (2.0.1)
rack
rack-proxy (0.6.0)
rack
......@@ -707,8 +705,8 @@ GEM
sprockets-rails
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
rails-dom-testing (1.0.8)
activesupport (>= 4.2.0.beta, < 5.0)
rails-dom-testing (1.0.9)
activesupport (>= 4.2.0, < 5.0)
nokogiri (~> 1.6)
rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3)
......@@ -1061,8 +1059,8 @@ DEPENDENCIES
devise (~> 4.2)
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.2.0)
doorkeeper-openid_connect (~> 1.2.0)
doorkeeper (~> 4.3)
doorkeeper-openid_connect (~> 1.3)
dropzonejs-rails (~> 0.7.1)
elasticsearch-api (= 5.0.3)
elasticsearch-model (~> 0.1.9)
......@@ -1103,7 +1101,7 @@ DEPENDENCIES
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.19)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
gpgme
grape (~> 1.0)
......@@ -1144,7 +1142,6 @@ DEPENDENCIES
nokogiri (~> 1.8.2)
oauth2 (~> 1.4)
octokit (~> 4.8)
oj (~> 2.17.4)
omniauth (~> 1.4.2)
omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.3.1)
......@@ -1156,14 +1153,13 @@ DEPENDENCIES
omniauth-google-oauth2 (~> 0.5.2)
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10.0)
omniauth-saml (~> 1.10)
omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12)
peek (~> 1.0.1)
peek-gc (~> 0.0.2)
peek-host (~> 1.0.0)
peek-mysql2 (~> 1.1.0)
peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0)
......
import './autosize';
import './bind_in_out';
import initCopyAsGFM from './copy_as_gfm';
import './markdown/render_gfm';
import initCopyAsGFM from './markdown/copy_as_gfm';
import initCopyToClipboard from './copy_to_clipboard';
import './details_behavior';
import installGlEmojiElement from './gl_emoji';
......
......@@ -2,8 +2,8 @@
import $ from 'jquery';
import _ from 'underscore';
import { insertText, getSelectedFragment, nodeMatchesSelector } from '../lib/utils/common_utils';
import { placeholderImage } from '../lazy_loader';
import { insertText, getSelectedFragment, nodeMatchesSelector } from '~/lib/utils/common_utils';
import { placeholderImage } from '~/lazy_loader';
const gfmRules = {
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
......
import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
import renderMath from './render_math';
import renderMermaid from './render_mermaid';
import syntaxHighlight from './syntax_highlight';
// Render Gitlab flavoured Markdown
//
......
import $ from 'jquery';
import { __ } from './locale';
import flash from './flash';
import { __ } from '~/locale';
import flash from '~/flash';
// Renders math using KaTeX in any element with the
// `js-render-math` class
......
import flash from '~/flash';
// Renders diagrams and flowcharts from text using Mermaid in any element with the
// `js-render-mermaid` class.
//
......@@ -12,8 +14,6 @@
// </pre>
//
import Flash from './flash';
export default function renderMermaid($els) {
if (!$els.length) return;
......@@ -52,6 +52,6 @@ export default function renderMermaid($els) {
});
});
}).catch((err) => {
Flash(`Can't load mermaid module: ${err}`);
flash(`Can't load mermaid module: ${err}`);
});
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */
import $ from 'jquery';
import { rstrip } from './lib/utils/common_utils';
window.ConfirmDangerModal = (function() {
function ConfirmDangerModal(form, text, arg) {
var project_path, submit, warningMessage;
warningMessage = (arg != null ? arg : {}).warningMessage;
this.form = form;
$('.js-confirm-text').html(text || '');
if (warningMessage) {
$('.js-warning-text').html(warningMessage);
function openConfirmDangerModal($form, text) {
$('.js-confirm-text').text(text || '');
$('.js-confirm-danger-input').val('');
$('#modal-confirm-danger').modal('show');
const confirmTextMatch = $('.js-confirm-danger-match').text();
const $submit = $('.js-confirm-danger-submit');
$submit.disable();
$('.js-confirm-danger-input').off('input').on('input', function handleInput() {
const confirmText = rstrip($(this).val());
if (confirmText === confirmTextMatch) {
$submit.enable();
} else {
$submit.disable();
}
$('.js-confirm-danger-input').val('');
$('#modal-confirm-danger').modal('show');
project_path = $('.js-confirm-danger-match').text();
submit = $('.js-confirm-danger-submit');
submit.disable();
$('.js-confirm-danger-input').off('input');
$('.js-confirm-danger-input').on('input', function() {
if (rstrip($(this).val()) === project_path) {
return submit.enable();
} else {
return submit.disable();
}
});
$('.js-confirm-danger-submit').off('click');
$('.js-confirm-danger-submit').on('click', (function(_this) {
return function() {
return _this.form.submit();
};
})(this));
}
});
$('.js-confirm-danger-submit').off('click').on('click', () => $form.submit());
}
return ConfirmDangerModal;
})();
export default function initConfirmDangerModal() {
$(document).on('click', '.js-confirm-danger', (e) => {
e.preventDefault();
const $btn = $(e.target);
const $form = $btn.closest('form');
const text = $btn.data('confirmDangerMessage');
openConfirmDangerModal($form, text);
});
}
......@@ -53,8 +53,12 @@ function initPageShortcuts(page) {
function initGFMInput() {
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
const gfm = new GfmAutoComplete(
gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
);
const enableGFM = convertPermissionToBoolean(
el.dataset.supportsAutocomplete,
);
gfm.setup($(el), {
emojis: true,
members: enableGFM,
......@@ -67,9 +71,9 @@ function initGFMInput() {
}
function initPerformanceBar() {
if (document.querySelector('#peek')) {
if (document.querySelector('#js-peek')) {
import('./performance_bar')
.then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap
.then(m => new m.default({ container: '#js-peek' })) // eslint-disable-line new-cap
.catch(() => Flash('Error loading performance bar module'));
}
}
......
......@@ -2,7 +2,7 @@ import $ from 'jquery';
import autosize from 'autosize';
import GfmAutoComplete from './gfm_auto_complete';
import dropzoneInput from './dropzone_input';
import textUtils from './lib/utils/text_markdown';
import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown';
export default class GLForm {
constructor(form, enableGFM = false) {
......@@ -47,7 +47,7 @@ export default class GLForm {
}
// form and textarea event listeners
this.addEventListeners();
textUtils.init(this.form);
addMarkdownListeners(this.form);
// hide discard button
this.form.find('.js-note-discard').hide();
this.form.show();
......@@ -86,7 +86,7 @@ export default class GLForm {
clearEventListeners() {
this.textarea.off('focus');
this.textarea.off('blur');
textUtils.removeListeners(this.form);
removeMarkdownListeners(this.form);
}
addEventListeners() {
......
<script>
import { mapState } from 'vuex';
import { sprintf, __ } from '~/locale';
import * as consts from 'ee/ide/stores/modules/commit/constants';
import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
export default {
......
<script>
import { mapActions } from 'vuex';
import router from 'ee/ide/ide_router';
import icon from '~/vue_shared/components/icon.vue';
import router from '../../ide_router';
export default {
components: {
......@@ -52,7 +52,7 @@
<button
type="button"
class="btn btn-blank multi-file-discard-btn"
@click="discardFileChanges(file)"
@click="discardFileChanges(file.path)"
>
Discard
</button>
......
......@@ -5,7 +5,6 @@
import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoPreview from './repo_preview.vue';
import repoEditor from './repo_editor.vue';
export default {
......@@ -16,7 +15,6 @@
repoFileButtons,
ideStatusBar,
repoEditor,
repoPreview,
},
props: {
emptyStateSvgPath: {
......@@ -33,18 +31,12 @@
},
},
computed: {
...mapState([
'currentBlobView',
'selectedFile',
'changedFiles',
]),
...mapGetters([
'activeFile',
]),
...mapState(['changedFiles', 'openFiles', 'viewer']),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = (e) => {
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, {
......@@ -67,20 +59,29 @@
<template
v-if="activeFile"
>
<repo-tabs/>
<component
<repo-tabs
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
/>
<repo-editor
class="multi-file-edit-pane-content"
:is="currentBlobView"
:file="activeFile"
/>
<repo-file-buttons
:file="activeFile"
/>
<repo-file-buttons />
<ide-status-bar
:file="selectedFile"
:file="activeFile"
/>
</template>
<template
v-else
>
<div class="ide-empty-state">
<div
v-once
class="ide-empty-state"
>
<div class="row js-empty-state">
<div class="col-xs-12">
<div class="svg-content svg-250">
......
<script>
import { mapState, mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue';
import { mapActions, mapGetters, mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue';
import ResizablePanel from './resizable_panel.vue';
export default {
components: {
repoCommitSection,
icon,
panelResizer,
export default {
components: {
repoCommitSection,
icon,
panelResizer,
ResizablePanel,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
data() {
return {
width: 340,
};
},
computed: {
...mapState([
'rightPanelCollapsed',
'changedFiles',
]),
currentIcon() {
return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
},
maxSize() {
return window.innerWidth / 2;
},
panelStyle() {
if (!this.rightPanelCollapsed) {
return { width: `${this.width}px` };
}
return {};
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
'setResizingStatus',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'right',
collapsed: !this.rightPanelCollapsed,
});
},
toggleFullbarCollapsed() {
if (this.rightPanelCollapsed) {
this.toggleCollapsed();
}
},
resizingStarted() {
this.setResizingStatus(true);
},
resizingEnded() {
this.setResizingStatus(false);
},
},
};
},
computed: {
...mapState(['changedFiles', 'rightPanelCollapsed']),
...mapGetters(['currentIcon']),
},
methods: {
...mapActions(['setPanelCollapsedStatus']),
},
};
</script>
<template>
<div
class="multi-file-commit-panel"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
:style="panelStyle"
@click="toggleFullbarCollapsed"
<resizable-panel
:collapsible="true"
:initial-width="340"
side="right"
>
<div
class="multi-file-commit-panel-section"
......@@ -104,7 +64,10 @@
<button
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
@click.stop="toggleCollapsed"
@click.stop="setPanelCollapsedStatus({
side: 'right',
collapsed: !rightPanelCollapsed,
})"
>
<icon
:name="currentIcon"
......@@ -117,15 +80,5 @@
:committed-state-svg-path="committedStateSvgPath"
/>
</div>
<panel-resizer
:size.sync="width"
:enabled="!rightPanelCollapsed"
:start-size="340"
:min-size="200"
:max-size="maxSize"
@resize-start="resizingStarted"
@resize-end="resizingEnded"
side="left"
/>
</div>
</resizable-panel>
</template>
<script>
import icon from '~/vue_shared/components/icon.vue';
import repoTree from './ide_repo_tree.vue';
import newDropdown from './new_dropdown/index.vue';
import icon from '~/vue_shared/components/icon.vue';
import repoTree from './ide_repo_tree.vue';
import newDropdown from './new_dropdown/index.vue';
export default {
components: {
repoTree,
icon,
newDropdown,
},
props: {
projectId: {
type: String,
required: true,
export default {
components: {
repoTree,
icon,
newDropdown,
},
branch: {
type: Object,
required: true,
props: {
projectId: {
type: String,
required: true,
},
branch: {
type: Object,
required: true,
},
},
},
};
};
</script>
<template>
......@@ -40,8 +40,8 @@ export default {
/>
</div>
</div>
<div>
<repo-tree :tree-id="branch.treeId" />
</div>
<repo-tree
:tree="branch.tree"
/>
</div>
</template>
<script>
import { mapState } from 'vuex';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import repoFile from './repo_file.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import RepoFile from './repo_file.vue';
export default {
components: {
repoFile,
skeletonLoadingContainer,
RepoFile,
SkeletonLoadingContainer,
},
props: {
treeId: {
type: String,
tree: {
type: Object,
required: true,
},
},
computed: {
...mapState([
'trees',
]),
...mapState({
projectName(state) {
return state.project.name;
},
}),
selctedTree() {
return this.trees[this.treeId].tree;
},
showLoading() {
return !this.trees[this.treeId] || this.trees[this.treeId].loading;
},
},
};
</script>
<template>
<div
class="ide-file-list"
v-if="treeId"
>
<template v-if="showLoading">
<template v-if="tree.loading">
<div
class="multi-file-loading-container"
v-for="n in 3"
......@@ -47,10 +29,13 @@ export default {
<skeleton-loading-container />
</div>
</template>
<repo-file
v-for="file in selctedTree"
:key="file.key"
:file="file"
/>
<template v-else>
<repo-file
v-for="file in tree.tree"
:key="file.key"
:file="file"
:level="0"
/>
</template>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import { mapState, mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import projectTree from './ide_project_tree.vue';
import ResizablePanel from './resizable_panel.vue';
export default {
components: {
......@@ -11,65 +12,27 @@
icon,
panelResizer,
skeletonLoadingContainer,
},
data() {
return {
width: 290,
};
ResizablePanel,
},
computed: {
...mapState([
'loading',
'projects',
'leftPanelCollapsed',
]),
currentIcon() {
return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left';
},
maxSize() {
return window.innerWidth / 2;
},
panelStyle() {
if (!this.leftPanelCollapsed) {
return { width: `${this.width}px` };
}
return {};
},
showLoading() {
return this.loading;
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
'setResizingStatus',
...mapGetters([
'projectsWithTrees',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'left',
collapsed: !this.leftPanelCollapsed,
});
},
resizingStarted() {
this.setResizingStatus(true);
},
resizingEnded() {
this.setResizingStatus(false);
},
},
};
</script>
<template>
<div
class="multi-file-commit-panel"
:class="{
'is-collapsed': leftPanelCollapsed,
}"
:style="panelStyle"
<resizable-panel
:collapsible="false"
:initial-width="290"
side="left"
>
<div class="multi-file-commit-panel-inner">
<template v-if="showLoading">
<template v-if="loading">
<div
class="multi-file-loading-container"
v-for="n in 3"
......@@ -79,36 +42,10 @@
</div>
</template>
<project-tree
v-for="project in projects"
v-for="project in projectsWithTrees"
:key="project.id"
:project="project"
/>
</div>
<button
type="button"
class="btn btn-transparent left-collapse-btn"
@click="toggleCollapsed"
>
<icon
:name="currentIcon"
:size="18"
/>
<span
v-if="!leftPanelCollapsed"
class="collapse-text"
>
Collapse sidebar
</span>
</button>
<panel-resizer
:size.sync="width"
:enabled="!leftPanelCollapsed"
:start-size="290"
:min-size="200"
:max-size="maxSize"
@resize-start="resizingStarted"
@resize-end="resizingEnded"
side="right"
/>
</div>
</resizable-panel>
</template>
<script>
import { mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
......@@ -20,11 +19,6 @@
required: true,
},
},
computed: {
...mapState([
'selectedFile',
]),
},
};
</script>
......@@ -35,32 +29,32 @@
name="branch"
:size="12"
/>
{{ selectedFile.branchId }}
{{ file.branchId }}
</div>
<div>
<div v-if="selectedFile.lastCommit && selectedFile.lastCommit.id">
<div v-if="file.lastCommit && file.lastCommit.id">
Last commit:
<a
v-tooltip
:title="selectedFile.lastCommit.message"
:href="selectedFile.lastCommit.url"
:title="file.lastCommit.message"
:href="file.lastCommit.url"
>
{{ timeFormated(selectedFile.lastCommit.updatedAt) }} by
{{ selectedFile.lastCommit.author }}
{{ timeFormated(file.lastCommit.updatedAt) }} by
{{ file.lastCommit.author }}
</a>
</div>
</div>
<div class="text-right">
{{ selectedFile.name }}
{{ file.name }}
</div>
<div class="text-right">
{{ selectedFile.eol }}
{{ file.eol }}
</div>
<div class="text-right">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
<div class="text-right">
{{ selectedFile.fileLanguage }}
{{ file.fileLanguage }}
</div>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import newModal from './modal.vue';
import upload from './upload.vue';
......@@ -18,10 +19,6 @@
type: String,
required: true,
},
parent: {
type: Object,
default: null,
},
},
data() {
return {
......@@ -31,6 +28,9 @@
};
},
methods: {
...mapActions([
'createTempEntry',
]),
createNewItem(type) {
this.modalType = type;
this.openModal = true;
......@@ -85,7 +85,7 @@
<upload
:branch-id="branch"
:path="path"
:parent="parent"
@create="createTempEntry"
/>
</li>
<li>
......@@ -104,8 +104,8 @@
:type="modalType"
:branch-id="branch"
:path="path"
:parent="parent"
@hide="hideModal"
@create="createTempEntry"
/>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
import modal from '~/vue_shared/components/modal.vue';
......@@ -12,10 +11,6 @@
type: String,
required: true,
},
parent: {
type: Object,
default: null,
},
type: {
type: String,
required: true,
......@@ -31,9 +26,6 @@
};
},
computed: {
...mapState([
'currentProjectId',
]),
modalTitle() {
if (this.type === 'tree') {
return __('Create new directory');
......@@ -60,15 +52,10 @@
this.$refs.fieldName.focus();
},
methods: {
...mapActions([
'createTempEntry',
]),
createEntryInStore() {
this.createTempEntry({
projectId: this.currentProjectId,
this.$emit('create', {
branchId: this.branchId,
parent: this.parent,
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
name: this.entryName,
type: this.type,
});
......
<script>
import { mapActions, mapState } from 'vuex';
export default {
props: {
branchId: {
type: String,
required: true,
},
parent: {
type: Object,
default: null,
path: {
type: String,
required: false,
default: '',
},
},
computed: {
...mapState([
'trees',
'currentProjectId',
]),
},
mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile);
},
......@@ -25,9 +18,6 @@
this.$refs.fileUpload.removeEventListener('change', this.openFile);
},
methods: {
...mapActions([
'createTempEntry',
]),
createFile(target, file, isText) {
const { name } = file;
let { result } = target;
......@@ -36,11 +26,9 @@
result = result.split('base64,')[1];
}
this.createTempEntry({
name,
projectId: this.currentProjectId,
this.$emit('create', {
name: `${(this.path ? `${this.path}/` : '')}${name}`,
branchId: this.branchId,
parent: this.parent,
type: 'blob',
content: result,
base64: !isText,
......
......@@ -5,9 +5,8 @@ import icon from '~/vue_shared/components/icon.vue';
import modal from '~/vue_shared/components/modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import commitFilesList from './commit_sidebar/list.vue';
import * as consts from 'ee/ide/stores/modules/commit/constants'; // eslint-disable-line import/first
import Actions from 'ee/ide/components/commit_sidebar/actions.vue'; // eslint-disable-line import/first
import * as consts from '../stores/modules/commit/constants';
import Actions from './commit_sidebar/actions.vue';
export default {
components: {
......@@ -53,7 +52,6 @@ export default {
},
methods: {
...mapActions([
'getTreeData',
'setPanelCollapsedStatus',
]),
...mapActions('commit', [
......
<script>
/* global monaco */
import { mapState, mapGetters, mapActions } from 'vuex';
import { mapState, mapActions } from 'vuex';
import flash from '~/flash';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
export default {
props: {
file: {
type: Object,
required: true,
},
},
computed: {
...mapGetters([
'activeFile',
'activeFileExtension',
]),
...mapState([
'leftPanelCollapsed',
'rightPanelCollapsed',
'panelResizing',
'viewer',
'delayViewerUpdated',
]),
shouldHideEditor() {
return this.activeFile && this.activeFile.binary && !this.activeFile.raw;
return this.file && this.file.binary && !this.file.raw;
},
},
watch: {
activeFile(oldVal, newVal) {
if (newVal && !newVal.active) {
file(oldVal, newVal) {
if (newVal.path !== this.file.path) {
this.initMonaco();
}
},
......@@ -34,11 +35,6 @@ export default {
rightPanelCollapsed() {
this.editor.updateDimensions();
},
panelResizing(isResizing) {
if (isResizing === false) {
this.editor.updateDimensions();
}
},
viewer() {
this.createEditorInstance();
},
......@@ -72,7 +68,7 @@ export default {
this.editor.clearEditor();
this.getRawFileData(this.activeFile)
this.getRawFileData(this.file)
.then(() => {
const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
......@@ -101,9 +97,9 @@ export default {
});
},
setupEditor() {
if (!this.activeFile || !this.editor.instance) return;
if (!this.file || !this.editor.instance) return;
this.model = this.editor.createModel(this.activeFile);
this.model = this.editor.createModel(this.file);
this.editor.attachModel(this.model);
......@@ -112,7 +108,7 @@ export default {
if (file.active) {
this.changeFileContent({
file,
path: file.path,
content: model.getModel().getValue(),
});
}
......@@ -127,8 +123,8 @@ export default {
});
this.editor.setPosition({
lineNumber: this.activeFile.editorRow,
column: this.activeFile.editorColumn,
lineNumber: this.file.editorRow,
column: this.file.editorColumn,
});
// Handle File Language
......@@ -152,7 +148,7 @@ export default {
>
<div
v-if="shouldHideEditor"
v-html="activeFile.html"
v-html="file.html"
>
</div>
<div
......
<script>
import { mapActions } from 'vuex';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import fileIcon from '~/vue_shared/components/file_icon.vue';
import router from '../ide_router';
import newDropdown from './new_dropdown/index.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue';
export default {
name: 'RepoFile',
components: {
skeletonLoadingContainer,
newDropdown,
fileStatusIcon,
fileIcon,
changedFileIcon,
},
props: {
file: {
type: Object,
required: true,
},
level: {
type: Number,
required: true,
},
},
computed: {
isTree() {
return this.file.type === 'tree';
},
isBlob() {
return this.file.type === 'blob';
},
levelIndentation() {
return {
marginLeft: `${this.level * 16}px`,
};
},
fileClass() {
return {
'file-open': this.isBlob && this.file.opened,
'file-active': this.isBlob && this.file.active,
folder: this.isTree,
};
},
},
updated() {
if (this.file.type === 'blob' && this.file.active) {
this.$el.scrollIntoView();
}
},
methods: {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
clickFile() {
// Manual Action if a tree is selected/opened
if (
this.isTree &&
this.$router.currentRoute.path === `/project${this.file.url}`
) {
this.toggleTreeOpen(this.file.path);
}
const delayPromise = this.file.changed
? Promise.resolve()
: this.updateDelayViewerUpdated(true);
return delayPromise.then(() => {
router.push(`/project${this.file.url}`);
});
},
},
};
</script>
<template>
<div>
<div
class="file"
:class="fileClass"
>
<div
class="file-name"
@click="clickFile"
role="button"
>
<span
class="ide-file-name str-truncated"
:style="levelIndentation"
>
<file-icon
:file-name="file.name"
:loading="file.loading"
:folder="isTree"
:opened="file.opened"
:size="16"
/>
{{ file.name }}
<file-status-icon
:file="file"
/>
</span>
<changed-file-icon
:file="file"
v-if="file.changed || file.tempFile"
class="prepend-top-5 pull-right"
/>
<new-dropdown
v-if="isTree"
:project-id="file.projectId"
:branch="file.branchId"
:path="file.path"
class="pull-right prepend-left-8"
/>
</div>
</div>
<template v-if="file.opened">
<repo-file
v-for="childFile in file.tree"
:key="childFile.key"
:file="childFile"
:level="level + 1"
/>
</template>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
props: {
file: {
type: Object,
required: true,
},
},
computed: {
...mapGetters([
'activeFile',
]),
showButtons() {
return this.activeFile.rawPath ||
this.activeFile.blamePath ||
this.activeFile.commitsPath ||
this.activeFile.permalink;
return this.file.rawPath ||
this.file.blamePath ||
this.file.commitsPath ||
this.file.permalink;
},
rawDownloadButtonLabel() {
return this.activeFile.binary ? 'Download' : 'Raw';
return this.file.binary ? 'Download' : 'Raw';
},
},
};
......@@ -25,7 +26,7 @@ export default {
class="multi-file-editor-btn-group"
>
<a
:href="activeFile.rawPath"
:href="file.rawPath"
target="_blank"
class="btn btn-default btn-sm raw"
rel="noopener noreferrer">
......@@ -38,19 +39,19 @@ export default {
aria-label="File actions"
>
<a
:href="activeFile.blamePath"
:href="file.blamePath"
class="btn btn-default btn-sm blame"
>
Blame
</a>
<a
:href="activeFile.commitsPath"
:href="file.commitsPath"
class="btn btn-default btn-sm history"
>
History
</a>
<a
:href="activeFile.permalink"
:href="file.permalink"
class="btn btn-default btn-sm permalink"
>
Permalink
......
......@@ -3,9 +3,8 @@
import fileIcon from '~/vue_shared/components/file_icon.vue';
import icon from '~/vue_shared/components/icon.vue';
import fileStatusIcon from 'ee/ide/components/repo_file_status_icon.vue';
import changedFileIcon from 'ee/ide/components/changed_file_icon.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue';
export default {
components: {
......@@ -67,7 +66,7 @@
<button
type="button"
class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)"
@click.stop.prevent="closeFile(tab.path)"
:aria-label="closeLabel"
>
<icon
......
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue';
......@@ -8,29 +8,33 @@
RepoTab,
EditorMode,
},
props: {
files: {
type: Array,
required: true,
},
viewer: {
type: String,
required: true,
},
hasChanges: {
type: Boolean,
required: true,
},
},
data() {
return {
showShadow: false,
};
},
computed: {
...mapGetters([
'hasChanges',
]),
...mapState([
'openFiles',
'viewer',
]),
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
this.showShadow =
this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions([
'updateViewer',
]),
...mapActions(['updateViewer']),
},
};
</script>
......@@ -42,7 +46,7 @@
ref="tabsScroller"
>
<repo-tab
v-for="tab in openFiles"
v-for="tab in files"
:key="tab.key"
:tab="tab"
/>
......
<script>
import { mapActions, mapState } from 'vuex';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
export default {
components: {
PanelResizer,
},
props: {
collapsible: {
type: Boolean,
required: true,
},
initialWidth: {
type: Number,
required: true,
},
minSize: {
type: Number,
required: false,
default: 200,
},
side: {
type: String,
required: true,
},
},
data() {
return {
width: this.initialWidth,
};
},
computed: {
...mapState({
collapsed(state) {
return state[`${this.side}PanelCollapsed`];
},
}),
panelStyle() {
if (!this.collapsed) {
return {
width: `${this.width}px`,
};
}
return {};
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
'setResizingStatus',
]),
toggleFullbarCollapsed() {
if (this.collapsed && this.collapsible) {
this.setPanelCollapsedStatus({
side: this.side,
collapsed: !this.collapsed,
});
}
},
},
maxSize: (window.innerWidth / 2),
};
</script>
<template>
<div
class="multi-file-commit-panel"
:class="{
'is-collapsed': collapsed && collapsible,
}"
:style="panelStyle"
@click="toggleFullbarCollapsed"
>
<slot></slot>
<panel-resizer
:size.sync="width"
:enabled="!collapsed"
:start-size="initialWidth"
:min-size="minSize"
:max-size="$options.maxSize"
@resize-start="setResizingStatus(true)"
@resize-end="setResizingStatus(false)"
:side="side === 'right' ? 'left' : 'right'"
/>
</div>
</template>
......@@ -2,9 +2,6 @@ import Vue from 'vue';
import VueRouter from 'vue-router';
import flash from '~/flash';
import store from './stores';
import {
getTreeEntry,
} from './stores/utils';
Vue.use(VueRouter);
......@@ -76,7 +73,7 @@ router.beforeEach((to, from, next) => {
})
.then(() => {
if (to.params[0]) {
const treeEntry = getTreeEntry(store, `${to.params.namespace}/${to.params.project}/${to.params.branch}`, to.params[0]);
const treeEntry = store.state.entries[to.params[0]];
if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry);
}
......
/* global monaco */
import Disposable from './disposable';
import eventHub from 'ee/ide/eventhub'; // eslint-disable-line import/first
import eventHub from '../../eventhub';
export default class Model {
constructor(monaco, file) {
......@@ -11,16 +10,16 @@ export default class Model {
this.content = file.content !== '' ? file.content : file.raw;
this.disposable.add(
this.originalModel = this.monaco.editor.createModel(
(this.originalModel = this.monaco.editor.createModel(
this.file.raw,
undefined,
new this.monaco.Uri(null, null, `original/${this.file.path}`),
),
this.model = this.monaco.editor.createModel(
)),
(this.model = this.monaco.editor.createModel(
this.content,
undefined,
new this.monaco.Uri(null, null, this.file.path),
),
)),
);
this.events = new Map();
......@@ -29,7 +28,10 @@ export default class Model {
this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
eventHub.$on(
`editor.update.model.content.${this.file.path}`,
this.updateContent,
);
}
get url() {
......@@ -63,9 +65,7 @@ export default class Model {
onChange(cb) {
this.events.set(
this.path,
this.disposable.add(
this.model.onDidChangeContent(e => cb(this, e)),
),
this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))),
);
}
......@@ -78,7 +78,13 @@ export default class Model {
this.disposable.dispose();
this.events.clear();
eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose);
eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
eventHub.$off(
`editor.update.model.dispose.${this.file.path}`,
this.dispose,
);
eventHub.$off(
`editor.update.model.content.${this.file.path}`,
this.updateContent,
);
}
}
import eventHub from 'ee/ide/eventhub';
import eventHub from '../../eventhub';
import Disposable from './disposable';
import Model from './model';
......@@ -26,7 +26,10 @@ export default class ModelManager {
this.models.set(model.path, model);
this.disposable.add(model);
eventHub.$on(`editor.update.model.dispose.${file.path}`, this.removeCachedModel.bind(this, file));
eventHub.$on(
`editor.update.model.dispose.${file.path}`,
this.removeCachedModel.bind(this, file),
);
return model;
}
......@@ -34,7 +37,10 @@ export default class ModelManager {
removeCachedModel(file) {
this.models.delete(file.path);
eventHub.$off(`editor.update.model.dispose.${file.path}`, this.removeCachedModel);
eventHub.$off(
`editor.update.model.dispose.${file.path}`,
this.removeCachedModel,
);
}
dispose() {
......
import Vue from 'vue';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
import * as types from './mutation_types';
import FilesDecoratorWorker from './workers/files_decorator_worker';
export const redirectToUrl = (_, url) => visitUrl(url);
......@@ -8,11 +10,11 @@ export const setInitialData = ({ commit }, data) =>
commit(types.SET_INITIAL_DATA, data);
export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach((file) => {
commit(types.DISCARD_FILE_CHANGES, file);
state.changedFiles.forEach(file => {
commit(types.DISCARD_FILE_CHANGES, file.path);
if (file.tempFile) {
dispatch('closeFile', file);
dispatch('closeFile', file.path);
}
});
......@@ -20,20 +22,7 @@ export const discardAllChanges = ({ state, commit, dispatch }) => {
};
export const closeAllFiles = ({ state, dispatch }) => {
state.openFiles.forEach(file => dispatch('closeFile', file));
};
export const toggleEditMode = ({ commit, dispatch }) => {
commit(types.TOGGLE_EDIT_MODE);
dispatch('toggleBlobView');
};
export const toggleBlobView = ({ commit, state }) => {
if (state.editMode) {
commit(types.SET_EDIT_MODE);
} else {
commit(types.SET_PREVIEW_MODE);
}
state.openFiles.forEach(file => dispatch('closeFile', file.path));
};
export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
......@@ -49,28 +38,63 @@ export const setResizingStatus = ({ commit }, resizing) => {
};
export const createTempEntry = (
{ state, dispatch },
{ projectId, branchId, parent, name, type, content = '', base64 = false },
) => {
const selectedParent = parent || state.trees[`${projectId}/${branchId}`];
if (type === 'tree') {
dispatch('createTempTree', {
projectId,
branchId,
parent: selectedParent,
name,
{ state, commit, dispatch },
{ branchId, name, type, content = '', base64 = false },
) =>
new Promise(resolve => {
const worker = new FilesDecoratorWorker();
const fullName =
name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (state.entries[name]) {
flash(
`The name "${name
.split('/')
.pop()}" is already taken in this directory.`,
'alert',
document,
null,
false,
true,
);
resolve();
return null;
}
worker.addEventListener('message', ({ data }) => {
const { file } = data;
worker.terminate();
commit(types.CREATE_TMP_ENTRY, {
data,
projectId: state.currentProjectId,
branchId,
});
if (type === 'blob') {
commit(types.TOGGLE_FILE_OPEN, file.path);
commit(types.ADD_FILE_TO_CHANGED, file.path);
dispatch('setFileActive', file.path);
}
resolve(file);
});
} else if (type === 'blob') {
dispatch('createTempFile', {
projectId,
worker.postMessage({
data: [fullName],
projectId: state.currentProjectId,
branchId,
parent: selectedParent,
name,
type,
tempFile: true,
base64,
content,
});
}
};
return null;
});
export const scrollToTab = () => {
Vue.nextTick(() => {
......@@ -95,4 +119,3 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
export * from './actions/branch';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash';
import eventHub from 'ee/ide/eventhub';
import eventHub from '../../eventhub';
import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import {
findEntry,
setPageTitle,
createTemp,
findIndexOfFile,
} from '../utils';
export const closeFile = ({ commit, state, dispatch }, file) => {
const indexOfClosedFile = findIndexOfFile(state.openFiles, file);
import { setPageTitle } from '../utils';
export const closeFile = ({ commit, state, getters, dispatch }, path) => {
const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path);
const file = state.entries[path];
const fileWasActive = file.active;
commit(types.TOGGLE_FILE_OPEN, file);
commit(types.SET_FILE_ACTIVE, { file, active: false });
commit(types.TOGGLE_FILE_OPEN, path);
commit(types.SET_FILE_ACTIVE, { path, active: false });
if (state.openFiles.length > 0 && fileWasActive) {
const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
const nextFileToOpen = state.openFiles[nextIndexToOpen];
const nextFileToOpen = state.entries[state.openFiles[nextIndexToOpen].path];
router.push(`/project${nextFileToOpen.url}`);
} else if (!state.openFiles.length) {
router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
}
dispatch('getLastCommitData');
eventHub.$emit(`editor.update.model.dispose.${file.path}`);
};
export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
const file = state.entries[path];
const currentActiveFile = getters.activeFile;
if (file.active) return;
if (currentActiveFile) {
commit(types.SET_FILE_ACTIVE, { file: currentActiveFile, active: false });
commit(types.SET_FILE_ACTIVE, {
path: currentActiveFile.path,
active: false,
});
}
commit(types.SET_FILE_ACTIVE, { file, active: true });
commit(types.SET_FILE_ACTIVE, { path, active: true });
dispatch('scrollToTab');
// reset hash for line highlighting
location.hash = '';
commit(types.SET_CURRENT_PROJECT, file.projectId);
commit(types.SET_CURRENT_BRANCH, file.branchId);
};
......@@ -54,104 +49,97 @@ export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
export const getFileData = ({ state, commit, dispatch }, file) => {
commit(types.TOGGLE_LOADING, { entry: file });
service.getFileData(file.url)
.then((res) => {
return service
.getFileData(file.url)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
return res.json();
})
.then((data) => {
.then(data => {
commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, file);
dispatch('setFileActive', file);
commit(types.TOGGLE_FILE_OPEN, file.path);
dispatch('setFileActive', file.path);
commit(types.TOGGLE_LOADING, { entry: file });
})
.catch(() => {
commit(types.TOGGLE_LOADING, { entry: file });
flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
flash(
'Error loading file data. Please try again.',
'alert',
document,
null,
false,
true,
);
});
};
export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFileData(file)
.then((raw) => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
})
.catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true));
export const changeFileContent = ({ state, commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content });
const indexOfChangedFile = findIndexOfFile(state.changedFiles, file);
export const getRawFileData = ({ commit, dispatch }, file) =>
service
.getRawFileData(file)
.then(raw => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
})
.catch(() =>
flash(
'Error loading file content. Please try again.',
'alert',
document,
null,
false,
true,
),
);
export const changeFileContent = ({ state, commit }, { path, content }) => {
const file = state.entries[path];
commit(types.UPDATE_FILE_CONTENT, { path, content });
const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path);
if (file.changed && indexOfChangedFile === -1) {
commit(types.ADD_FILE_TO_CHANGED, file);
commit(types.ADD_FILE_TO_CHANGED, path);
} else if (!file.changed && indexOfChangedFile !== -1) {
commit(types.REMOVE_FILE_FROM_CHANGED, file);
commit(types.REMOVE_FILE_FROM_CHANGED, path);
}
};
export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
if (state.selectedFile) {
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => {
if (getters.activeFile) {
commit(types.SET_FILE_LANGUAGE, { file: getters.activeFile, fileLanguage });
}
};
export const setFileEOL = ({ state, commit }, { eol }) => {
if (state.selectedFile) {
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
export const setFileEOL = ({ getters, commit }, { eol }) => {
if (getters.activeFile) {
commit(types.SET_FILE_EOL, { file: getters.activeFile, eol });
}
};
export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
if (state.selectedFile) {
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
export const setEditorPosition = (
{ getters, commit },
{ editorRow, editorColumn },
) => {
if (getters.activeFile) {
commit(types.SET_FILE_POSITION, {
file: getters.activeFile,
editorRow,
editorColumn,
});
}
};
export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
const path = parent.path !== undefined ? parent.path : '';
// We need to do the replacement otherwise the web_url + file.url duplicate
const newUrl = `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${name}`;
const file = createTemp({
projectId,
branchId,
name: name.replace(`${path}/`, ''),
path,
type: 'blob',
level: parent.level !== undefined ? parent.level + 1 : 0,
changed: true,
content,
base64,
url: newUrl,
});
if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`, 'alert', document, null, false, true);
commit(types.CREATE_TMP_FILE, {
parent,
file,
});
commit(types.TOGGLE_FILE_OPEN, file);
commit(types.ADD_FILE_TO_CHANGED, file);
dispatch('setFileActive', file);
if (!state.editMode && !file.base64) {
dispatch('toggleEditMode', true);
}
router.push(`/project${file.url}`);
return Promise.resolve(file);
};
export const discardFileChanges = ({ state, commit }, path) => {
const file = state.entries[path];
export const discardFileChanges = ({ commit }, file) => {
commit(types.DISCARD_FILE_CHANGES, file);
commit(types.REMOVE_FILE_FROM_CHANGED, file);
commit(types.DISCARD_FILE_CHANGES, path);
commit(types.REMOVE_FILE_FROM_CHANGED, path);
if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, file);
commit(types.TOGGLE_FILE_OPEN, path);
}
eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
......
......@@ -2,6 +2,29 @@ import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
export const getProjectData = (
{ commit, state, dispatch },
{ namespace, projectId, force = false } = {},
) => new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, { entry: state });
service.getProjectData(namespace, projectId)
.then(res => res.data)
.then((data) => {
commit(types.TOGGLE_LOADING, { entry: state });
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
resolve(data);
})
.catch(() => {
flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
});
} else {
resolve(state.projects[`${namespace}/${projectId}`]);
}
});
export const getBranchData = (
{ commit, state, dispatch },
{ projectId, branchId, force = false } = {},
......@@ -24,20 +47,3 @@ export const getBranchData = (
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});
export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
state.currentProjectId,
{
branch,
ref: state.currentBranchId,
},
)
.then(res => res.json())
.then((data) => {
const branchName = data.name;
const url = location.href.replace(state.currentBranchId, branchName);
if (this.$router) this.$router.push(url);
commit(types.SET_CURRENT_BRANCH, branchName);
});
import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
import {
findEntry,
} from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit, dispatch }, path) => {
commit(types.TOGGLE_TREE_OPEN, path);
};
export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
if (row.type === 'tree') {
dispatch('toggleTreeOpen', row.path);
} else if (row.type === 'blob' && (row.opened || row.changed)) {
if (row.changed && !row.opened) {
commit(types.TOGGLE_FILE_OPEN, row.path);
}
dispatch('setFileActive', row.path);
} else {
dispatch('getFileData', row);
}
};
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
service.getTreeLastCommit(tree.lastCommitPath)
.then((res) => {
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
return res.json();
})
.then((data) => {
data.forEach((lastCommit) => {
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
if (entry) {
commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
}
});
dispatch('getLastCommitData', tree);
})
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
};
export const getFiles = (
{ state, commit, dispatch },
{ projectId, branchId } = {},
) => new Promise((resolve, reject) => {
if (!state.trees[`${projectId}/${branchId}`]) {
const selectedProject = state.projects[projectId];
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
service
.getFiles(selectedProject.web_url, branchId)
.then(res => res.json())
.then((data) => {
const worker = new FilesDecoratorWorker();
worker.addEventListener('message', (e) => {
const { entries, treeList } = e.data;
const selectedTree = state.trees[`${projectId}/${branchId}`];
commit(types.SET_ENTRIES, entries);
commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList });
commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
worker.terminate();
resolve();
});
worker.postMessage({
data,
projectId,
branchId,
});
})
.catch((e) => {
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
reject(e);
});
} else {
resolve();
}
});
export const activeFile = state =>
state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
export const modifiedFiles = state =>
state.changedFiles.filter(f => !f.tempFile);
export const projectsWithTrees = state =>
Object.keys(state.projects).map(projectId => {
const project = state.projects[projectId];
return {
...project,
branches: Object.keys(project.branches).map(branchId => {
const branch = project.branches[branchId];
return {
...branch,
tree: state.trees[branch.treeId],
};
}),
};
});
// eslint-disable-next-line no-confusing-arrow
export const currentIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length;
......@@ -4,8 +4,7 @@ import state from './state';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import commitModule from 'ee/ide/stores/modules/commit'; // eslint-disable-line import/first
import commitModule from './modules/commit';
Vue.use(Vuex);
......
import $ from 'jquery';
import { sprintf, __ } from '~/locale';
import * as rootTypes from 'ee/ide/stores/mutation_types';
import { createCommitPayload, createNewMergeRequestUrl } from 'ee/ide/stores/utils';
import router from 'ee/ide/ide_router';
import service from 'ee/ide/services';
import flash from '~/flash';
import { stripHtml } from '~/lib/utils/text_utility';
import * as rootTypes from '../../mutation_types';
import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
import router from '../../../ide_router';
import service from '../../../services';
import * as types from './mutation_types';
import * as consts from './constants';
import eventHub from 'ee/ide/eventhub'; // eslint-disable-line import/first
import eventHub from '../../../eventhub';
export const updateCommitMessage = ({ commit }, message) => {
commit(types.UPDATE_COMMIT_MESSAGE, message);
......@@ -29,16 +28,18 @@ export const updateBranchName = ({ commit }, branchName) => {
export const setLastCommitMessage = ({ rootState, commit }, data) => {
const currentProject = rootState.projects[rootState.currentProjectId];
const commitStats = data.stats ?
sprintf(
__('with %{additions} additions, %{deletions} deletions.'),
{ additions: data.stats.additions, deletions: data.stats.deletions },
)
const commitStats = data.stats
? sprintf(__('with %{additions} additions, %{deletions} deletions.'), {
additions: data.stats.additions, // eslint-disable-line indent
deletions: data.stats.deletions, // eslint-disable-line indent
}) // eslint-disable-line indent
: '';
const commitMsg = sprintf(
__('Your changes have been committed. Commit %{commitId} %{commitStats}'),
{
commitId: `<a href="${currentProject.web_url}/commit/${data.short_id}" class="commit-sha">${data.short_id}</a>`,
commitId: `<a href="${currentProject.web_url}/commit/${
data.short_id
}" class="commit-sha">${data.short_id}</a>`,
commitStats,
},
false,
......@@ -53,7 +54,9 @@ export const checkCommitStatus = ({ rootState }) =>
.then(({ data }) => {
const { id } = data.commit;
const selectedBranch =
rootState.projects[rootState.currentProjectId].branches[rootState.currentBranchId];
rootState.projects[rootState.currentProjectId].branches[
rootState.currentBranchId
];
if (selectedBranch.workingReference !== id) {
return true;
......@@ -61,7 +64,16 @@ export const checkCommitStatus = ({ rootState }) =>
return false;
})
.catch(() => flash(__('Error checking branch data. Please try again.'), 'alert', document, null, false, true));
.catch(() =>
flash(
__('Error checking branch data. Please try again.'),
'alert',
document,
null,
false,
true,
),
);
export const updateFilesAfterCommit = (
{ commit, dispatch, state, rootState, rootGetters },
......@@ -78,88 +90,129 @@ export const updateFilesAfterCommit = (
},
};
commit(rootTypes.SET_BRANCH_WORKING_REFERENCE, {
projectId: rootState.currentProjectId,
branchId: rootState.currentBranchId,
reference: data.id,
}, { root: true });
commit(
rootTypes.SET_BRANCH_WORKING_REFERENCE,
{
projectId: rootState.currentProjectId,
branchId: rootState.currentBranchId,
reference: data.id,
},
{ root: true },
);
rootState.changedFiles.forEach((entry) => {
commit(rootTypes.SET_LAST_COMMIT_DATA, {
entry,
lastCommit,
}, { root: true });
rootState.changedFiles.forEach(entry => {
commit(
rootTypes.SET_LAST_COMMIT_DATA,
{
entry,
lastCommit,
},
{ root: true },
);
eventHub.$emit(`editor.update.model.content.${entry.path}`, entry.content);
commit(rootTypes.SET_FILE_RAW_DATA, {
file: entry,
raw: entry.content,
}, { root: true });
commit(rootTypes.TOGGLE_FILE_CHANGED, {
file: entry,
changed: false,
}, { root: true });
commit(
rootTypes.SET_FILE_RAW_DATA,
{
file: entry,
raw: entry.content,
},
{ root: true },
);
commit(
rootTypes.TOGGLE_FILE_CHANGED,
{
file: entry,
changed: false,
},
{ root: true },
);
});
commit(rootTypes.REMOVE_ALL_CHANGES_FILES, null, { root: true });
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
router.push(`/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`);
router.push(
`/project/${rootState.currentProjectId}/blob/${branch}/${
rootGetters.activeFile.path
}`,
);
}
dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH);
};
export const commitChanges = ({ commit, state, getters, dispatch, rootState }) => {
export const commitChanges = ({
commit,
state,
getters,
dispatch,
rootState,
}) => {
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const payload = createCommitPayload(getters.branchName, newBranch, state, rootState);
const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
const payload = createCommitPayload(
getters.branchName,
newBranch,
state,
rootState,
);
const getCommitStatus = newBranch
? Promise.resolve(false)
: dispatch('checkCommitStatus');
commit(types.UPDATE_LOADING, true);
return getCommitStatus.then(branchChanged => new Promise((resolve) => {
if (branchChanged) {
// show the modal with a Bootstrap call
$('#ide-create-branch-modal').modal('show');
} else {
resolve();
}
}))
.then(() => service.commit(rootState.currentProjectId, payload))
.then(({ data }) => {
commit(types.UPDATE_LOADING, false);
if (!data.short_id) {
flash(data.message, 'alert', document, null, false, true);
return;
}
dispatch('setLastCommitMessage', data);
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) {
dispatch(
'redirectToUrl',
createNewMergeRequestUrl(
rootState.projects[rootState.currentProjectId].web_url,
getters.branchName,
rootState.currentBranchId,
),
{ root: true },
);
} else {
dispatch('updateFilesAfterCommit', { data, branch: getters.branchName });
}
})
.catch((err) => {
let errMsg = __('Error committing changes. Please try again.');
if (err.response.data && err.response.data.message) {
errMsg += ` (${stripHtml(err.response.data.message)})`;
}
flash(errMsg, 'alert', document, null, false, true);
window.dispatchEvent(new Event('resize'));
commit(types.UPDATE_LOADING, false);
});
return getCommitStatus
.then(
branchChanged =>
new Promise(resolve => {
if (branchChanged) {
// show the modal with a Bootstrap call
$('#ide-create-branch-modal').modal('show');
} else {
resolve();
}
}),
)
.then(() => service.commit(rootState.currentProjectId, payload))
.then(({ data }) => {
commit(types.UPDATE_LOADING, false);
if (!data.short_id) {
flash(data.message, 'alert', document, null, false, true);
return;
}
dispatch('setLastCommitMessage', data);
dispatch('updateCommitMessage', '');
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) {
dispatch(
'redirectToUrl',
createNewMergeRequestUrl(
rootState.projects[rootState.currentProjectId].web_url,
getters.branchName,
rootState.currentBranchId,
),
{ root: true },
);
} else {
dispatch('updateFilesAfterCommit', {
data,
branch: getters.branchName,
});
}
})
.catch(err => {
let errMsg = __('Error committing changes. Please try again.');
if (err.response.data && err.response.data.message) {
errMsg += ` (${stripHtml(err.response.data.message)})`;
}
flash(errMsg, 'alert', document, null, false, true);
window.dispatchEvent(new Event('resize'));
commit(types.UPDATE_LOADING, false);
});
};
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
......@@ -20,7 +19,6 @@ export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
// Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
export const CREATE_TMP_TREE = 'CREATE_TMP_TREE';
export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
export const CREATE_TREE = 'CREATE_TREE';
export const REMOVE_ALL_CHANGES_FILES = 'REMOVE_ALL_CHANGES_FILES';
......@@ -35,17 +33,11 @@ export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
export const SET_FILE_EOL = 'SET_FILE_EOL';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
export const REMOVE_FILE_FROM_CHANGED = 'REMOVE_FILE_FROM_CHANGED';
export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
// Viewer mutation types
export const SET_PREVIEW_MODE = 'SET_PREVIEW_MODE';
export const SET_EDIT_MODE = 'SET_EDIT_MODE';
export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const SET_ENTRIES = 'SET_ENTRIES';
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
......@@ -8,25 +8,19 @@ export default {
[types.SET_INITIAL_DATA](state, data) {
Object.assign(state, data);
},
[types.SET_PREVIEW_MODE](state) {
Object.assign(state, {
currentBlobView: 'repo-preview',
});
},
[types.SET_EDIT_MODE](state) {
Object.assign(state, {
currentBlobView: 'repo-editor',
});
},
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
Object.assign(entry, {
loading: forceValue !== undefined ? forceValue : !entry.loading,
});
},
[types.TOGGLE_EDIT_MODE](state) {
Object.assign(state, {
editMode: !state.editMode,
});
if (entry.path) {
Object.assign(state.entries[entry.path], {
loading:
forceValue !== undefined
? forceValue
: !state.entries[entry.path].loading,
});
} else {
Object.assign(entry, {
loading: forceValue !== undefined ? forceValue : !entry.loading,
});
}
},
[types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
Object.assign(state, {
......@@ -57,6 +51,44 @@ export default {
lastCommitMsg,
});
},
[types.SET_ENTRIES](state, entries) {
Object.assign(state, {
entries,
});
},
[types.CREATE_TMP_ENTRY](state, { data, projectId, branchId }) {
Object.keys(data.entries).reduce((acc, key) => {
const entry = data.entries[key];
const foundEntry = state.entries[key];
if (!foundEntry) {
Object.assign(state.entries, {
[key]: entry,
});
} else {
const tree = entry.tree.filter(
f => foundEntry.tree.find(e => e.path === f.path) === undefined,
);
Object.assign(foundEntry, {
tree: foundEntry.tree.concat(tree),
});
}
return acc.concat(key);
}, []);
const foundEntry = state.trees[`${projectId}/${branchId}`].tree.find(
e => e.path === data.treeList[0].path,
);
if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], {
tree: state.trees[`${projectId}/${branchId}`].tree.concat(
data.treeList,
),
});
}
},
[types.UPDATE_VIEWER](state, viewer) {
Object.assign(state, {
viewer,
......
......@@ -7,16 +7,14 @@ export default {
});
},
[types.SET_BRANCH](state, { projectPath, branchName, branch }) {
// Add client side properties
Object.assign(branch, {
treeId: `${projectPath}/${branchName}`,
active: true,
workingReference: '',
});
Object.assign(state.projects[projectPath], {
branches: {
[branchName]: branch,
[branchName]: {
...branch,
treeId: `${projectPath}/${branchName}`,
active: true,
workingReference: '',
},
},
});
},
......
import * as types from '../mutation_types';
import { findIndexOfFile } from '../utils';
export default {
[types.SET_FILE_ACTIVE](state, { file, active }) {
Object.assign(file, {
[types.SET_FILE_ACTIVE](state, { path, active }) {
Object.assign(state.entries[path], {
active,
});
Object.assign(state, {
selectedFile: file,
});
},
[types.TOGGLE_FILE_OPEN](state, file) {
Object.assign(file, {
opened: !file.opened,
[types.TOGGLE_FILE_OPEN](state, path) {
Object.assign(state.entries[path], {
opened: !state.entries[path].opened,
});
if (file.opened) {
state.openFiles.push(file);
if (state.entries[path].opened) {
state.openFiles.push(state.entries[path]);
} else {
state.openFiles.splice(findIndexOfFile(state.openFiles, file), 1);
Object.assign(state, {
openFiles: state.openFiles.filter(f => f.path !== path),
});
}
},
[types.SET_FILE_DATA](state, { data, file }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
id: data.id,
blamePath: data.blame_path,
commitsPath: data.commits_path,
......@@ -34,53 +31,52 @@ export default {
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
raw,
});
},
[types.UPDATE_FILE_CONTENT](state, { file, content }) {
const changed = content !== file.raw;
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw;
Object.assign(file, {
Object.assign(state.entries[path], {
content,
changed,
});
},
[types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
fileLanguage,
});
},
[types.SET_FILE_EOL](state, { file, eol }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
eol,
});
},
[types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
editorRow,
editorColumn,
});
},
[types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, {
content: file.raw,
[types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], {
content: state.entries[path].raw,
changed: false,
});
},
[types.CREATE_TMP_FILE](state, { file, parent }) {
parent.tree.push(file);
},
[types.ADD_FILE_TO_CHANGED](state, file) {
state.changedFiles.push(file);
[types.ADD_FILE_TO_CHANGED](state, path) {
Object.assign(state, {
changedFiles: state.changedFiles.concat(state.entries[path]),
});
},
[types.REMOVE_FILE_FROM_CHANGED](state, file) {
const indexOfChangedFile = findIndexOfFile(state.changedFiles, file);
state.changedFiles.splice(indexOfChangedFile, 1);
[types.REMOVE_FILE_FROM_CHANGED](state, path) {
Object.assign(state, {
changedFiles: state.changedFiles.filter(f => f.path !== path),
});
},
[types.TOGGLE_FILE_CHANGED](state, { file, changed }) {
Object.assign(file, {
Object.assign(state.entries[file.path], {
changed,
});
},
......
import * as types from '../mutation_types';
export default {
[types.TOGGLE_TREE_OPEN](state, tree) {
Object.assign(tree, {
opened: !tree.opened,
[types.TOGGLE_TREE_OPEN](state, path) {
Object.assign(state.entries[path], {
opened: !state.entries[path].opened,
});
},
[types.CREATE_TREE](state, { treePath }) {
......@@ -16,14 +16,13 @@ export default {
}),
});
},
[types.SET_DIRECTORY_DATA](state, { data, tree }) {
Object.assign(tree, {
tree: data,
});
},
[types.SET_PARENT_TREE_URL](state, url) {
[types.SET_DIRECTORY_DATA](state, { data, treePath }) {
Object.assign(state, {
parentTreeUrl: url,
trees: Object.assign(state.trees, {
[treePath]: {
tree: data,
},
}),
});
},
[types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
......@@ -31,9 +30,6 @@ export default {
lastCommitPath: url,
});
},
[types.CREATE_TMP_TREE](state, { parent, tmpEntry }) {
parent.tree.push(tmpEntry);
},
[types.REMOVE_ALL_CHANGES_FILES](state) {
Object.assign(state, {
changedFiles: [],
......
export default () => ({
canCommit: false,
currentProjectId: '',
currentBranchId: '',
currentBlobView: 'repo-editor',
changedFiles: [],
editMode: true,
endpoints: {},
isInitialRoot: false,
lastCommitMsg: '',
lastCommitPath: '',
loading: false,
onTopOfBranch: false,
openFiles: [],
selectedFile: null,
path: '',
parentTreeUrl: '',
trees: {},
projects: {},
leftPanelCollapsed: false,
rightPanelCollapsed: false,
panelResizing: false,
entries: {},
viewer: 'editor',
delayViewerUpdated: false,
});
import _ from 'underscore';
export const dataStructure = () => ({
id: '',
key: '',
......@@ -9,9 +7,7 @@ export const dataStructure = () => ({
name: '',
url: '',
path: '',
level: 0,
tempFile: false,
icon: '',
tree: [],
loading: false,
opened: false,
......@@ -25,7 +21,6 @@ export const dataStructure = () => ({
updatedAt: '',
author: '',
},
tree_url: '',
blamePath: '',
commitsPath: '',
permalink: '',
......@@ -51,8 +46,6 @@ export const decorateData = (entity) => {
type,
url,
name,
icon,
tree_url,
path,
renderError,
content = '',
......@@ -61,7 +54,6 @@ export const decorateData = (entity) => {
opened = false,
changed = false,
parentTreeUrl = '',
level = 0,
base64 = false,
file_lock,
......@@ -77,11 +69,8 @@ export const decorateData = (entity) => {
type,
name,
url,
tree_url,
path,
level,
tempFile,
icon: `fa-${icon}`,
opened,
active,
parentTreeUrl,
......@@ -95,32 +84,6 @@ export const decorateData = (entity) => {
};
};
/*
Takes the multi-dimensional tree and returns a flattened array.
This allows for the table to recursively render the table rows but keeps the data
structure nested to make it easier to add new files/directories.
*/
export const treeList = (state, treeId) => {
const baseTree = state.trees[treeId];
if (baseTree) {
const mapTree = arr => (!arr.tree || !arr.tree.length ?
[] : _.map(arr.tree, a => [a, mapTree(a)]));
return _.chain(baseTree.tree)
.map(arr => [arr, mapTree(arr)])
.flatten()
.value();
}
return [];
};
export const getTree = state => (namespace, projectId, branch) => state.trees[`${namespace}/${projectId}/${branch}`];
export const getTreeEntry = (store, treeId, path) => {
const fileList = treeList(store.state, treeId);
return fileList ? fileList.find(file => file.path === path) : null;
};
export const findEntry = (tree, type, name, prop = 'name') => tree.find(
f => f.type === type && f[prop] === name,
);
......@@ -131,63 +94,6 @@ export const setPageTitle = (title) => {
document.title = title;
};
export const createTemp = ({
projectId, branchId, name, path, type, level, changed, content, base64, url,
}) => {
const treePath = path ? `${path}/${name}` : name;
return decorateData({
id: new Date().getTime().toString(),
projectId,
branchId,
name,
type,
tempFile: true,
path: treePath,
icon: type === 'tree' ? 'folder' : 'file-text-o',
changed,
content,
parentTreeUrl: '',
level,
base64,
renderError: base64,
url,
});
};
export const createOrMergeEntry = ({ projectId,
branchId,
entry,
type,
parentTreeUrl,
level,
state }) => {
if (state.changedFiles.length) {
const foundChangedFile = findEntry(state.changedFiles, type, entry.path, 'path');
if (foundChangedFile) {
return foundChangedFile;
}
}
if (state.openFiles.length) {
const foundOpenFile = findEntry(state.openFiles, type, entry.path, 'path');
if (foundOpenFile) {
return foundOpenFile;
}
}
return decorateData({
...entry,
projectId,
branchId,
type,
parentTreeUrl,
level,
});
};
export const createCommitPayload = (branch, newBranch, state, rootState) => ({
branch,
commit_message: state.commitMessage,
......@@ -214,11 +120,6 @@ const sortTreesByTypeAndName = (a, b) => {
return 0;
};
export const sortTree = (sortedTree) => {
sortedTree.forEach((el) => {
Object.assign(el, {
tree: el && el.tree ? sortTree(el.tree) : [],
});
});
return sortedTree.sort(sortTreesByTypeAndName);
};
export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, {
tree: entity.tree.length ? sortTree(entity.tree) : [],
})).sort(sortTreesByTypeAndName);
import {
decorateData,
sortTree,
} from '../utils';
self.addEventListener('message', (e) => {
const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data;
const treeList = [];
let file;
const entries = data.reduce((acc, path) => {
const pathSplit = path.split('/');
const blobName = pathSplit.pop().trim();
if (pathSplit.length > 0) {
pathSplit.reduce((pathAcc, folderName) => {
const parentFolder = acc[pathAcc[pathAcc.length - 1]];
const folderPath = `${(parentFolder ? `${parentFolder.path}/` : '')}${folderName}`;
const foundEntry = acc[folderPath];
if (!foundEntry) {
const tree = decorateData({
projectId,
branchId,
id: folderPath,
name: folderName,
path: folderPath,
url: `/${projectId}/tree/${branchId}/${folderPath}`,
type: 'tree',
parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
tempFile,
changed: tempFile,
opened: tempFile,
});
Object.assign(acc, {
[folderPath]: tree,
});
if (parentFolder) {
parentFolder.tree.push(tree);
} else {
treeList.push(tree);
}
pathAcc.push(tree.path);
} else {
pathAcc.push(foundEntry.path);
}
return pathAcc;
}, []);
}
if (blobName !== '') {
const fileFolder = acc[pathSplit.join('/')];
file = decorateData({
projectId,
branchId,
id: path,
name: blobName,
path,
url: `/${projectId}/blob/${branchId}/${path}`,
type: 'blob',
parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
tempFile,
changed: tempFile,
content,
base64,
});
Object.assign(acc, {
[path]: file,
});
if (fileFolder) {
fileFolder.tree.push(file);
} else {
treeList.push(file);
}
}
return acc;
}, {});
self.postMessage({
entries,
treeList: sortTree(treeList),
file,
});
});
/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
import $ from 'jquery';
import { insertText } from '~/lib/utils/common_utils';
const textUtils = {};
textUtils.selectedText = function(text, textarea) {
function selectedText(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
};
}
textUtils.lineBefore = function(text, textarea) {
function lineBefore(text, textarea) {
var split;
split = text.substring(0, textarea.selectionStart).trim().split('\n');
return split[split.length - 1];
};
}
textUtils.lineAfter = function(text, textarea) {
function lineAfter(text, textarea) {
return text.substring(textarea.selectionEnd).trim().split('\n')[0];
};
}
textUtils.blockTagText = function(text, textArea, blockTag, selected) {
var lineAfter, lineBefore;
lineBefore = this.lineBefore(text, textArea);
lineAfter = this.lineAfter(text, textArea);
if (lineBefore === blockTag && lineAfter === blockTag) {
function blockTagText(text, textArea, blockTag, selected) {
const before = lineBefore(text, textArea);
const after = lineAfter(text, textArea);
if (before === blockTag && after === blockTag) {
// To remove the block tag we have to select the line before & after
if (blockTag != null) {
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
......@@ -32,10 +29,30 @@ textUtils.blockTagText = function(text, textArea, blockTag, selected) {
} else {
return blockTag + "\n" + selected + "\n" + blockTag;
}
};
}
textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
function moveCursor(textArea, tag, wrapped, removedLastNewLine) {
var pos;
if (!textArea.setSelectionRange) {
return;
}
if (textArea.selectionStart === textArea.selectionEnd) {
if (wrapped) {
pos = textArea.selectionStart - tag.length;
} else {
pos = textArea.selectionStart;
}
if (removedLastNewLine) {
pos -= 1;
}
return textArea.setSelectionRange(pos, pos);
}
}
export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap) {
var textToInsert, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
removedLastNewLine = false;
removedFirstNewLine = false;
currentLineEmpty = false;
......@@ -67,9 +84,9 @@ textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') {
insertText = this.blockTagText(text, textArea, blockTag, selected);
textToInsert = blockTagText(text, textArea, blockTag, selected);
} else {
insertText = selectedSplit.map(function(val) {
textToInsert = selectedSplit.map(function(val) {
if (val.indexOf(tag) === 0) {
return "" + (val.replace(tag, ''));
} else {
......@@ -78,78 +95,42 @@ textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
}).join('\n');
}
} else {
insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
textToInsert = "" + startChar + tag + selected + (wrap ? tag : ' ');
}
if (removedFirstNewLine) {
insertText = '\n' + insertText;
textToInsert = '\n' + textToInsert;
}
if (removedLastNewLine) {
insertText += '\n';
textToInsert += '\n';
}
if (document.queryCommandSupported('insertText')) {
inserted = document.execCommand('insertText', false, insertText);
}
if (!inserted) {
try {
document.execCommand("ms-beginUndoUnit");
} catch (error) {}
textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
try {
document.execCommand("ms-endUndoUnit");
} catch (error) {}
}
return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
};
insertText(textArea, textToInsert);
return moveCursor(textArea, tag, wrap, removedLastNewLine);
}
textUtils.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
var pos;
if (!textArea.setSelectionRange) {
return;
}
if (textArea.selectionStart === textArea.selectionEnd) {
if (wrapped) {
pos = textArea.selectionStart - tag.length;
} else {
pos = textArea.selectionStart;
}
if (removedLastNewLine) {
pos -= 1;
}
return textArea.setSelectionRange(pos, pos);
}
};
textUtils.updateText = function(textArea, tag, blockTag, wrap) {
function updateText(textArea, tag, blockTag, wrap) {
var $textArea, selected, text;
$textArea = $(textArea);
textArea = $textArea.get(0);
text = $textArea.val();
selected = this.selectedText(text, textArea);
selected = selectedText(text, textArea);
$textArea.focus();
return this.insertText(textArea, text, tag, blockTag, selected, wrap);
};
return insertMarkdownText(textArea, text, tag, blockTag, selected, wrap);
}
textUtils.init = function(form) {
var self;
self = this;
function replaceRange(s, start, end, substitute) {
return s.substring(0, start) + substitute + s.substring(end);
}
export function addMarkdownListeners(form) {
return $('.js-md', form).off('click').on('click', function() {
var $this;
$this = $(this);
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('mdTag'), $this.data('mdBlock'), !$this.data('mdPrepend'));
const $this = $(this);
return updateText($this.closest('.md-area').find('textarea'), $this.data('mdTag'), $this.data('mdBlock'), !$this.data('mdPrepend'));
});
};
}
textUtils.removeListeners = function(form) {
export function removeMarkdownListeners(form) {
return $('.js-md', form).off('click');
};
textUtils.replaceRange = function(s, start, end, substitute) {
return s.substring(0, start) + substitute + s.substring(end);
};
export default textUtils;
}
/* eslint-disable import/first */
/* global ConfirmDangerModal */
/* global $ */
import jQuery from 'jquery';
......@@ -21,7 +20,6 @@ import './behaviors/';
// everything else
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
import './confirm_danger_modal';
import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
import initTodoToggle from './header';
......@@ -32,7 +30,6 @@ import LazyLoader from './lazy_loader';
import initLogoAnimation from './logo';
import './milestone_select';
import './projects_dropdown';
import './render_gfm';
import initBreadcrumbs from './breadcrumb';
// EE-only scripts
......@@ -218,16 +215,6 @@ document.addEventListener('DOMContentLoaded', () => {
$(document).trigger('toggle.comments');
});
$document.on('click', '.js-confirm-danger', (e) => {
const btn = $(e.target);
const form = btn.closest('form');
const text = btn.data('confirmDangerMessage');
e.preventDefault();
// eslint-disable-next-line no-new
new ConfirmDangerModal(form, text);
});
$document.on('breakpoint:change', (e, breakpoint) => {
if (breakpoint === 'sm' || breakpoint === 'xs') {
const $gutterIcon = $sidebarGutterToggle.find('i');
......
......@@ -73,6 +73,10 @@
type: String,
required: true,
},
emptyNoDataSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: {
type: String,
required: true,
......@@ -188,6 +192,7 @@
:clusters-path="clustersPath"
:empty-getting-started-svg-path="emptyGettingStartedSvgPath"
:empty-loading-svg-path="emptyLoadingSvgPath"
:empty-no-data-svg-path="emptyNoDataSvgPath"
:empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
/>
</template>
......@@ -27,6 +27,10 @@
type: String,
required: true,
},
emptyNoDataSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: {
type: String,
required: true,
......@@ -54,7 +58,7 @@
buttonPath: this.documentationPath,
},
noData: {
svgUrl: this.emptyUnableToConnectSvgPath,
svgUrl: this.emptyNoDataSvgPath,
title: 'No data found',
description: `You are connected to the Prometheus server, but there is currently
no data to display.`,
......
......@@ -105,6 +105,9 @@ export default class Notes {
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
this.$wrapperEl = hasVueMRDiscussionsCookie()
? $(document).find('.diffs')
: $(document);
this.cleanBinding();
this.addBinding();
this.setPollingInterval();
......@@ -138,10 +141,6 @@ export default class Notes {
}
addBinding() {
this.$wrapperEl = hasVueMRDiscussionsCookie()
? $(document).find('.diffs')
: $(document);
// Edit note link
this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this));
this.$wrapperEl.on('click', '.note-edit-cancel', this.cancelEdit);
......@@ -226,14 +225,9 @@ export default class Notes {
$(window).on('hashchange', this.onHashChange);
this.boundGetContent = this.getContent.bind(this);
document.addEventListener('refreshLegacyNotes', this.boundGetContent);
this.eventsBound = true;
}
cleanBinding() {
if (!this.eventsBound) {
return;
}
this.$wrapperEl.off('click', '.js-note-edit');
this.$wrapperEl.off('click', '.note-edit-cancel');
this.$wrapperEl.off('click', '.js-note-delete');
......
import groupAvatar from '~/group_avatar';
import TransferDropdown from '~/groups/transfer_dropdown';
import initConfirmDangerModal from '~/confirm_danger_modal';
document.addEventListener('DOMContentLoaded', () => {
groupAvatar();
new TransferDropdown(); // eslint-disable-line no-new
initConfirmDangerModal();
});
/* eslint-disable no-new */
import initSettingsPanels from '~/settings_panels';
import setupProjectEdit from '~/project_edit';
import initConfirmDangerModal from '~/confirm_danger_modal';
import ProjectNew from '../shared/project_new';
import projectAvatar from '../shared/project_avatar';
import initProjectPermissionsSettings from '../shared/permissions';
......@@ -12,4 +13,5 @@ document.addEventListener('DOMContentLoaded', () => {
initSettingsPanels();
projectAvatar();
initProjectPermissionsSettings();
initConfirmDangerModal();
});
import $ from 'jquery';
import 'vendor/peek';
import 'vendor/peek.performance_bar';
import { getParameterValues } from './lib/utils/url_utility';
export default class PerformanceBar {
constructor(opts) {
if (!PerformanceBar.singleton) {
this.init(opts);
PerformanceBar.singleton = this;
}
return PerformanceBar.singleton;
}
init(opts) {
const $container = $(opts.container);
this.$lineProfileLink = $container.find('.js-toggle-modal-peek-line-profile');
this.$lineProfileModal = $('#modal-peek-line-profile');
this.initEventListeners();
this.showModalOnLoad();
}
initEventListeners() {
this.$lineProfileLink.on('click', e => this.handleLineProfileLink(e));
$(document).on('click', '.js-lineprof-file', PerformanceBar.toggleLineProfileFile);
}
showModalOnLoad() {
// When a lineprofiler query-string param is present, we show the line
// profiler modal upon page load
if (/lineprofiler/.test(window.location.search)) {
PerformanceBar.toggleModal(this.$lineProfileModal);
}
}
handleLineProfileLink(e) {
const lineProfilerParameter = getParameterValues('lineprofiler');
const lineProfilerParameterRegex = new RegExp(`lineprofiler=${lineProfilerParameter[0]}`);
const shouldToggleModal = lineProfilerParameter.length > 0 &&
lineProfilerParameterRegex.test(e.currentTarget.href);
if (shouldToggleModal) {
e.preventDefault();
PerformanceBar.toggleModal(this.$lineProfileModal);
}
}
static toggleModal($modal) {
if ($modal.length) {
$modal.modal('toggle');
}
}
static toggleLineProfileFile(e) {
$(e.currentTarget).parents('.peek-rblineprof-file').find('.data').toggle();
}
}
<script>
import GlModal from '~/vue_shared/components/gl_modal.vue';
export default {
components: {
GlModal,
},
props: {
currentRequest: {
type: Object,
required: true,
},
metric: {
type: String,
required: true,
},
header: {
type: String,
required: true,
},
details: {
type: String,
required: true,
},
keys: {
type: Array,
required: true,
},
},
};
</script>
<template>
<div
:id="`peek-view-${metric}`"
class="view"
>
<button
:data-target="`#modal-peek-${metric}-details`"
class="btn-blank btn-link bold"
type="button"
data-toggle="modal"
>
<span
v-if="currentRequest.details"
class="bold"
>
{{ currentRequest.details[metric].duration }}
/
{{ currentRequest.details[metric].calls }}
</span>
</button>
<gl-modal
v-if="currentRequest.details"
:id="`modal-peek-${metric}-details`"
:header-title-text="header"
class="performance-bar-modal"
>
<table class="table">
<tr
v-for="(item, index) in currentRequest.details[metric][details]"
:key="index"
>
<td><strong>{{ item.duration }}ms</strong></td>
<td
v-for="key in keys"
:key="key"
>
{{ item[key] }}
</td>
</tr>
</table>
<div slot="footer">
</div>
</gl-modal>
{{ metric }}
</div>
</template>
<script>
import $ from 'jquery';
import PerformanceBarService from '../services/performance_bar_service';
import detailedMetric from './detailed_metric.vue';
import requestSelector from './request_selector.vue';
import simpleMetric from './simple_metric.vue';
import upstreamPerformanceBar from './upstream_performance_bar.vue';
import Flash from '../../flash';
export default {
components: {
detailedMetric,
requestSelector,
simpleMetric,
upstreamPerformanceBar,
},
props: {
store: {
type: Object,
required: true,
},
env: {
type: String,
required: true,
},
requestId: {
type: String,
required: true,
},
peekUrl: {
type: String,
required: true,
},
profileUrl: {
type: String,
required: true,
},
},
detailedMetrics: [
{ metric: 'pg', header: 'SQL queries', details: 'queries', keys: ['sql'] },
{
metric: 'gitaly',
header: 'Gitaly calls',
details: 'details',
keys: ['feature', 'request'],
},
],
simpleMetrics: ['redis', 'sidekiq'],
data() {
return { currentRequestId: '' };
},
computed: {
requests() {
return this.store.requestsWithDetails();
},
currentRequest: {
get() {
return this.store.findRequest(this.currentRequestId);
},
set(requestId) {
this.currentRequestId = requestId;
},
},
initialRequest() {
return this.currentRequestId === this.requestId;
},
lineProfileModal() {
return $('#modal-peek-line-profile');
},
},
mounted() {
this.interceptor = PerformanceBarService.registerInterceptor(
this.peekUrl,
this.loadRequestDetails,
);
this.loadRequestDetails(this.requestId, window.location.href);
this.currentRequest = this.requestId;
if (this.lineProfileModal.length) {
this.lineProfileModal.modal('toggle');
}
},
beforeDestroy() {
PerformanceBarService.removeInterceptor(this.interceptor);
},
methods: {
loadRequestDetails(requestId, requestUrl) {
if (!this.store.canTrackRequest(requestUrl)) {
return;
}
this.store.addRequest(requestId, requestUrl);
PerformanceBarService.fetchRequestDetails(this.peekUrl, requestId)
.then(res => {
this.store.addRequestDetails(requestId, res.data.data);
})
.catch(() =>
Flash(`Error getting performance bar results for ${requestId}`),
);
},
changeCurrentRequest(newRequestId) {
this.currentRequest = newRequestId;
},
},
};
</script>
<template>
<div
id="js-peek"
:class="env"
>
<request-selector
v-if="currentRequest"
:current-request="currentRequest"
:requests="requests"
@change-current-request="changeCurrentRequest"
/>
<div
id="peek-view-host"
class="view prepend-left-5"
>
<span
v-if="currentRequest && currentRequest.details"
class="current-host"
>
{{ currentRequest.details.host.hostname }}
</span>
</div>
<div
v-if="currentRequest"
class="wrapper"
>
<upstream-performance-bar
v-if="initialRequest && currentRequest.details"
/>
<detailed-metric
v-for="metric in $options.detailedMetrics"
:key="metric.metric"
:current-request="currentRequest"
:metric="metric.metric"
:header="metric.header"
:details="metric.details"
:keys="metric.keys"
/>
<div
v-if="initialRequest"
id="peek-view-rblineprof"
class="view"
>
<button
v-if="lineProfileModal.length"
class="btn-link btn-blank"
data-toggle="modal"
data-target="#modal-peek-line-profile"
>
profile
</button>
<a
v-else
:href="profileUrl"
>
profile
</a>
</div>
<simple-metric
v-for="metric in $options.simpleMetrics"
:current-request="currentRequest"
:key="metric"
:metric="metric"
/>
<div
id="peek-view-gc"
class="view"
>
<span
v-if="currentRequest.details"
class="bold"
>
<span title="Invoke Time">{{ currentRequest.details.gc.gc_time }}</span>ms
/
<span title="Invoke Count">{{ currentRequest.details.gc.invokes }}</span>
gc
</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
currentRequest: {
type: Object,
required: true,
},
requests: {
type: Array,
required: true,
},
},
data() {
return {
currentRequestId: this.currentRequest.id,
};
},
watch: {
currentRequestId(newRequestId) {
this.$emit('change-current-request', newRequestId);
},
},
methods: {
truncatedUrl(requestUrl) {
const components = requestUrl.replace(/\/$/, '').split('/');
let truncated = components[components.length - 1];
if (truncated.match(/^\d+$/)) {
truncated = `${components[components.length - 2]}/${truncated}`;
}
return truncated;
},
},
};
</script>
<template>
<div
id="peek-request-selector"
class="append-right-5 pull-right"
>
<select v-model="currentRequestId">
<option
v-for="request in requests"
:key="request.id"
:value="request.id"
>
{{ truncatedUrl(request.url) }}
</option>
</select>
</div>
</template>
<script>
export default {
props: {
currentRequest: {
type: Object,
required: true,
},
metric: {
type: String,
required: true,
},
},
};
</script>
<template>
<div
:id="`peek-view-${metric}`"
class="view"
>
<span
v-if="currentRequest.details"
class="bold"
>
{{ currentRequest.details[metric].duration }}
/
{{ currentRequest.details[metric].calls }}
</span>
{{ metric }}
</div>
</template>
<script>
export default {
mounted() {
const upstreamPerformanceBar = document
.getElementById('peek-view-performance-bar')
.cloneNode(true);
this.$refs.wrapper.appendChild(upstreamPerformanceBar);
},
};
</script>
<template>
<div
id="peek-view-performance-bar-vue"
class="view"
ref="wrapper"
></div>
</template>
import 'vendor/peek.performance_bar';
import Vue from 'vue';
import performanceBarApp from './components/performance_bar_app.vue';
import PerformanceBarStore from './stores/performance_bar_store';
export default () =>
new Vue({
el: '#js-peek',
components: {
performanceBarApp,
},
data() {
const performanceBarData = document.querySelector(this.$options.el)
.dataset;
const store = new PerformanceBarStore();
return {
store,
env: performanceBarData.env,
requestId: performanceBarData.requestId,
peekUrl: performanceBarData.peekUrl,
profileUrl: performanceBarData.profileUrl,
};
},
render(createElement) {
return createElement('performance-bar-app', {
props: {
store: this.store,
env: this.env,
requestId: this.requestId,
peekUrl: this.peekUrl,
profileUrl: this.profileUrl,
},
});
},
});
import axios from '../../lib/utils/axios_utils';
export default class PerformanceBarService {
static fetchRequestDetails(peekUrl, requestId) {
return axios.get(peekUrl, { params: { request_id: requestId } });
}
static registerInterceptor(peekUrl, callback) {
return axios.interceptors.response.use(response => {
const requestId = response.headers['x-request-id'];
const requestUrl = response.config.url;
if (requestUrl !== peekUrl && requestId) {
callback(requestId, requestUrl);
}
return response;
});
}
static removeInterceptor(interceptor) {
axios.interceptors.response.eject(interceptor);
}
}
export default class PerformanceBarStore {
constructor() {
this.requests = [];
}
addRequest(requestId, requestUrl, requestDetails) {
if (!this.findRequest(requestId)) {
this.requests.push({
id: requestId,
url: requestUrl,
details: requestDetails,
});
}
return this.requests;
}
findRequest(requestId) {
return this.requests.find(request => request.id === requestId);
}
addRequestDetails(requestId, requestDetails) {
const request = this.findRequest(requestId);
request.details = requestDetails;
return request;
}
requestsWithDetails() {
return this.requests.filter(request => request.details);
}
canTrackRequest(requestUrl) {
return (
this.requests.filter(request => request.url === requestUrl).length < 2
);
}
}
/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
import $ from 'jquery';
import Cookies from 'js-cookie';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import flash from '../flash';
......@@ -10,7 +9,6 @@ export default class Profile {
constructor({ form } = {}) {
this.onSubmitForm = this.onSubmitForm.bind(this);
this.form = form || $('.edit-user');
this.newRepoActivated = Cookies.get('new_repo');
this.setRepoRadio();
this.bindEvents();
this.initAvatarGlCrop();
......@@ -23,21 +21,28 @@ export default class Profile {
modalCrop: '.modal-profile-crop',
pickImageEl: '.js-choose-user-avatar-button',
uploadImageBtn: '.js-upload-user-avatar',
modalCropImg: '.modal-profile-crop-image'
modalCropImg: '.modal-profile-crop-image',
};
this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
this.avatarGlCrop = $('.js-user-avatar-input')
.glCrop(cropOpts)
.data('glcrop');
}
bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
$('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie);
$('.js-preferences-form').on(
'change.preference',
'input[type=radio]',
this.submitForm,
);
$('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
this.form.on('submit', this.onSubmitForm);
}
submitForm() {
return $(this).parents('form').submit();
return $(this)
.parents('form')
.submit();
}
onSubmitForm(e) {
......@@ -59,21 +64,13 @@ export default class Profile {
url: this.form.attr('action'),
data: formData,
})
.then(({ data }) => flash(data.message, 'notice'))
.then(() => {
window.scrollTo(0, 0);
// Enable submit button after requests ends
self.form.find(':input[disabled]').enable();
})
.catch(error => flash(error.message));
}
setNewRepoCookie() {
if (this.value === 'off') {
Cookies.remove('new_repo');
} else {
Cookies.set('new_repo', true, { expires_in: 365 });
}
.then(({ data }) => flash(data.message, 'notice'))
.then(() => {
window.scrollTo(0, 0);
// Enable submit button after requests ends
self.form.find(':input[disabled]').enable();
})
.catch(error => flash(error.message));
}
setRepoRadio() {
......
......@@ -3,7 +3,7 @@ import Mousetrap from 'mousetrap';
import _ from 'underscore';
import Sidebar from './right_sidebar';
import Shortcuts from './shortcuts';
import { CopyAsGFM } from './behaviors/copy_as_gfm';
import { CopyAsGFM } from './behaviors/markdown/copy_as_gfm';
export default class ShortcutsIssuable extends Shortcuts {
constructor(isMergeRequest) {
......
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetSHAMismatch',
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
The source branch HEAD has recently changed. Please reload the page and review the changes before merging
</span>
</div>
</div>
`,
};
<script>
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'ShaMismatch',
components: {
statusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="warning"
:show-disabled-button="true"
/>
<div class="media-body space-children">
<span class="bold">
The source branch HEAD has recently changed.
Please reload the page and review the changes before merging.
</span>
</div>
</div>
</template>
......@@ -28,7 +28,7 @@ export { default as NothingToMergeState } from './components/states/nothing_to_m
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue';
export { default as ReadyToMergeState } from 'ee/vue_merge_request_widget/components/states/mr_widget_ready_to_merge';
export { default as SHAMismatchState } from './components/states/mr_widget_sha_mismatch';
export { default as ShaMismatchState } from './components/states/sha_mismatch.vue';
export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue';
export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
......
......@@ -19,7 +19,7 @@ import {
MissingBranchState,
NotAllowedState,
ReadyToMergeState,
SHAMismatchState,
ShaMismatchState,
UnresolvedDiscussionsState,
PipelineBlockedState,
PipelineFailedState,
......@@ -228,7 +228,7 @@ export default {
'mr-widget-not-allowed': NotAllowedState,
'mr-widget-missing-branch': MissingBranchState,
'mr-widget-ready-to-merge': ReadyToMergeState,
'mr-widget-sha-mismatch': SHAMismatchState,
'mr-widget-sha-mismatch': ShaMismatchState,
'mr-widget-squash-before-merge': SquashBeforeMerge,
'mr-widget-checking': CheckingState,
'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
......
......@@ -16,7 +16,7 @@ const stateToComponentMap = {
mergeWhenPipelineSucceeds: 'mr-widget-merge-when-pipeline-succeeds',
failedToMerge: 'mr-widget-failed-to-merge',
autoMergeFailed: 'mr-widget-auto-merge-failed',
shaMismatch: 'mr-widget-sha-mismatch',
shaMismatch: 'sha-mismatch',
rebase: 'mr-widget-rebase',
};
......
......@@ -9,7 +9,8 @@
padding-left: $contextual-sidebar-width;
}
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
.issues-bulk-update.right-sidebar.right-sidebar-expanded
.issuable-sidebar-header {
padding: 10px 0 15px;
}
}
......@@ -61,7 +62,8 @@
}
.nav-sidebar {
transition: width $sidebar-transition-duration, left $sidebar-transition-duration;
transition: width $sidebar-transition-duration,
left $sidebar-transition-duration;
position: fixed;
z-index: 400;
width: $contextual-sidebar-width;
......@@ -75,7 +77,7 @@
&:not(.sidebar-collapsed-desktop) {
@media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
box-shadow: inset -2px 0 0 $border-color,
2px 1px 3px $dropdown-shadow-color;
2px 1px 3px $dropdown-shadow-color;
}
}
......@@ -234,7 +236,7 @@
border-radius: 0 3px 3px 0;
&::before {
content: "";
content: '';
position: absolute;
top: -30px;
bottom: -30px;
......@@ -305,7 +307,6 @@
}
}
// Collapsed nav
.toggle-sidebar-button,
......@@ -454,18 +455,3 @@
z-index: 300;
}
}
// Make issue boards full-height now that sub-nav is gone
.boards-list {
height: calc(100vh - #{$header-height});
@media (min-width: $screen-sm-min) {
height: calc(100vh - 180px);
}
}
.with-performance-bar .boards-list {
height: calc(100vh - #{$header-height} - #{$performance-bar-height});
}
.navbar-gitlab {
&.navbar-gitlab {
padding: 0 16px;
z-index: 1000;
margin-bottom: 0;
min-height: $header-height;
border: 0;
border-bottom: 1px solid $border-color;
position: fixed;
top: 0;
left: 0;
right: 0;
border-radius: 0;
.logo-text {
line-height: initial;
svg {
width: 55px;
height: 14px;
margin: 0;
fill: $white-light;
}
}
.container-fluid {
padding: 0;
.user-counter {
svg {
margin-right: 3px;
}
}
.navbar-toggle {
right: -10px;
border-radius: 0;
min-width: 45px;
padding: 0;
margin-right: -7px;
font-size: 14px;
text-align: center;
color: currentColor;
&:hover,
&:focus,
&.active {
color: currentColor;
background-color: transparent;
}
.more-icon,
.close-icon {
fill: $white-light;
margin: auto;
}
}
padding: 0 16px;
z-index: 1000;
margin-bottom: 0;
min-height: $header-height;
border: 0;
border-bottom: 1px solid $border-color;
position: fixed;
top: 0;
left: 0;
right: 0;
border-radius: 0;
.logo-text {
line-height: initial;
svg {
width: 55px;
height: 14px;
margin: 0;
fill: $white-light;
}
}
......@@ -184,6 +148,37 @@
}
.container-fluid {
padding: 0;
.user-counter {
svg {
margin-right: 3px;
}
}
.navbar-toggle {
right: -10px;
border-radius: 0;
min-width: 45px;
padding: 0;
margin-right: -7px;
font-size: 14px;
text-align: center;
color: currentColor;
&:hover,
&:focus,
&.active {
color: currentColor;
background-color: transparent;
}
.more-icon,
.close-icon {
fill: $white-light;
margin: auto;
}
}
.navbar-nav {
@media (max-width: $screen-xs-max) {
......@@ -337,7 +332,7 @@
.breadcrumbs {
display: -webkit-flex;
display: flex;
min-height: 48px;
min-height: $breadcrumb-min-height;
color: $gl-text-color;
}
......@@ -470,7 +465,7 @@
padding: 0 5px;
line-height: 12px;
border-radius: 7px;
box-shadow: 0 1px 0 rgba($gl-header-color, .2);
box-shadow: 0 1px 0 rgba($gl-header-color, 0.2);
&.issues-count {
background-color: $green-500;
......
......@@ -5,9 +5,9 @@ $grid-size: 8px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 250px;
$sidebar-transition-duration: .3s;
$sidebar-transition-duration: 0.3s;
$sidebar-breakpoint: 1024px;
$default-transition-duration: .15s;
$default-transition-duration: 0.15s;
$contextual-sidebar-width: 220px;
$contextual-sidebar-collapsed-width: 50px;
......@@ -130,7 +130,6 @@ $theme-green-800: #145d33;
$theme-green-900: #0d4524;
$theme-green-950: #072d16;
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
$almost-black: #242424;
......@@ -164,7 +163,7 @@ $gl-text-color-secondary: #707070;
$gl-text-color-tertiary: #949494;
$gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, 0.85);
$gl-text-color-disabled: #919191;
$gl-text-green: $green-600;
$gl-text-green-hover: $green-700;
......@@ -264,6 +263,7 @@ $highlight-changes-color: rgb(235, 255, 232);
$performance-bar-height: 35px;
$flash-height: 52px;
$context-header-height: 60px;
$breadcrumb-min-height: 48px;
$issue-box-upcoming-bg: #8f8f8f;
$pages-group-name-color: #4c4e54;
......@@ -302,7 +302,7 @@ $tanuki-yellow: #fca326;
*/
$gl-primary: $blue-500;
$gl-success: $green-500;
$gl-success-focus: rgba($gl-success, .4);
$gl-success-focus: rgba($gl-success, 0.4);
$gl-info: $blue-500;
$gl-warning: $orange-500;
$gl-danger: $red-500;
......@@ -338,8 +338,11 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
/*
* Fonts
*/
$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas',
'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
$regular_font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
/*
* Dropdowns
......@@ -350,16 +353,16 @@ $dropdown-max-height: 312px;
$dropdown-vertical-offset: 4px;
$dropdown-link-color: #555;
$dropdown-link-hover-bg: $row-hover;
$dropdown-empty-row-bg: rgba(#000, .04);
$dropdown-empty-row-bg: rgba(#000, 0.04);
$dropdown-border-color: $border-color;
$dropdown-shadow-color: rgba(#000, .1);
$dropdown-divider-color: rgba(#000, .1);
$dropdown-shadow-color: rgba(#000, 0.1);
$dropdown-divider-color: rgba(#000, 0.1);
$dropdown-title-btn-color: #bfbfbf;
$dropdown-input-color: #555;
$dropdown-input-fa-color: #c7c7c7;
$dropdown-input-focus-border: $focus-border-color;
$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4);
$dropdown-loading-bg: rgba(#fff, .6);
$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, 0.4);
$dropdown-loading-bg: rgba(#fff, 0.6);
$dropdown-chevron-size: 10px;
$dropdown-toggle-active-border-color: darken($border-color, 14%);
$dropdown-item-hover-bg: $gray-darker;
......@@ -374,9 +377,9 @@ $dropdown-hover-color: $blue-400;
/*
* Contextual Sidebar
*/
$link-active-background: rgba(0, 0, 0, .04);
$link-hover-background: rgba(0, 0, 0, .06);
$inactive-badge-background: rgba(0, 0, 0, .08);
$link-active-background: rgba(0, 0, 0, 0.04);
$link-hover-background: rgba(0, 0, 0, 0.06);
$inactive-badge-background: rgba(0, 0, 0, 0.08);
/*
* Buttons
......@@ -404,14 +407,14 @@ $status-icon-margin: $gl-btn-padding;
/*
* Award emoji
*/
$award-emoji-menu-shadow: rgba(0, 0, 0, .175);
$award-emoji-menu-shadow: rgba(0, 0, 0, 0.175);
$award-emoji-positive-add-bg: #fed159;
$award-emoji-positive-add-lines: #bb9c13;
/*
* Search Box
*/
$search-input-border-color: rgba($blue-400, .8);
$search-input-border-color: rgba($blue-400, 0.8);
$search-input-focus-shadow-color: $dropdown-input-focus-shadow;
$search-input-width: 220px;
$location-badge-active-bg: $blue-500;
......@@ -436,7 +439,7 @@ $zen-control-color: #555;
* Calendar
*/
$calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
$calendar-border-color: rgba(#000, 0.1);
$calendar-user-contrib-text: #959494;
/*
......@@ -459,6 +462,17 @@ $ci-skipped-color: #888;
*/
$issue-boards-font-size: 14px;
$issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
/*
The following heights are used in boards.scss and are used for calculation of the board height.
They probably should be derived in a smarter way.
*/
$issue-boards-filter-height: 68px;
$issue-boards-breadcrumbs-height-xs: 63px;
$issue-board-list-difference-xs: $header-height +
$issue-boards-breadcrumbs-height-xs;
$issue-board-list-difference-sm: $header-height + $breadcrumb-min-height;
$issue-board-list-difference-md: $issue-board-list-difference-sm +
$issue-boards-filter-height;
/*
* Avatar
......@@ -574,14 +588,14 @@ $label-padding: 7px;
$label-padding-modal: 10px;
$label-gray-bg: #f8fafc;
$label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1);
$label-remove-border: rgba(0, 0, 0, 0.1);
$label-border-radius: 100px;
/*
* Animation
*/
$fade-in-duration: 200ms;
$fade-mask-transition-duration: .1s;
$fade-mask-transition-duration: 0.1s;
$fade-mask-transition-curve: ease-in-out;
/*
......@@ -649,7 +663,6 @@ $stat-graph-selection-stroke: #333;
$select2-drop-shadow1: rgba(76, 86, 103, 0.247059);
$select2-drop-shadow2: rgba(31, 37, 50, 0.317647);
/*
* Todo
*/
......@@ -686,7 +699,6 @@ CI variable lists
*/
$ci-variable-remove-button-width: calc(1em + #{2 * $gl-padding});
/*
Filtered Search
*/
......@@ -728,7 +740,14 @@ Repo editor
*/
$repo-editor-grey: #f6f7f9;
$repo-editor-grey-darker: #e9ebee;
$repo-editor-linear-gradient: linear-gradient(to right, $repo-editor-grey 0%, $repo-editor-grey-darker, 20%, $repo-editor-grey 40%, $repo-editor-grey 100%);
$repo-editor-linear-gradient: linear-gradient(
to right,
$repo-editor-grey 0%,
$repo-editor-grey-darker,
20%,
$repo-editor-grey 40%,
$repo-editor-grey 100%
);
/*
Performance Bar
......@@ -739,8 +758,8 @@ $perf-bar-staging: #291430;
$perf-bar-development: #4c1210;
$perf-bar-bucket-bg: #111;
$perf-bar-bucket-color: #ccc;
$perf-bar-bucket-box-shadow-from: rgba($white-light, .2);
$perf-bar-bucket-box-shadow-to: rgba($black, .25);
$perf-bar-bucket-box-shadow-from: rgba($white-light, 0.2);
$perf-bar-bucket-box-shadow-to: rgba($black, 0.25);
/*
Issuable warning
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment