merge master and resolve conflicts

This commit is contained in:
Martin Wortschack 2018-09-11 08:43:10 +02:00
commit a2f7936c74
765 changed files with 11477 additions and 6664 deletions

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -1 +1 @@
0.117.2
0.120.0

View File

@ -1 +1 @@
8.1.1
8.3.1

View File

@ -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

View File

@ -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)

View File

@ -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
View File

@ -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.

View File

@ -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) {

View File

@ -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);

View File

@ -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);

View File

@ -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'),
);
});
}

View File

@ -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) {

View File

@ -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);

View File

@ -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);
}
},
},
});

View File

@ -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.'),
);
}

View File

@ -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();

View File

@ -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>

View File

@ -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.') }}

View File

@ -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"

View File

@ -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>

View File

@ -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(() => {

View File

@ -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>

View File

@ -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]"

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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 }) => {

View File

@ -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

View File

@ -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';

View 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: [],
});
}
}
}
},
};

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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));
}

View File

@ -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);

View File

@ -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"

View File

@ -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"

View File

@ -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>

View File

@ -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"

View File

@ -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);
},
},
};

View File

@ -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.'),
};

View File

@ -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));
}
}

View File

@ -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,
},
});
},

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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"
/>

View File

@ -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"

View File

@ -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);
}
};

View File

@ -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;

View File

@ -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(),
},
});

View File

@ -1,4 +1,3 @@
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {

View File

@ -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

View File

@ -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 () => {};

View File

@ -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,
};
});

View File

@ -1,4 +1,3 @@
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {

View File

@ -1,4 +1,3 @@
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {

View File

@ -1,4 +1,3 @@
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
import { normalizeJob } from './utils';

View File

@ -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,

View File

@ -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,
});

View File

@ -1,5 +1,3 @@
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {

View File

@ -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;

View File

@ -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
*

View File

@ -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);

View File

@ -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 || '/';

View File

@ -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"

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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;
}

View File

@ -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>

View File

@ -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>

View File

@ -25,7 +25,7 @@ export default {
required: true,
},
noteId: {
type: Number,
type: String,
required: true,
},
canAwardEmoji: {

View File

@ -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');

View File

@ -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">

View File

@ -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

View File

@ -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(() => {

View File

@ -138,6 +138,7 @@ export default {
.then(() => {
this.isLoading = false;
this.setNotesFetchedState(true);
eventHub.$emit('fetchedNotesData');
})
.then(() => this.$nextTick())
.then(() => this.checkLocationHash())

View File

@ -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;
});

View File

@ -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;

View File

@ -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) {

View File

@ -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();

View File

@ -1,3 +1,5 @@
import initGroupsList from '~/groups';
document.addEventListener('DOMContentLoaded', initGroupsList);
document.addEventListener('DOMContentLoaded', () => {
initGroupsList();
});

View File

@ -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,
);
}
}

View File

@ -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();
});

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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>

View File

@ -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