Commit c46f7b2a authored by Clement Ho's avatar Clement Ho

Merge branch 'master' into fix-webpack-chunk-naming

parents 0e50e9d9 45031729
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6" image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
.dedicated-runner: &dedicated-runner
retry: 1
tags:
- gitlab-org
.default-cache: &default-cache .default-cache: &default-cache
key: "ruby-235-with-yarn" key: "ruby-235-with-yarn"
paths: paths:
...@@ -42,11 +47,6 @@ stages: ...@@ -42,11 +47,6 @@ stages:
- post-cleanup - post-cleanup
# Predefined scopes # Predefined scopes
.dedicated-runner: &dedicated-runner
retry: 1
tags:
- gitlab-org
.tests-metadata-state: &tests-metadata-state .tests-metadata-state: &tests-metadata-state
<<: *dedicated-runner <<: *dedicated-runner
variables: variables:
...@@ -80,11 +80,15 @@ stages: ...@@ -80,11 +80,15 @@ stages:
except: except:
- /(^qa[\/-].*|.*-qa$)/ - /(^qa[\/-].*|.*-qa$)/
.except-docs-and-qa: &except-docs-and-qa
except:
- /(^docs[\/-].*|.*-docs$)/
- /(^qa[\/-].*|.*-qa$)/
.rspec-metadata: &rspec-metadata .rspec-metadata: &rspec-metadata
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache <<: *pull-cache
<<: *except-docs
<<: *except-qa
stage: test stage: test
script: script:
- JOB_NAME=( $CI_JOB_NAME ) - JOB_NAME=( $CI_JOB_NAME )
...@@ -121,9 +125,8 @@ stages: ...@@ -121,9 +125,8 @@ stages:
.spinach-metadata: &spinach-metadata .spinach-metadata: &spinach-metadata
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache <<: *pull-cache
<<: *except-docs
<<: *except-qa
stage: test stage: test
script: script:
- JOB_NAME=( $CI_JOB_NAME ) - JOB_NAME=( $CI_JOB_NAME )
...@@ -162,6 +165,7 @@ stages: ...@@ -162,6 +165,7 @@ stages:
# Trigger a package build in omnibus-gitlab repository # Trigger a package build in omnibus-gitlab repository
# #
package-qa: package-qa:
<<: *dedicated-runner
image: ruby:2.4-alpine image: ruby:2.4-alpine
before_script: [] before_script: []
stage: build stage: build
...@@ -175,6 +179,7 @@ package-qa: ...@@ -175,6 +179,7 @@ package-qa:
# Review docs base # Review docs base
.review-docs: &review-docs .review-docs: &review-docs
<<: *dedicated-runner
<<: *except-qa <<: *except-qa
image: ruby:2.4-alpine image: ruby:2.4-alpine
before_script: before_script:
...@@ -220,8 +225,7 @@ review-docs-cleanup: ...@@ -220,8 +225,7 @@ review-docs-cleanup:
# Retrieve knapsack and rspec_flaky reports # Retrieve knapsack and rspec_flaky reports
retrieve-tests-metadata: retrieve-tests-metadata:
<<: *tests-metadata-state <<: *tests-metadata-state
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
stage: prepare stage: prepare
cache: cache:
key: tests_metadata key: tests_metadata
...@@ -284,9 +288,9 @@ flaky-examples-check: ...@@ -284,9 +288,9 @@ flaky-examples-check:
- scripts/detect-new-flaky-examples $NEW_FLAKY_SPECS_REPORT - scripts/detect-new-flaky-examples $NEW_FLAKY_SPECS_REPORT
setup-test-env: setup-test-env:
<<: *use-pg
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs
<<: *use-pg
stage: prepare stage: prepare
cache: cache:
<<: *default-cache <<: *default-cache
...@@ -375,19 +379,18 @@ spinach-mysql 3 4: *spinach-metadata-mysql ...@@ -375,19 +379,18 @@ spinach-mysql 3 4: *spinach-metadata-mysql
SETUP_DB: "false" SETUP_DB: "false"
.rake-exec: &rake-exec .rake-exec: &rake-exec
<<: *ruby-static-analysis
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
<<: *ruby-static-analysis
stage: test stage: test
script: script:
- bundle exec rake $CI_JOB_NAME - bundle exec rake $CI_JOB_NAME
static-analysis: static-analysis:
<<: *ruby-static-analysis
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs
<<: *ruby-static-analysis
stage: test stage: test
script: script:
- scripts/static-analysis - scripts/static-analysis
...@@ -441,8 +444,7 @@ ee_compat_check: ...@@ -441,8 +444,7 @@ ee_compat_check:
# DB migration, rollback, and seed jobs # DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset .db-migrate-reset: &db-migrate-reset
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
stage: test stage: test
script: script:
...@@ -456,11 +458,16 @@ db:migrate:reset-mysql: ...@@ -456,11 +458,16 @@ db:migrate:reset-mysql:
<<: *db-migrate-reset <<: *db-migrate-reset
<<: *use-mysql <<: *use-mysql
db:check-schema-pg:
<<: *db-migrate-reset
<<: *use-pg
script:
- source scripts/schema_changed.sh
.migration-paths: &migration-paths .migration-paths: &migration-paths
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache <<: *pull-cache
<<: *except-docs
<<: *except-qa
stage: test stage: test
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
...@@ -486,8 +493,7 @@ migration:path-mysql: ...@@ -486,8 +493,7 @@ migration:path-mysql:
.db-rollback: &db-rollback .db-rollback: &db-rollback
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
stage: test stage: test
script: script:
...@@ -504,8 +510,7 @@ db:rollback-mysql: ...@@ -504,8 +510,7 @@ db:rollback-mysql:
.db-seed_fu: &db-seed_fu .db-seed_fu: &db-seed_fu
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
stage: test stage: test
variables: variables:
...@@ -530,17 +535,10 @@ db:seed_fu-mysql: ...@@ -530,17 +535,10 @@ db:seed_fu-mysql:
<<: *db-seed_fu <<: *db-seed_fu
<<: *use-mysql <<: *use-mysql
db:check-schema-pg:
<<: *db-migrate-reset
<<: *use-pg
script:
- source scripts/schema_changed.sh
# Frontend-related jobs # Frontend-related jobs
gitlab:assets:compile: gitlab:assets:compile:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
stage: test stage: test
dependencies: [] dependencies: []
...@@ -561,11 +559,10 @@ gitlab:assets:compile: ...@@ -561,11 +559,10 @@ gitlab:assets:compile:
- webpack-report/ - webpack-report/
karma: karma:
<<: *use-pg
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
<<: *use-pg
stage: test stage: test
variables: variables:
BABEL_ENV: "coverage" BABEL_ENV: "coverage"
...@@ -604,6 +601,7 @@ codequality: ...@@ -604,6 +601,7 @@ codequality:
paths: [codeclimate.json] paths: [codeclimate.json]
qa:internal: qa:internal:
<<: *dedicated-runner
<<: *except-docs <<: *except-docs
stage: test stage: test
variables: variables:
...@@ -616,8 +614,7 @@ qa:internal: ...@@ -616,8 +614,7 @@ qa:internal:
coverage: coverage:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
stage: post-test stage: post-test
services: [] services: []
...@@ -636,8 +633,7 @@ coverage: ...@@ -636,8 +633,7 @@ coverage:
lint:javascript:report: lint:javascript:report:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs-and-qa
<<: *except-qa
<<: *pull-cache <<: *pull-cache
stage: post-test stage: post-test
dependencies: dependencies:
...@@ -695,9 +691,9 @@ cache gems: ...@@ -695,9 +691,9 @@ cache gems:
- master@gitlab-org/gitlab-ee - master@gitlab-org/gitlab-ee
gitlab_git_test: gitlab_git_test:
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache <<: *pull-cache
<<: *except-docs
<<: *except-qa
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
script: script:
......
...@@ -263,7 +263,7 @@ gem 'gettext_i18n_rails', '~> 1.8.0' ...@@ -263,7 +263,7 @@ gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0' gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development gem 'gettext', '~> 3.2.2', require: false, group: :development
gem 'batch-loader' gem 'batch-loader', '~> 1.2.1'
# Perf bar # Perf bar
gem 'peek', '~> 1.0.1' gem 'peek', '~> 1.0.1'
......
...@@ -78,7 +78,7 @@ GEM ...@@ -78,7 +78,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2) babosa (1.0.2)
base32 (0.3.2) base32 (0.3.2)
batch-loader (1.1.1) batch-loader (1.2.1)
bcrypt (3.1.11) bcrypt (3.1.11)
bcrypt_pbkdf (1.0.0) bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0) benchmark-ips (2.3.0)
...@@ -988,7 +988,7 @@ DEPENDENCIES ...@@ -988,7 +988,7 @@ DEPENDENCIES
awesome_print (~> 1.2.0) awesome_print (~> 1.2.0)
babosa (~> 1.0.2) babosa (~> 1.0.2)
base32 (~> 0.3.0) base32 (~> 0.3.0)
batch-loader batch-loader (~> 1.2.1)
bcrypt_pbkdf (~> 1.0) bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0) benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0) better_errors (~> 2.1.0)
......
import $ from 'jquery'; import $ from 'jquery';
import axios from './lib/utils/axios_utils';
const Api = { const Api = {
groupsPath: '/api/:version/groups.json', groupsPath: '/api/:version/groups.json',
...@@ -6,6 +7,7 @@ const Api = { ...@@ -6,6 +7,7 @@ const Api = {
namespacesPath: '/api/:version/namespaces.json', namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json', groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json', projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels', projectLabelsPath: '/:namespace_path/:project_path/labels',
groupLabelsPath: '/groups/:namespace_path/labels', groupLabelsPath: '/groups/:namespace_path/labels',
licensePath: '/api/:version/templates/licenses/:key', licensePath: '/api/:version/templates/licenses/:key',
...@@ -76,6 +78,14 @@ const Api = { ...@@ -76,6 +78,14 @@ const Api = {
.done(projects => callback(projects)); .done(projects => callback(projects));
}, },
// Return single project
project(projectPath) {
const url = Api.buildUrl(Api.projectPath)
.replace(':id', encodeURIComponent(projectPath));
return axios.get(url);
},
newLabel(namespacePath, projectPath, data, callback) { newLabel(namespacePath, projectPath, data, callback) {
let url; let url;
...@@ -115,7 +125,7 @@ const Api = { ...@@ -115,7 +125,7 @@ const Api = {
commitMultiple(id, data) { commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const url = Api.buildUrl(Api.commitPath) const url = Api.buildUrl(Api.commitPath)
.replace(':id', id); .replace(':id', encodeURIComponent(id));
return this.wrapAjaxCall({ return this.wrapAjaxCall({
url, url,
type: 'POST', type: 'POST',
...@@ -127,7 +137,7 @@ const Api = { ...@@ -127,7 +137,7 @@ const Api = {
branchSingle(id, branch) { branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath) const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', id) .replace(':id', encodeURIComponent(id))
.replace(':branch', branch); .replace(':branch', branch);
return this.wrapAjaxCall({ return this.wrapAjaxCall({
......
...@@ -176,6 +176,7 @@ export default class ImageFile { ...@@ -176,6 +176,7 @@ export default class ImageFile {
left: dragTrackWidth left: dragTrackWidth
}); });
$frameAdded.css('opacity', 1);
framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10); framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
_this.initDraggable($dragger, framePadding, function(e, left) { _this.initDraggable($dragger, framePadding, function(e, left) {
......
...@@ -73,7 +73,6 @@ import initLegacyFilters from './init_legacy_filters'; ...@@ -73,7 +73,6 @@ import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar'; import initIssuableSidebar from './init_issuable_sidebar';
import initProjectVisibilitySelector from './project_visibility'; import initProjectVisibilitySelector from './project_visibility';
import GpgBadges from './gpg_badges'; import GpgBadges from './gpg_badges';
import UserFeatureHelper from './helpers/user_feature_helper';
import initChangesDropdown from './init_changes_dropdown'; import initChangesDropdown from './init_changes_dropdown';
import NewGroupChild from './groups/new_group_child'; import NewGroupChild from './groups/new_group_child';
import AbuseReports from './abuse_reports'; import AbuseReports from './abuse_reports';
...@@ -449,9 +448,6 @@ import Activities from './activities'; ...@@ -449,9 +448,6 @@ import Activities from './activities';
break; break;
case 'projects:tree:show': case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
if (UserFeatureHelper.isNewRepoEnabled()) break;
new TreeView(); new TreeView();
new BlobViewer(); new BlobViewer();
new NewCommitForm($('.js-create-dir-form')); new NewCommitForm($('.js-create-dir-form'));
...@@ -470,7 +466,6 @@ import Activities from './activities'; ...@@ -470,7 +466,6 @@ import Activities from './activities';
shortcut_handler = true; shortcut_handler = true;
break; break;
case 'projects:blob:show': case 'projects:blob:show':
if (UserFeatureHelper.isNewRepoEnabled()) break;
new BlobViewer(); new BlobViewer();
initBlob(); initBlob();
break; break;
......
import Mousetrap from 'mousetrap';
function addMousetrapClick(el, key) {
el.addEventListener('click', () => Mousetrap.trigger(key));
}
function domContentLoaded() {
addMousetrapClick(document.querySelector('.js-trigger-shortcut'), '?');
addMousetrapClick(document.querySelector('.js-trigger-search-bar'), 's');
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
...@@ -161,13 +161,16 @@ export default () => { ...@@ -161,13 +161,16 @@ export default () => {
const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')]; const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')];
sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => { const topItems = sidebar.querySelector('.sidebar-top-level-items');
clearTimeout(timeoutId); if (topItems) {
sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
timeoutId = setTimeout(() => { clearTimeout(timeoutId);
if (currentOpenMenu) hideMenu(currentOpenMenu);
}, getHideSubItemsInterval()); timeoutId = setTimeout(() => {
}); if (currentOpenMenu) hideMenu(currentOpenMenu);
}, getHideSubItemsInterval());
});
}
headerHeight = document.querySelector('.nav-sidebar').offsetTop; headerHeight = document.querySelector('.nav-sidebar').offsetTop;
......
...@@ -300,7 +300,7 @@ GitLabDropdown = (function() { ...@@ -300,7 +300,7 @@ GitLabDropdown = (function() {
return function(data) { return function(data) {
_this.fullData = data; _this.fullData = data;
_this.parseData(_this.fullData); _this.parseData(_this.fullData);
_this.focusTextInput(true); _this.focusTextInput();
if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') { if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
return _this.filter.input.trigger('input'); return _this.filter.input.trigger('input');
} }
...@@ -790,24 +790,16 @@ GitLabDropdown = (function() { ...@@ -790,24 +790,16 @@ GitLabDropdown = (function() {
return [selectedObject, isMarking]; return [selectedObject, isMarking];
}; };
GitLabDropdown.prototype.focusTextInput = function(triggerFocus = false) { GitLabDropdown.prototype.focusTextInput = function() {
if (this.options.filterable) { if (this.options.filterable) {
this.dropdown.one('transitionend', () => { const initialScrollTop = $(window).scrollTop();
const initialScrollTop = $(window).scrollTop();
if (this.dropdown.is('.open')) { if (this.dropdown.is('.open')) {
this.filterInput.focus(); this.filterInput.focus();
} }
if ($(window).scrollTop() < initialScrollTop) {
$(window).scrollTop(initialScrollTop);
}
});
if (triggerFocus) { if ($(window).scrollTop() < initialScrollTop) {
// This triggers after a ajax request $(window).scrollTop(initialScrollTop);
// in case of slow requests, the dropdown transition could already be finished
this.dropdown.trigger('transitionend');
} }
} }
}; };
......
...@@ -84,9 +84,12 @@ export default (function() { ...@@ -84,9 +84,12 @@ export default (function() {
return _.each(author_commits, (function(_this) { return _.each(author_commits, (function(_this) {
return function(d) { return function(d) {
_this.redraw_author_commit_info(d); _this.redraw_author_commit_info(d);
$(_this.authors[d.author_name].list_item).appendTo("ol"); if (_this.authors[d.author_name] != null) {
_this.authors[d.author_name].set_data(d.dates); $(_this.authors[d.author_name].list_item).appendTo("ol");
return _this.authors[d.author_name].redraw(); _this.authors[d.author_name].set_data(d.dates);
return _this.authors[d.author_name].redraw();
}
return '';
}; };
})(this)); })(this));
}; };
...@@ -108,10 +111,14 @@ export default (function() { ...@@ -108,10 +111,14 @@ export default (function() {
}; };
ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) { ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) {
var author_commit_info, author_list_item; var author_commit_info, author_list_item, $author;
author_list_item = $(this.authors[author.author_name].list_item); $author = this.authors[author.author_name];
author_commit_info = this.format_author_commit_info(author); if ($author != null) {
return author_list_item.find("span").html(author_commit_info); author_list_item = $(this.authors[author.author_name].list_item);
author_commit_info = this.format_author_commit_info(author);
return author_list_item.find("span").html(author_commit_info);
}
return '';
}; };
return ContributorsStatGraph; return ContributorsStatGraph;
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */
import _ from 'underscore'; import _ from 'underscore';
import d3 from 'd3'; import { extent, max } from 'd3-array';
import { select, event as d3Event } from 'd3-selection';
import { scaleTime, scaleLinear } from 'd3-scale';
import { axisLeft, axisBottom } from 'd3-axis';
import { area } from 'd3-shape';
import { brushX } from 'd3-brush';
import { timeParse } from 'd3-time-format';
import { dateTickFormat } from '../lib/utils/tick_formats'; import { dateTickFormat } from '../lib/utils/tick_formats';
const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse };
const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
const hasProp = {}.hasOwnProperty; const hasProp = {}.hasOwnProperty;
...@@ -71,8 +79,8 @@ export const ContributorsGraph = (function() { ...@@ -71,8 +79,8 @@ export const ContributorsGraph = (function() {
}; };
ContributorsGraph.prototype.create_scale = function(width, height) { ContributorsGraph.prototype.create_scale = function(width, height) {
this.x = d3.time.scale().range([0, width]).clamp(true); this.x = d3.scaleTime().range([0, width]).clamp(true);
return this.y = d3.scale.linear().range([height, 0]).nice(); return this.y = d3.scaleLinear().range([height, 0]).nice();
}; };
ContributorsGraph.prototype.draw_x_axis = function() { ContributorsGraph.prototype.draw_x_axis = function() {
...@@ -124,7 +132,7 @@ export const ContributorsMasterGraph = (function(superClass) { ...@@ -124,7 +132,7 @@ export const ContributorsMasterGraph = (function(superClass) {
ContributorsMasterGraph.prototype.parse_dates = function(data) { ContributorsMasterGraph.prototype.parse_dates = function(data) {
var parseDate; var parseDate;
parseDate = d3.time.format("%Y-%m-%d").parse; parseDate = d3.timeParse("%Y-%m-%d");
return data.forEach(function(d) { return data.forEach(function(d) {
return d.date = parseDate(d.date); return d.date = parseDate(d.date);
}); });
...@@ -135,11 +143,10 @@ export const ContributorsMasterGraph = (function(superClass) { ...@@ -135,11 +143,10 @@ export const ContributorsMasterGraph = (function(superClass) {
}; };
ContributorsMasterGraph.prototype.create_axes = function() { ContributorsMasterGraph.prototype.create_axes = function() {
this.x_axis = d3.svg.axis() this.x_axis = d3.axisBottom()
.scale(this.x) .scale(this.x)
.orient('bottom')
.tickFormat(dateTickFormat); .tickFormat(dateTickFormat);
return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); return this.y_axis = d3.axisLeft().scale(this.y).ticks(5);
}; };
ContributorsMasterGraph.prototype.create_svg = function() { ContributorsMasterGraph.prototype.create_svg = function() {
...@@ -147,16 +154,16 @@ export const ContributorsMasterGraph = (function(superClass) { ...@@ -147,16 +154,16 @@ export const ContributorsMasterGraph = (function(superClass) {
}; };
ContributorsMasterGraph.prototype.create_area = function(x, y) { ContributorsMasterGraph.prototype.create_area = function(x, y) {
return this.area = d3.svg.area().x(function(d) { return this.area = d3.area().x(function(d) {
return x(d.date); return x(d.date);
}).y0(this.height).y1(function(d) { }).y0(this.height).y1(function(d) {
d.commits = d.commits || d.additions || d.deletions; d.commits = d.commits || d.additions || d.deletions;
return y(d.commits); return y(d.commits);
}).interpolate("basis"); });
}; };
ContributorsMasterGraph.prototype.create_brush = function() { ContributorsMasterGraph.prototype.create_brush = function() {
return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content); return this.brush = d3.brushX(this.x).extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]]).on("end", this.update_content);
}; };
ContributorsMasterGraph.prototype.draw_path = function(data) { ContributorsMasterGraph.prototype.draw_path = function(data) {
...@@ -168,7 +175,12 @@ export const ContributorsMasterGraph = (function(superClass) { ...@@ -168,7 +175,12 @@ export const ContributorsMasterGraph = (function(superClass) {
}; };
ContributorsMasterGraph.prototype.update_content = function() { ContributorsMasterGraph.prototype.update_content = function() {
ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent()); // d3Event.selection replaces the function brush.empty() calls
if (d3Event.selection != null) {
ContributorsGraph.set_x_domain(d3Event.selection.map(this.x.invert));
} else {
ContributorsGraph.set_x_domain(this.x_max_domain);
}
return $("#brush_change").trigger('change'); return $("#brush_change").trigger('change');
}; };
...@@ -226,18 +238,17 @@ export const ContributorsAuthorGraph = (function(superClass) { ...@@ -226,18 +238,17 @@ export const ContributorsAuthorGraph = (function(superClass) {
}; };
ContributorsAuthorGraph.prototype.create_axes = function() { ContributorsAuthorGraph.prototype.create_axes = function() {
this.x_axis = d3.svg.axis() this.x_axis = d3.axisBottom()
.scale(this.x) .scale(this.x)
.orient('bottom')
.ticks(8) .ticks(8)
.tickFormat(dateTickFormat); .tickFormat(dateTickFormat);
return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); return this.y_axis = d3.axisLeft().scale(this.y).ticks(5);
}; };
ContributorsAuthorGraph.prototype.create_area = function(x, y) { ContributorsAuthorGraph.prototype.create_area = function(x, y) {
return this.area = d3.svg.area().x(function(d) { return this.area = d3.area().x(function(d) {
var parseDate; var parseDate;
parseDate = d3.time.format("%Y-%m-%d").parse; parseDate = d3.timeParse("%Y-%m-%d");
return x(parseDate(d)); return x(parseDate(d));
}).y0(this.height).y1((function(_this) { }).y0(this.height).y1((function(_this) {
return function(d) { return function(d) {
...@@ -247,11 +258,12 @@ export const ContributorsAuthorGraph = (function(superClass) { ...@@ -247,11 +258,12 @@ export const ContributorsAuthorGraph = (function(superClass) {
return y(0); return y(0);
} }
}; };
})(this)).interpolate("basis"); })(this));
}; };
ContributorsAuthorGraph.prototype.create_svg = function() { ContributorsAuthorGraph.prototype.create_svg = function() {
this.list_item = d3.selectAll(".person")[0].pop(); var persons = document.querySelectorAll('.person');
this.list_item = persons[persons.length - 1];
return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")"); return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
}; };
......
import Cookies from 'js-cookie';
export default {
isNewRepoEnabled() {
return Cookies.get('new_repo') === 'true';
},
};
<script> <script>
import { mapState } from 'vuex';
import icon from '../../../vue_shared/components/icon.vue'; import icon from '../../../vue_shared/components/icon.vue';
import listItem from './list_item.vue'; import listItem from './list_item.vue';
import listCollapsed from './list_collapsed.vue'; import listCollapsed from './list_collapsed.vue';
...@@ -18,72 +19,48 @@ ...@@ -18,72 +19,48 @@
type: Array, type: Array,
required: true, required: true,
}, },
collapsed: { },
type: Boolean, computed: {
required: true, ...mapState([
}, 'currentProjectId',
'currentBranchId',
'rightPanelCollapsed',
]),
}, },
methods: { methods: {
toggleCollapsed() { toggleCollapsed() {
this.$emit('toggleCollapsed'); this.$emit('toggleCollapsed');
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="multi-file-commit-panel-section"> <div class="multi-file-commit-list">
<header <list-collapsed
class="multi-file-commit-panel-header" v-if="rightPanelCollapsed"
:class="{ />
'is-collapsed': collapsed, <template v-else>
}" <ul
> v-if="fileList.length"
<icon class="list-unstyled append-bottom-0"
name="list-bulleted" >
:size="18" <li
css-classes="append-right-default" v-for="file in fileList"
/> :key="file.key"
<template v-if="!collapsed">
{{ title }}
<button
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
@click="toggleCollapsed"
>
<i
aria-hidden="true"
class="fa fa-angle-double-right"
>
</i>
</button>
</template>
</header>
<div class="multi-file-commit-list">
<list-collapsed
v-if="collapsed"
/>
<template v-else>
<ul
v-if="fileList.length"
class="list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
:key="file.key"
>
<list-item
:file="file"
/>
</li>
</ul>
<div
v-else
class="help-block prepend-top-0"
> >
No changes <list-item
</div> :file="file"
</template> />
</div> </li>
</ul>
<div
v-else
class="help-block prepend-top-0"
>
No changes
</div>
</template>
</div> </div>
</template> </template>
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import RepoSidebar from './repo_sidebar.vue'; import ideSidebar from './ide_side_bar.vue';
import RepoCommitSection from './repo_commit_section.vue'; import ideContextbar from './ide_context_bar.vue';
import RepoTabs from './repo_tabs.vue'; import repoTabs from './repo_tabs.vue';
import RepoFileButtons from './repo_file_buttons.vue'; import repoFileButtons from './repo_file_buttons.vue';
import RepoPreview from './repo_preview.vue'; import ideStatusBar from './ide_status_bar.vue';
import repoPreview from './repo_preview.vue';
import repoEditor from './repo_editor.vue'; import repoEditor from './repo_editor.vue';
export default { export default {
computed: { computed: {
...mapState([ ...mapState([
'currentBlobView', 'currentBlobView',
'selectedFile',
]), ]),
...mapGetters([ ...mapGetters([
'isCollapsed',
'changedFiles', 'changedFiles',
'activeFile',
]), ]),
}, },
components: { components: {
RepoSidebar, ideSidebar,
RepoTabs, ideContextbar,
RepoFileButtons, repoTabs,
repoFileButtons,
ideStatusBar,
repoEditor, repoEditor,
RepoCommitSection, repoPreview,
RepoPreview,
}, },
mounted() { mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?'; const returnValue = 'Are you sure you want to lose unsaved changes?';
...@@ -40,24 +43,31 @@ export default { ...@@ -40,24 +43,31 @@ export default {
</script> </script>
<template> <template>
<div <div
class="multi-file" class="ide-view"
:class="{
'is-collapsed': isCollapsed
}"
> >
<repo-sidebar/> <ide-sidebar/>
<div <div
v-if="isCollapsed"
class="multi-file-edit-pane" class="multi-file-edit-pane"
> >
<repo-tabs /> <template
<component v-if="activeFile">
class="multi-file-edit-pane-content" <repo-tabs/>
:is="currentBlobView" <component
/> class="multi-file-edit-pane-content"
<repo-file-buttons /> :is="currentBlobView"
/>
<repo-file-buttons/>
<ide-status-bar
:file="selectedFile"/>
</template>
<template
v-else>
<div class="ide-empty-state">
<h2 class="clgray">Welcome to the GitLab IDE</h2>
</div>
</template>
</div> </div>
<repo-commit-section /> <ide-contextbar/>
</div> </div>
</template> </template>
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
import repoCommitSection from './repo_commit_section.vue';
import icon from '../../vue_shared/components/icon.vue';
export default {
components: {
repoCommitSection,
icon,
},
computed: {
...mapState([
'rightPanelCollapsed',
]),
...mapGetters([
'changedFiles',
]),
currentIcon() {
return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'right',
collapsed: !this.rightPanelCollapsed,
});
},
},
};
</script>
<template>
<div
class="multi-file-commit-panel"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<div
class="multi-file-commit-panel-section">
<header
class="multi-file-commit-panel-header"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<div
class="multi-file-commit-panel-header-title"
v-if="!rightPanelCollapsed">
<icon
name="list-bulleted"
:size="18"
/>
Staged
</div>
<button
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
@click="toggleCollapsed"
>
<icon
:name="currentIcon"
:size="18"
/>
</button>
</header>
<repo-commit-section
class=""/>
</div>
</div>
</template>
<script>
import repoTree from './ide_repo_tree.vue';
import icon from '../../vue_shared/components/icon.vue';
import newDropdown from './new_dropdown/index.vue';
export default {
components: {
repoTree,
icon,
newDropdown,
},
props: {
projectId: {
type: String,
required: true,
},
branch: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="branch-container">
<div class="branch-header">
<div class="branch-header-title">
<icon
name="branch"
:size="12">
</icon>
{{ branch.name }}
</div>
<div class="branch-header-btns">
<new-dropdown
:project-id="projectId"
:branch="branch.name"
path=""/>
</div>
</div>
<div>
<repo-tree
:treeId="branch.treeId"/>
</div>
</div>
</template>
<script>
import branchesTree from './ide_project_branches_tree.vue';
import projectAvatarImage from '../../vue_shared/components/project_avatar/image.vue';
export default {
components: {
branchesTree,
projectAvatarImage,
},
props: {
project: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="projects-sidebar">
<div class="context-header">
<a
:title="project.name"
:href="project.web_url">
<div class="avatar-container s40 project-avatar">
<project-avatar-image
class="avatar-container project-avatar"
:link-href="project.path"
:img-src="project.avatar_url"
:img-alt="project.name"
:img-size="40"
/>
</div>
<div class="sidebar-context-title">
{{ project.name }}
</div>
</a>
</div>
<div class="multi-file-commit-panel-inner-scroll">
<branches-tree
v-for="(branch, index) in project.branches"
:key="branch.name"
:project-id="project.path_with_namespace"
:branch="branch"/>
</div>
</div>
</template>
<script> <script>
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState } from 'vuex';
import RepoPreviousDirectory from './repo_prev_directory.vue'; import RepoPreviousDirectory from './repo_prev_directory.vue';
import RepoFile from './repo_file.vue'; import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue'; import RepoLoadingFile from './repo_loading_file.vue';
import { treeList } from '../stores/utils';
export default { export default {
components: { components: {
...@@ -10,14 +11,11 @@ export default { ...@@ -10,14 +11,11 @@ export default {
'repo-file': RepoFile, 'repo-file': RepoFile,
'repo-loading-file': RepoLoadingFile, 'repo-loading-file': RepoLoadingFile,
}, },
created() { props: {
window.addEventListener('popstate', this.popHistoryState); treeId: {
}, type: String,
destroyed() { required: true,
window.removeEventListener('popstate', this.popHistoryState); },
},
mounted() {
this.getTreeData();
}, },
computed: { computed: {
...mapState([ ...mapState([
...@@ -29,57 +27,40 @@ export default { ...@@ -29,57 +27,40 @@ export default {
return state.project.name; return state.project.name;
}, },
}), }),
...mapGetters([ fetchedList() {
'treeList', return treeList(this.$store.state, this.treeId);
'isCollapsed', },
]), hasPreviousDirectory() {
}, return !this.isRoot && this.fetchedList.length;
methods: { },
...mapActions([ showLoading() {
'getTreeData', return this.loading;
'popHistoryState', },
]),
}, },
}; };
</script> </script>
<template> <template>
<div class="ide-file-list"> <div>
<table class="table"> <div class="ide-file-list">
<thead> <table class="table">
<tr> <tbody
<th v-if="treeId">
v-if="isCollapsed" <repo-previous-directory
> v-if="hasPreviousDirectory"
</th> />
<template v-else> <repo-loading-file
<th class="name multi-file-table-name"> v-if="showLoading"
Name v-for="n in 5"
</th> :key="n"
<th class="hidden-sm hidden-xs last-commit"> />
Last commit <repo-file
</th> v-for="file in fetchedList"
<th class="hidden-xs last-update text-right"> :key="file.key"
Last update :file="file"
</th> />
</template> </tbody>
</tr> </table>
</thead> </div>
<tbody>
<repo-previous-directory
v-if="!isRoot && treeList.length"
/>
<repo-loading-file
v-if="!treeList.length && loading"
v-for="n in 5"
:key="n"
/>
<repo-file
v-for="file in treeList"
:key="file.key"
:file="file"
/>
</tbody>
</table>
</div> </div>
</template> </template>
<script>
import { mapState, mapActions } from 'vuex';
import projectTree from './ide_project_tree.vue';
import icon from '../../vue_shared/components/icon.vue';
export default {
components: {
projectTree,
icon,
},
computed: {
...mapState([
'projects',
'leftPanelCollapsed',
]),
currentIcon() {
return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left';
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'left',
collapsed: !this.leftPanelCollapsed,
});
},
},
};
</script>
<template>
<div
class="multi-file-commit-panel"
:class="{
'is-collapsed': leftPanelCollapsed,
}"
>
<div class="multi-file-commit-panel-inner">
<project-tree
v-for="(project, index) in projects"
:key="project.id"
:project="project"/>
</div>
<button
type="button"
class="btn btn-transparent left-collapse-btn"
@click="toggleCollapsed"
>
<icon
:name="currentIcon"
:size="18"
/>
<span
v-if="!leftPanelCollapsed"
class="collapse-text"
>Collapse sidebar</span>
</button>
</div>
</template>
<script>
import { mapState } from 'vuex';
import icon from '../../vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
export default {
props: {
file: {
type: Object,
required: true,
},
},
components: {
icon,
},
directives: {
tooltip,
},
mixins: [
timeAgoMixin,
],
computed: {
...mapState([
'selectedFile',
]),
},
};
</script>
<template>
<div
class="ide-status-bar">
<div>
<icon
name="branch"
:size="12">
</icon>
{{ selectedFile.branchId }}
</div>
<div>
<div
v-if="selectedFile.lastCommit && selectedFile.lastCommit.id">
Last commit:
<a
v-tooltip
:title="selectedFile.lastCommit.message"
:href="selectedFile.lastCommit.url">
{{ timeFormated(selectedFile.lastCommit.updatedAt) }} by
{{ selectedFile.lastCommit.author }}
</a>
</div>
</div>
<div
class="text-right">
{{ selectedFile.name }}
</div>
<div
class="text-right">
{{ selectedFile.eol }}
</div>
<div
class="text-right">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
<div
class="text-right">
{{ selectedFile.fileLanguage }}
</div>
</div>
</template>
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
this.branchName = ''; this.branchName = '';
if (this.dropdownText) { if (this.dropdownText) {
this.dropdownText.textContent = this.currentBranch; this.dropdownText.textContent = this.currentBranchId;
} }
this.toggleDropdown(); this.toggleDropdown();
......
<script> <script>
import { mapState } from 'vuex';
import newModal from './modal.vue'; import newModal from './modal.vue';
import upload from './upload.vue'; import upload from './upload.vue';
import icon from '../../../vue_shared/components/icon.vue'; import icon from '../../../vue_shared/components/icon.vue';
export default { export default {
props: {
branch: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
parent: {
type: Object,
default: null,
},
},
components: { components: {
icon, icon,
newModal, newModal,
...@@ -16,11 +29,6 @@ ...@@ -16,11 +29,6 @@
modalType: '', modalType: '',
}; };
}, },
computed: {
...mapState([
'path',
]),
},
methods: { methods: {
createNewItem(type) { createNewItem(type) {
this.modalType = type; this.modalType = type;
...@@ -34,55 +42,59 @@ ...@@ -34,55 +42,59 @@
</script> </script>
<template> <template>
<div> <div class="repo-new-btn pull-right">
<ul class="breadcrumb repo-breadcrumb"> <div class="dropdown">
<li class="dropdown"> <button
<button type="button"
type="button" class="btn btn-sm btn-default dropdown-toggle add-to-tree"
class="btn btn-default dropdown-toggle add-to-tree" data-toggle="dropdown"
data-toggle="dropdown" aria-label="Create new file or directory"
aria-label="Create new file or directory" >
> <icon
<icon name="plus"
name="plus" :size="12"
css-classes="pull-left" css-classes="pull-left"
/> />
<icon <icon
name="arrow-down" name="arrow-down"
css-classes="pull-left" :size="12"
css-classes="pull-left"
/>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('blob')"
>
{{ __('New file') }}
</a>
</li>
<li>
<upload
:branch-id="branch"
:path="path"
:parent="parent"
/> />
</button> </li>
<ul class="dropdown-menu"> <li>
<li> <a
<a href="#"
href="#" role="button"
role="button" @click.prevent="createNewItem('tree')"
@click.prevent="createNewItem('blob')" >
> {{ __('New directory') }}
{{ __('New file') }} </a>
</a> </li>
</li> </ul>
<li> </div>
<upload
:path="path"
/>
</li>
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('tree')"
>
{{ __('New directory') }}
</a>
</li>
</ul>
</li>
</ul>
<new-modal <new-modal
v-if="openModal" v-if="openModal"
:type="modalType" :type="modalType"
:branch-id="branch"
:path="path" :path="path"
:parent="parent"
@toggle="toggleModalOpen" @toggle="toggleModalOpen"
/> />
</div> </div>
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import modal from '../../../vue_shared/components/modal.vue'; import modal from '../../../vue_shared/components/modal.vue';
export default { export default {
props: { props: {
branchId: {
type: String,
required: true,
},
parent: {
type: Object,
default: null,
},
type: { type: {
type: String, type: String,
required: true, required: true,
...@@ -28,6 +36,9 @@ ...@@ -28,6 +36,9 @@
]), ]),
createEntryInStore() { createEntryInStore() {
this.createTempEntry({ this.createTempEntry({
projectId: this.currentProjectId,
branchId: this.branchId,
parent: this.parent,
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''), name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
type: this.type, type: this.type,
}); });
...@@ -39,6 +50,9 @@ ...@@ -39,6 +50,9 @@
}, },
}, },
computed: { computed: {
...mapState([
'currentProjectId',
]),
modalTitle() { modalTitle() {
if (this.type === 'tree') { if (this.type === 'tree') {
return __('Create new directory'); return __('Create new directory');
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions, mapState } from 'vuex';
export default { export default {
props: { props: {
path: { branchId: {
type: String, type: String,
required: true, required: true,
}, },
parent: {
type: Object,
default: null,
},
},
computed: {
...mapState([
'trees',
'currentProjectId',
]),
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -22,6 +32,9 @@ ...@@ -22,6 +32,9 @@
this.createTempEntry({ this.createTempEntry({
name, name,
projectId: this.currentProjectId,
branchId: this.branchId,
parent: this.parent,
type: 'blob', type: 'blob',
content: result, content: result,
base64: !isText, base64: !isText,
...@@ -42,6 +55,9 @@ ...@@ -42,6 +55,9 @@
openFile() { openFile() {
Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file)); Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
}, },
startFileUpload() {
this.$refs.fileUpload.click();
},
}, },
mounted() { mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile); this.$refs.fileUpload.addEventListener('change', this.openFile);
...@@ -53,16 +69,19 @@ ...@@ -53,16 +69,19 @@
</script> </script>
<template> <template>
<label <div>
role="button" <a
class="menu-item" href="#"
> role="button"
{{ __('Upload file') }} @click.prevent="startFileUpload"
>
{{ __('Upload file') }}
</a>
<input <input
id="file-upload" id="file-upload"
type="file" type="file"
class="hidden" class="hidden"
ref="fileUpload" ref="fileUpload"
/> />
</label> </div>
</template> </template>
...@@ -20,12 +20,13 @@ export default { ...@@ -20,12 +20,13 @@ export default {
submitCommitsLoading: false, submitCommitsLoading: false,
startNewMR: false, startNewMR: false,
commitMessage: '', commitMessage: '',
collapsed: true,
}; };
}, },
computed: { computed: {
...mapState([ ...mapState([
'currentBranch', 'currentProjectId',
'currentBranchId',
'rightPanelCollapsed',
]), ]),
...mapGetters([ ...mapGetters([
'changedFiles', 'changedFiles',
...@@ -42,12 +43,13 @@ export default { ...@@ -42,12 +43,13 @@ export default {
'checkCommitStatus', 'checkCommitStatus',
'commitChanges', 'commitChanges',
'getTreeData', 'getTreeData',
'setPanelCollapsedStatus',
]), ]),
makeCommit(newBranch = false) { makeCommit(newBranch = false) {
const createNewBranch = newBranch || this.startNewMR; const createNewBranch = newBranch || this.startNewMR;
const payload = { const payload = {
branch: createNewBranch ? `${this.currentBranch}-${new Date().getTime().toString()}` : this.currentBranch, branch: createNewBranch ? `${this.currentBranchId}-${new Date().getTime().toString()}` : this.currentBranchId,
commit_message: this.commitMessage, commit_message: this.commitMessage,
actions: this.changedFiles.map(f => ({ actions: this.changedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update', action: f.tempFile ? 'create' : 'update',
...@@ -55,7 +57,7 @@ export default { ...@@ -55,7 +57,7 @@ export default {
content: f.content, content: f.content,
encoding: f.base64 ? 'base64' : 'text', encoding: f.base64 ? 'base64' : 'text',
})), })),
start_branch: createNewBranch ? this.currentBranch : undefined, start_branch: createNewBranch ? this.currentBranchId : undefined,
}; };
this.showNewBranchModal = false; this.showNewBranchModal = false;
...@@ -64,7 +66,12 @@ export default { ...@@ -64,7 +66,12 @@ export default {
this.commitChanges({ payload, newMr: this.startNewMR }) this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => { .then(() => {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
this.getTreeData(); this.$store.dispatch('getTreeData', {
projectId: this.currentProjectId,
branch: this.currentBranchId,
endpoint: `/tree/${this.currentBranchId}`,
force: true,
});
}) })
.catch(() => { .catch(() => {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
...@@ -86,19 +93,17 @@ export default { ...@@ -86,19 +93,17 @@ export default {
}); });
}, },
toggleCollapsed() { toggleCollapsed() {
this.collapsed = !this.collapsed; this.setPanelCollapsedStatus({
side: 'right',
collapsed: !this.rightPanelCollapsed,
});
}, },
}, },
}; };
</script> </script>
<template> <template>
<div <div class="multi-file-commit-panel-section">
class="multi-file-commit-panel"
:class="{
'is-collapsed': collapsed,
}"
>
<modal <modal
v-if="showNewBranchModal" v-if="showNewBranchModal"
:primary-button-label="__('Create new branch')" :primary-button-label="__('Create new branch')"
...@@ -108,28 +113,16 @@ export default { ...@@ -108,28 +113,16 @@ export default {
@toggle="showNewBranchModal = false" @toggle="showNewBranchModal = false"
@submit="makeCommit(true)" @submit="makeCommit(true)"
/> />
<button
v-if="collapsed"
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn is-collapsed prepend-top-10 append-bottom-10"
@click="toggleCollapsed"
>
<i
aria-hidden="true"
class="fa fa-angle-double-left"
>
</i>
</button>
<commit-files-list <commit-files-list
title="Staged" title="Staged"
:file-list="changedFiles" :file-list="changedFiles"
:collapsed="collapsed" :collapsed="rightPanelCollapsed"
@toggleCollapsed="toggleCollapsed" @toggleCollapsed="toggleCollapsed"
/> />
<form <form
class="form-horizontal multi-file-commit-form" class="form-horizontal multi-file-commit-form"
@submit.prevent="tryCommit" @submit.prevent="tryCommit"
v-if="!collapsed" v-if="!rightPanelCollapsed"
> >
<div class="multi-file-commit-fieldset"> <div class="multi-file-commit-fieldset">
<textarea <textarea
......
<script> <script>
/* global monaco */ /* global monaco */
import { mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '../../flash'; import flash from '../../flash';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
...@@ -24,6 +24,9 @@ export default { ...@@ -24,6 +24,9 @@ export default {
...mapActions([ ...mapActions([
'getRawFileData', 'getRawFileData',
'changeFileContent', 'changeFileContent',
'setFileLanguage',
'setEditorPosition',
'setFileEOL',
]), ]),
initMonaco() { initMonaco() {
if (this.shouldHideEditor) return; if (this.shouldHideEditor) return;
...@@ -43,12 +46,36 @@ export default { ...@@ -43,12 +46,36 @@ export default {
const model = this.editor.createModel(this.activeFile); const model = this.editor.createModel(this.activeFile);
this.editor.attachModel(model); this.editor.attachModel(model);
model.onChange((m) => { model.onChange((m) => {
this.changeFileContent({ this.changeFileContent({
file: this.activeFile, file: this.activeFile,
content: m.getValue(), content: m.getValue(),
}); });
}); });
// Handle Cursor Position
this.editor.onPositionChange((instance, e) => {
this.setEditorPosition({
editorRow: e.position.lineNumber,
editorColumn: e.position.column,
});
});
this.editor.setPosition({
lineNumber: this.activeFile.editorRow,
column: this.activeFile.editorColumn,
});
// Handle File Language
this.setFileLanguage({
fileLanguage: model.language,
});
// Get File eol
this.setFileEOL({
eol: model.eol,
});
}, },
}, },
watch: { watch: {
...@@ -57,12 +84,22 @@ export default { ...@@ -57,12 +84,22 @@ export default {
this.initMonaco(); this.initMonaco();
} }
}, },
leftPanelCollapsed() {
this.editor.updateDimensions();
},
rightPanelCollapsed() {
this.editor.updateDimensions();
},
}, },
computed: { computed: {
...mapGetters([ ...mapGetters([
'activeFile', 'activeFile',
'activeFileExtension', 'activeFileExtension',
]), ]),
...mapState([
'leftPanelCollapsed',
'rightPanelCollapsed',
]),
shouldHideEditor() { shouldHideEditor() {
return this.activeFile.binary && !this.activeFile.raw; return this.activeFile.binary && !this.activeFile.raw;
}, },
...@@ -76,13 +113,14 @@ export default { ...@@ -76,13 +113,14 @@ export default {
class="blob-viewer-container blob-editor-container" class="blob-viewer-container blob-editor-container"
> >
<div <div
v-show="shouldHideEditor" v-if="shouldHideEditor"
v-html="activeFile.html" v-html="activeFile.html"
> >
</div> </div>
<div <div
v-show="!shouldHideEditor" v-show="!shouldHideEditor"
ref="editor" ref="editor"
class="multi-file-editor-holder"
> >
</div> </div>
</div> </div>
......
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { mapState } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago'; import timeAgoMixin from '../../vue_shared/mixins/timeago';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
import newDropdown from './new_dropdown/index.vue';
export default { export default {
mixins: [ mixins: [
...@@ -9,20 +10,22 @@ ...@@ -9,20 +10,22 @@
], ],
components: { components: {
skeletonLoadingContainer, skeletonLoadingContainer,
newDropdown,
}, },
props: { props: {
file: { file: {
type: Object, type: Object,
required: true, required: true,
}, },
showExtraColumns: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
...mapGetters([ ...mapState([
'isCollapsed', 'leftPanelCollapsed',
]), ]),
isSubmodule() {
return this.file.type === 'submodule';
},
fileIcon() { fileIcon() {
return { return {
'fa-spinner fa-spin': this.file.loading, 'fa-spinner fa-spin': this.file.loading,
...@@ -30,6 +33,12 @@ ...@@ -30,6 +33,12 @@
'fa-folder-open': !this.file.loading && this.file.opened, 'fa-folder-open': !this.file.loading && this.file.opened,
}; };
}, },
isSubmodule() {
return this.file.type === 'submodule';
},
isTree() {
return this.file.type === 'tree';
},
levelIndentation() { levelIndentation() {
return { return {
marginLeft: `${this.file.level * 16}px`, marginLeft: `${this.file.level * 16}px`,
...@@ -39,13 +48,39 @@ ...@@ -39,13 +48,39 @@
return this.file.id.substr(0, 8); return this.file.id.substr(0, 8);
}, },
submoduleColSpan() { submoduleColSpan() {
return !this.isCollapsed && this.isSubmodule ? 3 : 1; return !this.leftPanelCollapsed && this.isSubmodule ? 3 : 1;
},
fileClass() {
if (this.file.type === 'blob') {
if (this.file.active) {
return 'file-open file-active';
}
return this.file.opened ? 'file-open' : '';
}
return '';
},
changedClass() {
return {
'fa-circle unsaved-icon': this.file.changed || this.file.tempFile,
};
}, },
}, },
methods: { methods: {
...mapActions([ clickFile(row) {
'clickedTreeRow', // Manual Action if a tree is selected/opened
]), if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) {
this.$store.dispatch('toggleTreeOpen', {
endpoint: this.file.url,
tree: this.file,
});
}
this.$router.push(`/project${row.url}`);
},
},
updated() {
if (this.file.type === 'blob' && this.file.active) {
this.$el.scrollIntoView();
}
}, },
}; };
</script> </script>
...@@ -53,7 +88,8 @@ ...@@ -53,7 +88,8 @@
<template> <template>
<tr <tr
class="file" class="file"
@click.prevent="clickedTreeRow(file)"> :class="fileClass"
@click="clickFile(file)">
<td <td
class="multi-file-table-name" class="multi-file-table-name"
:colspan="submoduleColSpan" :colspan="submoduleColSpan"
...@@ -66,11 +102,23 @@ ...@@ -66,11 +102,23 @@
> >
</i> </i>
<a <a
:href="file.url"
class="repo-file-name" class="repo-file-name"
> >
{{ file.name }} {{ file.name }}
</a> </a>
<new-dropdown
v-if="isTree"
:project-id="file.projectId"
:branch="file.branchId"
:path="file.path"
:parent="file"/>
<i
class="fa"
v-if="changedClass"
:class="changedClass"
aria-hidden="true"
>
</i>
<template v-if="isSubmodule && file.id"> <template v-if="isSubmodule && file.id">
@ @
<span class="commit-sha"> <span class="commit-sha">
...@@ -84,7 +132,7 @@ ...@@ -84,7 +132,7 @@
</template> </template>
</td> </td>
<template v-if="!isCollapsed && !isSubmodule"> <template v-if="showExtraColumns && !isSubmodule">
<td class="multi-file-table-col-commit-message hidden-sm hidden-xs"> <td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
<a <a
v-if="file.lastCommit.message" v-if="file.lastCommit.message"
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapState } from 'vuex';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
export default { export default {
...@@ -7,8 +7,8 @@ ...@@ -7,8 +7,8 @@
skeletonLoadingContainer, skeletonLoadingContainer,
}, },
computed: { computed: {
...mapGetters([ ...mapState([
'isCollapsed', 'leftPanelCollapsed',
]), ]),
}, },
}; };
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
:small="true" :small="true"
/> />
</td> </td>
<template v-if="!isCollapsed"> <template v-if="!leftPanelCollapsed">
<td <td
class="hidden-sm hidden-xs"> class="hidden-sm hidden-xs">
<skeleton-loading-container <skeleton-loading-container
......
<script> <script>
import { mapGetters, mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
export default { export default {
computed: { computed: {
...mapState([ ...mapState([
'parentTreeUrl', 'parentTreeUrl',
]), 'leftPanelCollapsed',
...mapGetters([
'isCollapsed',
]), ]),
colSpanCondition() { colSpanCondition() {
return this.isCollapsed ? undefined : 3; return this.leftPanelCollapsed ? undefined : 3;
}, },
}, },
methods: { methods: {
......
...@@ -27,16 +27,18 @@ export default { ...@@ -27,16 +27,18 @@ export default {
methods: { methods: {
...mapActions([ ...mapActions([
'setFileActive',
'closeFile', 'closeFile',
]), ]),
clickFile(tab) {
this.$router.push(`/project${tab.url}`);
},
}, },
}; };
</script> </script>
<template> <template>
<li <li
@click="setFileActive(tab)" @click="clickFile(tab)"
> >
<button <button
type="button" type="button"
......
import Vue from 'vue';
import VueRouter from 'vue-router';
import store from './stores';
import flash from '../flash';
import {
getTreeEntry,
} from './stores/utils';
Vue.use(VueRouter);
/**
* Routes below /-/ide/:
/project/h5bp/html5-boilerplate/blob/master
/project/h5bp/html5-boilerplate/blob/master/app/js/test.js
/project/h5bp/html5-boilerplate/mr/123
/project/h5bp/html5-boilerplate/mr/123/app/js/test.js
/workspace/123
/workspace/project/h5bp/html5-boilerplate/blob/my-special-branch
/workspace/project/h5bp/html5-boilerplate/mr/123
/ = /workspace
/settings
*/
// Unfortunately Vue Router doesn't work without at least a fake component
// If you do only data handling
const EmptyRouterComponent = {
render(createElement) {
return createElement('div');
},
};
const router = new VueRouter({
mode: 'history',
base: `${gon.relative_url_root}/-/ide/`,
routes: [
{
path: '/project/:namespace/:project',
component: EmptyRouterComponent,
children: [
{
path: ':targetmode/:branch/*',
component: EmptyRouterComponent,
},
{
path: 'mr/:mrid',
component: EmptyRouterComponent,
},
],
},
],
});
router.beforeEach((to, from, next) => {
if (to.params.namespace && to.params.project) {
store.dispatch('getProjectData', {
namespace: to.params.namespace,
projectId: to.params.project,
})
.then(() => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`;
if (to.params.branch) {
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: to.params.branch,
});
store.dispatch('getTreeData', {
projectId: fullProjectId,
branch: to.params.branch,
endpoint: `/tree/${to.params.branch}`,
})
.then(() => {
if (to.params[0]) {
const treeEntry = getTreeEntry(store, `${to.params.namespace}/${to.params.project}/${to.params.branch}`, to.params[0]);
if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry);
}
}
})
.catch((e) => {
flash('Error while loading the branch files. Please try again.');
throw e;
});
}
})
.catch((e) => {
flash('Error while loading the project data. Please try again.');
throw e;
});
}
next();
});
export default router;
import Vue from 'vue'; import Vue from 'vue';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { convertPermissionToBoolean } from '../lib/utils/common_utils'; import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import Repo from './components/repo.vue'; import ide from './components/ide.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 store from './stores';
import router from './ide_router';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
import ContextualSidebar from '../contextual_sidebar';
function initRepo(el) { function initIde(el) {
if (!el) return null; if (!el) return null;
return new Vue({ return new Vue({
el, el,
store, store,
router,
components: { components: {
repo: Repo, ide,
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -26,11 +27,6 @@ function initRepo(el) { ...@@ -26,11 +27,6 @@ function initRepo(el) {
const data = el.dataset; const data = el.dataset;
this.setInitialData({ this.setInitialData({
project: {
id: data.projectId,
name: data.projectName,
url: data.projectUrl,
},
endpoints: { endpoints: {
rootEndpoint: data.url, rootEndpoint: data.url,
newMergeRequestUrl: data.newMergeRequestUrl, newMergeRequestUrl: data.newMergeRequestUrl,
...@@ -38,69 +34,22 @@ function initRepo(el) { ...@@ -38,69 +34,22 @@ function initRepo(el) {
}, },
canCommit: convertPermissionToBoolean(data.canCommit), canCommit: convertPermissionToBoolean(data.canCommit),
onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch), onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
currentRef: data.ref,
path: data.currentPath, path: data.currentPath,
currentBranch: data.currentBranch,
isRoot: convertPermissionToBoolean(data.root), isRoot: convertPermissionToBoolean(data.root),
isInitialRoot: convertPermissionToBoolean(data.root), isInitialRoot: convertPermissionToBoolean(data.root),
}); });
}, },
render(createElement) { render(createElement) {
return createElement('repo'); return createElement('ide');
},
});
}
function initRepoEditButton(el) {
return new Vue({
el,
store,
components: {
repoEditButton: RepoEditButton,
},
render(createElement) {
return createElement('repo-edit-button');
},
});
}
function initNewDropdown(el) {
return new Vue({
el,
store,
components: {
newDropdown,
},
render(createElement) {
return createElement('new-dropdown');
},
});
}
function initNewBranchForm() {
const el = document.querySelector('.js-new-branch-dropdown');
if (!el) return null;
return new Vue({
el,
components: {
newBranchForm,
},
store,
render(createElement) {
return createElement('new-branch-form');
}, },
}); });
} }
const repo = document.getElementById('repo'); const ideElement = document.getElementById('ide');
const editButton = document.querySelector('.editable-mode');
const newDropdownHolder = document.querySelector('.js-new-dropdown');
Vue.use(Translate); Vue.use(Translate);
initRepo(repo); initIde(ideElement);
initRepoEditButton(editButton);
initNewBranchForm(); const contextualSidebar = new ContextualSidebar();
initNewDropdown(newDropdownHolder); contextualSidebar.bindEvents();
...@@ -28,6 +28,14 @@ export default class Model { ...@@ -28,6 +28,14 @@ export default class Model {
return this.model.uri.toString(); return this.model.uri.toString();
} }
get language() {
return this.model.getModeId();
}
get eol() {
return this.model.getEOL() === '\n' ? 'LF' : 'CRLF';
}
get path() { get path() {
return this.file.path; return this.file.path;
} }
......
...@@ -22,6 +22,11 @@ export default class Editor { ...@@ -22,6 +22,11 @@ export default class Editor {
this.modelManager = new ModelManager(this.monaco), this.modelManager = new ModelManager(this.monaco),
this.decorationsController = new DecorationsController(this), this.decorationsController = new DecorationsController(this),
); );
this.debouncedUpdate = _.debounce(() => {
this.updateDimensions();
}, 200);
window.addEventListener('resize', this.debouncedUpdate, false);
} }
createInstance(domElement) { createInstance(domElement) {
...@@ -32,6 +37,9 @@ export default class Editor { ...@@ -32,6 +37,9 @@ export default class Editor {
readOnly: false, readOnly: false,
contextmenu: true, contextmenu: true,
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
}), }),
this.dirtyDiffController = new DirtyDiffController( this.dirtyDiffController = new DirtyDiffController(
this.modelManager, this.decorationsController, this.modelManager, this.decorationsController,
...@@ -70,10 +78,32 @@ export default class Editor { ...@@ -70,10 +78,32 @@ export default class Editor {
dispose() { dispose() {
this.disposable.dispose(); this.disposable.dispose();
window.removeEventListener('resize', this.debouncedUpdate);
// dispose main monaco instance // dispose main monaco instance
if (this.instance) { if (this.instance) {
this.instance = null; this.instance = null;
} }
} }
updateDimensions() {
this.instance.layout();
}
setPosition({ lineNumber, column }) {
this.instance.revealPositionInCenter({
lineNumber,
column,
});
this.instance.setPosition({
lineNumber,
column,
});
}
onPositionChange(cb) {
this.disposable.add(
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
);
}
} }
...@@ -23,8 +23,11 @@ export default { ...@@ -23,8 +23,11 @@ export default {
return Vue.http.get(file.rawPath, { params: { format: 'json' } }) return Vue.http.get(file.rawPath, { params: { format: 'json' } })
.then(res => res.text()); .then(res => res.text());
}, },
getBranchData(projectId, currentBranch) { getProjectData(namespace, project) {
return Api.branchSingle(projectId, currentBranch); return Api.project(`${namespace}/${project}`);
},
getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId);
}, },
createBranch(projectId, payload) { createBranch(projectId, payload) {
const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId); const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
......
...@@ -6,9 +6,11 @@ import * as types from './mutation_types'; ...@@ -6,9 +6,11 @@ import * as types from './mutation_types';
export const redirectToUrl = (_, url) => visitUrl(url); export const redirectToUrl = (_, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); export const setInitialData = ({ commit }, data) =>
commit(types.SET_INITIAL_DATA, data);
export const closeDiscardPopup = ({ commit }) => commit(types.TOGGLE_DISCARD_POPUP, false); export const closeDiscardPopup = ({ commit }) =>
commit(types.TOGGLE_DISCARD_POPUP, false);
export const discardAllChanges = ({ commit, getters, dispatch }) => { export const discardAllChanges = ({ commit, getters, dispatch }) => {
const changedFiles = getters.changedFiles; const changedFiles = getters.changedFiles;
...@@ -26,7 +28,10 @@ export const closeAllFiles = ({ state, dispatch }) => { ...@@ -26,7 +28,10 @@ export const closeAllFiles = ({ state, dispatch }) => {
state.openFiles.forEach(file => dispatch('closeFile', { file })); state.openFiles.forEach(file => dispatch('closeFile', { file }));
}; };
export const toggleEditMode = ({ state, commit, getters, dispatch }, force = false) => { export const toggleEditMode = (
{ state, commit, getters, dispatch },
force = false,
) => {
const changedFiles = getters.changedFiles; const changedFiles = getters.changedFiles;
if (changedFiles.length && !force) { if (changedFiles.length && !force) {
...@@ -50,67 +55,105 @@ export const toggleBlobView = ({ commit, state }) => { ...@@ -50,67 +55,105 @@ export const toggleBlobView = ({ commit, state }) => {
} }
}; };
export const checkCommitStatus = ({ state }) => service.getBranchData( export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
state.project.id, if (side === 'left') {
state.currentBranch, commit(types.SET_LEFT_PANEL_COLLAPSED, collapsed);
) } else {
.then((data) => { commit(types.SET_RIGHT_PANEL_COLLAPSED, collapsed);
const { id } = data.commit; }
};
if (state.currentRef !== id) {
return true;
}
return false; export const checkCommitStatus = ({ state }) =>
}) service
.catch(() => flash('Error checking branch data. Please try again.')); .getBranchData(state.currentProjectId, state.currentBranchId)
.then((data) => {
export const commitChanges = ({ commit, state, dispatch, getters }, { payload, newMr }) => const { id } = data.commit;
service.commit(state.project.id, payload) const selectedBranch =
.then((data) => { state.projects[state.currentProjectId].branches[state.currentBranchId];
const { branch } = payload;
if (!data.short_id) { if (selectedBranch.workingReference !== id) {
flash(data.message); return true;
return; }
}
return false;
})
.catch(() => flash('Error checking branch data. Please try again.'));
export const commitChanges = (
{ commit, state, dispatch, getters },
{ payload, newMr },
) =>
service
.commit(state.currentProjectId, payload)
.then((data) => {
const { branch } = payload;
if (!data.short_id) {
flash(data.message);
return;
}
const selectedProject = state.projects[state.currentProjectId];
const lastCommit = {
commit_path: `${selectedProject.web_url}/commit/${data.id}`,
commit: {
message: data.message,
authored_date: data.committed_date,
},
};
flash(
`Your changes have been committed. Commit ${data.short_id} with ${
data.stats.additions
} additions, ${data.stats.deletions} deletions.`,
'notice',
);
if (newMr) {
dispatch(
'redirectToUrl',
`${
selectedProject.web_url
}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
);
} else {
commit(types.SET_BRANCH_WORKING_REFERENCE, {
projectId: state.currentProjectId,
branchId: state.currentBranchId,
reference: data.id,
});
const lastCommit = { getters.changedFiles.forEach((entry) => {
commit_path: `${state.project.url}/commit/${data.id}`, commit(types.SET_LAST_COMMIT_DATA, {
commit: { entry,
message: data.message, lastCommit,
authored_date: data.committed_date, });
},
};
flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
if (newMr) {
dispatch('redirectToUrl', `${state.endpoints.newMergeRequestUrl}${branch}`);
} else {
commit(types.SET_COMMIT_REF, data.id);
getters.changedFiles.forEach((entry) => {
commit(types.SET_LAST_COMMIT_DATA, {
entry,
lastCommit,
}); });
});
dispatch('discardAllChanges'); dispatch('discardAllChanges');
dispatch('closeAllFiles'); dispatch('closeAllFiles');
dispatch('toggleEditMode');
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
}) })
.catch(() => flash('Error committing changes. Please try again.')); .catch(() => flash('Error committing changes. Please try again.'));
export const createTempEntry = ({ state, dispatch }, { name, type, content = '', base64 = false }) => { export const createTempEntry = (
{ state, dispatch },
{ projectId, branchId, parent, name, type, content = '', base64 = false },
) => {
const selectedParent = parent || state.trees[`${projectId}/${branchId}`];
if (type === 'tree') { if (type === 'tree') {
dispatch('createTempTree', name); dispatch('createTempTree', {
projectId,
branchId,
parent: selectedParent,
name,
});
} else if (type === 'blob') { } else if (type === 'blob') {
dispatch('createTempFile', { dispatch('createTempFile', {
tree: state, projectId,
branchId,
parent: selectedParent,
name, name,
base64, base64,
content, content,
...@@ -118,17 +161,6 @@ export const createTempEntry = ({ state, dispatch }, { name, type, content = '', ...@@ -118,17 +161,6 @@ export const createTempEntry = ({ state, dispatch }, { name, type, 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 = () => { export const scrollToTab = () => {
Vue.nextTick(() => { Vue.nextTick(() => {
const tabs = document.getElementById('tabs'); const tabs = document.getElementById('tabs');
...@@ -143,4 +175,5 @@ export const scrollToTab = () => { ...@@ -143,4 +175,5 @@ export const scrollToTab = () => {
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project';
export * from './actions/branch'; export * from './actions/branch';
import service from '../../services';
import flash from '../../../flash';
import * as types from '../mutation_types';
export const getBranchData = (
{ commit, state, dispatch },
{ projectId, branchId, force = false } = {},
) => new Promise((resolve, reject) => {
if ((typeof state.projects[`${projectId}`] === 'undefined' ||
!state.projects[`${projectId}`].branches[branchId])
|| force) {
service.getBranchData(`${projectId}`, branchId)
.then((data) => {
const { id } = data.commit;
commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
resolve(data);
})
.catch(() => {
flash('Error loading branch data. Please try again.');
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});
export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
state.currentProjectId,
{
branch,
ref: state.currentBranchId,
},
)
.then(res => res.json())
.then((data) => {
const branchName = data.name;
const url = location.href.replace(state.currentBranchId, branchName);
if (this.$router) this.$router.push(url);
commit(types.SET_CURRENT_BRANCH, branchName);
});
...@@ -2,9 +2,9 @@ import { normalizeHeaders } from '../../../lib/utils/common_utils'; ...@@ -2,9 +2,9 @@ import { normalizeHeaders } from '../../../lib/utils/common_utils';
import flash from '../../../flash'; import flash from '../../../flash';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router';
import { import {
findEntry, findEntry,
pushState,
setPageTitle, setPageTitle,
createTemp, createTemp,
findIndexOfFile, findIndexOfFile,
...@@ -25,7 +25,7 @@ export const closeFile = ({ commit, state, dispatch }, { file, force = false }) ...@@ -25,7 +25,7 @@ export const closeFile = ({ commit, state, dispatch }, { file, force = false })
dispatch('setFileActive', nextFileToOpen); dispatch('setFileActive', nextFileToOpen);
} else if (!state.openFiles.length) { } else if (!state.openFiles.length) {
pushState(file.parentTreeUrl); router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
} }
dispatch('getLastCommitData'); dispatch('getLastCommitData');
...@@ -45,6 +45,9 @@ export const setFileActive = ({ commit, state, getters, dispatch }, file) => { ...@@ -45,6 +45,9 @@ export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
// reset hash for line highlighting // reset hash for line highlighting
location.hash = ''; location.hash = '';
commit(types.SET_CURRENT_PROJECT, file.projectId);
commit(types.SET_CURRENT_BRANCH, file.branchId);
}; };
export const getFileData = ({ state, commit, dispatch }, file) => { export const getFileData = ({ state, commit, dispatch }, file) => {
...@@ -63,8 +66,6 @@ export const getFileData = ({ state, commit, dispatch }, file) => { ...@@ -63,8 +66,6 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
commit(types.TOGGLE_FILE_OPEN, file); commit(types.TOGGLE_FILE_OPEN, file);
dispatch('setFileActive', file); dispatch('setFileActive', file);
commit(types.TOGGLE_LOADING, file); commit(types.TOGGLE_LOADING, file);
pushState(file.url);
}) })
.catch(() => { .catch(() => {
commit(types.TOGGLE_LOADING, file); commit(types.TOGGLE_LOADING, file);
...@@ -82,21 +83,39 @@ export const changeFileContent = ({ commit }, { file, content }) => { ...@@ -82,21 +83,39 @@ export const changeFileContent = ({ commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content }); commit(types.UPDATE_FILE_CONTENT, { file, content });
}; };
export const createTempFile = ({ state, commit, dispatch }, { tree, name, content = '', base64 = '' }) => { export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
};
export const setFileEOL = ({ state, commit }, { eol }) => {
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
};
export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
};
export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
const path = parent.path !== undefined ? parent.path : '';
// We need to do the replacement otherwise the web_url + file.url duplicate
const newUrl = `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${name}`;
const file = createTemp({ const file = createTemp({
name: name.replace(`${state.path}/`, ''), projectId,
path: tree.path, branchId,
name: name.replace(`${path}/`, ''),
path,
type: 'blob', type: 'blob',
level: tree.level !== undefined ? tree.level + 1 : 0, level: parent.level !== undefined ? parent.level + 1 : 0,
changed: true, changed: true,
content, content,
base64, base64,
url: newUrl,
}); });
if (findEntry(tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`); if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
commit(types.CREATE_TMP_FILE, { commit(types.CREATE_TMP_FILE, {
parent: tree, parent,
file, file,
}); });
commit(types.TOGGLE_FILE_OPEN, file); commit(types.TOGGLE_FILE_OPEN, file);
...@@ -106,5 +125,7 @@ export const createTempFile = ({ state, commit, dispatch }, { tree, name, conten ...@@ -106,5 +125,7 @@ export const createTempFile = ({ state, commit, dispatch }, { tree, name, conten
dispatch('toggleEditMode', true); dispatch('toggleEditMode', true);
} }
router.push(`/project${file.url}`);
return Promise.resolve(file); return Promise.resolve(file);
}; };
import service from '../../services';
import flash from '../../../flash';
import * as types from '../mutation_types';
// eslint-disable-next-line import/prefer-default-export
export const getProjectData = (
{ commit, state, dispatch },
{ namespace, projectId, force = false } = {},
) => new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) {
service.getProjectData(namespace, projectId)
.then(res => res.data)
.then((data) => {
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
resolve(data);
})
.catch(() => {
flash('Error loading project data. Please try again.');
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
});
} else {
resolve(state.projects[`${namespace}/${projectId}`]);
}
});
...@@ -3,8 +3,8 @@ import { normalizeHeaders } from '../../../lib/utils/common_utils'; ...@@ -3,8 +3,8 @@ import { normalizeHeaders } from '../../../lib/utils/common_utils';
import flash from '../../../flash'; import flash from '../../../flash';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router';
import { import {
pushState,
setPageTitle, setPageTitle,
findEntry, findEntry,
createTemp, createTemp,
...@@ -13,59 +13,69 @@ import { ...@@ -13,59 +13,69 @@ import {
export const getTreeData = ( export const getTreeData = (
{ commit, state, dispatch }, { commit, state, dispatch },
{ endpoint = state.endpoints.rootEndpoint, tree = state } = {}, { endpoint, tree = null, projectId, branch, force = false } = {},
) => { ) => new Promise((resolve, reject) => {
commit(types.TOGGLE_LOADING, tree); // We already have the base tree so we resolve immediately
if (!tree && state.trees[`${projectId}/${branch}`] && !force) {
service.getTreeData(endpoint) resolve();
.then((res) => { } else {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); if (tree) commit(types.TOGGLE_LOADING, tree);
const selectedProject = state.projects[projectId];
setPageTitle(pageTitle); // We are merging the web_url that we got on the project info with the endpoint
// we got on the tree entry, as both contain the projectId, we replace it in the tree endpoint
return res.json(); const completeEndpoint = selectedProject.web_url + (endpoint).replace(projectId, '');
}) if (completeEndpoint && (!tree || !tree.tempFile)) {
.then((data) => { service.getTreeData(completeEndpoint)
const prevLastCommitPath = tree.lastCommitPath; .then((res) => {
if (!state.isInitialRoot) { const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
commit(types.SET_ROOT, data.path === '/');
} setPageTitle(pageTitle);
return res.json();
})
.then((data) => {
if (!state.isInitialRoot) {
commit(types.SET_ROOT, data.path === '/');
}
dispatch('updateDirectoryData', { data, tree }); dispatch('updateDirectoryData', { data, tree, projectId, branch });
commit(types.SET_PARENT_TREE_URL, data.parent_tree_url); const selectedTree = tree || state.trees[`${projectId}/${branch}`];
commit(types.SET_LAST_COMMIT_URL, { tree, url: data.last_commit_path });
commit(types.TOGGLE_LOADING, tree);
if (prevLastCommitPath !== null) { commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
dispatch('getLastCommitData', tree); commit(types.SET_LAST_COMMIT_URL, { tree: selectedTree, url: data.last_commit_path });
} if (tree) commit(types.TOGGLE_LOADING, selectedTree);
pushState(endpoint); const prevLastCommitPath = selectedTree.lastCommitPath;
}) if (prevLastCommitPath !== null) {
.catch(() => { dispatch('getLastCommitData', selectedTree);
flash('Error loading tree data. Please try again.'); }
commit(types.TOGGLE_LOADING, tree); resolve(data);
}); })
}; .catch((e) => {
flash('Error loading tree data. Please try again.');
if (tree) commit(types.TOGGLE_LOADING, tree);
reject(e);
});
} else {
resolve();
}
}
});
export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => { export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
if (tree.opened) { if (tree.opened) {
// send empty data to clear the tree // send empty data to clear the tree
const data = { trees: [], blobs: [], submodules: [] }; const data = { trees: [], blobs: [], submodules: [] };
pushState(tree.parentTreeUrl); dispatch('updateDirectoryData', { data, tree, projectId: tree.projectId, branchId: tree.branchId });
commit(types.SET_PREVIOUS_URL, tree.parentTreeUrl);
dispatch('updateDirectoryData', { data, tree });
} else { } else {
commit(types.SET_PREVIOUS_URL, endpoint); dispatch('getTreeData', { endpoint, tree, projectId: tree.projectId, branch: tree.branchId });
dispatch('getTreeData', { endpoint, tree });
} }
commit(types.TOGGLE_TREE_OPEN, tree); commit(types.TOGGLE_TREE_OPEN, tree);
}; };
export const clickedTreeRow = ({ commit, dispatch }, row) => { export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
if (row.type === 'tree') { if (row.type === 'tree') {
dispatch('toggleTreeOpen', { dispatch('toggleTreeOpen', {
endpoint: row.url, endpoint: row.url,
...@@ -73,7 +83,6 @@ export const clickedTreeRow = ({ commit, dispatch }, row) => { ...@@ -73,7 +83,6 @@ export const clickedTreeRow = ({ commit, dispatch }, row) => {
}); });
} else if (row.type === 'submodule') { } else if (row.type === 'submodule') {
commit(types.TOGGLE_LOADING, row); commit(types.TOGGLE_LOADING, row);
visitUrl(row.url); visitUrl(row.url);
} else if (row.type === 'blob' && row.opened) { } else if (row.type === 'blob' && row.opened) {
dispatch('setFileActive', row); dispatch('setFileActive', row);
...@@ -82,43 +91,46 @@ export const clickedTreeRow = ({ commit, dispatch }, row) => { ...@@ -82,43 +91,46 @@ export const clickedTreeRow = ({ commit, dispatch }, row) => {
} }
}; };
export const createTempTree = ({ state, commit, dispatch }, name) => { export const createTempTree = (
let tree = state; { state, commit, dispatch },
{ projectId, branchId, parent, name },
) => {
let selectedTree = parent;
const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/'); const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
dirNames.forEach((dirName) => { dirNames.forEach((dirName) => {
const foundEntry = findEntry(tree, 'tree', dirName); const foundEntry = findEntry(selectedTree.tree, 'tree', dirName);
if (!foundEntry) { if (!foundEntry) {
const path = selectedTree.path !== undefined ? selectedTree.path : '';
const tmpEntry = createTemp({ const tmpEntry = createTemp({
projectId,
branchId,
name: dirName, name: dirName,
path: tree.path, path,
type: 'tree', type: 'tree',
level: tree.level !== undefined ? tree.level + 1 : 0, level: selectedTree.level !== undefined ? selectedTree.level + 1 : 0,
tree: [],
url: `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${dirName}`,
}); });
commit(types.CREATE_TMP_TREE, { commit(types.CREATE_TMP_TREE, {
parent: tree, parent: selectedTree,
tmpEntry, tmpEntry,
}); });
commit(types.TOGGLE_TREE_OPEN, tmpEntry); commit(types.TOGGLE_TREE_OPEN, tmpEntry);
tree = tmpEntry; router.push(`/project${tmpEntry.url}`);
selectedTree = tmpEntry;
} else { } else {
tree = foundEntry; selectedTree = foundEntry;
} }
}); });
if (tree.tempFile) {
dispatch('createTempFile', {
tree,
name: '.gitkeep',
});
}
}; };
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => { export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (tree.lastCommitPath === null || getters.isCollapsed) return; if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
service.getTreeLastCommit(tree.lastCommitPath) service.getTreeLastCommit(tree.lastCommitPath)
.then((res) => { .then((res) => {
...@@ -130,7 +142,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s ...@@ -130,7 +142,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
}) })
.then((data) => { .then((data) => {
data.forEach((lastCommit) => { data.forEach((lastCommit) => {
const entry = findEntry(tree, lastCommit.type, lastCommit.file_name); const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
if (entry) { if (entry) {
commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit }); commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
...@@ -142,11 +154,24 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s ...@@ -142,11 +154,24 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.')); .catch(() => flash('Error fetching log data.'));
}; };
export const updateDirectoryData = ({ commit, state }, { data, tree }) => { export const updateDirectoryData = (
const level = tree.level !== undefined ? tree.level + 1 : 0; { commit, state },
{ data, tree, projectId, branch },
) => {
if (!tree) {
const existingTree = state.trees[`${projectId}/${branch}`];
if (!existingTree) {
commit(types.CREATE_TREE, { treePath: `${projectId}/${branch}` });
}
}
const selectedTree = tree || state.trees[`${projectId}/${branch}`];
const level = selectedTree.level !== undefined ? selectedTree.level + 1 : 0;
const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl; const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
const createEntry = (entry, type) => createOrMergeEntry({ const createEntry = (entry, type) => createOrMergeEntry({
tree, tree: selectedTree,
projectId: `${projectId}`,
branchId: branch,
entry, entry,
level, level,
type, type,
...@@ -159,5 +184,5 @@ export const updateDirectoryData = ({ commit, state }, { data, tree }) => { ...@@ -159,5 +184,5 @@ export const updateDirectoryData = ({ commit, state }, { data, tree }) => {
...data.blobs.map(b => createEntry(b, 'blob')), ...data.blobs.map(b => createEntry(b, 'blob')),
]; ];
commit(types.SET_DIRECTORY_DATA, { tree, data: formattedData }); commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: formattedData });
}; };
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 changedFiles = state => state.openFiles.filter(file => file.changed);
export const activeFile = state => state.openFiles.find(file => file.active); export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const activeFileExtension = (state) => { export const activeFileExtension = (state) => {
const file = activeFile(state); const file = activeFile(state);
return file ? `.${file.path.split('.').pop()}` : ''; return file ? `.${file.path.split('.').pop()}` : '';
}; };
export const isCollapsed = state => !!state.openFiles.length;
export const canEditFile = (state) => { export const canEditFile = (state) => {
const currentActiveFile = activeFile(state); const currentActiveFile = activeFile(state);
const openedFiles = state.openFiles;
return state.canCommit && return state.canCommit &&
state.onTopOfBranch && (currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
openedFiles.length &&
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
}; };
export const addedFiles = state => changedFiles(state).filter(f => f.tempFile); export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
......
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA'; export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const TOGGLE_LOADING = 'TOGGLE_LOADING'; 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_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
export const SET_ROOT = 'SET_ROOT'; export const SET_ROOT = 'SET_ROOT';
export const SET_PREVIOUS_URL = 'SET_PREVIOUS_URL';
export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA'; export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
// Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
// Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
// Tree mutation types // Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA'; export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN'; export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
export const CREATE_TMP_TREE = 'CREATE_TMP_TREE'; export const CREATE_TMP_TREE = 'CREATE_TMP_TREE';
export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL'; export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
export const CREATE_TREE = 'CREATE_TREE';
// File mutation types // File mutation types
export const SET_FILE_DATA = 'SET_FILE_DATA'; export const SET_FILE_DATA = 'SET_FILE_DATA';
...@@ -18,6 +29,9 @@ export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN'; ...@@ -18,6 +29,9 @@ export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE'; export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA'; export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT'; export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
export const SET_FILE_EOL = 'SET_FILE_EOL';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES'; export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const CREATE_TMP_FILE = 'CREATE_TMP_FILE'; export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
...@@ -28,3 +42,4 @@ export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE'; ...@@ -28,3 +42,4 @@ export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP'; export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH'; export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
import * as types from './mutation_types'; import * as types from './mutation_types';
import projectMutations from './mutations/project';
import fileMutations from './mutations/file'; import fileMutations from './mutations/file';
import treeMutations from './mutations/tree'; import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch'; import branchMutations from './mutations/branch';
...@@ -32,29 +33,32 @@ export default { ...@@ -32,29 +33,32 @@ export default {
discardPopupOpen, discardPopupOpen,
}); });
}, },
[types.SET_COMMIT_REF](state, ref) {
Object.assign(state, {
currentRef: ref,
});
},
[types.SET_ROOT](state, isRoot) { [types.SET_ROOT](state, isRoot) {
Object.assign(state, { Object.assign(state, {
isRoot, isRoot,
isInitialRoot: isRoot, isInitialRoot: isRoot,
}); });
}, },
[types.SET_PREVIOUS_URL](state, previousUrl) { [types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
Object.assign(state, {
leftPanelCollapsed: collapsed,
});
},
[types.SET_RIGHT_PANEL_COLLAPSED](state, collapsed) {
Object.assign(state, { Object.assign(state, {
previousUrl, rightPanelCollapsed: collapsed,
}); });
}, },
[types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) { [types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
Object.assign(entry.lastCommit, { Object.assign(entry.lastCommit, {
id: lastCommit.commit.id,
url: lastCommit.commit_path, url: lastCommit.commit_path,
message: lastCommit.commit.message, message: lastCommit.commit.message,
author: lastCommit.commit.author_name,
updatedAt: lastCommit.commit.authored_date, updatedAt: lastCommit.commit.authored_date,
}); });
}, },
...projectMutations,
...fileMutations, ...fileMutations,
...treeMutations, ...treeMutations,
...branchMutations, ...branchMutations,
......
import * as types from '../mutation_types';
export default {
[types.SET_CURRENT_BRANCH](state, currentBranchId) {
Object.assign(state, {
currentBranchId,
});
},
[types.SET_BRANCH](state, { projectPath, branchName, branch }) {
// Add client side properties
Object.assign(branch, {
treeId: `${projectPath}/${branchName}`,
active: true,
workingReference: '',
});
Object.assign(state.projects[projectPath], {
branches: {
[branchName]: branch,
},
});
},
[types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) {
Object.assign(state.projects[projectId].branches[branchId], {
workingReference: reference,
});
},
};
...@@ -6,6 +6,10 @@ export default { ...@@ -6,6 +6,10 @@ export default {
Object.assign(file, { Object.assign(file, {
active, active,
}); });
Object.assign(state, {
selectedFile: file,
});
}, },
[types.TOGGLE_FILE_OPEN](state, file) { [types.TOGGLE_FILE_OPEN](state, file) {
Object.assign(file, { Object.assign(file, {
...@@ -42,6 +46,22 @@ export default { ...@@ -42,6 +46,22 @@ export default {
changed, changed,
}); });
}, },
[types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
Object.assign(file, {
fileLanguage,
});
},
[types.SET_FILE_EOL](state, { file, eol }) {
Object.assign(file, {
eol,
});
},
[types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
Object.assign(file, {
editorRow,
editorColumn,
});
},
[types.DISCARD_FILE_CHANGES](state, file) { [types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, { Object.assign(file, {
content: '', content: '',
......
import * as types from '../mutation_types';
export default {
[types.SET_CURRENT_PROJECT](state, currentProjectId) {
Object.assign(state, {
currentProjectId,
});
},
[types.SET_PROJECT](state, { projectPath, project }) {
// Add client side properties
Object.assign(project, {
tree: [],
branches: {},
active: true,
});
Object.assign(state, {
projects: Object.assign({}, state.projects, {
[projectPath]: project,
}),
});
},
};
...@@ -6,6 +6,15 @@ export default { ...@@ -6,6 +6,15 @@ export default {
opened: !tree.opened, opened: !tree.opened,
}); });
}, },
[types.CREATE_TREE](state, { treePath }) {
Object.assign(state, {
trees: Object.assign({}, state.trees, {
[treePath]: {
tree: [],
},
}),
});
},
[types.SET_DIRECTORY_DATA](state, { data, tree }) { [types.SET_DIRECTORY_DATA](state, { data, tree }) {
Object.assign(tree, { Object.assign(tree, {
tree: data, tree: data,
......
export default () => ({ export default () => ({
canCommit: false, canCommit: false,
currentBranch: '', currentProjectId: '',
currentBlobView: 'repo-preview', currentBranchId: '',
currentRef: '', currentBlobView: 'repo-editor',
discardPopupOpen: false, discardPopupOpen: false,
editMode: false, editMode: true,
endpoints: {}, endpoints: {},
isRoot: false, isRoot: false,
isInitialRoot: false, isInitialRoot: false,
...@@ -12,13 +12,11 @@ export default () => ({ ...@@ -12,13 +12,11 @@ export default () => ({
loading: false, loading: false,
onTopOfBranch: false, onTopOfBranch: false,
openFiles: [], openFiles: [],
selectedFile: null,
path: '', path: '',
project: {
id: 0,
name: '',
url: '',
},
parentTreeUrl: '', parentTreeUrl: '',
previousUrl: '', trees: {},
tree: [], projects: {},
leftPanelCollapsed: false,
rightPanelCollapsed: true,
}); });
...@@ -2,6 +2,8 @@ export const dataStructure = () => ({ ...@@ -2,6 +2,8 @@ export const dataStructure = () => ({
id: '', id: '',
key: '', key: '',
type: '', type: '',
projectId: '',
branchId: '',
name: '', name: '',
url: '', url: '',
path: '', path: '',
...@@ -15,9 +17,11 @@ export const dataStructure = () => ({ ...@@ -15,9 +17,11 @@ export const dataStructure = () => ({
changed: false, changed: false,
lastCommitPath: '', lastCommitPath: '',
lastCommit: { lastCommit: {
id: '',
url: '', url: '',
message: '', message: '',
updatedAt: '', updatedAt: '',
author: '',
}, },
tree_url: '', tree_url: '',
blamePath: '', blamePath: '',
...@@ -31,11 +35,17 @@ export const dataStructure = () => ({ ...@@ -31,11 +35,17 @@ export const dataStructure = () => ({
parentTreeUrl: '', parentTreeUrl: '',
renderError: false, renderError: false,
base64: false, base64: false,
editorRow: 1,
editorColumn: 1,
fileLanguage: '',
eol: '',
}); });
export const decorateData = (entity) => { export const decorateData = (entity) => {
const { const {
id, id,
projectId,
branchId,
type, type,
url, url,
name, name,
...@@ -56,6 +66,8 @@ export const decorateData = (entity) => { ...@@ -56,6 +66,8 @@ export const decorateData = (entity) => {
return { return {
...dataStructure(), ...dataStructure(),
id, id,
projectId,
branchId,
key: `${name}-${type}-${id}`, key: `${name}-${type}-${id}`,
type, type,
name, name,
...@@ -75,24 +87,51 @@ export const decorateData = (entity) => { ...@@ -75,24 +87,51 @@ export const decorateData = (entity) => {
}; };
}; };
export const findEntry = (state, type, name) => state.tree.find( /*
Takes the multi-dimensional tree and returns a flattened array.
This allows for the table to recursively render the table rows but keeps the data
structure nested to make it easier to add new files/directories.
*/
export const treeList = (state, treeId) => {
const baseTree = state.trees[treeId];
if (baseTree) {
const mapTree = arr => (!arr.tree || !arr.tree.length ?
[] : _.map(arr.tree, a => [a, mapTree(a)]));
return _.chain(baseTree.tree)
.map(arr => [arr, mapTree(arr)])
.flatten()
.value();
}
return [];
};
export const getTree = state => (namespace, projectId, branch) => state.trees[`${namespace}/${projectId}/${branch}`];
export const getTreeEntry = (store, treeId, path) => {
const fileList = treeList(store.state, treeId);
return fileList ? fileList.find(file => file.path === path) : null;
};
export const findEntry = (tree, type, name) => tree.find(
f => f.type === type && f.name === name, f => f.type === type && f.name === name,
); );
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path); export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
export const setPageTitle = (title) => { export const setPageTitle = (title) => {
document.title = title; document.title = title;
}; };
export const pushState = (url) => { export const createTemp = ({
history.pushState({ url }, '', url); projectId, branchId, name, path, type, level, changed, content, base64, url,
}; }) => {
export const createTemp = ({ name, path, type, level, changed, content, base64 }) => {
const treePath = path ? `${path}/${name}` : name; const treePath = path ? `${path}/${name}` : name;
return decorateData({ return decorateData({
id: new Date().getTime().toString(), id: new Date().getTime().toString(),
projectId,
branchId,
name, name,
type, type,
tempFile: true, tempFile: true,
...@@ -104,11 +143,18 @@ export const createTemp = ({ name, path, type, level, changed, content, base64 } ...@@ -104,11 +143,18 @@ export const createTemp = ({ name, path, type, level, changed, content, base64 }
level, level,
base64, base64,
renderError: base64, renderError: base64,
url,
}); });
}; };
export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level }) => { export const createOrMergeEntry = ({ tree,
const found = findEntry(tree, type, entry.name); projectId,
branchId,
entry,
type,
parentTreeUrl,
level }) => {
const found = findEntry(tree.tree || tree, type, entry.name);
if (found) { if (found) {
return Object.assign({}, found, { return Object.assign({}, found, {
...@@ -120,6 +166,8 @@ export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level }) ...@@ -120,6 +166,8 @@ export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level })
return decorateData({ return decorateData({
...entry, ...entry,
projectId,
branchId,
type, type,
parentTreeUrl, parentTreeUrl,
level, level,
......
...@@ -32,7 +32,7 @@ export default { ...@@ -32,7 +32,7 @@ export default {
showInlineEditButton: { showInlineEditButton: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: true,
}, },
showDeleteButton: { showDeleteButton: {
type: Boolean, type: Boolean,
......
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
v-tooltip v-tooltip
v-if="showInlineEditButton && canUpdate" v-if="showInlineEditButton && canUpdate"
type="button" type="button"
class="btn btn-default btn-edit btn-svg" class="btn btn-default btn-edit btn-svg js-issuable-edit"
v-html="pencilIcon" v-html="pencilIcon"
title="Edit title and description" title="Edit title and description"
data-placement="bottom" data-placement="bottom"
......
import Vue from 'vue'; import Vue from 'vue';
import eventHub from './event_hub';
import issuableApp from './components/app.vue'; import issuableApp from './components/app.vue';
import '../vue_shared/vue_resource_interceptor'; import '../vue_shared/vue_resource_interceptor';
...@@ -7,12 +6,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -7,12 +6,6 @@ document.addEventListener('DOMContentLoaded', () => {
const initialDataEl = document.getElementById('js-issuable-app-initial-data'); const initialDataEl = document.getElementById('js-issuable-app-initial-data');
const props = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"')); const props = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"'));
$('.js-issuable-edit').on('click', (e) => {
e.preventDefault();
eventHub.$emit('open.form');
});
return new Vue({ return new Vue({
el: document.getElementById('js-issuable-app'), el: document.getElementById('js-issuable-app'),
components: { components: {
......
/* 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 ContextualSidebar from './contextual_sidebar'; import ContextualSidebar from './contextual_sidebar';
import initFlyOutNav from './fly_out_nav'; import initFlyOutNav from './fly_out_nav';
(function() { function hideEndFade($scrollingTabs) {
var hideEndFade; $scrollingTabs.each(function scrollTabsLoop() {
const $this = $(this);
$this.siblings('.fade-right').toggleClass('scrolling', $this.width() < $this.prop('scrollWidth'));
});
}
hideEndFade = function($scrollingTabs) { export default function initLayoutNav() {
return $scrollingTabs.each(function() { const contextualSidebar = new ContextualSidebar();
var $this; contextualSidebar.bindEvents();
$this = $(this);
return $this.siblings('.fade-right').toggleClass('scrolling', $this.width() < $this.prop('scrollWidth')); initFlyOutNav();
});
};
$(document).on('init.scrolling-tabs', () => { $(document).on('init.scrolling-tabs', () => {
const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized'); const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized');
$scrollingTabs.addClass('is-initialized'); $scrollingTabs.addClass('is-initialized');
hideEndFade($scrollingTabs); $(window).on('resize.nav', () => {
$(window).off('resize.nav').on('resize.nav', function() { hideEndFade($scrollingTabs);
return hideEndFade($scrollingTabs); }).trigger('resize.nav');
});
$scrollingTabs.off('scroll').on('scroll', function(event) { $scrollingTabs.on('scroll', function tabsScrollEvent() {
var $this, currentPosition, maxPosition; const $this = $(this);
$this = $(this); const currentPosition = $this.scrollLeft();
currentPosition = $this.scrollLeft(); const maxPosition = $this.prop('scrollWidth') - $this.outerWidth();
maxPosition = $this.prop('scrollWidth') - $this.outerWidth();
$this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0); $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0);
return $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1); $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1);
}); });
$scrollingTabs.each(function () { $scrollingTabs.each(function scrollTabsEachLoop() {
var $this = $(this); const $this = $(this);
var scrollingTabWidth = $this.width(); const scrollingTabWidth = $this.width();
var $active = $this.find('.active'); const $active = $this.find('.active');
var activeWidth = $active.width(); const activeWidth = $active.width();
if ($active.length) { if ($active.length) {
var offset = $active.offset().left + activeWidth; const offset = $active.offset().left + activeWidth;
if (offset > scrollingTabWidth - 30) { if (offset > scrollingTabWidth - 30) {
var scrollLeft = scrollingTabWidth / 2; const scrollLeft = (offset - (scrollingTabWidth / 2)) - (activeWidth / 2);
scrollLeft = (offset - scrollLeft) - (activeWidth / 2);
$this.scrollLeft(scrollLeft); $this.scrollLeft(scrollLeft);
} }
} }
}); });
}); }).trigger('init.scrolling-tabs');
}
$(() => {
const contextualSidebar = new ContextualSidebar();
contextualSidebar.bindEvents();
initFlyOutNav();
});
}).call(window);
...@@ -2,7 +2,7 @@ import timeago from 'timeago.js'; ...@@ -2,7 +2,7 @@ import timeago from 'timeago.js';
import dateFormat from 'vendor/date.format'; import dateFormat from 'vendor/date.format';
import { pluralize } from './text_utility'; import { pluralize } from './text_utility';
import { import {
lang, languageCode,
s__, s__,
} from '../../locale'; } from '../../locale';
...@@ -24,7 +24,15 @@ export const getDayName = date => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', ' ...@@ -24,7 +24,15 @@ export const getDayName = date => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', '
*/ */
export const formatDate = datetime => dateFormat(datetime, 'mmm d, yyyy h:MMtt Z'); export const formatDate = datetime => dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
/**
* Timeago uses underscores instead of dashes to separate language from country code.
*
* see https://github.com/hustcc/timeago.js/tree/v3.0.0/locales
*/
const timeagoLanguageCode = languageCode().replace(/-/g, '_');
let timeagoInstance; let timeagoInstance;
/** /**
* Sets a timeago Instance * Sets a timeago Instance
*/ */
...@@ -67,8 +75,8 @@ export function getTimeago() { ...@@ -67,8 +75,8 @@ export function getTimeago() {
][index]; ][index];
}; };
timeago.register(lang, locale); timeago.register(timeagoLanguageCode, locale);
timeago.register(`${lang}-remaining`, localeRemaining); timeago.register(`${timeagoLanguageCode}-remaining`, localeRemaining);
timeagoInstance = timeago(); timeagoInstance = timeago();
} }
...@@ -83,7 +91,7 @@ export const renderTimeago = ($els) => { ...@@ -83,7 +91,7 @@ export const renderTimeago = ($els) => {
const timeagoEls = $els || document.querySelectorAll('.js-timeago-render'); const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
// timeago.js sets timeouts internally for each timeago value to be updated in real time // timeago.js sets timeouts internally for each timeago value to be updated in real time
getTimeago().render(timeagoEls, lang); getTimeago().render(timeagoEls, timeagoLanguageCode);
}; };
/** /**
...@@ -118,7 +126,7 @@ export const timeFor = (time, expiredLabel) => { ...@@ -118,7 +126,7 @@ export const timeFor = (time, expiredLabel) => {
if (new Date(time) < new Date()) { if (new Date(time) < new Date()) {
return expiredLabel || s__('Timeago|Past due'); return expiredLabel || s__('Timeago|Past due');
} }
return getTimeago().format(time, `${lang}-remaining`).trim(); return getTimeago().format(time, `${timeagoLanguageCode}-remaining`).trim();
}; };
export const getDayDifference = (a, b) => { export const getDayDifference = (a, b) => {
......
...@@ -41,7 +41,7 @@ import Flash, { removeFlashClickListener } from './flash'; ...@@ -41,7 +41,7 @@ import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown'; import './gl_dropdown';
import initTodoToggle from './header'; import initTodoToggle from './header';
import initImporterStatus from './importer_status'; import initImporterStatus from './importer_status';
import './layout_nav'; import initLayoutNav from './layout_nav';
import LazyLoader from './lazy_loader'; import LazyLoader from './lazy_loader';
import './line_highlighter'; import './line_highlighter';
import initLogoAnimation from './logo'; import initLogoAnimation from './logo';
...@@ -89,6 +89,7 @@ $(function () { ...@@ -89,6 +89,7 @@ $(function () {
var fitSidebarForSize; var fitSidebarForSize;
initBreadcrumbs(); initBreadcrumbs();
initLayoutNav();
initImporterStatus(); initImporterStatus();
initTodoToggle(); initTodoToggle();
initLogoAnimation(); initLogoAnimation();
...@@ -261,8 +262,6 @@ $(function () { ...@@ -261,8 +262,6 @@ $(function () {
renderTimeago(); renderTimeago();
$(document).trigger('init.scrolling-tabs');
$('form.filter-form').on('submit', function (event) { $('form.filter-form').on('submit', function (event) {
const link = document.createElement('a'); const link = document.createElement('a');
link.href = this.action; link.href = this.action;
......
<script> <script>
import d3 from 'd3'; import { scaleLinear, scaleTime } from 'd3-scale';
import { axisLeft, axisBottom } from 'd3-axis';
import { max, extent } from 'd3-array';
import { select } from 'd3-selection';
import GraphLegend from './graph/legend.vue'; import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue'; import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue'; import GraphDeployment from './graph/deployment.vue';
...@@ -7,10 +10,12 @@ ...@@ -7,10 +10,12 @@
import MonitoringMixin from '../mixins/monitoring_mixins'; import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import measurements from '../utils/measurements'; import measurements from '../utils/measurements';
import { timeScaleFormat, bisectDate } from '../utils/date_time_formatters'; import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters';
import createTimeSeries from '../utils/multiple_time_series'; import createTimeSeries from '../utils/multiple_time_series';
import bp from '../../breakpoints'; import bp from '../../breakpoints';
const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select };
export default { export default {
props: { props: {
graphData: { graphData: {
...@@ -156,25 +161,22 @@ ...@@ -156,25 +161,22 @@
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20; this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
} }
const axisXScale = d3.time.scale() const axisXScale = d3.scaleTime()
.range([0, this.graphWidth - 70]); .range([0, this.graphWidth - 70]);
const axisYScale = d3.scale.linear() const axisYScale = d3.scaleLinear()
.range([this.graphHeight - this.graphHeightOffset, 0]); .range([this.graphHeight - this.graphHeightOffset, 0]);
const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []); const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
axisXScale.domain(d3.extent(allValues, d => d.time)); axisXScale.domain(d3.extent(allValues, d => d.time));
axisYScale.domain([0, d3.max(allValues.map(d => d.value))]); axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
const xAxis = d3.svg.axis() const xAxis = d3.axisBottom()
.scale(axisXScale) .scale(axisXScale)
.ticks(d3.time.minute, 60) .tickFormat(timeScaleFormat);
.tickFormat(timeScaleFormat)
.orient('bottom');
const yAxis = d3.svg.axis() const yAxis = d3.axisLeft()
.scale(axisYScale) .scale(axisYScale)
.ticks(measurements.yTicks) .ticks(measurements.yTicks);
.orient('left');
d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis); d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis);
......
import d3 from 'd3'; import { timeFormat as time } from 'd3-time-format';
import { timeSecond, timeMinute, timeHour, timeDay, timeMonth, timeYear } from 'd3-time';
import { bisector } from 'd3-array';
export const dateFormat = d3.time.format('%b %-d, %Y'); const d3 = { time, bisector, timeSecond, timeMinute, timeHour, timeDay, timeMonth, timeYear };
export const dateFormatWithName = d3.time.format('%a, %b %-d');
export const timeFormat = d3.time.format('%-I:%M%p'); export const dateFormat = d3.time('%b %-d, %Y');
export const timeFormat = d3.time('%-I:%M%p');
export const dateFormatWithName = d3.time('%a, %b %-d');
export const bisectDate = d3.bisector(d => d.time).left; export const bisectDate = d3.bisector(d => d.time).left;
export const timeScaleFormat = d3.time.format.multi([ export function timeScaleFormat(date) {
['.%L', d => d.getMilliseconds()], let formatFunction;
[':%S', d => d.getSeconds()], if (d3.timeSecond(date) < date) {
['%-I:%M', d => d.getMinutes()], formatFunction = d3.time('.%L');
['%-I %p', d => d.getHours()], } else if (d3.timeMinute(date) < date) {
['%a %-d', d => d.getDay() && d.getDate() !== 1], formatFunction = d3.time(':%S');
['%b %-d', d => d.getDate() !== 1], } else if (d3.timeHour(date) < date) {
['%B', d => d.getMonth()], formatFunction = d3.time('%-I:%M');
['%Y', () => true], } else if (d3.timeDay(date) < date) {
]); formatFunction = d3.time('%-I %p');
} else if (d3.timeWeek(date) < date) {
formatFunction = d3.time('%a %d');
} else if (d3.timeMonth(date) < date) {
formatFunction = d3.time('%b %d');
} else if (d3.timeYear(date) < date) {
formatFunction = d3.time('%B');
} else {
formatFunction = d3.time('%Y');
}
return formatFunction(date);
}
import d3 from 'd3';
import _ from 'underscore'; import _ from 'underscore';
import { scaleLinear, scaleTime } from 'd3-scale';
import { line, area, curveLinear } from 'd3-shape';
import { extent, max } from 'd3-array';
import { timeMinute } from 'd3-time';
const d3 = { scaleLinear, scaleTime, line, area, curveLinear, extent, max, timeMinute };
const defaultColorPalette = { const defaultColorPalette = {
blue: ['#1f78d1', '#8fbce8'], blue: ['#1f78d1', '#8fbce8'],
...@@ -38,27 +43,27 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom ...@@ -38,27 +43,27 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
let lineColor = ''; let lineColor = '';
let areaColor = ''; let areaColor = '';
const timeSeriesScaleX = d3.time.scale() const timeSeriesScaleX = d3.scaleTime()
.range([0, graphWidth - 70]); .range([0, graphWidth - 70]);
const timeSeriesScaleY = d3.scale.linear() const timeSeriesScaleY = d3.scaleLinear()
.range([graphHeight - graphHeightOffset, 0]); .range([graphHeight - graphHeightOffset, 0]);
timeSeriesScaleX.domain(xDom); timeSeriesScaleX.domain(xDom);
timeSeriesScaleX.ticks(d3.time.minute, 60); timeSeriesScaleX.ticks(d3.timeMinute, 60);
timeSeriesScaleY.domain(yDom); timeSeriesScaleY.domain(yDom);
const defined = d => !isNaN(d.value) && d.value != null; const defined = d => !isNaN(d.value) && d.value != null;
const lineFunction = d3.svg.line() const lineFunction = d3.line()
.defined(defined) .defined(defined)
.interpolate('linear') .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate
.x(d => timeSeriesScaleX(d.time)) .x(d => timeSeriesScaleX(d.time))
.y(d => timeSeriesScaleY(d.value)); .y(d => timeSeriesScaleY(d.value));
const areaFunction = d3.svg.area() const areaFunction = d3.area()
.defined(defined) .defined(defined)
.interpolate('linear') .curve(d3.curveLinear)
.x(d => timeSeriesScaleX(d.time)) .x(d => timeSeriesScaleX(d.time))
.y0(graphHeight - graphHeightOffset) .y0(graphHeight - graphHeightOffset)
.y1(d => timeSeriesScaleY(d.value)); .y1(d => timeSeriesScaleY(d.value));
......
...@@ -6,11 +6,12 @@ export default class NewCommitForm { ...@@ -6,11 +6,12 @@ export default class NewCommitForm {
this.branchName = form.find('.js-branch-name'); this.branchName = form.find('.js-branch-name');
this.originalBranch = form.find('.js-original-branch'); this.originalBranch = form.find('.js-original-branch');
this.createMergeRequest = form.find('.js-create-merge-request'); this.createMergeRequest = form.find('.js-create-merge-request');
this.createMergeRequestContainer = form.find('.js-create-merge-request-container'); this.createMergeRequestContainer = form.find(
'.js-create-merge-request-container',
);
this.branchName.keyup(this.renderDestination); this.branchName.keyup(this.renderDestination);
this.renderDestination(); this.renderDestination();
} }
renderDestination() { renderDestination() {
var different; var different;
different = this.branchName.val() !== this.originalBranch.val(); different = this.branchName.val() !== this.originalBranch.val();
...@@ -23,6 +24,6 @@ export default class NewCommitForm { ...@@ -23,6 +24,6 @@ export default class NewCommitForm {
this.createMergeRequestContainer.hide(); this.createMergeRequestContainer.hide();
this.createMergeRequest.prop('checked', false); this.createMergeRequest.prop('checked', false);
} }
return this.wasDifferent = different; return (this.wasDifferent = different);
} }
} }
...@@ -117,12 +117,10 @@ ...@@ -117,12 +117,10 @@
}()); }());
markdownPreview = new window.MarkdownPreview(); markdownPreview = new window.MarkdownPreview();
previewButtonSelector = '.js-md-preview-button'; previewButtonSelector = '.js-md-preview-button';
writeButtonSelector = '.js-md-write-button'; writeButtonSelector = '.js-md-write-button';
lastTextareaPreviewed = null; lastTextareaPreviewed = null;
const markdownToolbar = $('.md-header-toolbar');
$.fn.setupMarkdownPreview = function () { $.fn.setupMarkdownPreview = function () {
var $form = $(this); var $form = $(this);
...@@ -146,6 +144,7 @@ ...@@ -146,6 +144,7 @@
// toggle content // toggle content
$form.find('.md-write-holder').hide(); $form.find('.md-write-holder').hide();
$form.find('.md-preview-holder').show(); $form.find('.md-preview-holder').show();
markdownToolbar.removeClass('active');
markdownPreview.showPreview($form); markdownPreview.showPreview($form);
}); });
...@@ -167,6 +166,7 @@ ...@@ -167,6 +166,7 @@
$form.find('.md-write-holder').show(); $form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus(); $form.find('textarea.markdown-area').focus();
$form.find('.md-preview-holder').hide(); $form.find('.md-preview-holder').hide();
markdownToolbar.addClass('active');
markdownPreview.hideReferencedCommands($form); markdownPreview.hideReferencedCommands($form);
}); });
......
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 = ({ state, commit }, branch) => service.createBranch(
state.project.id,
{
branch,
ref: state.currentBranch,
},
).then(res => res.json())
.then((data) => {
const branchName = data.name;
const url = location.href.replace(state.currentBranch, branchName);
pushState(url);
commit(types.SET_CURRENT_BRANCH, branchName);
});
import * as types from '../mutation_types';
export default {
[types.SET_CURRENT_BRANCH](state, currentBranch) {
Object.assign(state, {
currentBranch,
});
},
};
...@@ -51,7 +51,10 @@ export default class Shortcuts { ...@@ -51,7 +51,10 @@ export default class Shortcuts {
} }
onToggleHelp(e) { onToggleHelp(e) {
e.preventDefault(); if (e.preventDefault) {
e.preventDefault();
}
Shortcuts.toggleHelp(this.enabledHelp); Shortcuts.toggleHelp(this.enabledHelp);
} }
...@@ -112,6 +115,9 @@ export default class Shortcuts { ...@@ -112,6 +115,9 @@ export default class Shortcuts {
static focusSearch(e) { static focusSearch(e) {
$('#search').focus(); $('#search').focus();
e.preventDefault();
if (e.preventDefault) {
e.preventDefault();
}
} }
} }
import _ from 'underscore'; import _ from 'underscore';
import d3 from 'd3'; import { scaleLinear, scaleThreshold } from 'd3-scale';
import { select } from 'd3-selection';
import { getDayName, getDayDifference } from '../lib/utils/datetime_utility'; import { getDayName, getDayDifference } from '../lib/utils/datetime_utility';
const d3 = { select, scaleLinear, scaleThreshold };
const LOADING_HTML = ` const LOADING_HTML = `
<div class="text-center"> <div class="text-center">
<i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i> <i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i>
...@@ -28,7 +31,7 @@ function formatTooltipText({ date, count }) { ...@@ -28,7 +31,7 @@ function formatTooltipText({ date, count }) {
return `${contribText}<br />${dateDayName} ${dateText}`; return `${contribText}<br />${dateDayName} ${dateText}`;
} }
const initColorKey = () => d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]); const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]);
export default class ActivityCalendar { export default class ActivityCalendar {
constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) { constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) {
...@@ -205,7 +208,7 @@ export default class ActivityCalendar { ...@@ -205,7 +208,7 @@ export default class ActivityCalendar {
initColor() { initColor() {
const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
return d3.scale.threshold().domain([0, 10, 20, 30]).range(colorRange); return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange);
} }
clickDay(stamp) { clickDay(stamp) {
......
...@@ -62,7 +62,7 @@ export default { ...@@ -62,7 +62,7 @@ export default {
return this.mr.hasCI; return this.mr.hasCI;
}, },
shouldRenderRelatedLinks() { shouldRenderRelatedLinks() {
return !!this.mr.relatedLinks; return !!this.mr.relatedLinks && !this.mr.isNothingToMergeState;
}, },
shouldRenderDeployments() { shouldRenderDeployments() {
return this.mr.deployments.length; return this.mr.deployments.length;
......
import { stateKey } from './state_maps';
export default function deviseState(data) { export default function deviseState(data) {
if (data.project_archived) { if (data.project_archived) {
return 'archived'; return stateKey.archived;
} else if (data.branch_missing) { } else if (data.branch_missing) {
return 'missingBranch'; return stateKey.missingBranch;
} else if (!data.commits_count) { } else if (!data.commits_count) {
return 'nothingToMerge'; return stateKey.nothingToMerge;
} else if (this.mergeStatus === 'unchecked') { } else if (this.mergeStatus === 'unchecked') {
return 'checking'; return stateKey.checking;
} else if (data.has_conflicts) { } else if (data.has_conflicts) {
return 'conflicts'; return stateKey.conflicts;
} else if (data.work_in_progress) { } else if (data.work_in_progress) {
return 'workInProgress'; return stateKey.workInProgress;
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) { } else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
return 'pipelineFailed'; return stateKey.pipelineFailed;
} else if (this.hasMergeableDiscussionsState) { } else if (this.hasMergeableDiscussionsState) {
return 'unresolvedDiscussions'; return stateKey.unresolvedDiscussions;
} else if (this.isPipelineBlocked) { } else if (this.isPipelineBlocked) {
return 'pipelineBlocked'; return stateKey.pipelineBlocked;
} else if (this.hasSHAChanged) { } else if (this.hasSHAChanged) {
return 'shaMismatch'; return stateKey.shaMismatch;
} else if (this.mergeWhenPipelineSucceeds) { } else if (this.mergeWhenPipelineSucceeds) {
return this.mergeError ? 'autoMergeFailed' : 'mergeWhenPipelineSucceeds'; return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds;
} else if (!this.canMerge) { } else if (!this.canMerge) {
return 'notAllowedToMerge'; return stateKey.notAllowedToMerge;
} else if (this.canBeMerged) { } else if (this.canBeMerged) {
return 'readyToMerge'; return stateKey.readyToMerge;
} }
return null; return null;
} }
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import { getStateKey } from '../dependencies'; import { getStateKey } from '../dependencies';
import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility'; import { formatDate } from '../../lib/utils/datetime_utility';
export default class MergeRequestStore { export default class MergeRequestStore {
...@@ -120,6 +121,10 @@ export default class MergeRequestStore { ...@@ -120,6 +121,10 @@ export default class MergeRequestStore {
} }
} }
get isNothingToMergeState() {
return this.state === stateKey.nothingToMerge;
}
static getEventObject(event) { static getEventObject(event) {
return { return {
author: MergeRequestStore.getAuthorObject(event), author: MergeRequestStore.getAuthorObject(event),
......
...@@ -31,6 +31,23 @@ const statesToShowHelpWidget = [ ...@@ -31,6 +31,23 @@ const statesToShowHelpWidget = [
'autoMergeFailed', 'autoMergeFailed',
]; ];
export const stateKey = {
archived: 'archived',
missingBranch: 'missingBranch',
nothingToMerge: 'nothingToMerge',
checking: 'checking',
conflicts: 'conflicts',
workInProgress: 'workInProgress',
pipelineFailed: 'pipelineFailed',
unresolvedDiscussions: 'unresolvedDiscussions',
pipelineBlocked: 'pipelineBlocked',
shaMismatch: 'shaMismatch',
autoMergeFailed: 'autoMergeFailed',
mergeWhenPipelineSucceeds: 'mergeWhenPipelineSucceeds',
notAllowedToMerge: 'notAllowedToMerge',
readyToMerge: 'readyToMerge',
};
export default { export default {
stateToComponentMap, stateToComponentMap,
statesToShowHelpWidget, statesToShowHelpWidget,
......
...@@ -72,7 +72,9 @@ ...@@ -72,7 +72,9 @@
Preview Preview
</a> </a>
</li> </li>
<li class="md-header-toolbar"> <li
class="md-header-toolbar"
:class="{ active: !previewMarkdown }">
<toolbar-button <toolbar-button
tag="**" tag="**"
button-title="Add bold text" button-title="Add bold text"
......
<script>
/* This is a re-usable vue component for rendering a project avatar that
does not need to link to the project's profile. The image and an optional
tooltip can be configured by props passed to this component.
Sample configuration:
<project-avatar-image
:lazy="true"
:img-src="projectAvatarSrc"
:img-alt="tooltipText"
:tooltip-text="tooltipText"
tooltip-placement="top"
/>
*/
import defaultAvatarUrl from 'images/no_avatar.png';
import { placeholderImage } from '../../../lazy_loader';
import tooltip from '../../directives/tooltip';
export default {
name: 'ProjectAvatarImage',
props: {
lazy: {
type: Boolean,
required: false,
default: false,
},
imgSrc: {
type: String,
required: false,
default: defaultAvatarUrl,
},
cssClasses: {
type: String,
required: false,
default: '',
},
imgAlt: {
type: String,
required: false,
default: 'project avatar',
},
size: {
type: Number,
required: false,
default: 20,
},
tooltipText: {
type: String,
required: false,
default: '',
},
tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
},
directives: {
tooltip,
},
computed: {
// API response sends null when gravatar is disabled and
// we provide an empty string when we use it inside project avatar link.
// In both cases we should render the defaultAvatarUrl
sanitizedSource() {
return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
},
resultantSrcAttribute() {
return this.lazy ? placeholderImage : this.sanitizedSource;
},
tooltipContainer() {
return this.tooltipText ? 'body' : null;
},
avatarSizeClass() {
return `s${this.size}`;
},
},
};
</script>
<template>
<img
v-tooltip
class="avatar"
:class="{
lazy,
[avatarSizeClass]: true,
[cssClasses]: true
}"
:src="resultantSrcAttribute"
:width="size"
:height="size"
:alt="imgAlt"
:data-src="sanitizedSource"
:data-container="tooltipContainer"
:data-placement="tooltipPlacement"
:title="tooltipText"
/>
</template>
...@@ -9,12 +9,6 @@ ...@@ -9,12 +9,6 @@
padding-left: $contextual-sidebar-width; padding-left: $contextual-sidebar-width;
} }
// Override position: absolute
.right-sidebar {
position: fixed;
height: calc(100% - #{$header-height});
}
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header { .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
padding: 10px 0 15px; padding: 10px 0 15px;
} }
...@@ -29,7 +23,6 @@ ...@@ -29,7 +23,6 @@
.context-header { .context-header {
position: relative; position: relative;
margin-right: 2px; margin-right: 2px;
width: $contextual-sidebar-width;
a { a {
transition: padding $sidebar-transition-duration; transition: padding $sidebar-transition-duration;
......
...@@ -16,27 +16,18 @@ ...@@ -16,27 +16,18 @@
@mixin set-visible { @mixin set-visible {
transform: translateY(0); transform: translateY(0);
visibility: visible; display: block;
opacity: 1;
transition-duration: 100ms, 150ms, 25ms;
transition-delay: 35ms, 50ms, 25ms;
} }
@mixin set-invisible { @mixin set-invisible {
transform: translateY(-10px); transform: translateY(-10px);
visibility: hidden; display: none;
opacity: 0;
transition-property: opacity, transform, visibility;
transition-duration: 70ms, 250ms, 250ms;
transition-timing-function: linear, $dropdown-animation-timing;
transition-delay: 25ms, 50ms, 0ms;
} }
.open { .open {
.dropdown-menu, .dropdown-menu,
.dropdown-menu-nav { .dropdown-menu-nav {
@include set-visible; @include set-visible;
display: block;
min-height: 40px; min-height: 40px;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
...@@ -55,6 +46,11 @@ ...@@ -55,6 +46,11 @@
} }
} }
// Get search dropdown to line up with other nav dropdowns
.search-input-container .dropdown-menu {
margin-top: 11px;
}
.dropdown-toggle { .dropdown-toggle {
padding: 6px 8px 6px 10px; padding: 6px 8px 6px 10px;
background-color: $white-light; background-color: $white-light;
...@@ -214,7 +210,6 @@ ...@@ -214,7 +210,6 @@
.dropdown-menu, .dropdown-menu,
.dropdown-menu-nav { .dropdown-menu-nav {
@include set-invisible; @include set-invisible;
display: block;
position: absolute; position: absolute;
width: auto; width: auto;
top: 100%; top: 100%;
......
...@@ -74,7 +74,7 @@ ...@@ -74,7 +74,7 @@
} }
.md-header-tab { .md-header-tab {
@media(max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
flex: 1; flex: 1;
width: 100%; width: 100%;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
...@@ -82,16 +82,23 @@ ...@@ -82,16 +82,23 @@
} }
} }
.md-header-toolbar { .nav-links {
margin-left: auto; li.md-header-toolbar {
margin-left: auto;
display: none;
@media(max-width: $screen-xs-max) { &.active {
flex: none; display: block;
display: flex;
justify-content: center; @media (max-width: $screen-xs-max) {
width: 100%; flex: none;
padding-top: $gl-padding-top; display: flex;
padding-bottom: $gl-padding-top; justify-content: center;
width: 100%;
padding-top: $gl-padding-top;
padding-bottom: $gl-padding-top;
}
}
} }
} }
...@@ -175,7 +182,7 @@ ...@@ -175,7 +182,7 @@
margin-left: $gl-padding; margin-left: $gl-padding;
margin-right: -5px; margin-right: -5px;
@media(max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
} }
...@@ -239,7 +246,7 @@ ...@@ -239,7 +246,7 @@
} }
} }
@media(max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.atwho-view-ul { .atwho-view-ul {
width: 350px; width: 350px;
} }
......
...@@ -90,11 +90,6 @@ ...@@ -90,11 +90,6 @@
.right-sidebar { .right-sidebar {
border-left: 1px solid $border-color; border-left: 1px solid $border-color;
height: calc(100% - #{$header-height}); height: calc(100% - #{$header-height});
&.affix {
position: fixed;
top: $header-height;
}
} }
.with-performance-bar .right-sidebar.affix { .with-performance-bar .right-sidebar.affix {
......
...@@ -219,6 +219,7 @@ $gl-input-padding: 10px; ...@@ -219,6 +219,7 @@ $gl-input-padding: 10px;
$gl-vert-padding: 6px; $gl-vert-padding: 6px;
$gl-padding-top: 10px; $gl-padding-top: 10px;
$gl-sidebar-padding: 22px; $gl-sidebar-padding: 22px;
$gl-bar-padding: 3px;
/* /*
* Misc * Misc
......
...@@ -122,7 +122,7 @@ ...@@ -122,7 +122,7 @@
} }
.right-sidebar { .right-sidebar {
position: absolute; position: fixed;
top: $header-height; top: $header-height;
bottom: 0; bottom: 0;
right: 0; right: 0;
...@@ -502,7 +502,7 @@ ...@@ -502,7 +502,7 @@
top: $header-height + $performance-bar-height; top: $header-height + $performance-bar-height;
.issuable-sidebar { .issuable-sidebar {
height: calc(100% - #{$header-height} - #{$performance-bar-height}); height: calc(100% - #{$performance-bar-height});
} }
} }
......
...@@ -22,9 +22,10 @@ ...@@ -22,9 +22,10 @@
} }
} }
.multi-file { .ide-view {
display: flex; display: flex;
height: calc(100vh - 145px); height: calc(100vh - #{$header-height});
color: $almost-black;
border-top: 1px solid $white-dark; border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
...@@ -35,12 +36,47 @@ ...@@ -35,12 +36,47 @@
} }
} }
.with-performance-bar .ide-view {
height: calc(100vh - #{$header-height});
}
.ide-file-list { .ide-file-list {
flex: 1; flex: 1;
overflow: scroll;
.file { .file {
cursor: pointer; cursor: pointer;
&.file-open {
background: $white-normal;
}
.repo-file-name {
white-space: nowrap;
text-overflow: ellipsis;
}
.unsaved-icon {
color: $indigo-700;
float: right;
font-size: smaller;
line-height: 20px;
}
.repo-new-btn {
display: none;
margin-top: -4px;
margin-bottom: -4px;
}
&:hover {
.repo-new-btn {
display: block;
}
.unsaved-icon {
display: none;
}
}
} }
a { a {
...@@ -55,10 +91,9 @@ ...@@ -55,10 +91,9 @@
.multi-file-table-name, .multi-file-table-name,
.multi-file-table-col-commit-message { .multi-file-table-col-commit-message {
white-space: nowrap; overflow: visible;
overflow: hidden;
text-overflow: ellipsis;
max-width: 0; max-width: 0;
padding: 6px 12px;
} }
.multi-file-table-name { .multi-file-table-name {
...@@ -66,6 +101,7 @@ ...@@ -66,6 +101,7 @@
} }
.multi-file-table-col-commit-message { .multi-file-table-col-commit-message {
white-space: nowrap;
width: 50%; width: 50%;
} }
...@@ -79,7 +115,7 @@ ...@@ -79,7 +115,7 @@
.multi-file-tabs { .multi-file-tabs {
display: flex; display: flex;
overflow: scroll; overflow-x: auto;
background-color: $white-normal; background-color: $white-normal;
box-shadow: inset 0 -1px $white-dark; box-shadow: inset 0 -1px $white-dark;
...@@ -128,9 +164,38 @@ ...@@ -128,9 +164,38 @@
height: 0; height: 0;
} }
.blob-editor-container {
flex: 1;
height: 0;
display: flex;
flex-direction: column;
justify-content: center;
.vertical-center {
min-height: auto;
}
}
.multi-file-editor-holder {
height: 100%;
}
.multi-file-editor-btn-group { .multi-file-editor-btn-group {
padding: $grid-size; padding: $gl-bar-padding $gl-padding;
border-top: 1px solid $white-dark; border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
background: $white-light;
}
.ide-status-bar {
padding: $gl-bar-padding $gl-padding;
background: $white-light;
display: flex;
justify-content: space-between;
svg {
vertical-align: middle;
}
} }
// Not great, but this is to deal with our current output // Not great, but this is to deal with our current output
...@@ -138,10 +203,6 @@ ...@@ -138,10 +203,6 @@
height: 100%; height: 100%;
overflow: scroll; overflow: scroll;
.blob-viewer {
height: 100%;
}
.file-content.code { .file-content.code {
display: flex; display: flex;
...@@ -162,18 +223,101 @@ ...@@ -162,18 +223,101 @@
} }
} }
.file-content.blob-no-preview {
a {
margin-left: auto;
margin-right: auto;
}
}
.multi-file-commit-panel { .multi-file-commit-panel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
width: 290px; width: 290px;
padding: $gl-padding; padding: 0;
background-color: $gray-light; background-color: $gray-light;
border-left: 1px solid $white-dark; border-left: 1px solid $white-dark;
.projects-sidebar {
display: flex;
flex-direction: column;
}
.multi-file-commit-panel-inner {
display: flex;
flex: 1;
flex-direction: column;
}
.multi-file-commit-panel-inner-scroll {
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
}
&.is-collapsed { &.is-collapsed {
width: 60px; width: 60px;
padding: 0;
.multi-file-commit-list {
padding-top: $gl-padding;
overflow: hidden;
}
.multi-file-context-bar-icon {
align-items: center;
svg {
float: none;
margin: 0;
}
}
}
.branch-container {
border-left: 4px solid $indigo-700;
margin-bottom: $gl-bar-padding;
}
.branch-header {
background: $white-dark;
display: flex;
}
.branch-header-title {
flex: 1;
padding: $grid-size $gl-padding;
color: $indigo-700;
font-weight: $gl-font-weight-bold;
svg {
vertical-align: middle;
}
}
.branch-header-btns {
padding: $gl-vert-padding $gl-padding;
}
.left-collapse-btn {
display: none;
background: $gray-light;
text-align: left;
border-top: 1px solid $white-dark;
svg {
vertical-align: middle;
}
}
}
.multi-file-context-bar-icon {
padding: 10px;
svg {
margin-right: 10px;
float: left;
} }
} }
...@@ -186,9 +330,9 @@ ...@@ -186,9 +330,9 @@
.multi-file-commit-panel-header { .multi-file-commit-panel-header {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 0 12px;
margin-bottom: 12px; margin-bottom: 12px;
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding 0;
&.is-collapsed { &.is-collapsed {
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
...@@ -197,23 +341,33 @@ ...@@ -197,23 +341,33 @@
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
.multi-file-commit-panel-collapse-btn {
margin-right: auto;
margin-left: auto;
border-left: 0;
}
} }
} }
.multi-file-commit-panel-collapse-btn { .multi-file-commit-panel-header-title {
padding-top: 0; display: flex;
padding-bottom: 0; flex: 1;
margin-left: auto; padding: $gl-btn-padding;
font-size: 20px;
&.is-collapsed { svg {
margin-right: auto; margin-right: $gl-btn-padding;
} }
} }
.multi-file-commit-panel-collapse-btn {
border-left: 1px solid $white-dark;
}
.multi-file-commit-list { .multi-file-commit-list {
flex: 1; flex: 1;
overflow: scroll; overflow: auto;
padding: $gl-padding;
} }
.multi-file-commit-list-item { .multi-file-commit-list-item {
...@@ -244,7 +398,7 @@ ...@@ -244,7 +398,7 @@
} }
.multi-file-commit-form { .multi-file-commit-form {
padding-top: 12px; padding: $gl-padding;
border-top: 1px solid $white-dark; border-top: 1px solid $white-dark;
} }
...@@ -295,3 +449,40 @@ ...@@ -295,3 +449,40 @@
} }
} }
} }
.ide-loading {
display: flex;
height: 100vh;
align-items: center;
justify-content: center;
}
.ide-empty-state {
display: flex;
height: 100vh;
align-items: center;
justify-content: center;
}
.repo-new-btn {
.dropdown-toggle svg {
margin-top: -2px;
margin-bottom: 2px;
}
.dropdown-menu {
left: auto;
right: 0;
label {
font-weight: $gl-font-weight-normal;
padding: 5px 8px;
margin-bottom: 0;
}
}
}
.ide-flash-container.flash-container {
margin-top: $header-height;
margin-bottom: 0;
}
...@@ -108,13 +108,6 @@ input[type="checkbox"]:hover { ...@@ -108,13 +108,6 @@ input[type="checkbox"]:hover {
// Custom dropdown positioning // Custom dropdown positioning
.dropdown-menu { .dropdown-menu {
transition-property: opacity, transform;
transition-duration: 250ms, 250ms;
transition-delay: 0ms, 25ms;
transition-timing-function: $dropdown-animation-timing;
transform: translateY(0);
opacity: 0;
display: block;
left: -5px; left: -5px;
} }
...@@ -152,13 +145,6 @@ input[type="checkbox"]:hover { ...@@ -152,13 +145,6 @@ input[type="checkbox"]:hover {
background-color: $nav-badge-bg; background-color: $nav-badge-bg;
border-color: $border-color; border-color: $border-color;
} }
.dropdown-menu {
transition-duration: 100ms, 75ms;
transition-delay: 75ms, 100ms;
transform: translateY(7px);
opacity: 1;
}
} }
&.has-value { &.has-value {
......
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
} }
.axis { .axis {
fill: $stat-graph-axis-fill;
font-size: 10px; font-size: 10px;
} }
...@@ -54,9 +53,7 @@ ...@@ -54,9 +53,7 @@
} }
.selection rect { .selection rect {
fill: $stat-graph-selection-fill;
fill-opacity: 0.1; fill-opacity: 0.1;
stroke: $stat-graph-selection-stroke;
stroke-width: 1px; stroke-width: 1px;
stroke-opacity: 0.4; stroke-opacity: 0.4;
shape-rendering: crispedges; shape-rendering: crispedges;
......
...@@ -8,12 +8,12 @@ class AutocompleteController < ApplicationController ...@@ -8,12 +8,12 @@ class AutocompleteController < ApplicationController
def users def users
@users = AutocompleteUsersFinder.new(params: params, current_user: current_user, project: @project, group: @group).execute @users = AutocompleteUsersFinder.new(params: params, current_user: current_user, project: @project, group: @group).execute
render json: @users, only: [:name, :username, :id], methods: [:avatar_url] render json: UserSerializer.new.represent(@users)
end end
def user def user
@user = User.find(params[:id]) @user = User.find(params[:id])
render json: @user, only: [:name, :username, :id], methods: [:avatar_url] render json: UserSerializer.new.represent(@user)
end end
def projects def projects
......
class IdeController < ApplicationController
layout 'nav_only'
def index
end
end
...@@ -5,9 +5,6 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -5,9 +5,6 @@ class Projects::BlobController < Projects::ApplicationController
include RendersBlob include RendersBlob
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper
# Raised when given an invalid file path
InvalidPathError = Class.new(StandardError)
prepend_before_action :authenticate_user!, only: [:edit] prepend_before_action :authenticate_user!, only: [:edit]
before_action :require_non_empty_project, except: [:new, :create] before_action :require_non_empty_project, except: [:new, :create]
...@@ -61,7 +58,6 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -61,7 +58,6 @@ class Projects::BlobController < Projects::ApplicationController
create_commit(Files::UpdateService, success_path: -> { after_edit_path }, create_commit(Files::UpdateService, success_path: -> { after_edit_path },
failure_view: :edit, failure_view: :edit,
failure_path: project_blob_path(@project, @id)) failure_path: project_blob_path(@project, @id))
rescue Files::UpdateService::FileChangedError rescue Files::UpdateService::FileChangedError
@conflict = true @conflict = true
render :edit render :edit
...@@ -132,7 +128,6 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -132,7 +128,6 @@ class Projects::BlobController < Projects::ApplicationController
def assign_blob_vars def assign_blob_vars
@id = params[:id] @id = params[:id]
@ref, @path = extract_ref(@id) @ref, @path = extract_ref(@id)
rescue InvalidPathError rescue InvalidPathError
render_404 render_404
end end
......
...@@ -39,6 +39,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController ...@@ -39,6 +39,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
params.require(:cluster).permit( params.require(:cluster).permit(
:enabled, :enabled,
:name, :name,
:environment_scope,
provider_gcp_attributes: [ provider_gcp_attributes: [
:gcp_project_id, :gcp_project_id,
:zone, :zone,
......
...@@ -26,6 +26,7 @@ class Projects::Clusters::UserController < Projects::ApplicationController ...@@ -26,6 +26,7 @@ class Projects::Clusters::UserController < Projects::ApplicationController
params.require(:cluster).permit( params.require(:cluster).permit(
:enabled, :enabled,
:name, :name,
:environment_scope,
platform_kubernetes_attributes: [ platform_kubernetes_attributes: [
:namespace, :namespace,
:api_url, :api_url,
......
...@@ -87,6 +87,7 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -87,6 +87,7 @@ class Projects::ClustersController < Projects::ApplicationController
if cluster.managed? if cluster.managed?
params.require(:cluster).permit( params.require(:cluster).permit(
:enabled, :enabled,
:environment_scope,
platform_kubernetes_attributes: [ platform_kubernetes_attributes: [
:namespace :namespace
] ]
...@@ -95,6 +96,7 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -95,6 +96,7 @@ class Projects::ClustersController < Projects::ApplicationController
params.require(:cluster).permit( params.require(:cluster).permit(
:enabled, :enabled,
:name, :name,
:environment_scope,
platform_kubernetes_attributes: [ platform_kubernetes_attributes: [
:api_url, :api_url,
:token, :token,
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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