Commit ef3ebed6 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'master' into refactor-clusters

parents 253bf69d 01340794
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-phantomjs-2.1-node-8.x-yarn-1.0-postgresql-9.6"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.13-phantomjs-2.1-node-8.x-yarn-1.0-postgresql-9.6"
.default-cache: &default-cache
key: "ruby-233-with-yarn"
key: "ruby-235-with-yarn"
paths:
- vendor/ruby
- .yarn-cache/
......@@ -52,7 +52,8 @@ stages:
<<: *dedicated-runner
variables:
TESTS_METADATA_S3_BUCKET: "gitlab-ce-cache"
before_script: []
before_script:
- source scripts/utils.sh
artifacts:
expire_in: 31d
paths:
......@@ -454,7 +455,7 @@ db:migrate:reset-mysql:
variables:
SETUP_DB: "false"
script:
- git fetch origin v8.14.10
- git fetch origin v9.3.0
- git checkout -f FETCH_HEAD
- bundle install $BUNDLE_INSTALL_FLAGS
- cp config/gitlab.yml.example config/gitlab.yml
......@@ -550,7 +551,7 @@ karma:
<<: *dedicated-runner
<<: *except-docs
<<: *pull-cache
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-chrome-61.0-node-8.x-yarn-1.0-postgresql-9.6"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.13-chrome-61.0-node-8.x-yarn-1.0-postgresql-9.6"
stage: test
variables:
BABEL_ENV: "coverage"
......
# Documentation
- source: /doc/(.+?)\.md/ # doc/administration/build_artifacts.md
public: '\1.html' # doc/administration/build_artifacts.html
......@@ -121,7 +121,8 @@ linters:
# Avoid nesting selectors too deeply.
NestingDepth:
enabled: false
enabled: true
max_depth: 6
# Always use placeholder selectors in @extend.
PlaceholderInExtend:
......
......@@ -100,7 +100,7 @@ entry.
- [CHANGED] Added defaults for protected branches dropdowns on the repository settings. !14278
- [CHANGED] Show confirmation modal before deleting account. !14360
- [CHANGED] Allow creating merge requests across a fork network. !14422
- [CHANGED] Re-arrange <script> tags before <template> tags in .vue files. !14671
- [CHANGED] Re-arrange script HTML tags before template HTML tags in .vue files. !14671
- [CHANGED] Create idea of read-only database. !14688
- [CHANGED] Add active states to nav bar counters.
- [CHANGED] Add view replaced file link for image diffs.
......
......@@ -21,10 +21,10 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Workflow labels](#workflow-labels)
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-ci-discussion-edge-platform-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
- [Priority labels (~Deliverable and ~Stretch)](#priority-labels-deliverable-and-stretch)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design--ui-elements)
- [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker)
- [Issue triaging](#issue-triaging)
- [Feature proposals](#feature-proposals)
......
0.49.0
\ No newline at end of file
0.51.0
......@@ -90,7 +90,7 @@ gem 'kaminari', '~> 1.0'
gem 'hamlit', '~> 2.6.1'
# Files attachments
gem 'carrierwave', '~> 1.1'
gem 'carrierwave', '~> 1.2'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
......@@ -281,7 +281,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~>0.7.0.beta17'
gem 'prometheus-client-mmap', '~>0.7.0.beta18'
gem 'raindrops', '~> 0.18'
end
......@@ -398,7 +398,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.45.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.51.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -107,7 +107,7 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
carrierwave (1.1.0)
carrierwave (1.2.1)
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
......@@ -273,7 +273,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.45.0)
gitaly-proto (0.51.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -623,7 +623,7 @@ GEM
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.7.0.beta17)
prometheus-client-mmap (0.7.0.beta18)
mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4)
coderay (~> 1.1.0)
......@@ -990,7 +990,7 @@ DEPENDENCIES
bundler-audit (~> 0.5.0)
capybara (~> 2.15.0)
capybara-screenshot (~> 1.0.0)
carrierwave (~> 1.1)
carrierwave (~> 1.2)
charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
......@@ -1030,7 +1030,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.45.0)
gitaly-proto (~> 0.51.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
......@@ -1106,7 +1106,7 @@ DEPENDENCIES
pg (~> 0.18.2)
poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta17)
prometheus-client-mmap (~> 0.7.0.beta18)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
......
......@@ -16,6 +16,7 @@ const Api = {
usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath)
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */
/* eslint-disable no-param-reassign, prefer-template, no-var, no-void, consistent-return */
import AccessorUtilities from './lib/utils/accessor';
window.Autosave = (function() {
function Autosave(field, key, resource) {
export default class Autosave {
constructor(field, key, resource) {
this.field = field;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.resource = resource;
......@@ -12,14 +13,10 @@ window.Autosave = (function() {
this.key = 'autosave/' + key;
this.field.data('autosave', this);
this.restore();
this.field.on('input', (function(_this) {
return function() {
return _this.save();
};
})(this));
this.field.on('input', () => this.save());
}
Autosave.prototype.restore = function() {
restore() {
var text;
if (!this.isLocalStorageAvailable) return;
......@@ -40,9 +37,9 @@ window.Autosave = (function() {
field.dispatchEvent(event);
}
}
};
}
Autosave.prototype.save = function() {
save() {
var text;
text = this.field.val();
......@@ -51,15 +48,11 @@ window.Autosave = (function() {
}
return this.reset();
};
}
Autosave.prototype.reset = function() {
reset() {
if (!this.isLocalStorageAvailable) return;
return window.localStorage.removeItem(this.key);
};
return Autosave;
})();
export default window.Autosave;
}
}
import autosize from 'vendor/autosize';
import Autosize from 'autosize';
document.addEventListener('DOMContentLoaded', () => {
const autosizeEls = document.querySelectorAll('.js-autosize');
autosize(autosizeEls);
autosize.update(autosizeEls);
Autosize(autosizeEls);
Autosize.update(autosizeEls);
});
/* eslint-disable comma-dangle, space-before-function-paren, no-new */
/* global IssuableContext */
/* global MilestoneSelect */
/* global LabelsSelect */
/* global Sidebar */
......@@ -11,6 +10,7 @@ import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
import Assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
const Store = gl.issueBoards.BoardsStore;
......
......@@ -64,19 +64,16 @@ export default class Clusters {
this.poll = new Poll({
resource: this.service,
method: 'fetchData',
successCallback: (data) => {
const { status, status_reason } = data.data;
this.updateContainer(status, status_reason);
},
errorCallback: () => {
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
},
successCallback: data => this.handleSuccess(data),
errorCallback: () => Clusters.handleError(),
});
if (!Visibility.hidden()) {
this.poll.makeRequest();
} else {
this.service.fetchData();
this.service.fetchData()
.then(data => this.handleSuccess(data))
.catch(() => Clusters.handleError());
}
Visibility.change(() => {
......@@ -88,6 +85,15 @@ export default class Clusters {
});
}
static handleError() {
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
}
handleSuccess(data) {
const { status, status_reason } = data.data;
this.updateContainer(status, status_reason);
}
hideAll() {
this.errorContainer.classList.add('hidden');
this.successContainer.classList.add('hidden');
......
......@@ -25,6 +25,11 @@
type: String,
required: true,
},
viewType: {
type: String,
required: false,
default: 'child',
},
},
mixins: [
pipelinesMixin,
......@@ -110,6 +115,7 @@
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
/>
</div>
</div>
......
......@@ -2,7 +2,7 @@ import Cookies from 'js-cookie';
import _ from 'underscore';
import bp from './breakpoints';
export default class NewNavSidebar {
export default class ContextualSidebar {
constructor() {
this.initDomElements();
this.render();
......@@ -55,7 +55,7 @@ export default class NewNavSidebar {
this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
}
NewNavSidebar.setCollapsedCookie(collapsed);
ContextualSidebar.setCollapsedCookie(collapsed);
this.toggleSidebarOverflow();
}
......
/* eslint-disable class-methods-use-this */
import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff';
......@@ -8,7 +6,7 @@ import imageDiffHelper from './image_diff/helpers/index';
const UNFOLD_COUNT = 20;
let isBound = false;
class Diff {
export default class Diff {
constructor() {
const $diffFile = $('.files .diff-file');
......@@ -104,7 +102,7 @@ class Diff {
}
this.highlightSelectedLine();
}
// eslint-disable-next-line class-methods-use-this
handleParallelLineDown(e) {
const line = $(e.currentTarget);
const table = line.closest('table');
......@@ -116,11 +114,11 @@ class Diff {
table.addClass(`${lineClass}-selected`);
}
}
// eslint-disable-next-line class-methods-use-this
diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type');
}
// eslint-disable-next-line class-methods-use-this
lineNumbers(line) {
const children = line.find('.diff-line-num').toArray();
if (children.length !== 2) {
......@@ -128,7 +126,7 @@ class Diff {
}
return children.map(elm => parseInt($(elm).data('linenumber'), 10) || 0);
}
// eslint-disable-next-line class-methods-use-this
highlightSelectedLine() {
const hash = gl.utils.getLocationHash();
const $diffFiles = $('.diff-file');
......@@ -141,6 +139,3 @@ class Diff {
}
}
}
window.gl = window.gl || {};
window.gl.Diff = Diff;
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global ProjectSelect */
/* global IssuableIndex */
import IssuableIndex from './issuable_index';
/* global Milestone */
/* global IssuableForm */
import IssuableForm from './issuable_form';
/* global LabelsSelect */
/* global MilestoneSelect */
/* global NewBranchForm */
/* global NotificationsForm */
/* global NotificationsDropdown */
/* global GroupAvatar */
import groupAvatar from './group_avatar';
import GroupLabelSubscription from './group_label_subscription';
/* global LineHighlighter */
import BuildArtifacts from './build_artifacts';
import CILintEditor from './ci_lint_editor';
/* global GroupsSelect */
import groupsSelect from './groups_select';
/* global Search */
/* global Admin */
/* global NamespaceSelects */
......@@ -87,6 +88,7 @@ import U2FAuthenticate from './u2f/authenticate';
import Members from './members';
import memberExpirationDate from './member_expiration_date';
import DueDateSelectors from './due_date_select';
import Diff from './diff';
(function() {
var Dispatcher;
......@@ -171,7 +173,7 @@ import DueDateSelectors from './due_date_select';
filteredSearchManager.setup();
}
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
IssuableIndex.init(pagePrefix);
new IssuableIndex(pagePrefix);
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
......@@ -229,16 +231,21 @@ import DueDateSelectors from './due_date_select';
case 'projects:milestones:new':
case 'projects:milestones:edit':
case 'projects:milestones:update':
new ZenMode();
new DueDateSelectors();
new GLForm($('.milestone-form'), true);
break;
case 'groups:milestones:new':
case 'groups:milestones:edit':
case 'groups:milestones:update':
new ZenMode();
new DueDateSelectors();
new GLForm($('.milestone-form'), true);
new GLForm($('.milestone-form'), false);
break;
case 'projects:compare:show':
new gl.Diff();
initChangesDropdown();
new Diff();
const paddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
break;
case 'projects:branches:new':
case 'projects:branches:create':
......@@ -273,7 +280,7 @@ import DueDateSelectors from './due_date_select';
}
case 'projects:merge_requests:creations:diffs':
case 'projects:merge_requests:edit':
new gl.Diff();
new Diff();
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form'), true);
new IssuableForm($('.merge-request-form'));
......@@ -307,7 +314,7 @@ import DueDateSelectors from './due_date_select';
new GLForm($('.release-form'), true);
break;
case 'projects:merge_requests:show':
new gl.Diff();
new Diff();
shortcut_handler = new ShortcutsIssuable(true);
new ZenMode();
......@@ -323,7 +330,7 @@ import DueDateSelectors from './due_date_select';
new gl.Activities();
break;
case 'projects:commit:show':
new gl.Diff();
new Diff();
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
new MiniPipelineGraph({
......@@ -411,7 +418,7 @@ import DueDateSelectors from './due_date_select';
break;
case 'projects:project_members:index':
memberExpirationDate('.js-access-expiration-date-groups');
new GroupsSelect();
groupsSelect();
memberExpirationDate();
new Members();
new UsersSelect();
......@@ -422,11 +429,11 @@ import DueDateSelectors from './due_date_select';
case 'admin:groups:create':
BindInOut.initAll();
new Group();
new GroupAvatar();
groupAvatar();
break;
case 'groups:edit':
case 'admin:groups:edit':
new GroupAvatar();
groupAvatar();
break;
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
......@@ -473,7 +480,7 @@ import DueDateSelectors from './due_date_select';
const $el = $(el);
if ($el.find('.dropdown-group-label').length) {
new gl.GroupLabelSubscription($el);
new GroupLabelSubscription($el);
} else {
new gl.ProjectLabelSubscription($el);
}
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
/* global notes */
/* Developer beware! Do not add logic to showButton or hideButton
* that will force a reflow. Doing so will create a signficant performance
* bottleneck for pages with large diffs. For a comprehensive list of what
......@@ -20,8 +17,10 @@ const DIFF_EXPANDED_CLASS = 'diff-expanded';
export default {
init($diffFile) {
/* Caching is used only when the following members are *true*. This is because there are likely to be
* differently configured versions of diffs in the same session. However if these values are true, they
/* Caching is used only when the following members are *true*.
* This is because there are likely to be
* differently configured versions of diffs in the same session.
* However if these values are true, they
* will be true in all cases */
if (!this.userCanCreateNote) {
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
window.GroupAvatar = (function() {
function GroupAvatar() {
$('.js-choose-group-avatar-button').on("click", function() {
var form;
form = $(this).closest("form");
return form.find(".js-group-avatar-input").click();
});
$('.js-group-avatar-input').on("change", function() {
var filename, form;
form = $(this).closest("form");
filename = $(this).val().replace(/^.*[\\\/]/, '');
return form.find(".js-avatar-filename").text(filename);
});
}
return GroupAvatar;
})();
export default function groupAvatar() {
$('.js-choose-group-avatar-button').on('click', function onClickGroupAvatar() {
const form = $(this).closest('form');
return form.find('.js-group-avatar-input').click();
});
$('.js-group-avatar-input').on('change', function onChangeAvatarInput() {
const form = $(this).closest('form');
// eslint-disable-next-line no-useless-escape
const filename = $(this).val().replace(/^.*[\\\/]/, '');
return form.find('.js-avatar-filename').text(filename);
});
}
/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */
class GroupLabelSubscription {
export default class GroupLabelSubscription {
constructor(container) {
const $container = $(container);
this.$dropdown = $container.find('.dropdown');
......@@ -18,7 +16,7 @@ class GroupLabelSubscription {
$.ajax({
type: 'POST',
url: url
url,
}).done(() => {
this.toggleSubscriptionButtons();
this.$unsubscribeButtons.removeAttr('data-url');
......@@ -35,7 +33,7 @@ class GroupLabelSubscription {
$.ajax({
type: 'POST',
url: url
url,
}).done(() => {
this.toggleSubscriptionButtons();
});
......@@ -47,6 +45,3 @@ class GroupLabelSubscription {
this.$unsubscribeButtons.toggleClass('hidden');
}
}
window.gl = window.gl || {};
window.gl.GroupLabelSubscription = GroupLabelSubscription;
/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var,
camelcase, one-var-declaration-per-line, quotes, object-shorthand,
prefer-arrow-callback, comma-dangle, consistent-return, yoda,
prefer-rest-params, prefer-spread, no-unused-vars, prefer-template,
promise/catch-or-return */
import Api from './api';
import { normalizeCRLFHeaders } from './lib/utils/common_utils';
var slice = [].slice;
export default function groupsSelect() {
// Needs to be accessible in rspec
window.GROUP_SELECT_PER_PAGE = 20;
$('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
const $select = $(this);
const allAvailable = $select.data('all-available');
const skipGroups = $select.data('skip-groups') || [];
$select.select2({
placeholder: 'Search for a group',
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
ajax: {
url: Api.buildUrl(Api.groupsPath),
dataType: 'json',
quietMillis: 250,
transport(params) {
return $.ajax(params)
.then((data, status, xhr) => {
const results = data || [];
window.GroupsSelect = (function() {
function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) {
const self = _this;
return function(i, select) {
var all_available, skip_groups;
const $select = $(select);
all_available = $select.data('all-available');
skip_groups = $select.data('skip-groups') || [];
$select.select2({
placeholder: "Search for a group",
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
ajax: {
url: Api.buildUrl(Api.groupsPath),
dataType: 'json',
quietMillis: 250,
transport: function (params) {
$.ajax(params).then((data, status, xhr) => {
const results = data || [];
const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders());
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
return {
results,
pagination: {
more,
},
};
}).then(params.success).fail(params.error);
},
data: function (search, page) {
return {
search,
page,
per_page: GroupsSelect.PER_PAGE,
all_available,
};
},
results: function (data, page) {
if (data.length) return { results: [] };
const groups = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false;
const results = groups.filter(group => skip_groups.indexOf(group.id) === -1);
const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders());
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
return {
results,
page,
more,
pagination: {
more,
},
};
},
},
initSelection: function(element, callback) {
var id;
id = $(element).val();
if (id !== "") {
return Api.group(id, callback);
}
},
formatResult: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return self.formatResult.apply(self, args);
},
formatSelection: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return self.formatSelection.apply(self, args);
},
dropdownCssClass: "ajax-groups-dropdown select2-infinite",
// we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) {
return m;
}
});
self.dropdown = document.querySelector('.select2-infinite .select2-results');
$select.on('select2-loaded', self.forceOverflow.bind(self));
};
})(this));
}
GroupsSelect.prototype.formatResult = function(group) {
var avatar;
if (group.avatar_url) {
avatar = group.avatar_url;
} else {
avatar = gon.default_avatar_url;
}
return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>";
};
GroupsSelect.prototype.formatSelection = function(group) {
return group.full_name;
};
})
.then(params.success)
.fail(params.error);
},
data(search, page) {
return {
search,
page,
per_page: window.GROUP_SELECT_PER_PAGE,
all_available: allAvailable,
};
},
results(data, page) {
if (data.length) return { results: [] };
GroupsSelect.prototype.forceOverflow = function (e) {
this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight)}px`;
};
const groups = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false;
const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
GroupsSelect.PER_PAGE = 20;
return {
results,
page,
more,
};
},
},
// eslint-disable-next-line consistent-return
initSelection(element, callback) {
const id = $(element).val();
if (id !== '') {
return Api.group(id, callback);
}
},
formatResult(object) {
return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`;
},
formatSelection(object) {
return object.full_name;
},
dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
// we do not want to escape markup since we are displaying html in results
escapeMarkup(m) {
return m;
},
});
return GroupsSelect;
})();
$select.on('select2-loaded', () => {
const dropdown = document.querySelector('.select2-infinite .select2-results');
dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
});
});
}
......@@ -7,10 +7,12 @@ import { highCountTrim } from '~/lib/utils/text_utility';
* @param {jQuery.Event} e
* @param {String} count
*/
$(document).on('todo:toggle', (e, count) => {
const parsedCount = parseInt(count, 10);
const $todoPendingCount = $('.todos-count');
export default function initTodoToggle() {
$(document).on('todo:toggle', (e, count) => {
const parsedCount = parseInt(count, 10);
const $todoPendingCount = $('.todos-count');
$todoPendingCount.text(highCountTrim(parsedCount));
$todoPendingCount.toggleClass('hidden', parsedCount === 0);
});
$todoPendingCount.text(highCountTrim(parsedCount));
$todoPendingCount.toggleClass('hidden', parsedCount === 0);
});
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, vars-on-top, no-new, max-len */
class ImporterStatus {
constructor(jobsUrl, importUrl) {
this.jobsUrl = jobsUrl;
this.importUrl = importUrl;
this.initStatusPage();
this.setAutoUpdate();
}
(function() {
window.ImporterStatus = (function() {
function ImporterStatus(jobs_url, import_url) {
this.jobs_url = jobs_url;
this.import_url = import_url;
this.initStatusPage();
this.setAutoUpdate();
}
initStatusPage() {
$('.js-add-to-import')
.off('click')
.on('click', (event) => {
const $btn = $(event.currentTarget);
const $tr = $btn.closest('tr');
const $targetField = $tr.find('.import-target');
const $namespaceInput = $targetField.find('.js-select-namespace option:selected');
const id = $tr.attr('id').replace('repo_', '');
let targetNamespace;
let newName;
if ($namespaceInput.length > 0) {
targetNamespace = $namespaceInput[0].innerHTML;
newName = $targetField.find('#path').prop('value');
$targetField.empty().append(`${targetNamespace}/${newName}`);
}
$btn.disable().addClass('is-loading');
ImporterStatus.prototype.initStatusPage = function() {
$('.js-add-to-import').off('click').on('click', (function(_this) {
return function(e) {
var $btn, $namespace_input, $target_field, $tr, id, target_namespace, newName;
$btn = $(e.currentTarget);
$tr = $btn.closest('tr');
$target_field = $tr.find('.import-target');
$namespace_input = $target_field.find('.js-select-namespace option:selected');
id = $tr.attr('id').replace('repo_', '');
target_namespace = null;
newName = null;
if ($namespace_input.length > 0) {
target_namespace = $namespace_input[0].innerHTML;
newName = $target_field.find('#path').prop('value');
$target_field.empty().append(target_namespace + "/" + newName);
}
$btn.disable().addClass('is-loading');
return $.post(_this.import_url, {
repo_id: id,
target_namespace: target_namespace,
new_name: newName
}, {
dataType: 'script'
});
};
})(this));
return $('.js-import-all').off('click').on('click', function(e) {
var $btn;
$btn = $(this);
return $.post(this.importUrl, {
repo_id: id,
target_namespace: targetNamespace,
new_name: newName,
}, {
dataType: 'script',
});
});
$('.js-import-all')
.off('click')
.on('click', function onClickImportAll() {
const $btn = $(this);
$btn.disable().addClass('is-loading');
return $('.js-add-to-import').each(function() {
return $('.js-add-to-import').each(function triggerAddImport() {
return $(this).trigger('click');
});
});
};
}
setAutoUpdate() {
return setInterval(() => $.get(this.jobsUrl, data => $.each(data, (i, job) => {
const jobItem = $(`#project_${job.id}`);
const statusField = jobItem.find('.job-status');
ImporterStatus.prototype.setAutoUpdate = function() {
return setInterval(((function(_this) {
return function() {
return $.get(_this.jobs_url, function(data) {
return $.each(data, function(i, job) {
var job_item, status_field;
job_item = $("#project_" + job.id);
status_field = job_item.find(".job-status");
if (job.import_status === 'finished') {
job_item.removeClass("active").addClass("success");
return status_field.html('<span><i class="fa fa-check"></i> done</span>');
} else if (job.import_status === 'scheduled') {
return status_field.html("<i class='fa fa-spinner fa-spin'></i> scheduled");
} else if (job.import_status === 'started') {
return status_field.html("<i class='fa fa-spinner fa-spin'></i> started");
} else {
return status_field.html(job.import_status);
}
});
});
};
})(this)), 4000);
};
const spinner = '<i class="fa fa-spinner fa-spin"></i>';
return ImporterStatus;
})();
switch (job.import_status) {
case 'finished':
jobItem.removeClass('active').addClass('success');
statusField.html('<span><i class="fa fa-check"></i> done</span>');
break;
case 'scheduled':
statusField.html(`${spinner} scheduled`);
break;
case 'started':
statusField.html(`${spinner} started`);
break;
default:
statusField.html(job.import_status);
break;
}
})), 4000);
}
}
$(function() {
if ($('.js-importer-status').length) {
var jobsImportPath = $('.js-importer-status').data('jobs-import-path');
var importPath = $('.js-importer-status').data('import-path');
// eslint-disable-next-line consistent-return
export default function initImporterStatus() {
const importerStatus = document.querySelector('.js-importer-status');
new window.ImporterStatus(jobsImportPath, importPath);
}
});
}).call(window);
if (importerStatus) {
const data = importerStatus.dataset;
return new ImporterStatus(data.jobsImportPath, data.importPath);
}
}
import stickyMonitor from './lib/utils/sticky';
export default () => {
stickyMonitor(document.querySelector('.js-diff-files-changed'));
export default (stickyTop) => {
stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
$('.js-diff-stats-dropdown').glDropdown({
filterable: true,
......
/* eslint-disable no-new */
/* global MilestoneSelect */
/* global LabelsSelect */
/* global IssuableContext */
import IssuableContext from './issuable_context';
/* global Sidebar */
import DueDateSelectors from './due_date_select';
......
/* eslint-disable no-new */
/* global LabelsSelect */
/* global MilestoneSelect */
/* global IssueStatusSelect */
/* global SubscriptionSelect */
import UsersSelect from './users_select';
import issueStatusSelect from './issue_status_select';
export default () => {
new UsersSelect();
new LabelsSelect();
new MilestoneSelect();
new IssueStatusSelect();
issueStatusSelect();
new SubscriptionSelect();
};
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
/* global IssuableIndex */
import _ from 'underscore';
import Flash from './flash';
......
/* eslint-disable class-methods-use-this, no-new */
/* global LabelsSelect */
/* global MilestoneSelect */
/* global IssueStatusSelect */
/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import './milestone_select';
import issueStatusSelect from './issue_status_select';
import './subscription_select';
import './labels_select';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
......@@ -45,7 +48,7 @@ export default class IssuableBulkUpdateSidebar {
initDropdowns() {
new LabelsSelect();
new MilestoneSelect();
new IssueStatusSelect();
issueStatusSelect();
new SubscriptionSelect();
}
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
import Cookies from 'js-cookie';
import bp from './breakpoints';
import UsersSelect from './users_select';
const PARTICIPANTS_ROW_COUNT = 7;
export default class IssuableContext {
constructor(currentUser) {
this.userSelect = new UsersSelect(currentUser);
(function() {
this.IssuableContext = (function() {
function IssuableContext(currentUser) {
this.initParticipants();
new UsersSelect(currentUser);
$('select.select2').select2({
width: 'resolve',
dropdownAutoWidth: true
});
$(".issuable-sidebar .inline-update").on("change", "select", function() {
return $(this).submit();
});
$(".issuable-sidebar .inline-update").on("change", ".js-assignee", function() {
return $(this).submit();
});
$(document).off('click', '.issuable-sidebar .dropdown-content a').on('click', '.issuable-sidebar .dropdown-content a', function(e) {
return e.preventDefault();
});
$(document).off('click', '.edit-link').on('click', '.edit-link', function(e) {
var $block, $selectbox;
$('select.select2').select2({
width: 'resolve',
dropdownAutoWidth: true,
});
$('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
return $(this).submit();
});
$('.issuable-sidebar .inline-update').on('change', '.js-assignee', function onClickAssignee() {
return $(this).submit();
});
$(document)
.off('click', '.issuable-sidebar .dropdown-content a')
.on('click', '.issuable-sidebar .dropdown-content a', e => e.preventDefault());
$(document)
.off('click', '.edit-link')
.on('click', '.edit-link', function onClickEdit(e) {
e.preventDefault();
$block = $(this).parents('.block');
$selectbox = $block.find('.selectbox');
const $block = $(this).parents('.block');
const $selectbox = $block.find('.selectbox');
if ($selectbox.is(':visible')) {
$selectbox.hide();
$block.find('.value').show();
......@@ -35,47 +34,18 @@ const PARTICIPANTS_ROW_COUNT = 7;
$selectbox.show();
$block.find('.value').hide();
}
if ($selectbox.is(':visible')) {
return setTimeout(function() {
return $block.find('.dropdown-menu-toggle').trigger('click');
}, 0);
}
});
window.addEventListener('beforeunload', function() {
// collapsed_gutter cookie hides the sidebar
var bpBreakpoint = bp.getBreakpointSize();
if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') {
Cookies.set('collapsed_gutter', true);
}
});
}
IssuableContext.prototype.initParticipants = function() {
$(document).on("click", ".js-participants-more", this.toggleHiddenParticipants);
return $(".js-participants-author").each(function(i) {
if (i >= PARTICIPANTS_ROW_COUNT) {
return $(this).addClass("js-participants-hidden").hide();
if ($selectbox.is(':visible')) {
setTimeout(() => $block.find('.dropdown-menu-toggle').trigger('click'), 0);
}
});
};
IssuableContext.prototype.toggleHiddenParticipants = function(e) {
var currentText, lessText, originalText;
e.preventDefault();
currentText = $(this).text().trim();
lessText = $(this).data("less-text");
originalText = $(this).data("original-text");
if (currentText === originalText) {
$(this).text(lessText);
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
} else {
$(this).text(originalText);
window.addEventListener('beforeunload', () => {
// collapsed_gutter cookie hides the sidebar
const bpBreakpoint = bp.getBreakpointSize();
if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') {
Cookies.set('collapsed_gutter', true);
}
$(".js-participants-hidden").toggle();
};
return IssuableContext;
})();
}).call(window);
});
}
}
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* eslint-disable func-names, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* global GitLab */
/* global Autosave */
import Pikaday from 'pikaday';
import Autosave from './autosave';
import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
(function() {
this.IssuableForm = (function() {
IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
function IssuableForm(form) {
var $issuableDueDate, calendar;
this.form = form;
this.toggleWip = this.toggleWip.bind(this);
this.renderWipExplanation = this.renderWipExplanation.bind(this);
this.resetAutosave = this.resetAutosave.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
new UsersSelect();
new ZenMode();
this.titleField = this.form.find("input[name*='[title]']");
this.descriptionField = this.form.find("textarea[name*='[description]']");
if (!(this.titleField.length && this.descriptionField.length)) {
return;
}
this.initAutosave();
this.form.on("submit", this.handleSubmit);
this.form.on("click", ".btn-cancel", this.resetAutosave);
this.initWip();
$issuableDueDate = $('#issuable-due-date');
if ($issuableDueDate.length) {
calendar = new Pikaday({
field: $issuableDueDate.get(0),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: $issuableDueDate.parent().get(0),
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: function(dateText) {
$issuableDueDate.val(calendar.toString(dateText));
}
});
calendar.setDate(parsePikadayDate($issuableDueDate.val()));
}
export default class IssuableForm {
constructor(form) {
this.form = form;
this.toggleWip = this.toggleWip.bind(this);
this.renderWipExplanation = this.renderWipExplanation.bind(this);
this.resetAutosave = this.resetAutosave.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
new UsersSelect();
new ZenMode();
this.titleField = this.form.find('input[name*="[title]"]');
this.descriptionField = this.form.find('textarea[name*="[description]"]');
if (!(this.titleField.length && this.descriptionField.length)) {
return;
}
IssuableForm.prototype.initAutosave = function() {
new Autosave(this.titleField, [document.location.pathname, document.location.search, "title"]);
return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, "description"]);
};
IssuableForm.prototype.handleSubmit = function() {
return this.resetAutosave();
};
IssuableForm.prototype.resetAutosave = function() {
this.titleField.data("autosave").reset();
return this.descriptionField.data("autosave").reset();
};
IssuableForm.prototype.initWip = function() {
this.$wipExplanation = this.form.find(".js-wip-explanation");
this.$noWipExplanation = this.form.find(".js-no-wip-explanation");
if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) {
return;
}
this.form.on("click", ".js-toggle-wip", this.toggleWip);
this.titleField.on("keyup blur", this.renderWipExplanation);
return this.renderWipExplanation();
};
IssuableForm.prototype.workInProgress = function() {
return this.wipRegex.test(this.titleField.val());
};
IssuableForm.prototype.renderWipExplanation = function() {
if (this.workInProgress()) {
this.$wipExplanation.show();
return this.$noWipExplanation.hide();
} else {
this.$wipExplanation.hide();
return this.$noWipExplanation.show();
}
};
IssuableForm.prototype.toggleWip = function(event) {
event.preventDefault();
if (this.workInProgress()) {
this.removeWip();
} else {
this.addWip();
}
return this.renderWipExplanation();
};
IssuableForm.prototype.removeWip = function() {
return this.titleField.val(this.titleField.val().replace(this.wipRegex, ""));
};
IssuableForm.prototype.addWip = function() {
return this.titleField.val("WIP: " + (this.titleField.val()));
};
return IssuableForm;
})();
}).call(window);
this.initAutosave();
this.form.on('submit', this.handleSubmit);
this.form.on('click', '.btn-cancel', this.resetAutosave);
this.initWip();
const $issuableDueDate = $('#issuable-due-date');
if ($issuableDueDate.length) {
const calendar = new Pikaday({
field: $issuableDueDate.get(0),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: $issuableDueDate.parent().get(0),
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: dateText => $issuableDueDate.val(calendar.toString(dateText)),
});
calendar.setDate(parsePikadayDate($issuableDueDate.val()));
}
}
initAutosave() {
new Autosave(this.titleField, [document.location.pathname, document.location.search, 'title']);
return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, 'description']);
}
handleSubmit() {
return this.resetAutosave();
}
resetAutosave() {
this.titleField.data('autosave').reset();
return this.descriptionField.data('autosave').reset();
}
initWip() {
this.$wipExplanation = this.form.find('.js-wip-explanation');
this.$noWipExplanation = this.form.find('.js-no-wip-explanation');
if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) {
return;
}
this.form.on('click', '.js-toggle-wip', this.toggleWip);
this.titleField.on('keyup blur', this.renderWipExplanation);
return this.renderWipExplanation();
}
workInProgress() {
return this.wipRegex.test(this.titleField.val());
}
renderWipExplanation() {
if (this.workInProgress()) {
this.$wipExplanation.show();
return this.$noWipExplanation.hide();
} else {
this.$wipExplanation.hide();
return this.$noWipExplanation.show();
}
}
toggleWip(event) {
event.preventDefault();
if (this.workInProgress()) {
this.removeWip();
} else {
this.addWip();
}
return this.renderWipExplanation();
}
removeWip() {
return this.titleField.val(this.titleField.val().replace(this.wipRegex, ''));
}
addWip() {
this.titleField.val(`WIP: ${(this.titleField.val())}`);
}
}
/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */
/* global IssuableIndex */
import _ from 'underscore';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
((global) => {
var issuable_created;
issuable_created = false;
global.IssuableIndex = {
init: function(pagePrefix) {
IssuableIndex.initTemplates();
IssuableIndex.initSearch();
IssuableIndex.initBulkUpdate(pagePrefix);
IssuableIndex.initResetFilters();
IssuableIndex.resetIncomingEmailToken();
IssuableIndex.initLabelFilterRemove();
},
initTemplates: function() {
return IssuableIndex.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
},
initSearch: function() {
const $searchInput = $('#issuable_search');
IssuableIndex.initSearchState($searchInput);
// `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
const debouncedExecSearch = _.debounce(IssuableIndex.executeSearch, 1000, false);
$searchInput.off('keyup').on('keyup', debouncedExecSearch);
// ensures existing filters are preserved when manually submitted
$('#issuable_search_form').on('submit', (e) => {
e.preventDefault();
debouncedExecSearch(e);
});
},
initSearchState: function($searchInput) {
const currentSearchVal = $searchInput.val();
IssuableIndex.searchState = {
elem: $searchInput,
current: currentSearchVal
};
IssuableIndex.maybeFocusOnSearch();
},
accessSearchPristine: function(set) {
// store reference to previous value to prevent search on non-mutating keyup
const state = IssuableIndex.searchState;
const currentSearchVal = state.elem.val();
if (set) {
state.current = currentSearchVal;
} else {
return state.current === currentSearchVal;
}
},
maybeFocusOnSearch: function() {
const currentSearchVal = IssuableIndex.searchState.current;
if (currentSearchVal && currentSearchVal !== '') {
const queryLength = currentSearchVal.length;
const $searchInput = IssuableIndex.searchState.elem;
/* The following ensures that the cursor is initially placed at
* the end of search input when focus is applied. It accounts
* for differences in browser implementations of `setSelectionRange`
* and cursor placement for elements in focus.
*/
$searchInput.focus();
if ($searchInput.setSelectionRange) {
$searchInput.setSelectionRange(queryLength, queryLength);
} else {
$searchInput.val(currentSearchVal);
}
}
},
executeSearch: function(e) {
const $search = $('#issuable_search');
const $searchName = $search.attr('name');
const $searchValue = $search.val();
const $filtersForm = $('.js-filter-form');
const $input = $(`input[name='${$searchName}']`, $filtersForm);
const isPristine = IssuableIndex.accessSearchPristine();
if (isPristine) {
return;
}
if (!$input.length) {
$filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`);
} else {
$input.val($searchValue);
}
IssuableIndex.filterResults($filtersForm);
},
initLabelFilterRemove: function() {
return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
var $button;
$button = $(this);
// Remove the label input box
$('input[name="label_name[]"]').filter(function() {
return this.value === $button.data('label');
}).remove();
// Submit the form to get new data
IssuableIndex.filterResults($('.filter-form'));
});
},
filterResults: (function(_this) {
return function(form) {
var formAction, formData, issuesUrl;
formData = form.serializeArray();
formData = formData.filter(function(data) {
return data.value !== '';
});
formData = $.param(formData);
formAction = form.attr('action');
issuesUrl = formAction;
issuesUrl += "" + (formAction.indexOf('?') === -1 ? '?' : '&');
issuesUrl += formData;
return gl.utils.visitUrl(issuesUrl);
};
})(this),
initResetFilters: function() {
$('.reset-filters').on('click', function(e) {
e.preventDefault();
const target = e.target;
const $form = $(target).parents('.js-filter-form');
const baseIssuesUrl = target.href;
$form.attr('action', baseIssuesUrl);
gl.utils.visitUrl(baseIssuesUrl);
export default class IssuableIndex {
constructor(pagePrefix) {
this.initBulkUpdate(pagePrefix);
IssuableIndex.resetIncomingEmailToken();
}
initBulkUpdate(pagePrefix) {
const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
const alreadyInitialized = !!this.bulkUpdateSidebar;
if (userCanBulkUpdate && !alreadyInitialized) {
IssuableBulkUpdateActions.init({
prefixId: pagePrefix,
});
},
initBulkUpdate: function(pagePrefix) {
const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
const alreadyInitialized = !!this.bulkUpdateSidebar;
if (userCanBulkUpdate && !alreadyInitialized) {
IssuableBulkUpdateActions.init({
prefixId: pagePrefix,
});
this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
}
},
resetIncomingEmailToken: function() {
$('.incoming-email-token-reset').on('click', function(e) {
e.preventDefault();
$.ajax({
type: 'PUT',
url: $('.incoming-email-token-reset').attr('href'),
dataType: 'json',
success: function(response) {
$('#issue_email').val(response.new_issue_address).focus();
},
beforeSend: function() {
$('.incoming-email-token-reset').text('resetting...');
},
complete: function() {
$('.incoming-email-token-reset').text('reset it');
}
});
});
this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
}
};
})(window);
}
static resetIncomingEmailToken() {
$('.incoming-email-token-reset').on('click', (e) => {
e.preventDefault();
$.ajax({
type: 'PUT',
url: $('.incoming-email-token-reset').attr('href'),
dataType: 'json',
success(response) {
$('#issue_email').val(response.new_issue_address).focus();
},
beforeSend() {
$('.incoming-email-token-reset').text('resetting...');
},
complete() {
$('.incoming-email-token-reset').text('reset it');
},
});
});
}
}
......@@ -6,7 +6,7 @@ import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
class Issue {
export default class Issue {
constructor() {
if ($('a.btn-close').length) {
this.taskList = new TaskList({
......@@ -147,5 +147,3 @@ class Issue {
});
}
}
export default Issue;
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
(function() {
this.IssueStatusSelect = (function() {
function IssueStatusSelect() {
$('.js-issue-status').each(function(i, el) {
var fieldName;
fieldName = $(el).data("field-name");
return $(el).glDropdown({
selectable: true,
fieldName: fieldName,
toggleLabel: (function(_this) {
return function(selected, el, instance) {
var $item, label;
label = 'Author';
$item = instance.dropdown.find('.is-active');
if ($item.length) {
label = $item.text();
}
return label;
};
})(this),
clicked: function(options) {
return options.e.preventDefault();
},
id: function(obj, el) {
return $(el).data("id");
}
});
});
}
return IssueStatusSelect;
})();
}).call(window);
export default function issueStatusSelect() {
$('.js-issue-status').each((i, el) => {
const fieldName = $(el).data('field-name');
return $(el).glDropdown({
selectable: true,
fieldName,
toggleLabel(selected, element, instance) {
let label = 'Author';
const $item = instance.dropdown.find('.is-active');
if ($item.length) {
label = $item.text();
}
return label;
},
clicked(options) {
return options.e.preventDefault();
},
id(obj, element) {
return $(element).data('id');
},
});
});
}
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */
import _ from 'underscore';
import Cookies from 'js-cookie';
import NewNavSidebar from './new_sidebar';
import ContextualSidebar from './contextual_sidebar';
import initFlyOutNav from './fly_out_nav';
(function() {
......@@ -51,8 +51,8 @@ import initFlyOutNav from './fly_out_nav';
});
$(() => {
const newNavSidebar = new NewNavSidebar();
newNavSidebar.bindEvents();
const contextualSidebar = new ContextualSidebar();
contextualSidebar.bindEvents();
initFlyOutNav();
});
......
......@@ -28,14 +28,10 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {
}
};
export default (el, insertPlaceholder = true) => {
export default (el, stickyTop, insertPlaceholder = true) => {
if (!el) return;
const computedStyle = window.getComputedStyle(el);
if (!/sticky/.test(computedStyle.position)) return;
const stickyTop = parseInt(computedStyle.top, 10);
if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return;
document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), {
passive: true,
......
......@@ -41,7 +41,6 @@ import './behaviors/';
import './activities';
import './admin';
import './aside';
import './autosave';
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
import './commits';
......@@ -50,23 +49,13 @@ import './compare_autocomplete';
import './confirm_danger_modal';
import './copy_as_gfm';
import './copy_to_clipboard';
import './diff';
import './files_comment_button';
import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
import './gl_field_error';
import './gl_field_errors';
import './gl_form';
import './group_avatar';
import './group_label_subscription';
import './groups_select';
import './header';
import './importer_status';
import './issuable_index';
import './issuable_context';
import './issuable_form';
import './issue';
import './issue_status_select';
import initTodoToggle from './header';
import initImporterStatus from './importer_status';
import './labels_select';
import './layout_nav';
import LazyLoader from './lazy_loader';
......@@ -144,6 +133,8 @@ $(function () {
var fitSidebarForSize;
initBreadcrumbs();
initImporterStatus();
initTodoToggle();
// Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/';
......
......@@ -11,8 +11,8 @@ import {
handleLocationHash,
isMetaClick,
} from './lib/utils/common_utils';
import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff';
/* eslint-disable max-len */
// MergeRequestTabs
......@@ -67,6 +67,10 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) {
const mergeRequestTabs = document.querySelector('.js-tabs-affix');
const navbar = document.querySelector('.navbar-gitlab');
const paddingTop = 16;
this.diffsLoaded = false;
this.pipelinesLoaded = false;
this.commitsLoaded = false;
......@@ -76,6 +80,11 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
this.setCurrentAction = this.setCurrentAction.bind(this);
this.tabShown = this.tabShown.bind(this);
this.showTab = this.showTab.bind(this);
this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
if (mergeRequestTabs) {
this.stickyTop += mergeRequestTabs.offsetHeight;
}
if (stubLocation) {
location = stubLocation;
......@@ -278,7 +287,7 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
const $container = $('#diffs');
$container.html(data.html);
initChangesDropdown();
initChangesDropdown(this.stickyTop);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
......@@ -292,7 +301,7 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
}
this.diffsLoaded = true;
new gl.Diff();
new Diff();
this.scrollToElement('#diffs');
$('.diff-file').each((i, el) => {
......
......@@ -5,14 +5,14 @@ default-case, prefer-template, consistent-return, no-alert, no-return-assign,
no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new,
brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow,
newline-per-chained-call, no-useless-escape, class-methods-use-this */
/* global Autosave */
/* global ResolveService */
/* global mrRefreshWidgetUrl */
import $ from 'jquery';
import _ from 'underscore';
import Cookies from 'js-cookie';
import autosize from 'vendor/autosize';
import Autosize from 'autosize';
import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
......@@ -20,12 +20,12 @@ import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
import './autosave';
import Autosave from './autosave';
import TaskList from './task_list';
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
window.autosize = autosize;
window.autosize = Autosize;
function normalizeNewlines(str) {
return str.replace(/\r\n/g, '\n');
......@@ -1280,10 +1280,12 @@ export default class Notes {
* Get data from Form attributes to use for saving/submitting comment.
*/
getFormData($form) {
const content = $form.find('.js-note-text').val();
return {
formData: $form.serialize(),
formContent: _.escape($form.find('.js-note-text').val()),
formContent: _.escape(content),
formAction: $form.attr('action'),
formContentOriginal: content,
};
}
......@@ -1415,7 +1417,7 @@ export default class Notes {
const isMainForm = $form.hasClass('js-main-target-form');
const isDiscussionForm = $form.hasClass('js-discussion-note-form');
const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button');
const { formData, formContent, formAction } = this.getFormData($form);
const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form);
let noteUniqueId;
let systemNoteUniqueId;
let hasQuickActions = false;
......@@ -1574,7 +1576,7 @@ export default class Notes {
$form = $notesContainer.parent().find('form');
}
$form.find('.js-note-text').val(formContent);
$form.find('.js-note-text').val(formContentOriginal);
this.reenableTargetFormSubmitButton(e);
this.addNoteError($form);
});
......
<script>
/* global Autosave */
import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore';
import autosize from 'vendor/autosize';
import Autosize from 'autosize';
import Flash from '../../flash';
import '../../autosave';
import Autosave from '../../autosave';
import TaskList from '../../task_list';
import * as constants from '../constants';
import eventHub from '../event_hub';
......@@ -220,7 +219,7 @@
},
resizeTextarea() {
this.$nextTick(() => {
autosize.update(this.$refs.textarea);
Autosize.update(this.$refs.textarea);
});
},
},
......
......@@ -9,8 +9,8 @@
import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
import issueNoteEditedText from './issue_note_edited_text.vue';
import issueNoteForm from './issue_note_form.vue';
import placeholderNote from './issue_placeholder_note.vue';
import placeholderSystemNote from './issue_placeholder_system_note.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave';
export default {
......
......@@ -5,10 +5,10 @@
import * as constants from '../constants';
import issueNote from './issue_note.vue';
import issueDiscussion from './issue_discussion.vue';
import issueSystemNote from './issue_system_note.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue';
import issueCommentForm from './issue_comment_form.vue';
import placeholderNote from './issue_placeholder_note.vue';
import placeholderSystemNote from './issue_placeholder_system_note.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
......@@ -37,7 +37,7 @@
components: {
issueNote,
issueDiscussion,
issueSystemNote,
systemNote,
issueCommentForm,
loadingIcon,
placeholderNote,
......@@ -68,7 +68,7 @@
}
return placeholderNote;
} else if (note.individual_note) {
return note.notes[0].system ? issueSystemNote : issueNote;
return note.notes[0].system ? systemNote : issueNote;
}
return issueDiscussion;
......
/* globals Autosave */
import '../../autosave';
import Autosave from '../../autosave';
export default {
methods: {
......
......@@ -12,6 +12,15 @@
type: Object,
required: true,
},
// Can be rendered in 3 different places, with some visual differences
// Accepts root | child
// `root` -> main view
// `child` -> rendered inside MR or Commit View
viewType: {
type: String,
required: false,
default: 'root',
},
},
components: {
tablePagination,
......@@ -187,7 +196,7 @@
:empty-state-svg-path="emptyStateSvgPath"
/>
<error-state
<error-state
v-if="shouldRenderErrorState"
:error-state-svg-path="errorStateSvgPath"
/>
......@@ -206,6 +215,7 @@
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsPath"
:view-type="viewType"
/>
</div>
......
......@@ -21,6 +21,10 @@
type: String,
required: true,
},
viewType: {
type: String,
required: true,
},
},
components: {
pipelinesTableRowComponent,
......@@ -59,6 +63,7 @@
:pipeline="model"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
/>
</div>
</template>
......@@ -29,6 +29,10 @@ export default {
type: String,
required: true,
},
viewType: {
type: String,
required: true,
},
},
components: {
asyncButtonComponent,
......@@ -203,9 +207,13 @@ export default {
displayPipelineActions() {
return this.pipeline.flags.retryable ||
this.pipeline.flags.cancelable ||
this.pipeline.details.manual_actions.length ||
this.pipeline.details.artifacts.length;
this.pipeline.flags.cancelable ||
this.pipeline.details.manual_actions.length ||
this.pipeline.details.artifacts.length;
},
isChildView() {
return this.viewType === 'child';
},
},
};
......@@ -218,7 +226,10 @@ export default {
Status
</div>
<div class="table-mobile-content">
<ci-badge :status="pipelineStatus"/>
<ci-badge
:status="pipelineStatus"
:show-text="!isChildView"
/>
</div>
</div>
......@@ -240,7 +251,9 @@ export default {
:commit-url="commitUrl"
:short-sha="commitShortSha"
:title="commitTitle"
:author="commitAuthor"/>
:author="commitAuthor"
:show-branch="!isChildView"
/>
</div>
</div>
......
......@@ -2,13 +2,15 @@
import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
(function() {
this.ProjectSelect = (function() {
(function () {
this.ProjectSelect = (function () {
function ProjectSelect() {
$('.ajax-project-select').each(function(i, select) {
var placeholder;
const simpleFilter = $(select).data('simple-filter') || false;
this.groupId = $(select).data('group-id');
this.includeGroups = $(select).data('include-groups');
this.allProjects = $(select).data('all-projects') || false;
this.orderBy = $(select).data('order-by') || 'id';
this.withIssuesEnabled = $(select).data('with-issues-enabled');
this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
......@@ -21,10 +23,10 @@ import ProjectSelectComboButton from './project_select_combo_button';
$(select).select2({
placeholder: placeholder,
minimumInputLength: 0,
query: (function(_this) {
return function(query) {
query: (function (_this) {
return function (query) {
var finalCallback, projectsCallback;
finalCallback = function(projects) {
finalCallback = function (projects) {
var data;
data = {
results: projects
......@@ -32,9 +34,9 @@ import ProjectSelectComboButton from './project_select_combo_button';
return query.callback(data);
};
if (_this.includeGroups) {
projectsCallback = function(projects) {
projectsCallback = function (projects) {
var groupsCallback;
groupsCallback = function(groups) {
groupsCallback = function (groups) {
var data;
data = groups.concat(projects);
return finalCallback(data);
......@@ -50,23 +52,25 @@ import ProjectSelectComboButton from './project_select_combo_button';
return Api.projects(query.term, {
order_by: _this.orderBy,
with_issues_enabled: _this.withIssuesEnabled,
with_merge_requests_enabled: _this.withMergeRequestsEnabled
with_merge_requests_enabled: _this.withMergeRequestsEnabled,
membership: !_this.allProjects,
}, projectsCallback);
}
};
})(this),
id: function(project) {
if (simpleFilter) return project.id;
return JSON.stringify({
name: project.name,
url: project.web_url,
});
},
text: function(project) {
text: function (project) {
return project.name_with_namespace || project.name;
},
dropdownCssClass: "ajax-project-dropdown"
});
if (simpleFilter) return select;
return new ProjectSelectComboButton(select);
});
}
......
......@@ -29,11 +29,9 @@ export const fetchList = ({ commit }, { repo, page }) => {
});
};
export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath)
.then(res => res.json());
export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath);
export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath)
.then(res => res.json());
export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath);
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
<script>
import { mapState, mapActions } from 'vuex';
import flash, { hideFlash } from '../../flash';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {
loadingIcon,
},
data() {
return {
branchName: '',
loading: false,
};
},
computed: {
...mapState([
'currentBranch',
]),
btnDisabled() {
return this.loading || this.branchName === '';
},
},
methods: {
...mapActions([
'createNewBranch',
]),
toggleDropdown() {
this.$dropdown.dropdown('toggle');
},
submitNewBranch() {
// need to query as the element is appended outside of Vue
const flashEl = this.$refs.flashContainer.querySelector('.flash-alert');
this.loading = true;
if (flashEl) {
hideFlash(flashEl, false);
}
this.createNewBranch(this.branchName)
.then(() => {
this.loading = false;
this.branchName = '';
if (this.dropdownText) {
this.dropdownText.textContent = this.currentBranch;
}
this.toggleDropdown();
})
.catch(res => res.json().then((data) => {
this.loading = false;
flash(data.message, 'alert', this.$el);
}));
},
},
created() {
// Dropdown is outside of Vue instance & is controlled by Bootstrap
this.$dropdown = $('.git-revision-dropdown');
// text element is outside Vue app
this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
},
};
</script>
<template>
<div>
<div
class="flash-container"
ref="flashContainer"
>
</div>
<p>
Create from:
<code>{{ currentBranch }}</code>
</p>
<input
class="form-control js-new-branch-name"
type="text"
placeholder="Name new branch"
v-model="branchName"
@keyup.enter.stop.prevent="submitNewBranch"
/>
<div class="prepend-top-default clearfix">
<button
type="button"
class="btn btn-primary pull-left"
:disabled="btnDisabled"
@click.stop.prevent="submitNewBranch"
>
<loading-icon
v-if="loading"
:inline="true"
/>
<span>Create</span>
</button>
<button
type="button"
class="btn btn-default pull-right"
@click.stop.prevent="toggleDropdown"
>
Cancel
</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import newModal from './modal.vue';
import upload from './upload.vue';
export default {
components: {
newModal,
upload,
},
data() {
return {
openModal: false,
modalType: '',
};
},
computed: {
...mapState([
'path',
]),
},
methods: {
createNewItem(type) {
this.modalType = type;
this.toggleModalOpen();
},
toggleModalOpen() {
this.openModal = !this.openModal;
},
},
};
</script>
<template>
<div>
<ul class="breadcrumb repo-breadcrumb">
<li class="dropdown">
<button
type="button"
class="btn btn-default dropdown-toggle add-to-tree"
data-toggle="dropdown"
aria-label="Create new file or directory"
>
<i
class="fa fa-plus"
aria-hidden="true"
>
</i>
</button>
<ul class="dropdown-menu">
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('blob')"
>
{{ __('New file') }}
</a>
</li>
<li>
<upload
:path="path"
/>
</li>
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('tree')"
>
{{ __('New directory') }}
</a>
</li>
</ul>
</li>
</ul>
<new-modal
v-if="openModal"
:type="modalType"
:path="path"
@toggle="toggleModalOpen"
/>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import { __ } from '../../../locale';
import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
export default {
props: {
type: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
},
data() {
return {
entryName: this.path !== '' ? `${this.path}/` : '',
};
},
components: {
popupDialog,
},
methods: {
...mapActions([
'createTempEntry',
]),
createEntryInStore() {
this.createTempEntry({
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
type: this.type,
});
this.toggleModalOpen();
},
toggleModalOpen() {
this.$emit('toggle');
},
},
computed: {
modalTitle() {
if (this.type === 'tree') {
return __('Create new directory');
}
return __('Create new file');
},
buttonLabel() {
if (this.type === 'tree') {
return __('Create directory');
}
return __('Create file');
},
formLabelName() {
if (this.type === 'tree') {
return __('Directory name');
}
return __('File name');
},
},
mounted() {
this.$refs.fieldName.focus();
},
};
</script>
<template>
<popup-dialog
:title="modalTitle"
:primary-button-label="buttonLabel"
kind="success"
@toggle="toggleModalOpen"
@submit="createEntryInStore"
>
<form
class="form-horizontal"
slot="body"
@submit.prevent="createEntryInStore"
>
<fieldset class="form-group append-bottom-0">
<label class="label-light col-sm-3">
{{ formLabelName }}
</label>
<div class="col-sm-9">
<input
type="text"
class="form-control"
v-model="entryName"
ref="fieldName"
/>
</div>
</fieldset>
</form>
</popup-dialog>
</template>
<script>
import { mapActions } from 'vuex';
export default {
props: {
path: {
type: String,
required: true,
},
},
methods: {
...mapActions([
'createTempEntry',
]),
createFile(target, file, isText) {
const { name } = file;
let { result } = target;
if (!isText) {
result = result.split('base64,')[1];
}
this.createTempEntry({
name,
type: 'blob',
content: result,
base64: !isText,
});
},
readFile(file) {
const reader = new FileReader();
const isText = file.type.match(/text.*/) !== null;
reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
if (isText) {
reader.readAsText(file);
} else {
reader.readAsDataURL(file);
}
},
openFile() {
Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
},
},
mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile);
},
beforeDestroy() {
this.$refs.fileUpload.removeEventListener('change', this.openFile);
},
};
</script>
<template>
<label
role="button"
class="menu-item"
>
{{ __('Upload file') }}
<input
id="file-upload"
type="file"
class="hidden"
ref="fileUpload"
/>
</label>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
import RepoSidebar from './repo_sidebar.vue';
import RepoCommitSection from './repo_commit_section.vue';
import RepoTabs from './repo_tabs.vue';
import RepoFileButtons from './repo_file_buttons.vue';
import RepoPreview from './repo_preview.vue';
import RepoMixin from '../mixins/repo_mixin';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
import repoEditor from './repo_editor.vue';
export default {
data() {
return Store;
computed: {
...mapState([
'currentBlobView',
]),
...mapGetters([
'isCollapsed',
'changedFiles',
]),
},
mixins: [RepoMixin],
components: {
RepoSidebar,
RepoTabs,
RepoFileButtons,
'repo-editor': MonacoLoaderHelper.repoEditorLoader,
repoEditor,
RepoCommitSection,
PopupDialog,
RepoPreview,
},
mounted() {
Helper.getContent().catch(Helper.loadingError);
},
methods: {
toggleDialogOpen(toggle) {
this.dialog.open = toggle;
},
dialogSubmitted(status) {
this.toggleDialogOpen(false);
this.dialog.status = status;
},
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = (e) => {
if (!this.changedFiles.length) return undefined;
toggleBlobView: Store.toggleBlobView,
Object.assign(e, {
returnValue,
});
return returnValue;
};
},
};
</script>
<template>
<div class="repository-view">
<div class="tree-content-holder" :class="{'tree-content-holder-mini' : isMini}">
<div class="tree-content-holder" :class="{'tree-content-holder-mini' : isCollapsed}">
<repo-sidebar/>
<div v-if="isMini"
class="panel-right"
:class="{'edit-mode': editMode}">
<div
v-if="isCollapsed"
class="panel-right"
>
<repo-tabs/>
<component
:is="currentBlobView"
class="blob-viewer-container"/>
/>
<repo-file-buttons/>
</div>
</div>
<repo-commit-section/>
<popup-dialog
v-show="dialog.open"
:primary-button-label="__('Discard changes')"
kind="warning"
:title="__('Are you sure?')"
:text="__('Are you sure you want to discard your changes?')"
@toggle="toggleDialogOpen"
@submit="dialogSubmitted"
/>
<repo-commit-section v-if="changedFiles.length" />
</div>
</template>
<script>
import Flash from '../../flash';
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
import Service from '../services/repo_service';
import { mapGetters, mapState, mapActions } from 'vuex';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import { visitUrl } from '../../lib/utils/url_utility';
import { n__ } from '../../locale';
export default {
mixins: [RepoMixin],
data() {
return Store;
},
components: {
PopupDialog,
},
data() {
return {
showNewBranchDialog: false,
submitCommitsLoading: false,
startNewMR: false,
commitMessage: '',
};
},
computed: {
showCommitable() {
return this.isCommitable && this.changedFiles.length;
},
branchPaths() {
return this.changedFiles.map(f => f.path);
},
cantCommitYet() {
...mapState([
'currentBranch',
]),
...mapGetters([
'changedFiles',
]),
commitButtonDisabled() {
return !this.commitMessage || this.submitCommitsLoading;
},
filePluralize() {
return this.changedFiles.length > 1 ? 'files' : 'file';
commitButtonText() {
return n__('Commit %d file', 'Commit %d files', this.changedFiles.length);
},
},
methods: {
commitToNewBranch(status) {
if (status) {
this.showNewBranchDialog = false;
this.tryCommit(null, true, true);
} else {
// reset the state
}
},
...mapActions([
'checkCommitStatus',
'commitChanges',
'getTreeData',
]),
makeCommit(newBranch = false) {
const createNewBranch = newBranch || this.startNewMR;
makeCommit(newBranch) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({
action: 'update',
file_path: f.path,
content: f.newContent,
}));
const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch;
const payload = {
branch,
commit_message: commitMessage,
actions,
branch: createNewBranch ? `${this.currentBranch}-${new Date().getTime().toString()}` : this.currentBranch,
commit_message: this.commitMessage,
actions: this.changedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.content,
encoding: f.base64 ? 'base64' : 'text',
})),
start_branch: createNewBranch ? this.currentBranch : undefined,
};
if (newBranch) {
payload.start_branch = this.currentBranch;
}
this.showNewBranchDialog = false;
this.submitCommitsLoading = true;
Service.commitFiles(payload)
this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => {
this.resetCommitState();
if (this.startNewMR) {
this.redirectToNewMr(branch);
} else {
this.redirectToBranch(branch);
}
this.submitCommitsLoading = false;
this.getTreeData();
})
.catch(() => {
Flash('An error occurred while committing your changes');
this.submitCommitsLoading = false;
});
},
tryCommit() {
this.submitCommitsLoading = true;
tryCommit(e, skipBranchCheck = false, newBranch = false) {
if (skipBranchCheck) {
this.makeCommit(newBranch);
} else {
Store.setBranchHash()
.then(() => {
if (Store.branchChanged) {
Store.showNewBranchDialog = true;
return;
}
this.makeCommit(newBranch);
})
.catch(() => {
Flash('An error occurred while committing your changes');
});
}
},
redirectToNewMr(branch) {
visitUrl(this.newMrTemplateUrl.replace('{{source_branch}}', branch));
},
redirectToBranch(branch) {
visitUrl(this.customBranchURL.replace('{{branch}}', branch));
},
resetCommitState() {
this.submitCommitsLoading = false;
this.openedFiles = this.openedFiles.map((file) => {
const f = file;
f.changed = false;
return f;
});
this.changedFiles = [];
this.commitMessage = '';
this.editMode = false;
window.scrollTo(0, 0);
this.checkCommitStatus()
.then((branchChanged) => {
if (branchChanged) {
this.showNewBranchDialog = true;
} else {
this.makeCommit();
}
})
.catch(() => {
this.submitCommitsLoading = false;
});
},
},
};
</script>
<template>
<div
v-if="showCommitable"
id="commit-area">
<div id="commit-area">
<popup-dialog
v-if="showNewBranchDialog"
:primary-button-label="__('Create new branch')"
kind="primary"
:title="__('Branch has changed')"
:text="__('This branch has changed since you started editing. Would you like to create a new branch?')"
@submit="commitToNewBranch"
@toggle="showNewBranchDialog = false"
@submit="makeCommit(true)"
/>
<form
class="form-horizontal"
@submit.prevent="tryCommit">
@submit.prevent="tryCommit()">
<fieldset>
<div class="form-group">
<label class="col-md-4 control-label staged-files">
......@@ -142,10 +103,10 @@ export default {
<div class="col-md-6">
<ul class="list-unstyled changed-files">
<li
v-for="branchPath in branchPaths"
:key="branchPath">
v-for="(file, index) in changedFiles"
:key="index">
<span class="help-block">
{{branchPath}}
{{ file.path }}
</span>
</li>
</ul>
......@@ -180,9 +141,8 @@ export default {
</div>
<div class="col-md-offset-4 col-md-6">
<button
ref="submitCommit"
type="submit"
:disabled="cantCommitYet"
:disabled="commitButtonDisabled"
class="btn btn-success">
<i
v-if="submitCommitsLoading"
......@@ -191,7 +151,7 @@ export default {
aria-label="loading">
</i>
<span class="commit-summary">
Commit {{changedFiles.length}} {{filePluralize}}
{{ commitButtonText }}
</span>
</button>
</div>
......
<script>
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
import { mapGetters, mapActions, mapState } from 'vuex';
import popupDialog from '../../vue_shared/components/popup_dialog.vue';
export default {
data() {
return Store;
components: {
popupDialog,
},
mixins: [RepoMixin],
computed: {
...mapState([
'editMode',
'discardPopupOpen',
]),
...mapGetters([
'canEditFile',
]),
buttonLabel() {
return this.editMode ? this.__('Cancel edit') : this.__('Edit');
},
showButton() {
return this.isCommitable &&
!this.activeFile.render_error &&
!this.binary &&
this.openedFiles.length;
},
},
methods: {
editCancelClicked() {
if (this.changedFiles.length) {
this.dialog.open = true;
return;
}
this.editMode = !this.editMode;
Store.toggleBlobView();
},
...mapActions([
'toggleEditMode',
'closeDiscardPopup',
]),
},
};
</script>
<template>
<button
v-if="showButton"
class="btn btn-default"
type="button"
@click.prevent="editCancelClicked">
<i
v-if="!editMode"
class="fa fa-pencil"
aria-hidden="true">
</i>
<span>
{{buttonLabel}}
</span>
</button>
<div class="editable-mode">
<button
v-if="canEditFile"
class="btn btn-default"
type="button"
@click.prevent="toggleEditMode()">
<i
v-if="!editMode"
class="fa fa-pencil"
aria-hidden="true">
</i>
<span>
{{buttonLabel}}
</span>
</button>
<popup-dialog
v-if="discardPopupOpen"
class="text-left"
:primary-button-label="__('Discard changes')"
kind="warning"
:title="__('Are you sure?')"
:text="__('Are you sure you want to discard your changes?')"
@toggle="closeDiscardPopup"
@submit="toggleEditMode(true)"
/>
</div>
</template>
<script>
/* global monaco */
import Store from '../stores/repo_store';
import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper';
const RepoEditor = {
data() {
return Store;
},
import { mapGetters, mapActions } from 'vuex';
import flash from '../../flash';
import monacoLoader from '../monaco_loader';
export default {
destroyed() {
if (Helper.monacoInstance) {
Helper.monacoInstance.destroy();
if (this.monacoInstance) {
this.monacoInstance.destroy();
}
},
mounted() {
Service.getRaw(this.activeFile.raw_path)
.then((rawResponse) => {
Store.blobRaw = rawResponse.data;
Store.activeFile.plain = rawResponse.data;
const monacoInstance = Helper.monaco.editor.create(this.$el, {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
});
if (this.monaco) {
this.initMonaco();
} else {
monacoLoader(['vs/editor/editor.main'], () => {
this.monaco = monaco;
this.initMonaco();
});
}
},
methods: {
...mapActions([
'getRawFileData',
'changeFileContent',
]),
initMonaco() {
if (this.monacoInstance) {
this.monacoInstance.setModel(null);
}
Helper.monacoInstance = monacoInstance;
this.getRawFileData(this.activeFile)
.then(() => {
if (!this.monacoInstance) {
this.monacoInstance = this.monaco.editor.create(this.$el, {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
});
this.addMonacoEvents();
this.languages = this.monaco.languages.getLanguages();
this.setupEditor();
})
.catch(Helper.loadingError);
},
this.addMonacoEvents();
}
methods: {
this.setupEditor();
})
.catch(() => flash('Error setting up monaco. Please try again.'));
},
setupEditor() {
this.showHide();
if (!this.activeFile) return;
const content = this.activeFile.content !== '' ? this.activeFile.content : this.activeFile.raw;
Helper.setMonacoModelFromLanguage();
},
const foundLang = this.languages.find(lang =>
lang.extensions && lang.extensions.indexOf(this.activeFileExtension) === 0,
);
const newModel = this.monaco.editor.createModel(
content, foundLang ? foundLang.id : 'plaintext',
);
showHide() {
if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
this.$el.style.display = 'none';
} else {
this.$el.style.display = 'inline-block';
}
this.monacoInstance.setModel(newModel);
},
addMonacoEvents() {
Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
},
onMonacoEditorKeysPressed() {
Store.setActiveFileContents(Helper.monacoInstance.getValue());
},
onMonacoEditorMouseUp(e) {
if (!e.target.position) return;
const lineNumber = e.target.position.lineNumber;
if (e.target.element.classList.contains('line-numbers')) {
location.hash = `L${lineNumber}`;
Store.setActiveLine(lineNumber);
}
this.monacoInstance.onKeyUp(() => {
this.changeFileContent({
file: this.activeFile,
content: this.monacoInstance.getValue(),
});
});
},
},
watch: {
dialog: {
handler(obj) {
const newObj = obj;
if (newObj.status) {
newObj.status = false;
this.openedFiles = this.openedFiles.map((file) => {
const f = file;
if (f.active) {
this.blobRaw = f.plain;
}
f.changed = false;
delete f.newContent;
return f;
});
this.editMode = false;
Store.toggleBlobView();
}
},
deep: true,
},
blobRaw() {
if (Helper.monacoInstance) {
this.setupEditor();
}
},
activeLine() {
if (Helper.monacoInstance) {
Helper.monacoInstance.setPosition({
lineNumber: this.activeLine,
column: 1,
});
activeFile(oldVal, newVal) {
if (newVal && !newVal.active) {
this.initMonaco();
}
},
},
computed: {
...mapGetters([
'activeFile',
'activeFileExtension',
]),
shouldHideEditor() {
return !this.openedFiles.length || (this.binary && !this.activeFile.raw);
return this.activeFile.binary && !this.activeFile.raw;
},
},
};
export default RepoEditor;
</script>
<template>
<div id="ide" v-if='!shouldHideEditor'></div>
<div
id="ide"
v-if='!shouldHideEditor'
class="blob-viewer-container blob-editor-container"
>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
import eventHub from '../event_hub';
import repoMixin from '../mixins/repo_mixin';
export default {
mixins: [
repoMixin,
timeAgoMixin,
],
props: {
......@@ -15,13 +13,15 @@
},
},
computed: {
...mapGetters([
'isCollapsed',
]),
fileIcon() {
const classObj = {
return {
'fa-spinner fa-spin': this.file.loading,
[this.file.icon]: !this.file.loading,
'fa-folder-open': !this.file.loading && this.file.opened,
};
return classObj;
},
levelIndentation() {
return {
......@@ -33,9 +33,9 @@
},
},
methods: {
linkClicked(file) {
eventHub.$emit('fileNameClicked', file);
},
...mapActions([
'clickedTreeRow',
]),
},
};
</script>
......@@ -43,7 +43,7 @@
<template>
<tr
class="file"
@click.prevent="linkClicked(file)">
@click.prevent="clickedTreeRow(file)">
<td>
<i
class="fa fa-fw file-icon"
......@@ -71,7 +71,7 @@
</template>
</td>
<template v-if="!isMini">
<template v-if="!isCollapsed">
<td class="hidden-sm hidden-xs">
<a
@click.stop
......
<script>
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import RepoMixin from '../mixins/repo_mixin';
const RepoFileButtons = {
data() {
return Store;
},
mixins: [RepoMixin],
import { mapGetters } from 'vuex';
export default {
computed: {
rawDownloadButtonLabel() {
return this.binary ? 'Download' : 'Raw';
...mapGetters([
'activeFile',
]),
showButtons() {
return this.activeFile.rawPath ||
this.activeFile.blamePath ||
this.activeFile.commitsPath ||
this.activeFile.permalink;
},
canPreview() {
return Helper.isRenderable();
rawDownloadButtonLabel() {
return this.activeFile.binary ? 'Download' : 'Raw';
},
},
methods: {
rawPreviewToggle: Store.toggleRawPreview,
},
};
export default RepoFileButtons;
</script>
<template>
<div id="repo-file-buttons">
<div
v-if="showButtons"
class="repo-file-buttons"
>
<a
:href="activeFile.raw_path"
:href="activeFile.rawPath"
target="_blank"
class="btn btn-default raw"
rel="noopener noreferrer">
{{rawDownloadButtonLabel}}
{{ rawDownloadButtonLabel }}
</a>
<div
......@@ -44,12 +37,12 @@ export default RepoFileButtons;
role="group"
aria-label="File actions">
<a
:href="activeFile.blame_path"
:href="activeFile.blamePath"
class="btn btn-default blame">
Blame
</a>
<a
:href="activeFile.commits_path"
:href="activeFile.commitsPath"
class="btn btn-default history">
History
</a>
......@@ -59,13 +52,5 @@ export default RepoFileButtons;
Permalink
</a>
</div>
<a
v-if="canPreview"
href="#"
@click.prevent="rawPreviewToggle"
class="btn btn-default preview">
{{activeFileLabel}}
</a>
</div>
</template>
<script>
import repoMixin from '../mixins/repo_mixin';
import { mapGetters } from 'vuex';
export default {
mixins: [
repoMixin,
],
computed: {
...mapGetters([
'isCollapsed',
]),
},
methods: {
lineOfCode(n) {
return `skeleton-line-${n}`;
......@@ -28,7 +30,7 @@
</div>
</div>
</td>
<template v-if="!isMini">
<template v-if="!isCollapsed">
<td
class="hidden-sm hidden-xs">
<div class="animation-container">
......
<script>
import eventHub from '../event_hub';
import repoMixin from '../mixins/repo_mixin';
import { mapGetters, mapState, mapActions } from 'vuex';
export default {
mixins: [
repoMixin,
],
props: {
prevUrl: {
type: String,
required: true,
},
},
computed: {
...mapState([
'parentTreeUrl',
]),
...mapGetters([
'isCollapsed',
]),
colSpanCondition() {
return this.isMini ? undefined : 3;
return this.isCollapsed ? undefined : 3;
},
},
methods: {
linkClicked(file) {
eventHub.$emit('goToPreviousDirectoryClicked', file);
},
...mapActions([
'getTreeData',
]),
},
};
</script>
......@@ -30,9 +26,9 @@
<td
:colspan="colSpanCondition"
class="table-cell"
@click.prevent="linkClicked(prevUrl)"
@click.prevent="getTreeData({ endpoint: parentTreeUrl })"
>
<a :href="prevUrl">...</a>
<a :href="parentTreeUrl">...</a>
</td>
</tr>
</template>
<script>
/* global LineHighlighter */
import Store from '../stores/repo_store';
import { mapGetters } from 'vuex';
export default {
data() {
return Store;
},
computed: {
html() {
return this.activeFile.html;
...mapGetters([
'activeFile',
]),
renderErrorTooLarge() {
return this.activeFile.renderError === 'too_large';
},
},
methods: {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
highlightLine() {
if (Store.activeLine > -1) {
this.lineHighlighter.highlightHash(`#L${Store.activeLine}`);
}
},
},
mounted() {
this.highlightFile();
......@@ -29,38 +23,39 @@ export default {
scrollFileHolder: true,
});
},
watch: {
html() {
this.$nextTick(() => {
this.highlightFile();
this.highlightLine();
});
},
activeLine() {
this.highlightLine();
},
updated() {
this.$nextTick(() => {
this.highlightFile();
});
},
};
</script>
<template>
<div>
<div class="blob-viewer-container">
<div
v-if="!activeFile.render_error"
v-if="!activeFile.renderError"
v-html="activeFile.html">
</div>
<div
v-else-if="activeFile.tooLarge"
v-else-if="activeFile.tempFile"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed for this temporary file.
</p>
</div>
<div
v-else-if="renderErrorTooLarge"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.
The source could not be displayed because it is too large. You can <a :href="activeFile.rawPath" download>download</a> it instead.
</p>
</div>
<div
v-else
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.raw_path">download</a> it instead.
The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.rawPath" download>download</a> it instead.
</p>
</div>
</div>
......
<script>
import _ from 'underscore';
import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper';
import Store from '../stores/repo_store';
import eventHub from '../event_hub';
import { mapState, mapGetters, mapActions } from 'vuex';
import RepoPreviousDirectory from './repo_prev_directory.vue';
import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue';
import RepoMixin from '../mixins/repo_mixin';
export default {
mixins: [RepoMixin],
components: {
'repo-previous-directory': RepoPreviousDirectory,
'repo-file': RepoFile,
'repo-loading-file': RepoLoadingFile,
},
created() {
window.addEventListener('popstate', this.checkHistory);
window.addEventListener('popstate', this.popHistoryState);
},
destroyed() {
eventHub.$off('fileNameClicked', this.fileClicked);
eventHub.$off('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
window.removeEventListener('popstate', this.checkHistory);
window.removeEventListener('popstate', this.popHistoryState);
},
mounted() {
eventHub.$on('fileNameClicked', this.fileClicked);
eventHub.$on('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
},
data() {
return Store;
this.getTreeData();
},
computed: {
flattendFiles() {
const mapFiles = arr => (!arr.files.length ? [] : _.map(arr.files, a => [a, mapFiles(a)]));
return _.chain(this.files)
.map(arr => [arr, mapFiles(arr)])
.flatten()
.value();
},
...mapState([
'loading',
'isRoot',
]),
...mapState({
projectName(state) {
return state.project.name;
},
}),
...mapGetters([
'treeList',
'isCollapsed',
]),
},
methods: {
checkHistory() {
let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1);
if (!selectedFile) {
// Maybe it is not in the current tree but in the opened tabs
selectedFile = Helper.getFileFromPath(location.pathname);
}
let lineNumber = null;
if (location.hash.indexOf('#L') > -1) lineNumber = Number(location.hash.substr(2));
if (selectedFile) {
if (selectedFile.url !== this.activeFile.url) {
this.fileClicked(selectedFile, lineNumber);
} else {
Store.setActiveLine(lineNumber);
}
} else {
// Not opened at all lets open new tab
this.fileClicked({
url: location.href,
}, lineNumber);
}
},
fileClicked(clickedFile, lineNumber) {
const file = clickedFile;
if (file.loading) return;
if (file.type === 'tree' && file.opened) {
Helper.setDirectoryToClosed(file);
Store.setActiveLine(lineNumber);
} else if (file.type === 'submodule') {
file.loading = true;
gl.utils.visitUrl(file.url);
} else {
const openFile = Helper.getFileFromPath(file.url);
if (openFile) {
Store.setActiveFiles(openFile);
Store.setActiveLine(lineNumber);
} else {
file.loading = true;
Service.url = file.url;
Helper.getContent(file)
.then(() => {
file.loading = false;
Helper.scrollTabsRight();
Store.setActiveLine(lineNumber);
})
.catch(Helper.loadingError);
}
}
},
goToPreviousDirectoryClicked(prevURL) {
Service.url = prevURL;
Helper.getContent(null, true)
.then(() => Helper.scrollTabsRight())
.catch(Helper.loadingError);
},
...mapActions([
'getTreeData',
'popHistoryState',
]),
},
};
</script>
<template>
<div id="sidebar" :class="{'sidebar-mini' : isMini}">
<div id="sidebar" :class="{'sidebar-mini' : isCollapsed}">
<table class="table">
<thead>
<tr>
<th
v-if="isMini"
v-if="isCollapsed"
class="repo-file-options title"
>
<strong class="clgray">
......@@ -136,17 +71,16 @@ export default {
</thead>
<tbody>
<repo-previous-directory
v-if="!isRoot && !loading.tree"
:prev-url="prevURL"
v-if="!isRoot && treeList.length"
/>
<repo-loading-file
v-if="!flattendFiles.length && loading.tree"
v-if="!treeList.length && loading"
v-for="n in 5"
:key="n"
/>
<repo-file
v-for="file in flattendFiles"
:key="file.id"
v-for="(file, index) in treeList"
:key="index"
:file="file"
/>
</tbody>
......
<script>
import Store from '../stores/repo_store';
import { mapActions } from 'vuex';
const RepoTab = {
export default {
props: {
tab: {
type: Object,
......@@ -11,44 +11,38 @@ const RepoTab = {
computed: {
closeLabel() {
if (this.tab.changed) {
if (this.tab.changed || this.tab.tempFile) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
changedClass() {
const tabChangedObj = {
'fa-times close-icon': !this.tab.changed,
'fa-circle unsaved-icon': this.tab.changed,
'fa-times close-icon': !this.tab.changed && !this.tab.tempFile,
'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile,
};
return tabChangedObj;
},
},
methods: {
tabClicked(file) {
Store.setActiveFiles(file);
},
closeTab(file) {
if (file.changed) return;
Store.removeFromOpenedFiles(file);
},
...mapActions([
'setFileActive',
'closeFile',
]),
},
};
export default RepoTab;
</script>
<template>
<li
:class="{ active : tab.active }"
@click="tabClicked(tab)"
@click="setFileActive(tab)"
>
<button
type="button"
class="close-btn"
@click.stop.prevent="closeTab(tab)"
@click.stop.prevent="closeFile({ file: tab })"
:aria-label="closeLabel">
<i
class="fa"
......@@ -61,7 +55,7 @@ export default RepoTab;
href="#"
class="repo-tab"
:title="tab.url"
@click.prevent="tabClicked(tab)">
@click.prevent.stop="setFileActive(tab)">
{{tab.name}}
</a>
</li>
......
<script>
import Store from '../stores/repo_store';
import { mapState } from 'vuex';
import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin';
export default {
mixins: [RepoMixin],
components: {
'repo-tab': RepoTab,
},
data() {
return Store;
computed: {
...mapState([
'openFiles',
]),
},
};
</script>
......@@ -20,7 +20,7 @@
class="list-unstyled"
>
<repo-tab
v-for="tab in openedFiles"
v-for="tab in openFiles"
:key="tab.id"
:tab="tab"
/>
......
import Vue from 'vue';
export default new Vue();
/* global monaco */
import RepoEditor from '../components/repo_editor.vue';
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import monacoLoader from '../monaco_loader';
function repoEditorLoader() {
Store.monacoLoading = true;
return new Promise((resolve, reject) => {
monacoLoader(['vs/editor/editor.main'], () => {
Helper.monaco = monaco;
Store.monacoLoading = false;
resolve(RepoEditor);
}, () => {
Store.monacoLoading = false;
reject();
});
});
}
const MonacoLoaderHelper = {
repoEditorLoader,
};
export default MonacoLoaderHelper;
import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
import Service from '../services/repo_service';
import Store from '../stores/repo_store';
import Flash from '../../flash';
const RepoHelper = {
monacoInstance: null,
getDefaultActiveFile() {
return {
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);
},
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 (response.headers && response.headers['is-root'] && !Store.isInitialRoot) {
Store.isRoot = convertPermissionToBoolean(response.headers['is-root']);
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.raw_path)
.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;
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 = '';
Store.addToOpenedFiles(newFile);
Store.setActiveFiles(newFile);
},
serializeRepoEntity(type, entity, level = 0) {
const { id, url, name, icon, last_commit, tree_url } = entity;
return {
id,
type,
name,
url,
tree_url,
level,
icon: `fa-${icon}`,
files: [],
loading: false,
opened: false,
// 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.url === openedFile.url);
},
getFileFromPath(path) {
return Store.openedFiles.find(file => file.url === path);
},
loadingError() {
Flash('Unable to load this content at this time.');
},
};
export default RepoHelper;
import $ from 'jquery';
import Vue from 'vue';
import { mapActions } from 'vuex';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import Service from './services/repo_service';
import Store from './stores/repo_store';
import Repo from './components/repo.vue';
import RepoEditButton from './components/repo_edit_button.vue';
import newBranchForm from './components/new_branch_form.vue';
import newDropdown from './components/new_dropdown/index.vue';
import store from './stores';
import Translate from '../vue_shared/translate';
function initDropdowns() {
$('.js-tree-ref-target-holder').hide();
}
function addEventsForNonVueEls() {
window.onbeforeunload = function confirmUnload(e) {
const hasChanged = Store.openedFiles
.some(file => file.changed);
if (!hasChanged) return undefined;
const event = e || window.event;
if (event) event.returnValue = 'Are you sure you want to lose unsaved changes?';
// For Safari
return 'Are you sure you want to lose unsaved changes?';
};
}
function setInitialStore(data) {
Store.service = Service;
Store.service.url = data.url;
Store.service.refsUrl = data.refsUrl;
Store.projectId = data.projectId;
Store.projectName = data.projectName;
Store.projectUrl = data.projectUrl;
Store.canCommit = data.canCommit;
Store.onTopOfBranch = data.onTopOfBranch;
Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl);
Store.customBranchURL = decodeURIComponent(data.blobUrl);
Store.isRoot = convertPermissionToBoolean(data.root);
Store.isInitialRoot = convertPermissionToBoolean(data.root);
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
Store.checkIsCommitable();
Store.setBranchHash();
}
function initRepo(el) {
if (!el) return null;
return new Vue({
el,
store,
components: {
repo: Repo,
},
methods: {
...mapActions([
'setInitialData',
]),
},
created() {
const data = el.dataset;
this.setInitialData({
project: {
id: data.projectId,
name: data.projectName,
url: data.projectUrl,
},
endpoints: {
rootEndpoint: data.url,
newMergeRequestUrl: data.newMergeRequestUrl,
rootUrl: data.rootUrl,
},
canCommit: convertPermissionToBoolean(data.canCommit),
onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
currentRef: data.ref,
path: data.currentPath,
currentBranch: data.currentBranch,
isRoot: convertPermissionToBoolean(data.root),
isInitialRoot: convertPermissionToBoolean(data.root),
});
},
render(createElement) {
return createElement('repo');
},
......@@ -56,25 +54,53 @@ function initRepo(el) {
function initRepoEditButton(el) {
return new Vue({
el,
store,
components: {
repoEditButton: RepoEditButton,
},
render(createElement) {
return createElement('repo-edit-button');
},
});
}
function initRepoBundle() {
const repo = document.getElementById('repo');
const editButton = document.querySelector('.editable-mode');
setInitialStore(repo.dataset);
addEventsForNonVueEls();
initDropdowns();
function initNewDropdown(el) {
return new Vue({
el,
store,
components: {
newDropdown,
},
render(createElement) {
return createElement('new-dropdown');
},
});
}
function initNewBranchForm() {
const el = document.querySelector('.js-new-branch-dropdown');
Vue.use(Translate);
if (!el) return null;
initRepo(repo);
initRepoEditButton(editButton);
return new Vue({
el,
components: {
newBranchForm,
},
store,
render(createElement) {
return createElement('new-branch-form');
},
});
}
$(initRepoBundle);
const repo = document.getElementById('repo');
const editButton = document.querySelector('.editable-mode');
const newDropdownHolder = document.querySelector('.js-new-dropdown');
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);
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 Store from '../stores/repo_store';
import Api from '../../api';
import Helper from '../helpers/repo_helper';
const RepoService = {
url: '',
options: {
params: {
format: 'json',
},
},
richExtensionRegExp: /md/,
getRaw(url) {
return axios.get(url, {
// 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);
},
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: '',
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: '',
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.raw_path)
.then((rawResponse) => {
RepoStore.blobRaw = rawResponse.data;
Helper.findOpenedFileFromActive().plain = rawResponse.data;
}).catch(Helper.loadingError);
}
if (!file.loading) 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.url === activeFile.url;
if (activeFile.active) RepoStore.setActiveFile(activeFile, i);
return activeFile;
},
setActiveFile(activeFile, i) {
RepoStore.activeFile = Object.assign({}, RepoStore.activeFile, 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;
});
// 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.url === RepoStore.activeFile.url;
},
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,
});
};
......@@ -11,7 +11,7 @@ export default class MRWidgetService {
this.removeWIPResource = Vue.resource(endpoints.removeWIPPath);
this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath);
this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath);
this.pollResource = Vue.resource(`${endpoints.statusPath}?basic=true`);
this.pollResource = Vue.resource(`${endpoints.statusPath}?serializer=basic`);
this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath);
}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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