merge master and resolve conflicts
This commit is contained in:
commit
a2f7936c74
|
@ -33,6 +33,15 @@ rules:
|
||||||
- error
|
- error
|
||||||
- max: 1
|
- max: 1
|
||||||
promise/catch-or-return: error
|
promise/catch-or-return: error
|
||||||
|
no-param-reassign:
|
||||||
|
- error
|
||||||
|
- props: true
|
||||||
|
ignorePropertyModificationsFor:
|
||||||
|
- "acc" # for reduce accumulators
|
||||||
|
- "accumulator" # for reduce accumulators
|
||||||
|
- "el" # for DOM elements
|
||||||
|
- "element" # for DOM elements
|
||||||
|
- "state" # for Vuex mutations
|
||||||
no-underscore-dangle:
|
no-underscore-dangle:
|
||||||
- error
|
- error
|
||||||
- allow:
|
- allow:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-67.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29"
|
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-69.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29"
|
||||||
|
|
||||||
.dedicated-runner: &dedicated-runner
|
.dedicated-runner: &dedicated-runner
|
||||||
retry: 1
|
retry: 1
|
||||||
|
@ -708,7 +708,6 @@ gitlab:assets:compile:
|
||||||
SETUP_DB: "false"
|
SETUP_DB: "false"
|
||||||
SKIP_STORAGE_VALIDATION: "true"
|
SKIP_STORAGE_VALIDATION: "true"
|
||||||
WEBPACK_REPORT: "true"
|
WEBPACK_REPORT: "true"
|
||||||
NO_COMPRESSION: "true"
|
|
||||||
# we override the max_old_space_size to prevent OOM errors
|
# we override the max_old_space_size to prevent OOM errors
|
||||||
NODE_OPTIONS: --max_old_space_size=3584
|
NODE_OPTIONS: --max_old_space_size=3584
|
||||||
script:
|
script:
|
||||||
|
@ -722,6 +721,7 @@ gitlab:assets:compile:
|
||||||
expire_in: 31d
|
expire_in: 31d
|
||||||
paths:
|
paths:
|
||||||
- webpack-report/
|
- webpack-report/
|
||||||
|
- public/assets/
|
||||||
|
|
||||||
karma:
|
karma:
|
||||||
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
|
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
|
||||||
|
|
|
@ -13,3 +13,5 @@ db/ @abrandl @NikolayS
|
||||||
|
|
||||||
# Feature specific owners
|
# Feature specific owners
|
||||||
/ee/lib/gitlab/code_owners/ @reprazent
|
/ee/lib/gitlab/code_owners/ @reprazent
|
||||||
|
/ee/lib/ee/gitlab/auth/ldap/ @dblessing @mkozono
|
||||||
|
/lib/gitlab/auth/ldap/ @dblessing @mkozono
|
||||||
|
|
|
@ -36,7 +36,7 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
|
||||||
- [Release Scoping labels](#release-scoping-labels)
|
- [Release Scoping labels](#release-scoping-labels)
|
||||||
- [Priority labels](#priority-labels)
|
- [Priority labels](#priority-labels)
|
||||||
- [Severity labels](#severity-labels)
|
- [Severity labels](#severity-labels)
|
||||||
- [Severity impact guidance](#severity-impact-guidance)
|
- [Severity impact guidance](#severity-impact-guidance)
|
||||||
- [Label for community contributors](#label-for-community-contributors)
|
- [Label for community contributors](#label-for-community-contributors)
|
||||||
- [Implement design & UI elements](#implement-design--ui-elements)
|
- [Implement design & UI elements](#implement-design--ui-elements)
|
||||||
- [Issue tracker](#issue-tracker)
|
- [Issue tracker](#issue-tracker)
|
||||||
|
|
|
@ -4,4 +4,6 @@ danger.import_dangerfile(path: 'danger/changelog')
|
||||||
danger.import_dangerfile(path: 'danger/specs')
|
danger.import_dangerfile(path: 'danger/specs')
|
||||||
danger.import_dangerfile(path: 'danger/gemfile')
|
danger.import_dangerfile(path: 'danger/gemfile')
|
||||||
danger.import_dangerfile(path: 'danger/database')
|
danger.import_dangerfile(path: 'danger/database')
|
||||||
|
danger.import_dangerfile(path: 'danger/documentation')
|
||||||
danger.import_dangerfile(path: 'danger/frozen_string')
|
danger.import_dangerfile(path: 'danger/frozen_string')
|
||||||
|
danger.import_dangerfile(path: 'danger/commit_messages')
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
0.117.2
|
0.120.0
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
8.1.1
|
8.3.1
|
||||||
|
|
4
Gemfile
4
Gemfile
|
@ -170,7 +170,7 @@ gem 'state_machines-activerecord', '~> 0.5.1'
|
||||||
gem 'acts-as-taggable-on', '~> 5.0'
|
gem 'acts-as-taggable-on', '~> 5.0'
|
||||||
|
|
||||||
# Background jobs
|
# Background jobs
|
||||||
gem 'sidekiq', '~> 5.1'
|
gem 'sidekiq', '~> 5.2.1'
|
||||||
gem 'sidekiq-cron', '~> 0.6.0'
|
gem 'sidekiq-cron', '~> 0.6.0'
|
||||||
gem 'redis-namespace', '~> 1.6.0'
|
gem 'redis-namespace', '~> 1.6.0'
|
||||||
gem 'sidekiq-limit_fetch', '~> 3.4', require: false
|
gem 'sidekiq-limit_fetch', '~> 3.4', require: false
|
||||||
|
@ -425,7 +425,7 @@ group :ed25519 do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Gitaly GRPC client
|
# Gitaly GRPC client
|
||||||
gem 'gitaly-proto', '~> 0.113.0', require: 'gitaly'
|
gem 'gitaly-proto', '~> 0.117.0', require: 'gitaly'
|
||||||
gem 'grpc', '~> 1.11.0'
|
gem 'grpc', '~> 1.11.0'
|
||||||
|
|
||||||
# Locked until https://github.com/google/protobuf/issues/4210 is closed
|
# Locked until https://github.com/google/protobuf/issues/4210 is closed
|
||||||
|
|
17
Gemfile.lock
17
Gemfile.lock
|
@ -133,7 +133,7 @@ GEM
|
||||||
concurrent-ruby (1.0.5)
|
concurrent-ruby (1.0.5)
|
||||||
concurrent-ruby-ext (1.0.5)
|
concurrent-ruby-ext (1.0.5)
|
||||||
concurrent-ruby (= 1.0.5)
|
concurrent-ruby (= 1.0.5)
|
||||||
connection_pool (2.2.1)
|
connection_pool (2.2.2)
|
||||||
crack (0.4.3)
|
crack (0.4.3)
|
||||||
safe_yaml (~> 1.0.0)
|
safe_yaml (~> 1.0.0)
|
||||||
crass (1.0.4)
|
crass (1.0.4)
|
||||||
|
@ -208,7 +208,7 @@ GEM
|
||||||
fast_blank (1.0.0)
|
fast_blank (1.0.0)
|
||||||
fast_gettext (1.6.0)
|
fast_gettext (1.6.0)
|
||||||
ffaker (2.4.0)
|
ffaker (2.4.0)
|
||||||
ffi (1.9.18)
|
ffi (1.9.25)
|
||||||
flipper (0.13.0)
|
flipper (0.13.0)
|
||||||
flipper-active_record (0.13.0)
|
flipper-active_record (0.13.0)
|
||||||
activerecord (>= 3.2, < 6)
|
activerecord (>= 3.2, < 6)
|
||||||
|
@ -276,7 +276,7 @@ GEM
|
||||||
gettext_i18n_rails (>= 0.7.1)
|
gettext_i18n_rails (>= 0.7.1)
|
||||||
po_to_json (>= 1.0.0)
|
po_to_json (>= 1.0.0)
|
||||||
rails (>= 3.2.0)
|
rails (>= 3.2.0)
|
||||||
gitaly-proto (0.113.0)
|
gitaly-proto (0.117.0)
|
||||||
google-protobuf (~> 3.1)
|
google-protobuf (~> 3.1)
|
||||||
grpc (~> 1.10)
|
grpc (~> 1.10)
|
||||||
github-linguist (5.3.3)
|
github-linguist (5.3.3)
|
||||||
|
@ -649,7 +649,7 @@ GEM
|
||||||
httpclient (>= 2.4)
|
httpclient (>= 2.4)
|
||||||
multi_json (>= 1.3.6)
|
multi_json (>= 1.3.6)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rack-protection (2.0.1)
|
rack-protection (2.0.3)
|
||||||
rack
|
rack
|
||||||
rack-proxy (0.6.0)
|
rack-proxy (0.6.0)
|
||||||
rack
|
rack
|
||||||
|
@ -843,9 +843,8 @@ GEM
|
||||||
rack
|
rack
|
||||||
shoulda-matchers (3.1.2)
|
shoulda-matchers (3.1.2)
|
||||||
activesupport (>= 4.0.0)
|
activesupport (>= 4.0.0)
|
||||||
sidekiq (5.1.3)
|
sidekiq (5.2.1)
|
||||||
concurrent-ruby (~> 1.0)
|
connection_pool (~> 2.2, >= 2.2.2)
|
||||||
connection_pool (~> 2.2, >= 2.2.0)
|
|
||||||
rack-protection (>= 1.5.0)
|
rack-protection (>= 1.5.0)
|
||||||
redis (>= 3.3.5, < 5)
|
redis (>= 3.3.5, < 5)
|
||||||
sidekiq-cron (0.6.0)
|
sidekiq-cron (0.6.0)
|
||||||
|
@ -1038,7 +1037,7 @@ DEPENDENCIES
|
||||||
gettext (~> 3.2.2)
|
gettext (~> 3.2.2)
|
||||||
gettext_i18n_rails (~> 1.8.0)
|
gettext_i18n_rails (~> 1.8.0)
|
||||||
gettext_i18n_rails_js (~> 1.3)
|
gettext_i18n_rails_js (~> 1.3)
|
||||||
gitaly-proto (~> 0.113.0)
|
gitaly-proto (~> 0.117.0)
|
||||||
github-linguist (~> 5.3.3)
|
github-linguist (~> 5.3.3)
|
||||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||||
gitlab-gollum-lib (~> 4.2)
|
gitlab-gollum-lib (~> 4.2)
|
||||||
|
@ -1166,7 +1165,7 @@ DEPENDENCIES
|
||||||
settingslogic (~> 2.0.9)
|
settingslogic (~> 2.0.9)
|
||||||
sham_rack (~> 1.3.6)
|
sham_rack (~> 1.3.6)
|
||||||
shoulda-matchers (~> 3.1.2)
|
shoulda-matchers (~> 3.1.2)
|
||||||
sidekiq (~> 5.1)
|
sidekiq (~> 5.2.1)
|
||||||
sidekiq-cron (~> 0.6.0)
|
sidekiq-cron (~> 0.6.0)
|
||||||
sidekiq-limit_fetch (~> 3.4)
|
sidekiq-limit_fetch (~> 3.4)
|
||||||
simple_po_parser (~> 1.1.2)
|
simple_po_parser (~> 1.1.2)
|
||||||
|
|
|
@ -136,7 +136,7 @@ GEM
|
||||||
concurrent-ruby (1.0.5)
|
concurrent-ruby (1.0.5)
|
||||||
concurrent-ruby-ext (1.0.5)
|
concurrent-ruby-ext (1.0.5)
|
||||||
concurrent-ruby (= 1.0.5)
|
concurrent-ruby (= 1.0.5)
|
||||||
connection_pool (2.2.1)
|
connection_pool (2.2.2)
|
||||||
crack (0.4.3)
|
crack (0.4.3)
|
||||||
safe_yaml (~> 1.0.0)
|
safe_yaml (~> 1.0.0)
|
||||||
crass (1.0.4)
|
crass (1.0.4)
|
||||||
|
@ -211,7 +211,7 @@ GEM
|
||||||
fast_blank (1.0.0)
|
fast_blank (1.0.0)
|
||||||
fast_gettext (1.6.0)
|
fast_gettext (1.6.0)
|
||||||
ffaker (2.4.0)
|
ffaker (2.4.0)
|
||||||
ffi (1.9.18)
|
ffi (1.9.25)
|
||||||
flipper (0.13.0)
|
flipper (0.13.0)
|
||||||
flipper-active_record (0.13.0)
|
flipper-active_record (0.13.0)
|
||||||
activerecord (>= 3.2, < 6)
|
activerecord (>= 3.2, < 6)
|
||||||
|
@ -279,7 +279,7 @@ GEM
|
||||||
gettext_i18n_rails (>= 0.7.1)
|
gettext_i18n_rails (>= 0.7.1)
|
||||||
po_to_json (>= 1.0.0)
|
po_to_json (>= 1.0.0)
|
||||||
rails (>= 3.2.0)
|
rails (>= 3.2.0)
|
||||||
gitaly-proto (0.113.0)
|
gitaly-proto (0.117.0)
|
||||||
google-protobuf (~> 3.1)
|
google-protobuf (~> 3.1)
|
||||||
grpc (~> 1.10)
|
grpc (~> 1.10)
|
||||||
github-linguist (5.3.3)
|
github-linguist (5.3.3)
|
||||||
|
@ -653,7 +653,7 @@ GEM
|
||||||
httpclient (>= 2.4)
|
httpclient (>= 2.4)
|
||||||
multi_json (>= 1.3.6)
|
multi_json (>= 1.3.6)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rack-protection (2.0.1)
|
rack-protection (2.0.3)
|
||||||
rack
|
rack
|
||||||
rack-proxy (0.6.0)
|
rack-proxy (0.6.0)
|
||||||
rack
|
rack
|
||||||
|
@ -851,9 +851,8 @@ GEM
|
||||||
rack
|
rack
|
||||||
shoulda-matchers (3.1.2)
|
shoulda-matchers (3.1.2)
|
||||||
activesupport (>= 4.0.0)
|
activesupport (>= 4.0.0)
|
||||||
sidekiq (5.1.3)
|
sidekiq (5.2.1)
|
||||||
concurrent-ruby (~> 1.0)
|
connection_pool (~> 2.2, >= 2.2.2)
|
||||||
connection_pool (~> 2.2, >= 2.2.0)
|
|
||||||
rack-protection (>= 1.5.0)
|
rack-protection (>= 1.5.0)
|
||||||
redis (>= 3.3.5, < 5)
|
redis (>= 3.3.5, < 5)
|
||||||
sidekiq-cron (0.6.0)
|
sidekiq-cron (0.6.0)
|
||||||
|
@ -1047,7 +1046,7 @@ DEPENDENCIES
|
||||||
gettext (~> 3.2.2)
|
gettext (~> 3.2.2)
|
||||||
gettext_i18n_rails (~> 1.8.0)
|
gettext_i18n_rails (~> 1.8.0)
|
||||||
gettext_i18n_rails_js (~> 1.3)
|
gettext_i18n_rails_js (~> 1.3)
|
||||||
gitaly-proto (~> 0.113.0)
|
gitaly-proto (~> 0.117.0)
|
||||||
github-linguist (~> 5.3.3)
|
github-linguist (~> 5.3.3)
|
||||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||||
gitlab-gollum-lib (~> 4.2)
|
gitlab-gollum-lib (~> 4.2)
|
||||||
|
@ -1176,7 +1175,7 @@ DEPENDENCIES
|
||||||
settingslogic (~> 2.0.9)
|
settingslogic (~> 2.0.9)
|
||||||
sham_rack (~> 1.3.6)
|
sham_rack (~> 1.3.6)
|
||||||
shoulda-matchers (~> 3.1.2)
|
shoulda-matchers (~> 3.1.2)
|
||||||
sidekiq (~> 5.1)
|
sidekiq (~> 5.2.1)
|
||||||
sidekiq-cron (~> 0.6.0)
|
sidekiq-cron (~> 0.6.0)
|
||||||
sidekiq-limit_fetch (~> 3.4)
|
sidekiq-limit_fetch (~> 3.4)
|
||||||
simple_po_parser (~> 1.1.2)
|
simple_po_parser (~> 1.1.2)
|
||||||
|
|
18
LICENSE
18
LICENSE
|
@ -1,7 +1,19 @@
|
||||||
Copyright GitLab B.V.
|
Copyright GitLab B.V.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
/* global ListIssue */
|
/* global ListIssue */
|
||||||
import queryData from '~/boards/utils/query_data';
|
import { urlParamsToObject } from '~/lib/utils/common_utils';
|
||||||
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
|
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||||
import ModalHeader from './header.vue';
|
import ModalHeader from './header.vue';
|
||||||
import ModalList from './list.vue';
|
import ModalList from './list.vue';
|
||||||
|
@ -109,13 +109,11 @@
|
||||||
loadIssues(clearIssues = false) {
|
loadIssues(clearIssues = false) {
|
||||||
if (!this.showAddIssuesModal) return false;
|
if (!this.showAddIssuesModal) return false;
|
||||||
|
|
||||||
return gl.boardService
|
return gl.boardService.getBacklog({
|
||||||
.getBacklog(
|
...urlParamsToObject(this.filter.path),
|
||||||
queryData(this.filter.path, {
|
page: this.page,
|
||||||
page: this.page,
|
per: this.perPage,
|
||||||
per: this.perPage,
|
})
|
||||||
}),
|
|
||||||
)
|
|
||||||
.then(res => res.data)
|
.then(res => res.data)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (clearIssues) {
|
if (clearIssues) {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import ListLabel from '~/vue_shared/models/label';
|
import ListLabel from '~/vue_shared/models/label';
|
||||||
import ListAssignee from '~/vue_shared/models/assignee';
|
import ListAssignee from '~/vue_shared/models/assignee';
|
||||||
import queryData from '../utils/query_data';
|
import { urlParamsToObject } from '~/lib/utils/common_utils';
|
||||||
|
|
||||||
const PER_PAGE = 20;
|
const PER_PAGE = 20;
|
||||||
|
|
||||||
|
@ -115,7 +115,10 @@ class List {
|
||||||
}
|
}
|
||||||
|
|
||||||
getIssues(emptyIssues = true) {
|
getIssues(emptyIssues = true) {
|
||||||
const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page });
|
const data = {
|
||||||
|
...urlParamsToObject(gl.issueBoards.BoardsStore.filter.path),
|
||||||
|
page: this.page,
|
||||||
|
};
|
||||||
|
|
||||||
if (this.label && data.label_name) {
|
if (this.label && data.label_name) {
|
||||||
data.label_name = data.label_name.filter(label => label !== this.label.title);
|
data.label_name = data.label_name.filter(label => label !== this.label.title);
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
export default (path, extraData) => path.split('&').reduce((dataParam, filterParam) => {
|
|
||||||
if (filterParam === '') return dataParam;
|
|
||||||
|
|
||||||
const data = dataParam;
|
|
||||||
const paramSplit = filterParam.split('=');
|
|
||||||
const paramKeyNormalized = paramSplit[0].replace('[]', '');
|
|
||||||
const isArray = paramSplit[0].indexOf('[]');
|
|
||||||
const value = decodeURIComponent(paramSplit[1].replace(/\+/g, ' '));
|
|
||||||
|
|
||||||
if (isArray !== -1) {
|
|
||||||
if (!data[paramKeyNormalized]) {
|
|
||||||
data[paramKeyNormalized] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
data[paramKeyNormalized].push(value);
|
|
||||||
} else {
|
|
||||||
data[paramKeyNormalized] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}, extraData);
|
|
|
@ -1,16 +1,12 @@
|
||||||
import Visibility from 'visibilityjs';
|
import Visibility from 'visibilityjs';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import initDismissableCallout from '~/dismissable_callout';
|
||||||
import { s__, sprintf } from '../locale';
|
import { s__, sprintf } from '../locale';
|
||||||
import Flash from '../flash';
|
import Flash from '../flash';
|
||||||
import Poll from '../lib/utils/poll';
|
import Poll from '../lib/utils/poll';
|
||||||
import initSettingsPanels from '../settings_panels';
|
import initSettingsPanels from '../settings_panels';
|
||||||
import eventHub from './event_hub';
|
import eventHub from './event_hub';
|
||||||
import {
|
import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants';
|
||||||
APPLICATION_STATUS,
|
|
||||||
REQUEST_LOADING,
|
|
||||||
REQUEST_SUCCESS,
|
|
||||||
REQUEST_FAILURE,
|
|
||||||
} from './constants';
|
|
||||||
import ClustersService from './services/clusters_service';
|
import ClustersService from './services/clusters_service';
|
||||||
import ClustersStore from './stores/clusters_store';
|
import ClustersStore from './stores/clusters_store';
|
||||||
import applications from './components/applications.vue';
|
import applications from './components/applications.vue';
|
||||||
|
@ -66,6 +62,7 @@ export default class Clusters {
|
||||||
this.showTokenButton = document.querySelector('.js-show-cluster-token');
|
this.showTokenButton = document.querySelector('.js-show-cluster-token');
|
||||||
this.tokenField = document.querySelector('.js-cluster-token');
|
this.tokenField = document.querySelector('.js-cluster-token');
|
||||||
|
|
||||||
|
initDismissableCallout('.js-cluster-security-warning');
|
||||||
initSettingsPanels();
|
initSettingsPanels();
|
||||||
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
|
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
|
||||||
this.initApplications();
|
this.initApplications();
|
||||||
|
@ -129,7 +126,8 @@ export default class Clusters {
|
||||||
if (!Visibility.hidden()) {
|
if (!Visibility.hidden()) {
|
||||||
this.poll.makeRequest();
|
this.poll.makeRequest();
|
||||||
} else {
|
} else {
|
||||||
this.service.fetchData()
|
this.service
|
||||||
|
.fetchData()
|
||||||
.then(data => this.handleSuccess(data))
|
.then(data => this.handleSuccess(data))
|
||||||
.catch(() => Clusters.handleError());
|
.catch(() => Clusters.handleError());
|
||||||
}
|
}
|
||||||
|
@ -177,15 +175,21 @@ export default class Clusters {
|
||||||
|
|
||||||
checkForNewInstalls(prevApplicationMap, newApplicationMap) {
|
checkForNewInstalls(prevApplicationMap, newApplicationMap) {
|
||||||
const appTitles = Object.keys(newApplicationMap)
|
const appTitles = Object.keys(newApplicationMap)
|
||||||
.filter(appId => newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
|
.filter(
|
||||||
prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
|
appId =>
|
||||||
prevApplicationMap[appId].status !== null)
|
newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
|
||||||
|
prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
|
||||||
|
prevApplicationMap[appId].status !== null,
|
||||||
|
)
|
||||||
.map(appId => newApplicationMap[appId].title);
|
.map(appId => newApplicationMap[appId].title);
|
||||||
|
|
||||||
if (appTitles.length > 0) {
|
if (appTitles.length > 0) {
|
||||||
const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'), {
|
const text = sprintf(
|
||||||
appList: appTitles.join(', '),
|
s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'),
|
||||||
});
|
{
|
||||||
|
appList: appTitles.join(', '),
|
||||||
|
},
|
||||||
|
);
|
||||||
Flash(text, 'notice', this.successApplicationContainer);
|
Flash(text, 'notice', this.successApplicationContainer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -218,13 +222,18 @@ export default class Clusters {
|
||||||
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
|
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
|
||||||
this.store.updateAppProperty(appId, 'requestReason', null);
|
this.store.updateAppProperty(appId, 'requestReason', null);
|
||||||
|
|
||||||
this.service.installApplication(appId, data.params)
|
this.service
|
||||||
|
.installApplication(appId, data.params)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
|
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
|
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
|
||||||
this.store.updateAppProperty(appId, 'requestReason', s__('ClusterIntegration|Request to begin installing failed'));
|
this.store.updateAppProperty(
|
||||||
|
appId,
|
||||||
|
'requestReason',
|
||||||
|
s__('ClusterIntegration|Request to begin installing failed'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import setupToggleButtons from '~/toggle_buttons';
|
import setupToggleButtons from '~/toggle_buttons';
|
||||||
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
|
import initDismissableCallout from '~/dismissable_callout';
|
||||||
|
|
||||||
import ClustersService from './services/clusters_service';
|
import ClustersService from './services/clusters_service';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const clusterList = document.querySelector('.js-clusters-list');
|
const clusterList = document.querySelector('.js-clusters-list');
|
||||||
|
|
||||||
gcpSignupOffer();
|
initDismissableCallout('.gcp-signup-offer');
|
||||||
|
|
||||||
// The empty state won't have a clusterList
|
// The empty state won't have a clusterList
|
||||||
if (clusterList) {
|
if (clusterList) {
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import progressBar from '@gitlab-org/gitlab-ui/dist/base/progress_bar';
|
import progressBar from '@gitlab-org/gitlab-ui/dist/components/base/progress_bar';
|
||||||
|
import modal from '@gitlab-org/gitlab-ui/dist/components/base/modal';
|
||||||
|
|
||||||
|
import dModal from '@gitlab-org/gitlab-ui/dist/directives/modal';
|
||||||
|
import dTooltip from '@gitlab-org/gitlab-ui/dist/directives/tooltip';
|
||||||
|
|
||||||
Vue.component('gl-progress-bar', progressBar);
|
Vue.component('gl-progress-bar', progressBar);
|
||||||
|
Vue.component('gl-ui-modal', modal);
|
||||||
|
|
||||||
|
Vue.directive('gl-modal', dModal);
|
||||||
|
Vue.directive('gl-tooltip', dTooltip);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* eslint-disable object-shorthand, func-names, comma-dangle, no-else-return, quotes */
|
/* eslint-disable object-shorthand, func-names, no-else-return */
|
||||||
/* global CommentsStore */
|
/* global CommentsStore */
|
||||||
/* global ResolveService */
|
/* global ResolveService */
|
||||||
|
|
||||||
|
@ -25,44 +25,44 @@ const ResolveDiscussionBtn = Vue.extend({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
showButton: function () {
|
showButton: function() {
|
||||||
if (this.discussion) {
|
if (this.discussion) {
|
||||||
return this.discussion.isResolvable();
|
return this.discussion.isResolvable();
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isDiscussionResolved: function () {
|
isDiscussionResolved: function() {
|
||||||
if (this.discussion) {
|
if (this.discussion) {
|
||||||
return this.discussion.isResolved();
|
return this.discussion.isResolved();
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
buttonText: function () {
|
buttonText: function() {
|
||||||
if (this.isDiscussionResolved) {
|
if (this.isDiscussionResolved) {
|
||||||
return "Unresolve discussion";
|
return 'Unresolve discussion';
|
||||||
} else {
|
} else {
|
||||||
return "Resolve discussion";
|
return 'Resolve discussion';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
loading: function () {
|
loading: function() {
|
||||||
if (this.discussion) {
|
if (this.discussion) {
|
||||||
return this.discussion.loading;
|
return this.discussion.loading;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
created: function () {
|
created: function() {
|
||||||
CommentsStore.createDiscussion(this.discussionId, this.canResolve);
|
CommentsStore.createDiscussion(this.discussionId, this.canResolve);
|
||||||
|
|
||||||
this.discussion = CommentsStore.state[this.discussionId];
|
this.discussion = CommentsStore.state[this.discussionId];
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve: function () {
|
resolve: function() {
|
||||||
ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
|
ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,7 @@ window.gl = window.gl || {};
|
||||||
|
|
||||||
class ResolveServiceClass {
|
class ResolveServiceClass {
|
||||||
constructor(root) {
|
constructor(root) {
|
||||||
this.noteResource = Vue.resource(
|
this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`);
|
||||||
`${root}/notes{/noteId}/resolve?html=true`,
|
|
||||||
);
|
|
||||||
this.discussionResource = Vue.resource(
|
this.discussionResource = Vue.resource(
|
||||||
`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`,
|
`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`,
|
||||||
);
|
);
|
||||||
|
@ -51,10 +49,7 @@ class ResolveServiceClass {
|
||||||
discussion.updateHeadline(data);
|
discussion.updateHeadline(data);
|
||||||
})
|
})
|
||||||
.catch(
|
.catch(
|
||||||
() =>
|
() => new Flash('An error occurred when trying to resolve a discussion. Please try again.'),
|
||||||
new Flash(
|
|
||||||
'An error occurred when trying to resolve a discussion. Please try again.',
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ export default {
|
||||||
emailPatchPath: state => state.diffs.emailPatchPath,
|
emailPatchPath: state => state.diffs.emailPatchPath,
|
||||||
}),
|
}),
|
||||||
...mapGetters('diffs', ['isParallelView']),
|
...mapGetters('diffs', ['isParallelView']),
|
||||||
...mapGetters(['isNotesFetched']),
|
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
|
||||||
targetBranch() {
|
targetBranch() {
|
||||||
return {
|
return {
|
||||||
branchName: this.targetBranchName,
|
branchName: this.targetBranchName,
|
||||||
|
@ -112,13 +112,26 @@ export default {
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.adjustView();
|
this.adjustView();
|
||||||
|
eventHub.$once('fetchedNotesData', this.setDiscussions);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles', 'startRenderDiffsQueue']),
|
...mapActions('diffs', [
|
||||||
|
'setBaseConfig',
|
||||||
|
'fetchDiffFiles',
|
||||||
|
'startRenderDiffsQueue',
|
||||||
|
'assignDiscussionsToDiff',
|
||||||
|
]),
|
||||||
|
|
||||||
fetchData() {
|
fetchData() {
|
||||||
this.fetchDiffFiles()
|
this.fetchDiffFiles()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
requestIdleCallback(this.startRenderDiffsQueue, { timeout: 1000 });
|
requestIdleCallback(
|
||||||
|
() => {
|
||||||
|
this.setDiscussions();
|
||||||
|
this.startRenderDiffsQueue();
|
||||||
|
},
|
||||||
|
{ timeout: 1000 },
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
createFlash(__('Something went wrong on our end. Please try again!'));
|
createFlash(__('Something went wrong on our end. Please try again!'));
|
||||||
|
@ -128,6 +141,16 @@ export default {
|
||||||
eventHub.$emit('fetchNotesData');
|
eventHub.$emit('fetchNotesData');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setDiscussions() {
|
||||||
|
if (this.isNotesFetched) {
|
||||||
|
requestIdleCallback(
|
||||||
|
() => {
|
||||||
|
this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode);
|
||||||
|
},
|
||||||
|
{ timeout: 1000 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
adjustView() {
|
adjustView() {
|
||||||
if (this.shouldShow && this.isParallelView) {
|
if (this.shouldShow && this.isParallelView) {
|
||||||
window.mrTabs.expandViewContainer();
|
window.mrTabs.expandViewContainer();
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { mapActions } from 'vuex';
|
||||||
import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
|
import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -11,6 +12,14 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions('diffs', ['removeDiscussionsFromDiff']),
|
||||||
|
deleteNoteHandler(discussion) {
|
||||||
|
if (discussion.notes.length <= 1) {
|
||||||
|
this.removeDiscussionsFromDiff(discussion);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -31,6 +40,7 @@ export default {
|
||||||
:render-diff-file="false"
|
:render-diff-file="false"
|
||||||
:always-expanded="true"
|
:always-expanded="true"
|
||||||
:discussions-by-diff-order="true"
|
:discussions-by-diff-order="true"
|
||||||
|
@noteDeleted="deleteNoteHandler"
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapActions } from 'vuex';
|
import { mapActions, mapGetters } from 'vuex';
|
||||||
import _ from 'underscore';
|
import _ from 'underscore';
|
||||||
import { __, sprintf } from '~/locale';
|
import { __, sprintf } from '~/locale';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
|
@ -30,6 +30,7 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
|
||||||
isCollapsed() {
|
isCollapsed() {
|
||||||
return this.file.collapsed || false;
|
return this.file.collapsed || false;
|
||||||
},
|
},
|
||||||
|
@ -44,23 +45,23 @@ export default {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
showExpandMessage() {
|
showExpandMessage() {
|
||||||
return this.isCollapsed && !this.isLoadingCollapsedDiff && !this.file.tooLarge;
|
return (
|
||||||
|
!this.isCollapsed &&
|
||||||
|
!this.file.highlightedDiffLines &&
|
||||||
|
!this.isLoadingCollapsedDiff &&
|
||||||
|
!this.file.tooLarge &&
|
||||||
|
this.file.text
|
||||||
|
);
|
||||||
},
|
},
|
||||||
showLoadingIcon() {
|
showLoadingIcon() {
|
||||||
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
|
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('diffs', ['loadCollapsedDiff']),
|
...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff']),
|
||||||
handleToggle() {
|
handleToggle() {
|
||||||
const { collapsed, highlightedDiffLines, parallelDiffLines } = this.file;
|
const { highlightedDiffLines, parallelDiffLines } = this.file;
|
||||||
|
if (!highlightedDiffLines && parallelDiffLines !== undefined && !parallelDiffLines.length) {
|
||||||
if (
|
|
||||||
collapsed &&
|
|
||||||
!highlightedDiffLines &&
|
|
||||||
parallelDiffLines !== undefined &&
|
|
||||||
!parallelDiffLines.length
|
|
||||||
) {
|
|
||||||
this.handleLoadCollapsedDiff();
|
this.handleLoadCollapsedDiff();
|
||||||
} else {
|
} else {
|
||||||
this.file.collapsed = !this.file.collapsed;
|
this.file.collapsed = !this.file.collapsed;
|
||||||
|
@ -76,6 +77,14 @@ export default {
|
||||||
this.file.collapsed = false;
|
this.file.collapsed = false;
|
||||||
this.file.renderIt = true;
|
this.file.renderIt = true;
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
requestIdleCallback(
|
||||||
|
() => {
|
||||||
|
this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode);
|
||||||
|
},
|
||||||
|
{ timeout: 1000 },
|
||||||
|
);
|
||||||
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.isLoadingCollapsedDiff = false;
|
this.isLoadingCollapsedDiff = false;
|
||||||
createFlash(__('Something went wrong on our end. Please try again!'));
|
createFlash(__('Something went wrong on our end. Please try again!'));
|
||||||
|
@ -136,11 +145,11 @@ export default {
|
||||||
:diff-file="file"
|
:diff-file="file"
|
||||||
/>
|
/>
|
||||||
<loading-icon
|
<loading-icon
|
||||||
v-else-if="showLoadingIcon"
|
v-if="showLoadingIcon"
|
||||||
class="diff-content loading"
|
class="diff-content loading"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="showExpandMessage"
|
v-else-if="showExpandMessage"
|
||||||
class="nothing-here-block diff-collapsed"
|
class="nothing-here-block diff-collapsed"
|
||||||
>
|
>
|
||||||
{{ __('This diff is collapsed.') }}
|
{{ __('This diff is collapsed.') }}
|
||||||
|
|
|
@ -1,15 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapActions } from 'vuex';
|
import { mapActions } from 'vuex';
|
||||||
import Icon from '~/vue_shared/components/icon.vue';
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
import tooltip from '~/vue_shared/directives/tooltip';
|
|
||||||
import { pluralize, truncate } from '~/lib/utils/text_utility';
|
import { pluralize, truncate } from '~/lib/utils/text_utility';
|
||||||
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
|
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||||
import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
|
import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
directives: {
|
|
||||||
tooltip,
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
Icon,
|
Icon,
|
||||||
UserAvatarImage,
|
UserAvatarImage,
|
||||||
|
@ -91,10 +87,10 @@ export default {
|
||||||
@click.native="toggleDiscussions"
|
@click.native="toggleDiscussions"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-tooltip
|
v-gl-tooltip
|
||||||
v-if="moreText"
|
v-if="moreText"
|
||||||
:title="moreText"
|
:title="moreText"
|
||||||
class="diff-comments-more-count has-tooltip js-diff-comment-avatar js-diff-comment-plus"
|
class="diff-comments-more-count js-diff-comment-avatar js-diff-comment-plus"
|
||||||
data-container="body"
|
data-container="body"
|
||||||
data-placement="top"
|
data-placement="top"
|
||||||
role="button"
|
role="button"
|
||||||
|
|
|
@ -13,6 +13,10 @@ export default {
|
||||||
Icon,
|
Icon,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
line: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
fileHash: {
|
fileHash: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -21,31 +25,16 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
lineType: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
lineNumber: {
|
lineNumber: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
lineCode: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
linePosition: {
|
linePosition: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
metaData: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
showCommentButton: {
|
showCommentButton: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
@ -76,11 +65,6 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
discussions: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
|
@ -89,7 +73,7 @@ export default {
|
||||||
}),
|
}),
|
||||||
...mapGetters(['isLoggedIn']),
|
...mapGetters(['isLoggedIn']),
|
||||||
lineHref() {
|
lineHref() {
|
||||||
return this.lineCode ? `#${this.lineCode}` : '#';
|
return `#${this.line.lineCode || ''}`;
|
||||||
},
|
},
|
||||||
shouldShowCommentButton() {
|
shouldShowCommentButton() {
|
||||||
return (
|
return (
|
||||||
|
@ -103,20 +87,19 @@ export default {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
hasDiscussions() {
|
hasDiscussions() {
|
||||||
return this.discussions.length > 0;
|
return this.line.discussions && this.line.discussions.length > 0;
|
||||||
},
|
},
|
||||||
shouldShowAvatarsOnGutter() {
|
shouldShowAvatarsOnGutter() {
|
||||||
if (!this.lineType && this.linePosition === LINE_POSITION_RIGHT) {
|
if (!this.line.type && this.linePosition === LINE_POSITION_RIGHT) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.showCommentButton && this.hasDiscussions;
|
return this.showCommentButton && this.hasDiscussions;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('diffs', ['loadMoreLines', 'showCommentForm']),
|
...mapActions('diffs', ['loadMoreLines', 'showCommentForm']),
|
||||||
handleCommentButton() {
|
handleCommentButton() {
|
||||||
this.showCommentForm({ lineCode: this.lineCode });
|
this.showCommentForm({ lineCode: this.line.lineCode });
|
||||||
},
|
},
|
||||||
handleLoadMoreLines() {
|
handleLoadMoreLines() {
|
||||||
if (this.isRequesting) {
|
if (this.isRequesting) {
|
||||||
|
@ -125,8 +108,8 @@ export default {
|
||||||
|
|
||||||
this.isRequesting = true;
|
this.isRequesting = true;
|
||||||
const endpoint = this.contextLinesPath;
|
const endpoint = this.contextLinesPath;
|
||||||
const oldLineNumber = this.metaData.oldPos || 0;
|
const oldLineNumber = this.line.metaData.oldPos || 0;
|
||||||
const newLineNumber = this.metaData.newPos || 0;
|
const newLineNumber = this.line.metaData.newPos || 0;
|
||||||
const offset = newLineNumber - oldLineNumber;
|
const offset = newLineNumber - oldLineNumber;
|
||||||
const bottom = this.isBottom;
|
const bottom = this.isBottom;
|
||||||
const { fileHash } = this;
|
const { fileHash } = this;
|
||||||
|
@ -201,7 +184,7 @@ export default {
|
||||||
</a>
|
</a>
|
||||||
<diff-gutter-avatars
|
<diff-gutter-avatars
|
||||||
v-if="shouldShowAvatarsOnGutter"
|
v-if="shouldShowAvatarsOnGutter"
|
||||||
:discussions="discussions"
|
:discussions="line.discussions"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import noteForm from '../../notes/components/note_form.vue';
|
||||||
import { getNoteFormData } from '../store/utils';
|
import { getNoteFormData } from '../store/utils';
|
||||||
import autosave from '../../notes/mixins/autosave';
|
import autosave from '../../notes/mixins/autosave';
|
||||||
import { DIFF_NOTE_TYPE } from '../constants';
|
import { DIFF_NOTE_TYPE } from '../constants';
|
||||||
|
import { reduceDiscussionsToLineCodes } from '../../notes/stores/utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -52,7 +53,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('diffs', ['cancelCommentForm']),
|
...mapActions('diffs', ['cancelCommentForm', 'assignDiscussionsToDiff']),
|
||||||
...mapActions(['saveNote', 'refetchDiscussionById']),
|
...mapActions(['saveNote', 'refetchDiscussionById']),
|
||||||
handleCancelCommentForm(shouldConfirm, isDirty) {
|
handleCancelCommentForm(shouldConfirm, isDirty) {
|
||||||
if (shouldConfirm && isDirty) {
|
if (shouldConfirm && isDirty) {
|
||||||
|
@ -88,7 +89,10 @@ export default {
|
||||||
const endpoint = this.getNotesDataByProp('discussionsPath');
|
const endpoint = this.getNotesDataByProp('discussionsPath');
|
||||||
|
|
||||||
this.refetchDiscussionById({ path: endpoint, discussionId: result.discussion_id })
|
this.refetchDiscussionById({ path: endpoint, discussionId: result.discussion_id })
|
||||||
.then(() => {
|
.then(selectedDiscussion => {
|
||||||
|
const lineCodeDiscussions = reduceDiscussionsToLineCodes([selectedDiscussion]);
|
||||||
|
this.assignDiscussionsToDiff(lineCodeDiscussions);
|
||||||
|
|
||||||
this.handleCancelCommentForm();
|
this.handleCancelCommentForm();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|
|
@ -11,8 +11,6 @@ import {
|
||||||
LINE_HOVER_CLASS_NAME,
|
LINE_HOVER_CLASS_NAME,
|
||||||
LINE_UNFOLD_CLASS_NAME,
|
LINE_UNFOLD_CLASS_NAME,
|
||||||
INLINE_DIFF_VIEW_TYPE,
|
INLINE_DIFF_VIEW_TYPE,
|
||||||
LINE_POSITION_LEFT,
|
|
||||||
LINE_POSITION_RIGHT,
|
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -67,42 +65,24 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
discussions: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['isLoggedIn']),
|
...mapGetters(['isLoggedIn']),
|
||||||
normalizedLine() {
|
|
||||||
let normalizedLine;
|
|
||||||
|
|
||||||
if (this.diffViewType === INLINE_DIFF_VIEW_TYPE) {
|
|
||||||
normalizedLine = this.line;
|
|
||||||
} else if (this.linePosition === LINE_POSITION_LEFT) {
|
|
||||||
normalizedLine = this.line.left;
|
|
||||||
} else if (this.linePosition === LINE_POSITION_RIGHT) {
|
|
||||||
normalizedLine = this.line.right;
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizedLine;
|
|
||||||
},
|
|
||||||
isMatchLine() {
|
isMatchLine() {
|
||||||
return this.normalizedLine.type === MATCH_LINE_TYPE;
|
return this.line.type === MATCH_LINE_TYPE;
|
||||||
},
|
},
|
||||||
isContextLine() {
|
isContextLine() {
|
||||||
return this.normalizedLine.type === CONTEXT_LINE_TYPE;
|
return this.line.type === CONTEXT_LINE_TYPE;
|
||||||
},
|
},
|
||||||
isMetaLine() {
|
isMetaLine() {
|
||||||
const { type } = this.normalizedLine;
|
const { type } = this.line;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
|
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
classNameMap() {
|
classNameMap() {
|
||||||
const { type } = this.normalizedLine;
|
const { type } = this.line;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[type]: type,
|
[type]: type,
|
||||||
|
@ -116,9 +96,9 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
lineNumber() {
|
lineNumber() {
|
||||||
const { lineType, normalizedLine } = this;
|
const { lineType } = this;
|
||||||
|
|
||||||
return lineType === OLD_LINE_TYPE ? normalizedLine.oldLine : normalizedLine.newLine;
|
return lineType === OLD_LINE_TYPE ? this.line.oldLine : this.line.newLine;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -129,20 +109,17 @@ export default {
|
||||||
:class="classNameMap"
|
:class="classNameMap"
|
||||||
>
|
>
|
||||||
<diff-line-gutter-content
|
<diff-line-gutter-content
|
||||||
|
:line="line"
|
||||||
:file-hash="fileHash"
|
:file-hash="fileHash"
|
||||||
:context-lines-path="contextLinesPath"
|
:context-lines-path="contextLinesPath"
|
||||||
:line-type="normalizedLine.type"
|
|
||||||
:line-code="normalizedLine.lineCode"
|
|
||||||
:line-position="linePosition"
|
:line-position="linePosition"
|
||||||
:line-number="lineNumber"
|
:line-number="lineNumber"
|
||||||
:meta-data="normalizedLine.metaData"
|
|
||||||
:show-comment-button="showCommentButton"
|
:show-comment-button="showCommentButton"
|
||||||
:is-hover="isHover"
|
:is-hover="isHover"
|
||||||
:is-bottom="isBottom"
|
:is-bottom="isBottom"
|
||||||
:is-match-line="isMatchLine"
|
:is-match-line="isMatchLine"
|
||||||
:is-context-line="isContentLine"
|
:is-context-line="isContentLine"
|
||||||
:is-meta-line="isMetaLine"
|
:is-meta-line="isMetaLine"
|
||||||
:discussions="discussions"
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -21,18 +21,13 @@ export default {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
discussions: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
|
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
|
||||||
}),
|
}),
|
||||||
className() {
|
className() {
|
||||||
return this.discussions.length ? '' : 'js-temp-notes-holder';
|
return this.line.discussions.length ? '' : 'js-temp-notes-holder';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -44,14 +39,13 @@ export default {
|
||||||
class="notes_holder"
|
class="notes_holder"
|
||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
class="notes_line"
|
class="notes_content"
|
||||||
colspan="2"
|
colspan="3"
|
||||||
></td>
|
>
|
||||||
<td class="notes_content">
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<diff-discussions
|
<diff-discussions
|
||||||
v-if="discussions.length"
|
v-if="line.discussions.length"
|
||||||
:discussions="discussions"
|
:discussions="line.discussions"
|
||||||
/>
|
/>
|
||||||
<diff-line-note-form
|
<diff-line-note-form
|
||||||
v-if="diffLineCommentForms[line.lineCode]"
|
v-if="diffLineCommentForms[line.lineCode]"
|
||||||
|
|
|
@ -33,11 +33,6 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
discussions: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -94,7 +89,6 @@ export default {
|
||||||
:is-bottom="isBottom"
|
:is-bottom="isBottom"
|
||||||
:is-hover="isHover"
|
:is-hover="isHover"
|
||||||
:show-comment-button="true"
|
:show-comment-button="true"
|
||||||
:discussions="discussions"
|
|
||||||
class="diff-line-num old_line"
|
class="diff-line-num old_line"
|
||||||
/>
|
/>
|
||||||
<diff-table-cell
|
<diff-table-cell
|
||||||
|
@ -104,7 +98,6 @@ export default {
|
||||||
:line-type="newLineType"
|
:line-type="newLineType"
|
||||||
:is-bottom="isBottom"
|
:is-bottom="isBottom"
|
||||||
:is-hover="isHover"
|
:is-hover="isHover"
|
||||||
:discussions="discussions"
|
|
||||||
class="diff-line-num new_line"
|
class="diff-line-num new_line"
|
||||||
/>
|
/>
|
||||||
<td
|
<td
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import { mapGetters, mapState } from 'vuex';
|
import { mapGetters, mapState } from 'vuex';
|
||||||
import inlineDiffTableRow from './inline_diff_table_row.vue';
|
import inlineDiffTableRow from './inline_diff_table_row.vue';
|
||||||
import inlineDiffCommentRow from './inline_diff_comment_row.vue';
|
import inlineDiffCommentRow from './inline_diff_comment_row.vue';
|
||||||
import { trimFirstCharOfLineContent } from '../store/utils';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -20,29 +19,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('diffs', [
|
...mapGetters('diffs', ['commitId', 'shouldRenderInlineCommentRow']),
|
||||||
'commitId',
|
|
||||||
'shouldRenderInlineCommentRow',
|
|
||||||
'singleDiscussionByLineCode',
|
|
||||||
]),
|
|
||||||
...mapState({
|
...mapState({
|
||||||
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
|
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
|
||||||
}),
|
}),
|
||||||
normalizedDiffLines() {
|
|
||||||
return this.diffLines.map(line => (line.richText ? trimFirstCharOfLineContent(line) : line));
|
|
||||||
},
|
|
||||||
diffLinesLength() {
|
diffLinesLength() {
|
||||||
return this.normalizedDiffLines.length;
|
return this.diffLines.length;
|
||||||
},
|
},
|
||||||
userColorScheme() {
|
userColorScheme() {
|
||||||
return window.gon.user_color_scheme;
|
return window.gon.user_color_scheme;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
discussionsList(line) {
|
|
||||||
return line.lineCode !== undefined ? this.singleDiscussionByLineCode(line.lineCode) : [];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -53,7 +40,7 @@ export default {
|
||||||
class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view">
|
class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view">
|
||||||
<tbody>
|
<tbody>
|
||||||
<template
|
<template
|
||||||
v-for="(line, index) in normalizedDiffLines"
|
v-for="(line, index) in diffLines"
|
||||||
>
|
>
|
||||||
<inline-diff-table-row
|
<inline-diff-table-row
|
||||||
:file-hash="diffFile.fileHash"
|
:file-hash="diffFile.fileHash"
|
||||||
|
@ -61,7 +48,6 @@ export default {
|
||||||
:line="line"
|
:line="line"
|
||||||
:is-bottom="index + 1 === diffLinesLength"
|
:is-bottom="index + 1 === diffLinesLength"
|
||||||
:key="line.lineCode"
|
:key="line.lineCode"
|
||||||
:discussions="discussionsList(line)"
|
|
||||||
/>
|
/>
|
||||||
<inline-diff-comment-row
|
<inline-diff-comment-row
|
||||||
v-if="shouldRenderInlineCommentRow(line)"
|
v-if="shouldRenderInlineCommentRow(line)"
|
||||||
|
@ -69,7 +55,6 @@ export default {
|
||||||
:line="line"
|
:line="line"
|
||||||
:line-index="index"
|
:line-index="index"
|
||||||
:key="index"
|
:key="index"
|
||||||
:discussions="discussionsList(line)"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -21,51 +21,49 @@ export default {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
leftDiscussions: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
rightDiscussions: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
|
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
|
||||||
}),
|
}),
|
||||||
leftLineCode() {
|
leftLineCode() {
|
||||||
return this.line.left.lineCode;
|
return this.line.left && this.line.left.lineCode;
|
||||||
},
|
},
|
||||||
rightLineCode() {
|
rightLineCode() {
|
||||||
return this.line.right.lineCode;
|
return this.line.right && this.line.right.lineCode;
|
||||||
},
|
},
|
||||||
hasExpandedDiscussionOnLeft() {
|
hasExpandedDiscussionOnLeft() {
|
||||||
const discussions = this.leftDiscussions;
|
return this.line.left && this.line.left.discussions
|
||||||
|
? this.line.left.discussions.every(discussion => discussion.expanded)
|
||||||
return discussions ? discussions.every(discussion => discussion.expanded) : false;
|
: false;
|
||||||
},
|
},
|
||||||
hasExpandedDiscussionOnRight() {
|
hasExpandedDiscussionOnRight() {
|
||||||
const discussions = this.rightDiscussions;
|
return this.line.right && this.line.right.discussions
|
||||||
|
? this.line.right.discussions.every(discussion => discussion.expanded)
|
||||||
return discussions ? discussions.every(discussion => discussion.expanded) : false;
|
: false;
|
||||||
},
|
},
|
||||||
hasAnyExpandedDiscussion() {
|
hasAnyExpandedDiscussion() {
|
||||||
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
|
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
|
||||||
},
|
},
|
||||||
shouldRenderDiscussionsOnLeft() {
|
shouldRenderDiscussionsOnLeft() {
|
||||||
return this.leftDiscussions && this.hasExpandedDiscussionOnLeft;
|
return this.line.left && this.line.left.discussions && this.hasExpandedDiscussionOnLeft;
|
||||||
},
|
},
|
||||||
shouldRenderDiscussionsOnRight() {
|
shouldRenderDiscussionsOnRight() {
|
||||||
return this.rightDiscussions && this.hasExpandedDiscussionOnRight && this.line.right.type;
|
return (
|
||||||
|
this.line.right &&
|
||||||
|
this.line.right.discussions &&
|
||||||
|
this.hasExpandedDiscussionOnRight &&
|
||||||
|
this.line.right.type
|
||||||
|
);
|
||||||
},
|
},
|
||||||
showRightSideCommentForm() {
|
showRightSideCommentForm() {
|
||||||
return this.line.right.type && this.diffLineCommentForms[this.rightLineCode];
|
return (
|
||||||
|
this.line.right && this.line.right.type && this.diffLineCommentForms[this.rightLineCode]
|
||||||
|
);
|
||||||
},
|
},
|
||||||
className() {
|
className() {
|
||||||
return this.leftDiscussions.length > 0 || this.rightDiscussions.length > 0
|
return (this.left && this.line.left.discussions.length > 0) ||
|
||||||
|
(this.right && this.line.right.discussions.length > 0)
|
||||||
? ''
|
? ''
|
||||||
: 'js-temp-notes-holder';
|
: 'js-temp-notes-holder';
|
||||||
},
|
},
|
||||||
|
@ -85,8 +83,8 @@ export default {
|
||||||
class="content"
|
class="content"
|
||||||
>
|
>
|
||||||
<diff-discussions
|
<diff-discussions
|
||||||
v-if="leftDiscussions.length"
|
v-if="line.left.discussions.length"
|
||||||
:discussions="leftDiscussions"
|
:discussions="line.left.discussions"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<diff-line-note-form
|
<diff-line-note-form
|
||||||
|
@ -104,8 +102,8 @@ export default {
|
||||||
class="content"
|
class="content"
|
||||||
>
|
>
|
||||||
<diff-discussions
|
<diff-discussions
|
||||||
v-if="rightDiscussions.length"
|
v-if="line.right.discussions.length"
|
||||||
:discussions="rightDiscussions"
|
:discussions="line.right.discussions"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<diff-line-note-form
|
<diff-line-note-form
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import DiffTableCell from './diff_table_cell.vue';
|
import DiffTableCell from './diff_table_cell.vue';
|
||||||
import {
|
import {
|
||||||
NEW_LINE_TYPE,
|
NEW_LINE_TYPE,
|
||||||
|
@ -10,8 +9,7 @@ import {
|
||||||
OLD_NO_NEW_LINE_TYPE,
|
OLD_NO_NEW_LINE_TYPE,
|
||||||
PARALLEL_DIFF_VIEW_TYPE,
|
PARALLEL_DIFF_VIEW_TYPE,
|
||||||
NEW_NO_NEW_LINE_TYPE,
|
NEW_NO_NEW_LINE_TYPE,
|
||||||
LINE_POSITION_LEFT,
|
EMPTY_CELL_TYPE,
|
||||||
LINE_POSITION_RIGHT,
|
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -36,16 +34,6 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
leftDiscussions: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
rightDiscussions: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -54,29 +42,26 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('diffs', ['isParallelView']),
|
|
||||||
isContextLine() {
|
isContextLine() {
|
||||||
return this.line.left.type === CONTEXT_LINE_TYPE;
|
return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE;
|
||||||
},
|
},
|
||||||
classNameMap() {
|
classNameMap() {
|
||||||
return {
|
return {
|
||||||
[CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
|
[CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
|
||||||
[PARALLEL_DIFF_VIEW_TYPE]: this.isParallelView,
|
[PARALLEL_DIFF_VIEW_TYPE]: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
parallelViewLeftLineType() {
|
parallelViewLeftLineType() {
|
||||||
if (this.line.right.type === NEW_NO_NEW_LINE_TYPE) {
|
if (this.line.right && this.line.right.type === NEW_NO_NEW_LINE_TYPE) {
|
||||||
return OLD_NO_NEW_LINE_TYPE;
|
return OLD_NO_NEW_LINE_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.line.left.type;
|
return this.line.left ? this.line.left.type : EMPTY_CELL_TYPE;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.newLineType = NEW_LINE_TYPE;
|
this.newLineType = NEW_LINE_TYPE;
|
||||||
this.oldLineType = OLD_LINE_TYPE;
|
this.oldLineType = OLD_LINE_TYPE;
|
||||||
this.linePositionLeft = LINE_POSITION_LEFT;
|
|
||||||
this.linePositionRight = LINE_POSITION_RIGHT;
|
|
||||||
this.parallelDiffViewType = PARALLEL_DIFF_VIEW_TYPE;
|
this.parallelDiffViewType = PARALLEL_DIFF_VIEW_TYPE;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -116,47 +101,57 @@ export default {
|
||||||
@mouseover="handleMouseMove"
|
@mouseover="handleMouseMove"
|
||||||
@mouseout="handleMouseMove"
|
@mouseout="handleMouseMove"
|
||||||
>
|
>
|
||||||
<diff-table-cell
|
<template v-if="line.left">
|
||||||
:file-hash="fileHash"
|
<diff-table-cell
|
||||||
:context-lines-path="contextLinesPath"
|
:file-hash="fileHash"
|
||||||
:line="line"
|
:context-lines-path="contextLinesPath"
|
||||||
:line-type="oldLineType"
|
:line="line.left"
|
||||||
:line-position="linePositionLeft"
|
:line-type="oldLineType"
|
||||||
:is-bottom="isBottom"
|
:is-bottom="isBottom"
|
||||||
:is-hover="isLeftHover"
|
:is-hover="isLeftHover"
|
||||||
:show-comment-button="true"
|
:show-comment-button="true"
|
||||||
:diff-view-type="parallelDiffViewType"
|
:diff-view-type="parallelDiffViewType"
|
||||||
:discussions="leftDiscussions"
|
line-position="left"
|
||||||
class="diff-line-num old_line"
|
class="diff-line-num old_line"
|
||||||
/>
|
/>
|
||||||
<td
|
<td
|
||||||
:id="line.left.lineCode"
|
:id="line.left.lineCode"
|
||||||
:class="parallelViewLeftLineType"
|
:class="parallelViewLeftLineType"
|
||||||
class="line_content parallel left-side"
|
class="line_content parallel left-side"
|
||||||
@mousedown.native="handleParallelLineMouseDown"
|
@mousedown.native="handleParallelLineMouseDown"
|
||||||
v-html="line.left.richText"
|
v-html="line.left.richText"
|
||||||
>
|
>
|
||||||
</td>
|
</td>
|
||||||
<diff-table-cell
|
</template>
|
||||||
:file-hash="fileHash"
|
<template v-else>
|
||||||
:context-lines-path="contextLinesPath"
|
<td class="diff-line-num old_line empty-cell"></td>
|
||||||
:line="line"
|
<td class="line_content parallel left-side empty-cell"></td>
|
||||||
:line-type="newLineType"
|
</template>
|
||||||
:line-position="linePositionRight"
|
<template v-if="line.right">
|
||||||
:is-bottom="isBottom"
|
<diff-table-cell
|
||||||
:is-hover="isRightHover"
|
:file-hash="fileHash"
|
||||||
:show-comment-button="true"
|
:context-lines-path="contextLinesPath"
|
||||||
:diff-view-type="parallelDiffViewType"
|
:line="line.right"
|
||||||
:discussions="rightDiscussions"
|
:line-type="newLineType"
|
||||||
class="diff-line-num new_line"
|
:is-bottom="isBottom"
|
||||||
/>
|
:is-hover="isRightHover"
|
||||||
<td
|
:show-comment-button="true"
|
||||||
:id="line.right.lineCode"
|
:diff-view-type="parallelDiffViewType"
|
||||||
:class="line.right.type"
|
line-position="right"
|
||||||
class="line_content parallel right-side"
|
class="diff-line-num new_line"
|
||||||
@mousedown.native="handleParallelLineMouseDown"
|
/>
|
||||||
v-html="line.right.richText"
|
<td
|
||||||
>
|
:id="line.right.lineCode"
|
||||||
</td>
|
:class="line.right.type"
|
||||||
|
class="line_content parallel right-side"
|
||||||
|
@mousedown.native="handleParallelLineMouseDown"
|
||||||
|
v-html="line.right.richText"
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<td class="diff-line-num old_line empty-cell"></td>
|
||||||
|
<td class="line_content parallel right-side empty-cell"></td>
|
||||||
|
</template>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
import { mapState, mapGetters } from 'vuex';
|
import { mapState, mapGetters } from 'vuex';
|
||||||
import parallelDiffTableRow from './parallel_diff_table_row.vue';
|
import parallelDiffTableRow from './parallel_diff_table_row.vue';
|
||||||
import parallelDiffCommentRow from './parallel_diff_comment_row.vue';
|
import parallelDiffCommentRow from './parallel_diff_comment_row.vue';
|
||||||
import { EMPTY_CELL_TYPE } from '../constants';
|
|
||||||
import { trimFirstCharOfLineContent } from '../store/utils';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -21,46 +19,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('diffs', [
|
...mapGetters('diffs', ['commitId', 'shouldRenderParallelCommentRow']),
|
||||||
'commitId',
|
|
||||||
'singleDiscussionByLineCode',
|
|
||||||
'shouldRenderParallelCommentRow',
|
|
||||||
]),
|
|
||||||
...mapState({
|
...mapState({
|
||||||
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
|
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
|
||||||
}),
|
}),
|
||||||
parallelDiffLines() {
|
|
||||||
return this.diffLines.map(line => {
|
|
||||||
const parallelLine = Object.assign({}, line);
|
|
||||||
|
|
||||||
if (line.left) {
|
|
||||||
parallelLine.left = trimFirstCharOfLineContent(line.left);
|
|
||||||
} else {
|
|
||||||
parallelLine.left = { type: EMPTY_CELL_TYPE };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.right) {
|
|
||||||
parallelLine.right = trimFirstCharOfLineContent(line.right);
|
|
||||||
} else {
|
|
||||||
parallelLine.right = { type: EMPTY_CELL_TYPE };
|
|
||||||
}
|
|
||||||
|
|
||||||
return parallelLine;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
diffLinesLength() {
|
diffLinesLength() {
|
||||||
return this.parallelDiffLines.length;
|
return this.diffLines.length;
|
||||||
},
|
},
|
||||||
userColorScheme() {
|
userColorScheme() {
|
||||||
return window.gon.user_color_scheme;
|
return window.gon.user_color_scheme;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
discussionsByLine(line, leftOrRight) {
|
|
||||||
return line[leftOrRight] && line[leftOrRight].lineCode !== undefined ?
|
|
||||||
this.singleDiscussionByLineCode(line[leftOrRight].lineCode) : [];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -73,7 +42,7 @@ export default {
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<template
|
<template
|
||||||
v-for="(line, index) in parallelDiffLines"
|
v-for="(line, index) in diffLines"
|
||||||
>
|
>
|
||||||
<parallel-diff-table-row
|
<parallel-diff-table-row
|
||||||
:file-hash="diffFile.fileHash"
|
:file-hash="diffFile.fileHash"
|
||||||
|
@ -81,8 +50,6 @@ export default {
|
||||||
:line="line"
|
:line="line"
|
||||||
:is-bottom="index + 1 === diffLinesLength"
|
:is-bottom="index + 1 === diffLinesLength"
|
||||||
:key="index"
|
:key="index"
|
||||||
:left-discussions="discussionsByLine(line, 'left')"
|
|
||||||
:right-discussions="discussionsByLine(line, 'right')"
|
|
||||||
/>
|
/>
|
||||||
<parallel-diff-comment-row
|
<parallel-diff-comment-row
|
||||||
v-if="shouldRenderParallelCommentRow(line)"
|
v-if="shouldRenderParallelCommentRow(line)"
|
||||||
|
@ -90,8 +57,6 @@ export default {
|
||||||
:line="line"
|
:line="line"
|
||||||
:diff-file-hash="diffFile.fileHash"
|
:diff-file-hash="diffFile.fileHash"
|
||||||
:line-index="index"
|
:line-index="index"
|
||||||
:left-discussions="discussionsByLine(line, 'left')"
|
|
||||||
:right-discussions="discussionsByLine(line, 'right')"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import axios from '~/lib/utils/axios_utils';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
|
import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
|
||||||
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||||
|
import { getDiffPositionByLineCode } from './utils';
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
import {
|
import {
|
||||||
PARALLEL_DIFF_VIEW_TYPE,
|
PARALLEL_DIFF_VIEW_TYPE,
|
||||||
|
@ -29,25 +30,53 @@ export const fetchDiffFiles = ({ state, commit }) => {
|
||||||
.then(handleLocationHash);
|
.then(handleLocationHash);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const startRenderDiffsQueue = ({ state, commit }) => {
|
// This is adding line discussions to the actual lines in the diff tree
|
||||||
const checkItem = () => {
|
// once for parallel and once for inline mode
|
||||||
const nextFile = state.diffFiles.find(
|
export const assignDiscussionsToDiff = ({ state, commit }, allLineDiscussions) => {
|
||||||
file => !file.renderIt && (!file.collapsed || !file.text),
|
const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
|
||||||
);
|
|
||||||
if (nextFile) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
commit(types.RENDER_FILE, nextFile);
|
|
||||||
});
|
|
||||||
requestIdleCallback(
|
|
||||||
() => {
|
|
||||||
checkItem();
|
|
||||||
},
|
|
||||||
{ timeout: 1000 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkItem();
|
Object.values(allLineDiscussions).forEach(discussions => {
|
||||||
|
if (discussions.length > 0) {
|
||||||
|
const { fileHash } = discussions[0];
|
||||||
|
commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
|
||||||
|
fileHash,
|
||||||
|
discussions,
|
||||||
|
diffPositionByLineCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
|
||||||
|
const { fileHash, line_code } = removeDiscussion;
|
||||||
|
commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode: line_code });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const startRenderDiffsQueue = ({ state, commit }) => {
|
||||||
|
const checkItem = () =>
|
||||||
|
new Promise(resolve => {
|
||||||
|
const nextFile = state.diffFiles.find(
|
||||||
|
file => !file.renderIt && (!file.collapsed || !file.text),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nextFile) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
commit(types.RENDER_FILE, nextFile);
|
||||||
|
});
|
||||||
|
requestIdleCallback(
|
||||||
|
() => {
|
||||||
|
checkItem()
|
||||||
|
.then(resolve)
|
||||||
|
.catch(() => {});
|
||||||
|
},
|
||||||
|
{ timeout: 1000 },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return checkItem();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setInlineDiffViewType = ({ commit }) => {
|
export const setInlineDiffViewType = ({ commit }) => {
|
||||||
|
|
|
@ -17,7 +17,10 @@ export const commitId = state => (state.commit && state.commit.id ? state.commit
|
||||||
export const diffHasAllExpandedDiscussions = (state, getters) => diff => {
|
export const diffHasAllExpandedDiscussions = (state, getters) => diff => {
|
||||||
const discussions = getters.getDiffFileDiscussions(diff);
|
const discussions = getters.getDiffFileDiscussions(diff);
|
||||||
|
|
||||||
return (discussions.length && discussions.every(discussion => discussion.expanded)) || false;
|
return (
|
||||||
|
(discussions && discussions.length && discussions.every(discussion => discussion.expanded)) ||
|
||||||
|
false
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,7 +31,10 @@ export const diffHasAllExpandedDiscussions = (state, getters) => diff => {
|
||||||
export const diffHasAllCollpasedDiscussions = (state, getters) => diff => {
|
export const diffHasAllCollpasedDiscussions = (state, getters) => diff => {
|
||||||
const discussions = getters.getDiffFileDiscussions(diff);
|
const discussions = getters.getDiffFileDiscussions(diff);
|
||||||
|
|
||||||
return (discussions.length && discussions.every(discussion => !discussion.expanded)) || false;
|
return (
|
||||||
|
(discussions && discussions.length && discussions.every(discussion => !discussion.expanded)) ||
|
||||||
|
false
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,7 +46,9 @@ export const diffHasExpandedDiscussions = (state, getters) => diff => {
|
||||||
const discussions = getters.getDiffFileDiscussions(diff);
|
const discussions = getters.getDiffFileDiscussions(diff);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(discussions.length && discussions.find(discussion => discussion.expanded) !== undefined) ||
|
(discussions &&
|
||||||
|
discussions.length &&
|
||||||
|
discussions.find(discussion => discussion.expanded) !== undefined) ||
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -64,45 +72,38 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) =
|
||||||
discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash),
|
discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash),
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
export const singleDiscussionByLineCode = (state, getters, rootState, rootGetters) => lineCode => {
|
export const shouldRenderParallelCommentRow = state => line => {
|
||||||
if (!lineCode || lineCode === undefined) return [];
|
const hasDiscussion =
|
||||||
const discussions = rootGetters.discussionsByLineCode;
|
(line.left && line.left.discussions && line.left.discussions.length) ||
|
||||||
return discussions[lineCode] || [];
|
(line.right && line.right.discussions && line.right.discussions.length);
|
||||||
};
|
|
||||||
|
|
||||||
export const shouldRenderParallelCommentRow = (state, getters) => line => {
|
const hasExpandedDiscussionOnLeft =
|
||||||
const leftLineCode = line.left.lineCode;
|
line.left && line.left.discussions && line.left.discussions.length
|
||||||
const rightLineCode = line.right.lineCode;
|
? line.left.discussions.every(discussion => discussion.expanded)
|
||||||
const leftDiscussions = getters.singleDiscussionByLineCode(leftLineCode);
|
: false;
|
||||||
const rightDiscussions = getters.singleDiscussionByLineCode(rightLineCode);
|
const hasExpandedDiscussionOnRight =
|
||||||
const hasDiscussion = leftDiscussions.length || rightDiscussions.length;
|
line.right && line.right.discussions && line.right.discussions.length
|
||||||
|
? line.right.discussions.every(discussion => discussion.expanded)
|
||||||
const hasExpandedDiscussionOnLeft = leftDiscussions.length
|
: false;
|
||||||
? leftDiscussions.every(discussion => discussion.expanded)
|
|
||||||
: false;
|
|
||||||
const hasExpandedDiscussionOnRight = rightDiscussions.length
|
|
||||||
? rightDiscussions.every(discussion => discussion.expanded)
|
|
||||||
: false;
|
|
||||||
|
|
||||||
if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
|
if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasCommentFormOnLeft = state.diffLineCommentForms[leftLineCode];
|
const hasCommentFormOnLeft = line.left && state.diffLineCommentForms[line.left.lineCode];
|
||||||
const hasCommentFormOnRight = state.diffLineCommentForms[rightLineCode];
|
const hasCommentFormOnRight = line.right && state.diffLineCommentForms[line.right.lineCode];
|
||||||
|
|
||||||
return hasCommentFormOnLeft || hasCommentFormOnRight;
|
return hasCommentFormOnLeft || hasCommentFormOnRight;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shouldRenderInlineCommentRow = (state, getters) => line => {
|
export const shouldRenderInlineCommentRow = state => line => {
|
||||||
if (state.diffLineCommentForms[line.lineCode]) return true;
|
if (state.diffLineCommentForms[line.lineCode]) return true;
|
||||||
|
|
||||||
const lineDiscussions = getters.singleDiscussionByLineCode(line.lineCode);
|
if (!line.discussions || line.discussions.length === 0) {
|
||||||
if (lineDiscussions.length === 0) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return lineDiscussions.every(discussion => discussion.expanded);
|
return line.discussions.every(discussion => discussion.expanded);
|
||||||
};
|
};
|
||||||
|
|
||||||
// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests
|
// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests
|
||||||
|
|
|
@ -9,3 +9,5 @@ export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES';
|
||||||
export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS';
|
export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS';
|
||||||
export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
|
export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
|
||||||
export const RENDER_FILE = 'RENDER_FILE';
|
export const RENDER_FILE = 'RENDER_FILE';
|
||||||
|
export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE';
|
||||||
|
export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE';
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import _ from 'underscore';
|
|
||||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||||
import { findDiffFile, addLineReferences, removeMatchLine, addContextLines } from './utils';
|
import {
|
||||||
import { LINES_TO_BE_RENDERED_DIRECTLY, MAX_LINES_TO_BE_RENDERED } from '../constants';
|
findDiffFile,
|
||||||
|
addLineReferences,
|
||||||
|
removeMatchLine,
|
||||||
|
addContextLines,
|
||||||
|
prepareDiffData,
|
||||||
|
isDiscussionApplicableToLine,
|
||||||
|
} from './utils';
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -17,38 +22,7 @@ export default {
|
||||||
|
|
||||||
[types.SET_DIFF_DATA](state, data) {
|
[types.SET_DIFF_DATA](state, data) {
|
||||||
const diffData = convertObjectPropsToCamelCase(data, { deep: true });
|
const diffData = convertObjectPropsToCamelCase(data, { deep: true });
|
||||||
let showingLines = 0;
|
prepareDiffData(diffData);
|
||||||
const filesLength = diffData.diffFiles.length;
|
|
||||||
let i;
|
|
||||||
for (i = 0; i < filesLength; i += 1) {
|
|
||||||
const file = diffData.diffFiles[i];
|
|
||||||
if (file.parallelDiffLines) {
|
|
||||||
const linesLength = file.parallelDiffLines.length;
|
|
||||||
let u = 0;
|
|
||||||
for (u = 0; u < linesLength; u += 1) {
|
|
||||||
const line = file.parallelDiffLines[u];
|
|
||||||
if (line.left) delete line.left.text;
|
|
||||||
if (line.right) delete line.right.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.highlightedDiffLines) {
|
|
||||||
const linesLength = file.highlightedDiffLines.length;
|
|
||||||
let u;
|
|
||||||
for (u = 0; u < linesLength; u += 1) {
|
|
||||||
const line = file.highlightedDiffLines[u];
|
|
||||||
delete line.text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.highlightedDiffLines) {
|
|
||||||
showingLines += file.parallelDiffLines.length;
|
|
||||||
}
|
|
||||||
Object.assign(file, {
|
|
||||||
renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
|
|
||||||
collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(state, {
|
Object.assign(state, {
|
||||||
...diffData,
|
...diffData,
|
||||||
|
@ -98,19 +72,93 @@ export default {
|
||||||
|
|
||||||
[types.ADD_COLLAPSED_DIFFS](state, { file, data }) {
|
[types.ADD_COLLAPSED_DIFFS](state, { file, data }) {
|
||||||
const normalizedData = convertObjectPropsToCamelCase(data, { deep: true });
|
const normalizedData = convertObjectPropsToCamelCase(data, { deep: true });
|
||||||
|
prepareDiffData(normalizedData);
|
||||||
const [newFileData] = normalizedData.diffFiles.filter(f => f.fileHash === file.fileHash);
|
const [newFileData] = normalizedData.diffFiles.filter(f => f.fileHash === file.fileHash);
|
||||||
|
const selectedFile = state.diffFiles.find(f => f.fileHash === file.fileHash);
|
||||||
if (newFileData) {
|
Object.assign(selectedFile, { ...newFileData });
|
||||||
const index = _.findIndex(state.diffFiles, f => f.fileHash === file.fileHash);
|
|
||||||
state.diffFiles.splice(index, 1, newFileData);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.EXPAND_ALL_FILES](state) {
|
[types.EXPAND_ALL_FILES](state) {
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
state.diffFiles = state.diffFiles.map(file => ({
|
state.diffFiles = state.diffFiles.map(file => ({
|
||||||
...file,
|
...file,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, discussions, diffPositionByLineCode }) {
|
||||||
|
const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash);
|
||||||
|
const firstDiscussion = discussions[0];
|
||||||
|
const isDiffDiscussion = firstDiscussion.diff_discussion;
|
||||||
|
const hasLineCode = firstDiscussion.line_code;
|
||||||
|
const isResolvable = firstDiscussion.resolvable;
|
||||||
|
const diffPosition = diffPositionByLineCode[firstDiscussion.line_code];
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectedFile &&
|
||||||
|
isDiffDiscussion &&
|
||||||
|
hasLineCode &&
|
||||||
|
isResolvable &&
|
||||||
|
diffPosition &&
|
||||||
|
isDiscussionApplicableToLine(firstDiscussion, diffPosition)
|
||||||
|
) {
|
||||||
|
const targetLine = selectedFile.parallelDiffLines.find(
|
||||||
|
line =>
|
||||||
|
(line.left && line.left.lineCode === firstDiscussion.line_code) ||
|
||||||
|
(line.right && line.right.lineCode === firstDiscussion.line_code),
|
||||||
|
);
|
||||||
|
if (targetLine) {
|
||||||
|
if (targetLine.left && targetLine.left.lineCode === firstDiscussion.line_code) {
|
||||||
|
Object.assign(targetLine.left, {
|
||||||
|
discussions,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.assign(targetLine.right, {
|
||||||
|
discussions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedFile.highlightedDiffLines) {
|
||||||
|
const targetInlineLine = selectedFile.highlightedDiffLines.find(
|
||||||
|
line => line.lineCode === firstDiscussion.line_code,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetInlineLine) {
|
||||||
|
Object.assign(targetInlineLine, {
|
||||||
|
discussions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
|
||||||
|
const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash);
|
||||||
|
if (selectedFile) {
|
||||||
|
const targetLine = selectedFile.parallelDiffLines.find(
|
||||||
|
line =>
|
||||||
|
(line.left && line.left.lineCode === lineCode) ||
|
||||||
|
(line.right && line.right.lineCode === lineCode),
|
||||||
|
);
|
||||||
|
if (targetLine) {
|
||||||
|
const side = targetLine.left && targetLine.left.lineCode === lineCode ? 'left' : 'right';
|
||||||
|
|
||||||
|
Object.assign(targetLine[side], {
|
||||||
|
discussions: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedFile.highlightedDiffLines) {
|
||||||
|
const targetInlineLine = selectedFile.highlightedDiffLines.find(
|
||||||
|
line => line.lineCode === lineCode,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetInlineLine) {
|
||||||
|
Object.assign(targetInlineLine, {
|
||||||
|
discussions: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,6 +8,8 @@ import {
|
||||||
NEW_LINE_TYPE,
|
NEW_LINE_TYPE,
|
||||||
OLD_LINE_TYPE,
|
OLD_LINE_TYPE,
|
||||||
MATCH_LINE_TYPE,
|
MATCH_LINE_TYPE,
|
||||||
|
LINES_TO_BE_RENDERED_DIRECTLY,
|
||||||
|
MAX_LINES_TO_BE_RENDERED,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
|
|
||||||
export function findDiffFile(files, hash) {
|
export function findDiffFile(files, hash) {
|
||||||
|
@ -161,6 +163,11 @@ export function addContextLines(options) {
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
export function trimFirstCharOfLineContent(line = {}) {
|
export function trimFirstCharOfLineContent(line = {}) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
delete line.text;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
line.discussions = [];
|
||||||
|
|
||||||
const parsedLine = Object.assign({}, line);
|
const parsedLine = Object.assign({}, line);
|
||||||
|
|
||||||
if (line.richText) {
|
if (line.richText) {
|
||||||
|
@ -174,7 +181,44 @@ export function trimFirstCharOfLineContent(line = {}) {
|
||||||
return parsedLine;
|
return parsedLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDiffRefsByLineCode(diffFiles) {
|
// This prepares and optimizes the incoming diff data from the server
|
||||||
|
// by setting up incremental rendering and removing unneeded data
|
||||||
|
export function prepareDiffData(diffData) {
|
||||||
|
const filesLength = diffData.diffFiles.length;
|
||||||
|
let showingLines = 0;
|
||||||
|
for (let i = 0; i < filesLength; i += 1) {
|
||||||
|
const file = diffData.diffFiles[i];
|
||||||
|
|
||||||
|
if (file.parallelDiffLines) {
|
||||||
|
const linesLength = file.parallelDiffLines.length;
|
||||||
|
for (let u = 0; u < linesLength; u += 1) {
|
||||||
|
const line = file.parallelDiffLines[u];
|
||||||
|
if (line.left) {
|
||||||
|
line.left = trimFirstCharOfLineContent(line.left);
|
||||||
|
}
|
||||||
|
if (line.right) {
|
||||||
|
line.right = trimFirstCharOfLineContent(line.right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.highlightedDiffLines) {
|
||||||
|
const linesLength = file.highlightedDiffLines.length;
|
||||||
|
for (let u = 0; u < linesLength; u += 1) {
|
||||||
|
const line = file.highlightedDiffLines[u];
|
||||||
|
Object.assign(line, { ...trimFirstCharOfLineContent(line) });
|
||||||
|
}
|
||||||
|
showingLines += file.parallelDiffLines.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(file, {
|
||||||
|
renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
|
||||||
|
collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDiffPositionByLineCode(diffFiles) {
|
||||||
return diffFiles.reduce((acc, diffFile) => {
|
return diffFiles.reduce((acc, diffFile) => {
|
||||||
const { baseSha, headSha, startSha } = diffFile.diffRefs;
|
const { baseSha, headSha, startSha } = diffFile.diffRefs;
|
||||||
const { newPath, oldPath } = diffFile;
|
const { newPath, oldPath } = diffFile;
|
||||||
|
@ -194,3 +238,12 @@ export function getDiffRefsByLineCode(diffFiles) {
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This method will check whether the discussion is still applicable
|
||||||
|
// to the diff line in question regarding different versions of the MR
|
||||||
|
export function isDiscussionApplicableToLine(discussion, diffPosition) {
|
||||||
|
const originalRefs = convertObjectPropsToCamelCase(discussion.original_position.formatter);
|
||||||
|
const refs = convertObjectPropsToCamelCase(discussion.position.formatter);
|
||||||
|
|
||||||
|
return _.isEqual(refs, diffPosition) || _.isEqual(originalRefs, diffPosition);
|
||||||
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ import axios from '~/lib/utils/axios_utils';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import Flash from '~/flash';
|
import Flash from '~/flash';
|
||||||
|
|
||||||
export default function gcpSignupOffer() {
|
export default function initDismissableCallout(alertSelector) {
|
||||||
const alertEl = document.querySelector('.gcp-signup-offer');
|
const alertEl = document.querySelector(alertSelector);
|
||||||
if (!alertEl) {
|
if (!alertEl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
|
@ -7,6 +7,19 @@ import axios from './lib/utils/axios_utils';
|
||||||
|
|
||||||
Dropzone.autoDiscover = false;
|
Dropzone.autoDiscover = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the error message string from the given response.
|
||||||
|
*
|
||||||
|
* @param {String|Object} res
|
||||||
|
*/
|
||||||
|
function getErrorMessage(res) {
|
||||||
|
if (!res || _.isString(res)) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.message;
|
||||||
|
}
|
||||||
|
|
||||||
export default function dropzoneInput(form) {
|
export default function dropzoneInput(form) {
|
||||||
const divHover = '<div class="div-dropzone-hover"></div>';
|
const divHover = '<div class="div-dropzone-hover"></div>';
|
||||||
const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
|
const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
|
||||||
|
@ -18,7 +31,7 @@ export default function dropzoneInput(form) {
|
||||||
const $uploadingErrorContainer = form.find('.uploading-error-container');
|
const $uploadingErrorContainer = form.find('.uploading-error-container');
|
||||||
const $uploadingErrorMessage = form.find('.uploading-error-message');
|
const $uploadingErrorMessage = form.find('.uploading-error-message');
|
||||||
const $uploadingProgressContainer = form.find('.uploading-progress-container');
|
const $uploadingProgressContainer = form.find('.uploading-progress-container');
|
||||||
const uploadsPath = window.uploads_path || null;
|
const uploadsPath = form.data('uploads-path') || window.uploads_path || null;
|
||||||
const maxFileSize = gon.max_file_size || 10;
|
const maxFileSize = gon.max_file_size || 10;
|
||||||
const formTextarea = form.find('.js-gfm-input');
|
const formTextarea = form.find('.js-gfm-input');
|
||||||
let handlePaste;
|
let handlePaste;
|
||||||
|
@ -42,7 +55,7 @@ export default function dropzoneInput(form) {
|
||||||
|
|
||||||
if (!uploadsPath) {
|
if (!uploadsPath) {
|
||||||
$formDropzone.addClass('js-invalid-dropzone');
|
$formDropzone.addClass('js-invalid-dropzone');
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dropzone = $formDropzone.dropzone({
|
const dropzone = $formDropzone.dropzone({
|
||||||
|
@ -84,9 +97,7 @@ export default function dropzoneInput(form) {
|
||||||
// xhr object (xhr.responseText is error message).
|
// xhr object (xhr.responseText is error message).
|
||||||
// On error we hide the 'Attach' and 'Cancel' buttons
|
// On error we hide the 'Attach' and 'Cancel' buttons
|
||||||
// and show an error.
|
// and show an error.
|
||||||
|
const message = getErrorMessage(errorMessage || xhr.responseText);
|
||||||
// If there's xhr error message, let's show it instead of dropzone's one.
|
|
||||||
const message = xhr ? xhr.responseText : errorMessage;
|
|
||||||
|
|
||||||
$uploadingErrorContainer.removeClass('hide');
|
$uploadingErrorContainer.removeClass('hide');
|
||||||
$uploadingErrorMessage.html(message);
|
$uploadingErrorMessage.html(message);
|
||||||
|
@ -274,4 +285,6 @@ export default function dropzoneInput(form) {
|
||||||
$(this).closest('.gfm-form').find('.div-dropzone').click();
|
$(this).closest('.gfm-form').find('.div-dropzone').click();
|
||||||
formTextarea.focus();
|
formTextarea.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return Dropzone.forElement($formDropzone.get(0));
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,8 +65,8 @@ export const hideMenu = (el) => {
|
||||||
|
|
||||||
const parentEl = el.parentNode;
|
const parentEl = el.parentNode;
|
||||||
|
|
||||||
el.style.display = ''; // eslint-disable-line no-param-reassign
|
el.style.display = '';
|
||||||
el.style.transform = ''; // eslint-disable-line no-param-reassign
|
el.style.transform = '';
|
||||||
el.classList.remove(IS_ABOVE_CLASS);
|
el.classList.remove(IS_ABOVE_CLASS);
|
||||||
parentEl.classList.remove(IS_OVER_CLASS);
|
parentEl.classList.remove(IS_OVER_CLASS);
|
||||||
parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS);
|
parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS);
|
||||||
|
|
|
@ -2,14 +2,15 @@
|
||||||
/* global Flash */
|
/* global Flash */
|
||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { s__ } from '~/locale';
|
import { s__, sprintf } from '~/locale';
|
||||||
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
|
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||||
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
|
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
|
||||||
|
import { HIDDEN_CLASS } from '~/lib/utils/constants';
|
||||||
import { getParameterByName } from '~/lib/utils/common_utils';
|
import { getParameterByName } from '~/lib/utils/common_utils';
|
||||||
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||||
|
|
||||||
import eventHub from '../event_hub';
|
import eventHub from '../event_hub';
|
||||||
import { COMMON_STR } from '../constants';
|
import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants';
|
||||||
import groupsComponent from './groups.vue';
|
import groupsComponent from './groups.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -19,6 +20,16 @@ export default {
|
||||||
groupsComponent,
|
groupsComponent,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
action: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
containerId: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
store: {
|
store: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -56,31 +67,28 @@ export default {
|
||||||
? COMMON_STR.GROUP_SEARCH_EMPTY
|
? COMMON_STR.GROUP_SEARCH_EMPTY
|
||||||
: COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
|
: COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
|
||||||
|
|
||||||
eventHub.$on('fetchPage', this.fetchPage);
|
eventHub.$on(`${this.action}fetchPage`, this.fetchPage);
|
||||||
eventHub.$on('toggleChildren', this.toggleChildren);
|
eventHub.$on(`${this.action}toggleChildren`, this.toggleChildren);
|
||||||
eventHub.$on('showLeaveGroupModal', this.showLeaveGroupModal);
|
eventHub.$on(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
|
||||||
eventHub.$on('updatePagination', this.updatePagination);
|
eventHub.$on(`${this.action}updatePagination`, this.updatePagination);
|
||||||
eventHub.$on('updateGroups', this.updateGroups);
|
eventHub.$on(`${this.action}updateGroups`, this.updateGroups);
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchAllGroups();
|
this.fetchAllGroups();
|
||||||
|
|
||||||
|
if (this.containerId) {
|
||||||
|
this.containerEl = document.getElementById(this.containerId);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
eventHub.$off('fetchPage', this.fetchPage);
|
eventHub.$off(`${this.action}fetchPage`, this.fetchPage);
|
||||||
eventHub.$off('toggleChildren', this.toggleChildren);
|
eventHub.$off(`${this.action}toggleChildren`, this.toggleChildren);
|
||||||
eventHub.$off('showLeaveGroupModal', this.showLeaveGroupModal);
|
eventHub.$off(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
|
||||||
eventHub.$off('updatePagination', this.updatePagination);
|
eventHub.$off(`${this.action}updatePagination`, this.updatePagination);
|
||||||
eventHub.$off('updateGroups', this.updateGroups);
|
eventHub.$off(`${this.action}updateGroups`, this.updateGroups);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchGroups({
|
fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) {
|
||||||
parentId,
|
|
||||||
page,
|
|
||||||
filterGroupsBy,
|
|
||||||
sortBy,
|
|
||||||
archived,
|
|
||||||
updatePagination,
|
|
||||||
}) {
|
|
||||||
return this.service
|
return this.service
|
||||||
.getGroups(parentId, page, filterGroupsBy, sortBy, archived)
|
.getGroups(parentId, page, filterGroupsBy, sortBy, archived)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
@ -165,13 +173,13 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showLeaveGroupModal(group, parentGroup) {
|
showLeaveGroupModal(group, parentGroup) {
|
||||||
|
const { fullName } = group;
|
||||||
this.targetGroup = group;
|
this.targetGroup = group;
|
||||||
this.targetParentGroup = parentGroup;
|
this.targetParentGroup = parentGroup;
|
||||||
this.showModal = true;
|
this.showModal = true;
|
||||||
this.groupLeaveConfirmationMessage = s__(
|
this.groupLeaveConfirmationMessage = sprintf(
|
||||||
`GroupsTree|Are you sure you want to leave the "${
|
s__('GroupsTree|Are you sure you want to leave the "%{fullName}" group?'),
|
||||||
group.fullName
|
{ fullName },
|
||||||
}" group?`,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
hideLeaveGroupModal() {
|
hideLeaveGroupModal() {
|
||||||
|
@ -197,16 +205,35 @@ export default {
|
||||||
this.targetGroup.isBeingRemoved = false;
|
this.targetGroup.isBeingRemoved = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
showEmptyState() {
|
||||||
|
const { containerEl } = this;
|
||||||
|
const contentListEl = containerEl.querySelector(CONTENT_LIST_CLASS);
|
||||||
|
const emptyStateEl = containerEl.querySelector('.empty-state');
|
||||||
|
|
||||||
|
if (contentListEl) {
|
||||||
|
contentListEl.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emptyStateEl) {
|
||||||
|
emptyStateEl.classList.remove(HIDDEN_CLASS);
|
||||||
|
}
|
||||||
|
},
|
||||||
updatePagination(headers) {
|
updatePagination(headers) {
|
||||||
this.store.setPaginationInfo(headers);
|
this.store.setPaginationInfo(headers);
|
||||||
},
|
},
|
||||||
updateGroups(groups, fromSearch) {
|
updateGroups(groups, fromSearch) {
|
||||||
this.isSearchEmpty = groups ? groups.length === 0 : false;
|
const hasGroups = groups && groups.length > 0;
|
||||||
|
this.isSearchEmpty = !hasGroups;
|
||||||
|
|
||||||
if (fromSearch) {
|
if (fromSearch) {
|
||||||
this.store.setSearchedGroups(groups);
|
this.store.setSearchedGroups(groups);
|
||||||
} else {
|
} else {
|
||||||
this.store.setGroups(groups);
|
this.store.setGroups(groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.action && !hasGroups && !fromSearch) {
|
||||||
|
this.showEmptyState();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -226,6 +253,7 @@ export default {
|
||||||
:search-empty="isSearchEmpty"
|
:search-empty="isSearchEmpty"
|
||||||
:search-empty-message="searchEmptyMessage"
|
:search-empty-message="searchEmptyMessage"
|
||||||
:page-info="pageInfo"
|
:page-info="pageInfo"
|
||||||
|
:action="action"
|
||||||
/>
|
/>
|
||||||
<deprecated-modal
|
<deprecated-modal
|
||||||
v-show="showModal"
|
v-show="showModal"
|
||||||
|
|
|
@ -11,8 +11,12 @@ export default {
|
||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ([]),
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -37,6 +41,7 @@ export default {
|
||||||
:key="index"
|
:key="index"
|
||||||
:group="group"
|
:group="group"
|
||||||
:parent-group="parentGroup"
|
:parent-group="parentGroup"
|
||||||
|
:action="action"
|
||||||
/>
|
/>
|
||||||
<li
|
<li
|
||||||
v-if="hasMoreChildren"
|
v-if="hasMoreChildren"
|
||||||
|
|
|
@ -30,6 +30,11 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
action: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
groupDomId() {
|
groupDomId() {
|
||||||
|
@ -56,10 +61,12 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
onClickRowGroup(e) {
|
onClickRowGroup(e) {
|
||||||
const NO_EXPAND_CLS = 'no-expand';
|
const NO_EXPAND_CLS = 'no-expand';
|
||||||
if (!(e.target.classList.contains(NO_EXPAND_CLS) ||
|
const targetClasses = e.target.classList;
|
||||||
e.target.parentElement.classList.contains(NO_EXPAND_CLS))) {
|
const parentElClasses = e.target.parentElement.classList;
|
||||||
|
|
||||||
|
if (!(targetClasses.contains(NO_EXPAND_CLS) || parentElClasses.contains(NO_EXPAND_CLS))) {
|
||||||
if (this.hasChildren) {
|
if (this.hasChildren) {
|
||||||
eventHub.$emit('toggleChildren', this.group);
|
eventHub.$emit(`${this.action}toggleChildren`, this.group);
|
||||||
} else {
|
} else {
|
||||||
visitUrl(this.group.relativePath);
|
visitUrl(this.group.relativePath);
|
||||||
}
|
}
|
||||||
|
@ -93,7 +100,7 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
:class="{ 'content-loading': group.isChildrenLoading }"
|
:class="{ 'content-loading': group.isChildrenLoading }"
|
||||||
class="avatar-container s24 d-none d-sm-block"
|
class="avatar-container s24 d-none d-sm-flex"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
:href="group.relativePath"
|
:href="group.relativePath"
|
||||||
|
@ -158,6 +165,7 @@ export default {
|
||||||
v-if="group.isOpen && hasChildren"
|
v-if="group.isOpen && hasChildren"
|
||||||
:parent-group="group"
|
:parent-group="group"
|
||||||
:groups="group.children"
|
:groups="group.children"
|
||||||
|
:action="action"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,39 +1,44 @@
|
||||||
<script>
|
<script>
|
||||||
import tablePagination from '~/vue_shared/components/table_pagination.vue';
|
import tablePagination from '~/vue_shared/components/table_pagination.vue';
|
||||||
import eventHub from '../event_hub';
|
import eventHub from '../event_hub';
|
||||||
import { getParameterByName } from '../../lib/utils/common_utils';
|
import { getParameterByName } from '../../lib/utils/common_utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
tablePagination,
|
tablePagination,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
groups: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
props: {
|
pageInfo: {
|
||||||
groups: {
|
type: Object,
|
||||||
type: Array,
|
required: true,
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
pageInfo: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
searchEmpty: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
searchEmptyMessage: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
searchEmpty: {
|
||||||
change(page) {
|
type: Boolean,
|
||||||
const filterGroupsParam = getParameterByName('filter_groups');
|
required: true,
|
||||||
const sortParam = getParameterByName('sort');
|
|
||||||
const archivedParam = getParameterByName('archived');
|
|
||||||
eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
searchEmptyMessage: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
change(page) {
|
||||||
|
const filterGroupsParam = getParameterByName('filter_groups');
|
||||||
|
const sortParam = getParameterByName('sort');
|
||||||
|
const archivedParam = getParameterByName('archived');
|
||||||
|
eventHub.$emit(`${this.action}fetchPage`, page, filterGroupsParam, sortParam, archivedParam);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -47,6 +52,7 @@
|
||||||
<group-folder
|
<group-folder
|
||||||
v-if="!searchEmpty"
|
v-if="!searchEmpty"
|
||||||
:groups="groups"
|
:groups="groups"
|
||||||
|
:action="action"
|
||||||
/>
|
/>
|
||||||
<table-pagination
|
<table-pagination
|
||||||
v-if="!searchEmpty"
|
v-if="!searchEmpty"
|
||||||
|
|
|
@ -21,6 +21,11 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
action: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
leaveBtnTitle() {
|
leaveBtnTitle() {
|
||||||
|
@ -32,7 +37,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onLeaveGroup() {
|
onLeaveGroup() {
|
||||||
eventHub.$emit('showLeaveGroupModal', this.group, this.parentGroup);
|
eventHub.$emit(`${this.action}showLeaveGroupModal`, this.group, this.parentGroup);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,13 +2,23 @@ import { __, s__ } from '../locale';
|
||||||
|
|
||||||
export const MAX_CHILDREN_COUNT = 20;
|
export const MAX_CHILDREN_COUNT = 20;
|
||||||
|
|
||||||
|
export const ACTIVE_TAB_SUBGROUPS_AND_PROJECTS = 'subgroups_and_projects';
|
||||||
|
export const ACTIVE_TAB_SHARED = 'shared';
|
||||||
|
export const ACTIVE_TAB_ARCHIVED = 'archived';
|
||||||
|
|
||||||
|
export const GROUPS_LIST_HOLDER_CLASS = '.js-groups-list-holder';
|
||||||
|
export const GROUPS_FILTER_FORM_CLASS = '.js-group-filter-form';
|
||||||
|
export const CONTENT_LIST_CLASS = '.content-list';
|
||||||
|
|
||||||
export const COMMON_STR = {
|
export const COMMON_STR = {
|
||||||
FAILURE: __('An error occurred. Please try again.'),
|
FAILURE: __('An error occurred. Please try again.'),
|
||||||
LEAVE_FORBIDDEN: s__('GroupsTree|Failed to leave the group. Please make sure you are not the only owner.'),
|
LEAVE_FORBIDDEN: s__(
|
||||||
|
'GroupsTree|Failed to leave the group. Please make sure you are not the only owner.',
|
||||||
|
),
|
||||||
LEAVE_BTN_TITLE: s__('GroupsTree|Leave this group'),
|
LEAVE_BTN_TITLE: s__('GroupsTree|Leave this group'),
|
||||||
EDIT_BTN_TITLE: s__('GroupsTree|Edit group'),
|
EDIT_BTN_TITLE: s__('GroupsTree|Edit group'),
|
||||||
GROUP_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups matched your search'),
|
GROUP_SEARCH_EMPTY: s__('GroupsTree|No groups matched your search'),
|
||||||
GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups or projects matched your search'),
|
GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|No groups or projects matched your search'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ITEM_TYPE = {
|
export const ITEM_TYPE = {
|
||||||
|
@ -17,8 +27,12 @@ export const ITEM_TYPE = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GROUP_VISIBILITY_TYPE = {
|
export const GROUP_VISIBILITY_TYPE = {
|
||||||
public: __('Public - The group and any public projects can be viewed without any authentication.'),
|
public: __(
|
||||||
internal: __('Internal - The group and any internal projects can be viewed by any logged in user.'),
|
'Public - The group and any public projects can be viewed without any authentication.',
|
||||||
|
),
|
||||||
|
internal: __(
|
||||||
|
'Internal - The group and any internal projects can be viewed by any logged in user.',
|
||||||
|
),
|
||||||
private: __('Private - The group and its projects can only be viewed by members.'),
|
private: __('Private - The group and its projects can only be viewed by members.'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,23 @@ import eventHub from './event_hub';
|
||||||
import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils';
|
import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils';
|
||||||
|
|
||||||
export default class GroupFilterableList extends FilterableList {
|
export default class GroupFilterableList extends FilterableList {
|
||||||
constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) {
|
constructor({
|
||||||
|
form,
|
||||||
|
filter,
|
||||||
|
holder,
|
||||||
|
filterEndpoint,
|
||||||
|
pagePath,
|
||||||
|
dropdownSel,
|
||||||
|
filterInputField,
|
||||||
|
action,
|
||||||
|
}) {
|
||||||
super(form, filter, holder, filterInputField);
|
super(form, filter, holder, filterInputField);
|
||||||
this.form = form;
|
this.form = form;
|
||||||
this.filterEndpoint = filterEndpoint;
|
this.filterEndpoint = filterEndpoint;
|
||||||
this.pagePath = pagePath;
|
this.pagePath = pagePath;
|
||||||
this.filterInputField = filterInputField;
|
this.filterInputField = filterInputField;
|
||||||
this.$dropdown = $(dropdownSel);
|
this.$dropdown = $(dropdownSel);
|
||||||
|
this.action = action;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilterEndpoint() {
|
getFilterEndpoint() {
|
||||||
|
@ -20,15 +30,16 @@ export default class GroupFilterableList extends FilterableList {
|
||||||
getPagePath(queryData) {
|
getPagePath(queryData) {
|
||||||
const params = queryData ? $.param(queryData) : '';
|
const params = queryData ? $.param(queryData) : '';
|
||||||
const queryString = params ? `?${params}` : '';
|
const queryString = params ? `?${params}` : '';
|
||||||
return `${this.pagePath}${queryString}`;
|
const path = this.pagePath || window.location.pathname;
|
||||||
|
return `${path}${queryString}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
super.bindEvents();
|
super.bindEvents();
|
||||||
|
|
||||||
this.onFilterOptionClikWrapper = this.onOptionClick.bind(this);
|
this.onFilterOptionClickWrapper = this.onOptionClick.bind(this);
|
||||||
|
|
||||||
this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper);
|
this.$dropdown.on('click', 'a', this.onFilterOptionClickWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
onFilterInput() {
|
onFilterInput() {
|
||||||
|
@ -53,7 +64,12 @@ export default class GroupFilterableList extends FilterableList {
|
||||||
}
|
}
|
||||||
|
|
||||||
setDefaultFilterOption() {
|
setDefaultFilterOption() {
|
||||||
const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').first().text());
|
const defaultOption = $.trim(
|
||||||
|
this.$dropdown
|
||||||
|
.find('.dropdown-menu li.js-filter-sort-order a')
|
||||||
|
.first()
|
||||||
|
.text(),
|
||||||
|
);
|
||||||
this.$dropdown.find('.dropdown-label').text(defaultOption);
|
this.$dropdown.find('.dropdown-label').text(defaultOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,11 +81,19 @@ export default class GroupFilterableList extends FilterableList {
|
||||||
// Get type of option selected from dropdown
|
// Get type of option selected from dropdown
|
||||||
const currentTargetClassList = e.currentTarget.parentElement.classList;
|
const currentTargetClassList = e.currentTarget.parentElement.classList;
|
||||||
const isOptionFilterBySort = currentTargetClassList.contains('js-filter-sort-order');
|
const isOptionFilterBySort = currentTargetClassList.contains('js-filter-sort-order');
|
||||||
const isOptionFilterByArchivedProjects = currentTargetClassList.contains('js-filter-archived-projects');
|
const isOptionFilterByArchivedProjects = currentTargetClassList.contains(
|
||||||
|
'js-filter-archived-projects',
|
||||||
|
);
|
||||||
|
|
||||||
// Get option query param, also preserve currently applied query param
|
// Get option query param, also preserve currently applied query param
|
||||||
const sortParam = getParameterByName('sort', isOptionFilterBySort ? e.currentTarget.href : window.location.href);
|
const sortParam = getParameterByName(
|
||||||
const archivedParam = getParameterByName('archived', isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href);
|
'sort',
|
||||||
|
isOptionFilterBySort ? e.currentTarget.href : window.location.href,
|
||||||
|
);
|
||||||
|
const archivedParam = getParameterByName(
|
||||||
|
'archived',
|
||||||
|
isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href,
|
||||||
|
);
|
||||||
|
|
||||||
if (sortParam) {
|
if (sortParam) {
|
||||||
queryData.sort = sortParam;
|
queryData.sort = sortParam;
|
||||||
|
@ -86,7 +110,9 @@ export default class GroupFilterableList extends FilterableList {
|
||||||
this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text));
|
this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text));
|
||||||
this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').removeClass('is-active');
|
this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').removeClass('is-active');
|
||||||
} else if (isOptionFilterByArchivedProjects) {
|
} else if (isOptionFilterByArchivedProjects) {
|
||||||
this.$dropdown.find('.dropdown-menu li.js-filter-archived-projects a').removeClass('is-active');
|
this.$dropdown
|
||||||
|
.find('.dropdown-menu li.js-filter-archived-projects a')
|
||||||
|
.removeClass('is-active');
|
||||||
}
|
}
|
||||||
|
|
||||||
$(e.target).addClass('is-active');
|
$(e.target).addClass('is-active');
|
||||||
|
@ -98,11 +124,19 @@ export default class GroupFilterableList extends FilterableList {
|
||||||
onFilterSuccess(res, queryData) {
|
onFilterSuccess(res, queryData) {
|
||||||
const currentPath = this.getPagePath(queryData);
|
const currentPath = this.getPagePath(queryData);
|
||||||
|
|
||||||
window.history.replaceState({
|
window.history.replaceState(
|
||||||
page: currentPath,
|
{
|
||||||
}, document.title, currentPath);
|
page: currentPath,
|
||||||
|
},
|
||||||
|
document.title,
|
||||||
|
currentPath,
|
||||||
|
);
|
||||||
|
|
||||||
eventHub.$emit('updateGroups', res.data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField));
|
eventHub.$emit(
|
||||||
eventHub.$emit('updatePagination', normalizeHeaders(res.headers));
|
`${this.action}updateGroups`,
|
||||||
|
res.data,
|
||||||
|
Object.prototype.hasOwnProperty.call(queryData, this.filterInputField),
|
||||||
|
);
|
||||||
|
eventHub.$emit(`${this.action}updatePagination`, normalizeHeaders(res.headers));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,18 +7,26 @@ import GroupsService from './service/groups_service';
|
||||||
import groupsApp from './components/app.vue';
|
import groupsApp from './components/app.vue';
|
||||||
import groupFolderComponent from './components/group_folder.vue';
|
import groupFolderComponent from './components/group_folder.vue';
|
||||||
import groupItemComponent from './components/group_item.vue';
|
import groupItemComponent from './components/group_item.vue';
|
||||||
|
import { GROUPS_LIST_HOLDER_CLASS, CONTENT_LIST_CLASS } from './constants';
|
||||||
|
|
||||||
Vue.use(Translate);
|
Vue.use(Translate);
|
||||||
|
|
||||||
export default () => {
|
export default (containerId = 'js-groups-tree', endpoint, action = '') => {
|
||||||
const el = document.getElementById('js-groups-tree');
|
const containerEl = document.getElementById(containerId);
|
||||||
|
let dataEl;
|
||||||
|
|
||||||
// Don't do anything if element doesn't exist (No groups)
|
// Don't do anything if element doesn't exist (No groups)
|
||||||
// This is for when the user enters directly to the page via URL
|
// This is for when the user enters directly to the page via URL
|
||||||
if (!el) {
|
if (!containerEl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const el = action ? containerEl.querySelector(GROUPS_LIST_HOLDER_CLASS) : containerEl;
|
||||||
|
|
||||||
|
if (action) {
|
||||||
|
dataEl = containerEl.querySelector(CONTENT_LIST_CLASS);
|
||||||
|
}
|
||||||
|
|
||||||
Vue.component('group-folder', groupFolderComponent);
|
Vue.component('group-folder', groupFolderComponent);
|
||||||
Vue.component('group-item', groupItemComponent);
|
Vue.component('group-item', groupItemComponent);
|
||||||
|
|
||||||
|
@ -29,20 +37,26 @@ export default () => {
|
||||||
groupsApp,
|
groupsApp,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const { dataset } = this.$options.el;
|
const { dataset } = dataEl || this.$options.el;
|
||||||
const hideProjects = dataset.hideProjects === 'true';
|
const hideProjects = dataset.hideProjects === 'true';
|
||||||
|
const service = new GroupsService(endpoint || dataset.endpoint);
|
||||||
const store = new GroupsStore(hideProjects);
|
const store = new GroupsStore(hideProjects);
|
||||||
const service = new GroupsService(dataset.endpoint);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
action,
|
||||||
store,
|
store,
|
||||||
service,
|
service,
|
||||||
hideProjects,
|
hideProjects,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
containerId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
const { dataset } = this.$options.el;
|
if (this.action) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { dataset } = dataEl || this.$options.el;
|
||||||
let groupFilterList = null;
|
let groupFilterList = null;
|
||||||
const form = document.querySelector(dataset.formSel);
|
const form = document.querySelector(dataset.formSel);
|
||||||
const filter = document.querySelector(dataset.filterSel);
|
const filter = document.querySelector(dataset.filterSel);
|
||||||
|
@ -52,10 +66,11 @@ export default () => {
|
||||||
form,
|
form,
|
||||||
filter,
|
filter,
|
||||||
holder,
|
holder,
|
||||||
filterEndpoint: dataset.endpoint,
|
filterEndpoint: endpoint || dataset.endpoint,
|
||||||
pagePath: dataset.path,
|
pagePath: dataset.path,
|
||||||
dropdownSel: dataset.dropdownSel,
|
dropdownSel: dataset.dropdownSel,
|
||||||
filterInputField: 'filter',
|
filterInputField: 'filter',
|
||||||
|
action: this.action,
|
||||||
};
|
};
|
||||||
|
|
||||||
groupFilterList = new GroupFilterableList(opts);
|
groupFilterList = new GroupFilterableList(opts);
|
||||||
|
@ -64,9 +79,11 @@ export default () => {
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
return createElement('groups-app', {
|
return createElement('groups-app', {
|
||||||
props: {
|
props: {
|
||||||
|
action: this.action,
|
||||||
store: this.store,
|
store: this.store,
|
||||||
service: this.service,
|
service: this.service,
|
||||||
hideProjects: this.hideProjects,
|
hideProjects: this.hideProjects,
|
||||||
|
containerId: this.containerId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script>
|
||||||
|
import $ from 'jquery';
|
||||||
|
import { mapActions } from 'vuex';
|
||||||
|
import { __ } from '~/locale';
|
||||||
|
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||||
|
import ChangedFileIcon from '../changed_file_icon.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
FileIcon,
|
||||||
|
ChangedFileIcon,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
activeFile: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
activeButtonText() {
|
||||||
|
return this.activeFile.staged ? __('Unstage') : __('Stage');
|
||||||
|
},
|
||||||
|
isStaged() {
|
||||||
|
return !this.activeFile.changed && this.activeFile.staged;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['stageChange', 'unstageChange']),
|
||||||
|
actionButtonClicked() {
|
||||||
|
if (this.activeFile.staged) {
|
||||||
|
this.unstageChange(this.activeFile.path);
|
||||||
|
} else {
|
||||||
|
this.stageChange(this.activeFile.path);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showDiscardModal() {
|
||||||
|
$(document.getElementById(`discard-file-${this.activeFile.path}`)).modal('show');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="d-flex ide-commit-editor-header align-items-center">
|
||||||
|
<file-icon
|
||||||
|
:file-name="activeFile.name"
|
||||||
|
:size="16"
|
||||||
|
class="mr-2"
|
||||||
|
/>
|
||||||
|
<strong class="mr-2">
|
||||||
|
{{ activeFile.path }}
|
||||||
|
</strong>
|
||||||
|
<changed-file-icon
|
||||||
|
:file="activeFile"
|
||||||
|
/>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<button
|
||||||
|
v-if="!isStaged"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-remove btn-inverted append-right-8"
|
||||||
|
@click="showDiscardModal"
|
||||||
|
>
|
||||||
|
{{ __('Discard') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="{
|
||||||
|
'btn-success': !isStaged,
|
||||||
|
'btn-warning': isStaged
|
||||||
|
}"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-inverted"
|
||||||
|
@click="actionButtonClicked"
|
||||||
|
>
|
||||||
|
{{ activeButtonText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -1,7 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
|
import $ from 'jquery';
|
||||||
import { mapActions } from 'vuex';
|
import { mapActions } from 'vuex';
|
||||||
import { __, sprintf } from '~/locale';
|
import { __, sprintf } from '~/locale';
|
||||||
import Icon from '~/vue_shared/components/icon.vue';
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
|
import GlModal from '~/vue_shared/components/gl_modal.vue';
|
||||||
import tooltip from '~/vue_shared/directives/tooltip';
|
import tooltip from '~/vue_shared/directives/tooltip';
|
||||||
import ListItem from './list_item.vue';
|
import ListItem from './list_item.vue';
|
||||||
|
|
||||||
|
@ -9,6 +11,7 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
Icon,
|
Icon,
|
||||||
ListItem,
|
ListItem,
|
||||||
|
GlModal,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
tooltip,
|
tooltip,
|
||||||
|
@ -56,6 +59,11 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
emptyStateText: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: __('No changes'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
titleText() {
|
titleText() {
|
||||||
|
@ -68,11 +76,19 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['stageAllChanges', 'unstageAllChanges']),
|
...mapActions(['stageAllChanges', 'unstageAllChanges', 'discardAllChanges']),
|
||||||
actionBtnClicked() {
|
actionBtnClicked() {
|
||||||
this[this.action]();
|
this[this.action]();
|
||||||
|
|
||||||
|
$(this.$refs.actionBtn).tooltip('hide');
|
||||||
|
},
|
||||||
|
openDiscardModal() {
|
||||||
|
$('#discard-all-changes').modal('show');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
discardModalText: __(
|
||||||
|
"You will loose all the unstaged changes you've made in this project. This action cannot be undone.",
|
||||||
|
),
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -81,27 +97,32 @@ export default {
|
||||||
class="ide-commit-list-container"
|
class="ide-commit-list-container"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="multi-file-commit-panel-header"
|
class="multi-file-commit-panel-header d-flex mb-0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="multi-file-commit-panel-header-title"
|
class="d-flex align-items-center flex-fill"
|
||||||
>
|
>
|
||||||
<icon
|
<icon
|
||||||
v-once
|
v-once
|
||||||
:name="iconName"
|
:name="iconName"
|
||||||
:size="18"
|
:size="18"
|
||||||
|
class="append-right-8"
|
||||||
/>
|
/>
|
||||||
{{ titleText }}
|
<strong>
|
||||||
|
{{ titleText }}
|
||||||
|
</strong>
|
||||||
<div class="d-flex ml-auto">
|
<div class="d-flex ml-auto">
|
||||||
<button
|
<button
|
||||||
v-tooltip
|
v-tooltip
|
||||||
v-show="filesLength"
|
ref="actionBtn"
|
||||||
:class="{
|
|
||||||
'd-flex': filesLength
|
|
||||||
}"
|
|
||||||
:title="actionBtnText"
|
:title="actionBtnText"
|
||||||
|
:aria-label="actionBtnText"
|
||||||
|
:disabled="!filesLength"
|
||||||
|
:class="{
|
||||||
|
'disabled-content': !filesLength
|
||||||
|
}"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-default ide-staged-action-btn p-0 order-1 align-items-center"
|
class="d-flex ide-staged-action-btn p-0 border-0 align-items-center"
|
||||||
data-placement="bottom"
|
data-placement="bottom"
|
||||||
data-container="body"
|
data-container="body"
|
||||||
data-boundary="viewport"
|
data-boundary="viewport"
|
||||||
|
@ -109,18 +130,32 @@ export default {
|
||||||
>
|
>
|
||||||
<icon
|
<icon
|
||||||
:name="actionBtnIcon"
|
:name="actionBtnIcon"
|
||||||
:size="12"
|
:size="16"
|
||||||
class="ml-auto mr-auto"
|
class="ml-auto mr-auto"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<span
|
<button
|
||||||
|
v-tooltip
|
||||||
|
v-if="!stagedList"
|
||||||
|
:title="__('Discard all changes')"
|
||||||
|
:aria-label="__('Discard all changes')"
|
||||||
|
:disabled="!filesLength"
|
||||||
:class="{
|
:class="{
|
||||||
'rounded-right': !filesLength
|
'disabled-content': !filesLength
|
||||||
}"
|
}"
|
||||||
class="ide-commit-file-count order-0 rounded-left text-center"
|
type="button"
|
||||||
|
class="d-flex ide-staged-action-btn p-0 border-0 align-items-center"
|
||||||
|
data-placement="bottom"
|
||||||
|
data-container="body"
|
||||||
|
data-boundary="viewport"
|
||||||
|
@click="openDiscardModal"
|
||||||
>
|
>
|
||||||
{{ filesLength }}
|
<icon
|
||||||
</span>
|
:size="16"
|
||||||
|
name="remove-all"
|
||||||
|
class="ml-auto mr-auto"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@ -143,9 +178,19 @@ export default {
|
||||||
</ul>
|
</ul>
|
||||||
<p
|
<p
|
||||||
v-else
|
v-else
|
||||||
class="multi-file-commit-list form-text text-muted"
|
class="multi-file-commit-list form-text text-muted text-center"
|
||||||
>
|
>
|
||||||
{{ __('No changes') }}
|
{{ emptyStateText }}
|
||||||
</p>
|
</p>
|
||||||
|
<gl-modal
|
||||||
|
v-if="!stagedList"
|
||||||
|
id="discard-all-changes"
|
||||||
|
:footer-primary-button-text="__('Discard all changes')"
|
||||||
|
:header-title-text="__('Discard all unstaged changes?')"
|
||||||
|
footer-primary-button-variant="danger"
|
||||||
|
@submit="discardAllChanges"
|
||||||
|
>
|
||||||
|
{{ $options.discardModalText }}
|
||||||
|
</gl-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { mapActions } from 'vuex';
|
import { mapActions } from 'vuex';
|
||||||
import tooltip from '~/vue_shared/directives/tooltip';
|
import tooltip from '~/vue_shared/directives/tooltip';
|
||||||
import Icon from '~/vue_shared/components/icon.vue';
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
|
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||||
import StageButton from './stage_button.vue';
|
import StageButton from './stage_button.vue';
|
||||||
import UnstageButton from './unstage_button.vue';
|
import UnstageButton from './unstage_button.vue';
|
||||||
import { viewerTypes } from '../../constants';
|
import { viewerTypes } from '../../constants';
|
||||||
|
@ -12,6 +13,7 @@ export default {
|
||||||
Icon,
|
Icon,
|
||||||
StageButton,
|
StageButton,
|
||||||
UnstageButton,
|
UnstageButton,
|
||||||
|
FileIcon,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
tooltip,
|
tooltip,
|
||||||
|
@ -48,7 +50,7 @@ export default {
|
||||||
return `${getCommitIconMap(this.file).icon}${suffix}`;
|
return `${getCommitIconMap(this.file).icon}${suffix}`;
|
||||||
},
|
},
|
||||||
iconClass() {
|
iconClass() {
|
||||||
return `${getCommitIconMap(this.file).class} append-right-8`;
|
return `${getCommitIconMap(this.file).class} ml-auto mr-auto`;
|
||||||
},
|
},
|
||||||
fullKey() {
|
fullKey() {
|
||||||
return `${this.keyPrefix}-${this.file.key}`;
|
return `${this.keyPrefix}-${this.file.key}`;
|
||||||
|
@ -105,17 +107,24 @@ export default {
|
||||||
@click="openFileInEditor"
|
@click="openFileInEditor"
|
||||||
>
|
>
|
||||||
<span class="multi-file-commit-list-file-path d-flex align-items-center">
|
<span class="multi-file-commit-list-file-path d-flex align-items-center">
|
||||||
<icon
|
<file-icon
|
||||||
:name="iconName"
|
:file-name="file.name"
|
||||||
:size="16"
|
class="append-right-8"
|
||||||
:css-classes="iconClass"
|
|
||||||
/>{{ file.name }}
|
/>{{ file.name }}
|
||||||
</span>
|
</span>
|
||||||
|
<div class="ml-auto d-flex align-items-center">
|
||||||
|
<div class="d-flex align-items-center ide-commit-list-changed-icon">
|
||||||
|
<icon
|
||||||
|
:name="iconName"
|
||||||
|
:size="16"
|
||||||
|
:css-classes="iconClass"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<component
|
||||||
|
:is="actionComponent"
|
||||||
|
:path="file.path"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<component
|
|
||||||
:is="actionComponent"
|
|
||||||
:path="file.path"
|
|
||||||
class="d-flex position-absolute"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
<script>
|
<script>
|
||||||
|
import $ from 'jquery';
|
||||||
import { mapActions } from 'vuex';
|
import { mapActions } from 'vuex';
|
||||||
|
import { sprintf, __ } from '~/locale';
|
||||||
import Icon from '~/vue_shared/components/icon.vue';
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
import tooltip from '~/vue_shared/directives/tooltip';
|
import tooltip from '~/vue_shared/directives/tooltip';
|
||||||
|
import GlModal from '~/vue_shared/components/gl_modal.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Icon,
|
Icon,
|
||||||
|
GlModal,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
tooltip,
|
tooltip,
|
||||||
|
@ -16,8 +20,22 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
modalId() {
|
||||||
|
return `discard-file-${this.path}`;
|
||||||
|
},
|
||||||
|
modalTitle() {
|
||||||
|
return sprintf(
|
||||||
|
__('Discard changes to %{path}?'),
|
||||||
|
{ path: this.path },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['stageChange', 'discardFileChanges']),
|
...mapActions(['stageChange', 'discardFileChanges']),
|
||||||
|
showDiscardModal() {
|
||||||
|
$(document.getElementById(this.modalId)).modal('show');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -25,51 +43,50 @@ export default {
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-once
|
v-once
|
||||||
class="multi-file-discard-btn dropdown"
|
class="multi-file-discard-btn d-flex"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-tooltip
|
v-tooltip
|
||||||
:aria-label="__('Stage changes')"
|
:aria-label="__('Stage changes')"
|
||||||
:title="__('Stage changes')"
|
:title="__('Stage changes')"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-blank append-right-5 d-flex align-items-center"
|
class="btn btn-blank align-items-center"
|
||||||
data-container="body"
|
data-container="body"
|
||||||
data-boundary="viewport"
|
data-boundary="viewport"
|
||||||
data-placement="bottom"
|
data-placement="bottom"
|
||||||
@click.stop="stageChange(path)"
|
@click.stop.prevent="stageChange(path)"
|
||||||
>
|
>
|
||||||
<icon
|
<icon
|
||||||
:size="12"
|
:size="16"
|
||||||
name="mobile-issue-close"
|
name="mobile-issue-close"
|
||||||
|
class="ml-auto mr-auto"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-tooltip
|
v-tooltip
|
||||||
:title="__('More actions')"
|
:aria-label="__('Discard changes')"
|
||||||
|
:title="__('Discard changes')"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-blank d-flex align-items-center"
|
class="btn btn-blank align-items-center"
|
||||||
data-container="body"
|
data-container="body"
|
||||||
data-boundary="viewport"
|
data-boundary="viewport"
|
||||||
data-placement="bottom"
|
data-placement="bottom"
|
||||||
data-toggle="dropdown"
|
@click.stop.prevent="showDiscardModal"
|
||||||
data-display="static"
|
|
||||||
>
|
>
|
||||||
<icon
|
<icon
|
||||||
:size="12"
|
:size="16"
|
||||||
name="ellipsis_h"
|
name="remove"
|
||||||
|
class="ml-auto mr-auto"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu dropdown-menu-right">
|
<gl-modal
|
||||||
<ul>
|
:id="modalId"
|
||||||
<li>
|
:header-title-text="modalTitle"
|
||||||
<button
|
:footer-primary-button-text="__('Discard changes')"
|
||||||
type="button"
|
footer-primary-button-variant="danger"
|
||||||
@click.stop="discardFileChanges(path)"
|
@submit="discardFileChanges(path)"
|
||||||
>
|
>
|
||||||
{{ __('Discard changes') }}
|
{{ __("You will loose all changes you've made to this file. This action cannot be undone.") }}
|
||||||
</button>
|
</gl-modal>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -25,22 +25,23 @@ export default {
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-once
|
v-once
|
||||||
class="multi-file-discard-btn"
|
class="multi-file-discard-btn d-flex"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-tooltip
|
v-tooltip
|
||||||
:aria-label="__('Unstage changes')"
|
:aria-label="__('Unstage changes')"
|
||||||
:title="__('Unstage changes')"
|
:title="__('Unstage changes')"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-blank d-flex align-items-center"
|
class="btn btn-blank align-items-center"
|
||||||
data-container="body"
|
data-container="body"
|
||||||
data-boundary="viewport"
|
data-boundary="viewport"
|
||||||
data-placement="bottom"
|
data-placement="bottom"
|
||||||
@click="unstageChange(path)"
|
@click.stop.prevent="unstageChange(path)"
|
||||||
>
|
>
|
||||||
<icon
|
<icon
|
||||||
:size="12"
|
:size="16"
|
||||||
name="history"
|
name="redo"
|
||||||
|
class="ml-auto mr-auto"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
<script>
|
||||||
|
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||||
|
import Dropdown from './dropdown.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Dropdown,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['activeFile']),
|
||||||
|
...mapGetters('fileTemplates', ['templateTypes']),
|
||||||
|
...mapState('fileTemplates', ['selectedTemplateType', 'updateSuccess']),
|
||||||
|
showTemplatesDropdown() {
|
||||||
|
return Object.keys(this.selectedTemplateType).length > 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
activeFile: 'setInitialType',
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setInitialType();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions('fileTemplates', [
|
||||||
|
'setSelectedTemplateType',
|
||||||
|
'fetchTemplate',
|
||||||
|
'undoFileTemplate',
|
||||||
|
]),
|
||||||
|
setInitialType() {
|
||||||
|
const initialTemplateType = this.templateTypes.find(t => t.name === this.activeFile.name);
|
||||||
|
|
||||||
|
if (initialTemplateType) {
|
||||||
|
this.setSelectedTemplateType(initialTemplateType);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectTemplateType(templateType) {
|
||||||
|
this.setSelectedTemplateType(templateType);
|
||||||
|
},
|
||||||
|
selectTemplate(template) {
|
||||||
|
this.fetchTemplate(template);
|
||||||
|
},
|
||||||
|
undo() {
|
||||||
|
this.undoFileTemplate();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="d-flex align-items-center ide-file-templates">
|
||||||
|
<strong class="append-right-default">
|
||||||
|
{{ __('File templates') }}
|
||||||
|
</strong>
|
||||||
|
<dropdown
|
||||||
|
:data="templateTypes"
|
||||||
|
:label="selectedTemplateType.name || __('Choose a type...')"
|
||||||
|
class="mr-2"
|
||||||
|
@click="selectTemplateType"
|
||||||
|
/>
|
||||||
|
<dropdown
|
||||||
|
v-if="showTemplatesDropdown"
|
||||||
|
:label="__('Choose a template...')"
|
||||||
|
:is-async-data="true"
|
||||||
|
:searchable="true"
|
||||||
|
:title="__('File templates')"
|
||||||
|
class="mr-2"
|
||||||
|
@click="selectTemplate"
|
||||||
|
/>
|
||||||
|
<transition name="fade">
|
||||||
|
<button
|
||||||
|
v-show="updateSuccess"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-default"
|
||||||
|
@click="undo"
|
||||||
|
>
|
||||||
|
{{ __('Undo') }}
|
||||||
|
</button>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,125 @@
|
||||||
|
<script>
|
||||||
|
import $ from 'jquery';
|
||||||
|
import { mapActions, mapState } from 'vuex';
|
||||||
|
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||||
|
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
DropdownButton,
|
||||||
|
LoadingIcon,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
isAsyncData: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
searchable: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
search: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState('fileTemplates', ['templates', 'isLoading']),
|
||||||
|
outputData() {
|
||||||
|
return (this.isAsyncData ? this.templates : this.data).filter(t => {
|
||||||
|
if (!this.searchable) return true;
|
||||||
|
|
||||||
|
return t.name.toLowerCase().indexOf(this.search.toLowerCase()) >= 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
showLoading() {
|
||||||
|
return this.isAsyncData ? this.isLoading : false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
$(this.$el).on('show.bs.dropdown', this.fetchTemplatesIfAsync);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
$(this.$el).off('show.bs.dropdown', this.fetchTemplatesIfAsync);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions('fileTemplates', ['fetchTemplateTypes']),
|
||||||
|
fetchTemplatesIfAsync() {
|
||||||
|
if (this.isAsyncData) {
|
||||||
|
this.fetchTemplateTypes();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clickItem(item) {
|
||||||
|
this.$emit('click', item);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="dropdown">
|
||||||
|
<dropdown-button
|
||||||
|
:toggle-text="label"
|
||||||
|
data-display="static"
|
||||||
|
/>
|
||||||
|
<div class="dropdown-menu pb-0">
|
||||||
|
<div
|
||||||
|
v-if="title"
|
||||||
|
class="dropdown-title ml-0 mr-0"
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!showLoading && searchable"
|
||||||
|
class="dropdown-input"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="search"
|
||||||
|
:placeholder="__('Filter...')"
|
||||||
|
type="search"
|
||||||
|
class="dropdown-input-field"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
aria-hidden="true"
|
||||||
|
class="fa fa-search dropdown-input-search"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<loading-icon
|
||||||
|
v-if="showLoading"
|
||||||
|
size="2"
|
||||||
|
/>
|
||||||
|
<ul v-else>
|
||||||
|
<li
|
||||||
|
v-for="(item, index) in outputData"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="clickItem(item)"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -10,6 +10,7 @@ import RepoEditor from './repo_editor.vue';
|
||||||
import FindFile from './file_finder/index.vue';
|
import FindFile from './file_finder/index.vue';
|
||||||
import RightPane from './panes/right.vue';
|
import RightPane from './panes/right.vue';
|
||||||
import ErrorMessage from './error_message.vue';
|
import ErrorMessage from './error_message.vue';
|
||||||
|
import CommitEditorHeader from './commit_sidebar/editor_header.vue';
|
||||||
|
|
||||||
const originalStopCallback = Mousetrap.stopCallback;
|
const originalStopCallback = Mousetrap.stopCallback;
|
||||||
|
|
||||||
|
@ -23,6 +24,7 @@ export default {
|
||||||
FindFile,
|
FindFile,
|
||||||
RightPane,
|
RightPane,
|
||||||
ErrorMessage,
|
ErrorMessage,
|
||||||
|
CommitEditorHeader,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState([
|
...mapState([
|
||||||
|
@ -34,7 +36,7 @@ export default {
|
||||||
'currentProjectId',
|
'currentProjectId',
|
||||||
'errorMessage',
|
'errorMessage',
|
||||||
]),
|
]),
|
||||||
...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges']),
|
...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges', 'isCommitModeActive']),
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
window.onbeforeunload = e => this.onBeforeUnload(e);
|
window.onbeforeunload = e => this.onBeforeUnload(e);
|
||||||
|
@ -96,7 +98,12 @@ export default {
|
||||||
<template
|
<template
|
||||||
v-if="activeFile"
|
v-if="activeFile"
|
||||||
>
|
>
|
||||||
|
<commit-editor-header
|
||||||
|
v-if="isCommitModeActive"
|
||||||
|
:active-file="activeFile"
|
||||||
|
/>
|
||||||
<repo-tabs
|
<repo-tabs
|
||||||
|
v-else
|
||||||
:active-file="activeFile"
|
:active-file="activeFile"
|
||||||
:files="openFiles"
|
:files="openFiles"
|
||||||
:viewer="viewer"
|
:viewer="viewer"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
|
import $ from 'jquery';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import { mapActions, mapState } from 'vuex';
|
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||||
import GlModal from '~/vue_shared/components/gl_modal.vue';
|
import GlModal from '~/vue_shared/components/gl_modal.vue';
|
||||||
import { modalTypes } from '../../constants';
|
import { modalTypes } from '../../constants';
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['entryModal']),
|
...mapState(['entryModal']),
|
||||||
|
...mapGetters('fileTemplates', ['templateTypes']),
|
||||||
entryName: {
|
entryName: {
|
||||||
get() {
|
get() {
|
||||||
if (this.entryModal.type === modalTypes.rename) {
|
if (this.entryModal.type === modalTypes.rename) {
|
||||||
|
@ -31,7 +33,9 @@ export default {
|
||||||
if (this.entryModal.type === modalTypes.tree) {
|
if (this.entryModal.type === modalTypes.tree) {
|
||||||
return __('Create new directory');
|
return __('Create new directory');
|
||||||
} else if (this.entryModal.type === modalTypes.rename) {
|
} else if (this.entryModal.type === modalTypes.rename) {
|
||||||
return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
|
return this.entryModal.entry.type === modalTypes.tree
|
||||||
|
? __('Rename folder')
|
||||||
|
: __('Rename file');
|
||||||
}
|
}
|
||||||
|
|
||||||
return __('Create new file');
|
return __('Create new file');
|
||||||
|
@ -40,11 +44,16 @@ export default {
|
||||||
if (this.entryModal.type === modalTypes.tree) {
|
if (this.entryModal.type === modalTypes.tree) {
|
||||||
return __('Create directory');
|
return __('Create directory');
|
||||||
} else if (this.entryModal.type === modalTypes.rename) {
|
} else if (this.entryModal.type === modalTypes.rename) {
|
||||||
return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
|
return this.entryModal.entry.type === modalTypes.tree
|
||||||
|
? __('Rename folder')
|
||||||
|
: __('Rename file');
|
||||||
}
|
}
|
||||||
|
|
||||||
return __('Create file');
|
return __('Create file');
|
||||||
},
|
},
|
||||||
|
isCreatingNew() {
|
||||||
|
return this.entryModal.type !== modalTypes.rename;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['createTempEntry', 'renameEntry']),
|
...mapActions(['createTempEntry', 'renameEntry']),
|
||||||
|
@ -61,6 +70,14 @@ export default {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
createFromTemplate(template) {
|
||||||
|
this.createTempEntry({
|
||||||
|
name: template.name,
|
||||||
|
type: this.entryModal.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#ide-new-entry').modal('toggle');
|
||||||
|
},
|
||||||
focusInput() {
|
focusInput() {
|
||||||
this.$refs.fieldName.focus();
|
this.$refs.fieldName.focus();
|
||||||
},
|
},
|
||||||
|
@ -77,6 +94,7 @@ export default {
|
||||||
:header-title-text="modalTitle"
|
:header-title-text="modalTitle"
|
||||||
:footer-primary-button-text="buttonLabel"
|
:footer-primary-button-text="buttonLabel"
|
||||||
footer-primary-button-variant="success"
|
footer-primary-button-variant="success"
|
||||||
|
modal-size="lg"
|
||||||
@submit="submitForm"
|
@submit="submitForm"
|
||||||
@open="focusInput"
|
@open="focusInput"
|
||||||
@closed="closedModal"
|
@closed="closedModal"
|
||||||
|
@ -84,16 +102,35 @@ export default {
|
||||||
<div
|
<div
|
||||||
class="form-group row"
|
class="form-group row"
|
||||||
>
|
>
|
||||||
<label class="label-bold col-form-label col-sm-3">
|
<label class="label-bold col-form-label col-sm-2">
|
||||||
{{ __('Name') }}
|
{{ __('Name') }}
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-10">
|
||||||
<input
|
<input
|
||||||
ref="fieldName"
|
ref="fieldName"
|
||||||
v-model="entryName"
|
v-model="entryName"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
|
placeholder="/dir/file_name"
|
||||||
/>
|
/>
|
||||||
|
<ul
|
||||||
|
v-if="isCreatingNew"
|
||||||
|
class="prepend-top-default list-inline"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="(template, index) in templateTypes"
|
||||||
|
:key="index"
|
||||||
|
class="list-inline-item"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-missing p-1 pr-2 pl-2"
|
||||||
|
@click="createFromTemplate(template)"
|
||||||
|
>
|
||||||
|
{{ template.name }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</gl-modal>
|
</gl-modal>
|
||||||
|
|
|
@ -95,8 +95,9 @@ export default {
|
||||||
:file-list="changedFiles"
|
:file-list="changedFiles"
|
||||||
:action-btn-text="__('Stage all changes')"
|
:action-btn-text="__('Stage all changes')"
|
||||||
:active-file-key="activeFileKey"
|
:active-file-key="activeFileKey"
|
||||||
|
:empty-state-text="__('There are no unstaged changes')"
|
||||||
action="stageAllChanges"
|
action="stageAllChanges"
|
||||||
action-btn-icon="mobile-issue-close"
|
action-btn-icon="stage-all"
|
||||||
item-action-component="stage-button"
|
item-action-component="stage-button"
|
||||||
class="is-first"
|
class="is-first"
|
||||||
icon-name="unstaged"
|
icon-name="unstaged"
|
||||||
|
@ -108,8 +109,9 @@ export default {
|
||||||
:action-btn-text="__('Unstage all changes')"
|
:action-btn-text="__('Unstage all changes')"
|
||||||
:staged-list="true"
|
:staged-list="true"
|
||||||
:active-file-key="activeFileKey"
|
:active-file-key="activeFileKey"
|
||||||
|
:empty-state-text="__('There are no staged changes')"
|
||||||
action="unstageAllChanges"
|
action="unstageAllChanges"
|
||||||
action-btn-icon="history"
|
action-btn-icon="unstage-all"
|
||||||
item-action-component="unstage-button"
|
item-action-component="unstage-button"
|
||||||
icon-name="staged"
|
icon-name="staged"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -6,12 +6,14 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
|
||||||
import { activityBarViews, viewerTypes } from '../constants';
|
import { activityBarViews, viewerTypes } from '../constants';
|
||||||
import Editor from '../lib/editor';
|
import Editor from '../lib/editor';
|
||||||
import ExternalLink from './external_link.vue';
|
import ExternalLink from './external_link.vue';
|
||||||
|
import FileTemplatesBar from './file_templates/bar.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ContentViewer,
|
ContentViewer,
|
||||||
DiffViewer,
|
DiffViewer,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
|
FileTemplatesBar,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
file: {
|
file: {
|
||||||
|
@ -34,6 +36,7 @@ export default {
|
||||||
'isCommitModeActive',
|
'isCommitModeActive',
|
||||||
'isReviewModeActive',
|
'isReviewModeActive',
|
||||||
]),
|
]),
|
||||||
|
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
|
||||||
shouldHideEditor() {
|
shouldHideEditor() {
|
||||||
return this.file && this.file.binary && !this.file.content;
|
return this.file && this.file.binary && !this.file.content;
|
||||||
},
|
},
|
||||||
|
@ -216,7 +219,7 @@ export default {
|
||||||
id="ide"
|
id="ide"
|
||||||
class="blob-viewer-container blob-editor-container"
|
class="blob-viewer-container blob-editor-container"
|
||||||
>
|
>
|
||||||
<div class="ide-mode-tabs clearfix" >
|
<div class="ide-mode-tabs clearfix">
|
||||||
<ul
|
<ul
|
||||||
v-if="!shouldHideEditor && isEditModeActive"
|
v-if="!shouldHideEditor && isEditModeActive"
|
||||||
class="nav-links float-left"
|
class="nav-links float-left"
|
||||||
|
@ -249,6 +252,9 @@ export default {
|
||||||
:file="file"
|
:file="file"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<file-templates-bar
|
||||||
|
v-if="showFileTemplatesBar(file.name)"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-show="!shouldHideEditor && file.viewMode ==='editor'"
|
v-show="!shouldHideEditor && file.viewMode ==='editor'"
|
||||||
ref="editor"
|
ref="editor"
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
|
||||||
import flash from '~/flash';
|
import flash from '~/flash';
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
import FilesDecoratorWorker from './workers/files_decorator_worker';
|
import FilesDecoratorWorker from './workers/files_decorator_worker';
|
||||||
|
import { stageKeys } from '../constants';
|
||||||
|
|
||||||
export const redirectToUrl = (_, url) => visitUrl(url);
|
export const redirectToUrl = (_, url) => visitUrl(url);
|
||||||
|
|
||||||
|
@ -122,14 +123,28 @@ export const scrollToTab = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const stageAllChanges = ({ state, commit }) => {
|
export const stageAllChanges = ({ state, commit, dispatch }) => {
|
||||||
|
const openFile = state.openFiles[0];
|
||||||
|
|
||||||
commit(types.SET_LAST_COMMIT_MSG, '');
|
commit(types.SET_LAST_COMMIT_MSG, '');
|
||||||
|
|
||||||
state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
|
state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
|
||||||
|
|
||||||
|
dispatch('openPendingTab', {
|
||||||
|
file: state.stagedFiles.find(f => f.path === openFile.path),
|
||||||
|
keyPrefix: stageKeys.staged,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const unstageAllChanges = ({ state, commit }) => {
|
export const unstageAllChanges = ({ state, commit, dispatch }) => {
|
||||||
|
const openFile = state.openFiles[0];
|
||||||
|
|
||||||
state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path));
|
state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path));
|
||||||
|
|
||||||
|
dispatch('openPendingTab', {
|
||||||
|
file: state.changedFiles.find(f => f.path === openFile.path),
|
||||||
|
keyPrefix: stageKeys.unstaged,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateViewer = ({ commit }, viewer) => {
|
export const updateViewer = ({ commit }, viewer) => {
|
||||||
|
@ -206,6 +221,7 @@ export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
|
||||||
|
|
||||||
export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => {
|
export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => {
|
||||||
const entry = state.entries[entryPath || path];
|
const entry = state.entries[entryPath || path];
|
||||||
|
|
||||||
commit(types.RENAME_ENTRY, { path, name, entryPath });
|
commit(types.RENAME_ENTRY, { path, name, entryPath });
|
||||||
|
|
||||||
if (entry.type === 'tree') {
|
if (entry.type === 'tree') {
|
||||||
|
@ -214,7 +230,7 @@ export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!entryPath) {
|
if (!entryPath && !entry.tempFile) {
|
||||||
dispatch('deleteEntry', path);
|
dispatch('deleteEntry', path);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@ import service from '../../services';
|
||||||
import * as types from '../mutation_types';
|
import * as types from '../mutation_types';
|
||||||
import router from '../../ide_router';
|
import router from '../../ide_router';
|
||||||
import { setPageTitle } from '../utils';
|
import { setPageTitle } from '../utils';
|
||||||
import { viewerTypes } from '../../constants';
|
import { viewerTypes, stageKeys } from '../../constants';
|
||||||
|
|
||||||
export const closeFile = ({ commit, state, dispatch }, file) => {
|
export const closeFile = ({ commit, state, dispatch }, file) => {
|
||||||
const { path } = file;
|
const { path } = file;
|
||||||
|
@ -208,8 +208,9 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) =
|
||||||
eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content);
|
eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const stageChange = ({ commit, state }, path) => {
|
export const stageChange = ({ commit, state, dispatch }, path) => {
|
||||||
const stagedFile = state.stagedFiles.find(f => f.path === path);
|
const stagedFile = state.stagedFiles.find(f => f.path === path);
|
||||||
|
const openFile = state.openFiles.find(f => f.path === path);
|
||||||
|
|
||||||
commit(types.STAGE_CHANGE, path);
|
commit(types.STAGE_CHANGE, path);
|
||||||
commit(types.SET_LAST_COMMIT_MSG, '');
|
commit(types.SET_LAST_COMMIT_MSG, '');
|
||||||
|
@ -217,21 +218,39 @@ export const stageChange = ({ commit, state }, path) => {
|
||||||
if (stagedFile) {
|
if (stagedFile) {
|
||||||
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
|
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (openFile && openFile.active) {
|
||||||
|
const file = state.stagedFiles.find(f => f.path === path);
|
||||||
|
|
||||||
|
dispatch('openPendingTab', {
|
||||||
|
file,
|
||||||
|
keyPrefix: stageKeys.staged,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const unstageChange = ({ commit }, path) => {
|
export const unstageChange = ({ commit, dispatch, state }, path) => {
|
||||||
|
const openFile = state.openFiles.find(f => f.path === path);
|
||||||
|
|
||||||
commit(types.UNSTAGE_CHANGE, path);
|
commit(types.UNSTAGE_CHANGE, path);
|
||||||
|
|
||||||
|
if (openFile && openFile.active) {
|
||||||
|
const file = state.changedFiles.find(f => f.path === path);
|
||||||
|
|
||||||
|
dispatch('openPendingTab', {
|
||||||
|
file,
|
||||||
|
keyPrefix: stageKeys.unstaged,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
|
export const openPendingTab = ({ commit, getters, state }, { file, keyPrefix }) => {
|
||||||
if (getters.activeFile && getters.activeFile.key === `${keyPrefix}-${file.key}`) return false;
|
if (getters.activeFile && getters.activeFile.key === `${keyPrefix}-${file.key}`) return false;
|
||||||
|
|
||||||
state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`));
|
state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`));
|
||||||
|
|
||||||
commit(types.ADD_PENDING_TAB, { file, keyPrefix });
|
commit(types.ADD_PENDING_TAB, { file, keyPrefix });
|
||||||
|
|
||||||
dispatch('scrollToTab');
|
|
||||||
|
|
||||||
router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`);
|
router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import commitModule from './modules/commit';
|
||||||
import pipelines from './modules/pipelines';
|
import pipelines from './modules/pipelines';
|
||||||
import mergeRequests from './modules/merge_requests';
|
import mergeRequests from './modules/merge_requests';
|
||||||
import branches from './modules/branches';
|
import branches from './modules/branches';
|
||||||
|
import fileTemplates from './modules/file_templates';
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
|
@ -22,6 +23,7 @@ export const createStore = () =>
|
||||||
pipelines,
|
pipelines,
|
||||||
mergeRequests,
|
mergeRequests,
|
||||||
branches,
|
branches,
|
||||||
|
fileTemplates: fileTemplates(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable no-param-reassign */
|
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Api from '~/api';
|
import Api from '~/api';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
|
import eventHub from '../../../eventhub';
|
||||||
|
|
||||||
export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES);
|
export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES);
|
||||||
export const receiveTemplateTypesError = ({ commit, dispatch }) => {
|
export const receiveTemplateTypesError = ({ commit, dispatch }) => {
|
||||||
|
@ -31,9 +32,23 @@ export const fetchTemplateTypes = ({ dispatch, state }) => {
|
||||||
.catch(() => dispatch('receiveTemplateTypesError'));
|
.catch(() => dispatch('receiveTemplateTypesError'));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setSelectedTemplateType = ({ commit }, type) =>
|
export const setSelectedTemplateType = ({ commit, dispatch, rootGetters }, type) => {
|
||||||
commit(types.SET_SELECTED_TEMPLATE_TYPE, type);
|
commit(types.SET_SELECTED_TEMPLATE_TYPE, type);
|
||||||
|
|
||||||
|
if (rootGetters.activeFile.prevPath === type.name) {
|
||||||
|
dispatch('discardFileChanges', rootGetters.activeFile.path, { root: true });
|
||||||
|
} else if (rootGetters.activeFile.name !== type.name) {
|
||||||
|
dispatch(
|
||||||
|
'renameEntry',
|
||||||
|
{
|
||||||
|
path: rootGetters.activeFile.path,
|
||||||
|
name: type.name,
|
||||||
|
},
|
||||||
|
{ root: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const receiveTemplateError = ({ dispatch }, template) => {
|
export const receiveTemplateError = ({ dispatch }, template) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
'setErrorMessage',
|
'setErrorMessage',
|
||||||
|
@ -69,6 +84,7 @@ export const setFileTemplate = ({ dispatch, commit, rootGetters }, template) =>
|
||||||
{ root: true },
|
{ root: true },
|
||||||
);
|
);
|
||||||
commit(types.SET_UPDATE_SUCCESS, true);
|
commit(types.SET_UPDATE_SUCCESS, true);
|
||||||
|
eventHub.$emit(`editor.update.model.new.content.${rootGetters.activeFile.key}`, template.content);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => {
|
export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => {
|
||||||
|
@ -76,6 +92,12 @@ export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => {
|
||||||
|
|
||||||
dispatch('changeFileContent', { path: file.path, content: file.raw }, { root: true });
|
dispatch('changeFileContent', { path: file.path, content: file.raw }, { root: true });
|
||||||
commit(types.SET_UPDATE_SUCCESS, false);
|
commit(types.SET_UPDATE_SUCCESS, false);
|
||||||
|
|
||||||
|
eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.raw);
|
||||||
|
|
||||||
|
if (file.prevPath) {
|
||||||
|
dispatch('discardFileChanges', file.path, { root: true });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { activityBarViews } from '../../../constants';
|
||||||
|
|
||||||
export const templateTypes = () => [
|
export const templateTypes = () => [
|
||||||
{
|
{
|
||||||
name: '.gitlab-ci.yml',
|
name: '.gitlab-ci.yml',
|
||||||
|
@ -17,7 +19,8 @@ export const templateTypes = () => [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const showFileTemplatesBar = (_, getters) => name =>
|
export const showFileTemplatesBar = (_, getters, rootState) => name =>
|
||||||
getters.templateTypes.find(t => t.name === name);
|
getters.templateTypes.find(t => t.name === name) &&
|
||||||
|
rootState.currentActivityView === activityBarViews.edit;
|
||||||
|
|
||||||
export default () => {};
|
export default () => {};
|
||||||
|
|
|
@ -3,10 +3,10 @@ import * as actions from './actions';
|
||||||
import * as getters from './getters';
|
import * as getters from './getters';
|
||||||
import mutations from './mutations';
|
import mutations from './mutations';
|
||||||
|
|
||||||
export default {
|
export default () => ({
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
actions,
|
actions,
|
||||||
state: createState(),
|
state: createState(),
|
||||||
getters,
|
getters,
|
||||||
mutations,
|
mutations,
|
||||||
};
|
});
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable no-param-reassign */
|
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable no-param-reassign */
|
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable no-param-reassign */
|
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
import { normalizeJob } from './utils';
|
import { normalizeJob } from './utils';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* eslint-disable no-param-reassign */
|
import Vue from 'vue';
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
import projectMutations from './mutations/project';
|
import projectMutations from './mutations/project';
|
||||||
import mergeRequestMutation from './mutations/merge_request';
|
import mergeRequestMutation from './mutations/merge_request';
|
||||||
|
@ -227,7 +227,7 @@ export default {
|
||||||
path: newPath,
|
path: newPath,
|
||||||
name: entryPath ? oldEntry.name : name,
|
name: entryPath ? oldEntry.name : name,
|
||||||
tempFile: true,
|
tempFile: true,
|
||||||
prevPath: oldEntry.path,
|
prevPath: oldEntry.tempFile ? null : oldEntry.path,
|
||||||
url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath),
|
url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath),
|
||||||
tree: [],
|
tree: [],
|
||||||
parentPath,
|
parentPath,
|
||||||
|
@ -246,6 +246,20 @@ export default {
|
||||||
if (newEntry.type === 'blob') {
|
if (newEntry.type === 'blob') {
|
||||||
state.changedFiles = state.changedFiles.concat(newEntry);
|
state.changedFiles = state.changedFiles.concat(newEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.entries[newPath].opened) {
|
||||||
|
state.openFiles.push(state.entries[newPath]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldEntry.tempFile) {
|
||||||
|
const filterMethod = f => f.path !== oldEntry.path;
|
||||||
|
|
||||||
|
state.openFiles = state.openFiles.filter(filterMethod);
|
||||||
|
state.changedFiles = state.changedFiles.filter(filterMethod);
|
||||||
|
parent.tree = parent.tree.filter(filterMethod);
|
||||||
|
|
||||||
|
Vue.delete(state.entries, oldEntry.path);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
...projectMutations,
|
...projectMutations,
|
||||||
...mergeRequestMutation,
|
...mergeRequestMutation,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable no-param-reassign */
|
|
||||||
import * as types from '../mutation_types';
|
import * as types from '../mutation_types';
|
||||||
import { sortTree } from '../utils';
|
import { sortTree } from '../utils';
|
||||||
import { diffModes } from '../../constants';
|
import { diffModes } from '../../constants';
|
||||||
|
@ -56,7 +55,7 @@ export default {
|
||||||
f => f.path === file.path && f.pending && !(f.tempFile && !f.prevPath),
|
f => f.path === file.path && f.pending && !(f.tempFile && !f.prevPath),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (file.tempFile) {
|
if (file.tempFile && file.content === '') {
|
||||||
Object.assign(state.entries[file.path], {
|
Object.assign(state.entries[file.path], {
|
||||||
content: raw,
|
content: raw,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
/* eslint-disable no-param-reassign */
|
|
||||||
|
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -131,16 +131,43 @@ export const parseUrlPathname = url => {
|
||||||
return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`;
|
return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// We can trust that each param has one & since values containing & will be encoded
|
const splitPath = (path = '') => path
|
||||||
// Remove the first character of search as it is always ?
|
.replace(/^\?/, '')
|
||||||
export const getUrlParamsArray = () =>
|
.split('&');
|
||||||
window.location.search
|
|
||||||
.slice(1)
|
export const urlParamsToArray = (path = '') => splitPath(path)
|
||||||
.split('&')
|
.filter(param => param.length > 0)
|
||||||
.map(param => {
|
.map(param => {
|
||||||
const split = param.split('=');
|
const split = param.split('=');
|
||||||
return [decodeURI(split[0]), split[1]].join('=');
|
return [decodeURI(split[0]), split[1]].join('=');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getUrlParamsArray = () => urlParamsToArray(window.location.search);
|
||||||
|
|
||||||
|
export const urlParamsToObject = (path = '') => splitPath(path)
|
||||||
|
.reduce((dataParam, filterParam) => {
|
||||||
|
if (filterParam === '') {
|
||||||
|
return dataParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = dataParam;
|
||||||
|
let [key, value] = filterParam.split('=');
|
||||||
|
const isArray = key.includes('[]');
|
||||||
|
key = key.replace('[]', '');
|
||||||
|
value = decodeURIComponent(value.replace(/\+/g, ' '));
|
||||||
|
|
||||||
|
if (isArray) {
|
||||||
|
if (!data[key]) {
|
||||||
|
data[key] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
data[key].push(value);
|
||||||
|
} else {
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}, {});
|
||||||
|
|
||||||
export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
|
export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,16 @@ export const dasherize = str => str.replace(/[_\s]+/g, '-');
|
||||||
*/
|
*/
|
||||||
export const slugify = str => str.trim().toLowerCase();
|
export const slugify = str => str.trim().toLowerCase();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces whitespaces with hyphens and converts to lower case
|
||||||
|
* @param {String} str
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
export const slugifyWithHyphens = str => {
|
||||||
|
const regex = new RegExp(/\s+/, 'g');
|
||||||
|
return str.toLowerCase().replace(regex, '-');
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Truncates given text
|
* Truncates given text
|
||||||
*
|
*
|
||||||
|
|
|
@ -47,9 +47,9 @@ export function removeParamQueryString(url, param) {
|
||||||
return urlVariables.filter(variable => variable.indexOf(param) === -1).join('&');
|
return urlVariables.filter(variable => variable.indexOf(param) === -1).join('&');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeParams(params) {
|
export function removeParams(params, source = window.location.href) {
|
||||||
const url = document.createElement('a');
|
const url = document.createElement('a');
|
||||||
url.href = window.location.href;
|
url.href = source;
|
||||||
|
|
||||||
params.forEach(param => {
|
params.forEach(param => {
|
||||||
url.search = removeParamQueryString(url.search, param);
|
url.search = removeParamQueryString(url.search, param);
|
||||||
|
|
|
@ -29,6 +29,7 @@ import './milestone_select';
|
||||||
import './frequent_items';
|
import './frequent_items';
|
||||||
import initBreadcrumbs from './breadcrumb';
|
import initBreadcrumbs from './breadcrumb';
|
||||||
import initDispatcher from './dispatcher';
|
import initDispatcher from './dispatcher';
|
||||||
|
import initUsagePingConsent from './usage_ping_consent';
|
||||||
|
|
||||||
// expose jQuery as global (TODO: remove these)
|
// expose jQuery as global (TODO: remove these)
|
||||||
window.jQuery = jQuery;
|
window.jQuery = jQuery;
|
||||||
|
@ -78,6 +79,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
initImporterStatus();
|
initImporterStatus();
|
||||||
initTodoToggle();
|
initTodoToggle();
|
||||||
initLogoAnimation();
|
initLogoAnimation();
|
||||||
|
initUsagePingConsent();
|
||||||
|
|
||||||
// Set the default path for all cookies to GitLab's root directory
|
// Set the default path for all cookies to GitLab's root directory
|
||||||
Cookies.defaults.path = gon.relative_url_root || '/';
|
Cookies.defaults.path = gon.relative_url_root || '/';
|
||||||
|
|
|
@ -82,11 +82,12 @@ export default {
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
currentXCoordinate: 0,
|
currentXCoordinate: 0,
|
||||||
currentCoordinates: [],
|
currentCoordinates: {},
|
||||||
showFlag: false,
|
showFlag: false,
|
||||||
showFlagContent: false,
|
showFlagContent: false,
|
||||||
timeSeries: [],
|
timeSeries: [],
|
||||||
realPixelRatio: 1,
|
realPixelRatio: 1,
|
||||||
|
seriesUnderMouse: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -126,6 +127,9 @@ export default {
|
||||||
this.draw();
|
this.draw();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
showDot(path) {
|
||||||
|
return this.showFlagContent && this.seriesUnderMouse.includes(path);
|
||||||
|
},
|
||||||
draw() {
|
draw() {
|
||||||
const breakpointSize = bp.getBreakpointSize();
|
const breakpointSize = bp.getBreakpointSize();
|
||||||
const query = this.graphData.queries[0];
|
const query = this.graphData.queries[0];
|
||||||
|
@ -155,7 +159,24 @@ export default {
|
||||||
point.y = e.clientY;
|
point.y = e.clientY;
|
||||||
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
|
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
|
||||||
point.x += 7;
|
point.x += 7;
|
||||||
const firstTimeSeries = this.timeSeries[0];
|
|
||||||
|
this.seriesUnderMouse = this.timeSeries.filter((series) => {
|
||||||
|
const mouseX = series.timeSeriesScaleX.invert(point.x);
|
||||||
|
let minDistance = Infinity;
|
||||||
|
|
||||||
|
const closestTickMark = Object.keys(this.allXAxisValues).reduce((closest, x) => {
|
||||||
|
const distance = Math.abs(Number(new Date(x)) - Number(mouseX));
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
return closest;
|
||||||
|
});
|
||||||
|
|
||||||
|
return series.values.find(v => v.time.toString() === closestTickMark);
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstTimeSeries = this.seriesUnderMouse[0];
|
||||||
const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
|
const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
|
||||||
const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
|
const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
|
||||||
const d0 = firstTimeSeries.values[overlayIndex - 1];
|
const d0 = firstTimeSeries.values[overlayIndex - 1];
|
||||||
|
@ -190,6 +211,17 @@ export default {
|
||||||
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))]);
|
||||||
|
|
||||||
|
this.allXAxisValues = this.timeSeries.reduce((obj, series) => {
|
||||||
|
const seriesKeys = {};
|
||||||
|
series.values.forEach(v => {
|
||||||
|
seriesKeys[v.time] = true;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...obj,
|
||||||
|
...seriesKeys,
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
|
||||||
const xAxis = d3
|
const xAxis = d3
|
||||||
.axisBottom()
|
.axisBottom()
|
||||||
.scale(axisXScale)
|
.scale(axisXScale)
|
||||||
|
@ -277,9 +309,8 @@ export default {
|
||||||
:line-style="path.lineStyle"
|
:line-style="path.lineStyle"
|
||||||
:line-color="path.lineColor"
|
:line-color="path.lineColor"
|
||||||
:area-color="path.areaColor"
|
:area-color="path.areaColor"
|
||||||
:current-coordinates="currentCoordinates[index]"
|
:current-coordinates="currentCoordinates[path.metricTag]"
|
||||||
:current-time-series-index="index"
|
:show-dot="showDot(path)"
|
||||||
:show-dot="showFlagContent"
|
|
||||||
/>
|
/>
|
||||||
<graph-deployment
|
<graph-deployment
|
||||||
:deployment-data="reducedDeploymentData"
|
:deployment-data="reducedDeploymentData"
|
||||||
|
@ -303,7 +334,7 @@ export default {
|
||||||
:graph-height="graphHeight"
|
:graph-height="graphHeight"
|
||||||
:graph-height-offset="graphHeightOffset"
|
:graph-height-offset="graphHeightOffset"
|
||||||
:show-flag-content="showFlagContent"
|
:show-flag-content="showFlagContent"
|
||||||
:time-series="timeSeries"
|
:time-series="seriesUnderMouse"
|
||||||
:unit-of-display="unitOfDisplay"
|
:unit-of-display="unitOfDisplay"
|
||||||
:legend-title="legendTitle"
|
:legend-title="legendTitle"
|
||||||
:deployment-flag-data="deploymentFlagData"
|
:deployment-flag-data="deploymentFlagData"
|
||||||
|
|
|
@ -52,7 +52,7 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
currentCoordinates: {
|
currentCoordinates: {
|
||||||
type: Array,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -91,8 +91,8 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
seriesMetricValue(seriesIndex, series) {
|
seriesMetricValue(seriesIndex, series) {
|
||||||
const indexFromCoordinates = this.currentCoordinates[seriesIndex]
|
const indexFromCoordinates = this.currentCoordinates[series.metricTag]
|
||||||
? this.currentCoordinates[seriesIndex].currentDataIndex : 0;
|
? this.currentCoordinates[series.metricTag].currentDataIndex : 0;
|
||||||
const index = this.deploymentFlagData
|
const index = this.deploymentFlagData
|
||||||
? this.deploymentFlagData.seriesIndex
|
? this.deploymentFlagData.seriesIndex
|
||||||
: indexFromCoordinates;
|
: indexFromCoordinates;
|
||||||
|
|
|
@ -50,19 +50,24 @@ const mixins = {
|
||||||
},
|
},
|
||||||
|
|
||||||
positionFlag() {
|
positionFlag() {
|
||||||
const timeSeries = this.timeSeries[0];
|
const timeSeries = this.seriesUnderMouse[0];
|
||||||
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
|
if (!timeSeries) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate);
|
||||||
|
|
||||||
this.currentData = timeSeries.values[hoveredDataIndex];
|
this.currentData = timeSeries.values[hoveredDataIndex];
|
||||||
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
|
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
|
||||||
|
|
||||||
this.currentCoordinates = this.timeSeries.map((series) => {
|
this.currentCoordinates = {};
|
||||||
const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate, 1);
|
|
||||||
|
this.seriesUnderMouse.forEach((series) => {
|
||||||
|
const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate);
|
||||||
const currentData = series.values[currentDataIndex];
|
const currentData = series.values[currentDataIndex];
|
||||||
const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
|
const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
|
||||||
const currentY = Math.floor(series.timeSeriesScaleY(currentData.value));
|
const currentY = Math.floor(series.timeSeriesScaleY(currentData.value));
|
||||||
|
|
||||||
return {
|
this.currentCoordinates[series.metricTag] = {
|
||||||
currentX,
|
currentX,
|
||||||
currentY,
|
currentY,
|
||||||
currentDataIndex,
|
currentDataIndex,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import _ from 'underscore';
|
||||||
import { scaleLinear, scaleTime } from 'd3-scale';
|
import { scaleLinear, scaleTime } from 'd3-scale';
|
||||||
import { line, area, curveLinear } from 'd3-shape';
|
import { line, area, curveLinear } from 'd3-shape';
|
||||||
import { extent, max, sum } from 'd3-array';
|
import { extent, max, sum } from 'd3-array';
|
||||||
import { timeMinute } from 'd3-time';
|
import { timeMinute, timeSecond } from 'd3-time';
|
||||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||||
|
|
||||||
const d3 = {
|
const d3 = {
|
||||||
|
@ -14,6 +14,7 @@ const d3 = {
|
||||||
extent,
|
extent,
|
||||||
max,
|
max,
|
||||||
timeMinute,
|
timeMinute,
|
||||||
|
timeSecond,
|
||||||
sum,
|
sum,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -51,6 +52,24 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
|
||||||
return defaultColorPalette[pick];
|
return defaultColorPalette[pick];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findByDate(series, time) {
|
||||||
|
const val = series.find(v => Math.abs(d3.timeSecond.count(time, v.time)) < 60);
|
||||||
|
if (val) {
|
||||||
|
return val.value;
|
||||||
|
}
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The timeseries data may have gaps in it
|
||||||
|
// but we need a regularly-spaced set of time/value pairs
|
||||||
|
// this gives us a complete range of one minute intervals
|
||||||
|
// offset the same amount as the original data
|
||||||
|
const [minX, maxX] = xDom;
|
||||||
|
const offset = d3.timeMinute(minX) - Number(minX);
|
||||||
|
const datesWithoutGaps = d3.timeSecond.every(60)
|
||||||
|
.range(d3.timeMinute.offset(minX, -1), maxX)
|
||||||
|
.map(d => d - offset);
|
||||||
|
|
||||||
query.result.forEach((timeSeries, timeSeriesNumber) => {
|
query.result.forEach((timeSeries, timeSeriesNumber) => {
|
||||||
let metricTag = '';
|
let metricTag = '';
|
||||||
let lineColor = '';
|
let lineColor = '';
|
||||||
|
@ -119,9 +138,14 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const values = datesWithoutGaps.map(time => ({
|
||||||
|
time,
|
||||||
|
value: findByDate(timeSeries.values, time),
|
||||||
|
}));
|
||||||
|
|
||||||
timeSeriesParsed.push({
|
timeSeriesParsed.push({
|
||||||
linePath: lineFunction(timeSeries.values),
|
linePath: lineFunction(values),
|
||||||
areaPath: areaFunction(timeSeries.values),
|
areaPath: areaFunction(values),
|
||||||
timeSeriesScaleX,
|
timeSeriesScaleX,
|
||||||
timeSeriesScaleY,
|
timeSeriesScaleY,
|
||||||
values: timeSeries.values,
|
values: timeSeries.values,
|
||||||
|
|
|
@ -154,7 +154,11 @@ export default class Notes {
|
||||||
this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
|
this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
|
||||||
|
|
||||||
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
|
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
|
||||||
this.$wrapperEl.on('click', '.js-toggle-lazy-diff-retry-button', this.onClickRetryLazyLoad.bind(this));
|
this.$wrapperEl.on(
|
||||||
|
'click',
|
||||||
|
'.js-toggle-lazy-diff-retry-button',
|
||||||
|
this.onClickRetryLazyLoad.bind(this),
|
||||||
|
);
|
||||||
|
|
||||||
// fetch notes when tab becomes visible
|
// fetch notes when tab becomes visible
|
||||||
this.$wrapperEl.on('visibilitychange', this.visibilityChange);
|
this.$wrapperEl.on('visibilitychange', this.visibilityChange);
|
||||||
|
@ -252,9 +256,7 @@ export default class Notes {
|
||||||
discussionNoteForm = $textarea.closest('.js-discussion-note-form');
|
discussionNoteForm = $textarea.closest('.js-discussion-note-form');
|
||||||
if (discussionNoteForm.length) {
|
if (discussionNoteForm.length) {
|
||||||
if ($textarea.val() !== '') {
|
if ($textarea.val() !== '') {
|
||||||
if (
|
if (!window.confirm('Are you sure you want to cancel creating this comment?')) {
|
||||||
!window.confirm('Are you sure you want to cancel creating this comment?')
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -266,9 +268,7 @@ export default class Notes {
|
||||||
originalText = $textarea.closest('form').data('originalNote');
|
originalText = $textarea.closest('form').data('originalNote');
|
||||||
newText = $textarea.val();
|
newText = $textarea.val();
|
||||||
if (originalText !== newText) {
|
if (originalText !== newText) {
|
||||||
if (
|
if (!window.confirm('Are you sure you want to cancel editing this comment?')) {
|
||||||
!window.confirm('Are you sure you want to cancel editing this comment?')
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1074,7 +1074,7 @@ export default class Notes {
|
||||||
addForm = false;
|
addForm = false;
|
||||||
let lineTypeSelector = '';
|
let lineTypeSelector = '';
|
||||||
rowCssToAdd =
|
rowCssToAdd =
|
||||||
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>';
|
'<tr class="notes_holder js-temp-notes-holder"><td class="notes_content" colspan="3"><div class="content"></div></td></tr>';
|
||||||
// In parallel view, look inside the correct left/right pane
|
// In parallel view, look inside the correct left/right pane
|
||||||
if (this.isParallelView()) {
|
if (this.isParallelView()) {
|
||||||
lineTypeSelector = `.${lineType}`;
|
lineTypeSelector = `.${lineType}`;
|
||||||
|
@ -1316,8 +1316,7 @@ export default class Notes {
|
||||||
|
|
||||||
$retryButton.prop('disabled', true);
|
$retryButton.prop('disabled', true);
|
||||||
|
|
||||||
return this.loadLazyDiff(e)
|
return this.loadLazyDiff(e).then(() => {
|
||||||
.then(() => {
|
|
||||||
$retryButton.prop('disabled', false);
|
$retryButton.prop('disabled', false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1343,18 +1342,18 @@ export default class Notes {
|
||||||
*/
|
*/
|
||||||
if (url) {
|
if (url) {
|
||||||
return axios
|
return axios
|
||||||
.get(url)
|
.get(url)
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
// Reset state in case last request returned error
|
// Reset state in case last request returned error
|
||||||
$successContainer.removeClass('hidden');
|
$successContainer.removeClass('hidden');
|
||||||
$errorContainer.addClass('hidden');
|
$errorContainer.addClass('hidden');
|
||||||
|
|
||||||
Notes.renderDiffContent($container, data);
|
Notes.renderDiffContent($container, data);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
$successContainer.addClass('hidden');
|
$successContainer.addClass('hidden');
|
||||||
$errorContainer.removeClass('hidden');
|
$errorContainer.removeClass('hidden');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@ -1545,12 +1544,8 @@ export default class Notes {
|
||||||
<div class="note-header">
|
<div class="note-header">
|
||||||
<div class="note-header-info">
|
<div class="note-header-info">
|
||||||
<a href="/${_.escape(currentUsername)}">
|
<a href="/${_.escape(currentUsername)}">
|
||||||
<span class="d-none d-sm-inline-block">${_.escape(
|
<span class="d-none d-sm-inline-block">${_.escape(currentUsername)}</span>
|
||||||
currentUsername,
|
<span class="note-headline-light">${_.escape(currentUsername)}</span>
|
||||||
)}</span>
|
|
||||||
<span class="note-headline-light">${_.escape(
|
|
||||||
currentUsername,
|
|
||||||
)}</span>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1565,9 +1560,7 @@ export default class Notes {
|
||||||
);
|
);
|
||||||
|
|
||||||
$tempNote.find('.d-none.d-sm-inline-block').text(_.escape(currentUserFullname));
|
$tempNote.find('.d-none.d-sm-inline-block').text(_.escape(currentUserFullname));
|
||||||
$tempNote
|
$tempNote.find('.note-headline-light').text(`@${_.escape(currentUsername)}`);
|
||||||
.find('.note-headline-light')
|
|
||||||
.text(`@${_.escape(currentUsername)}`);
|
|
||||||
|
|
||||||
return $tempNote;
|
return $tempNote;
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,10 +148,9 @@ export default {
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="notes_holder">
|
<tr class="notes_holder">
|
||||||
<td
|
<td
|
||||||
class="notes_line"
|
class="notes_content"
|
||||||
colspan="2"
|
colspan="3"
|
||||||
></td>
|
>
|
||||||
<td class="notes_content">
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -24,12 +24,13 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
noteId: {
|
noteId: {
|
||||||
type: Number,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
noteUrl: {
|
noteUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: false,
|
||||||
|
default: '',
|
||||||
},
|
},
|
||||||
accessLevel: {
|
accessLevel: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -225,11 +226,11 @@ export default {
|
||||||
Report as abuse
|
Report as abuse
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li v-if="noteUrl">
|
||||||
<button
|
<button
|
||||||
:data-clipboard-text="noteUrl"
|
:data-clipboard-text="noteUrl"
|
||||||
type="button"
|
type="button"
|
||||||
css-class="btn-default btn-transparent"
|
class="btn-default btn-transparent js-btn-copy-note-link"
|
||||||
>
|
>
|
||||||
Copy link
|
Copy link
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -25,7 +25,7 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
noteId: {
|
noteId: {
|
||||||
type: Number,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
canAwardEmoji: {
|
canAwardEmoji: {
|
||||||
|
|
|
@ -20,9 +20,9 @@ export default {
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
noteId: {
|
noteId: {
|
||||||
type: Number,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 0,
|
default: '',
|
||||||
},
|
},
|
||||||
markdownVersion: {
|
markdownVersion: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
@ -67,7 +67,10 @@ export default {
|
||||||
'getUserDataByProp',
|
'getUserDataByProp',
|
||||||
]),
|
]),
|
||||||
noteHash() {
|
noteHash() {
|
||||||
return `#note_${this.noteId}`;
|
if (this.noteId) {
|
||||||
|
return `#note_${this.noteId}`;
|
||||||
|
}
|
||||||
|
return '#';
|
||||||
},
|
},
|
||||||
markdownPreviewPath() {
|
markdownPreviewPath() {
|
||||||
return this.getNoteableDataByProp('preview_note_path');
|
return this.getNoteableDataByProp('preview_note_path');
|
||||||
|
|
|
@ -9,7 +9,8 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
author: {
|
author: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: false,
|
||||||
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -21,7 +22,7 @@ export default {
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
noteId: {
|
noteId: {
|
||||||
type: Number,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
includeToggle: {
|
includeToggle: {
|
||||||
|
@ -72,7 +73,10 @@ export default {
|
||||||
{{ __('Toggle discussion') }}
|
{{ __('Toggle discussion') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<a :href="author.path">
|
<a
|
||||||
|
v-if="Object.keys(author).length"
|
||||||
|
:href="author.path"
|
||||||
|
>
|
||||||
<span class="note-header-author-name">{{ author.name }}</span>
|
<span class="note-header-author-name">{{ author.name }}</span>
|
||||||
<span
|
<span
|
||||||
v-if="author.status_tooltip_html"
|
v-if="author.status_tooltip_html"
|
||||||
|
@ -81,6 +85,9 @@ export default {
|
||||||
@{{ author.username }}
|
@{{ author.username }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
<span v-else>
|
||||||
|
{{ __('A deleted user') }}
|
||||||
|
</span>
|
||||||
<span class="note-headline-light">
|
<span class="note-headline-light">
|
||||||
<span class="note-headline-meta">
|
<span class="note-headline-meta">
|
||||||
<template v-if="actionText">
|
<template v-if="actionText">
|
||||||
|
|
|
@ -137,8 +137,10 @@ export default {
|
||||||
return this.unresolvedDiscussions.length > 1;
|
return this.unresolvedDiscussions.length > 1;
|
||||||
},
|
},
|
||||||
showJumpToNextDiscussion() {
|
showJumpToNextDiscussion() {
|
||||||
return this.hasMultipleUnresolvedDiscussions &&
|
return (
|
||||||
!this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder);
|
this.hasMultipleUnresolvedDiscussions &&
|
||||||
|
!this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
shouldRenderDiffs() {
|
shouldRenderDiffs() {
|
||||||
const { diffDiscussion, diffFile } = this.transformedDiscussion;
|
const { diffDiscussion, diffFile } = this.transformedDiscussion;
|
||||||
|
@ -256,11 +258,16 @@ Please check your network connection and try again.`;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
jumpToNextDiscussion() {
|
jumpToNextDiscussion() {
|
||||||
const nextId =
|
const nextId = this.nextUnresolvedDiscussionId(
|
||||||
this.nextUnresolvedDiscussionId(this.discussion.id, this.discussionsByDiffOrder);
|
this.discussion.id,
|
||||||
|
this.discussionsByDiffOrder,
|
||||||
|
);
|
||||||
|
|
||||||
this.jumpToDiscussion(nextId);
|
this.jumpToDiscussion(nextId);
|
||||||
},
|
},
|
||||||
|
deleteNoteHandler(note) {
|
||||||
|
this.$emit('noteDeleted', this.discussion, note);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -270,6 +277,7 @@ Please check your network connection and try again.`;
|
||||||
<div class="timeline-entry-inner">
|
<div class="timeline-entry-inner">
|
||||||
<div class="timeline-icon">
|
<div class="timeline-icon">
|
||||||
<user-avatar-link
|
<user-avatar-link
|
||||||
|
v-if="author"
|
||||||
:link-href="author.path"
|
:link-href="author.path"
|
||||||
:img-src="author.avatar_url"
|
:img-src="author.avatar_url"
|
||||||
:img-alt="author.name"
|
:img-alt="author.name"
|
||||||
|
@ -344,6 +352,7 @@ Please check your network connection and try again.`;
|
||||||
:is="componentName(note)"
|
:is="componentName(note)"
|
||||||
:note="componentData(note)"
|
:note="componentData(note)"
|
||||||
:key="note.id"
|
:key="note.id"
|
||||||
|
@handleDeleteNote="deleteNoteHandler"
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -86,6 +86,7 @@ export default {
|
||||||
// eslint-disable-next-line no-alert
|
// eslint-disable-next-line no-alert
|
||||||
if (window.confirm('Are you sure you want to delete this comment?')) {
|
if (window.confirm('Are you sure you want to delete this comment?')) {
|
||||||
this.isDeleting = true;
|
this.isDeleting = true;
|
||||||
|
this.$emit('handleDeleteNote', this.note);
|
||||||
|
|
||||||
this.deleteNote(this.note)
|
this.deleteNote(this.note)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
|
@ -138,6 +138,7 @@ export default {
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.setNotesFetchedState(true);
|
this.setNotesFetchedState(true);
|
||||||
|
eventHub.$emit('fetchedNotesData');
|
||||||
})
|
})
|
||||||
.then(() => this.$nextTick())
|
.then(() => this.$nextTick())
|
||||||
.then(() => this.checkLocationHash())
|
.then(() => this.checkLocationHash())
|
||||||
|
|
|
@ -43,14 +43,23 @@ export const fetchDiscussions = ({ commit }, path) =>
|
||||||
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
|
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const refetchDiscussionById = ({ commit }, { path, discussionId }) =>
|
export const refetchDiscussionById = ({ commit, state }, { path, discussionId }) =>
|
||||||
service
|
new Promise(resolve => {
|
||||||
.fetchDiscussions(path)
|
service
|
||||||
.then(res => res.json())
|
.fetchDiscussions(path)
|
||||||
.then(discussions => {
|
.then(res => res.json())
|
||||||
const selectedDiscussion = discussions.find(discussion => discussion.id === discussionId);
|
.then(discussions => {
|
||||||
if (selectedDiscussion) commit(types.UPDATE_DISCUSSION, selectedDiscussion);
|
const selectedDiscussion = discussions.find(discussion => discussion.id === discussionId);
|
||||||
});
|
if (selectedDiscussion) {
|
||||||
|
commit(types.UPDATE_DISCUSSION, selectedDiscussion);
|
||||||
|
// We need to refetch as it is now the transformed one in state
|
||||||
|
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
|
||||||
|
|
||||||
|
resolve(discussion);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
export const deleteNote = ({ commit }, note) =>
|
export const deleteNote = ({ commit }, note) =>
|
||||||
service.deleteNote(note.path).then(() => {
|
service.deleteNote(note.path).then(() => {
|
||||||
|
@ -152,26 +161,28 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
|
||||||
const replyId = noteData.data.in_reply_to_discussion_id;
|
const replyId = noteData.data.in_reply_to_discussion_id;
|
||||||
const methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote';
|
const methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote';
|
||||||
|
|
||||||
commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders
|
|
||||||
$('.notes-form .flash-container').hide(); // hide previous flash notification
|
$('.notes-form .flash-container').hide(); // hide previous flash notification
|
||||||
|
commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders
|
||||||
|
|
||||||
if (hasQuickActions) {
|
if (replyId) {
|
||||||
placeholderText = utils.stripQuickActions(placeholderText);
|
if (hasQuickActions) {
|
||||||
}
|
placeholderText = utils.stripQuickActions(placeholderText);
|
||||||
|
}
|
||||||
|
|
||||||
if (placeholderText.length) {
|
if (placeholderText.length) {
|
||||||
commit(types.SHOW_PLACEHOLDER_NOTE, {
|
commit(types.SHOW_PLACEHOLDER_NOTE, {
|
||||||
noteBody: placeholderText,
|
noteBody: placeholderText,
|
||||||
replyId,
|
replyId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasQuickActions) {
|
if (hasQuickActions) {
|
||||||
commit(types.SHOW_PLACEHOLDER_NOTE, {
|
commit(types.SHOW_PLACEHOLDER_NOTE, {
|
||||||
isSystemNote: true,
|
isSystemNote: true,
|
||||||
noteBody: utils.getQuickActionText(note),
|
noteBody: utils.getQuickActionText(note),
|
||||||
replyId,
|
replyId,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dispatch(methodToDispatch, noteData).then(res => {
|
return dispatch(methodToDispatch, noteData).then(res => {
|
||||||
|
@ -211,7 +222,9 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
|
||||||
if (errors && errors.commands_only) {
|
if (errors && errors.commands_only) {
|
||||||
Flash(errors.commands_only, 'notice', noteData.flashContainer);
|
Flash(errors.commands_only, 'notice', noteData.flashContainer);
|
||||||
}
|
}
|
||||||
commit(types.REMOVE_PLACEHOLDER_NOTES);
|
if (replyId) {
|
||||||
|
commit(types.REMOVE_PLACEHOLDER_NOTES);
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import _ from 'underscore';
|
import _ from 'underscore';
|
||||||
import * as constants from '../constants';
|
import * as constants from '../constants';
|
||||||
|
import { reduceDiscussionsToLineCodes } from './utils';
|
||||||
import { collapseSystemNotes } from './collapse_utils';
|
import { collapseSystemNotes } from './collapse_utils';
|
||||||
|
|
||||||
export const discussions = state => collapseSystemNotes(state.discussions);
|
export const discussions = state => collapseSystemNotes(state.discussions);
|
||||||
|
@ -28,17 +29,8 @@ export const notesById = state =>
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
export const discussionsByLineCode = state =>
|
export const discussionsStructuredByLineCode = state =>
|
||||||
state.discussions.reduce((acc, note) => {
|
reduceDiscussionsToLineCodes(state.discussions);
|
||||||
if (note.diff_discussion && note.line_code && note.resolvable) {
|
|
||||||
// For context about line notes: there might be multiple notes with the same line code
|
|
||||||
const items = acc[note.line_code] || [];
|
|
||||||
items.push(note);
|
|
||||||
|
|
||||||
Object.assign(acc, { [note.line_code]: items });
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
export const noteableType = state => {
|
export const noteableType = state => {
|
||||||
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
|
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
|
||||||
|
|
|
@ -54,13 +54,12 @@ export default {
|
||||||
|
|
||||||
[types.EXPAND_DISCUSSION](state, { discussionId }) {
|
[types.EXPAND_DISCUSSION](state, { discussionId }) {
|
||||||
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
|
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
|
||||||
|
Object.assign(discussion, { expanded: true });
|
||||||
discussion.expanded = true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.COLLAPSE_DISCUSSION](state, { discussionId }) {
|
[types.COLLAPSE_DISCUSSION](state, { discussionId }) {
|
||||||
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
|
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
|
||||||
discussion.expanded = false;
|
Object.assign(discussion, { expanded: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.REMOVE_PLACEHOLDER_NOTES](state) {
|
[types.REMOVE_PLACEHOLDER_NOTES](state) {
|
||||||
|
@ -95,10 +94,15 @@ export default {
|
||||||
[types.SET_USER_DATA](state, data) {
|
[types.SET_USER_DATA](state, data) {
|
||||||
Object.assign(state, { userData: data });
|
Object.assign(state, { userData: data });
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.SET_INITIAL_DISCUSSIONS](state, discussionsData) {
|
[types.SET_INITIAL_DISCUSSIONS](state, discussionsData) {
|
||||||
const discussions = [];
|
const discussions = [];
|
||||||
|
|
||||||
discussionsData.forEach(discussion => {
|
discussionsData.forEach(discussion => {
|
||||||
|
if (discussion.diff_file) {
|
||||||
|
Object.assign(discussion, { fileHash: discussion.diff_file.file_hash });
|
||||||
|
}
|
||||||
|
|
||||||
// To support legacy notes, should be very rare case.
|
// To support legacy notes, should be very rare case.
|
||||||
if (discussion.individual_note && discussion.notes.length > 1) {
|
if (discussion.individual_note && discussion.notes.length > 1) {
|
||||||
discussion.notes.forEach(n => {
|
discussion.notes.forEach(n => {
|
||||||
|
@ -168,8 +172,7 @@ export default {
|
||||||
|
|
||||||
[types.TOGGLE_DISCUSSION](state, { discussionId }) {
|
[types.TOGGLE_DISCUSSION](state, { discussionId }) {
|
||||||
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
|
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
|
||||||
|
Object.assign(discussion, { expanded: !discussion.expanded });
|
||||||
discussion.expanded = !discussion.expanded;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.UPDATE_NOTE](state, note) {
|
[types.UPDATE_NOTE](state, note) {
|
||||||
|
@ -185,16 +188,12 @@ export default {
|
||||||
|
|
||||||
[types.UPDATE_DISCUSSION](state, noteData) {
|
[types.UPDATE_DISCUSSION](state, noteData) {
|
||||||
const note = noteData;
|
const note = noteData;
|
||||||
let index = 0;
|
const selectedDiscussion = state.discussions.find(disc => disc.id === note.id);
|
||||||
|
|
||||||
state.discussions.forEach((n, i) => {
|
|
||||||
if (n.id === note.id) {
|
|
||||||
index = i;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
note.expanded = true; // override expand flag to prevent collapse
|
note.expanded = true; // override expand flag to prevent collapse
|
||||||
state.discussions.splice(index, 1, note);
|
if (note.diff_file) {
|
||||||
|
Object.assign(note, { fileHash: note.diff_file.file_hash });
|
||||||
|
}
|
||||||
|
Object.assign(selectedDiscussion, { ...note });
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.CLOSE_ISSUE](state) {
|
[types.CLOSE_ISSUE](state) {
|
||||||
|
|
|
@ -2,13 +2,11 @@ import AjaxCache from '~/lib/utils/ajax_cache';
|
||||||
|
|
||||||
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
|
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
|
||||||
|
|
||||||
export const findNoteObjectById = (notes, id) =>
|
export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0];
|
||||||
notes.filter(n => n.id === id)[0];
|
|
||||||
|
|
||||||
export const getQuickActionText = note => {
|
export const getQuickActionText = note => {
|
||||||
let text = 'Applying command';
|
let text = 'Applying command';
|
||||||
const quickActions =
|
const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
|
||||||
AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
|
|
||||||
|
|
||||||
const executedCommands = quickActions.filter(command => {
|
const executedCommands = quickActions.filter(command => {
|
||||||
const commandRegex = new RegExp(`/${command.name}`);
|
const commandRegex = new RegExp(`/${command.name}`);
|
||||||
|
@ -27,7 +25,18 @@ export const getQuickActionText = note => {
|
||||||
return text;
|
return text;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const reduceDiscussionsToLineCodes = selectedDiscussions =>
|
||||||
|
selectedDiscussions.reduce((acc, note) => {
|
||||||
|
if (note.diff_discussion && note.line_code && note.resolvable) {
|
||||||
|
// For context about line notes: there might be multiple notes with the same line code
|
||||||
|
const items = acc[note.line_code] || [];
|
||||||
|
items.push(note);
|
||||||
|
|
||||||
|
Object.assign(acc, { [note.line_code]: items });
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
|
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
|
||||||
|
|
||||||
export const stripQuickActions = note =>
|
export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
|
||||||
note.replace(REGEX_QUICK_ACTIONS, '').trim();
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
import initGroupsList from '~/groups';
|
import initGroupsList from '~/groups';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initGroupsList);
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initGroupsList();
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
import $ from 'jquery';
|
||||||
|
import { removeParams } from '~/lib/utils/url_utility';
|
||||||
|
import createGroupTree from '~/groups';
|
||||||
|
import {
|
||||||
|
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
|
||||||
|
ACTIVE_TAB_SHARED,
|
||||||
|
ACTIVE_TAB_ARCHIVED,
|
||||||
|
CONTENT_LIST_CLASS,
|
||||||
|
GROUPS_LIST_HOLDER_CLASS,
|
||||||
|
GROUPS_FILTER_FORM_CLASS,
|
||||||
|
} from '~/groups/constants';
|
||||||
|
import UserTabs from '~/pages/users/user_tabs';
|
||||||
|
import GroupFilterableList from '~/groups/groups_filterable_list';
|
||||||
|
|
||||||
|
export default class GroupTabs extends UserTabs {
|
||||||
|
constructor({ defaultAction = 'subgroups_and_projects', action, parentEl }) {
|
||||||
|
super({ defaultAction, action, parentEl });
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
this.$parentEl
|
||||||
|
.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
|
||||||
|
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
tabShown(event) {
|
||||||
|
const $target = $(event.target);
|
||||||
|
const action = $target.data('action') || $target.data('targetSection');
|
||||||
|
const source = $target.attr('href') || $target.data('targetPath');
|
||||||
|
|
||||||
|
document.querySelector(GROUPS_FILTER_FORM_CLASS).action = source;
|
||||||
|
|
||||||
|
this.setTab(action);
|
||||||
|
return this.setCurrentAction(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTab(action) {
|
||||||
|
const loadableActions = [
|
||||||
|
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
|
||||||
|
ACTIVE_TAB_SHARED,
|
||||||
|
ACTIVE_TAB_ARCHIVED,
|
||||||
|
];
|
||||||
|
this.enableSearchBar(action);
|
||||||
|
this.action = action;
|
||||||
|
|
||||||
|
if (this.loaded[action]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadableActions.includes(action)) {
|
||||||
|
this.cleanFilterState();
|
||||||
|
this.loadTab(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTab(action) {
|
||||||
|
const elId = `js-groups-${action}-tree`;
|
||||||
|
const endpoint = this.getEndpoint(action);
|
||||||
|
|
||||||
|
this.toggleLoading(true);
|
||||||
|
|
||||||
|
createGroupTree(elId, endpoint, action);
|
||||||
|
this.loaded[action] = true;
|
||||||
|
|
||||||
|
this.toggleLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
getEndpoint(action) {
|
||||||
|
const { endpointsDefault, endpointsShared } = this.$parentEl.data();
|
||||||
|
let endpoint;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case ACTIVE_TAB_ARCHIVED:
|
||||||
|
endpoint = `${endpointsDefault}?archived=only`;
|
||||||
|
break;
|
||||||
|
case ACTIVE_TAB_SHARED:
|
||||||
|
endpoint = endpointsShared;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// ACTIVE_TAB_SUBGROUPS_AND_PROJECTS
|
||||||
|
endpoint = endpointsDefault;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
enableSearchBar(action) {
|
||||||
|
const containerEl = document.getElementById(action);
|
||||||
|
const form = document.querySelector(GROUPS_FILTER_FORM_CLASS);
|
||||||
|
const filter = form.querySelector('.js-groups-list-filter');
|
||||||
|
const holder = containerEl.querySelector(GROUPS_LIST_HOLDER_CLASS);
|
||||||
|
const dataEl = containerEl.querySelector(CONTENT_LIST_CLASS);
|
||||||
|
const endpoint = this.getEndpoint(action);
|
||||||
|
|
||||||
|
if (!dataEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { dataset } = dataEl;
|
||||||
|
const opts = {
|
||||||
|
form,
|
||||||
|
filter,
|
||||||
|
holder,
|
||||||
|
filterEndpoint: endpoint || dataset.endpoint,
|
||||||
|
pagePath: null,
|
||||||
|
dropdownSel: '.js-group-filter-dropdown-wrap',
|
||||||
|
filterInputField: 'filter',
|
||||||
|
action,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.loaded[action]) {
|
||||||
|
const filterableList = new GroupFilterableList(opts);
|
||||||
|
filterableList.initSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanFilterState() {
|
||||||
|
const values = Object.values(this.loaded);
|
||||||
|
const loadedTabs = values.filter(e => e === true);
|
||||||
|
|
||||||
|
if (!loadedTabs.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newState = removeParams(['page'], window.location.search);
|
||||||
|
|
||||||
|
window.history.replaceState(
|
||||||
|
{
|
||||||
|
url: newState,
|
||||||
|
},
|
||||||
|
document.title,
|
||||||
|
newState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,22 @@
|
||||||
/* eslint-disable no-new */
|
/* eslint-disable no-new */
|
||||||
|
|
||||||
|
import { getPagePath } from '~/lib/utils/common_utils';
|
||||||
|
import { ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED } from '~/groups/constants';
|
||||||
import NewGroupChild from '~/groups/new_group_child';
|
import NewGroupChild from '~/groups/new_group_child';
|
||||||
import notificationsDropdown from '~/notifications_dropdown';
|
import notificationsDropdown from '~/notifications_dropdown';
|
||||||
import NotificationsForm from '~/notifications_form';
|
import NotificationsForm from '~/notifications_form';
|
||||||
import ProjectsList from '~/projects_list';
|
import ProjectsList from '~/projects_list';
|
||||||
import ShortcutsNavigation from '~/shortcuts_navigation';
|
import ShortcutsNavigation from '~/shortcuts_navigation';
|
||||||
import initGroupsList from '~/groups';
|
import GroupTabs from './group_tabs';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
|
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
|
||||||
|
const loadableActions = [ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED];
|
||||||
|
const paths = window.location.pathname.split('/');
|
||||||
|
const subpath = paths[paths.length - 1];
|
||||||
|
const action = loadableActions.includes(subpath) ? subpath : getPagePath(1);
|
||||||
|
|
||||||
|
new GroupTabs({ parentEl: '.groups-listing', action });
|
||||||
new ShortcutsNavigation();
|
new ShortcutsNavigation();
|
||||||
new NotificationsForm();
|
new NotificationsForm();
|
||||||
notificationsDropdown();
|
notificationsDropdown();
|
||||||
|
@ -17,6 +25,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
if (newGroupChildWrapper) {
|
if (newGroupChildWrapper) {
|
||||||
new NewGroupChild(newGroupChildWrapper);
|
new NewGroupChild(newGroupChildWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
initGroupsList();
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
|
import initDismissableCallout from '~/dismissable_callout';
|
||||||
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
|
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
|
||||||
import Project from './project';
|
import Project from './project';
|
||||||
import ShortcutsNavigation from '../../shortcuts_navigation';
|
import ShortcutsNavigation from '../../shortcuts_navigation';
|
||||||
|
@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
if (newClusterViews.indexOf(page) > -1) {
|
if (newClusterViews.indexOf(page) > -1) {
|
||||||
gcpSignupOffer();
|
initDismissableCallout('.gcp-signup-offer');
|
||||||
initGkeDropdowns();
|
initGkeDropdowns();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,13 @@ export default class Project {
|
||||||
.remove();
|
.remove();
|
||||||
return e.preventDefault();
|
return e.preventDefault();
|
||||||
});
|
});
|
||||||
|
$('.hide-auto-devops-implicitly-enabled-banner').on('click', function(e) {
|
||||||
|
const projectId = $(this).data('project-id');
|
||||||
|
const cookieKey = `hide_auto_devops_implicitly_enabled_banner_${projectId}`;
|
||||||
|
Cookies.set(cookieKey, 'false');
|
||||||
|
$(this).parents('.auto-devops-implicitly-enabled-banner').remove();
|
||||||
|
return e.preventDefault();
|
||||||
|
});
|
||||||
Project.projectSelectDropdown();
|
Project.projectSelectDropdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
modalId() {
|
||||||
|
return 'delete-wiki-modal';
|
||||||
|
},
|
||||||
message() {
|
message() {
|
||||||
return s__('WikiPageConfirmDelete|Are you sure you want to delete this page?');
|
return s__('WikiPageConfirmDelete|Are you sure you want to delete this page?');
|
||||||
},
|
},
|
||||||
|
@ -47,31 +50,41 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<gl-modal
|
<div class="d-inline-block">
|
||||||
id="delete-wiki-modal"
|
<button
|
||||||
:header-title-text="title"
|
v-gl-modal="modalId"
|
||||||
:footer-primary-button-text="s__('WikiPageConfirmDelete|Delete page')"
|
type="button"
|
||||||
footer-primary-button-variant="danger"
|
class="btn btn-danger"
|
||||||
@submit="onSubmit"
|
|
||||||
>
|
|
||||||
{{ message }}
|
|
||||||
<form
|
|
||||||
ref="form"
|
|
||||||
:action="deleteWikiUrl"
|
|
||||||
method="post"
|
|
||||||
class="js-requires-input"
|
|
||||||
>
|
>
|
||||||
<input
|
{{ __('Delete') }}
|
||||||
ref="method"
|
</button>
|
||||||
type="hidden"
|
<gl-ui-modal
|
||||||
name="_method"
|
:title="title"
|
||||||
value="delete"
|
:ok-title="s__('WikiPageConfirmDelete|Delete page')"
|
||||||
/>
|
:modal-id="modalId"
|
||||||
<input
|
title-tag="h4"
|
||||||
:value="csrfToken"
|
ok-variant="danger"
|
||||||
type="hidden"
|
@ok="onSubmit"
|
||||||
name="authenticity_token"
|
>
|
||||||
/>
|
{{ message }}
|
||||||
</form>
|
<form
|
||||||
</gl-modal>
|
ref="form"
|
||||||
|
:action="deleteWikiUrl"
|
||||||
|
method="post"
|
||||||
|
class="js-requires-input"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="method"
|
||||||
|
type="hidden"
|
||||||
|
name="_method"
|
||||||
|
value="delete"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
:value="csrfToken"
|
||||||
|
type="hidden"
|
||||||
|
name="authenticity_token"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</gl-ui-modal>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -14,15 +14,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
new ZenMode(); // eslint-disable-line no-new
|
new ZenMode(); // eslint-disable-line no-new
|
||||||
new GLForm($('.wiki-form')); // eslint-disable-line no-new
|
new GLForm($('.wiki-form')); // eslint-disable-line no-new
|
||||||
|
|
||||||
const deleteWikiButton = document.getElementById('delete-wiki-button');
|
const deleteWikiModalWrapperEl = document.getElementById('delete-wiki-modal-wrapper');
|
||||||
|
|
||||||
if (deleteWikiButton) {
|
if (deleteWikiModalWrapperEl) {
|
||||||
Vue.use(Translate);
|
Vue.use(Translate);
|
||||||
|
|
||||||
const { deleteWikiUrl, pageTitle } = deleteWikiButton.dataset;
|
const { deleteWikiUrl, pageTitle } = deleteWikiModalWrapperEl.dataset;
|
||||||
const deleteWikiModalEl = document.getElementById('delete-wiki-modal');
|
|
||||||
const deleteModal = new Vue({ // eslint-disable-line
|
new Vue({ // eslint-disable-line no-new
|
||||||
el: deleteWikiModalEl,
|
el: deleteWikiModalWrapperEl,
|
||||||
data: {
|
data: {
|
||||||
deleteWikiUrl: '',
|
deleteWikiUrl: '',
|
||||||
},
|
},
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue