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 .default-cache: &default-cache
key: "ruby-233-with-yarn" key: "ruby-235-with-yarn"
paths: paths:
- vendor/ruby - vendor/ruby
- .yarn-cache/ - .yarn-cache/
...@@ -455,7 +455,7 @@ db:migrate:reset-mysql: ...@@ -455,7 +455,7 @@ db:migrate:reset-mysql:
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
script: script:
- git fetch origin v8.14.10 - git fetch origin v9.3.0
- git checkout -f FETCH_HEAD - git checkout -f FETCH_HEAD
- bundle install $BUNDLE_INSTALL_FLAGS - bundle install $BUNDLE_INSTALL_FLAGS
- cp config/gitlab.yml.example config/gitlab.yml - cp config/gitlab.yml.example config/gitlab.yml
...@@ -551,7 +551,7 @@ karma: ...@@ -551,7 +551,7 @@ karma:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs
<<: *pull-cache <<: *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 stage: test
variables: variables:
BABEL_ENV: "coverage" BABEL_ENV: "coverage"
......
## Contributor license agreement ## Developer Certificate of Origin + License
By submitting code as an individual you agree to the By contributing to GitLab B.V., You accept and agree to the following terms and
[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md). conditions for Your present and future Contributions submitted to GitLab B.V.
By submitting code as an entity you agree to the Except for the license granted herein to GitLab B.V. and recipients of software
[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md). 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._ _This notice should stay as the first item in the CONTRIBUTING.md file._
......
...@@ -90,7 +90,7 @@ gem 'kaminari', '~> 1.0' ...@@ -90,7 +90,7 @@ gem 'kaminari', '~> 1.0'
gem 'hamlit', '~> 2.6.1' gem 'hamlit', '~> 2.6.1'
# Files attachments # Files attachments
gem 'carrierwave', '~> 1.1' gem 'carrierwave', '~> 1.2'
# Drag and Drop UI # Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1' gem 'dropzonejs-rails', '~> 0.7.1'
...@@ -398,7 +398,7 @@ group :ed25519 do ...@@ -398,7 +398,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # 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 gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -107,7 +107,7 @@ GEM ...@@ -107,7 +107,7 @@ GEM
capybara-screenshot (1.0.14) capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3) capybara (>= 1.0, < 3)
launchy launchy
carrierwave (1.1.0) carrierwave (1.2.1)
activemodel (>= 4.0.0) activemodel (>= 4.0.0)
activesupport (>= 4.0.0) activesupport (>= 4.0.0)
mime-types (>= 1.16) mime-types (>= 1.16)
...@@ -273,7 +273,7 @@ GEM ...@@ -273,7 +273,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.48.0) gitaly-proto (0.51.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -291,7 +291,7 @@ GEM ...@@ -291,7 +291,7 @@ GEM
diff-lcs (~> 1.1) diff-lcs (~> 1.1)
mime-types (>= 1.16) mime-types (>= 1.16)
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab-markup (1.6.2) gitlab-markup (1.6.3)
gitlab_omniauth-ldap (2.0.4) gitlab_omniauth-ldap (2.0.4)
net-ldap (~> 0.16) net-ldap (~> 0.16)
omniauth (~> 1.3) omniauth (~> 1.3)
...@@ -990,7 +990,7 @@ DEPENDENCIES ...@@ -990,7 +990,7 @@ DEPENDENCIES
bundler-audit (~> 0.5.0) bundler-audit (~> 0.5.0)
capybara (~> 2.15.0) capybara (~> 2.15.0)
capybara-screenshot (~> 1.0.0) capybara-screenshot (~> 1.0.0)
carrierwave (~> 1.1) carrierwave (~> 1.2)
charlock_holmes (~> 0.7.5) charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2) chronic (~> 0.10.2)
chronic_duration (~> 0.10.6) chronic_duration (~> 0.10.6)
...@@ -1030,7 +1030,7 @@ DEPENDENCIES ...@@ -1030,7 +1030,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.48.0) gitaly-proto (~> 0.51.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2) gitlab-markup (~> 1.6.2)
......
...@@ -16,6 +16,7 @@ const Api = { ...@@ -16,6 +16,7 @@ const Api = {
usersPath: '/api/:version/users.json', usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits', commitPath: '/api/:version/projects/:id/repository/commits',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) { group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath) const url = Api.buildUrl(Api.groupPath)
......
/* eslint-disable comma-dangle, space-before-function-paren, no-new */ /* eslint-disable comma-dangle, space-before-function-paren, no-new */
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global LabelsSelect */
/* global Sidebar */ /* global Sidebar */
import Vue from 'vue'; import Vue from 'vue';
...@@ -11,6 +10,7 @@ import Assignees from '../../sidebar/components/assignees/assignees'; ...@@ -11,6 +10,7 @@ import Assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select'; import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue'; import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context'; import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
......
...@@ -64,19 +64,16 @@ export default class Clusters { ...@@ -64,19 +64,16 @@ export default class Clusters {
this.poll = new Poll({ this.poll = new Poll({
resource: this.service, resource: this.service,
method: 'fetchData', method: 'fetchData',
successCallback: (data) => { successCallback: data => this.handleSuccess(data),
const { status, status_reason } = data.data; errorCallback: () => Clusters.handleError(),
this.updateContainer(status, status_reason);
},
errorCallback: () => {
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
},
}); });
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
this.poll.makeRequest(); this.poll.makeRequest();
} else { } else {
this.service.fetchData(); this.service.fetchData()
.then(data => this.handleSuccess(data))
.catch(() => Clusters.handleError());
} }
Visibility.change(() => { Visibility.change(() => {
...@@ -88,6 +85,15 @@ export default class Clusters { ...@@ -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() { hideAll() {
this.errorContainer.classList.add('hidden'); this.errorContainer.classList.add('hidden');
this.successContainer.classList.add('hidden'); this.successContainer.classList.add('hidden');
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
import IssuableIndex from './issuable_index'; import IssuableIndex from './issuable_index';
/* global Milestone */ /* global Milestone */
import IssuableForm from './issuable_form'; import IssuableForm from './issuable_form';
/* global LabelsSelect */ import LabelsSelect from './labels_select';
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global NewBranchForm */ /* global NewBranchForm */
/* global NotificationsForm */ /* 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 */ /* 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 */ /* global fuzzaldrinPlus */
import _ from 'underscore'; import _ from 'underscore';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { isObject } from './lib/utils/type_utility'; import { isObject } from './lib/utils/type_utility';
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput; var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput;
......
...@@ -7,10 +7,12 @@ import { highCountTrim } from '~/lib/utils/text_utility'; ...@@ -7,10 +7,12 @@ import { highCountTrim } from '~/lib/utils/text_utility';
* @param {jQuery.Event} e * @param {jQuery.Event} e
* @param {String} count * @param {String} count
*/ */
$(document).on('todo:toggle', (e, count) => { export default function initTodoToggle() {
const parsedCount = parseInt(count, 10); $(document).on('todo:toggle', (e, count) => {
const $todoPendingCount = $('.todos-count'); const parsedCount = parseInt(count, 10);
const $todoPendingCount = $('.todos-count');
$todoPendingCount.text(highCountTrim(parsedCount)); $todoPendingCount.text(highCountTrim(parsedCount));
$todoPendingCount.toggleClass('hidden', parsedCount === 0); $todoPendingCount.toggleClass('hidden', parsedCount === 0);
}); });
}
/* eslint-disable no-new */ /* eslint-disable no-new */
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global LabelsSelect */ import LabelsSelect from './labels_select';
import IssuableContext from './issuable_context'; import IssuableContext from './issuable_context';
/* global Sidebar */ /* global Sidebar */
......
/* eslint-disable no-new */ /* eslint-disable no-new */
/* global LabelsSelect */ import LabelsSelect from './labels_select';
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global IssueStatusSelect */
/* global SubscriptionSelect */ /* global SubscriptionSelect */
import UsersSelect from './users_select'; import UsersSelect from './users_select';
import issueStatusSelect from './issue_status_select';
export default () => { export default () => {
new UsersSelect(); new UsersSelect();
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); new MilestoneSelect();
new IssueStatusSelect(); issueStatusSelect();
new SubscriptionSelect(); new SubscriptionSelect();
}; };
/* eslint-disable class-methods-use-this, no-new */ /* eslint-disable class-methods-use-this, no-new */
/* global LabelsSelect */
/* global MilestoneSelect */ /* global MilestoneSelect */
/* global IssueStatusSelect */
/* global SubscriptionSelect */ /* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import './milestone_select'; import './milestone_select';
import './issue_status_select'; import issueStatusSelect from './issue_status_select';
import './subscription_select'; import './subscription_select';
import './labels_select'; import LabelsSelect from './labels_select';
const HIDDEN_CLASS = 'hidden'; const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content'; const DISABLED_CONTENT_CLASS = 'disabled-content';
...@@ -49,7 +47,7 @@ export default class IssuableBulkUpdateSidebar { ...@@ -49,7 +47,7 @@ export default class IssuableBulkUpdateSidebar {
initDropdowns() { initDropdowns() {
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); new MilestoneSelect();
new IssueStatusSelect(); issueStatusSelect();
new SubscriptionSelect(); new SubscriptionSelect();
} }
......
...@@ -2,11 +2,8 @@ import Cookies from 'js-cookie'; ...@@ -2,11 +2,8 @@ import Cookies from 'js-cookie';
import bp from './breakpoints'; import bp from './breakpoints';
import UsersSelect from './users_select'; import UsersSelect from './users_select';
const PARTICIPANTS_ROW_COUNT = 7;
export default class IssuableContext { export default class IssuableContext {
constructor(currentUser) { constructor(currentUser) {
this.initParticipants();
this.userSelect = new UsersSelect(currentUser); this.userSelect = new UsersSelect(currentUser);
$('select.select2').select2({ $('select.select2').select2({
...@@ -51,29 +48,4 @@ export default class IssuableContext { ...@@ -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'; ...@@ -6,7 +6,7 @@ import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown'; import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper'; import IssuablesHelper from './helpers/issuables_helper';
class Issue { export default class Issue {
constructor() { constructor() {
if ($('a.btn-close').length) { if ($('a.btn-close').length) {
this.taskList = new TaskList({ this.taskList = new TaskList({
...@@ -147,5 +147,3 @@ class Issue { ...@@ -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 */ export default function issueStatusSelect() {
(function() { $('.js-issue-status').each((i, el) => {
this.IssueStatusSelect = (function() { const fieldName = $(el).data('field-name');
function IssueStatusSelect() { return $(el).glDropdown({
$('.js-issue-status').each(function(i, el) { selectable: true,
var fieldName; fieldName,
fieldName = $(el).data("field-name"); toggleLabel(selected, element, instance) {
return $(el).glDropdown({ let label = 'Author';
selectable: true, const $item = instance.dropdown.find('.is-active');
fieldName: fieldName, if ($item.length) {
toggleLabel: (function(_this) { label = $item.text();
return function(selected, el, instance) { }
var $item, label; return label;
label = 'Author'; },
$item = instance.dropdown.find('.is-active'); clicked(options) {
if ($item.length) { return options.e.preventDefault();
label = $item.text(); },
} id(obj, element) {
return label; return $(element).data('id');
}; },
})(this), });
clicked: function(options) { });
return options.e.preventDefault(); }
},
id: function(obj, el) {
return $(el).data("id");
}
});
});
}
return IssueStatusSelect;
})();
}).call(window);
This diff is collapsed.
/* eslint-disable one-export, one-var, one-var-declaration-per-line */
import _ from 'underscore'; import _ from 'underscore';
export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
...@@ -21,7 +19,10 @@ export default class LazyLoader { ...@@ -21,7 +19,10 @@ export default class LazyLoader {
} }
searchLazyImages() { searchLazyImages() {
this.lazyImages = [].slice.call(document.querySelectorAll('.lazy')); this.lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
this.checkElementsInView();
if (this.lazyImages.length) {
this.checkElementsInView();
}
} }
startContentObserver() { startContentObserver() {
const contentNode = document.querySelector(this.observerNode) || document.querySelector('body'); const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
...@@ -45,15 +46,13 @@ export default class LazyLoader { ...@@ -45,15 +46,13 @@ export default class LazyLoader {
checkElementsInView() { checkElementsInView() {
const scrollTop = pageYOffset; const scrollTop = pageYOffset;
const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD; const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD;
let imgBoundRect, imgTop, imgBound;
// Loading Images which are in the current viewport or close to them // Loading Images which are in the current viewport or close to them
this.lazyImages = this.lazyImages.filter((selectedImage) => { this.lazyImages = this.lazyImages.filter((selectedImage) => {
if (selectedImage.getAttribute('data-src')) { if (selectedImage.getAttribute('data-src')) {
imgBoundRect = selectedImage.getBoundingClientRect(); const imgBoundRect = selectedImage.getBoundingClientRect();
const imgTop = scrollTop + imgBoundRect.top;
imgTop = scrollTop + imgBoundRect.top; const imgBound = imgTop + imgBoundRect.height;
imgBound = imgTop + imgBoundRect.height;
if (scrollTop < imgBound && visHeight > imgTop) { if (scrollTop < imgBound && visHeight > imgTop) {
LazyLoader.loadImage(selectedImage); LazyLoader.loadImage(selectedImage);
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */ export default function initLogoAnimation() {
window.addEventListener('beforeunload', () => {
(function() {
window.addEventListener('beforeunload', function() {
$('.tanuki-logo').addClass('animate'); $('.tanuki-logo').addClass('animate');
}); });
}).call(window); }
...@@ -12,7 +12,6 @@ import svg4everybody from 'svg4everybody'; ...@@ -12,7 +12,6 @@ import svg4everybody from 'svg4everybody';
// libraries with import side-effects // libraries with import side-effects
import 'mousetrap'; import 'mousetrap';
import 'mousetrap/plugins/pause/mousetrap-pause'; import 'mousetrap/plugins/pause/mousetrap-pause';
import 'vendor/fuzzaldrin-plus';
// expose common libraries as globals (TODO: remove these) // expose common libraries as globals (TODO: remove these)
window.jQuery = jQuery; window.jQuery = jQuery;
...@@ -54,16 +53,12 @@ import './gl_dropdown'; ...@@ -54,16 +53,12 @@ import './gl_dropdown';
import './gl_field_error'; import './gl_field_error';
import './gl_field_errors'; import './gl_field_errors';
import './gl_form'; import './gl_form';
import './header'; import initTodoToggle from './header';
import initImporterStatus from './importer_status'; import initImporterStatus from './importer_status';
import './issuable_form';
import './issue';
import './issue_status_select';
import './labels_select';
import './layout_nav'; import './layout_nav';
import LazyLoader from './lazy_loader'; import LazyLoader from './lazy_loader';
import './line_highlighter'; import './line_highlighter';
import './logo'; import initLogoAnimation from './logo';
import './merge_request'; import './merge_request';
import './merge_request_tabs'; import './merge_request_tabs';
import './milestone'; import './milestone';
...@@ -137,6 +132,8 @@ $(function () { ...@@ -137,6 +132,8 @@ $(function () {
initBreadcrumbs(); initBreadcrumbs();
initImporterStatus(); initImporterStatus();
initTodoToggle();
initLogoAnimation();
// Set the default path for all cookies to GitLab's root directory // Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/'; 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 */ /* 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() { (function() {
this.ProjectFindFile = (function() { this.ProjectFindFile = (function() {
......
<script> <script>
import { mapState, mapActions } from 'vuex';
import flash, { hideFlash } from '../../flash'; import flash, { hideFlash } from '../../flash';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import eventHub from '../event_hub';
export default { export default {
components: { components: {
loadingIcon, loadingIcon,
}, },
props: {
currentBranch: {
type: String,
required: true,
},
},
data() { data() {
return { return {
branchName: '', branchName: '',
...@@ -20,11 +14,17 @@ ...@@ -20,11 +14,17 @@
}; };
}, },
computed: { computed: {
...mapState([
'currentBranch',
]),
btnDisabled() { btnDisabled() {
return this.loading || this.branchName === ''; return this.loading || this.branchName === '';
}, },
}, },
methods: { methods: {
...mapActions([
'createNewBranch',
]),
toggleDropdown() { toggleDropdown() {
this.$dropdown.dropdown('toggle'); this.$dropdown.dropdown('toggle');
}, },
...@@ -38,19 +38,21 @@ ...@@ -38,19 +38,21 @@
hideFlash(flashEl, false); hideFlash(flashEl, false);
} }
eventHub.$emit('createNewBranch', this.branchName); this.createNewBranch(this.branchName)
}, .then(() => {
showErrorMessage(message) { this.loading = false;
this.loading = false; this.branchName = '';
flash(message, 'alert', this.$el);
},
createdNewBranch(newBranchName) {
this.loading = false;
this.branchName = '';
if (this.dropdownText) { if (this.dropdownText) {
this.dropdownText.textContent = newBranchName; this.dropdownText.textContent = this.currentBranch;
} }
this.toggleDropdown();
})
.catch(res => res.json().then((data) => {
this.loading = false;
flash(data.message, 'alert', this.$el);
}));
}, },
}, },
created() { created() {
...@@ -59,15 +61,6 @@ ...@@ -59,15 +61,6 @@
// text element is outside Vue app // text element is outside Vue app
this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text'); 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>
......
<script> <script>
import RepoStore from '../../stores/repo_store'; import { mapState } from 'vuex';
import RepoHelper from '../../helpers/repo_helper';
import eventHub from '../../event_hub';
import newModal from './modal.vue'; import newModal from './modal.vue';
import upload from './upload.vue'; import upload from './upload.vue';
...@@ -14,9 +12,13 @@ ...@@ -14,9 +12,13 @@
return { return {
openModal: false, openModal: false,
modalType: '', modalType: '',
currentPath: RepoStore.path,
}; };
}, },
computed: {
...mapState([
'path',
]),
},
methods: { methods: {
createNewItem(type) { createNewItem(type) {
this.modalType = type; this.modalType = type;
...@@ -25,19 +27,6 @@ ...@@ -25,19 +27,6 @@
toggleModalOpen() { toggleModalOpen() {
this.openModal = !this.openModal; 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> </script>
...@@ -70,7 +59,7 @@ ...@@ -70,7 +59,7 @@
</li> </li>
<li> <li>
<upload <upload
:current-path="currentPath" :path="path"
/> />
</li> </li>
<li> <li>
...@@ -88,7 +77,7 @@ ...@@ -88,7 +77,7 @@
<new-modal <new-modal
v-if="openModal" v-if="openModal"
:type="modalType" :type="modalType"
:current-path="currentPath" :path="path"
@toggle="toggleModalOpen" @toggle="toggleModalOpen"
/> />
</div> </div>
......
<script> <script>
import { mapActions } from 'vuex';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import popupDialog from '../../../vue_shared/components/popup_dialog.vue'; import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
import eventHub from '../../event_hub';
export default { export default {
props: { props: {
currentPath: { type: {
type: String, type: String,
required: true, required: true,
}, },
type: { path: {
type: String, type: String,
required: true, required: true,
}, },
}, },
data() { data() {
return { return {
entryName: this.currentPath !== '' ? `${this.currentPath}/` : '', entryName: this.path !== '' ? `${this.path}/` : '',
}; };
}, },
components: { components: {
popupDialog, popupDialog,
}, },
methods: { methods: {
...mapActions([
'createTempEntry',
]),
createEntryInStore() { createEntryInStore() {
eventHub.$emit('createNewEntry', { this.createTempEntry({
name: this.entryName, name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
type: this.type, type: this.type,
toggleModal: true,
}); });
this.toggleModalOpen();
}, },
toggleModalOpen() { toggleModalOpen() {
this.$emit('toggle'); this.$emit('toggle');
......
<script> <script>
import eventHub from '../../event_hub'; import { mapActions } from 'vuex';
export default { export default {
props: { props: {
currentPath: { path: {
type: String, type: String,
required: true, required: true,
}, },
}, },
methods: { methods: {
...mapActions([
'createTempEntry',
]),
createFile(target, file, isText) { createFile(target, file, isText) {
const { name } = file; const { name } = file;
const nameWithPath = `${this.currentPath !== '' ? `${this.currentPath}/` : ''}${name}`;
let { result } = target; let { result } = target;
if (!isText) { if (!isText) {
result = result.split('base64,')[1]; result = result.split('base64,')[1];
} }
eventHub.$emit('createNewEntry', { this.createTempEntry({
name: nameWithPath, name,
type: 'blob', type: 'blob',
content: result, content: result,
toggleModal: false,
base64: !isText, base64: !isText,
}, isText); });
}, },
readFile(file) { readFile(file) {
const reader = new FileReader(); const reader = new FileReader();
......
<script> <script>
import { mapState, mapGetters } from 'vuex';
import RepoSidebar from './repo_sidebar.vue'; import RepoSidebar from './repo_sidebar.vue';
import RepoCommitSection from './repo_commit_section.vue'; import RepoCommitSection from './repo_commit_section.vue';
import RepoTabs from './repo_tabs.vue'; import RepoTabs from './repo_tabs.vue';
import RepoFileButtons from './repo_file_buttons.vue'; import RepoFileButtons from './repo_file_buttons.vue';
import RepoPreview from './repo_preview.vue'; import RepoPreview from './repo_preview.vue';
import RepoMixin from '../mixins/repo_mixin'; import repoEditor from './repo_editor.vue';
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';
export default { export default {
data() { computed: {
return Store; ...mapState([
'currentBlobView',
]),
...mapGetters([
'isCollapsed',
'changedFiles',
]),
}, },
mixins: [RepoMixin],
components: { components: {
RepoSidebar, RepoSidebar,
RepoTabs, RepoTabs,
RepoFileButtons, RepoFileButtons,
'repo-editor': MonacoLoaderHelper.repoEditorLoader, repoEditor,
RepoCommitSection, RepoCommitSection,
PopupDialog,
RepoPreview, RepoPreview,
}, },
created() {
eventHub.$on('createNewBranch', this.createNewBranch);
},
mounted() { mounted() {
Helper.getContent().catch(Helper.loadingError); const returnValue = 'Are you sure you want to lose unsaved changes?';
}, window.onbeforeunload = (e) => {
destroyed() { if (!this.changedFiles.length) return undefined;
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);
eventHub.$emit('createNewBranchSuccess', newBranchName); Object.assign(e, {
eventHub.$emit('toggleNewBranchDropdown'); returnValue,
}).catch((err) => {
eventHub.$emit('createNewBranchError', err.response.data.message);
}); });
}, return returnValue;
};
}, },
}; };
</script> </script>
<template> <template>
<div class="repository-view"> <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/> <repo-sidebar/>
<div v-if="isMini" <div
class="panel-right" v-if="isCollapsed"
:class="{'edit-mode': editMode}"> class="panel-right"
>
<repo-tabs/> <repo-tabs/>
<component <component
:is="currentBlobView" :is="currentBlobView"
class="blob-viewer-container"/> />
<repo-file-buttons/> <repo-file-buttons/>
</div> </div>
</div> </div>
<repo-commit-section/> <repo-commit-section v-if="changedFiles.length" />
<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"
/>
</div> </div>
</template> </template>
<script> <script>
import Flash from '../../flash'; import { mapGetters, mapState, mapActions } from 'vuex';
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
import Service from '../services/repo_service';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import { visitUrl } from '../../lib/utils/url_utility'; import { n__ } from '../../locale';
export default { export default {
mixins: [RepoMixin],
data() {
return Store;
},
components: { components: {
PopupDialog, PopupDialog,
}, },
data() {
return {
showNewBranchDialog: false,
submitCommitsLoading: false,
startNewMR: false,
commitMessage: '',
};
},
computed: { computed: {
showCommitable() { ...mapState([
return this.isCommitable && this.changedFiles.length; 'currentBranch',
}, ]),
...mapGetters([
branchPaths() { 'changedFiles',
return this.changedFiles.map(f => f.path); ]),
}, commitButtonDisabled() {
cantCommitYet() {
return !this.commitMessage || this.submitCommitsLoading; return !this.commitMessage || this.submitCommitsLoading;
}, },
commitButtonText() {
filePluralize() { return n__('Commit %d file', 'Commit %d files', this.changedFiles.length);
return this.changedFiles.length > 1 ? 'files' : 'file';
}, },
}, },
methods: { methods: {
commitToNewBranch(status) { ...mapActions([
if (status) { 'checkCommitStatus',
this.showNewBranchDialog = false; 'commitChanges',
this.tryCommit(null, true, true); 'getTreeData',
} else { ]),
// reset the state 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 = { const payload = {
branch, branch: createNewBranch ? `${this.currentBranch}-${new Date().getTime().toString()}` : this.currentBranch,
commit_message: commitMessage, commit_message: this.commitMessage,
actions, 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; this.showNewBranchDialog = false;
} this.submitCommitsLoading = true;
Service.commitFiles(payload)
this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => { .then(() => {
this.resetCommitState(); this.submitCommitsLoading = false;
if (this.startNewMR) { this.getTreeData();
this.redirectToNewMr(branch);
} else {
this.redirectToBranch(branch);
}
}) })
.catch(() => { .catch(() => {
Flash('An error occurred while committing your changes'); this.submitCommitsLoading = false;
}); });
}, },
tryCommit() {
tryCommit(e, skipBranchCheck = false, newBranch = false) {
this.submitCommitsLoading = true; this.submitCommitsLoading = true;
if (skipBranchCheck) { this.checkCommitStatus()
this.makeCommit(newBranch); .then((branchChanged) => {
} else { if (branchChanged) {
Store.setBranchHash() this.showNewBranchDialog = true;
.then(() => { } else {
if (Store.branchChanged) { this.makeCommit();
Store.showNewBranchDialog = true; }
return; })
} .catch(() => {
this.makeCommit(newBranch); this.submitCommitsLoading = false;
}) });
.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);
}, },
}, },
}; };
</script> </script>
<template> <template>
<div <div id="commit-area">
v-if="showCommitable"
id="commit-area">
<popup-dialog <popup-dialog
v-if="showNewBranchDialog" v-if="showNewBranchDialog"
:primary-button-label="__('Create new branch')" :primary-button-label="__('Create new branch')"
kind="primary" kind="primary"
:title="__('Branch has changed')" :title="__('Branch has changed')"
:text="__('This branch has changed since you started editing. Would you like to create a new branch?')" :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 <form
class="form-horizontal" class="form-horizontal"
@submit.prevent="tryCommit"> @submit.prevent="tryCommit()">
<fieldset> <fieldset>
<div class="form-group"> <div class="form-group">
<label class="col-md-4 control-label staged-files"> <label class="col-md-4 control-label staged-files">
...@@ -145,10 +103,10 @@ export default { ...@@ -145,10 +103,10 @@ export default {
<div class="col-md-6"> <div class="col-md-6">
<ul class="list-unstyled changed-files"> <ul class="list-unstyled changed-files">
<li <li
v-for="branchPath in branchPaths" v-for="(file, index) in changedFiles"
:key="branchPath"> :key="index">
<span class="help-block"> <span class="help-block">
{{branchPath}} {{ file.path }}
</span> </span>
</li> </li>
</ul> </ul>
...@@ -183,9 +141,8 @@ export default { ...@@ -183,9 +141,8 @@ export default {
</div> </div>
<div class="col-md-offset-4 col-md-6"> <div class="col-md-offset-4 col-md-6">
<button <button
ref="submitCommit"
type="submit" type="submit"
:disabled="cantCommitYet" :disabled="commitButtonDisabled"
class="btn btn-success"> class="btn btn-success">
<i <i
v-if="submitCommitsLoading" v-if="submitCommitsLoading"
...@@ -194,7 +151,7 @@ export default { ...@@ -194,7 +151,7 @@ export default {
aria-label="loading"> aria-label="loading">
</i> </i>
<span class="commit-summary"> <span class="commit-summary">
Commit {{changedFiles.length}} {{filePluralize}} {{ commitButtonText }}
</span> </span>
</button> </button>
</div> </div>
......
<script> <script>
import Store from '../stores/repo_store'; import { mapGetters, mapActions, mapState } from 'vuex';
import RepoMixin from '../mixins/repo_mixin'; import popupDialog from '../../vue_shared/components/popup_dialog.vue';
export default { export default {
data() { components: {
return Store; popupDialog,
}, },
mixins: [RepoMixin],
computed: { computed: {
...mapState([
'editMode',
'discardPopupOpen',
]),
...mapGetters([
'canEditFile',
]),
buttonLabel() { buttonLabel() {
return this.editMode ? this.__('Cancel edit') : this.__('Edit'); return this.editMode ? this.__('Cancel edit') : this.__('Edit');
}, },
showButton() {
return this.isCommitable &&
!this.activeFile.render_error &&
!this.binary &&
this.openedFiles.length;
},
}, },
methods: { methods: {
editCancelClicked() { ...mapActions([
if (this.changedFiles.length) { 'toggleEditMode',
this.dialog.open = true; 'closeDiscardPopup',
return; ]),
}
this.editMode = !this.editMode;
Store.toggleBlobView();
},
}, },
}; };
</script> </script>
<template> <template>
<button <div class="editable-mode">
v-if="showButton" <button
class="btn btn-default" v-if="canEditFile"
type="button" class="btn btn-default"
@click.prevent="editCancelClicked"> type="button"
<i @click.prevent="toggleEditMode()">
v-if="!editMode" <i
class="fa fa-pencil" v-if="!editMode"
aria-hidden="true"> class="fa fa-pencil"
</i> aria-hidden="true">
<span> </i>
{{buttonLabel}} <span>
</span> {{buttonLabel}}
</button> </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> </template>
<script> <script>
/* global monaco */ /* global monaco */
import Store from '../stores/repo_store'; import { mapGetters, mapActions } from 'vuex';
import Service from '../services/repo_service'; import flash from '../../flash';
import Helper from '../helpers/repo_helper'; import monacoLoader from '../monaco_loader';
const RepoEditor = {
data() {
return Store;
},
export default {
destroyed() { destroyed() {
if (Helper.monacoInstance) { if (this.monacoInstance) {
Helper.monacoInstance.destroy(); this.monacoInstance.destroy();
} }
}, },
mounted() { mounted() {
Service.getRaw(this.activeFile) if (this.monaco) {
.then((rawResponse) => { this.initMonaco();
Store.blobRaw = rawResponse.data; } else {
Store.activeFile.plain = rawResponse.data; monacoLoader(['vs/editor/editor.main'], () => {
this.monaco = monaco;
const monacoInstance = Helper.monaco.editor.create(this.$el, {
model: null, this.initMonaco();
readOnly: false, });
contextmenu: true, }
scrollBeyondLastLine: false, },
}); 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(); this.addMonacoEvents();
}) }
.catch(Helper.loadingError);
},
methods: { this.setupEditor();
})
.catch(() => flash('Error setting up monaco. Please try again.'));
},
setupEditor() { 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() { this.monacoInstance.setModel(newModel);
if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
this.$el.style.display = 'none';
} else {
this.$el.style.display = 'inline-block';
}
}, },
addMonacoEvents() { addMonacoEvents() {
Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp); this.monacoInstance.onKeyUp(() => {
Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this)); this.changeFileContent({
}, file: this.activeFile,
content: this.monacoInstance.getValue(),
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);
}
}, },
}, },
watch: { watch: {
dialog: { activeFile(oldVal, newVal) {
handler(obj) { if (newVal && !newVal.active) {
const newObj = obj; this.initMonaco();
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,
});
} }
}, },
}, },
computed: { computed: {
...mapGetters([
'activeFile',
'activeFileExtension',
]),
shouldHideEditor() { shouldHideEditor() {
return !this.openedFiles.length || (this.binary && !this.activeFile.raw); return this.activeFile.binary && !this.activeFile.raw;
}, },
}, },
}; };
export default RepoEditor;
</script> </script>
<template> <template>
<div id="ide" v-if='!shouldHideEditor'></div> <div
id="ide"
v-if='!shouldHideEditor'
class="blob-viewer-container blob-editor-container"
>
</div>
</template> </template>
<script> <script>
import { mapActions, mapGetters } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago'; import timeAgoMixin from '../../vue_shared/mixins/timeago';
import eventHub from '../event_hub';
import repoMixin from '../mixins/repo_mixin';
export default { export default {
mixins: [ mixins: [
repoMixin,
timeAgoMixin, timeAgoMixin,
], ],
props: { props: {
...@@ -15,13 +13,15 @@ ...@@ -15,13 +13,15 @@
}, },
}, },
computed: { computed: {
...mapGetters([
'isCollapsed',
]),
fileIcon() { fileIcon() {
const classObj = { return {
'fa-spinner fa-spin': this.file.loading, 'fa-spinner fa-spin': this.file.loading,
[this.file.icon]: !this.file.loading, [this.file.icon]: !this.file.loading,
'fa-folder-open': !this.file.loading && this.file.opened, 'fa-folder-open': !this.file.loading && this.file.opened,
}; };
return classObj;
}, },
levelIndentation() { levelIndentation() {
return { return {
...@@ -33,9 +33,9 @@ ...@@ -33,9 +33,9 @@
}, },
}, },
methods: { methods: {
linkClicked(file) { ...mapActions([
eventHub.$emit('fileNameClicked', file); 'clickedTreeRow',
}, ]),
}, },
}; };
</script> </script>
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
<template> <template>
<tr <tr
class="file" class="file"
@click.prevent="linkClicked(file)"> @click.prevent="clickedTreeRow(file)">
<td> <td>
<i <i
class="fa fa-fw file-icon" class="fa fa-fw file-icon"
...@@ -71,7 +71,7 @@ ...@@ -71,7 +71,7 @@
</template> </template>
</td> </td>
<template v-if="!isMini"> <template v-if="!isCollapsed">
<td class="hidden-sm hidden-xs"> <td class="hidden-sm hidden-xs">
<a <a
@click.stop @click.stop
......
<script> <script>
import Store from '../stores/repo_store'; import { mapGetters } from 'vuex';
import Helper from '../helpers/repo_helper';
import RepoMixin from '../mixins/repo_mixin';
const RepoFileButtons = {
data() {
return Store;
},
mixins: [RepoMixin],
export default {
computed: { computed: {
...mapGetters([
'activeFile',
]),
showButtons() { showButtons() {
return this.activeFile.raw_path || return this.activeFile.rawPath ||
this.activeFile.blame_path || this.activeFile.blamePath ||
this.activeFile.commits_path || this.activeFile.commitsPath ||
this.activeFile.permalink; this.activeFile.permalink;
}, },
rawDownloadButtonLabel() { rawDownloadButtonLabel() {
return this.binary ? 'Download' : 'Raw'; return this.activeFile.binary ? 'Download' : 'Raw';
},
canPreview() {
return Helper.isRenderable();
}, },
}, },
methods: {
rawPreviewToggle: Store.toggleRawPreview,
},
}; };
export default RepoFileButtons;
</script> </script>
<template> <template>
...@@ -40,11 +25,11 @@ export default RepoFileButtons; ...@@ -40,11 +25,11 @@ export default RepoFileButtons;
class="repo-file-buttons" class="repo-file-buttons"
> >
<a <a
:href="activeFile.raw_path" :href="activeFile.rawPath"
target="_blank" target="_blank"
class="btn btn-default raw" class="btn btn-default raw"
rel="noopener noreferrer"> rel="noopener noreferrer">
{{rawDownloadButtonLabel}} {{ rawDownloadButtonLabel }}
</a> </a>
<div <div
...@@ -52,12 +37,12 @@ export default RepoFileButtons; ...@@ -52,12 +37,12 @@ export default RepoFileButtons;
role="group" role="group"
aria-label="File actions"> aria-label="File actions">
<a <a
:href="activeFile.blame_path" :href="activeFile.blamePath"
class="btn btn-default blame"> class="btn btn-default blame">
Blame Blame
</a> </a>
<a <a
:href="activeFile.commits_path" :href="activeFile.commitsPath"
class="btn btn-default history"> class="btn btn-default history">
History History
</a> </a>
...@@ -67,13 +52,5 @@ export default RepoFileButtons; ...@@ -67,13 +52,5 @@ export default RepoFileButtons;
Permalink Permalink
</a> </a>
</div> </div>
<a
v-if="canPreview"
href="#"
@click.prevent="rawPreviewToggle"
class="btn btn-default preview">
{{activeFileLabel}}
</a>
</div> </div>
</template> </template>
<script> <script>
import repoMixin from '../mixins/repo_mixin'; import { mapGetters } from 'vuex';
export default { export default {
mixins: [ computed: {
repoMixin, ...mapGetters([
], 'isCollapsed',
]),
},
methods: { methods: {
lineOfCode(n) { lineOfCode(n) {
return `skeleton-line-${n}`; return `skeleton-line-${n}`;
...@@ -28,7 +30,7 @@ ...@@ -28,7 +30,7 @@
</div> </div>
</div> </div>
</td> </td>
<template v-if="!isMini"> <template v-if="!isCollapsed">
<td <td
class="hidden-sm hidden-xs"> class="hidden-sm hidden-xs">
<div class="animation-container"> <div class="animation-container">
......
<script> <script>
import eventHub from '../event_hub'; import { mapGetters, mapState, mapActions } from 'vuex';
import repoMixin from '../mixins/repo_mixin';
export default { export default {
mixins: [
repoMixin,
],
props: {
prevUrl: {
type: String,
required: true,
},
},
computed: { computed: {
...mapState([
'parentTreeUrl',
]),
...mapGetters([
'isCollapsed',
]),
colSpanCondition() { colSpanCondition() {
return this.isMini ? undefined : 3; return this.isCollapsed ? undefined : 3;
}, },
}, },
methods: { methods: {
linkClicked(file) { ...mapActions([
eventHub.$emit('goToPreviousDirectoryClicked', file); 'getTreeData',
}, ]),
}, },
}; };
</script> </script>
...@@ -30,9 +26,9 @@ ...@@ -30,9 +26,9 @@
<td <td
:colspan="colSpanCondition" :colspan="colSpanCondition"
class="table-cell" class="table-cell"
@click.prevent="linkClicked(prevUrl)" @click.prevent="getTreeData({ endpoint: parentTreeUrl })"
> >
<a :href="prevUrl">...</a> <a :href="parentTreeUrl">...</a>
</td> </td>
</tr> </tr>
</template> </template>
<script> <script>
/* global LineHighlighter */ /* global LineHighlighter */
import { mapGetters } from 'vuex';
import Store from '../stores/repo_store';
export default { export default {
data() {
return Store;
},
computed: { computed: {
html() { ...mapGetters([
return this.activeFile.html; 'activeFile',
]),
renderErrorTooLarge() {
return this.activeFile.renderError === 'too_large';
}, },
}, },
methods: { methods: {
highlightFile() { highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight(); $(this.$el).find('.file-content').syntaxHighlight();
}, },
highlightLine() {
if (Store.activeLine > -1) {
this.lineHighlighter.highlightHash(`#L${Store.activeLine}`);
}
},
}, },
mounted() { mounted() {
this.highlightFile(); this.highlightFile();
...@@ -29,24 +23,18 @@ export default { ...@@ -29,24 +23,18 @@ export default {
scrollFileHolder: true, scrollFileHolder: true,
}); });
}, },
watch: { updated() {
html() { this.$nextTick(() => {
this.$nextTick(() => { this.highlightFile();
this.highlightFile(); });
this.highlightLine();
});
},
activeLine() {
this.highlightLine();
},
}, },
}; };
</script> </script>
<template> <template>
<div> <div class="blob-viewer-container">
<div <div
v-if="!activeFile.render_error" v-if="!activeFile.renderError"
v-html="activeFile.html"> v-html="activeFile.html">
</div> </div>
<div <div
...@@ -57,17 +45,17 @@ export default { ...@@ -57,17 +45,17 @@ export default {
</p> </p>
</div> </div>
<div <div
v-else-if="activeFile.tooLarge" v-else-if="renderErrorTooLarge"
class="vertical-center render-error"> class="vertical-center render-error">
<p class="text-center"> <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> </p>
</div> </div>
<div <div
v-else v-else
class="vertical-center render-error"> class="vertical-center render-error">
<p class="text-center"> <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> </p>
</div> </div>
</div> </div>
......
<script> <script>
import _ from 'underscore'; import { mapState, mapGetters, mapActions } from 'vuex';
import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper';
import Store from '../stores/repo_store';
import eventHub from '../event_hub';
import RepoPreviousDirectory from './repo_prev_directory.vue'; import RepoPreviousDirectory from './repo_prev_directory.vue';
import RepoFile from './repo_file.vue'; import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue'; import RepoLoadingFile from './repo_loading_file.vue';
import RepoMixin from '../mixins/repo_mixin';
export default { export default {
mixins: [RepoMixin],
components: { components: {
'repo-previous-directory': RepoPreviousDirectory, 'repo-previous-directory': RepoPreviousDirectory,
'repo-file': RepoFile, 'repo-file': RepoFile,
'repo-loading-file': RepoLoadingFile, 'repo-loading-file': RepoLoadingFile,
}, },
created() { created() {
window.addEventListener('popstate', this.checkHistory); window.addEventListener('popstate', this.popHistoryState);
}, },
destroyed() { destroyed() {
eventHub.$off('fileNameClicked', this.fileClicked); window.removeEventListener('popstate', this.popHistoryState);
eventHub.$off('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
window.removeEventListener('popstate', this.checkHistory);
}, },
mounted() { mounted() {
eventHub.$on('fileNameClicked', this.fileClicked); this.getTreeData();
eventHub.$on('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
},
data() {
return Store;
}, },
computed: { computed: {
flattendFiles() { ...mapState([
const mapFiles = arr => (!arr.files.length ? [] : _.map(arr.files, a => [a, mapFiles(a)])); 'loading',
'isRoot',
return _.chain(this.files) ]),
.map(arr => [arr, mapFiles(arr)]) ...mapState({
.flatten() projectName(state) {
.value(); return state.project.name;
}, },
}),
...mapGetters([
'treeList',
'isCollapsed',
]),
}, },
methods: { methods: {
checkHistory() { ...mapActions([
let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1); 'getTreeData',
if (!selectedFile) { 'popHistoryState',
// 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);
},
}, },
}; };
</script> </script>
<template> <template>
<div id="sidebar" :class="{'sidebar-mini' : isMini}"> <div id="sidebar" :class="{'sidebar-mini' : isCollapsed}">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th <th
v-if="isMini" v-if="isCollapsed"
class="repo-file-options title" class="repo-file-options title"
> >
<strong class="clgray"> <strong class="clgray">
...@@ -136,17 +71,16 @@ export default { ...@@ -136,17 +71,16 @@ export default {
</thead> </thead>
<tbody> <tbody>
<repo-previous-directory <repo-previous-directory
v-if="!isRoot && !loading.tree" v-if="!isRoot && treeList.length"
:prev-url="prevURL"
/> />
<repo-loading-file <repo-loading-file
v-if="!flattendFiles.length && loading.tree" v-if="!treeList.length && loading"
v-for="n in 5" v-for="n in 5"
:key="n" :key="n"
/> />
<repo-file <repo-file
v-for="file in flattendFiles" v-for="(file, index) in treeList"
:key="file.id" :key="index"
:file="file" :file="file"
/> />
</tbody> </tbody>
......
<script> <script>
import Store from '../stores/repo_store'; import { mapActions } from 'vuex';
const RepoTab = { export default {
props: { props: {
tab: { tab: {
type: Object, type: Object,
...@@ -11,7 +11,7 @@ const RepoTab = { ...@@ -11,7 +11,7 @@ const RepoTab = {
computed: { computed: {
closeLabel() { closeLabel() {
if (this.tab.changed) { if (this.tab.changed || this.tab.tempFile) {
return `${this.tab.name} changed`; return `${this.tab.name} changed`;
} }
return `Close ${this.tab.name}`; return `Close ${this.tab.name}`;
...@@ -26,29 +26,23 @@ const RepoTab = { ...@@ -26,29 +26,23 @@ const RepoTab = {
}, },
methods: { methods: {
tabClicked(file) { ...mapActions([
Store.setActiveFiles(file); 'setFileActive',
}, 'closeFile',
closeTab(file) { ]),
if (file.changed || file.tempFile) return;
Store.removeFromOpenedFiles(file);
},
}, },
}; };
export default RepoTab;
</script> </script>
<template> <template>
<li <li
:class="{ active : tab.active }" :class="{ active : tab.active }"
@click="tabClicked(tab)" @click="setFileActive(tab)"
> >
<button <button
type="button" type="button"
class="close-btn" class="close-btn"
@click.stop.prevent="closeTab(tab)" @click.stop.prevent="closeFile({ file: tab })"
:aria-label="closeLabel"> :aria-label="closeLabel">
<i <i
class="fa" class="fa"
...@@ -61,7 +55,7 @@ export default RepoTab; ...@@ -61,7 +55,7 @@ export default RepoTab;
href="#" href="#"
class="repo-tab" class="repo-tab"
:title="tab.url" :title="tab.url"
@click.prevent="tabClicked(tab)"> @click.prevent.stop="setFileActive(tab)">
{{tab.name}} {{tab.name}}
</a> </a>
</li> </li>
......
<script> <script>
import Store from '../stores/repo_store'; import { mapState } from 'vuex';
import RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin';
export default { export default {
mixins: [RepoMixin],
components: { components: {
'repo-tab': RepoTab, 'repo-tab': RepoTab,
}, },
data() { computed: {
return Store; ...mapState([
'openFiles',
]),
}, },
}; };
</script> </script>
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
class="list-unstyled" class="list-unstyled"
> >
<repo-tab <repo-tab
v-for="tab in openedFiles" v-for="tab in openFiles"
:key="tab.id" :key="tab.id"
:tab="tab" :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 Vue from 'vue';
import { mapActions } from 'vuex';
import { convertPermissionToBoolean } from '../lib/utils/common_utils'; 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 Repo from './components/repo.vue';
import RepoEditButton from './components/repo_edit_button.vue'; import RepoEditButton from './components/repo_edit_button.vue';
import newBranchForm from './components/new_branch_form.vue'; import newBranchForm from './components/new_branch_form.vue';
import newDropdown from './components/new_dropdown/index.vue'; import newDropdown from './components/new_dropdown/index.vue';
import store from './stores';
import Translate from '../vue_shared/translate'; 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) { function initRepo(el) {
if (!el) return null;
return new Vue({ return new Vue({
el, el,
store,
components: { components: {
repo: Repo, 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) { render(createElement) {
return createElement('repo'); return createElement('repo');
}, },
...@@ -59,15 +54,20 @@ function initRepo(el) { ...@@ -59,15 +54,20 @@ function initRepo(el) {
function initRepoEditButton(el) { function initRepoEditButton(el) {
return new Vue({ return new Vue({
el, el,
store,
components: { components: {
repoEditButton: RepoEditButton, repoEditButton: RepoEditButton,
}, },
render(createElement) {
return createElement('repo-edit-button');
},
}); });
} }
function initNewDropdown(el) { function initNewDropdown(el) {
return new Vue({ return new Vue({
el, el,
store,
components: { components: {
newDropdown, newDropdown,
}, },
...@@ -87,32 +87,20 @@ function initNewBranchForm() { ...@@ -87,32 +87,20 @@ function initNewBranchForm() {
components: { components: {
newBranchForm, newBranchForm,
}, },
store,
render(createElement) { render(createElement) {
return createElement('new-branch-form', { return createElement('new-branch-form');
props: {
currentBranch: Store.currentBranch,
},
});
}, },
}); });
} }
function initRepoBundle() { const repo = document.getElementById('repo');
const repo = document.getElementById('repo'); const editButton = document.querySelector('.editable-mode');
const editButton = document.querySelector('.editable-mode'); const newDropdownHolder = document.querySelector('.js-new-dropdown');
const newDropdownHolder = document.querySelector('.js-new-dropdown');
setInitialStore(repo.dataset);
addEventsForNonVueEls();
initDropdowns();
Vue.use(Translate);
initRepo(repo);
initRepoEditButton(editButton);
initNewBranchForm();
initNewDropdown(newDropdownHolder);
}
$(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 { ...@@ -7,6 +7,7 @@ export default class SidebarService {
constructor(endpointMap) { constructor(endpointMap) {
if (!SidebarService.singleton) { if (!SidebarService.singleton) {
this.endpoint = endpointMap.endpoint; this.endpoint = endpointMap.endpoint;
this.toggleSubscriptionEndpoint = endpointMap.toggleSubscriptionEndpoint;
this.moveIssueEndpoint = endpointMap.moveIssueEndpoint; this.moveIssueEndpoint = endpointMap.moveIssueEndpoint;
this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint; this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint;
...@@ -36,6 +37,10 @@ export default class SidebarService { ...@@ -36,6 +37,10 @@ export default class SidebarService {
}); });
} }
toggleSubscription() {
return Vue.http.post(this.toggleSubscriptionEndpoint);
}
moveIssue(moveToProjectId) { moveIssue(moveToProjectId) {
return Vue.http.post(this.moveIssueEndpoint, { return Vue.http.post(this.moveIssueEndpoint, {
move_to_project_id: moveToProjectId, move_to_project_id: moveToProjectId,
......
...@@ -4,6 +4,8 @@ import SidebarAssignees from './components/assignees/sidebar_assignees'; ...@@ -4,6 +4,8 @@ import SidebarAssignees from './components/assignees/sidebar_assignees';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue'; import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue'; import SidebarMoveIssue from './lib/sidebar_move_issue';
import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue'; 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 Translate from '../vue_shared/translate';
import Mediator from './sidebar_mediator'; import Mediator from './sidebar_mediator';
...@@ -49,6 +51,36 @@ function mountLockComponent(mediator) { ...@@ -49,6 +51,36 @@ function mountLockComponent(mediator) {
}).$mount(el); }).$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() { function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions); const mediator = new Mediator(sidebarOptions);
...@@ -63,6 +95,8 @@ function domContentLoaded() { ...@@ -63,6 +95,8 @@ function domContentLoaded() {
mountConfidentialComponent(mediator); mountConfidentialComponent(mediator);
mountLockComponent(mediator); mountLockComponent(mediator);
mountParticipantsComponent();
mountSubscriptionsComponent();
new SidebarMoveIssue( new SidebarMoveIssue(
mediator, mediator,
......
...@@ -8,6 +8,7 @@ export default class SidebarMediator { ...@@ -8,6 +8,7 @@ export default class SidebarMediator {
this.store = new Store(options); this.store = new Store(options);
this.service = new Service({ this.service = new Service({
endpoint: options.endpoint, endpoint: options.endpoint,
toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint,
moveIssueEndpoint: options.moveIssueEndpoint, moveIssueEndpoint: options.moveIssueEndpoint,
projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint, projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
}); });
...@@ -39,10 +40,25 @@ export default class SidebarMediator { ...@@ -39,10 +40,25 @@ export default class SidebarMediator {
.then((data) => { .then((data) => {
this.store.setAssigneeData(data); this.store.setAssigneeData(data);
this.store.setTimeTrackingData(data); this.store.setTimeTrackingData(data);
this.store.setParticipantsData(data);
this.store.setSubscriptionsData(data);
}) })
.catch(() => new Flash('Error occurred when fetching sidebar 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) { fetchAutocompleteProjects(searchTerm) {
return this.service.getProjectsAutocomplete(searchTerm) return this.service.getProjectsAutocomplete(searchTerm)
.then(response => response.json()) .then(response => response.json())
......
...@@ -12,10 +12,14 @@ export default class SidebarStore { ...@@ -12,10 +12,14 @@ export default class SidebarStore {
this.assignees = []; this.assignees = [];
this.isFetching = { this.isFetching = {
assignees: true, assignees: true,
participants: true,
subscriptions: true,
}; };
this.autocompleteProjects = []; this.autocompleteProjects = [];
this.moveToProjectId = 0; this.moveToProjectId = 0;
this.isLockDialogOpen = false; this.isLockDialogOpen = false;
this.participants = [];
this.subscribed = null;
SidebarStore.singleton = this; SidebarStore.singleton = this;
} }
...@@ -37,6 +41,20 @@ export default class SidebarStore { ...@@ -37,6 +41,20 @@ export default class SidebarStore {
this.humanTotalTimeSpent = data.human_total_time_spent; 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) { addAssignee(assignee) {
if (!this.findAssignee(assignee)) { if (!this.findAssignee(assignee)) {
this.assignees.push(assignee); this.assignees.push(assignee);
...@@ -61,6 +79,10 @@ export default class SidebarStore { ...@@ -61,6 +79,10 @@ export default class SidebarStore {
this.autocompleteProjects = projects; this.autocompleteProjects = projects;
} }
setSubscribedState(subscribed) {
this.subscribed = subscribed;
}
setMoveToProjectId(moveToProjectId) { setMoveToProjectId(moveToProjectId) {
this.moveToProjectId = moveToProjectId; this.moveToProjectId = moveToProjectId;
} }
......
...@@ -11,7 +11,7 @@ export default class MRWidgetService { ...@@ -11,7 +11,7 @@ export default class MRWidgetService {
this.removeWIPResource = Vue.resource(endpoints.removeWIPPath); this.removeWIPResource = Vue.resource(endpoints.removeWIPPath);
this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath); this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath);
this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath); 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); this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath);
} }
......
...@@ -23,11 +23,16 @@ ...@@ -23,11 +23,16 @@
@include webkit-prefix(animation-duration, 2s); @include webkit-prefix(animation-duration, 2s);
} }
&.spin { &.spin-cw {
transform-origin: center; transform-origin: center;
animation: spin 4s linear infinite; animation: spin 4s linear infinite;
} }
&.spin-ccw {
transform-origin: center;
animation: spin 4s linear infinite reverse;
}
&.flipOutX, &.flipOutX,
&.flipOutY, &.flipOutY,
&.bounceIn, &.bounceIn,
......
...@@ -209,7 +209,6 @@ ...@@ -209,7 +209,6 @@
padding: 24px 0 0; padding: 24px 0 0;
.nav-links { .nav-links {
justify-content: center;
width: 100%; width: 100%;
float: none; float: none;
...@@ -217,6 +216,14 @@ ...@@ -217,6 +216,14 @@
float: none; float: none;
} }
} }
li:first-child {
margin-left: auto;
}
li:last-child {
margin-right: auto;
}
} }
.group-info { .group-info {
......
...@@ -542,7 +542,9 @@ ...@@ -542,7 +542,9 @@
} }
.participants-list { .participants-list {
margin: -5px; display: flex;
flex-wrap: wrap;
margin: -7px;
} }
...@@ -553,7 +555,7 @@ ...@@ -553,7 +555,7 @@
.participants-author { .participants-author {
display: inline-block; display: inline-block;
padding: 5px; padding: 7px;
&:nth-of-type(7n) { &:nth-of-type(7n) {
padding-right: 0; padding-right: 0;
......
.monaco-loader { .modal.popup-dialog {
position: absolute; display: block;
top: 0; background-color: $black-transparent;
left: 0; z-index: 2100;
right: 0;
bottom: 0; @media (min-width: $screen-md-min) {
background: $black-transparent; .modal-dialog {
width: 600px;
margin: 30px auto;
}
}
} }
.project-refs-form, .project-refs-form,
...@@ -41,6 +45,7 @@ ...@@ -41,6 +45,7 @@
} }
.tree-content-holder { .tree-content-holder {
display: -webkit-flex;
display: flex; display: flex;
min-height: 300px; min-height: 300px;
} }
...@@ -50,7 +55,9 @@ ...@@ -50,7 +55,9 @@
} }
.panel-right { .panel-right {
display: -webkit-flex;
display: flex; display: flex;
-webkit-flex-direction: column;
flex-direction: column; flex-direction: column;
width: 80%; width: 80%;
height: 100%; height: 100%;
...@@ -68,10 +75,6 @@ ...@@ -68,10 +75,6 @@
text-decoration: underline; text-decoration: underline;
} }
} }
.cursor {
display: none !important;
}
} }
.blob-no-preview { .blob-no-preview {
...@@ -81,21 +84,12 @@ ...@@ -81,21 +84,12 @@
} }
} }
&.edit-mode { &.blob-editor-container {
.blob-viewer-container { overflow: hidden;
overflow: hidden;
}
.monaco-editor.vs {
.cursor {
background: $black;
border-color: $black;
display: block !important;
}
}
} }
.blob-viewer-container { .blob-viewer-container {
-webkit-flex: 1;
flex: 1; flex: 1;
overflow: auto; overflow: auto;
...@@ -125,6 +119,7 @@ ...@@ -125,6 +119,7 @@
} }
#tabs { #tabs {
position: relative;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
width: 100%; width: 100%;
...@@ -153,6 +148,10 @@ ...@@ -153,6 +148,10 @@
vertical-align: middle; vertical-align: middle;
text-decoration: none; text-decoration: none;
margin-right: 12px; margin-right: 12px;
&:focus {
outline: none;
}
} }
.close-btn { .close-btn {
...@@ -299,23 +298,3 @@ ...@@ -299,23 +298,3 @@
width: 100%; 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 @@ ...@@ -50,3 +50,10 @@
font-size: 11px; font-size: 11px;
} }
} }
@media (max-width: $screen-md-max) {
.runners-content {
width: 100%;
overflow: auto;
}
}
...@@ -12,12 +12,7 @@ class Projects::GroupLinksController < Projects::ApplicationController ...@@ -12,12 +12,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
if group if group
return render_404 unless can?(current_user, :read_group, group) return render_404 unless can?(current_user, :read_group, group)
Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group)
project.project_group_links.create(
group: group,
group_access: params[:link_group_access],
expires_at: params[:expires_at]
)
else else
flash[:alert] = 'Please select a group.' flash[:alert] = 'Please select a group.'
end end
...@@ -32,7 +27,9 @@ class Projects::GroupLinksController < Projects::ApplicationController ...@@ -32,7 +27,9 @@ class Projects::GroupLinksController < Projects::ApplicationController
end end
def destroy 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| respond_to do |format|
format.html do format.html do
...@@ -47,4 +44,8 @@ class Projects::GroupLinksController < Projects::ApplicationController ...@@ -47,4 +44,8 @@ class Projects::GroupLinksController < Projects::ApplicationController
def group_link_params def group_link_params
params.require(:group_link).permit(:group_access, :expires_at) params.require(:group_link).permit(:group_access, :expires_at)
end end
def group_link_create_params
params.permit(:link_group_access, :expires_at)
end
end end
...@@ -74,7 +74,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -74,7 +74,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
render json: serializer.represent(@issue) render json: serializer.represent(@issue, serializer: params[:serializer])
end end
end end
end end
......
...@@ -83,7 +83,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -83,7 +83,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000) 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 end
format.patch do format.patch do
......
This diff is collapsed.
...@@ -19,11 +19,7 @@ module NavHelper ...@@ -19,11 +19,7 @@ module NavHelper
end end
elsif current_path?('jobs#show') elsif current_path?('jobs#show')
%w[page-gutter build-sidebar right-sidebar-expanded] %w[page-gutter build-sidebar right-sidebar-expanded]
elsif current_path?('wikis#show') || elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access')
current_path?('wikis#edit') ||
current_path?('wikis#update') ||
current_path?('wikis#history') ||
current_path?('wikis#git_access')
%w[page-gutter wiki-sidebar right-sidebar-expanded] %w[page-gutter wiki-sidebar right-sidebar-expanded]
else else
[] []
......
...@@ -13,6 +13,8 @@ module Subscribable ...@@ -13,6 +13,8 @@ module Subscribable
end end
def subscribed?(user, project = nil) def subscribed?(user, project = nil)
return false unless user
if subscription = subscriptions.find_by(user: user, project: project) if subscription = subscriptions.find_by(user: user, project: project)
subscription.subscribed subscription.subscribed
else 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