merge master and resolve conflicts
This commit is contained in:
commit
a2f7936c74
|
@ -33,6 +33,15 @@ rules:
|
|||
- error
|
||||
- max: 1
|
||||
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:
|
||||
- error
|
||||
- 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
|
||||
retry: 1
|
||||
|
@ -708,7 +708,6 @@ gitlab:assets:compile:
|
|||
SETUP_DB: "false"
|
||||
SKIP_STORAGE_VALIDATION: "true"
|
||||
WEBPACK_REPORT: "true"
|
||||
NO_COMPRESSION: "true"
|
||||
# we override the max_old_space_size to prevent OOM errors
|
||||
NODE_OPTIONS: --max_old_space_size=3584
|
||||
script:
|
||||
|
@ -722,6 +721,7 @@ gitlab:assets:compile:
|
|||
expire_in: 31d
|
||||
paths:
|
||||
- webpack-report/
|
||||
- public/assets/
|
||||
|
||||
karma:
|
||||
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
|
||||
|
|
|
@ -13,3 +13,5 @@ db/ @abrandl @NikolayS
|
|||
|
||||
# Feature specific owners
|
||||
/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)
|
||||
- [Priority labels](#priority-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)
|
||||
- [Implement design & UI elements](#implement-design--ui-elements)
|
||||
- [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/gemfile')
|
||||
danger.import_dangerfile(path: 'danger/database')
|
||||
danger.import_dangerfile(path: 'danger/documentation')
|
||||
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'
|
||||
|
||||
# Background jobs
|
||||
gem 'sidekiq', '~> 5.1'
|
||||
gem 'sidekiq', '~> 5.2.1'
|
||||
gem 'sidekiq-cron', '~> 0.6.0'
|
||||
gem 'redis-namespace', '~> 1.6.0'
|
||||
gem 'sidekiq-limit_fetch', '~> 3.4', require: false
|
||||
|
@ -425,7 +425,7 @@ group :ed25519 do
|
|||
end
|
||||
|
||||
# Gitaly GRPC client
|
||||
gem 'gitaly-proto', '~> 0.113.0', require: 'gitaly'
|
||||
gem 'gitaly-proto', '~> 0.117.0', require: 'gitaly'
|
||||
gem 'grpc', '~> 1.11.0'
|
||||
|
||||
# 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-ext (1.0.5)
|
||||
concurrent-ruby (= 1.0.5)
|
||||
connection_pool (2.2.1)
|
||||
connection_pool (2.2.2)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.4)
|
||||
|
@ -208,7 +208,7 @@ GEM
|
|||
fast_blank (1.0.0)
|
||||
fast_gettext (1.6.0)
|
||||
ffaker (2.4.0)
|
||||
ffi (1.9.18)
|
||||
ffi (1.9.25)
|
||||
flipper (0.13.0)
|
||||
flipper-active_record (0.13.0)
|
||||
activerecord (>= 3.2, < 6)
|
||||
|
@ -276,7 +276,7 @@ GEM
|
|||
gettext_i18n_rails (>= 0.7.1)
|
||||
po_to_json (>= 1.0.0)
|
||||
rails (>= 3.2.0)
|
||||
gitaly-proto (0.113.0)
|
||||
gitaly-proto (0.117.0)
|
||||
google-protobuf (~> 3.1)
|
||||
grpc (~> 1.10)
|
||||
github-linguist (5.3.3)
|
||||
|
@ -649,7 +649,7 @@ GEM
|
|||
httpclient (>= 2.4)
|
||||
multi_json (>= 1.3.6)
|
||||
rack (>= 1.1)
|
||||
rack-protection (2.0.1)
|
||||
rack-protection (2.0.3)
|
||||
rack
|
||||
rack-proxy (0.6.0)
|
||||
rack
|
||||
|
@ -843,9 +843,8 @@ GEM
|
|||
rack
|
||||
shoulda-matchers (3.1.2)
|
||||
activesupport (>= 4.0.0)
|
||||
sidekiq (5.1.3)
|
||||
concurrent-ruby (~> 1.0)
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
sidekiq (5.2.1)
|
||||
connection_pool (~> 2.2, >= 2.2.2)
|
||||
rack-protection (>= 1.5.0)
|
||||
redis (>= 3.3.5, < 5)
|
||||
sidekiq-cron (0.6.0)
|
||||
|
@ -1038,7 +1037,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly-proto (~> 0.113.0)
|
||||
gitaly-proto (~> 0.117.0)
|
||||
github-linguist (~> 5.3.3)
|
||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||
gitlab-gollum-lib (~> 4.2)
|
||||
|
@ -1166,7 +1165,7 @@ DEPENDENCIES
|
|||
settingslogic (~> 2.0.9)
|
||||
sham_rack (~> 1.3.6)
|
||||
shoulda-matchers (~> 3.1.2)
|
||||
sidekiq (~> 5.1)
|
||||
sidekiq (~> 5.2.1)
|
||||
sidekiq-cron (~> 0.6.0)
|
||||
sidekiq-limit_fetch (~> 3.4)
|
||||
simple_po_parser (~> 1.1.2)
|
||||
|
|
|
@ -136,7 +136,7 @@ GEM
|
|||
concurrent-ruby (1.0.5)
|
||||
concurrent-ruby-ext (1.0.5)
|
||||
concurrent-ruby (= 1.0.5)
|
||||
connection_pool (2.2.1)
|
||||
connection_pool (2.2.2)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.4)
|
||||
|
@ -211,7 +211,7 @@ GEM
|
|||
fast_blank (1.0.0)
|
||||
fast_gettext (1.6.0)
|
||||
ffaker (2.4.0)
|
||||
ffi (1.9.18)
|
||||
ffi (1.9.25)
|
||||
flipper (0.13.0)
|
||||
flipper-active_record (0.13.0)
|
||||
activerecord (>= 3.2, < 6)
|
||||
|
@ -279,7 +279,7 @@ GEM
|
|||
gettext_i18n_rails (>= 0.7.1)
|
||||
po_to_json (>= 1.0.0)
|
||||
rails (>= 3.2.0)
|
||||
gitaly-proto (0.113.0)
|
||||
gitaly-proto (0.117.0)
|
||||
google-protobuf (~> 3.1)
|
||||
grpc (~> 1.10)
|
||||
github-linguist (5.3.3)
|
||||
|
@ -653,7 +653,7 @@ GEM
|
|||
httpclient (>= 2.4)
|
||||
multi_json (>= 1.3.6)
|
||||
rack (>= 1.1)
|
||||
rack-protection (2.0.1)
|
||||
rack-protection (2.0.3)
|
||||
rack
|
||||
rack-proxy (0.6.0)
|
||||
rack
|
||||
|
@ -851,9 +851,8 @@ GEM
|
|||
rack
|
||||
shoulda-matchers (3.1.2)
|
||||
activesupport (>= 4.0.0)
|
||||
sidekiq (5.1.3)
|
||||
concurrent-ruby (~> 1.0)
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
sidekiq (5.2.1)
|
||||
connection_pool (~> 2.2, >= 2.2.2)
|
||||
rack-protection (>= 1.5.0)
|
||||
redis (>= 3.3.5, < 5)
|
||||
sidekiq-cron (0.6.0)
|
||||
|
@ -1047,7 +1046,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly-proto (~> 0.113.0)
|
||||
gitaly-proto (~> 0.117.0)
|
||||
github-linguist (~> 5.3.3)
|
||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||
gitlab-gollum-lib (~> 4.2)
|
||||
|
@ -1176,7 +1175,7 @@ DEPENDENCIES
|
|||
settingslogic (~> 2.0.9)
|
||||
sham_rack (~> 1.3.6)
|
||||
shoulda-matchers (~> 3.1.2)
|
||||
sidekiq (~> 5.1)
|
||||
sidekiq (~> 5.2.1)
|
||||
sidekiq-cron (~> 0.6.0)
|
||||
sidekiq-limit_fetch (~> 3.4)
|
||||
simple_po_parser (~> 1.1.2)
|
||||
|
|
18
LICENSE
18
LICENSE
|
@ -1,7 +1,19 @@
|
|||
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>
|
||||
/* 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 ModalHeader from './header.vue';
|
||||
import ModalList from './list.vue';
|
||||
|
@ -109,13 +109,11 @@
|
|||
loadIssues(clearIssues = false) {
|
||||
if (!this.showAddIssuesModal) return false;
|
||||
|
||||
return gl.boardService
|
||||
.getBacklog(
|
||||
queryData(this.filter.path, {
|
||||
page: this.page,
|
||||
per: this.perPage,
|
||||
}),
|
||||
)
|
||||
return gl.boardService.getBacklog({
|
||||
...urlParamsToObject(this.filter.path),
|
||||
page: this.page,
|
||||
per: this.perPage,
|
||||
})
|
||||
.then(res => res.data)
|
||||
.then(data => {
|
||||
if (clearIssues) {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import { __ } from '~/locale';
|
||||
import ListLabel from '~/vue_shared/models/label';
|
||||
import ListAssignee from '~/vue_shared/models/assignee';
|
||||
import queryData from '../utils/query_data';
|
||||
import { urlParamsToObject } from '~/lib/utils/common_utils';
|
||||
|
||||
const PER_PAGE = 20;
|
||||
|
||||
|
@ -115,7 +115,10 @@ class List {
|
|||
}
|
||||
|
||||
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) {
|
||||
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 Vue from 'vue';
|
||||
import initDismissableCallout from '~/dismissable_callout';
|
||||
import { s__, sprintf } from '../locale';
|
||||
import Flash from '../flash';
|
||||
import Poll from '../lib/utils/poll';
|
||||
import initSettingsPanels from '../settings_panels';
|
||||
import eventHub from './event_hub';
|
||||
import {
|
||||
APPLICATION_STATUS,
|
||||
REQUEST_LOADING,
|
||||
REQUEST_SUCCESS,
|
||||
REQUEST_FAILURE,
|
||||
} from './constants';
|
||||
import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants';
|
||||
import ClustersService from './services/clusters_service';
|
||||
import ClustersStore from './stores/clusters_store';
|
||||
import applications from './components/applications.vue';
|
||||
|
@ -66,6 +62,7 @@ export default class Clusters {
|
|||
this.showTokenButton = document.querySelector('.js-show-cluster-token');
|
||||
this.tokenField = document.querySelector('.js-cluster-token');
|
||||
|
||||
initDismissableCallout('.js-cluster-security-warning');
|
||||
initSettingsPanels();
|
||||
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
|
||||
this.initApplications();
|
||||
|
@ -129,7 +126,8 @@ export default class Clusters {
|
|||
if (!Visibility.hidden()) {
|
||||
this.poll.makeRequest();
|
||||
} else {
|
||||
this.service.fetchData()
|
||||
this.service
|
||||
.fetchData()
|
||||
.then(data => this.handleSuccess(data))
|
||||
.catch(() => Clusters.handleError());
|
||||
}
|
||||
|
@ -177,15 +175,21 @@ export default class Clusters {
|
|||
|
||||
checkForNewInstalls(prevApplicationMap, newApplicationMap) {
|
||||
const appTitles = Object.keys(newApplicationMap)
|
||||
.filter(appId => newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
|
||||
prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
|
||||
prevApplicationMap[appId].status !== null)
|
||||
.filter(
|
||||
appId =>
|
||||
newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
|
||||
prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
|
||||
prevApplicationMap[appId].status !== null,
|
||||
)
|
||||
.map(appId => newApplicationMap[appId].title);
|
||||
|
||||
if (appTitles.length > 0) {
|
||||
const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'), {
|
||||
appList: appTitles.join(', '),
|
||||
});
|
||||
const text = sprintf(
|
||||
s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'),
|
||||
{
|
||||
appList: appTitles.join(', '),
|
||||
},
|
||||
);
|
||||
Flash(text, 'notice', this.successApplicationContainer);
|
||||
}
|
||||
}
|
||||
|
@ -218,13 +222,18 @@ export default class Clusters {
|
|||
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
|
||||
this.store.updateAppProperty(appId, 'requestReason', null);
|
||||
|
||||
this.service.installApplication(appId, data.params)
|
||||
this.service
|
||||
.installApplication(appId, data.params)
|
||||
.then(() => {
|
||||
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
|
||||
})
|
||||
.catch(() => {
|
||||
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 { __ } from '~/locale';
|
||||
import setupToggleButtons from '~/toggle_buttons';
|
||||
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
|
||||
import initDismissableCallout from '~/dismissable_callout';
|
||||
|
||||
import ClustersService from './services/clusters_service';
|
||||
|
||||
export default () => {
|
||||
const clusterList = document.querySelector('.js-clusters-list');
|
||||
|
||||
gcpSignupOffer();
|
||||
initDismissableCallout('.gcp-signup-offer');
|
||||
|
||||
// The empty state won't have a clusterList
|
||||
if (clusterList) {
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
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-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 ResolveService */
|
||||
|
||||
|
@ -25,44 +25,44 @@ const ResolveDiscussionBtn = Vue.extend({
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
showButton: function () {
|
||||
showButton: function() {
|
||||
if (this.discussion) {
|
||||
return this.discussion.isResolvable();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
isDiscussionResolved: function () {
|
||||
isDiscussionResolved: function() {
|
||||
if (this.discussion) {
|
||||
return this.discussion.isResolved();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
buttonText: function () {
|
||||
buttonText: function() {
|
||||
if (this.isDiscussionResolved) {
|
||||
return "Unresolve discussion";
|
||||
return 'Unresolve discussion';
|
||||
} else {
|
||||
return "Resolve discussion";
|
||||
return 'Resolve discussion';
|
||||
}
|
||||
},
|
||||
loading: function () {
|
||||
loading: function() {
|
||||
if (this.discussion) {
|
||||
return this.discussion.loading;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
created: function () {
|
||||
created: function() {
|
||||
CommentsStore.createDiscussion(this.discussionId, this.canResolve);
|
||||
|
||||
this.discussion = CommentsStore.state[this.discussionId];
|
||||
},
|
||||
methods: {
|
||||
resolve: function () {
|
||||
resolve: function() {
|
||||
ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -8,9 +8,7 @@ window.gl = window.gl || {};
|
|||
|
||||
class ResolveServiceClass {
|
||||
constructor(root) {
|
||||
this.noteResource = Vue.resource(
|
||||
`${root}/notes{/noteId}/resolve?html=true`,
|
||||
);
|
||||
this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`);
|
||||
this.discussionResource = Vue.resource(
|
||||
`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`,
|
||||
);
|
||||
|
@ -51,10 +49,7 @@ class ResolveServiceClass {
|
|||
discussion.updateHeadline(data);
|
||||
})
|
||||
.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,
|
||||
}),
|
||||
...mapGetters('diffs', ['isParallelView']),
|
||||
...mapGetters(['isNotesFetched']),
|
||||
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
|
||||
targetBranch() {
|
||||
return {
|
||||
branchName: this.targetBranchName,
|
||||
|
@ -112,13 +112,26 @@ export default {
|
|||
},
|
||||
created() {
|
||||
this.adjustView();
|
||||
eventHub.$once('fetchedNotesData', this.setDiscussions);
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles', 'startRenderDiffsQueue']),
|
||||
...mapActions('diffs', [
|
||||
'setBaseConfig',
|
||||
'fetchDiffFiles',
|
||||
'startRenderDiffsQueue',
|
||||
'assignDiscussionsToDiff',
|
||||
]),
|
||||
|
||||
fetchData() {
|
||||
this.fetchDiffFiles()
|
||||
.then(() => {
|
||||
requestIdleCallback(this.startRenderDiffsQueue, { timeout: 1000 });
|
||||
requestIdleCallback(
|
||||
() => {
|
||||
this.setDiscussions();
|
||||
this.startRenderDiffsQueue();
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
createFlash(__('Something went wrong on our end. Please try again!'));
|
||||
|
@ -128,6 +141,16 @@ export default {
|
|||
eventHub.$emit('fetchNotesData');
|
||||
}
|
||||
},
|
||||
setDiscussions() {
|
||||
if (this.isNotesFetched) {
|
||||
requestIdleCallback(
|
||||
() => {
|
||||
this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode);
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
}
|
||||
},
|
||||
adjustView() {
|
||||
if (this.shouldShow && this.isParallelView) {
|
||||
window.mrTabs.expandViewContainer();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
|
||||
|
||||
export default {
|
||||
|
@ -11,6 +12,14 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['removeDiscussionsFromDiff']),
|
||||
deleteNoteHandler(discussion) {
|
||||
if (discussion.notes.length <= 1) {
|
||||
this.removeDiscussionsFromDiff(discussion);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -31,6 +40,7 @@ export default {
|
|||
:render-diff-file="false"
|
||||
:always-expanded="true"
|
||||
:discussions-by-diff-order="true"
|
||||
@noteDeleted="deleteNoteHandler"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import _ from 'underscore';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import createFlash from '~/flash';
|
||||
|
@ -30,6 +30,7 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
|
||||
isCollapsed() {
|
||||
return this.file.collapsed || false;
|
||||
},
|
||||
|
@ -44,23 +45,23 @@ export default {
|
|||
);
|
||||
},
|
||||
showExpandMessage() {
|
||||
return this.isCollapsed && !this.isLoadingCollapsedDiff && !this.file.tooLarge;
|
||||
return (
|
||||
!this.isCollapsed &&
|
||||
!this.file.highlightedDiffLines &&
|
||||
!this.isLoadingCollapsedDiff &&
|
||||
!this.file.tooLarge &&
|
||||
this.file.text
|
||||
);
|
||||
},
|
||||
showLoadingIcon() {
|
||||
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['loadCollapsedDiff']),
|
||||
...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff']),
|
||||
handleToggle() {
|
||||
const { collapsed, highlightedDiffLines, parallelDiffLines } = this.file;
|
||||
|
||||
if (
|
||||
collapsed &&
|
||||
!highlightedDiffLines &&
|
||||
parallelDiffLines !== undefined &&
|
||||
!parallelDiffLines.length
|
||||
) {
|
||||
const { highlightedDiffLines, parallelDiffLines } = this.file;
|
||||
if (!highlightedDiffLines && parallelDiffLines !== undefined && !parallelDiffLines.length) {
|
||||
this.handleLoadCollapsedDiff();
|
||||
} else {
|
||||
this.file.collapsed = !this.file.collapsed;
|
||||
|
@ -76,6 +77,14 @@ export default {
|
|||
this.file.collapsed = false;
|
||||
this.file.renderIt = true;
|
||||
})
|
||||
.then(() => {
|
||||
requestIdleCallback(
|
||||
() => {
|
||||
this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode);
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
this.isLoadingCollapsedDiff = false;
|
||||
createFlash(__('Something went wrong on our end. Please try again!'));
|
||||
|
@ -136,11 +145,11 @@ export default {
|
|||
:diff-file="file"
|
||||
/>
|
||||
<loading-icon
|
||||
v-else-if="showLoadingIcon"
|
||||
v-if="showLoadingIcon"
|
||||
class="diff-content loading"
|
||||
/>
|
||||
<div
|
||||
v-if="showExpandMessage"
|
||||
v-else-if="showExpandMessage"
|
||||
class="nothing-here-block diff-collapsed"
|
||||
>
|
||||
{{ __('This diff is collapsed.') }}
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import { pluralize, truncate } from '~/lib/utils/text_utility';
|
||||
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
UserAvatarImage,
|
||||
|
@ -91,10 +87,10 @@ export default {
|
|||
@click.native="toggleDiscussions"
|
||||
/>
|
||||
<span
|
||||
v-tooltip
|
||||
v-gl-tooltip
|
||||
v-if="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-placement="top"
|
||||
role="button"
|
||||
|
|
|
@ -13,6 +13,10 @@ export default {
|
|||
Icon,
|
||||
},
|
||||
props: {
|
||||
line: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
fileHash: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -21,31 +25,16 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
lineType: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
lineNumber: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
lineCode: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
linePosition: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
metaData: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
showCommentButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -76,11 +65,6 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
discussions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
|
@ -89,7 +73,7 @@ export default {
|
|||
}),
|
||||
...mapGetters(['isLoggedIn']),
|
||||
lineHref() {
|
||||
return this.lineCode ? `#${this.lineCode}` : '#';
|
||||
return `#${this.line.lineCode || ''}`;
|
||||
},
|
||||
shouldShowCommentButton() {
|
||||
return (
|
||||
|
@ -103,20 +87,19 @@ export default {
|
|||
);
|
||||
},
|
||||
hasDiscussions() {
|
||||
return this.discussions.length > 0;
|
||||
return this.line.discussions && this.line.discussions.length > 0;
|
||||
},
|
||||
shouldShowAvatarsOnGutter() {
|
||||
if (!this.lineType && this.linePosition === LINE_POSITION_RIGHT) {
|
||||
if (!this.line.type && this.linePosition === LINE_POSITION_RIGHT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.showCommentButton && this.hasDiscussions;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['loadMoreLines', 'showCommentForm']),
|
||||
handleCommentButton() {
|
||||
this.showCommentForm({ lineCode: this.lineCode });
|
||||
this.showCommentForm({ lineCode: this.line.lineCode });
|
||||
},
|
||||
handleLoadMoreLines() {
|
||||
if (this.isRequesting) {
|
||||
|
@ -125,8 +108,8 @@ export default {
|
|||
|
||||
this.isRequesting = true;
|
||||
const endpoint = this.contextLinesPath;
|
||||
const oldLineNumber = this.metaData.oldPos || 0;
|
||||
const newLineNumber = this.metaData.newPos || 0;
|
||||
const oldLineNumber = this.line.metaData.oldPos || 0;
|
||||
const newLineNumber = this.line.metaData.newPos || 0;
|
||||
const offset = newLineNumber - oldLineNumber;
|
||||
const bottom = this.isBottom;
|
||||
const { fileHash } = this;
|
||||
|
@ -201,7 +184,7 @@ export default {
|
|||
</a>
|
||||
<diff-gutter-avatars
|
||||
v-if="shouldShowAvatarsOnGutter"
|
||||
:discussions="discussions"
|
||||
:discussions="line.discussions"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -6,6 +6,7 @@ import noteForm from '../../notes/components/note_form.vue';
|
|||
import { getNoteFormData } from '../store/utils';
|
||||
import autosave from '../../notes/mixins/autosave';
|
||||
import { DIFF_NOTE_TYPE } from '../constants';
|
||||
import { reduceDiscussionsToLineCodes } from '../../notes/stores/utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -52,7 +53,7 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['cancelCommentForm']),
|
||||
...mapActions('diffs', ['cancelCommentForm', 'assignDiscussionsToDiff']),
|
||||
...mapActions(['saveNote', 'refetchDiscussionById']),
|
||||
handleCancelCommentForm(shouldConfirm, isDirty) {
|
||||
if (shouldConfirm && isDirty) {
|
||||
|
@ -88,7 +89,10 @@ export default {
|
|||
const endpoint = this.getNotesDataByProp('discussionsPath');
|
||||
|
||||
this.refetchDiscussionById({ path: endpoint, discussionId: result.discussion_id })
|
||||
.then(() => {
|
||||
.then(selectedDiscussion => {
|
||||
const lineCodeDiscussions = reduceDiscussionsToLineCodes([selectedDiscussion]);
|
||||
this.assignDiscussionsToDiff(lineCodeDiscussions);
|
||||
|
||||
this.handleCancelCommentForm();
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
|
@ -11,8 +11,6 @@ import {
|
|||
LINE_HOVER_CLASS_NAME,
|
||||
LINE_UNFOLD_CLASS_NAME,
|
||||
INLINE_DIFF_VIEW_TYPE,
|
||||
LINE_POSITION_LEFT,
|
||||
LINE_POSITION_RIGHT,
|
||||
} from '../constants';
|
||||
|
||||
export default {
|
||||
|
@ -67,42 +65,24 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
discussions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...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() {
|
||||
return this.normalizedLine.type === MATCH_LINE_TYPE;
|
||||
return this.line.type === MATCH_LINE_TYPE;
|
||||
},
|
||||
isContextLine() {
|
||||
return this.normalizedLine.type === CONTEXT_LINE_TYPE;
|
||||
return this.line.type === CONTEXT_LINE_TYPE;
|
||||
},
|
||||
isMetaLine() {
|
||||
const { type } = this.normalizedLine;
|
||||
const { type } = this.line;
|
||||
|
||||
return (
|
||||
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
|
||||
);
|
||||
},
|
||||
classNameMap() {
|
||||
const { type } = this.normalizedLine;
|
||||
const { type } = this.line;
|
||||
|
||||
return {
|
||||
[type]: type,
|
||||
|
@ -116,9 +96,9 @@ export default {
|
|||
};
|
||||
},
|
||||
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"
|
||||
>
|
||||
<diff-line-gutter-content
|
||||
:line="line"
|
||||
:file-hash="fileHash"
|
||||
:context-lines-path="contextLinesPath"
|
||||
:line-type="normalizedLine.type"
|
||||
:line-code="normalizedLine.lineCode"
|
||||
:line-position="linePosition"
|
||||
:line-number="lineNumber"
|
||||
:meta-data="normalizedLine.metaData"
|
||||
:show-comment-button="showCommentButton"
|
||||
:is-hover="isHover"
|
||||
:is-bottom="isBottom"
|
||||
:is-match-line="isMatchLine"
|
||||
:is-context-line="isContentLine"
|
||||
:is-meta-line="isMetaLine"
|
||||
:discussions="discussions"
|
||||
/>
|
||||
</td>
|
||||
</template>
|
||||
|
|
|
@ -21,18 +21,13 @@ export default {
|
|||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
discussions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
|
||||
}),
|
||||
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"
|
||||
>
|
||||
<td
|
||||
class="notes_line"
|
||||
colspan="2"
|
||||
></td>
|
||||
<td class="notes_content">
|
||||
class="notes_content"
|
||||
colspan="3"
|
||||
>
|
||||
<div class="content">
|
||||
<diff-discussions
|
||||
v-if="discussions.length"
|
||||
:discussions="discussions"
|
||||
v-if="line.discussions.length"
|
||||
:discussions="line.discussions"
|
||||
/>
|
||||
<diff-line-note-form
|
||||
v-if="diffLineCommentForms[line.lineCode]"
|
||||
|
|
|
@ -33,11 +33,6 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
discussions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -94,7 +89,6 @@ export default {
|
|||
:is-bottom="isBottom"
|
||||
:is-hover="isHover"
|
||||
:show-comment-button="true"
|
||||
:discussions="discussions"
|
||||
class="diff-line-num old_line"
|
||||
/>
|
||||
<diff-table-cell
|
||||
|
@ -104,7 +98,6 @@ export default {
|
|||
:line-type="newLineType"
|
||||
:is-bottom="isBottom"
|
||||
:is-hover="isHover"
|
||||
:discussions="discussions"
|
||||
class="diff-line-num new_line"
|
||||
/>
|
||||
<td
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import { mapGetters, mapState } from 'vuex';
|
||||
import inlineDiffTableRow from './inline_diff_table_row.vue';
|
||||
import inlineDiffCommentRow from './inline_diff_comment_row.vue';
|
||||
import { trimFirstCharOfLineContent } from '../store/utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -20,29 +19,17 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('diffs', [
|
||||
'commitId',
|
||||
'shouldRenderInlineCommentRow',
|
||||
'singleDiscussionByLineCode',
|
||||
]),
|
||||
...mapGetters('diffs', ['commitId', 'shouldRenderInlineCommentRow']),
|
||||
...mapState({
|
||||
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
|
||||
}),
|
||||
normalizedDiffLines() {
|
||||
return this.diffLines.map(line => (line.richText ? trimFirstCharOfLineContent(line) : line));
|
||||
},
|
||||
diffLinesLength() {
|
||||
return this.normalizedDiffLines.length;
|
||||
return this.diffLines.length;
|
||||
},
|
||||
userColorScheme() {
|
||||
return window.gon.user_color_scheme;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
discussionsList(line) {
|
||||
return line.lineCode !== undefined ? this.singleDiscussionByLineCode(line.lineCode) : [];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -53,7 +40,7 @@ export default {
|
|||
class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view">
|
||||
<tbody>
|
||||
<template
|
||||
v-for="(line, index) in normalizedDiffLines"
|
||||
v-for="(line, index) in diffLines"
|
||||
>
|
||||
<inline-diff-table-row
|
||||
:file-hash="diffFile.fileHash"
|
||||
|
@ -61,7 +48,6 @@ export default {
|
|||
:line="line"
|
||||
:is-bottom="index + 1 === diffLinesLength"
|
||||
:key="line.lineCode"
|
||||
:discussions="discussionsList(line)"
|
||||
/>
|
||||
<inline-diff-comment-row
|
||||
v-if="shouldRenderInlineCommentRow(line)"
|
||||
|
@ -69,7 +55,6 @@ export default {
|
|||
:line="line"
|
||||
:line-index="index"
|
||||
:key="index"
|
||||
:discussions="discussionsList(line)"
|
||||
/>
|
||||
</template>
|
||||
</tbody>
|
||||
|
|
|
@ -21,51 +21,49 @@ export default {
|
|||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
leftDiscussions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
rightDiscussions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
|
||||
}),
|
||||
leftLineCode() {
|
||||
return this.line.left.lineCode;
|
||||
return this.line.left && this.line.left.lineCode;
|
||||
},
|
||||
rightLineCode() {
|
||||
return this.line.right.lineCode;
|
||||
return this.line.right && this.line.right.lineCode;
|
||||
},
|
||||
hasExpandedDiscussionOnLeft() {
|
||||
const discussions = this.leftDiscussions;
|
||||
|
||||
return discussions ? discussions.every(discussion => discussion.expanded) : false;
|
||||
return this.line.left && this.line.left.discussions
|
||||
? this.line.left.discussions.every(discussion => discussion.expanded)
|
||||
: false;
|
||||
},
|
||||
hasExpandedDiscussionOnRight() {
|
||||
const discussions = this.rightDiscussions;
|
||||
|
||||
return discussions ? discussions.every(discussion => discussion.expanded) : false;
|
||||
return this.line.right && this.line.right.discussions
|
||||
? this.line.right.discussions.every(discussion => discussion.expanded)
|
||||
: false;
|
||||
},
|
||||
hasAnyExpandedDiscussion() {
|
||||
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
|
||||
},
|
||||
shouldRenderDiscussionsOnLeft() {
|
||||
return this.leftDiscussions && this.hasExpandedDiscussionOnLeft;
|
||||
return this.line.left && this.line.left.discussions && this.hasExpandedDiscussionOnLeft;
|
||||
},
|
||||
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() {
|
||||
return this.line.right.type && this.diffLineCommentForms[this.rightLineCode];
|
||||
return (
|
||||
this.line.right && this.line.right.type && this.diffLineCommentForms[this.rightLineCode]
|
||||
);
|
||||
},
|
||||
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';
|
||||
},
|
||||
|
@ -85,8 +83,8 @@ export default {
|
|||
class="content"
|
||||
>
|
||||
<diff-discussions
|
||||
v-if="leftDiscussions.length"
|
||||
:discussions="leftDiscussions"
|
||||
v-if="line.left.discussions.length"
|
||||
:discussions="line.left.discussions"
|
||||
/>
|
||||
</div>
|
||||
<diff-line-note-form
|
||||
|
@ -104,8 +102,8 @@ export default {
|
|||
class="content"
|
||||
>
|
||||
<diff-discussions
|
||||
v-if="rightDiscussions.length"
|
||||
:discussions="rightDiscussions"
|
||||
v-if="line.right.discussions.length"
|
||||
:discussions="line.right.discussions"
|
||||
/>
|
||||
</div>
|
||||
<diff-line-note-form
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import { mapGetters } from 'vuex';
|
||||
import DiffTableCell from './diff_table_cell.vue';
|
||||
import {
|
||||
NEW_LINE_TYPE,
|
||||
|
@ -10,8 +9,7 @@ import {
|
|||
OLD_NO_NEW_LINE_TYPE,
|
||||
PARALLEL_DIFF_VIEW_TYPE,
|
||||
NEW_NO_NEW_LINE_TYPE,
|
||||
LINE_POSITION_LEFT,
|
||||
LINE_POSITION_RIGHT,
|
||||
EMPTY_CELL_TYPE,
|
||||
} from '../constants';
|
||||
|
||||
export default {
|
||||
|
@ -36,16 +34,6 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
leftDiscussions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
rightDiscussions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -54,29 +42,26 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('diffs', ['isParallelView']),
|
||||
isContextLine() {
|
||||
return this.line.left.type === CONTEXT_LINE_TYPE;
|
||||
return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE;
|
||||
},
|
||||
classNameMap() {
|
||||
return {
|
||||
[CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
|
||||
[PARALLEL_DIFF_VIEW_TYPE]: this.isParallelView,
|
||||
[PARALLEL_DIFF_VIEW_TYPE]: true,
|
||||
};
|
||||
},
|
||||
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 this.line.left.type;
|
||||
return this.line.left ? this.line.left.type : EMPTY_CELL_TYPE;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.newLineType = NEW_LINE_TYPE;
|
||||
this.oldLineType = OLD_LINE_TYPE;
|
||||
this.linePositionLeft = LINE_POSITION_LEFT;
|
||||
this.linePositionRight = LINE_POSITION_RIGHT;
|
||||
this.parallelDiffViewType = PARALLEL_DIFF_VIEW_TYPE;
|
||||
},
|
||||
methods: {
|
||||
|
@ -116,47 +101,57 @@ export default {
|
|||
@mouseover="handleMouseMove"
|
||||
@mouseout="handleMouseMove"
|
||||
>
|
||||
<diff-table-cell
|
||||
:file-hash="fileHash"
|
||||
:context-lines-path="contextLinesPath"
|
||||
:line="line"
|
||||
:line-type="oldLineType"
|
||||
:line-position="linePositionLeft"
|
||||
:is-bottom="isBottom"
|
||||
:is-hover="isLeftHover"
|
||||
:show-comment-button="true"
|
||||
:diff-view-type="parallelDiffViewType"
|
||||
:discussions="leftDiscussions"
|
||||
class="diff-line-num old_line"
|
||||
/>
|
||||
<td
|
||||
:id="line.left.lineCode"
|
||||
:class="parallelViewLeftLineType"
|
||||
class="line_content parallel left-side"
|
||||
@mousedown.native="handleParallelLineMouseDown"
|
||||
v-html="line.left.richText"
|
||||
>
|
||||
</td>
|
||||
<diff-table-cell
|
||||
:file-hash="fileHash"
|
||||
:context-lines-path="contextLinesPath"
|
||||
:line="line"
|
||||
:line-type="newLineType"
|
||||
:line-position="linePositionRight"
|
||||
:is-bottom="isBottom"
|
||||
:is-hover="isRightHover"
|
||||
:show-comment-button="true"
|
||||
:diff-view-type="parallelDiffViewType"
|
||||
:discussions="rightDiscussions"
|
||||
class="diff-line-num new_line"
|
||||
/>
|
||||
<td
|
||||
:id="line.right.lineCode"
|
||||
:class="line.right.type"
|
||||
class="line_content parallel right-side"
|
||||
@mousedown.native="handleParallelLineMouseDown"
|
||||
v-html="line.right.richText"
|
||||
>
|
||||
</td>
|
||||
<template v-if="line.left">
|
||||
<diff-table-cell
|
||||
:file-hash="fileHash"
|
||||
:context-lines-path="contextLinesPath"
|
||||
:line="line.left"
|
||||
:line-type="oldLineType"
|
||||
:is-bottom="isBottom"
|
||||
:is-hover="isLeftHover"
|
||||
:show-comment-button="true"
|
||||
:diff-view-type="parallelDiffViewType"
|
||||
line-position="left"
|
||||
class="diff-line-num old_line"
|
||||
/>
|
||||
<td
|
||||
:id="line.left.lineCode"
|
||||
:class="parallelViewLeftLineType"
|
||||
class="line_content parallel left-side"
|
||||
@mousedown.native="handleParallelLineMouseDown"
|
||||
v-html="line.left.richText"
|
||||
>
|
||||
</td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<td class="diff-line-num old_line empty-cell"></td>
|
||||
<td class="line_content parallel left-side empty-cell"></td>
|
||||
</template>
|
||||
<template v-if="line.right">
|
||||
<diff-table-cell
|
||||
:file-hash="fileHash"
|
||||
:context-lines-path="contextLinesPath"
|
||||
:line="line.right"
|
||||
:line-type="newLineType"
|
||||
:is-bottom="isBottom"
|
||||
:is-hover="isRightHover"
|
||||
:show-comment-button="true"
|
||||
:diff-view-type="parallelDiffViewType"
|
||||
line-position="right"
|
||||
class="diff-line-num new_line"
|
||||
/>
|
||||
<td
|
||||
:id="line.right.lineCode"
|
||||
: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>
|
||||
</template>
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
import { mapState, mapGetters } from 'vuex';
|
||||
import parallelDiffTableRow from './parallel_diff_table_row.vue';
|
||||
import parallelDiffCommentRow from './parallel_diff_comment_row.vue';
|
||||
import { EMPTY_CELL_TYPE } from '../constants';
|
||||
import { trimFirstCharOfLineContent } from '../store/utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -21,46 +19,17 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('diffs', [
|
||||
'commitId',
|
||||
'singleDiscussionByLineCode',
|
||||
'shouldRenderParallelCommentRow',
|
||||
]),
|
||||
...mapGetters('diffs', ['commitId', 'shouldRenderParallelCommentRow']),
|
||||
...mapState({
|
||||
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() {
|
||||
return this.parallelDiffLines.length;
|
||||
return this.diffLines.length;
|
||||
},
|
||||
userColorScheme() {
|
||||
return window.gon.user_color_scheme;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
discussionsByLine(line, leftOrRight) {
|
||||
return line[leftOrRight] && line[leftOrRight].lineCode !== undefined ?
|
||||
this.singleDiscussionByLineCode(line[leftOrRight].lineCode) : [];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -73,7 +42,7 @@ export default {
|
|||
<table>
|
||||
<tbody>
|
||||
<template
|
||||
v-for="(line, index) in parallelDiffLines"
|
||||
v-for="(line, index) in diffLines"
|
||||
>
|
||||
<parallel-diff-table-row
|
||||
:file-hash="diffFile.fileHash"
|
||||
|
@ -81,8 +50,6 @@ export default {
|
|||
:line="line"
|
||||
:is-bottom="index + 1 === diffLinesLength"
|
||||
:key="index"
|
||||
:left-discussions="discussionsByLine(line, 'left')"
|
||||
:right-discussions="discussionsByLine(line, 'right')"
|
||||
/>
|
||||
<parallel-diff-comment-row
|
||||
v-if="shouldRenderParallelCommentRow(line)"
|
||||
|
@ -90,8 +57,6 @@ export default {
|
|||
:line="line"
|
||||
:diff-file-hash="diffFile.fileHash"
|
||||
:line-index="index"
|
||||
:left-discussions="discussionsByLine(line, 'left')"
|
||||
:right-discussions="discussionsByLine(line, 'right')"
|
||||
/>
|
||||
</template>
|
||||
</tbody>
|
||||
|
|
|
@ -3,6 +3,7 @@ import axios from '~/lib/utils/axios_utils';
|
|||
import Cookies from 'js-cookie';
|
||||
import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
|
||||
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
import { getDiffPositionByLineCode } from './utils';
|
||||
import * as types from './mutation_types';
|
||||
import {
|
||||
PARALLEL_DIFF_VIEW_TYPE,
|
||||
|
@ -29,25 +30,53 @@ export const fetchDiffFiles = ({ state, commit }) => {
|
|||
.then(handleLocationHash);
|
||||
};
|
||||
|
||||
export const startRenderDiffsQueue = ({ state, commit }) => {
|
||||
const checkItem = () => {
|
||||
const nextFile = state.diffFiles.find(
|
||||
file => !file.renderIt && (!file.collapsed || !file.text),
|
||||
);
|
||||
if (nextFile) {
|
||||
requestAnimationFrame(() => {
|
||||
commit(types.RENDER_FILE, nextFile);
|
||||
});
|
||||
requestIdleCallback(
|
||||
() => {
|
||||
checkItem();
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
}
|
||||
};
|
||||
// This is adding line discussions to the actual lines in the diff tree
|
||||
// once for parallel and once for inline mode
|
||||
export const assignDiscussionsToDiff = ({ state, commit }, allLineDiscussions) => {
|
||||
const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
|
||||
|
||||
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 }) => {
|
||||
|
|
|
@ -17,7 +17,10 @@ export const commitId = state => (state.commit && state.commit.id ? state.commit
|
|||
export const diffHasAllExpandedDiscussions = (state, getters) => 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 => {
|
||||
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);
|
||||
|
||||
return (
|
||||
(discussions.length && discussions.find(discussion => discussion.expanded) !== undefined) ||
|
||||
(discussions &&
|
||||
discussions.length &&
|
||||
discussions.find(discussion => discussion.expanded) !== undefined) ||
|
||||
false
|
||||
);
|
||||
};
|
||||
|
@ -64,45 +72,38 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) =
|
|||
discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash),
|
||||
) || [];
|
||||
|
||||
export const singleDiscussionByLineCode = (state, getters, rootState, rootGetters) => lineCode => {
|
||||
if (!lineCode || lineCode === undefined) return [];
|
||||
const discussions = rootGetters.discussionsByLineCode;
|
||||
return discussions[lineCode] || [];
|
||||
};
|
||||
export const shouldRenderParallelCommentRow = state => line => {
|
||||
const hasDiscussion =
|
||||
(line.left && line.left.discussions && line.left.discussions.length) ||
|
||||
(line.right && line.right.discussions && line.right.discussions.length);
|
||||
|
||||
export const shouldRenderParallelCommentRow = (state, getters) => line => {
|
||||
const leftLineCode = line.left.lineCode;
|
||||
const rightLineCode = line.right.lineCode;
|
||||
const leftDiscussions = getters.singleDiscussionByLineCode(leftLineCode);
|
||||
const rightDiscussions = getters.singleDiscussionByLineCode(rightLineCode);
|
||||
const hasDiscussion = leftDiscussions.length || rightDiscussions.length;
|
||||
|
||||
const hasExpandedDiscussionOnLeft = leftDiscussions.length
|
||||
? leftDiscussions.every(discussion => discussion.expanded)
|
||||
: false;
|
||||
const hasExpandedDiscussionOnRight = rightDiscussions.length
|
||||
? rightDiscussions.every(discussion => discussion.expanded)
|
||||
: false;
|
||||
const hasExpandedDiscussionOnLeft =
|
||||
line.left && line.left.discussions && line.left.discussions.length
|
||||
? line.left.discussions.every(discussion => discussion.expanded)
|
||||
: false;
|
||||
const hasExpandedDiscussionOnRight =
|
||||
line.right && line.right.discussions && line.right.discussions.length
|
||||
? line.right.discussions.every(discussion => discussion.expanded)
|
||||
: false;
|
||||
|
||||
if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hasCommentFormOnLeft = state.diffLineCommentForms[leftLineCode];
|
||||
const hasCommentFormOnRight = state.diffLineCommentForms[rightLineCode];
|
||||
const hasCommentFormOnLeft = line.left && state.diffLineCommentForms[line.left.lineCode];
|
||||
const hasCommentFormOnRight = line.right && state.diffLineCommentForms[line.right.lineCode];
|
||||
|
||||
return hasCommentFormOnLeft || hasCommentFormOnRight;
|
||||
};
|
||||
|
||||
export const shouldRenderInlineCommentRow = (state, getters) => line => {
|
||||
export const shouldRenderInlineCommentRow = state => line => {
|
||||
if (state.diffLineCommentForms[line.lineCode]) return true;
|
||||
|
||||
const lineDiscussions = getters.singleDiscussionByLineCode(line.lineCode);
|
||||
if (lineDiscussions.length === 0) {
|
||||
if (!line.discussions || line.discussions.length === 0) {
|
||||
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
|
||||
|
|
|
@ -9,3 +9,5 @@ export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES';
|
|||
export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS';
|
||||
export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
|
||||
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 _ from 'underscore';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { findDiffFile, addLineReferences, removeMatchLine, addContextLines } from './utils';
|
||||
import { LINES_TO_BE_RENDERED_DIRECTLY, MAX_LINES_TO_BE_RENDERED } from '../constants';
|
||||
import {
|
||||
findDiffFile,
|
||||
addLineReferences,
|
||||
removeMatchLine,
|
||||
addContextLines,
|
||||
prepareDiffData,
|
||||
isDiscussionApplicableToLine,
|
||||
} from './utils';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
|
@ -17,38 +22,7 @@ export default {
|
|||
|
||||
[types.SET_DIFF_DATA](state, data) {
|
||||
const diffData = convertObjectPropsToCamelCase(data, { deep: true });
|
||||
let showingLines = 0;
|
||||
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,
|
||||
});
|
||||
}
|
||||
prepareDiffData(diffData);
|
||||
|
||||
Object.assign(state, {
|
||||
...diffData,
|
||||
|
@ -98,19 +72,93 @@ export default {
|
|||
|
||||
[types.ADD_COLLAPSED_DIFFS](state, { file, data }) {
|
||||
const normalizedData = convertObjectPropsToCamelCase(data, { deep: true });
|
||||
prepareDiffData(normalizedData);
|
||||
const [newFileData] = normalizedData.diffFiles.filter(f => f.fileHash === file.fileHash);
|
||||
|
||||
if (newFileData) {
|
||||
const index = _.findIndex(state.diffFiles, f => f.fileHash === file.fileHash);
|
||||
state.diffFiles.splice(index, 1, newFileData);
|
||||
}
|
||||
const selectedFile = state.diffFiles.find(f => f.fileHash === file.fileHash);
|
||||
Object.assign(selectedFile, { ...newFileData });
|
||||
},
|
||||
|
||||
[types.EXPAND_ALL_FILES](state) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
state.diffFiles = state.diffFiles.map(file => ({
|
||||
...file,
|
||||
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,
|
||||
OLD_LINE_TYPE,
|
||||
MATCH_LINE_TYPE,
|
||||
LINES_TO_BE_RENDERED_DIRECTLY,
|
||||
MAX_LINES_TO_BE_RENDERED,
|
||||
} from '../constants';
|
||||
|
||||
export function findDiffFile(files, hash) {
|
||||
|
@ -161,6 +163,11 @@ export function addContextLines(options) {
|
|||
* @returns {Object}
|
||||
*/
|
||||
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);
|
||||
|
||||
if (line.richText) {
|
||||
|
@ -174,7 +181,44 @@ export function trimFirstCharOfLineContent(line = {}) {
|
|||
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) => {
|
||||
const { baseSha, headSha, startSha } = diffFile.diffRefs;
|
||||
const { newPath, oldPath } = diffFile;
|
||||
|
@ -194,3 +238,12 @@ export function getDiffRefsByLineCode(diffFiles) {
|
|||
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 Flash from '~/flash';
|
||||
|
||||
export default function gcpSignupOffer() {
|
||||
const alertEl = document.querySelector('.gcp-signup-offer');
|
||||
export default function initDismissableCallout(alertSelector) {
|
||||
const alertEl = document.querySelector(alertSelector);
|
||||
if (!alertEl) {
|
||||
return;
|
||||
}
|
|
@ -7,6 +7,19 @@ import axios from './lib/utils/axios_utils';
|
|||
|
||||
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) {
|
||||
const divHover = '<div class="div-dropzone-hover"></div>';
|
||||
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 $uploadingErrorMessage = form.find('.uploading-error-message');
|
||||
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 formTextarea = form.find('.js-gfm-input');
|
||||
let handlePaste;
|
||||
|
@ -42,7 +55,7 @@ export default function dropzoneInput(form) {
|
|||
|
||||
if (!uploadsPath) {
|
||||
$formDropzone.addClass('js-invalid-dropzone');
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const dropzone = $formDropzone.dropzone({
|
||||
|
@ -84,9 +97,7 @@ export default function dropzoneInput(form) {
|
|||
// xhr object (xhr.responseText is error message).
|
||||
// On error we hide the 'Attach' and 'Cancel' buttons
|
||||
// and show an error.
|
||||
|
||||
// If there's xhr error message, let's show it instead of dropzone's one.
|
||||
const message = xhr ? xhr.responseText : errorMessage;
|
||||
const message = getErrorMessage(errorMessage || xhr.responseText);
|
||||
|
||||
$uploadingErrorContainer.removeClass('hide');
|
||||
$uploadingErrorMessage.html(message);
|
||||
|
@ -274,4 +285,6 @@ export default function dropzoneInput(form) {
|
|||
$(this).closest('.gfm-form').find('.div-dropzone').click();
|
||||
formTextarea.focus();
|
||||
});
|
||||
|
||||
return Dropzone.forElement($formDropzone.get(0));
|
||||
}
|
||||
|
|
|
@ -65,8 +65,8 @@ export const hideMenu = (el) => {
|
|||
|
||||
const parentEl = el.parentNode;
|
||||
|
||||
el.style.display = ''; // eslint-disable-line no-param-reassign
|
||||
el.style.transform = ''; // eslint-disable-line no-param-reassign
|
||||
el.style.display = '';
|
||||
el.style.transform = '';
|
||||
el.classList.remove(IS_ABOVE_CLASS);
|
||||
parentEl.classList.remove(IS_OVER_CLASS);
|
||||
parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS);
|
||||
|
|
|
@ -2,14 +2,15 @@
|
|||
/* global Flash */
|
||||
|
||||
import $ from 'jquery';
|
||||
import { s__ } from '~/locale';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import loadingIcon from '~/vue_shared/components/loading_icon.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 { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
|
||||
import eventHub from '../event_hub';
|
||||
import { COMMON_STR } from '../constants';
|
||||
import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants';
|
||||
import groupsComponent from './groups.vue';
|
||||
|
||||
export default {
|
||||
|
@ -19,6 +20,16 @@ export default {
|
|||
groupsComponent,
|
||||
},
|
||||
props: {
|
||||
action: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
containerId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
required: true,
|
||||
|
@ -56,31 +67,28 @@ export default {
|
|||
? COMMON_STR.GROUP_SEARCH_EMPTY
|
||||
: COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
|
||||
|
||||
eventHub.$on('fetchPage', this.fetchPage);
|
||||
eventHub.$on('toggleChildren', this.toggleChildren);
|
||||
eventHub.$on('showLeaveGroupModal', this.showLeaveGroupModal);
|
||||
eventHub.$on('updatePagination', this.updatePagination);
|
||||
eventHub.$on('updateGroups', this.updateGroups);
|
||||
eventHub.$on(`${this.action}fetchPage`, this.fetchPage);
|
||||
eventHub.$on(`${this.action}toggleChildren`, this.toggleChildren);
|
||||
eventHub.$on(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
|
||||
eventHub.$on(`${this.action}updatePagination`, this.updatePagination);
|
||||
eventHub.$on(`${this.action}updateGroups`, this.updateGroups);
|
||||
},
|
||||
mounted() {
|
||||
this.fetchAllGroups();
|
||||
|
||||
if (this.containerId) {
|
||||
this.containerEl = document.getElementById(this.containerId);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
eventHub.$off('fetchPage', this.fetchPage);
|
||||
eventHub.$off('toggleChildren', this.toggleChildren);
|
||||
eventHub.$off('showLeaveGroupModal', this.showLeaveGroupModal);
|
||||
eventHub.$off('updatePagination', this.updatePagination);
|
||||
eventHub.$off('updateGroups', this.updateGroups);
|
||||
eventHub.$off(`${this.action}fetchPage`, this.fetchPage);
|
||||
eventHub.$off(`${this.action}toggleChildren`, this.toggleChildren);
|
||||
eventHub.$off(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
|
||||
eventHub.$off(`${this.action}updatePagination`, this.updatePagination);
|
||||
eventHub.$off(`${this.action}updateGroups`, this.updateGroups);
|
||||
},
|
||||
methods: {
|
||||
fetchGroups({
|
||||
parentId,
|
||||
page,
|
||||
filterGroupsBy,
|
||||
sortBy,
|
||||
archived,
|
||||
updatePagination,
|
||||
}) {
|
||||
fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) {
|
||||
return this.service
|
||||
.getGroups(parentId, page, filterGroupsBy, sortBy, archived)
|
||||
.then(res => {
|
||||
|
@ -165,13 +173,13 @@ export default {
|
|||
}
|
||||
},
|
||||
showLeaveGroupModal(group, parentGroup) {
|
||||
const { fullName } = group;
|
||||
this.targetGroup = group;
|
||||
this.targetParentGroup = parentGroup;
|
||||
this.showModal = true;
|
||||
this.groupLeaveConfirmationMessage = s__(
|
||||
`GroupsTree|Are you sure you want to leave the "${
|
||||
group.fullName
|
||||
}" group?`,
|
||||
this.groupLeaveConfirmationMessage = sprintf(
|
||||
s__('GroupsTree|Are you sure you want to leave the "%{fullName}" group?'),
|
||||
{ fullName },
|
||||
);
|
||||
},
|
||||
hideLeaveGroupModal() {
|
||||
|
@ -197,16 +205,35 @@ export default {
|
|||
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) {
|
||||
this.store.setPaginationInfo(headers);
|
||||
},
|
||||
updateGroups(groups, fromSearch) {
|
||||
this.isSearchEmpty = groups ? groups.length === 0 : false;
|
||||
const hasGroups = groups && groups.length > 0;
|
||||
this.isSearchEmpty = !hasGroups;
|
||||
|
||||
if (fromSearch) {
|
||||
this.store.setSearchedGroups(groups);
|
||||
} else {
|
||||
this.store.setGroups(groups);
|
||||
}
|
||||
|
||||
if (this.action && !hasGroups && !fromSearch) {
|
||||
this.showEmptyState();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -226,6 +253,7 @@ export default {
|
|||
:search-empty="isSearchEmpty"
|
||||
:search-empty-message="searchEmptyMessage"
|
||||
:page-info="pageInfo"
|
||||
:action="action"
|
||||
/>
|
||||
<deprecated-modal
|
||||
v-show="showModal"
|
||||
|
|
|
@ -11,8 +11,12 @@ export default {
|
|||
},
|
||||
groups: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: () => ([]),
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
@ -37,6 +41,7 @@ export default {
|
|||
:key="index"
|
||||
:group="group"
|
||||
:parent-group="parentGroup"
|
||||
:action="action"
|
||||
/>
|
||||
<li
|
||||
v-if="hasMoreChildren"
|
||||
|
|
|
@ -30,6 +30,11 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
groupDomId() {
|
||||
|
@ -56,10 +61,12 @@ export default {
|
|||
methods: {
|
||||
onClickRowGroup(e) {
|
||||
const NO_EXPAND_CLS = 'no-expand';
|
||||
if (!(e.target.classList.contains(NO_EXPAND_CLS) ||
|
||||
e.target.parentElement.classList.contains(NO_EXPAND_CLS))) {
|
||||
const targetClasses = e.target.classList;
|
||||
const parentElClasses = e.target.parentElement.classList;
|
||||
|
||||
if (!(targetClasses.contains(NO_EXPAND_CLS) || parentElClasses.contains(NO_EXPAND_CLS))) {
|
||||
if (this.hasChildren) {
|
||||
eventHub.$emit('toggleChildren', this.group);
|
||||
eventHub.$emit(`${this.action}toggleChildren`, this.group);
|
||||
} else {
|
||||
visitUrl(this.group.relativePath);
|
||||
}
|
||||
|
@ -93,7 +100,7 @@ export default {
|
|||
</div>
|
||||
<div
|
||||
:class="{ 'content-loading': group.isChildrenLoading }"
|
||||
class="avatar-container s24 d-none d-sm-block"
|
||||
class="avatar-container s24 d-none d-sm-flex"
|
||||
>
|
||||
<a
|
||||
:href="group.relativePath"
|
||||
|
@ -158,6 +165,7 @@ export default {
|
|||
v-if="group.isOpen && hasChildren"
|
||||
:parent-group="group"
|
||||
:groups="group.children"
|
||||
:action="action"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
|
|
|
@ -1,39 +1,44 @@
|
|||
<script>
|
||||
import tablePagination from '~/vue_shared/components/table_pagination.vue';
|
||||
import eventHub from '../event_hub';
|
||||
import { getParameterByName } from '../../lib/utils/common_utils';
|
||||
import tablePagination from '~/vue_shared/components/table_pagination.vue';
|
||||
import eventHub from '../event_hub';
|
||||
import { getParameterByName } from '../../lib/utils/common_utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
tablePagination,
|
||||
export default {
|
||||
components: {
|
||||
tablePagination,
|
||||
},
|
||||
props: {
|
||||
groups: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
groups: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
pageInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
searchEmpty: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
searchEmptyMessage: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
pageInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
methods: {
|
||||
change(page) {
|
||||
const filterGroupsParam = getParameterByName('filter_groups');
|
||||
const sortParam = getParameterByName('sort');
|
||||
const archivedParam = getParameterByName('archived');
|
||||
eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam);
|
||||
},
|
||||
searchEmpty: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
@ -47,6 +52,7 @@
|
|||
<group-folder
|
||||
v-if="!searchEmpty"
|
||||
:groups="groups"
|
||||
:action="action"
|
||||
/>
|
||||
<table-pagination
|
||||
v-if="!searchEmpty"
|
||||
|
|
|
@ -21,6 +21,11 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
leaveBtnTitle() {
|
||||
|
@ -32,7 +37,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
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 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 = {
|
||||
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'),
|
||||
EDIT_BTN_TITLE: s__('GroupsTree|Edit group'),
|
||||
GROUP_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups matched your search'),
|
||||
GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups or projects matched your search'),
|
||||
GROUP_SEARCH_EMPTY: s__('GroupsTree|No groups matched your search'),
|
||||
GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|No groups or projects matched your search'),
|
||||
};
|
||||
|
||||
export const ITEM_TYPE = {
|
||||
|
@ -17,8 +27,12 @@ export const ITEM_TYPE = {
|
|||
};
|
||||
|
||||
export const GROUP_VISIBILITY_TYPE = {
|
||||
public: __('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.'),
|
||||
public: __(
|
||||
'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.'),
|
||||
};
|
||||
|
||||
|
|
|
@ -4,13 +4,23 @@ import eventHub from './event_hub';
|
|||
import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils';
|
||||
|
||||
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);
|
||||
this.form = form;
|
||||
this.filterEndpoint = filterEndpoint;
|
||||
this.pagePath = pagePath;
|
||||
this.filterInputField = filterInputField;
|
||||
this.$dropdown = $(dropdownSel);
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
getFilterEndpoint() {
|
||||
|
@ -20,15 +30,16 @@ export default class GroupFilterableList extends FilterableList {
|
|||
getPagePath(queryData) {
|
||||
const params = queryData ? $.param(queryData) : '';
|
||||
const queryString = params ? `?${params}` : '';
|
||||
return `${this.pagePath}${queryString}`;
|
||||
const path = this.pagePath || window.location.pathname;
|
||||
return `${path}${queryString}`;
|
||||
}
|
||||
|
||||
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() {
|
||||
|
@ -53,7 +64,12 @@ export default class GroupFilterableList extends FilterableList {
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -65,11 +81,19 @@ export default class GroupFilterableList extends FilterableList {
|
|||
// Get type of option selected from dropdown
|
||||
const currentTargetClassList = e.currentTarget.parentElement.classList;
|
||||
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
|
||||
const sortParam = getParameterByName('sort', isOptionFilterBySort ? e.currentTarget.href : window.location.href);
|
||||
const archivedParam = getParameterByName('archived', isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href);
|
||||
const sortParam = getParameterByName(
|
||||
'sort',
|
||||
isOptionFilterBySort ? e.currentTarget.href : window.location.href,
|
||||
);
|
||||
const archivedParam = getParameterByName(
|
||||
'archived',
|
||||
isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href,
|
||||
);
|
||||
|
||||
if (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-menu li.js-filter-sort-order a').removeClass('is-active');
|
||||
} 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');
|
||||
|
@ -98,11 +124,19 @@ export default class GroupFilterableList extends FilterableList {
|
|||
onFilterSuccess(res, queryData) {
|
||||
const currentPath = this.getPagePath(queryData);
|
||||
|
||||
window.history.replaceState({
|
||||
page: currentPath,
|
||||
}, document.title, currentPath);
|
||||
window.history.replaceState(
|
||||
{
|
||||
page: currentPath,
|
||||
},
|
||||
document.title,
|
||||
currentPath,
|
||||
);
|
||||
|
||||
eventHub.$emit('updateGroups', res.data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField));
|
||||
eventHub.$emit('updatePagination', normalizeHeaders(res.headers));
|
||||
eventHub.$emit(
|
||||
`${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 groupFolderComponent from './components/group_folder.vue';
|
||||
import groupItemComponent from './components/group_item.vue';
|
||||
import { GROUPS_LIST_HOLDER_CLASS, CONTENT_LIST_CLASS } from './constants';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
export default () => {
|
||||
const el = document.getElementById('js-groups-tree');
|
||||
export default (containerId = 'js-groups-tree', endpoint, action = '') => {
|
||||
const containerEl = document.getElementById(containerId);
|
||||
let dataEl;
|
||||
|
||||
// Don't do anything if element doesn't exist (No groups)
|
||||
// This is for when the user enters directly to the page via URL
|
||||
if (!el) {
|
||||
if (!containerEl) {
|
||||
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-item', groupItemComponent);
|
||||
|
||||
|
@ -29,20 +37,26 @@ export default () => {
|
|||
groupsApp,
|
||||
},
|
||||
data() {
|
||||
const { dataset } = this.$options.el;
|
||||
const { dataset } = dataEl || this.$options.el;
|
||||
const hideProjects = dataset.hideProjects === 'true';
|
||||
const service = new GroupsService(endpoint || dataset.endpoint);
|
||||
const store = new GroupsStore(hideProjects);
|
||||
const service = new GroupsService(dataset.endpoint);
|
||||
|
||||
return {
|
||||
action,
|
||||
store,
|
||||
service,
|
||||
hideProjects,
|
||||
loading: true,
|
||||
containerId,
|
||||
};
|
||||
},
|
||||
beforeMount() {
|
||||
const { dataset } = this.$options.el;
|
||||
if (this.action) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { dataset } = dataEl || this.$options.el;
|
||||
let groupFilterList = null;
|
||||
const form = document.querySelector(dataset.formSel);
|
||||
const filter = document.querySelector(dataset.filterSel);
|
||||
|
@ -52,10 +66,11 @@ export default () => {
|
|||
form,
|
||||
filter,
|
||||
holder,
|
||||
filterEndpoint: dataset.endpoint,
|
||||
filterEndpoint: endpoint || dataset.endpoint,
|
||||
pagePath: dataset.path,
|
||||
dropdownSel: dataset.dropdownSel,
|
||||
filterInputField: 'filter',
|
||||
action: this.action,
|
||||
};
|
||||
|
||||
groupFilterList = new GroupFilterableList(opts);
|
||||
|
@ -64,9 +79,11 @@ export default () => {
|
|||
render(createElement) {
|
||||
return createElement('groups-app', {
|
||||
props: {
|
||||
action: this.action,
|
||||
store: this.store,
|
||||
service: this.service,
|
||||
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>
|
||||
import $ from 'jquery';
|
||||
import { mapActions } from 'vuex';
|
||||
import { __, sprintf } from '~/locale';
|
||||
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 ListItem from './list_item.vue';
|
||||
|
||||
|
@ -9,6 +11,7 @@ export default {
|
|||
components: {
|
||||
Icon,
|
||||
ListItem,
|
||||
GlModal,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
|
@ -56,6 +59,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emptyStateText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: __('No changes'),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
titleText() {
|
||||
|
@ -68,11 +76,19 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['stageAllChanges', 'unstageAllChanges']),
|
||||
...mapActions(['stageAllChanges', 'unstageAllChanges', 'discardAllChanges']),
|
||||
actionBtnClicked() {
|
||||
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>
|
||||
|
||||
|
@ -81,27 +97,32 @@ export default {
|
|||
class="ide-commit-list-container"
|
||||
>
|
||||
<header
|
||||
class="multi-file-commit-panel-header"
|
||||
class="multi-file-commit-panel-header d-flex mb-0"
|
||||
>
|
||||
<div
|
||||
class="multi-file-commit-panel-header-title"
|
||||
class="d-flex align-items-center flex-fill"
|
||||
>
|
||||
<icon
|
||||
v-once
|
||||
:name="iconName"
|
||||
:size="18"
|
||||
class="append-right-8"
|
||||
/>
|
||||
{{ titleText }}
|
||||
<strong>
|
||||
{{ titleText }}
|
||||
</strong>
|
||||
<div class="d-flex ml-auto">
|
||||
<button
|
||||
v-tooltip
|
||||
v-show="filesLength"
|
||||
:class="{
|
||||
'd-flex': filesLength
|
||||
}"
|
||||
ref="actionBtn"
|
||||
:title="actionBtnText"
|
||||
:aria-label="actionBtnText"
|
||||
:disabled="!filesLength"
|
||||
:class="{
|
||||
'disabled-content': !filesLength
|
||||
}"
|
||||
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-container="body"
|
||||
data-boundary="viewport"
|
||||
|
@ -109,18 +130,32 @@ export default {
|
|||
>
|
||||
<icon
|
||||
:name="actionBtnIcon"
|
||||
:size="12"
|
||||
:size="16"
|
||||
class="ml-auto mr-auto"
|
||||
/>
|
||||
</button>
|
||||
<span
|
||||
<button
|
||||
v-tooltip
|
||||
v-if="!stagedList"
|
||||
:title="__('Discard all changes')"
|
||||
:aria-label="__('Discard all changes')"
|
||||
:disabled="!filesLength"
|
||||
: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 }}
|
||||
</span>
|
||||
<icon
|
||||
:size="16"
|
||||
name="remove-all"
|
||||
class="ml-auto mr-auto"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
@ -143,9 +178,19 @@ export default {
|
|||
</ul>
|
||||
<p
|
||||
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>
|
||||
<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>
|
||||
</template>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { mapActions } from 'vuex';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import StageButton from './stage_button.vue';
|
||||
import UnstageButton from './unstage_button.vue';
|
||||
import { viewerTypes } from '../../constants';
|
||||
|
@ -12,6 +13,7 @@ export default {
|
|||
Icon,
|
||||
StageButton,
|
||||
UnstageButton,
|
||||
FileIcon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
|
@ -48,7 +50,7 @@ export default {
|
|||
return `${getCommitIconMap(this.file).icon}${suffix}`;
|
||||
},
|
||||
iconClass() {
|
||||
return `${getCommitIconMap(this.file).class} append-right-8`;
|
||||
return `${getCommitIconMap(this.file).class} ml-auto mr-auto`;
|
||||
},
|
||||
fullKey() {
|
||||
return `${this.keyPrefix}-${this.file.key}`;
|
||||
|
@ -105,17 +107,24 @@ export default {
|
|||
@click="openFileInEditor"
|
||||
>
|
||||
<span class="multi-file-commit-list-file-path d-flex align-items-center">
|
||||
<icon
|
||||
:name="iconName"
|
||||
:size="16"
|
||||
:css-classes="iconClass"
|
||||
<file-icon
|
||||
:file-name="file.name"
|
||||
class="append-right-8"
|
||||
/>{{ file.name }}
|
||||
</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>
|
||||
<component
|
||||
:is="actionComponent"
|
||||
:path="file.path"
|
||||
class="d-flex position-absolute"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import { mapActions } from 'vuex';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import GlModal from '~/vue_shared/components/gl_modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
GlModal,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
|
@ -16,8 +20,22 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
modalId() {
|
||||
return `discard-file-${this.path}`;
|
||||
},
|
||||
modalTitle() {
|
||||
return sprintf(
|
||||
__('Discard changes to %{path}?'),
|
||||
{ path: this.path },
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['stageChange', 'discardFileChanges']),
|
||||
showDiscardModal() {
|
||||
$(document.getElementById(this.modalId)).modal('show');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -25,51 +43,50 @@ export default {
|
|||
<template>
|
||||
<div
|
||||
v-once
|
||||
class="multi-file-discard-btn dropdown"
|
||||
class="multi-file-discard-btn d-flex"
|
||||
>
|
||||
<button
|
||||
v-tooltip
|
||||
:aria-label="__('Stage changes')"
|
||||
:title="__('Stage changes')"
|
||||
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-boundary="viewport"
|
||||
data-placement="bottom"
|
||||
@click.stop="stageChange(path)"
|
||||
@click.stop.prevent="stageChange(path)"
|
||||
>
|
||||
<icon
|
||||
:size="12"
|
||||
:size="16"
|
||||
name="mobile-issue-close"
|
||||
class="ml-auto mr-auto"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-tooltip
|
||||
:title="__('More actions')"
|
||||
:aria-label="__('Discard changes')"
|
||||
:title="__('Discard changes')"
|
||||
type="button"
|
||||
class="btn btn-blank d-flex align-items-center"
|
||||
class="btn btn-blank align-items-center"
|
||||
data-container="body"
|
||||
data-boundary="viewport"
|
||||
data-placement="bottom"
|
||||
data-toggle="dropdown"
|
||||
data-display="static"
|
||||
@click.stop.prevent="showDiscardModal"
|
||||
>
|
||||
<icon
|
||||
:size="12"
|
||||
name="ellipsis_h"
|
||||
:size="16"
|
||||
name="remove"
|
||||
class="ml-auto mr-auto"
|
||||
/>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<ul>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
@click.stop="discardFileChanges(path)"
|
||||
>
|
||||
{{ __('Discard changes') }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<gl-modal
|
||||
:id="modalId"
|
||||
:header-title-text="modalTitle"
|
||||
:footer-primary-button-text="__('Discard changes')"
|
||||
footer-primary-button-variant="danger"
|
||||
@submit="discardFileChanges(path)"
|
||||
>
|
||||
{{ __("You will loose all changes you've made to this file. This action cannot be undone.") }}
|
||||
</gl-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -25,22 +25,23 @@ export default {
|
|||
<template>
|
||||
<div
|
||||
v-once
|
||||
class="multi-file-discard-btn"
|
||||
class="multi-file-discard-btn d-flex"
|
||||
>
|
||||
<button
|
||||
v-tooltip
|
||||
:aria-label="__('Unstage changes')"
|
||||
:title="__('Unstage changes')"
|
||||
type="button"
|
||||
class="btn btn-blank d-flex align-items-center"
|
||||
class="btn btn-blank align-items-center"
|
||||
data-container="body"
|
||||
data-boundary="viewport"
|
||||
data-placement="bottom"
|
||||
@click="unstageChange(path)"
|
||||
@click.stop.prevent="unstageChange(path)"
|
||||
>
|
||||
<icon
|
||||
:size="12"
|
||||
name="history"
|
||||
:size="16"
|
||||
name="redo"
|
||||
class="ml-auto mr-auto"
|
||||
/>
|
||||
</button>
|
||||
</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 RightPane from './panes/right.vue';
|
||||
import ErrorMessage from './error_message.vue';
|
||||
import CommitEditorHeader from './commit_sidebar/editor_header.vue';
|
||||
|
||||
const originalStopCallback = Mousetrap.stopCallback;
|
||||
|
||||
|
@ -23,6 +24,7 @@ export default {
|
|||
FindFile,
|
||||
RightPane,
|
||||
ErrorMessage,
|
||||
CommitEditorHeader,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
|
@ -34,7 +36,7 @@ export default {
|
|||
'currentProjectId',
|
||||
'errorMessage',
|
||||
]),
|
||||
...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges']),
|
||||
...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges', 'isCommitModeActive']),
|
||||
},
|
||||
mounted() {
|
||||
window.onbeforeunload = e => this.onBeforeUnload(e);
|
||||
|
@ -96,7 +98,12 @@ export default {
|
|||
<template
|
||||
v-if="activeFile"
|
||||
>
|
||||
<commit-editor-header
|
||||
v-if="isCommitModeActive"
|
||||
:active-file="activeFile"
|
||||
/>
|
||||
<repo-tabs
|
||||
v-else
|
||||
:active-file="activeFile"
|
||||
:files="openFiles"
|
||||
:viewer="viewer"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import { __ } from '~/locale';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import GlModal from '~/vue_shared/components/gl_modal.vue';
|
||||
import { modalTypes } from '../../constants';
|
||||
|
||||
|
@ -15,6 +16,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState(['entryModal']),
|
||||
...mapGetters('fileTemplates', ['templateTypes']),
|
||||
entryName: {
|
||||
get() {
|
||||
if (this.entryModal.type === modalTypes.rename) {
|
||||
|
@ -31,7 +33,9 @@ export default {
|
|||
if (this.entryModal.type === modalTypes.tree) {
|
||||
return __('Create new directory');
|
||||
} 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');
|
||||
|
@ -40,11 +44,16 @@ export default {
|
|||
if (this.entryModal.type === modalTypes.tree) {
|
||||
return __('Create directory');
|
||||
} 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');
|
||||
},
|
||||
isCreatingNew() {
|
||||
return this.entryModal.type !== modalTypes.rename;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...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() {
|
||||
this.$refs.fieldName.focus();
|
||||
},
|
||||
|
@ -77,6 +94,7 @@ export default {
|
|||
:header-title-text="modalTitle"
|
||||
:footer-primary-button-text="buttonLabel"
|
||||
footer-primary-button-variant="success"
|
||||
modal-size="lg"
|
||||
@submit="submitForm"
|
||||
@open="focusInput"
|
||||
@closed="closedModal"
|
||||
|
@ -84,16 +102,35 @@ export default {
|
|||
<div
|
||||
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') }}
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="col-sm-10">
|
||||
<input
|
||||
ref="fieldName"
|
||||
v-model="entryName"
|
||||
type="text"
|
||||
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>
|
||||
</gl-modal>
|
||||
|
|
|
@ -95,8 +95,9 @@ export default {
|
|||
:file-list="changedFiles"
|
||||
:action-btn-text="__('Stage all changes')"
|
||||
:active-file-key="activeFileKey"
|
||||
:empty-state-text="__('There are no unstaged changes')"
|
||||
action="stageAllChanges"
|
||||
action-btn-icon="mobile-issue-close"
|
||||
action-btn-icon="stage-all"
|
||||
item-action-component="stage-button"
|
||||
class="is-first"
|
||||
icon-name="unstaged"
|
||||
|
@ -108,8 +109,9 @@ export default {
|
|||
:action-btn-text="__('Unstage all changes')"
|
||||
:staged-list="true"
|
||||
:active-file-key="activeFileKey"
|
||||
:empty-state-text="__('There are no staged changes')"
|
||||
action="unstageAllChanges"
|
||||
action-btn-icon="history"
|
||||
action-btn-icon="unstage-all"
|
||||
item-action-component="unstage-button"
|
||||
icon-name="staged"
|
||||
/>
|
||||
|
|
|
@ -6,12 +6,14 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
|
|||
import { activityBarViews, viewerTypes } from '../constants';
|
||||
import Editor from '../lib/editor';
|
||||
import ExternalLink from './external_link.vue';
|
||||
import FileTemplatesBar from './file_templates/bar.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContentViewer,
|
||||
DiffViewer,
|
||||
ExternalLink,
|
||||
FileTemplatesBar,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
|
@ -34,6 +36,7 @@ export default {
|
|||
'isCommitModeActive',
|
||||
'isReviewModeActive',
|
||||
]),
|
||||
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
|
||||
shouldHideEditor() {
|
||||
return this.file && this.file.binary && !this.file.content;
|
||||
},
|
||||
|
@ -216,7 +219,7 @@ export default {
|
|||
id="ide"
|
||||
class="blob-viewer-container blob-editor-container"
|
||||
>
|
||||
<div class="ide-mode-tabs clearfix" >
|
||||
<div class="ide-mode-tabs clearfix">
|
||||
<ul
|
||||
v-if="!shouldHideEditor && isEditModeActive"
|
||||
class="nav-links float-left"
|
||||
|
@ -249,6 +252,9 @@ export default {
|
|||
:file="file"
|
||||
/>
|
||||
</div>
|
||||
<file-templates-bar
|
||||
v-if="showFileTemplatesBar(file.name)"
|
||||
/>
|
||||
<div
|
||||
v-show="!shouldHideEditor && file.viewMode ==='editor'"
|
||||
ref="editor"
|
||||
|
|
|
@ -4,6 +4,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
|
|||
import flash from '~/flash';
|
||||
import * as types from './mutation_types';
|
||||
import FilesDecoratorWorker from './workers/files_decorator_worker';
|
||||
import { stageKeys } from '../constants';
|
||||
|
||||
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, '');
|
||||
|
||||
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));
|
||||
|
||||
dispatch('openPendingTab', {
|
||||
file: state.changedFiles.find(f => f.path === openFile.path),
|
||||
keyPrefix: stageKeys.unstaged,
|
||||
});
|
||||
};
|
||||
|
||||
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 }) => {
|
||||
const entry = state.entries[entryPath || path];
|
||||
|
||||
commit(types.RENAME_ENTRY, { path, name, entryPath });
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@ import service from '../../services';
|
|||
import * as types from '../mutation_types';
|
||||
import router from '../../ide_router';
|
||||
import { setPageTitle } from '../utils';
|
||||
import { viewerTypes } from '../../constants';
|
||||
import { viewerTypes, stageKeys } from '../../constants';
|
||||
|
||||
export const closeFile = ({ commit, state, dispatch }, 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);
|
||||
};
|
||||
|
||||
export const stageChange = ({ commit, state }, path) => {
|
||||
export const stageChange = ({ commit, state, dispatch }, 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.SET_LAST_COMMIT_MSG, '');
|
||||
|
@ -217,21 +218,39 @@ export const stageChange = ({ commit, state }, path) => {
|
|||
if (stagedFile) {
|
||||
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);
|
||||
|
||||
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;
|
||||
|
||||
state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`));
|
||||
|
||||
commit(types.ADD_PENDING_TAB, { file, keyPrefix });
|
||||
|
||||
dispatch('scrollToTab');
|
||||
|
||||
router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`);
|
||||
|
||||
return true;
|
||||
|
|
|
@ -8,6 +8,7 @@ import commitModule from './modules/commit';
|
|||
import pipelines from './modules/pipelines';
|
||||
import mergeRequests from './modules/merge_requests';
|
||||
import branches from './modules/branches';
|
||||
import fileTemplates from './modules/file_templates';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
|
@ -22,6 +23,7 @@ export const createStore = () =>
|
|||
pipelines,
|
||||
mergeRequests,
|
||||
branches,
|
||||
fileTemplates: fileTemplates(),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Api from '~/api';
|
||||
import { __ } from '~/locale';
|
||||
import * as types from './mutation_types';
|
||||
import eventHub from '../../../eventhub';
|
||||
|
||||
export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES);
|
||||
export const receiveTemplateTypesError = ({ commit, dispatch }) => {
|
||||
|
@ -31,9 +32,23 @@ export const fetchTemplateTypes = ({ dispatch, state }) => {
|
|||
.catch(() => dispatch('receiveTemplateTypesError'));
|
||||
};
|
||||
|
||||
export const setSelectedTemplateType = ({ commit }, type) =>
|
||||
export const setSelectedTemplateType = ({ commit, dispatch, rootGetters }, 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) => {
|
||||
dispatch(
|
||||
'setErrorMessage',
|
||||
|
@ -69,6 +84,7 @@ export const setFileTemplate = ({ dispatch, commit, rootGetters }, template) =>
|
|||
{ root: 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 }) => {
|
||||
|
@ -76,6 +92,12 @@ export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => {
|
|||
|
||||
dispatch('changeFileContent', { path: file.path, content: file.raw }, { root: true });
|
||||
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
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { activityBarViews } from '../../../constants';
|
||||
|
||||
export const templateTypes = () => [
|
||||
{
|
||||
name: '.gitlab-ci.yml',
|
||||
|
@ -17,7 +19,8 @@ export const templateTypes = () => [
|
|||
},
|
||||
];
|
||||
|
||||
export const showFileTemplatesBar = (_, getters) => name =>
|
||||
getters.templateTypes.find(t => t.name === name);
|
||||
export const showFileTemplatesBar = (_, getters, rootState) => name =>
|
||||
getters.templateTypes.find(t => t.name === name) &&
|
||||
rootState.currentActivityView === activityBarViews.edit;
|
||||
|
||||
export default () => {};
|
||||
|
|
|
@ -3,10 +3,10 @@ import * as actions from './actions';
|
|||
import * as getters from './getters';
|
||||
import mutations from './mutations';
|
||||
|
||||
export default {
|
||||
export default () => ({
|
||||
namespaced: true,
|
||||
actions,
|
||||
state: createState(),
|
||||
getters,
|
||||
mutations,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
import * as types from './mutation_types';
|
||||
import { normalizeJob } from './utils';
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
import Vue from 'vue';
|
||||
import * as types from './mutation_types';
|
||||
import projectMutations from './mutations/project';
|
||||
import mergeRequestMutation from './mutations/merge_request';
|
||||
|
@ -227,7 +227,7 @@ export default {
|
|||
path: newPath,
|
||||
name: entryPath ? oldEntry.name : name,
|
||||
tempFile: true,
|
||||
prevPath: oldEntry.path,
|
||||
prevPath: oldEntry.tempFile ? null : oldEntry.path,
|
||||
url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath),
|
||||
tree: [],
|
||||
parentPath,
|
||||
|
@ -246,6 +246,20 @@ export default {
|
|||
if (newEntry.type === 'blob') {
|
||||
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,
|
||||
...mergeRequestMutation,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
import * as types from '../mutation_types';
|
||||
import { sortTree } from '../utils';
|
||||
import { diffModes } from '../../constants';
|
||||
|
@ -56,7 +55,7 @@ export default {
|
|||
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], {
|
||||
content: raw,
|
||||
});
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
|
|
|
@ -131,16 +131,43 @@ export const parseUrlPathname = url => {
|
|||
return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`;
|
||||
};
|
||||
|
||||
// We can trust that each param has one & since values containing & will be encoded
|
||||
// Remove the first character of search as it is always ?
|
||||
export const getUrlParamsArray = () =>
|
||||
window.location.search
|
||||
.slice(1)
|
||||
.split('&')
|
||||
.map(param => {
|
||||
const split = param.split('=');
|
||||
return [decodeURI(split[0]), split[1]].join('=');
|
||||
});
|
||||
const splitPath = (path = '') => path
|
||||
.replace(/^\?/, '')
|
||||
.split('&');
|
||||
|
||||
export const urlParamsToArray = (path = '') => splitPath(path)
|
||||
.filter(param => param.length > 0)
|
||||
.map(param => {
|
||||
const split = param.split('=');
|
||||
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;
|
||||
|
||||
|
|
|
@ -48,6 +48,16 @@ export const dasherize = str => str.replace(/[_\s]+/g, '-');
|
|||
*/
|
||||
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
|
||||
*
|
||||
|
|
|
@ -47,9 +47,9 @@ export function removeParamQueryString(url, param) {
|
|||
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');
|
||||
url.href = window.location.href;
|
||||
url.href = source;
|
||||
|
||||
params.forEach(param => {
|
||||
url.search = removeParamQueryString(url.search, param);
|
||||
|
|
|
@ -29,6 +29,7 @@ import './milestone_select';
|
|||
import './frequent_items';
|
||||
import initBreadcrumbs from './breadcrumb';
|
||||
import initDispatcher from './dispatcher';
|
||||
import initUsagePingConsent from './usage_ping_consent';
|
||||
|
||||
// expose jQuery as global (TODO: remove these)
|
||||
window.jQuery = jQuery;
|
||||
|
@ -78,6 +79,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
initImporterStatus();
|
||||
initTodoToggle();
|
||||
initLogoAnimation();
|
||||
initUsagePingConsent();
|
||||
|
||||
// Set the default path for all cookies to GitLab's root directory
|
||||
Cookies.defaults.path = gon.relative_url_root || '/';
|
||||
|
|
|
@ -82,11 +82,12 @@ export default {
|
|||
value: 0,
|
||||
},
|
||||
currentXCoordinate: 0,
|
||||
currentCoordinates: [],
|
||||
currentCoordinates: {},
|
||||
showFlag: false,
|
||||
showFlagContent: false,
|
||||
timeSeries: [],
|
||||
realPixelRatio: 1,
|
||||
seriesUnderMouse: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -126,6 +127,9 @@ export default {
|
|||
this.draw();
|
||||
},
|
||||
methods: {
|
||||
showDot(path) {
|
||||
return this.showFlagContent && this.seriesUnderMouse.includes(path);
|
||||
},
|
||||
draw() {
|
||||
const breakpointSize = bp.getBreakpointSize();
|
||||
const query = this.graphData.queries[0];
|
||||
|
@ -155,7 +159,24 @@ export default {
|
|||
point.y = e.clientY;
|
||||
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
|
||||
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 overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
|
||||
const d0 = firstTimeSeries.values[overlayIndex - 1];
|
||||
|
@ -190,6 +211,17 @@ export default {
|
|||
axisXScale.domain(d3.extent(allValues, d => d.time));
|
||||
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
|
||||
.axisBottom()
|
||||
.scale(axisXScale)
|
||||
|
@ -277,9 +309,8 @@ export default {
|
|||
:line-style="path.lineStyle"
|
||||
:line-color="path.lineColor"
|
||||
:area-color="path.areaColor"
|
||||
:current-coordinates="currentCoordinates[index]"
|
||||
:current-time-series-index="index"
|
||||
:show-dot="showFlagContent"
|
||||
:current-coordinates="currentCoordinates[path.metricTag]"
|
||||
:show-dot="showDot(path)"
|
||||
/>
|
||||
<graph-deployment
|
||||
:deployment-data="reducedDeploymentData"
|
||||
|
@ -303,7 +334,7 @@ export default {
|
|||
:graph-height="graphHeight"
|
||||
:graph-height-offset="graphHeightOffset"
|
||||
:show-flag-content="showFlagContent"
|
||||
:time-series="timeSeries"
|
||||
:time-series="seriesUnderMouse"
|
||||
:unit-of-display="unitOfDisplay"
|
||||
:legend-title="legendTitle"
|
||||
:deployment-flag-data="deploymentFlagData"
|
||||
|
|
|
@ -52,7 +52,7 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
currentCoordinates: {
|
||||
type: Array,
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
@ -91,8 +91,8 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
seriesMetricValue(seriesIndex, series) {
|
||||
const indexFromCoordinates = this.currentCoordinates[seriesIndex]
|
||||
? this.currentCoordinates[seriesIndex].currentDataIndex : 0;
|
||||
const indexFromCoordinates = this.currentCoordinates[series.metricTag]
|
||||
? this.currentCoordinates[series.metricTag].currentDataIndex : 0;
|
||||
const index = this.deploymentFlagData
|
||||
? this.deploymentFlagData.seriesIndex
|
||||
: indexFromCoordinates;
|
||||
|
|
|
@ -50,19 +50,24 @@ const mixins = {
|
|||
},
|
||||
|
||||
positionFlag() {
|
||||
const timeSeries = this.timeSeries[0];
|
||||
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
|
||||
const timeSeries = this.seriesUnderMouse[0];
|
||||
if (!timeSeries) {
|
||||
return;
|
||||
}
|
||||
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate);
|
||||
|
||||
this.currentData = timeSeries.values[hoveredDataIndex];
|
||||
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
|
||||
|
||||
this.currentCoordinates = this.timeSeries.map((series) => {
|
||||
const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate, 1);
|
||||
this.currentCoordinates = {};
|
||||
|
||||
this.seriesUnderMouse.forEach((series) => {
|
||||
const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate);
|
||||
const currentData = series.values[currentDataIndex];
|
||||
const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
|
||||
const currentY = Math.floor(series.timeSeriesScaleY(currentData.value));
|
||||
|
||||
return {
|
||||
this.currentCoordinates[series.metricTag] = {
|
||||
currentX,
|
||||
currentY,
|
||||
currentDataIndex,
|
||||
|
|
|
@ -2,7 +2,7 @@ import _ from 'underscore';
|
|||
import { scaleLinear, scaleTime } from 'd3-scale';
|
||||
import { line, area, curveLinear } from 'd3-shape';
|
||||
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';
|
||||
|
||||
const d3 = {
|
||||
|
@ -14,6 +14,7 @@ const d3 = {
|
|||
extent,
|
||||
max,
|
||||
timeMinute,
|
||||
timeSecond,
|
||||
sum,
|
||||
};
|
||||
|
||||
|
@ -51,6 +52,24 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
|
|||
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) => {
|
||||
let metricTag = '';
|
||||
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({
|
||||
linePath: lineFunction(timeSeries.values),
|
||||
areaPath: areaFunction(timeSeries.values),
|
||||
linePath: lineFunction(values),
|
||||
areaPath: areaFunction(values),
|
||||
timeSeriesScaleX,
|
||||
timeSeriesScaleY,
|
||||
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', '.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
|
||||
this.$wrapperEl.on('visibilitychange', this.visibilityChange);
|
||||
|
@ -252,9 +256,7 @@ export default class Notes {
|
|||
discussionNoteForm = $textarea.closest('.js-discussion-note-form');
|
||||
if (discussionNoteForm.length) {
|
||||
if ($textarea.val() !== '') {
|
||||
if (
|
||||
!window.confirm('Are you sure you want to cancel creating this comment?')
|
||||
) {
|
||||
if (!window.confirm('Are you sure you want to cancel creating this comment?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -266,9 +268,7 @@ export default class Notes {
|
|||
originalText = $textarea.closest('form').data('originalNote');
|
||||
newText = $textarea.val();
|
||||
if (originalText !== newText) {
|
||||
if (
|
||||
!window.confirm('Are you sure you want to cancel editing this comment?')
|
||||
) {
|
||||
if (!window.confirm('Are you sure you want to cancel editing this comment?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -1074,7 +1074,7 @@ export default class Notes {
|
|||
addForm = false;
|
||||
let lineTypeSelector = '';
|
||||
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
|
||||
if (this.isParallelView()) {
|
||||
lineTypeSelector = `.${lineType}`;
|
||||
|
@ -1316,8 +1316,7 @@ export default class Notes {
|
|||
|
||||
$retryButton.prop('disabled', true);
|
||||
|
||||
return this.loadLazyDiff(e)
|
||||
.then(() => {
|
||||
return this.loadLazyDiff(e).then(() => {
|
||||
$retryButton.prop('disabled', false);
|
||||
});
|
||||
}
|
||||
|
@ -1343,18 +1342,18 @@ export default class Notes {
|
|||
*/
|
||||
if (url) {
|
||||
return axios
|
||||
.get(url)
|
||||
.then(({ data }) => {
|
||||
// Reset state in case last request returned error
|
||||
$successContainer.removeClass('hidden');
|
||||
$errorContainer.addClass('hidden');
|
||||
.get(url)
|
||||
.then(({ data }) => {
|
||||
// Reset state in case last request returned error
|
||||
$successContainer.removeClass('hidden');
|
||||
$errorContainer.addClass('hidden');
|
||||
|
||||
Notes.renderDiffContent($container, data);
|
||||
})
|
||||
.catch(() => {
|
||||
$successContainer.addClass('hidden');
|
||||
$errorContainer.removeClass('hidden');
|
||||
});
|
||||
Notes.renderDiffContent($container, data);
|
||||
})
|
||||
.catch(() => {
|
||||
$successContainer.addClass('hidden');
|
||||
$errorContainer.removeClass('hidden');
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -1545,12 +1544,8 @@ export default class Notes {
|
|||
<div class="note-header">
|
||||
<div class="note-header-info">
|
||||
<a href="/${_.escape(currentUsername)}">
|
||||
<span class="d-none d-sm-inline-block">${_.escape(
|
||||
currentUsername,
|
||||
)}</span>
|
||||
<span class="note-headline-light">${_.escape(
|
||||
currentUsername,
|
||||
)}</span>
|
||||
<span class="d-none d-sm-inline-block">${_.escape(currentUsername)}</span>
|
||||
<span class="note-headline-light">${_.escape(currentUsername)}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1565,9 +1560,7 @@ export default class Notes {
|
|||
);
|
||||
|
||||
$tempNote.find('.d-none.d-sm-inline-block').text(_.escape(currentUserFullname));
|
||||
$tempNote
|
||||
.find('.note-headline-light')
|
||||
.text(`@${_.escape(currentUsername)}`);
|
||||
$tempNote.find('.note-headline-light').text(`@${_.escape(currentUsername)}`);
|
||||
|
||||
return $tempNote;
|
||||
}
|
||||
|
|
|
@ -148,10 +148,9 @@ export default {
|
|||
</tr>
|
||||
<tr class="notes_holder">
|
||||
<td
|
||||
class="notes_line"
|
||||
colspan="2"
|
||||
></td>
|
||||
<td class="notes_content">
|
||||
class="notes_content"
|
||||
colspan="3"
|
||||
>
|
||||
<slot></slot>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -24,12 +24,13 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
noteId: {
|
||||
type: Number,
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
noteUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
accessLevel: {
|
||||
type: String,
|
||||
|
@ -225,11 +226,11 @@ export default {
|
|||
Report as abuse
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<li v-if="noteUrl">
|
||||
<button
|
||||
:data-clipboard-text="noteUrl"
|
||||
type="button"
|
||||
css-class="btn-default btn-transparent"
|
||||
class="btn-default btn-transparent js-btn-copy-note-link"
|
||||
>
|
||||
Copy link
|
||||
</button>
|
||||
|
|
|
@ -25,7 +25,7 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
noteId: {
|
||||
type: Number,
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
canAwardEmoji: {
|
||||
|
|
|
@ -20,9 +20,9 @@ export default {
|
|||
default: '',
|
||||
},
|
||||
noteId: {
|
||||
type: Number,
|
||||
type: String,
|
||||
required: false,
|
||||
default: 0,
|
||||
default: '',
|
||||
},
|
||||
markdownVersion: {
|
||||
type: Number,
|
||||
|
@ -67,7 +67,10 @@ export default {
|
|||
'getUserDataByProp',
|
||||
]),
|
||||
noteHash() {
|
||||
return `#note_${this.noteId}`;
|
||||
if (this.noteId) {
|
||||
return `#note_${this.noteId}`;
|
||||
}
|
||||
return '#';
|
||||
},
|
||||
markdownPreviewPath() {
|
||||
return this.getNoteableDataByProp('preview_note_path');
|
||||
|
|
|
@ -9,7 +9,8 @@ export default {
|
|||
props: {
|
||||
author: {
|
||||
type: Object,
|
||||
required: true,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
createdAt: {
|
||||
type: String,
|
||||
|
@ -21,7 +22,7 @@ export default {
|
|||
default: '',
|
||||
},
|
||||
noteId: {
|
||||
type: Number,
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
includeToggle: {
|
||||
|
@ -72,7 +73,10 @@ export default {
|
|||
{{ __('Toggle discussion') }}
|
||||
</button>
|
||||
</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
|
||||
v-if="author.status_tooltip_html"
|
||||
|
@ -81,6 +85,9 @@ export default {
|
|||
@{{ author.username }}
|
||||
</span>
|
||||
</a>
|
||||
<span v-else>
|
||||
{{ __('A deleted user') }}
|
||||
</span>
|
||||
<span class="note-headline-light">
|
||||
<span class="note-headline-meta">
|
||||
<template v-if="actionText">
|
||||
|
|
|
@ -137,8 +137,10 @@ export default {
|
|||
return this.unresolvedDiscussions.length > 1;
|
||||
},
|
||||
showJumpToNextDiscussion() {
|
||||
return this.hasMultipleUnresolvedDiscussions &&
|
||||
!this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder);
|
||||
return (
|
||||
this.hasMultipleUnresolvedDiscussions &&
|
||||
!this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder)
|
||||
);
|
||||
},
|
||||
shouldRenderDiffs() {
|
||||
const { diffDiscussion, diffFile } = this.transformedDiscussion;
|
||||
|
@ -256,11 +258,16 @@ Please check your network connection and try again.`;
|
|||
});
|
||||
},
|
||||
jumpToNextDiscussion() {
|
||||
const nextId =
|
||||
this.nextUnresolvedDiscussionId(this.discussion.id, this.discussionsByDiffOrder);
|
||||
const nextId = this.nextUnresolvedDiscussionId(
|
||||
this.discussion.id,
|
||||
this.discussionsByDiffOrder,
|
||||
);
|
||||
|
||||
this.jumpToDiscussion(nextId);
|
||||
},
|
||||
deleteNoteHandler(note) {
|
||||
this.$emit('noteDeleted', this.discussion, note);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -270,6 +277,7 @@ Please check your network connection and try again.`;
|
|||
<div class="timeline-entry-inner">
|
||||
<div class="timeline-icon">
|
||||
<user-avatar-link
|
||||
v-if="author"
|
||||
:link-href="author.path"
|
||||
:img-src="author.avatar_url"
|
||||
:img-alt="author.name"
|
||||
|
@ -344,6 +352,7 @@ Please check your network connection and try again.`;
|
|||
:is="componentName(note)"
|
||||
:note="componentData(note)"
|
||||
:key="note.id"
|
||||
@handleDeleteNote="deleteNoteHandler"
|
||||
/>
|
||||
</ul>
|
||||
<div
|
||||
|
|
|
@ -86,6 +86,7 @@ export default {
|
|||
// eslint-disable-next-line no-alert
|
||||
if (window.confirm('Are you sure you want to delete this comment?')) {
|
||||
this.isDeleting = true;
|
||||
this.$emit('handleDeleteNote', this.note);
|
||||
|
||||
this.deleteNote(this.note)
|
||||
.then(() => {
|
||||
|
|
|
@ -138,6 +138,7 @@ export default {
|
|||
.then(() => {
|
||||
this.isLoading = false;
|
||||
this.setNotesFetchedState(true);
|
||||
eventHub.$emit('fetchedNotesData');
|
||||
})
|
||||
.then(() => this.$nextTick())
|
||||
.then(() => this.checkLocationHash())
|
||||
|
|
|
@ -43,14 +43,23 @@ export const fetchDiscussions = ({ commit }, path) =>
|
|||
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
|
||||
});
|
||||
|
||||
export const refetchDiscussionById = ({ commit }, { path, discussionId }) =>
|
||||
service
|
||||
.fetchDiscussions(path)
|
||||
.then(res => res.json())
|
||||
.then(discussions => {
|
||||
const selectedDiscussion = discussions.find(discussion => discussion.id === discussionId);
|
||||
if (selectedDiscussion) commit(types.UPDATE_DISCUSSION, selectedDiscussion);
|
||||
});
|
||||
export const refetchDiscussionById = ({ commit, state }, { path, discussionId }) =>
|
||||
new Promise(resolve => {
|
||||
service
|
||||
.fetchDiscussions(path)
|
||||
.then(res => res.json())
|
||||
.then(discussions => {
|
||||
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) =>
|
||||
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 methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote';
|
||||
|
||||
commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders
|
||||
$('.notes-form .flash-container').hide(); // hide previous flash notification
|
||||
commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders
|
||||
|
||||
if (hasQuickActions) {
|
||||
placeholderText = utils.stripQuickActions(placeholderText);
|
||||
}
|
||||
if (replyId) {
|
||||
if (hasQuickActions) {
|
||||
placeholderText = utils.stripQuickActions(placeholderText);
|
||||
}
|
||||
|
||||
if (placeholderText.length) {
|
||||
commit(types.SHOW_PLACEHOLDER_NOTE, {
|
||||
noteBody: placeholderText,
|
||||
replyId,
|
||||
});
|
||||
}
|
||||
if (placeholderText.length) {
|
||||
commit(types.SHOW_PLACEHOLDER_NOTE, {
|
||||
noteBody: placeholderText,
|
||||
replyId,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasQuickActions) {
|
||||
commit(types.SHOW_PLACEHOLDER_NOTE, {
|
||||
isSystemNote: true,
|
||||
noteBody: utils.getQuickActionText(note),
|
||||
replyId,
|
||||
});
|
||||
if (hasQuickActions) {
|
||||
commit(types.SHOW_PLACEHOLDER_NOTE, {
|
||||
isSystemNote: true,
|
||||
noteBody: utils.getQuickActionText(note),
|
||||
replyId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return dispatch(methodToDispatch, noteData).then(res => {
|
||||
|
@ -211,7 +222,9 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
|
|||
if (errors && errors.commands_only) {
|
||||
Flash(errors.commands_only, 'notice', noteData.flashContainer);
|
||||
}
|
||||
commit(types.REMOVE_PLACEHOLDER_NOTES);
|
||||
if (replyId) {
|
||||
commit(types.REMOVE_PLACEHOLDER_NOTES);
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import _ from 'underscore';
|
||||
import * as constants from '../constants';
|
||||
import { reduceDiscussionsToLineCodes } from './utils';
|
||||
import { collapseSystemNotes } from './collapse_utils';
|
||||
|
||||
export const discussions = state => collapseSystemNotes(state.discussions);
|
||||
|
@ -28,17 +29,8 @@ export const notesById = state =>
|
|||
return acc;
|
||||
}, {});
|
||||
|
||||
export const discussionsByLineCode = state =>
|
||||
state.discussions.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 discussionsStructuredByLineCode = state =>
|
||||
reduceDiscussionsToLineCodes(state.discussions);
|
||||
|
||||
export const noteableType = state => {
|
||||
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
|
||||
|
|
|
@ -54,13 +54,12 @@ export default {
|
|||
|
||||
[types.EXPAND_DISCUSSION](state, { discussionId }) {
|
||||
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
|
||||
|
||||
discussion.expanded = true;
|
||||
Object.assign(discussion, { expanded: true });
|
||||
},
|
||||
|
||||
[types.COLLAPSE_DISCUSSION](state, { discussionId }) {
|
||||
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
|
||||
discussion.expanded = false;
|
||||
Object.assign(discussion, { expanded: false });
|
||||
},
|
||||
|
||||
[types.REMOVE_PLACEHOLDER_NOTES](state) {
|
||||
|
@ -95,10 +94,15 @@ export default {
|
|||
[types.SET_USER_DATA](state, data) {
|
||||
Object.assign(state, { userData: data });
|
||||
},
|
||||
|
||||
[types.SET_INITIAL_DISCUSSIONS](state, discussionsData) {
|
||||
const discussions = [];
|
||||
|
||||
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.
|
||||
if (discussion.individual_note && discussion.notes.length > 1) {
|
||||
discussion.notes.forEach(n => {
|
||||
|
@ -168,8 +172,7 @@ export default {
|
|||
|
||||
[types.TOGGLE_DISCUSSION](state, { discussionId }) {
|
||||
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
|
||||
|
||||
discussion.expanded = !discussion.expanded;
|
||||
Object.assign(discussion, { expanded: !discussion.expanded });
|
||||
},
|
||||
|
||||
[types.UPDATE_NOTE](state, note) {
|
||||
|
@ -185,16 +188,12 @@ export default {
|
|||
|
||||
[types.UPDATE_DISCUSSION](state, noteData) {
|
||||
const note = noteData;
|
||||
let index = 0;
|
||||
|
||||
state.discussions.forEach((n, i) => {
|
||||
if (n.id === note.id) {
|
||||
index = i;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedDiscussion = state.discussions.find(disc => disc.id === note.id);
|
||||
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) {
|
||||
|
|
|
@ -2,13 +2,11 @@ import AjaxCache from '~/lib/utils/ajax_cache';
|
|||
|
||||
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
|
||||
|
||||
export const findNoteObjectById = (notes, id) =>
|
||||
notes.filter(n => n.id === id)[0];
|
||||
export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0];
|
||||
|
||||
export const getQuickActionText = note => {
|
||||
let text = 'Applying command';
|
||||
const quickActions =
|
||||
AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
|
||||
const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
|
||||
|
||||
const executedCommands = quickActions.filter(command => {
|
||||
const commandRegex = new RegExp(`/${command.name}`);
|
||||
|
@ -27,7 +25,18 @@ export const getQuickActionText = note => {
|
|||
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 stripQuickActions = note =>
|
||||
note.replace(REGEX_QUICK_ACTIONS, '').trim();
|
||||
export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
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 */
|
||||
|
||||
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 notificationsDropdown from '~/notifications_dropdown';
|
||||
import NotificationsForm from '~/notifications_form';
|
||||
import ProjectsList from '~/projects_list';
|
||||
import ShortcutsNavigation from '~/shortcuts_navigation';
|
||||
import initGroupsList from '~/groups';
|
||||
import GroupTabs from './group_tabs';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
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 NotificationsForm();
|
||||
notificationsDropdown();
|
||||
|
@ -17,6 +25,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
if (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 Project from './project';
|
||||
import ShortcutsNavigation from '../../shortcuts_navigation';
|
||||
|
@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
];
|
||||
|
||||
if (newClusterViews.indexOf(page) > -1) {
|
||||
gcpSignupOffer();
|
||||
initDismissableCallout('.gcp-signup-offer');
|
||||
initGkeDropdowns();
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,13 @@ export default class Project {
|
|||
.remove();
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,9 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
modalId() {
|
||||
return 'delete-wiki-modal';
|
||||
},
|
||||
message() {
|
||||
return s__('WikiPageConfirmDelete|Are you sure you want to delete this page?');
|
||||
},
|
||||
|
@ -47,31 +50,41 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-modal
|
||||
id="delete-wiki-modal"
|
||||
:header-title-text="title"
|
||||
:footer-primary-button-text="s__('WikiPageConfirmDelete|Delete page')"
|
||||
footer-primary-button-variant="danger"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
{{ message }}
|
||||
<form
|
||||
ref="form"
|
||||
:action="deleteWikiUrl"
|
||||
method="post"
|
||||
class="js-requires-input"
|
||||
<div class="d-inline-block">
|
||||
<button
|
||||
v-gl-modal="modalId"
|
||||
type="button"
|
||||
class="btn btn-danger"
|
||||
>
|
||||
<input
|
||||
ref="method"
|
||||
type="hidden"
|
||||
name="_method"
|
||||
value="delete"
|
||||
/>
|
||||
<input
|
||||
:value="csrfToken"
|
||||
type="hidden"
|
||||
name="authenticity_token"
|
||||
/>
|
||||
</form>
|
||||
</gl-modal>
|
||||
{{ __('Delete') }}
|
||||
</button>
|
||||
<gl-ui-modal
|
||||
:title="title"
|
||||
:ok-title="s__('WikiPageConfirmDelete|Delete page')"
|
||||
:modal-id="modalId"
|
||||
title-tag="h4"
|
||||
ok-variant="danger"
|
||||
@ok="onSubmit"
|
||||
>
|
||||
{{ message }}
|
||||
<form
|
||||
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>
|
||||
|
|
|
@ -14,15 +14,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
new ZenMode(); // 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);
|
||||
|
||||
const { deleteWikiUrl, pageTitle } = deleteWikiButton.dataset;
|
||||
const deleteWikiModalEl = document.getElementById('delete-wiki-modal');
|
||||
const deleteModal = new Vue({ // eslint-disable-line
|
||||
el: deleteWikiModalEl,
|
||||
const { deleteWikiUrl, pageTitle } = deleteWikiModalWrapperEl.dataset;
|
||||
|
||||
new Vue({ // eslint-disable-line no-new
|
||||
el: deleteWikiModalWrapperEl,
|
||||
data: {
|
||||
deleteWikiUrl: '',
|
||||
},
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue