Commit ad2d7963 authored by Simon Knox's avatar Simon Knox

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into...

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into 2518-saved-configuration-for-issue-board
parents 8dade803 e54dd249
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-phantomjs-2.1-node-8.x-yarn-1.0-postgresql-9.6"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.13-phantomjs-2.1-node-8.x-yarn-1.0-postgresql-9.6"
.default-cache: &default-cache
key: "ruby-233-with-yarn"
key: "ruby-235-with-yarn"
paths:
- vendor/ruby
- .yarn-cache/
......@@ -455,7 +455,7 @@ db:migrate:reset-mysql:
variables:
SETUP_DB: "false"
script:
- git fetch origin v8.14.10
- git fetch origin v9.3.0
- git checkout -f FETCH_HEAD
- bundle install $BUNDLE_INSTALL_FLAGS
- cp config/gitlab.yml.example config/gitlab.yml
......@@ -551,7 +551,7 @@ karma:
<<: *dedicated-runner
<<: *except-docs
<<: *pull-cache
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-chrome-61.0-node-8.x-yarn-1.0-postgresql-9.6"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.13-chrome-61.0-node-8.x-yarn-1.0-postgresql-9.6"
stage: test
variables:
BABEL_ENV: "coverage"
......
## Contributor license agreement
## Developer Certificate of Origin + License
By submitting code as an individual you agree to the
[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md).
By submitting code as an entity you agree to the
[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
By contributing to GitLab B.V., You accept and agree to the following terms and
conditions for Your present and future Contributions submitted to GitLab B.V.
Except for the license granted herein to GitLab B.V. and recipients of software
distributed by GitLab B.V., You reserve all right, title, and interest in and to
Your Contributions. All Contributions are subject to the following DCO + License
terms.
[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md)
_This notice should stay as the first item in the CONTRIBUTING.md file._
......
......@@ -90,7 +90,7 @@ gem 'kaminari', '~> 1.0'
gem 'hamlit', '~> 2.6.1'
# Files attachments
gem 'carrierwave', '~> 1.1'
gem 'carrierwave', '~> 1.2'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
......@@ -398,7 +398,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.48.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.51.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -107,7 +107,7 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
carrierwave (1.1.0)
carrierwave (1.2.1)
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
......@@ -273,7 +273,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.48.0)
gitaly-proto (0.51.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -291,7 +291,7 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16)
posix-spawn (~> 0.3)
gitlab-markup (1.6.2)
gitlab-markup (1.6.3)
gitlab_omniauth-ldap (2.0.4)
net-ldap (~> 0.16)
omniauth (~> 1.3)
......@@ -990,7 +990,7 @@ DEPENDENCIES
bundler-audit (~> 0.5.0)
capybara (~> 2.15.0)
capybara-screenshot (~> 1.0.0)
carrierwave (~> 1.1)
carrierwave (~> 1.2)
charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
......@@ -1030,7 +1030,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.48.0)
gitaly-proto (~> 0.51.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
......
......@@ -16,6 +16,7 @@ const Api = {
usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath)
......
/* eslint-disable comma-dangle, space-before-function-paren, no-new */
/* global MilestoneSelect */
/* global LabelsSelect */
/* global Sidebar */
import Vue from 'vue';
......@@ -11,6 +10,7 @@ import Assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
const Store = gl.issueBoards.BoardsStore;
......
......@@ -64,19 +64,16 @@ export default class Clusters {
this.poll = new Poll({
resource: this.service,
method: 'fetchData',
successCallback: (data) => {
const { status, status_reason } = data.data;
this.updateContainer(status, status_reason);
},
errorCallback: () => {
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
},
successCallback: data => this.handleSuccess(data),
errorCallback: () => Clusters.handleError(),
});
if (!Visibility.hidden()) {
this.poll.makeRequest();
} else {
this.service.fetchData();
this.service.fetchData()
.then(data => this.handleSuccess(data))
.catch(() => Clusters.handleError());
}
Visibility.change(() => {
......@@ -88,6 +85,15 @@ export default class Clusters {
});
}
static handleError() {
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
}
handleSuccess(data) {
const { status, status_reason } = data.data;
this.updateContainer(status, status_reason);
}
hideAll() {
this.errorContainer.classList.add('hidden');
this.successContainer.classList.add('hidden');
......
......@@ -3,7 +3,7 @@
import IssuableIndex from './issuable_index';
/* global Milestone */
import IssuableForm from './issuable_form';
/* global LabelsSelect */
import LabelsSelect from './labels_select';
/* global MilestoneSelect */
/* global NewBranchForm */
/* global NotificationsForm */
......
/* eslint-disable func-names, no-underscore-dangle, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */
import _ from 'underscore';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { isObject } from './lib/utils/type_utility';
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput;
......
......@@ -7,10 +7,12 @@ import { highCountTrim } from '~/lib/utils/text_utility';
* @param {jQuery.Event} e
* @param {String} count
*/
$(document).on('todo:toggle', (e, count) => {
const parsedCount = parseInt(count, 10);
const $todoPendingCount = $('.todos-count');
export default function initTodoToggle() {
$(document).on('todo:toggle', (e, count) => {
const parsedCount = parseInt(count, 10);
const $todoPendingCount = $('.todos-count');
$todoPendingCount.text(highCountTrim(parsedCount));
$todoPendingCount.toggleClass('hidden', parsedCount === 0);
});
$todoPendingCount.text(highCountTrim(parsedCount));
$todoPendingCount.toggleClass('hidden', parsedCount === 0);
});
}
/* eslint-disable no-new */
/* global MilestoneSelect */
/* global LabelsSelect */
import LabelsSelect from './labels_select';
import IssuableContext from './issuable_context';
/* global Sidebar */
......
/* eslint-disable no-new */
/* global LabelsSelect */
import LabelsSelect from './labels_select';
/* global MilestoneSelect */
/* global IssueStatusSelect */
/* global SubscriptionSelect */
import UsersSelect from './users_select';
import issueStatusSelect from './issue_status_select';
export default () => {
new UsersSelect();
new LabelsSelect();
new MilestoneSelect();
new IssueStatusSelect();
issueStatusSelect();
new SubscriptionSelect();
};
/* eslint-disable class-methods-use-this, no-new */
/* global LabelsSelect */
/* global MilestoneSelect */
/* global IssueStatusSelect */
/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import './milestone_select';
import './issue_status_select';
import issueStatusSelect from './issue_status_select';
import './subscription_select';
import './labels_select';
import LabelsSelect from './labels_select';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
......@@ -49,7 +47,7 @@ export default class IssuableBulkUpdateSidebar {
initDropdowns() {
new LabelsSelect();
new MilestoneSelect();
new IssueStatusSelect();
issueStatusSelect();
new SubscriptionSelect();
}
......
......@@ -2,11 +2,8 @@ import Cookies from 'js-cookie';
import bp from './breakpoints';
import UsersSelect from './users_select';
const PARTICIPANTS_ROW_COUNT = 7;
export default class IssuableContext {
constructor(currentUser) {
this.initParticipants();
this.userSelect = new UsersSelect(currentUser);
$('select.select2').select2({
......@@ -51,29 +48,4 @@ export default class IssuableContext {
}
});
}
initParticipants() {
$(document).on('click', '.js-participants-more', this.toggleHiddenParticipants);
return $('.js-participants-author').each(function forEachAuthor(i) {
if (i >= PARTICIPANTS_ROW_COUNT) {
$(this).addClass('js-participants-hidden').hide();
}
});
}
toggleHiddenParticipants() {
const currentText = $(this).text().trim();
const lessText = $(this).data('less-text');
const originalText = $(this).data('original-text');
if (currentText === originalText) {
$(this).text(lessText);
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
} else {
$(this).text(originalText);
}
$('.js-participants-hidden').toggle();
}
}
......@@ -6,7 +6,7 @@ import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
class Issue {
export default class Issue {
constructor() {
if ($('a.btn-close').length) {
this.taskList = new TaskList({
......@@ -147,5 +147,3 @@ class Issue {
});
}
}
export default Issue;
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
(function() {
this.IssueStatusSelect = (function() {
function IssueStatusSelect() {
$('.js-issue-status').each(function(i, el) {
var fieldName;
fieldName = $(el).data("field-name");
return $(el).glDropdown({
selectable: true,
fieldName: fieldName,
toggleLabel: (function(_this) {
return function(selected, el, instance) {
var $item, label;
label = 'Author';
$item = instance.dropdown.find('.is-active');
if ($item.length) {
label = $item.text();
}
return label;
};
})(this),
clicked: function(options) {
return options.e.preventDefault();
},
id: function(obj, el) {
return $(el).data("id");
}
});
});
}
return IssueStatusSelect;
})();
}).call(window);
export default function issueStatusSelect() {
$('.js-issue-status').each((i, el) => {
const fieldName = $(el).data('field-name');
return $(el).glDropdown({
selectable: true,
fieldName,
toggleLabel(selected, element, instance) {
let label = 'Author';
const $item = instance.dropdown.find('.is-active');
if ($item.length) {
label = $item.text();
}
return label;
},
clicked(options) {
return options.e.preventDefault();
},
id(obj, element) {
return $(element).data('id');
},
});
});
}
This diff is collapsed.
/* eslint-disable one-export, one-var, one-var-declaration-per-line */
import _ from 'underscore';
export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
......@@ -21,7 +19,10 @@ export default class LazyLoader {
}
searchLazyImages() {
this.lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
this.checkElementsInView();
if (this.lazyImages.length) {
this.checkElementsInView();
}
}
startContentObserver() {
const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
......@@ -45,15 +46,13 @@ export default class LazyLoader {
checkElementsInView() {
const scrollTop = pageYOffset;
const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD;
let imgBoundRect, imgTop, imgBound;
// Loading Images which are in the current viewport or close to them
this.lazyImages = this.lazyImages.filter((selectedImage) => {
if (selectedImage.getAttribute('data-src')) {
imgBoundRect = selectedImage.getBoundingClientRect();
imgTop = scrollTop + imgBoundRect.top;
imgBound = imgTop + imgBoundRect.height;
const imgBoundRect = selectedImage.getBoundingClientRect();
const imgTop = scrollTop + imgBoundRect.top;
const imgBound = imgTop + imgBoundRect.height;
if (scrollTop < imgBound && visHeight > imgTop) {
LazyLoader.loadImage(selectedImage);
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */
(function() {
window.addEventListener('beforeunload', function() {
export default function initLogoAnimation() {
window.addEventListener('beforeunload', () => {
$('.tanuki-logo').addClass('animate');
});
}).call(window);
}
......@@ -12,7 +12,6 @@ import svg4everybody from 'svg4everybody';
// libraries with import side-effects
import 'mousetrap';
import 'mousetrap/plugins/pause/mousetrap-pause';
import 'vendor/fuzzaldrin-plus';
// expose common libraries as globals (TODO: remove these)
window.jQuery = jQuery;
......@@ -54,16 +53,12 @@ import './gl_dropdown';
import './gl_field_error';
import './gl_field_errors';
import './gl_form';
import './header';
import initTodoToggle from './header';
import initImporterStatus from './importer_status';
import './issuable_form';
import './issue';
import './issue_status_select';
import './labels_select';
import './layout_nav';
import LazyLoader from './lazy_loader';
import './line_highlighter';
import './logo';
import initLogoAnimation from './logo';
import './merge_request';
import './merge_request_tabs';
import './milestone';
......@@ -137,6 +132,8 @@ $(function () {
initBreadcrumbs();
initImporterStatus();
initTodoToggle();
initLogoAnimation();
// Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/';
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, prefer-template, no-unused-vars, no-return-assign */
/* global fuzzaldrinPlus */
import fuzzaldrinPlus from 'fuzzaldrin-plus';
(function() {
this.ProjectFindFile = (function() {
......
<script>
import { mapState, mapActions } from 'vuex';
import flash, { hideFlash } from '../../flash';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import eventHub from '../event_hub';
export default {
components: {
loadingIcon,
},
props: {
currentBranch: {
type: String,
required: true,
},
},
data() {
return {
branchName: '',
......@@ -20,11 +14,17 @@
};
},
computed: {
...mapState([
'currentBranch',
]),
btnDisabled() {
return this.loading || this.branchName === '';
},
},
methods: {
...mapActions([
'createNewBranch',
]),
toggleDropdown() {
this.$dropdown.dropdown('toggle');
},
......@@ -38,19 +38,21 @@
hideFlash(flashEl, false);
}
eventHub.$emit('createNewBranch', this.branchName);
},
showErrorMessage(message) {
this.loading = false;
flash(message, 'alert', this.$el);
},
createdNewBranch(newBranchName) {
this.loading = false;
this.branchName = '';
this.createNewBranch(this.branchName)
.then(() => {
this.loading = false;
this.branchName = '';
if (this.dropdownText) {
this.dropdownText.textContent = newBranchName;
}
if (this.dropdownText) {
this.dropdownText.textContent = this.currentBranch;
}
this.toggleDropdown();
})
.catch(res => res.json().then((data) => {
this.loading = false;
flash(data.message, 'alert', this.$el);
}));
},
},
created() {
......@@ -59,15 +61,6 @@
// text element is outside Vue app
this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
eventHub.$on('createNewBranchSuccess', this.createdNewBranch);
eventHub.$on('createNewBranchError', this.showErrorMessage);
eventHub.$on('toggleNewBranchDropdown', this.toggleDropdown);
},
destroyed() {
eventHub.$off('createNewBranchSuccess', this.createdNewBranch);
eventHub.$off('toggleNewBranchDropdown', this.toggleDropdown);
eventHub.$off('createNewBranchError', this.showErrorMessage);
},
};
</script>
......
<script>
import RepoStore from '../../stores/repo_store';
import RepoHelper from '../../helpers/repo_helper';
import eventHub from '../../event_hub';
import { mapState } from 'vuex';
import newModal from './modal.vue';
import upload from './upload.vue';
......@@ -14,9 +12,13 @@
return {
openModal: false,
modalType: '',
currentPath: RepoStore.path,
};
},
computed: {
...mapState([
'path',
]),
},
methods: {
createNewItem(type) {
this.modalType = type;
......@@ -25,19 +27,6 @@
toggleModalOpen() {
this.openModal = !this.openModal;
},
createNewEntryInStore(options, openEditMode = true) {
RepoHelper.createNewEntry(options, openEditMode);
if (options.toggleModal) {
this.toggleModalOpen();
}
},
},
created() {
eventHub.$on('createNewEntry', this.createNewEntryInStore);
},
beforeDestroy() {
eventHub.$off('createNewEntry', this.createNewEntryInStore);
},
};
</script>
......@@ -70,7 +59,7 @@
</li>
<li>
<upload
:current-path="currentPath"
:path="path"
/>
</li>
<li>
......@@ -88,7 +77,7 @@
<new-modal
v-if="openModal"
:type="modalType"
:current-path="currentPath"
:path="path"
@toggle="toggleModalOpen"
/>
</div>
......
<script>
import { mapActions } from 'vuex';
import { __ } from '../../../locale';
import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
import eventHub from '../../event_hub';
export default {
props: {
currentPath: {
type: {
type: String,
required: true,
},
type: {
path: {
type: String,
required: true,
},
},
data() {
return {
entryName: this.currentPath !== '' ? `${this.currentPath}/` : '',
entryName: this.path !== '' ? `${this.path}/` : '',
};
},
components: {
popupDialog,
},
methods: {
...mapActions([
'createTempEntry',
]),
createEntryInStore() {
eventHub.$emit('createNewEntry', {
name: this.entryName,
this.createTempEntry({
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
type: this.type,
toggleModal: true,
});
this.toggleModalOpen();
},
toggleModalOpen() {
this.$emit('toggle');
......
<script>
import eventHub from '../../event_hub';
import { mapActions } from 'vuex';
export default {
props: {
currentPath: {
path: {
type: String,
required: true,
},
},
methods: {
...mapActions([
'createTempEntry',
]),
createFile(target, file, isText) {
const { name } = file;
const nameWithPath = `${this.currentPath !== '' ? `${this.currentPath}/` : ''}${name}`;
let { result } = target;
if (!isText) {
result = result.split('base64,')[1];
}
eventHub.$emit('createNewEntry', {
name: nameWithPath,
this.createTempEntry({
name,
type: 'blob',
content: result,
toggleModal: false,
base64: !isText,
}, isText);
});
},
readFile(file) {
const reader = new FileReader();
......
<script>
import { mapState, mapGetters } from 'vuex';
import RepoSidebar from './repo_sidebar.vue';
import RepoCommitSection from './repo_commit_section.vue';
import RepoTabs from './repo_tabs.vue';
import RepoFileButtons from './repo_file_buttons.vue';
import RepoPreview from './repo_preview.vue';
import RepoMixin from '../mixins/repo_mixin';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
import eventHub from '../event_hub';
import repoEditor from './repo_editor.vue';
export default {
data() {
return Store;
computed: {
...mapState([
'currentBlobView',
]),
...mapGetters([
'isCollapsed',
'changedFiles',
]),
},
mixins: [RepoMixin],
components: {
RepoSidebar,
RepoTabs,
RepoFileButtons,
'repo-editor': MonacoLoaderHelper.repoEditorLoader,
repoEditor,
RepoCommitSection,
PopupDialog,
RepoPreview,
},
created() {
eventHub.$on('createNewBranch', this.createNewBranch);
},
mounted() {
Helper.getContent().catch(Helper.loadingError);
},
destroyed() {
eventHub.$off('createNewBranch', this.createNewBranch);
},
methods: {
getCurrentLocation() {
return location.href;
},
toggleDialogOpen(toggle) {
this.dialog.open = toggle;
},
dialogSubmitted(status) {
this.toggleDialogOpen(false);
this.dialog.status = status;
// remove tmp files
Helper.removeAllTmpFiles('openedFiles');
Helper.removeAllTmpFiles('files');
},
toggleBlobView: Store.toggleBlobView,
createNewBranch(branch) {
Service.createBranch({
branch,
ref: Store.currentBranch,
}).then((res) => {
const newBranchName = res.data.name;
const newUrl = this.getCurrentLocation().replace(Store.currentBranch, newBranchName);
Store.currentBranch = newBranchName;
history.pushState({ key: Helper.key }, '', newUrl);
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = (e) => {
if (!this.changedFiles.length) return undefined;
eventHub.$emit('createNewBranchSuccess', newBranchName);
eventHub.$emit('toggleNewBranchDropdown');
}).catch((err) => {
eventHub.$emit('createNewBranchError', err.response.data.message);
Object.assign(e, {
returnValue,
});
},
return returnValue;
};
},
};
</script>
<template>
<div class="repository-view">
<div class="tree-content-holder" :class="{'tree-content-holder-mini' : isMini}">
<div class="tree-content-holder" :class="{'tree-content-holder-mini' : isCollapsed}">
<repo-sidebar/>
<div v-if="isMini"
class="panel-right"
:class="{'edit-mode': editMode}">
<div
v-if="isCollapsed"
class="panel-right"
>
<repo-tabs/>
<component
:is="currentBlobView"
class="blob-viewer-container"/>
/>
<repo-file-buttons/>
</div>
</div>
<repo-commit-section/>
<popup-dialog
v-show="dialog.open"
:primary-button-label="__('Discard changes')"
kind="warning"
:title="__('Are you sure?')"
:text="__('Are you sure you want to discard your changes?')"
@toggle="toggleDialogOpen"
@submit="dialogSubmitted"
/>
<repo-commit-section v-if="changedFiles.length" />
</div>
</template>
<script>
import Flash from '../../flash';
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
import Service from '../services/repo_service';
import { mapGetters, mapState, mapActions } from 'vuex';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import { visitUrl } from '../../lib/utils/url_utility';
import { n__ } from '../../locale';
export default {
mixins: [RepoMixin],
data() {
return Store;
},
components: {
PopupDialog,
},
data() {
return {
showNewBranchDialog: false,
submitCommitsLoading: false,
startNewMR: false,
commitMessage: '',
};
},
computed: {
showCommitable() {
return this.isCommitable && this.changedFiles.length;
},
branchPaths() {
return this.changedFiles.map(f => f.path);
},
cantCommitYet() {
...mapState([
'currentBranch',
]),
...mapGetters([
'changedFiles',
]),
commitButtonDisabled() {
return !this.commitMessage || this.submitCommitsLoading;
},
filePluralize() {
return this.changedFiles.length > 1 ? 'files' : 'file';
commitButtonText() {
return n__('Commit %d file', 'Commit %d files', this.changedFiles.length);
},
},
methods: {
commitToNewBranch(status) {
if (status) {
this.showNewBranchDialog = false;
this.tryCommit(null, true, true);
} else {
// reset the state
}
},
...mapActions([
'checkCommitStatus',
'commitChanges',
'getTreeData',
]),
makeCommit(newBranch = false) {
const createNewBranch = newBranch || this.startNewMR;
makeCommit(newBranch) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.newContent,
encoding: f.base64 ? 'base64' : 'text',
}));
const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch;
const payload = {
branch,
commit_message: commitMessage,
actions,
branch: createNewBranch ? `${this.currentBranch}-${new Date().getTime().toString()}` : this.currentBranch,
commit_message: this.commitMessage,
actions: this.changedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.content,
encoding: f.base64 ? 'base64' : 'text',
})),
start_branch: createNewBranch ? this.currentBranch : undefined,
};
if (newBranch) {
payload.start_branch = this.currentBranch;
}
Service.commitFiles(payload)
this.showNewBranchDialog = false;
this.submitCommitsLoading = true;
this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => {
this.resetCommitState();
if (this.startNewMR) {
this.redirectToNewMr(branch);
} else {
this.redirectToBranch(branch);
}
this.submitCommitsLoading = false;
this.getTreeData();
})
.catch(() => {
Flash('An error occurred while committing your changes');
this.submitCommitsLoading = false;
});
},
tryCommit(e, skipBranchCheck = false, newBranch = false) {
tryCommit() {
this.submitCommitsLoading = true;
if (skipBranchCheck) {
this.makeCommit(newBranch);
} else {
Store.setBranchHash()
.then(() => {
if (Store.branchChanged) {
Store.showNewBranchDialog = true;
return;
}
this.makeCommit(newBranch);
})
.catch(() => {
this.submitCommitsLoading = false;
Flash('An error occurred while committing your changes');
});
}
},
redirectToNewMr(branch) {
visitUrl(this.newMrTemplateUrl.replace('{{source_branch}}', branch));
},
redirectToBranch(branch) {
visitUrl(this.customBranchURL.replace('{{branch}}', branch));
},
resetCommitState() {
this.submitCommitsLoading = false;
this.openedFiles = this.openedFiles.map((file) => {
const f = file;
f.changed = false;
return f;
});
this.changedFiles = [];
this.commitMessage = '';
this.editMode = false;
window.scrollTo(0, 0);
this.checkCommitStatus()
.then((branchChanged) => {
if (branchChanged) {
this.showNewBranchDialog = true;
} else {
this.makeCommit();
}
})
.catch(() => {
this.submitCommitsLoading = false;
});
},
},
};
</script>
<template>
<div
v-if="showCommitable"
id="commit-area">
<div id="commit-area">
<popup-dialog
v-if="showNewBranchDialog"
:primary-button-label="__('Create new branch')"
kind="primary"
:title="__('Branch has changed')"
:text="__('This branch has changed since you started editing. Would you like to create a new branch?')"
@submit="commitToNewBranch"
@toggle="showNewBranchDialog = false"
@submit="makeCommit(true)"
/>
<form
class="form-horizontal"
@submit.prevent="tryCommit">
@submit.prevent="tryCommit()">
<fieldset>
<div class="form-group">
<label class="col-md-4 control-label staged-files">
......@@ -145,10 +103,10 @@ export default {
<div class="col-md-6">
<ul class="list-unstyled changed-files">
<li
v-for="branchPath in branchPaths"
:key="branchPath">
v-for="(file, index) in changedFiles"
:key="index">
<span class="help-block">
{{branchPath}}
{{ file.path }}
</span>
</li>
</ul>
......@@ -183,9 +141,8 @@ export default {
</div>
<div class="col-md-offset-4 col-md-6">
<button
ref="submitCommit"
type="submit"
:disabled="cantCommitYet"
:disabled="commitButtonDisabled"
class="btn btn-success">
<i
v-if="submitCommitsLoading"
......@@ -194,7 +151,7 @@ export default {
aria-label="loading">
</i>
<span class="commit-summary">
Commit {{changedFiles.length}} {{filePluralize}}
{{ commitButtonText }}
</span>
</button>
</div>
......
<script>
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
import { mapGetters, mapActions, mapState } from 'vuex';
import popupDialog from '../../vue_shared/components/popup_dialog.vue';
export default {
data() {
return Store;
components: {
popupDialog,
},
mixins: [RepoMixin],
computed: {
...mapState([
'editMode',
'discardPopupOpen',
]),
...mapGetters([
'canEditFile',
]),
buttonLabel() {
return this.editMode ? this.__('Cancel edit') : this.__('Edit');
},
showButton() {
return this.isCommitable &&
!this.activeFile.render_error &&
!this.binary &&
this.openedFiles.length;
},
},
methods: {
editCancelClicked() {
if (this.changedFiles.length) {
this.dialog.open = true;
return;
}
this.editMode = !this.editMode;
Store.toggleBlobView();
},
...mapActions([
'toggleEditMode',
'closeDiscardPopup',
]),
},
};
</script>
<template>
<button
v-if="showButton"
class="btn btn-default"
type="button"
@click.prevent="editCancelClicked">
<i
v-if="!editMode"
class="fa fa-pencil"
aria-hidden="true">
</i>
<span>
{{buttonLabel}}
</span>
</button>
<div class="editable-mode">
<button
v-if="canEditFile"
class="btn btn-default"
type="button"
@click.prevent="toggleEditMode()">
<i
v-if="!editMode"
class="fa fa-pencil"
aria-hidden="true">
</i>
<span>
{{buttonLabel}}
</span>
</button>
<popup-dialog
v-if="discardPopupOpen"
class="text-left"
:primary-button-label="__('Discard changes')"
kind="warning"
:title="__('Are you sure?')"
:text="__('Are you sure you want to discard your changes?')"
@toggle="closeDiscardPopup"
@submit="toggleEditMode(true)"
/>
</div>
</template>
<script>
/* global monaco */
import Store from '../stores/repo_store';
import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper';
const RepoEditor = {
data() {
return Store;
},
import { mapGetters, mapActions } from 'vuex';
import flash from '../../flash';
import monacoLoader from '../monaco_loader';
export default {
destroyed() {
if (Helper.monacoInstance) {
Helper.monacoInstance.destroy();
if (this.monacoInstance) {
this.monacoInstance.destroy();
}
},
mounted() {
Service.getRaw(this.activeFile)
.then((rawResponse) => {
Store.blobRaw = rawResponse.data;
Store.activeFile.plain = rawResponse.data;
const monacoInstance = Helper.monaco.editor.create(this.$el, {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
});
if (this.monaco) {
this.initMonaco();
} else {
monacoLoader(['vs/editor/editor.main'], () => {
this.monaco = monaco;
this.initMonaco();
});
}
},
methods: {
...mapActions([
'getRawFileData',
'changeFileContent',
]),
initMonaco() {
if (this.monacoInstance) {
this.monacoInstance.setModel(null);
}
Helper.monacoInstance = monacoInstance;
this.getRawFileData(this.activeFile)
.then(() => {
if (!this.monacoInstance) {
this.monacoInstance = this.monaco.editor.create(this.$el, {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
});
this.addMonacoEvents();
this.languages = this.monaco.languages.getLanguages();
this.setupEditor();
})
.catch(Helper.loadingError);
},
this.addMonacoEvents();
}
methods: {
this.setupEditor();
})
.catch(() => flash('Error setting up monaco. Please try again.'));
},
setupEditor() {
this.showHide();
if (!this.activeFile) return;
const content = this.activeFile.content !== '' ? this.activeFile.content : this.activeFile.raw;
Helper.setMonacoModelFromLanguage();
},
const foundLang = this.languages.find(lang =>
lang.extensions && lang.extensions.indexOf(this.activeFileExtension) === 0,
);
const newModel = this.monaco.editor.createModel(
content, foundLang ? foundLang.id : 'plaintext',
);
showHide() {
if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
this.$el.style.display = 'none';
} else {
this.$el.style.display = 'inline-block';
}
this.monacoInstance.setModel(newModel);
},
addMonacoEvents() {
Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
},
onMonacoEditorKeysPressed() {
Store.setActiveFileContents(Helper.monacoInstance.getValue());
},
onMonacoEditorMouseUp(e) {
if (!e.target.position) return;
const lineNumber = e.target.position.lineNumber;
if (e.target.element.classList.contains('line-numbers')) {
location.hash = `L${lineNumber}`;
Store.setActiveLine(lineNumber);
}
this.monacoInstance.onKeyUp(() => {
this.changeFileContent({
file: this.activeFile,
content: this.monacoInstance.getValue(),
});
});
},
},
watch: {
dialog: {
handler(obj) {
const newObj = obj;
if (newObj.status) {
newObj.status = false;
this.openedFiles = this.openedFiles.map((file) => {
const f = file;
if (f.active) {
this.blobRaw = f.plain;
}
f.changed = false;
delete f.newContent;
return f;
});
this.editMode = false;
Store.toggleBlobView();
}
},
deep: true,
},
blobRaw() {
if (Helper.monacoInstance) {
this.setupEditor();
}
},
activeLine() {
if (Helper.monacoInstance) {
Helper.monacoInstance.setPosition({
lineNumber: this.activeLine,
column: 1,
});
activeFile(oldVal, newVal) {
if (newVal && !newVal.active) {
this.initMonaco();
}
},
},
computed: {
...mapGetters([
'activeFile',
'activeFileExtension',
]),
shouldHideEditor() {
return !this.openedFiles.length || (this.binary && !this.activeFile.raw);
return this.activeFile.binary && !this.activeFile.raw;
},
},
};
export default RepoEditor;
</script>
<template>
<div id="ide" v-if='!shouldHideEditor'></div>
<div
id="ide"
v-if='!shouldHideEditor'
class="blob-viewer-container blob-editor-container"
>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
import eventHub from '../event_hub';
import repoMixin from '../mixins/repo_mixin';
export default {
mixins: [
repoMixin,
timeAgoMixin,
],
props: {
......@@ -15,13 +13,15 @@
},
},
computed: {
...mapGetters([
'isCollapsed',
]),
fileIcon() {
const classObj = {
return {
'fa-spinner fa-spin': this.file.loading,
[this.file.icon]: !this.file.loading,
'fa-folder-open': !this.file.loading && this.file.opened,
};
return classObj;
},
levelIndentation() {
return {
......@@ -33,9 +33,9 @@
},
},
methods: {
linkClicked(file) {
eventHub.$emit('fileNameClicked', file);
},
...mapActions([
'clickedTreeRow',
]),
},
};
</script>
......@@ -43,7 +43,7 @@
<template>
<tr
class="file"
@click.prevent="linkClicked(file)">
@click.prevent="clickedTreeRow(file)">
<td>
<i
class="fa fa-fw file-icon"
......@@ -71,7 +71,7 @@
</template>
</td>
<template v-if="!isMini">
<template v-if="!isCollapsed">
<td class="hidden-sm hidden-xs">
<a
@click.stop
......
<script>
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import RepoMixin from '../mixins/repo_mixin';
const RepoFileButtons = {
data() {
return Store;
},
mixins: [RepoMixin],
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters([
'activeFile',
]),
showButtons() {
return this.activeFile.raw_path ||
this.activeFile.blame_path ||
this.activeFile.commits_path ||
return this.activeFile.rawPath ||
this.activeFile.blamePath ||
this.activeFile.commitsPath ||
this.activeFile.permalink;
},
rawDownloadButtonLabel() {
return this.binary ? 'Download' : 'Raw';
},
canPreview() {
return Helper.isRenderable();
return this.activeFile.binary ? 'Download' : 'Raw';
},
},
methods: {
rawPreviewToggle: Store.toggleRawPreview,
},
};
export default RepoFileButtons;
</script>
<template>
......@@ -40,11 +25,11 @@ export default RepoFileButtons;
class="repo-file-buttons"
>
<a
:href="activeFile.raw_path"
:href="activeFile.rawPath"
target="_blank"
class="btn btn-default raw"
rel="noopener noreferrer">
{{rawDownloadButtonLabel}}
{{ rawDownloadButtonLabel }}
</a>
<div
......@@ -52,12 +37,12 @@ export default RepoFileButtons;
role="group"
aria-label="File actions">
<a
:href="activeFile.blame_path"
:href="activeFile.blamePath"
class="btn btn-default blame">
Blame
</a>
<a
:href="activeFile.commits_path"
:href="activeFile.commitsPath"
class="btn btn-default history">
History
</a>
......@@ -67,13 +52,5 @@ export default RepoFileButtons;
Permalink
</a>
</div>
<a
v-if="canPreview"
href="#"
@click.prevent="rawPreviewToggle"
class="btn btn-default preview">
{{activeFileLabel}}
</a>
</div>
</template>
<script>
import repoMixin from '../mixins/repo_mixin';
import { mapGetters } from 'vuex';
export default {
mixins: [
repoMixin,
],
computed: {
...mapGetters([
'isCollapsed',
]),
},
methods: {
lineOfCode(n) {
return `skeleton-line-${n}`;
......@@ -28,7 +30,7 @@
</div>
</div>
</td>
<template v-if="!isMini">
<template v-if="!isCollapsed">
<td
class="hidden-sm hidden-xs">
<div class="animation-container">
......
<script>
import eventHub from '../event_hub';
import repoMixin from '../mixins/repo_mixin';
import { mapGetters, mapState, mapActions } from 'vuex';
export default {
mixins: [
repoMixin,
],
props: {
prevUrl: {
type: String,
required: true,
},
},
computed: {
...mapState([
'parentTreeUrl',
]),
...mapGetters([
'isCollapsed',
]),
colSpanCondition() {
return this.isMini ? undefined : 3;
return this.isCollapsed ? undefined : 3;
},
},
methods: {
linkClicked(file) {
eventHub.$emit('goToPreviousDirectoryClicked', file);
},
...mapActions([
'getTreeData',
]),
},
};
</script>
......@@ -30,9 +26,9 @@
<td
:colspan="colSpanCondition"
class="table-cell"
@click.prevent="linkClicked(prevUrl)"
@click.prevent="getTreeData({ endpoint: parentTreeUrl })"
>
<a :href="prevUrl">...</a>
<a :href="parentTreeUrl">...</a>
</td>
</tr>
</template>
<script>
/* global LineHighlighter */
import Store from '../stores/repo_store';
import { mapGetters } from 'vuex';
export default {
data() {
return Store;
},
computed: {
html() {
return this.activeFile.html;
...mapGetters([
'activeFile',
]),
renderErrorTooLarge() {
return this.activeFile.renderError === 'too_large';
},
},
methods: {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
highlightLine() {
if (Store.activeLine > -1) {
this.lineHighlighter.highlightHash(`#L${Store.activeLine}`);
}
},
},
mounted() {
this.highlightFile();
......@@ -29,24 +23,18 @@ export default {
scrollFileHolder: true,
});
},
watch: {
html() {
this.$nextTick(() => {
this.highlightFile();
this.highlightLine();
});
},
activeLine() {
this.highlightLine();
},
updated() {
this.$nextTick(() => {
this.highlightFile();
});
},
};
</script>
<template>
<div>
<div class="blob-viewer-container">
<div
v-if="!activeFile.render_error"
v-if="!activeFile.renderError"
v-html="activeFile.html">
</div>
<div
......@@ -57,17 +45,17 @@ export default {
</p>
</div>
<div
v-else-if="activeFile.tooLarge"
v-else-if="renderErrorTooLarge"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.
The source could not be displayed because it is too large. You can <a :href="activeFile.rawPath" download>download</a> it instead.
</p>
</div>
<div
v-else
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.raw_path">download</a> it instead.
The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.rawPath" download>download</a> it instead.
</p>
</div>
</div>
......
<script>
import _ from 'underscore';
import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper';
import Store from '../stores/repo_store';
import eventHub from '../event_hub';
import { mapState, mapGetters, mapActions } from 'vuex';
import RepoPreviousDirectory from './repo_prev_directory.vue';
import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue';
import RepoMixin from '../mixins/repo_mixin';
export default {
mixins: [RepoMixin],
components: {
'repo-previous-directory': RepoPreviousDirectory,
'repo-file': RepoFile,
'repo-loading-file': RepoLoadingFile,
},
created() {
window.addEventListener('popstate', this.checkHistory);
window.addEventListener('popstate', this.popHistoryState);
},
destroyed() {
eventHub.$off('fileNameClicked', this.fileClicked);
eventHub.$off('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
window.removeEventListener('popstate', this.checkHistory);
window.removeEventListener('popstate', this.popHistoryState);
},
mounted() {
eventHub.$on('fileNameClicked', this.fileClicked);
eventHub.$on('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
},
data() {
return Store;
this.getTreeData();
},
computed: {
flattendFiles() {
const mapFiles = arr => (!arr.files.length ? [] : _.map(arr.files, a => [a, mapFiles(a)]));
return _.chain(this.files)
.map(arr => [arr, mapFiles(arr)])
.flatten()
.value();
},
...mapState([
'loading',
'isRoot',
]),
...mapState({
projectName(state) {
return state.project.name;
},
}),
...mapGetters([
'treeList',
'isCollapsed',
]),
},
methods: {
checkHistory() {
let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1);
if (!selectedFile) {
// Maybe it is not in the current tree but in the opened tabs
selectedFile = Helper.getFileFromPath(location.pathname);
}
let lineNumber = null;
if (location.hash.indexOf('#L') > -1) lineNumber = Number(location.hash.substr(2));
if (selectedFile) {
if (selectedFile.url !== this.activeFile.url) {
this.fileClicked(selectedFile, lineNumber);
} else {
Store.setActiveLine(lineNumber);
}
} else {
// Not opened at all lets open new tab
this.fileClicked({
url: location.href,
}, lineNumber);
}
},
fileClicked(clickedFile, lineNumber) {
const file = clickedFile;
if (file.loading) return;
if (file.type === 'tree' && file.opened) {
Helper.setDirectoryToClosed(file);
Store.setActiveLine(lineNumber);
} else if (file.type === 'submodule') {
file.loading = true;
gl.utils.visitUrl(file.url);
} else {
const openFile = Helper.getFileFromPath(file.url);
if (openFile) {
Store.setActiveFiles(openFile);
Store.setActiveLine(lineNumber);
} else {
file.loading = true;
Service.url = file.url;
Helper.getContent(file)
.then(() => {
file.loading = false;
Helper.scrollTabsRight();
Store.setActiveLine(lineNumber);
})
.catch(Helper.loadingError);
}
}
},
goToPreviousDirectoryClicked(prevURL) {
Service.url = prevURL;
Helper.getContent(null, true)
.then(() => Helper.scrollTabsRight())
.catch(Helper.loadingError);
},
...mapActions([
'getTreeData',
'popHistoryState',
]),
},
};
</script>
<template>
<div id="sidebar" :class="{'sidebar-mini' : isMini}">
<div id="sidebar" :class="{'sidebar-mini' : isCollapsed}">
<table class="table">
<thead>
<tr>
<th
v-if="isMini"
v-if="isCollapsed"
class="repo-file-options title"
>
<strong class="clgray">
......@@ -136,17 +71,16 @@ export default {
</thead>
<tbody>
<repo-previous-directory
v-if="!isRoot && !loading.tree"
:prev-url="prevURL"
v-if="!isRoot && treeList.length"
/>
<repo-loading-file
v-if="!flattendFiles.length && loading.tree"
v-if="!treeList.length && loading"
v-for="n in 5"
:key="n"
/>
<repo-file
v-for="file in flattendFiles"
:key="file.id"
v-for="(file, index) in treeList"
:key="index"
:file="file"
/>
</tbody>
......
<script>
import Store from '../stores/repo_store';
import { mapActions } from 'vuex';
const RepoTab = {
export default {
props: {
tab: {
type: Object,
......@@ -11,7 +11,7 @@ const RepoTab = {
computed: {
closeLabel() {
if (this.tab.changed) {
if (this.tab.changed || this.tab.tempFile) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
......@@ -26,29 +26,23 @@ const RepoTab = {
},
methods: {
tabClicked(file) {
Store.setActiveFiles(file);
},
closeTab(file) {
if (file.changed || file.tempFile) return;
Store.removeFromOpenedFiles(file);
},
...mapActions([
'setFileActive',
'closeFile',
]),
},
};
export default RepoTab;
</script>
<template>
<li
:class="{ active : tab.active }"
@click="tabClicked(tab)"
@click="setFileActive(tab)"
>
<button
type="button"
class="close-btn"
@click.stop.prevent="closeTab(tab)"
@click.stop.prevent="closeFile({ file: tab })"
:aria-label="closeLabel">
<i
class="fa"
......@@ -61,7 +55,7 @@ export default RepoTab;
href="#"
class="repo-tab"
:title="tab.url"
@click.prevent="tabClicked(tab)">
@click.prevent.stop="setFileActive(tab)">
{{tab.name}}
</a>
</li>
......
<script>
import Store from '../stores/repo_store';
import { mapState } from 'vuex';
import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin';
export default {
mixins: [RepoMixin],
components: {
'repo-tab': RepoTab,
},
data() {
return Store;
computed: {
...mapState([
'openFiles',
]),
},
};
</script>
......@@ -20,7 +20,7 @@
class="list-unstyled"
>
<repo-tab
v-for="tab in openedFiles"
v-for="tab in openFiles"
:key="tab.id"
:tab="tab"
/>
......
import Vue from 'vue';
export default new Vue();
/* global monaco */
import RepoEditor from '../components/repo_editor.vue';
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import monacoLoader from '../monaco_loader';
function repoEditorLoader() {
Store.monacoLoading = true;
return new Promise((resolve, reject) => {
monacoLoader(['vs/editor/editor.main'], () => {
Helper.monaco = monaco;
Store.monacoLoading = false;
resolve(RepoEditor);
}, () => {
Store.monacoLoading = false;
reject();
});
});
}
const MonacoLoaderHelper = {
repoEditorLoader,
};
export default MonacoLoaderHelper;
import Service from '../services/repo_service';
import Store from '../stores/repo_store';
import Flash from '../../flash';
const RepoHelper = {
monacoInstance: null,
getDefaultActiveFile() {
return {
id: '',
active: true,
binary: false,
extension: '',
html: '',
mime_type: '',
name: '',
plain: '',
size: 0,
url: '',
raw: false,
newContent: '',
changed: false,
loading: false,
};
},
key: '',
Time: window.performance
&& window.performance.now
? window.performance
: Date,
getFileExtension(fileName) {
return fileName.split('.').pop();
},
getLanguageIDForFile(file, langs) {
const ext = RepoHelper.getFileExtension(file.name);
const foundLang = RepoHelper.findLanguage(ext, langs);
return foundLang ? foundLang.id : 'plaintext';
},
setMonacoModelFromLanguage() {
RepoHelper.monacoInstance.setModel(null);
const languages = RepoHelper.monaco.languages.getLanguages();
const languageID = RepoHelper.getLanguageIDForFile(Store.activeFile, languages);
const newModel = RepoHelper.monaco.editor.createModel(Store.blobRaw, languageID);
RepoHelper.monacoInstance.setModel(newModel);
},
findLanguage(ext, langs) {
return langs.find(lang => lang.extensions && lang.extensions.indexOf(`.${ext}`) > -1);
},
setDirectoryOpen(tree, title) {
if (!tree) return;
Object.assign(tree, {
opened: true,
});
RepoHelper.updateHistoryEntry(tree.url, title);
Store.path = tree.path;
},
setDirectoryToClosed(entry) {
Object.assign(entry, {
opened: false,
files: [],
});
},
isRenderable() {
const okExts = ['md', 'svg'];
return okExts.indexOf(Store.activeFile.extension) > -1;
},
setBinaryDataAsBase64(file) {
Service.getBase64Content(file.raw_path)
.then((response) => {
Store.blobRaw = response;
file.base64 = response; // eslint-disable-line no-param-reassign
})
.catch(RepoHelper.loadingError);
},
getContent(treeOrFile, emptyFiles = false) {
let file = treeOrFile;
if (!Store.files.length) {
Store.loading.tree = true;
}
return Service.getContent()
.then((response) => {
const data = response.data;
if (response.headers && response.headers['page-title']) data.pageTitle = decodeURI(response.headers['page-title']);
if (data.path && !Store.isInitialRoot) {
Store.isRoot = data.path === '/';
Store.isInitialRoot = Store.isRoot;
}
if (file && file.type === 'blob') {
if (!file) file = data;
Store.binary = data.binary;
if (data.binary) {
// file might be undefined
RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview();
} else if (!Store.isPreviewView() && !data.render_error) {
Service.getRaw(data)
.then((rawResponse) => {
Store.blobRaw = rawResponse.data;
data.plain = rawResponse.data;
RepoHelper.setFile(data, file);
}).catch(RepoHelper.loadingError);
}
if (Store.isPreviewView()) {
RepoHelper.setFile(data, file);
}
} else {
Store.loading.tree = false;
RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name);
if (emptyFiles) {
Store.files = [];
}
this.addToDirectory(file, data);
Store.prevURL = Service.blobURLtoParentTree(Service.url);
}
}).catch(RepoHelper.loadingError);
},
addToDirectory(file, data) {
const tree = file || Store;
// TODO: Figure out why `popstate` is being trigger in the specs
if (!tree.files) return;
const files = tree.files.concat(this.dataToListOfFiles(data, file ? file.level + 1 : 0));
tree.files = files;
},
setFile(data, file) {
const newFile = data;
newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh.
if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
newFile.tooLarge = true;
}
newFile.newContent = file.newContent ? file.newContent : '';
Store.addToOpenedFiles(newFile);
Store.setActiveFiles(newFile);
},
serializeRepoEntity(type, entity, level = 0) {
const {
id,
url,
name,
icon,
last_commit,
tree_url,
path,
tempFile,
active,
opened,
} = entity;
return {
id,
type,
name,
url,
tree_url,
path,
level,
tempFile,
icon: `fa-${icon}`,
files: [],
loading: false,
opened,
active,
// eslint-disable-next-line camelcase
lastCommit: last_commit ? {
url: `${Store.projectUrl}/commit/${last_commit.id}`,
message: last_commit.message,
updatedAt: last_commit.committed_date,
} : {},
};
},
scrollTabsRight() {
const tabs = document.getElementById('tabs');
if (!tabs) return;
tabs.scrollLeft = tabs.scrollWidth;
},
dataToListOfFiles(data, level) {
const { blobs, trees, submodules } = data;
return [
...trees.map(tree => RepoHelper.serializeRepoEntity('tree', tree, level)),
...submodules.map(submodule => RepoHelper.serializeRepoEntity('submodule', submodule, level)),
...blobs.map(blob => RepoHelper.serializeRepoEntity('blob', blob, level)),
];
},
genKey() {
return RepoHelper.Time.now().toFixed(3);
},
updateHistoryEntry(url, title) {
const history = window.history;
RepoHelper.key = RepoHelper.genKey();
if (document.location.pathname !== url) {
history.pushState({ key: RepoHelper.key }, '', url);
}
if (title) {
document.title = title;
}
},
findOpenedFileFromActive() {
return Store.openedFiles.find(openedFile => Store.activeFile.id === openedFile.id);
},
getFileFromPath(path) {
return Store.openedFiles.find(file => file.url === path);
},
loadingError() {
Flash('Unable to load this content at this time.');
},
openEditMode() {
Store.editMode = true;
Store.currentBlobView = 'repo-editor';
},
updateStorePath(path) {
Store.path = path;
},
findOrCreateEntry(type, tree, name) {
let exists = true;
let foundEntry = tree.files.find(dir => dir.type === type && dir.name === name);
if (!foundEntry) {
foundEntry = RepoHelper.serializeRepoEntity(type, {
id: name,
name,
path: tree.path ? `${tree.path}/${name}` : name,
icon: type === 'tree' ? 'folder' : 'file-text-o',
tempFile: true,
opened: true,
active: true,
}, tree.level !== undefined ? tree.level + 1 : 0);
exists = false;
tree.files.push(foundEntry);
}
return {
entry: foundEntry,
exists,
};
},
removeAllTmpFiles(storeFilesKey) {
Store[storeFilesKey] = Store[storeFilesKey].filter(f => !f.tempFile);
},
createNewEntry(options, openEditMode = true) {
const {
name,
type,
content = '',
base64 = false,
} = options;
const originalPath = Store.path;
let entryName = name;
if (entryName.indexOf(`${originalPath}/`) !== 0) {
this.updateStorePath('');
} else {
entryName = entryName.replace(`${originalPath}/`, '');
}
if (entryName === '') return;
const fileName = type === 'tree' ? '.gitkeep' : entryName;
let tree = Store;
if (type === 'tree') {
const dirNames = entryName.split('/');
dirNames.forEach((dirName) => {
if (dirName === '') return;
tree = this.findOrCreateEntry('tree', tree, dirName).entry;
});
}
if ((type === 'tree' && tree.tempFile) || type === 'blob') {
const file = this.findOrCreateEntry('blob', tree, fileName);
if (file.exists) {
Flash(`The name "${file.entry.name}" is already taken in this directory.`);
} else {
const { entry } = file;
entry.newContent = content;
entry.base64 = base64;
if (entry.base64) {
entry.render_error = true;
}
this.setFile(entry, entry);
if (openEditMode) {
this.openEditMode();
} else {
file.entry.render_error = 'asdsad';
}
}
}
this.updateStorePath(originalPath);
},
};
export default RepoHelper;
import $ from 'jquery';
import Vue from 'vue';
import { mapActions } from 'vuex';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import Service from './services/repo_service';
import Store from './stores/repo_store';
import Repo from './components/repo.vue';
import RepoEditButton from './components/repo_edit_button.vue';
import newBranchForm from './components/new_branch_form.vue';
import newDropdown from './components/new_dropdown/index.vue';
import store from './stores';
import Translate from '../vue_shared/translate';
function initDropdowns() {
$('.js-tree-ref-target-holder').hide();
}
function addEventsForNonVueEls() {
window.onbeforeunload = function confirmUnload(e) {
const hasChanged = Store.openedFiles
.some(file => file.changed);
if (!hasChanged) return undefined;
const event = e || window.event;
if (event) event.returnValue = 'Are you sure you want to lose unsaved changes?';
// For Safari
return 'Are you sure you want to lose unsaved changes?';
};
}
function setInitialStore(data) {
Store.service = Service;
Store.service.url = data.url;
Store.service.refsUrl = data.refsUrl;
Store.path = data.currentPath;
Store.projectId = data.projectId;
Store.projectName = data.projectName;
Store.projectUrl = data.projectUrl;
Store.canCommit = data.canCommit;
Store.onTopOfBranch = data.onTopOfBranch;
Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl);
Store.customBranchURL = decodeURIComponent(data.blobUrl);
Store.isRoot = convertPermissionToBoolean(data.root);
Store.isInitialRoot = convertPermissionToBoolean(data.root);
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
Store.checkIsCommitable();
Store.setBranchHash();
}
function initRepo(el) {
if (!el) return null;
return new Vue({
el,
store,
components: {
repo: Repo,
},
methods: {
...mapActions([
'setInitialData',
]),
},
created() {
const data = el.dataset;
this.setInitialData({
project: {
id: data.projectId,
name: data.projectName,
url: data.projectUrl,
},
endpoints: {
rootEndpoint: data.url,
newMergeRequestUrl: data.newMergeRequestUrl,
rootUrl: data.rootUrl,
},
canCommit: convertPermissionToBoolean(data.canCommit),
onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
currentRef: data.ref,
path: data.currentPath,
currentBranch: data.currentBranch,
isRoot: convertPermissionToBoolean(data.root),
isInitialRoot: convertPermissionToBoolean(data.root),
});
},
render(createElement) {
return createElement('repo');
},
......@@ -59,15 +54,20 @@ function initRepo(el) {
function initRepoEditButton(el) {
return new Vue({
el,
store,
components: {
repoEditButton: RepoEditButton,
},
render(createElement) {
return createElement('repo-edit-button');
},
});
}
function initNewDropdown(el) {
return new Vue({
el,
store,
components: {
newDropdown,
},
......@@ -87,32 +87,20 @@ function initNewBranchForm() {
components: {
newBranchForm,
},
store,
render(createElement) {
return createElement('new-branch-form', {
props: {
currentBranch: Store.currentBranch,
},
});
return createElement('new-branch-form');
},
});
}
function initRepoBundle() {
const repo = document.getElementById('repo');
const editButton = document.querySelector('.editable-mode');
const newDropdownHolder = document.querySelector('.js-new-dropdown');
setInitialStore(repo.dataset);
addEventsForNonVueEls();
initDropdowns();
Vue.use(Translate);
initRepo(repo);
initRepoEditButton(editButton);
initNewBranchForm();
initNewDropdown(newDropdownHolder);
}
const repo = document.getElementById('repo');
const editButton = document.querySelector('.editable-mode');
const newDropdownHolder = document.querySelector('.js-new-dropdown');
$(initRepoBundle);
Vue.use(Translate);
export default initRepoBundle;
initRepo(repo);
initRepoEditButton(editButton);
initNewBranchForm();
initNewDropdown(newDropdownHolder);
import Store from '../stores/repo_store';
const RepoMixin = {
computed: {
isMini() {
return !!Store.openedFiles.length;
},
changedFiles() {
const changedFileList = this.openedFiles
.filter(file => file.changed || file.tempFile);
return changedFileList;
},
},
};
export default RepoMixin;
import Vue from 'vue';
import VueResource from 'vue-resource';
import Api from '../../api';
Vue.use(VueResource);
export default {
getTreeData(endpoint) {
return Vue.http.get(endpoint, { params: { format: 'json' } });
},
getFileData(endpoint) {
return Vue.http.get(endpoint, { params: { format: 'json' } });
},
getRawFileData(file) {
if (file.tempFile) {
return Promise.resolve(file.content);
}
return Vue.http.get(file.rawPath, { params: { format: 'json' } })
.then(res => res.text());
},
getBranchData(projectId, currentBranch) {
return Api.branchSingle(projectId, currentBranch);
},
createBranch(projectId, payload) {
const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
return Vue.http.post(url, payload);
},
commit(projectId, payload) {
return Api.commitMultiple(projectId, payload);
},
};
import axios from 'axios';
import csrf from '../../lib/utils/csrf';
import Store from '../stores/repo_store';
import Api from '../../api';
import Helper from '../helpers/repo_helper';
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
const RepoService = {
url: '',
options: {
params: {
format: 'json',
},
},
createBranchPath: '/api/:version/projects/:id/repository/branches',
richExtensionRegExp: /md/,
getRaw(file) {
if (file.tempFile) {
return Promise.resolve({
data: file.newContent ? file.newContent : '',
});
}
return axios.get(file.raw_path, {
// Stop Axios from parsing a JSON file into a JS object
transformResponse: [res => res],
});
},
buildParams(url = this.url) {
// shallow clone object without reference
const params = Object.assign({}, this.options.params);
if (this.urlIsRichBlob(url)) params.viewer = 'rich';
return params;
},
urlIsRichBlob(url = this.url) {
const extension = Helper.getFileExtension(url);
return this.richExtensionRegExp.test(extension);
},
getContent(url = this.url) {
const params = this.buildParams(url);
return axios.get(url, {
params,
});
},
getBase64Content(url = this.url) {
const request = axios.get(url, {
responseType: 'arraybuffer',
});
return request.then(response => this.bufferToBase64(response.data));
},
bufferToBase64(data) {
return new Buffer(data, 'binary').toString('base64');
},
blobURLtoParentTree(url) {
const urlArray = url.split('/');
urlArray.pop();
const blobIndex = urlArray.lastIndexOf('blob');
if (blobIndex > -1) urlArray[blobIndex] = 'tree';
return urlArray.join('/');
},
getBranch() {
return Api.branchSingle(Store.projectId, Store.currentBranch);
},
commitFiles(payload) {
return Api.commitMultiple(Store.projectId, payload)
.then(this.commitFlash);
},
createBranch(payload) {
const url = Api.buildUrl(this.createBranchPath)
.replace(':id', Store.projectId);
return axios.post(url, payload);
},
commitFlash(data) {
if (data.short_id && data.stats) {
window.Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
} else {
window.Flash(data.message);
}
},
};
export default RepoService;
import Vue from 'vue';
import flash from '../../flash';
import service from '../services';
import * as types from './mutation_types';
export const redirectToUrl = url => gl.utils.visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const closeDiscardPopup = ({ commit }) => commit(types.TOGGLE_DISCARD_POPUP, false);
export const discardAllChanges = ({ commit, getters, dispatch }) => {
const changedFiles = getters.changedFiles;
changedFiles.forEach((file) => {
commit(types.DISCARD_FILE_CHANGES, file);
if (file.tempFile) {
dispatch('closeFile', { file, force: true });
}
});
};
export const closeAllFiles = ({ state, dispatch }) => {
state.openFiles.forEach(file => dispatch('closeFile', { file }));
};
export const toggleEditMode = ({ state, commit, getters, dispatch }, force = false) => {
const changedFiles = getters.changedFiles;
if (changedFiles.length && !force) {
commit(types.TOGGLE_DISCARD_POPUP, true);
} else {
commit(types.TOGGLE_EDIT_MODE);
commit(types.TOGGLE_DISCARD_POPUP, false);
dispatch('toggleBlobView');
if (!state.editMode) {
dispatch('discardAllChanges');
}
}
};
export const toggleBlobView = ({ commit, state }) => {
if (state.editMode) {
commit(types.SET_EDIT_MODE);
} else {
commit(types.SET_PREVIEW_MODE);
}
};
export const checkCommitStatus = ({ state }) => service.getBranchData(
state.project.id,
state.currentBranch,
)
.then((data) => {
const { id } = data.commit;
if (state.currentRef !== id) {
return true;
}
return false;
})
.catch(() => flash('Error checking branch data. Please try again.'));
export const commitChanges = ({ commit, state, dispatch }, { payload, newMr }) =>
service.commit(state.project.id, payload)
.then((data) => {
const { branch } = payload;
if (!data.short_id) {
flash(data.message);
return;
}
flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
if (newMr) {
redirectToUrl(`${state.endpoints.newMergeRequestUrl}${branch}`);
} else {
commit(types.SET_COMMIT_REF, data.id);
dispatch('discardAllChanges');
dispatch('closeAllFiles');
dispatch('toggleEditMode');
window.scrollTo(0, 0);
}
})
.catch(() => flash('Error committing changes. Please try again.'));
export const createTempEntry = ({ state, dispatch }, { name, type, content = '', base64 = false }) => {
if (type === 'tree') {
dispatch('createTempTree', name);
} else if (type === 'blob') {
dispatch('createTempFile', {
tree: state,
name,
base64,
content,
});
}
};
export const popHistoryState = ({ state, dispatch, getters }) => {
const treeList = getters.treeList;
const tree = treeList.find(file => file.url === state.previousUrl);
if (!tree) return;
if (tree.type === 'tree') {
dispatch('toggleTreeOpen', { endpoint: tree.url, tree });
}
};
export const scrollToTab = () => {
Vue.nextTick(() => {
const tabs = document.getElementById('tabs');
if (tabs) {
const tabEl = tabs.querySelector('.active .repo-tab');
tabEl.focus();
}
});
};
export * from './actions/tree';
export * from './actions/file';
export * from './actions/branch';
import service from '../../services';
import * as types from '../mutation_types';
import { pushState } from '../utils';
// eslint-disable-next-line import/prefer-default-export
export const createNewBranch = ({ rootState, commit }, branch) => service.createBranch(
rootState.project.id,
{
branch,
ref: rootState.currentBranch,
},
).then(res => res.json())
.then((data) => {
const branchName = data.name;
const url = location.href.replace(rootState.currentBranch, branchName);
pushState(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,
pushState,
setPageTitle,
createTemp,
findIndexOfFile,
} from '../utils';
export const closeFile = ({ commit, state, dispatch }, { file, force = false }) => {
if ((file.changed || file.tempFile) && !force) return;
const indexOfClosedFile = findIndexOfFile(state.openFiles, file);
const fileWasActive = file.active;
commit(types.TOGGLE_FILE_OPEN, file);
commit(types.SET_FILE_ACTIVE, { file, active: false });
if (state.openFiles.length > 0 && fileWasActive) {
const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
const nextFileToOpen = state.openFiles[nextIndexToOpen];
dispatch('setFileActive', nextFileToOpen);
} else if (!state.openFiles.length) {
pushState(file.parentTreeUrl);
}
};
export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
const currentActiveFile = getters.activeFile;
if (file.active) return;
if (currentActiveFile) {
commit(types.SET_FILE_ACTIVE, { file: currentActiveFile, active: false });
}
commit(types.SET_FILE_ACTIVE, { file, active: true });
dispatch('scrollToTab');
// reset hash for line highlighting
location.hash = '';
};
export const getFileData = ({ state, commit, dispatch }, file) => {
commit(types.TOGGLE_LOADING, file);
service.getFileData(file.url)
.then((res) => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
return res.json();
})
.then((data) => {
commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, file);
dispatch('setFileActive', file);
commit(types.TOGGLE_LOADING, file);
pushState(file.url);
})
.catch(() => {
commit(types.TOGGLE_LOADING, file);
flash('Error loading file data. Please try again.');
});
};
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.'));
export const changeFileContent = ({ commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content });
};
export const createTempFile = ({ state, commit, dispatch }, { tree, name, content = '', base64 = '' }) => {
const file = createTemp({
name: name.replace(`${state.path}/`, ''),
path: tree.path,
type: 'blob',
level: tree.level !== undefined ? tree.level + 1 : 0,
changed: true,
content,
base64,
});
if (findEntry(tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
commit(types.CREATE_TMP_FILE, {
parent: tree,
file,
});
commit(types.TOGGLE_FILE_OPEN, file);
dispatch('setFileActive', file);
if (!state.editMode && !file.base64) {
dispatch('toggleEditMode', true);
}
return Promise.resolve(file);
};
import { normalizeHeaders } from '../../../lib/utils/common_utils';
import flash from '../../../flash';
import service from '../../services';
import * as types from '../mutation_types';
import {
pushState,
setPageTitle,
findEntry,
createTemp,
} from '../utils';
export const getTreeData = (
{ commit, state },
{ endpoint = state.endpoints.rootEndpoint, tree = state } = {},
) => {
commit(types.TOGGLE_LOADING, tree);
service.getTreeData(endpoint)
.then((res) => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
return res.json();
})
.then((data) => {
if (!state.isInitialRoot) {
commit(types.SET_ROOT, data.path === '/');
}
commit(types.SET_DIRECTORY_DATA, { data, tree });
commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
commit(types.TOGGLE_LOADING, tree);
pushState(endpoint);
})
.catch(() => {
flash('Error loading tree data. Please try again.');
commit(types.TOGGLE_LOADING, tree);
});
};
export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
if (tree.opened) {
// send empty data to clear the tree
const data = { trees: [], blobs: [], submodules: [] };
pushState(tree.parentTreeUrl);
commit(types.SET_PREVIOUS_URL, tree.parentTreeUrl);
commit(types.SET_DIRECTORY_DATA, { data, tree });
} else {
commit(types.SET_PREVIOUS_URL, endpoint);
dispatch('getTreeData', { endpoint, tree });
}
commit(types.TOGGLE_TREE_OPEN, tree);
};
export const clickedTreeRow = ({ commit, dispatch }, row) => {
if (row.type === 'tree') {
dispatch('toggleTreeOpen', {
endpoint: row.url,
tree: row,
});
} else if (row.type === 'submodule') {
commit(types.TOGGLE_LOADING, row);
gl.utils.visitUrl(row.url);
} else if (row.type === 'blob' && row.opened) {
dispatch('setFileActive', row);
} else {
dispatch('getFileData', row);
}
};
export const createTempTree = ({ state, commit, dispatch }, name) => {
let tree = state;
const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
dirNames.forEach((dirName) => {
const foundEntry = findEntry(tree, 'tree', dirName);
if (!foundEntry) {
const tmpEntry = createTemp({
name: dirName,
path: tree.path,
type: 'tree',
level: tree.level !== undefined ? tree.level + 1 : 0,
});
commit(types.CREATE_TMP_TREE, {
parent: tree,
tmpEntry,
});
commit(types.TOGGLE_TREE_OPEN, tmpEntry);
tree = tmpEntry;
} else {
tree = foundEntry;
}
});
if (tree.tempFile) {
dispatch('createTempFile', {
tree,
name: '.gitkeep',
});
}
};
import _ from 'underscore';
/*
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) => {
const mapTree = arr => (!arr.tree.length ? [] : _.map(arr.tree, a => [a, mapTree(a)]));
return _.chain(state.tree)
.map(arr => [arr, mapTree(arr)])
.flatten()
.value();
};
export const changedFiles = state => state.openFiles.filter(file => file.changed);
export const activeFile = state => state.openFiles.find(file => file.active);
export const activeFileExtension = (state) => {
const file = activeFile(state);
return file ? `.${file.path.split('.').pop()}` : '';
};
export const isCollapsed = state => !!state.openFiles.length;
export const canEditFile = (state) => {
const currentActiveFile = activeFile(state);
const openedFiles = state.openFiles;
return state.canCommit &&
state.onTopOfBranch &&
openedFiles.length &&
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
};
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
Vue.use(Vuex);
export default new Vuex.Store({
state: state(),
actions,
mutations,
getters,
});
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
export const SET_COMMIT_REF = 'SET_COMMIT_REF';
export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
export const SET_ROOT = 'SET_ROOT';
export const SET_PREVIOUS_URL = 'SET_PREVIOUS_URL';
// 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';
// File mutation types
export const SET_FILE_DATA = 'SET_FILE_DATA';
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
// 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 TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
import * as types from './mutation_types';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
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) {
Object.assign(entry, {
loading: !entry.loading,
});
},
[types.TOGGLE_EDIT_MODE](state) {
Object.assign(state, {
editMode: !state.editMode,
});
},
[types.TOGGLE_DISCARD_POPUP](state, discardPopupOpen) {
Object.assign(state, {
discardPopupOpen,
});
},
[types.SET_COMMIT_REF](state, ref) {
Object.assign(state, {
currentRef: ref,
});
},
[types.SET_ROOT](state, isRoot) {
Object.assign(state, {
isRoot,
isInitialRoot: isRoot,
});
},
[types.SET_PREVIOUS_URL](state, previousUrl) {
Object.assign(state, {
previousUrl,
});
},
...fileMutations,
...treeMutations,
...branchMutations,
};
import * as types from '../mutation_types';
export default {
[types.SET_CURRENT_BRANCH](state, currentBranch) {
Object.assign(state, {
currentBranch,
});
},
};
import * as types from '../mutation_types';
import { findIndexOfFile } from '../utils';
export default {
[types.SET_FILE_ACTIVE](state, { file, active }) {
Object.assign(file, {
active,
});
},
[types.TOGGLE_FILE_OPEN](state, file) {
Object.assign(file, {
opened: !file.opened,
});
if (file.opened) {
state.openFiles.push(file);
} else {
state.openFiles.splice(findIndexOfFile(state.openFiles, file), 1);
}
},
[types.SET_FILE_DATA](state, { data, file }) {
Object.assign(file, {
blamePath: data.blame_path,
commitsPath: data.commits_path,
permalink: data.permalink,
rawPath: data.raw_path,
binary: data.binary,
html: data.html,
renderError: data.render_error,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
Object.assign(file, {
raw,
});
},
[types.UPDATE_FILE_CONTENT](state, { file, content }) {
const changed = content !== file.raw;
Object.assign(file, {
content,
changed,
});
},
[types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, {
content: '',
changed: false,
});
},
[types.CREATE_TMP_FILE](state, { file, parent }) {
parent.tree.push(file);
},
};
import * as types from '../mutation_types';
import * as utils from '../utils';
export default {
[types.TOGGLE_TREE_OPEN](state, tree) {
Object.assign(tree, {
opened: !tree.opened,
});
},
[types.SET_DIRECTORY_DATA](state, { data, tree }) {
const level = tree.level !== undefined ? tree.level + 1 : 0;
const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
Object.assign(tree, {
tree: [
...data.trees.map(t => utils.decorateData({
...t,
type: 'tree',
parentTreeUrl,
level,
}, state.project.url)),
...data.submodules.map(m => utils.decorateData({
...m,
type: 'submodule',
parentTreeUrl,
level,
}, state.project.url)),
...data.blobs.map(b => utils.decorateData({
...b,
type: 'blob',
parentTreeUrl,
level,
}, state.project.url)),
],
});
},
[types.SET_PARENT_TREE_URL](state, url) {
Object.assign(state, {
parentTreeUrl: url,
});
},
[types.CREATE_TMP_TREE](state, { parent, tmpEntry }) {
parent.tree.push(tmpEntry);
},
};
import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
const RepoStore = {
monacoLoading: false,
service: '',
canCommit: false,
onTopOfBranch: false,
editMode: false,
isRoot: null,
isInitialRoot: null,
prevURL: '',
projectId: '',
projectName: '',
projectUrl: '',
branchUrl: '',
blobRaw: '',
currentBlobView: 'repo-preview',
openedFiles: [],
submitCommitsLoading: false,
dialog: {
open: false,
title: '',
status: false,
},
showNewBranchDialog: false,
activeFile: Helper.getDefaultActiveFile(),
activeFileIndex: 0,
activeLine: -1,
activeFileLabel: 'Raw',
files: [],
isCommitable: false,
binary: false,
currentBranch: '',
startNewMR: false,
currentHash: '',
currentShortHash: '',
customBranchURL: '',
newMrTemplateUrl: '',
branchChanged: false,
commitMessage: '',
path: '',
loading: {
tree: false,
blob: false,
},
setBranchHash() {
return Service.getBranch()
.then((data) => {
if (RepoStore.currentHash !== '' && data.commit.id !== RepoStore.currentHash) {
RepoStore.branchChanged = true;
}
RepoStore.currentHash = data.commit.id;
RepoStore.currentShortHash = data.commit.short_id;
});
},
// mutations
checkIsCommitable() {
RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
},
toggleRawPreview() {
RepoStore.activeFile.raw = !RepoStore.activeFile.raw;
RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source';
},
setActiveFiles(file) {
if (RepoStore.isActiveFile(file)) return;
RepoStore.openedFiles = RepoStore.openedFiles
.map((openedFile, i) => RepoStore.setFileActivity(file, openedFile, i));
RepoStore.setActiveToRaw();
if (file.binary) {
RepoStore.blobRaw = file.base64;
} else if (file.newContent || file.plain) {
RepoStore.blobRaw = file.newContent || file.plain;
} else {
Service.getRaw(file)
.then((rawResponse) => {
RepoStore.blobRaw = rawResponse.data;
Helper.findOpenedFileFromActive().plain = rawResponse.data;
}).catch(Helper.loadingError);
}
if (!file.loading && !file.tempFile) {
Helper.updateHistoryEntry(file.url, file.pageTitle || file.name);
}
RepoStore.binary = file.binary;
RepoStore.setActiveLine(-1);
},
setFileActivity(file, openedFile, i) {
const activeFile = openedFile;
activeFile.active = file.id === activeFile.id;
if (activeFile.active) RepoStore.setActiveFile(activeFile, i);
return activeFile;
},
setActiveFile(activeFile, i) {
RepoStore.activeFile = Object.assign({}, Helper.getDefaultActiveFile(), activeFile);
RepoStore.activeFileIndex = i;
},
setActiveLine(activeLine) {
if (!isNaN(activeLine)) RepoStore.activeLine = activeLine;
},
setActiveToRaw() {
RepoStore.activeFile.raw = false;
// can't get vue to listen to raw for some reason so RepoStore for now.
RepoStore.activeFileLabel = 'Display source';
},
removeFromOpenedFiles(file) {
if (file.type === 'tree') return;
let foundIndex;
RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => {
if (openedFile.path === file.path) foundIndex = i;
return openedFile.path !== file.path;
});
// remove the file from the sidebar if it is a tempFile
if (file.tempFile) {
RepoStore.files = RepoStore.files.filter(f => !(f.tempFile && f.path === file.path));
}
// now activate the right tab based on what you closed.
if (RepoStore.openedFiles.length === 0) {
RepoStore.activeFile = {};
return;
}
if (RepoStore.openedFiles.length === 1 || foundIndex === 0) {
RepoStore.setActiveFiles(RepoStore.openedFiles[0]);
return;
}
if (foundIndex && foundIndex > 0) {
RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]);
}
},
addToOpenedFiles(file) {
const openFile = file;
const openedFilesAlreadyExists = RepoStore.openedFiles
.some(openedFile => openedFile.path === openFile.path);
if (openedFilesAlreadyExists) return;
openFile.changed = false;
openFile.active = true;
RepoStore.openedFiles.push(openFile);
},
setActiveFileContents(contents) {
if (!RepoStore.editMode) return;
const currentFile = RepoStore.openedFiles[RepoStore.activeFileIndex];
RepoStore.activeFile.newContent = contents;
RepoStore.activeFile.changed = RepoStore.activeFile.plain !== RepoStore.activeFile.newContent;
currentFile.changed = RepoStore.activeFile.changed;
currentFile.newContent = contents;
},
toggleBlobView() {
RepoStore.currentBlobView = RepoStore.isPreviewView() ? 'repo-editor' : 'repo-preview';
},
setViewToPreview() {
RepoStore.currentBlobView = 'repo-preview';
},
// getters
isActiveFile(file) {
return file && file.id === RepoStore.activeFile.id;
},
isPreviewView() {
return RepoStore.currentBlobView === 'repo-preview';
},
};
export default RepoStore;
export default () => ({
canCommit: false,
currentBranch: '',
currentBlobView: 'repo-preview',
currentRef: '',
discardPopupOpen: false,
editMode: false,
endpoints: {},
isRoot: false,
isInitialRoot: false,
loading: false,
onTopOfBranch: false,
openFiles: [],
path: '',
project: {
id: 0,
name: '',
url: '',
},
parentTreeUrl: '',
previousUrl: '',
tree: [],
});
export const dataStructure = () => ({
id: '',
type: '',
name: '',
url: '',
path: '',
level: 0,
tempFile: false,
icon: '',
tree: [],
loading: false,
opened: false,
active: false,
changed: false,
lastCommit: {},
tree_url: '',
blamePath: '',
commitsPath: '',
permalink: '',
rawPath: '',
binary: false,
html: '',
raw: '',
content: '',
parentTreeUrl: '',
renderError: false,
base64: false,
});
export const decorateData = (entity, projectUrl = '') => {
const {
id,
type,
url,
name,
icon,
last_commit,
tree_url,
path,
renderError,
content = '',
tempFile = false,
active = false,
opened = false,
changed = false,
parentTreeUrl = '',
level = 0,
base64 = false,
} = entity;
return {
...dataStructure(),
id,
type,
name,
url,
tree_url,
path,
level,
tempFile,
icon: `fa-${icon}`,
opened,
active,
parentTreeUrl,
changed,
renderError,
content,
base64,
// eslint-disable-next-line camelcase
lastCommit: last_commit ? {
url: `${projectUrl}/commit/${last_commit.id}`,
message: last_commit.message,
updatedAt: last_commit.committed_date,
} : {},
};
};
export const findEntry = (state, type, name) => state.tree.find(
f => f.type === type && f.name === name,
);
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
export const setPageTitle = (title) => {
document.title = title;
};
export const pushState = (url) => {
history.pushState({ url }, '', url);
};
export const createTemp = ({ name, path, type, level, changed, content, base64 }) => {
const treePath = path ? `${path}/${name}` : name;
return decorateData({
id: new Date().getTime().toString(),
name,
type,
tempFile: true,
path: treePath,
icon: type === 'tree' ? 'folder' : 'file-text-o',
changed,
content,
parentTreeUrl: '',
level,
base64,
renderError: base64,
});
};
<script>
import { __, n__, sprintf } from '../../../locale';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
props: {
loading: {
type: Boolean,
required: false,
default: false,
},
participants: {
type: Array,
required: false,
default: () => [],
},
numberOfLessParticipants: {
type: Number,
required: false,
default: 7,
},
},
data() {
return {
isShowingMoreParticipants: false,
};
},
components: {
loadingIcon,
userAvatarImage,
},
computed: {
lessParticipants() {
return this.participants.slice(0, this.numberOfLessParticipants);
},
visibleParticipants() {
return this.isShowingMoreParticipants ? this.participants : this.lessParticipants;
},
hasMoreParticipants() {
return this.participants.length > this.numberOfLessParticipants;
},
toggleLabel() {
let label = '';
if (this.isShowingMoreParticipants) {
label = __('- show less');
} else {
label = sprintf(__('+ %{moreCount} more'), {
moreCount: this.participants.length - this.numberOfLessParticipants,
});
}
return label;
},
participantLabel() {
return sprintf(
n__('%{count} participant', '%{count} participants', this.participants.length),
{ count: this.loading ? '' : this.participantCount },
);
},
participantCount() {
return this.participants.length;
},
},
methods: {
toggleMoreParticipants() {
this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
},
},
};
</script>
<template>
<div>
<div class="sidebar-collapsed-icon">
<i
class="fa fa-users"
aria-hidden="true">
</i>
<loading-icon
v-if="loading"
class="js-participants-collapsed-loading-icon" />
<span
v-else
class="js-participants-collapsed-count">
{{ participantCount }}
</span>
</div>
<div class="title hide-collapsed">
<loading-icon
v-if="loading"
:inline="true"
class="js-participants-expanded-loading-icon" />
{{ participantLabel }}
</div>
<div class="participants-list hide-collapsed">
<div
v-for="participant in visibleParticipants"
:key="participant.id"
class="participants-author js-participants-author">
<a
class="author_link"
:href="participant.web_url">
<user-avatar-image
:lazy="true"
:img-src="participant.avatar_url"
css-classes="avatar-inline"
:size="24"
:tooltip-text="participant.name"
tooltip-placement="bottom" />
</a>
</div>
</div>
<div
v-if="hasMoreParticipants"
class="participants-more hide-collapsed">
<button
type="button"
class="btn-transparent btn-blank js-toggle-participants-button"
@click="toggleMoreParticipants">
{{ toggleLabel }}
</button>
</div>
</div>
</template>
<script>
import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import participants from './participants.vue';
export default {
data() {
return {
mediator: new Mediator(),
store: new Store(),
};
},
components: {
participants,
},
};
</script>
<template>
<div class="block participants">
<participants
:loading="store.isFetching.participants"
:participants="store.participants"
:number-of-less-participants="7" />
</div>
</template>
<script>
import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
import Flash from '../../../flash';
import subscriptions from './subscriptions.vue';
export default {
data() {
return {
mediator: new Mediator(),
store: new Store(),
};
},
components: {
subscriptions,
},
methods: {
onToggleSubscription() {
this.mediator.toggleSubscription()
.catch(() => {
Flash('Error occurred when toggling the notification subscription');
});
},
},
created() {
eventHub.$on('toggleSubscription', this.onToggleSubscription);
},
beforeDestroy() {
eventHub.$off('toggleSubscription', this.onToggleSubscription);
},
};
</script>
<template>
<div class="block subscriptions">
<subscriptions
:loading="store.isFetching.subscriptions"
:subscribed="store.subscribed" />
</div>
</template>
<script>
import { __ } from '../../../locale';
import eventHub from '../../event_hub';
import loadingButton from '../../../vue_shared/components/loading_button.vue';
export default {
props: {
loading: {
type: Boolean,
required: false,
default: false,
},
subscribed: {
type: Boolean,
required: false,
},
},
components: {
loadingButton,
},
computed: {
buttonLabel() {
let label;
if (this.subscribed === false) {
label = __('Subscribe');
} else if (this.subscribed === true) {
label = __('Unsubscribe');
}
return label;
},
},
methods: {
toggleSubscription() {
eventHub.$emit('toggleSubscription');
},
},
};
</script>
<template>
<div>
<div class="sidebar-collapsed-icon">
<i
class="fa fa-rss"
aria-hidden="true">
</i>
</div>
<span class="issuable-header-text hide-collapsed pull-left">
{{ __('Notifications') }}
</span>
<loading-button
ref="loadingButton"
class="btn btn-default pull-right hide-collapsed js-issuable-subscribe-button"
:loading="loading"
:label="buttonLabel"
@click="toggleSubscription"
/>
</div>
</template>
......@@ -7,6 +7,7 @@ export default class SidebarService {
constructor(endpointMap) {
if (!SidebarService.singleton) {
this.endpoint = endpointMap.endpoint;
this.toggleSubscriptionEndpoint = endpointMap.toggleSubscriptionEndpoint;
this.moveIssueEndpoint = endpointMap.moveIssueEndpoint;
this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint;
......@@ -36,6 +37,10 @@ export default class SidebarService {
});
}
toggleSubscription() {
return Vue.http.post(this.toggleSubscriptionEndpoint);
}
moveIssue(moveToProjectId) {
return Vue.http.post(this.moveIssueEndpoint, {
move_to_project_id: moveToProjectId,
......
......@@ -4,6 +4,8 @@ import SidebarAssignees from './components/assignees/sidebar_assignees';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
import sidebarParticipants from './components/participants/sidebar_participants.vue';
import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
import Translate from '../vue_shared/translate';
import Mediator from './sidebar_mediator';
......@@ -49,6 +51,36 @@ function mountLockComponent(mediator) {
}).$mount(el);
}
function mountParticipantsComponent() {
const el = document.querySelector('.js-sidebar-participants-entry-point');
if (!el) return;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
sidebarParticipants,
},
render: createElement => createElement('sidebar-participants', {}),
});
}
function mountSubscriptionsComponent() {
const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
if (!el) return;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
sidebarSubscriptions,
},
render: createElement => createElement('sidebar-subscriptions', {}),
});
}
function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions);
......@@ -63,6 +95,8 @@ function domContentLoaded() {
mountConfidentialComponent(mediator);
mountLockComponent(mediator);
mountParticipantsComponent();
mountSubscriptionsComponent();
new SidebarMoveIssue(
mediator,
......
......@@ -8,6 +8,7 @@ export default class SidebarMediator {
this.store = new Store(options);
this.service = new Service({
endpoint: options.endpoint,
toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint,
moveIssueEndpoint: options.moveIssueEndpoint,
projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
});
......@@ -39,10 +40,25 @@ export default class SidebarMediator {
.then((data) => {
this.store.setAssigneeData(data);
this.store.setTimeTrackingData(data);
this.store.setParticipantsData(data);
this.store.setSubscriptionsData(data);
})
.catch(() => new Flash('Error occurred when fetching sidebar data'));
}
toggleSubscription() {
this.store.setFetchingState('subscriptions', true);
return this.service.toggleSubscription()
.then(() => {
this.store.setSubscribedState(!this.store.subscribed);
this.store.setFetchingState('subscriptions', false);
})
.catch((err) => {
this.store.setFetchingState('subscriptions', false);
throw err;
});
}
fetchAutocompleteProjects(searchTerm) {
return this.service.getProjectsAutocomplete(searchTerm)
.then(response => response.json())
......
......@@ -12,10 +12,14 @@ export default class SidebarStore {
this.assignees = [];
this.isFetching = {
assignees: true,
participants: true,
subscriptions: true,
};
this.autocompleteProjects = [];
this.moveToProjectId = 0;
this.isLockDialogOpen = false;
this.participants = [];
this.subscribed = null;
SidebarStore.singleton = this;
}
......@@ -37,6 +41,20 @@ export default class SidebarStore {
this.humanTotalTimeSpent = data.human_total_time_spent;
}
setParticipantsData(data) {
this.isFetching.participants = false;
this.participants = data.participants || [];
}
setSubscriptionsData(data) {
this.isFetching.subscriptions = false;
this.subscribed = data.subscribed || false;
}
setFetchingState(key, value) {
this.isFetching[key] = value;
}
addAssignee(assignee) {
if (!this.findAssignee(assignee)) {
this.assignees.push(assignee);
......@@ -61,6 +79,10 @@ export default class SidebarStore {
this.autocompleteProjects = projects;
}
setSubscribedState(subscribed) {
this.subscribed = subscribed;
}
setMoveToProjectId(moveToProjectId) {
this.moveToProjectId = moveToProjectId;
}
......
......@@ -11,7 +11,7 @@ export default class MRWidgetService {
this.removeWIPResource = Vue.resource(endpoints.removeWIPPath);
this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath);
this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath);
this.pollResource = Vue.resource(`${endpoints.statusPath}?basic=true`);
this.pollResource = Vue.resource(`${endpoints.statusPath}?serializer=basic`);
this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath);
}
......
......@@ -23,11 +23,16 @@
@include webkit-prefix(animation-duration, 2s);
}
&.spin {
&.spin-cw {
transform-origin: center;
animation: spin 4s linear infinite;
}
&.spin-ccw {
transform-origin: center;
animation: spin 4s linear infinite reverse;
}
&.flipOutX,
&.flipOutY,
&.bounceIn,
......
......@@ -209,7 +209,6 @@
padding: 24px 0 0;
.nav-links {
justify-content: center;
width: 100%;
float: none;
......@@ -217,6 +216,14 @@
float: none;
}
}
li:first-child {
margin-left: auto;
}
li:last-child {
margin-right: auto;
}
}
.group-info {
......
......@@ -542,7 +542,9 @@
}
.participants-list {
margin: -5px;
display: flex;
flex-wrap: wrap;
margin: -7px;
}
......@@ -553,7 +555,7 @@
.participants-author {
display: inline-block;
padding: 5px;
padding: 7px;
&:nth-of-type(7n) {
padding-right: 0;
......
.monaco-loader {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: $black-transparent;
.modal.popup-dialog {
display: block;
background-color: $black-transparent;
z-index: 2100;
@media (min-width: $screen-md-min) {
.modal-dialog {
width: 600px;
margin: 30px auto;
}
}
}
.project-refs-form,
......@@ -41,6 +45,7 @@
}
.tree-content-holder {
display: -webkit-flex;
display: flex;
min-height: 300px;
}
......@@ -50,7 +55,9 @@
}
.panel-right {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
width: 80%;
height: 100%;
......@@ -68,10 +75,6 @@
text-decoration: underline;
}
}
.cursor {
display: none !important;
}
}
.blob-no-preview {
......@@ -81,21 +84,12 @@
}
}
&.edit-mode {
.blob-viewer-container {
overflow: hidden;
}
.monaco-editor.vs {
.cursor {
background: $black;
border-color: $black;
display: block !important;
}
}
&.blob-editor-container {
overflow: hidden;
}
.blob-viewer-container {
-webkit-flex: 1;
flex: 1;
overflow: auto;
......@@ -125,6 +119,7 @@
}
#tabs {
position: relative;
flex-shrink: 0;
display: flex;
width: 100%;
......@@ -153,6 +148,10 @@
vertical-align: middle;
text-decoration: none;
margin-right: 12px;
&:focus {
outline: none;
}
}
.close-btn {
......@@ -299,23 +298,3 @@
width: 100%;
}
}
@keyframes swipeRightAppear {
0% {
transform: scaleX(0.00);
}
100% {
transform: scaleX(1.00);
}
}
@keyframes swipeRightDissapear {
0% {
transform: scaleX(1.00);
}
100% {
transform: scaleX(0.00);
}
}
......@@ -50,3 +50,10 @@
font-size: 11px;
}
}
@media (max-width: $screen-md-max) {
.runners-content {
width: 100%;
overflow: auto;
}
}
......@@ -12,12 +12,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
if group
return render_404 unless can?(current_user, :read_group, group)
project.project_group_links.create(
group: group,
group_access: params[:link_group_access],
expires_at: params[:expires_at]
)
Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group)
else
flash[:alert] = 'Please select a group.'
end
......@@ -32,7 +27,9 @@ class Projects::GroupLinksController < Projects::ApplicationController
end
def destroy
project.project_group_links.find(params[:id]).destroy
group_link = project.project_group_links.find(params[:id])
::Projects::GroupLinks::DestroyService.new(project, current_user).execute(group_link)
respond_to do |format|
format.html do
......@@ -47,4 +44,8 @@ class Projects::GroupLinksController < Projects::ApplicationController
def group_link_params
params.require(:group_link).permit(:group_access, :expires_at)
end
def group_link_create_params
params.permit(:link_group_access, :expires_at)
end
end
......@@ -74,7 +74,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format|
format.html
format.json do
render json: serializer.represent(@issue)
render json: serializer.represent(@issue, serializer: params[:serializer])
end
end
end
......
......@@ -83,7 +83,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: serializer.represent(@merge_request, basic: params[:basic])
render json: serializer.represent(@merge_request, serializer: params[:serializer])
end
format.patch do
......
This diff is collapsed.
......@@ -19,11 +19,7 @@ module NavHelper
end
elsif current_path?('jobs#show')
%w[page-gutter build-sidebar right-sidebar-expanded]
elsif current_path?('wikis#show') ||
current_path?('wikis#edit') ||
current_path?('wikis#update') ||
current_path?('wikis#history') ||
current_path?('wikis#git_access')
elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access')
%w[page-gutter wiki-sidebar right-sidebar-expanded]
else
[]
......
......@@ -13,6 +13,8 @@ module Subscribable
end
def subscribed?(user, project = nil)
return false unless user
if subscription = subscriptions.find_by(user: user, project: project)
subscription.subscribed
else
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
class IssueSidebarEntity < IssuableSidebarEntity
expose :assignees, using: API::Entities::UserBasic
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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