Commit 6d4e044a authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'ce-to-ee-2018-09-08' into 'master'

CE upstream - 2018-09-08 09:21 UTC

Closes gitlab-runner#2992 and #2934

See merge request gitlab-org/gitlab-ee!7291
parents 03416324 670a50eb
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-67.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29" image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-69.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29"
.dedicated-runner: &dedicated-runner .dedicated-runner: &dedicated-runner
retry: 1 retry: 1
...@@ -888,7 +888,6 @@ gitlab:assets:compile: ...@@ -888,7 +888,6 @@ gitlab:assets:compile:
SETUP_DB: "false" SETUP_DB: "false"
SKIP_STORAGE_VALIDATION: "true" SKIP_STORAGE_VALIDATION: "true"
WEBPACK_REPORT: "true" WEBPACK_REPORT: "true"
NO_COMPRESSION: "true"
# we override the max_old_space_size to prevent OOM errors # we override the max_old_space_size to prevent OOM errors
NODE_OPTIONS: --max_old_space_size=3584 NODE_OPTIONS: --max_old_space_size=3584
script: script:
...@@ -902,6 +901,7 @@ gitlab:assets:compile: ...@@ -902,6 +901,7 @@ gitlab:assets:compile:
expire_in: 31d expire_in: 31d
paths: paths:
- webpack-report/ - webpack-report/
- public/assets/
karma: karma:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
......
...@@ -13,3 +13,5 @@ db/ @abrandl @NikolayS ...@@ -13,3 +13,5 @@ db/ @abrandl @NikolayS
# Feature specific owners # Feature specific owners
/ee/lib/gitlab/code_owners/ @reprazent /ee/lib/gitlab/code_owners/ @reprazent
/ee/lib/ee/gitlab/auth/ldap/ @dblessing @mkozono
/lib/gitlab/auth/ldap/ @dblessing @mkozono
...@@ -4,4 +4,5 @@ danger.import_dangerfile(path: 'danger/changelog') ...@@ -4,4 +4,5 @@ danger.import_dangerfile(path: 'danger/changelog')
danger.import_dangerfile(path: 'danger/specs') danger.import_dangerfile(path: 'danger/specs')
danger.import_dangerfile(path: 'danger/gemfile') danger.import_dangerfile(path: 'danger/gemfile')
danger.import_dangerfile(path: 'danger/database') danger.import_dangerfile(path: 'danger/database')
danger.import_dangerfile(path: 'danger/documentation')
danger.import_dangerfile(path: 'danger/frozen_string') danger.import_dangerfile(path: 'danger/frozen_string')
...@@ -232,7 +232,7 @@ GEM ...@@ -232,7 +232,7 @@ GEM
fast_blank (1.0.0) fast_blank (1.0.0)
fast_gettext (1.6.0) fast_gettext (1.6.0)
ffaker (2.4.0) ffaker (2.4.0)
ffi (1.9.18) ffi (1.9.25)
flipper (0.13.0) flipper (0.13.0)
flipper-active_record (0.13.0) flipper-active_record (0.13.0)
activerecord (>= 3.2, < 6) activerecord (>= 3.2, < 6)
......
...@@ -235,7 +235,7 @@ GEM ...@@ -235,7 +235,7 @@ GEM
fast_blank (1.0.0) fast_blank (1.0.0)
fast_gettext (1.6.0) fast_gettext (1.6.0)
ffaker (2.4.0) ffaker (2.4.0)
ffi (1.9.18) ffi (1.9.25)
flipper (0.13.0) flipper (0.13.0)
flipper-active_record (0.13.0) flipper-active_record (0.13.0)
activerecord (>= 3.2, < 6) activerecord (>= 3.2, < 6)
......
import Vue from 'vue'; import Vue from 'vue';
import progressBar from '@gitlab-org/gitlab-ui/dist/base/progress_bar'; import progressBar from '@gitlab-org/gitlab-ui/dist/components/base/progress_bar';
import modal from '@gitlab-org/gitlab-ui/dist/components/base/modal';
import dModal from '@gitlab-org/gitlab-ui/dist/directives/modal';
Vue.component('gl-progress-bar', progressBar); Vue.component('gl-progress-bar', progressBar);
Vue.component('gl-ui-modal', modal);
Vue.directive('gl-modal', dModal);
/* eslint-disable object-shorthand, func-names, comma-dangle, no-else-return, quotes */ /* eslint-disable object-shorthand, func-names, no-else-return */
/* global CommentsStore */ /* global CommentsStore */
/* global ResolveService */ /* global ResolveService */
...@@ -25,44 +25,44 @@ const ResolveDiscussionBtn = Vue.extend({ ...@@ -25,44 +25,44 @@ const ResolveDiscussionBtn = Vue.extend({
}; };
}, },
computed: { computed: {
showButton: function () { showButton: function() {
if (this.discussion) { if (this.discussion) {
return this.discussion.isResolvable(); return this.discussion.isResolvable();
} else { } else {
return false; return false;
} }
}, },
isDiscussionResolved: function () { isDiscussionResolved: function() {
if (this.discussion) { if (this.discussion) {
return this.discussion.isResolved(); return this.discussion.isResolved();
} else { } else {
return false; return false;
} }
}, },
buttonText: function () { buttonText: function() {
if (this.isDiscussionResolved) { if (this.isDiscussionResolved) {
return "Unresolve discussion"; return 'Unresolve discussion';
} else { } else {
return "Resolve discussion"; return 'Resolve discussion';
} }
}, },
loading: function () { loading: function() {
if (this.discussion) { if (this.discussion) {
return this.discussion.loading; return this.discussion.loading;
} else { } else {
return false; return false;
} }
} },
}, },
created: function () { created: function() {
CommentsStore.createDiscussion(this.discussionId, this.canResolve); CommentsStore.createDiscussion(this.discussionId, this.canResolve);
this.discussion = CommentsStore.state[this.discussionId]; this.discussion = CommentsStore.state[this.discussionId];
}, },
methods: { methods: {
resolve: function () { resolve: function() {
ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId); ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
} },
}, },
}); });
......
...@@ -8,9 +8,7 @@ window.gl = window.gl || {}; ...@@ -8,9 +8,7 @@ window.gl = window.gl || {};
class ResolveServiceClass { class ResolveServiceClass {
constructor(root) { constructor(root) {
this.noteResource = Vue.resource( this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`);
`${root}/notes{/noteId}/resolve?html=true`,
);
this.discussionResource = Vue.resource( this.discussionResource = Vue.resource(
`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`, `${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`,
); );
...@@ -51,10 +49,7 @@ class ResolveServiceClass { ...@@ -51,10 +49,7 @@ class ResolveServiceClass {
discussion.updateHeadline(data); discussion.updateHeadline(data);
}) })
.catch( .catch(
() => () => new Flash('An error occurred when trying to resolve a discussion. Please try again.'),
new Flash(
'An error occurred when trying to resolve a discussion. Please try again.',
),
); );
} }
......
...@@ -59,7 +59,7 @@ export default { ...@@ -59,7 +59,7 @@ export default {
emailPatchPath: state => state.diffs.emailPatchPath, emailPatchPath: state => state.diffs.emailPatchPath,
}), }),
...mapGetters('diffs', ['isParallelView']), ...mapGetters('diffs', ['isParallelView']),
...mapGetters(['isNotesFetched']), ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
targetBranch() { targetBranch() {
return { return {
branchName: this.targetBranchName, branchName: this.targetBranchName,
...@@ -112,13 +112,26 @@ export default { ...@@ -112,13 +112,26 @@ export default {
}, },
created() { created() {
this.adjustView(); this.adjustView();
eventHub.$once('fetchedNotesData', this.setDiscussions);
}, },
methods: { methods: {
...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles', 'startRenderDiffsQueue']), ...mapActions('diffs', [
'setBaseConfig',
'fetchDiffFiles',
'startRenderDiffsQueue',
'assignDiscussionsToDiff',
]),
fetchData() { fetchData() {
this.fetchDiffFiles() this.fetchDiffFiles()
.then(() => { .then(() => {
requestIdleCallback(this.startRenderDiffsQueue, { timeout: 1000 }); requestIdleCallback(
() => {
this.setDiscussions();
this.startRenderDiffsQueue();
},
{ timeout: 1000 },
);
}) })
.catch(() => { .catch(() => {
createFlash(__('Something went wrong on our end. Please try again!')); createFlash(__('Something went wrong on our end. Please try again!'));
...@@ -128,6 +141,16 @@ export default { ...@@ -128,6 +141,16 @@ export default {
eventHub.$emit('fetchNotesData'); eventHub.$emit('fetchNotesData');
} }
}, },
setDiscussions() {
if (this.isNotesFetched) {
requestIdleCallback(
() => {
this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode);
},
{ timeout: 1000 },
);
}
},
adjustView() { adjustView() {
if (this.shouldShow && this.isParallelView) { if (this.shouldShow && this.isParallelView) {
window.mrTabs.expandViewContainer(); window.mrTabs.expandViewContainer();
......
<script> <script>
import { mapActions } from 'vuex';
import noteableDiscussion from '../../notes/components/noteable_discussion.vue'; import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
export default { export default {
...@@ -11,6 +12,14 @@ export default { ...@@ -11,6 +12,14 @@ export default {
required: true, required: true,
}, },
}, },
methods: {
...mapActions('diffs', ['removeDiscussionsFromDiff']),
deleteNoteHandler(discussion) {
if (discussion.notes.length <= 1) {
this.removeDiscussionsFromDiff(discussion);
}
},
},
}; };
</script> </script>
...@@ -31,6 +40,7 @@ export default { ...@@ -31,6 +40,7 @@ export default {
:render-diff-file="false" :render-diff-file="false"
:always-expanded="true" :always-expanded="true"
:discussions-by-diff-order="true" :discussions-by-diff-order="true"
@noteDeleted="deleteNoteHandler"
/> />
</ul> </ul>
</div> </div>
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
...@@ -30,6 +30,7 @@ export default { ...@@ -30,6 +30,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
isCollapsed() { isCollapsed() {
return this.file.collapsed || false; return this.file.collapsed || false;
}, },
...@@ -44,23 +45,23 @@ export default { ...@@ -44,23 +45,23 @@ export default {
); );
}, },
showExpandMessage() { showExpandMessage() {
return this.isCollapsed && !this.isLoadingCollapsedDiff && !this.file.tooLarge; return (
!this.isCollapsed &&
!this.file.highlightedDiffLines &&
!this.isLoadingCollapsedDiff &&
!this.file.tooLarge &&
this.file.text
);
}, },
showLoadingIcon() { showLoadingIcon() {
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed); return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
}, },
}, },
methods: { methods: {
...mapActions('diffs', ['loadCollapsedDiff']), ...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff']),
handleToggle() { handleToggle() {
const { collapsed, highlightedDiffLines, parallelDiffLines } = this.file; const { highlightedDiffLines, parallelDiffLines } = this.file;
if (!highlightedDiffLines && parallelDiffLines !== undefined && !parallelDiffLines.length) {
if (
collapsed &&
!highlightedDiffLines &&
parallelDiffLines !== undefined &&
!parallelDiffLines.length
) {
this.handleLoadCollapsedDiff(); this.handleLoadCollapsedDiff();
} else { } else {
this.file.collapsed = !this.file.collapsed; this.file.collapsed = !this.file.collapsed;
...@@ -76,6 +77,14 @@ export default { ...@@ -76,6 +77,14 @@ export default {
this.file.collapsed = false; this.file.collapsed = false;
this.file.renderIt = true; this.file.renderIt = true;
}) })
.then(() => {
requestIdleCallback(
() => {
this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode);
},
{ timeout: 1000 },
);
})
.catch(() => { .catch(() => {
this.isLoadingCollapsedDiff = false; this.isLoadingCollapsedDiff = false;
createFlash(__('Something went wrong on our end. Please try again!')); createFlash(__('Something went wrong on our end. Please try again!'));
...@@ -136,11 +145,11 @@ export default { ...@@ -136,11 +145,11 @@ export default {
:diff-file="file" :diff-file="file"
/> />
<loading-icon <loading-icon
v-else-if="showLoadingIcon" v-if="showLoadingIcon"
class="diff-content loading" class="diff-content loading"
/> />
<div <div
v-if="showExpandMessage" v-else-if="showExpandMessage"
class="nothing-here-block diff-collapsed" class="nothing-here-block diff-collapsed"
> >
{{ __('This diff is collapsed.') }} {{ __('This diff is collapsed.') }}
......
...@@ -13,6 +13,10 @@ export default { ...@@ -13,6 +13,10 @@ export default {
Icon, Icon,
}, },
props: { props: {
line: {
type: Object,
required: true,
},
fileHash: { fileHash: {
type: String, type: String,
required: true, required: true,
...@@ -21,31 +25,16 @@ export default { ...@@ -21,31 +25,16 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
lineType: {
type: String,
required: false,
default: '',
},
lineNumber: { lineNumber: {
type: Number, type: Number,
required: false, required: false,
default: 0, default: 0,
}, },
lineCode: {
type: String,
required: false,
default: '',
},
linePosition: { linePosition: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
metaData: {
type: Object,
required: false,
default: () => ({}),
},
showCommentButton: { showCommentButton: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -76,11 +65,6 @@ export default { ...@@ -76,11 +65,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
discussions: {
type: Array,
required: false,
default: () => [],
},
}, },
computed: { computed: {
...mapState({ ...mapState({
...@@ -89,7 +73,7 @@ export default { ...@@ -89,7 +73,7 @@ export default {
}), }),
...mapGetters(['isLoggedIn']), ...mapGetters(['isLoggedIn']),
lineHref() { lineHref() {
return this.lineCode ? `#${this.lineCode}` : '#'; return `#${this.line.lineCode || ''}`;
}, },
shouldShowCommentButton() { shouldShowCommentButton() {
return ( return (
...@@ -103,20 +87,19 @@ export default { ...@@ -103,20 +87,19 @@ export default {
); );
}, },
hasDiscussions() { hasDiscussions() {
return this.discussions.length > 0; return this.line.discussions && this.line.discussions.length > 0;
}, },
shouldShowAvatarsOnGutter() { shouldShowAvatarsOnGutter() {
if (!this.lineType && this.linePosition === LINE_POSITION_RIGHT) { if (!this.line.type && this.linePosition === LINE_POSITION_RIGHT) {
return false; return false;
} }
return this.showCommentButton && this.hasDiscussions; return this.showCommentButton && this.hasDiscussions;
}, },
}, },
methods: { methods: {
...mapActions('diffs', ['loadMoreLines', 'showCommentForm']), ...mapActions('diffs', ['loadMoreLines', 'showCommentForm']),
handleCommentButton() { handleCommentButton() {
this.showCommentForm({ lineCode: this.lineCode }); this.showCommentForm({ lineCode: this.line.lineCode });
}, },
handleLoadMoreLines() { handleLoadMoreLines() {
if (this.isRequesting) { if (this.isRequesting) {
...@@ -125,8 +108,8 @@ export default { ...@@ -125,8 +108,8 @@ export default {
this.isRequesting = true; this.isRequesting = true;
const endpoint = this.contextLinesPath; const endpoint = this.contextLinesPath;
const oldLineNumber = this.metaData.oldPos || 0; const oldLineNumber = this.line.metaData.oldPos || 0;
const newLineNumber = this.metaData.newPos || 0; const newLineNumber = this.line.metaData.newPos || 0;
const offset = newLineNumber - oldLineNumber; const offset = newLineNumber - oldLineNumber;
const bottom = this.isBottom; const bottom = this.isBottom;
const { fileHash } = this; const { fileHash } = this;
...@@ -201,7 +184,7 @@ export default { ...@@ -201,7 +184,7 @@ export default {
</a> </a>
<diff-gutter-avatars <diff-gutter-avatars
v-if="shouldShowAvatarsOnGutter" v-if="shouldShowAvatarsOnGutter"
:discussions="discussions" :discussions="line.discussions"
/> />
</template> </template>
</div> </div>
......
...@@ -6,6 +6,7 @@ import noteForm from '../../notes/components/note_form.vue'; ...@@ -6,6 +6,7 @@ import noteForm from '../../notes/components/note_form.vue';
import { getNoteFormData } from '../store/utils'; import { getNoteFormData } from '../store/utils';
import autosave from '../../notes/mixins/autosave'; import autosave from '../../notes/mixins/autosave';
import { DIFF_NOTE_TYPE } from '../constants'; import { DIFF_NOTE_TYPE } from '../constants';
import { reduceDiscussionsToLineCodes } from '../../notes/stores/utils';
export default { export default {
components: { components: {
...@@ -52,7 +53,7 @@ export default { ...@@ -52,7 +53,7 @@ export default {
} }
}, },
methods: { methods: {
...mapActions('diffs', ['cancelCommentForm']), ...mapActions('diffs', ['cancelCommentForm', 'assignDiscussionsToDiff']),
...mapActions(['saveNote', 'refetchDiscussionById']), ...mapActions(['saveNote', 'refetchDiscussionById']),
handleCancelCommentForm(shouldConfirm, isDirty) { handleCancelCommentForm(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) { if (shouldConfirm && isDirty) {
...@@ -88,7 +89,10 @@ export default { ...@@ -88,7 +89,10 @@ export default {
const endpoint = this.getNotesDataByProp('discussionsPath'); const endpoint = this.getNotesDataByProp('discussionsPath');
this.refetchDiscussionById({ path: endpoint, discussionId: result.discussion_id }) this.refetchDiscussionById({ path: endpoint, discussionId: result.discussion_id })
.then(() => { .then(selectedDiscussion => {
const lineCodeDiscussions = reduceDiscussionsToLineCodes([selectedDiscussion]);
this.assignDiscussionsToDiff(lineCodeDiscussions);
this.handleCancelCommentForm(); this.handleCancelCommentForm();
}) })
.catch(() => { .catch(() => {
......
...@@ -11,8 +11,6 @@ import { ...@@ -11,8 +11,6 @@ import {
LINE_HOVER_CLASS_NAME, LINE_HOVER_CLASS_NAME,
LINE_UNFOLD_CLASS_NAME, LINE_UNFOLD_CLASS_NAME,
INLINE_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE,
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
} from '../constants'; } from '../constants';
export default { export default {
...@@ -67,42 +65,24 @@ export default { ...@@ -67,42 +65,24 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
discussions: {
type: Array,
required: false,
default: () => [],
},
}, },
computed: { computed: {
...mapGetters(['isLoggedIn']), ...mapGetters(['isLoggedIn']),
normalizedLine() {
let normalizedLine;
if (this.diffViewType === INLINE_DIFF_VIEW_TYPE) {
normalizedLine = this.line;
} else if (this.linePosition === LINE_POSITION_LEFT) {
normalizedLine = this.line.left;
} else if (this.linePosition === LINE_POSITION_RIGHT) {
normalizedLine = this.line.right;
}
return normalizedLine;
},
isMatchLine() { isMatchLine() {
return this.normalizedLine.type === MATCH_LINE_TYPE; return this.line.type === MATCH_LINE_TYPE;
}, },
isContextLine() { isContextLine() {
return this.normalizedLine.type === CONTEXT_LINE_TYPE; return this.line.type === CONTEXT_LINE_TYPE;
}, },
isMetaLine() { isMetaLine() {
const { type } = this.normalizedLine; const { type } = this.line;
return ( return (
type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE
); );
}, },
classNameMap() { classNameMap() {
const { type } = this.normalizedLine; const { type } = this.line;
return { return {
[type]: type, [type]: type,
...@@ -116,9 +96,9 @@ export default { ...@@ -116,9 +96,9 @@ export default {
}; };
}, },
lineNumber() { lineNumber() {
const { lineType, normalizedLine } = this; const { lineType } = this;
return lineType === OLD_LINE_TYPE ? normalizedLine.oldLine : normalizedLine.newLine; return lineType === OLD_LINE_TYPE ? this.line.oldLine : this.line.newLine;
}, },
}, },
}; };
...@@ -129,20 +109,17 @@ export default { ...@@ -129,20 +109,17 @@ export default {
:class="classNameMap" :class="classNameMap"
> >
<diff-line-gutter-content <diff-line-gutter-content
:line="line"
:file-hash="fileHash" :file-hash="fileHash"
:context-lines-path="contextLinesPath" :context-lines-path="contextLinesPath"
:line-type="normalizedLine.type"
:line-code="normalizedLine.lineCode"
:line-position="linePosition" :line-position="linePosition"
:line-number="lineNumber" :line-number="lineNumber"
:meta-data="normalizedLine.metaData"
:show-comment-button="showCommentButton" :show-comment-button="showCommentButton"
:is-hover="isHover" :is-hover="isHover"
:is-bottom="isBottom" :is-bottom="isBottom"
:is-match-line="isMatchLine" :is-match-line="isMatchLine"
:is-context-line="isContentLine" :is-context-line="isContentLine"
:is-meta-line="isMetaLine" :is-meta-line="isMetaLine"
:discussions="discussions"
/> />
</td> </td>
</template> </template>
...@@ -21,18 +21,13 @@ export default { ...@@ -21,18 +21,13 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
discussions: {
type: Array,
required: false,
default: () => [],
},
}, },
computed: { computed: {
...mapState({ ...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms, diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}), }),
className() { className() {
return this.discussions.length ? '' : 'js-temp-notes-holder'; return this.line.discussions.length ? '' : 'js-temp-notes-holder';
}, },
}, },
}; };
...@@ -49,8 +44,8 @@ export default { ...@@ -49,8 +44,8 @@ export default {
> >
<div class="content"> <div class="content">
<diff-discussions <diff-discussions
v-if="discussions.length" v-if="line.discussions.length"
:discussions="discussions" :discussions="line.discussions"
/> />
<diff-line-note-form <diff-line-note-form
v-if="diffLineCommentForms[line.lineCode]" v-if="diffLineCommentForms[line.lineCode]"
......
...@@ -33,11 +33,6 @@ export default { ...@@ -33,11 +33,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
discussions: {
type: Array,
required: false,
default: () => [],
},
}, },
data() { data() {
return { return {
...@@ -94,7 +89,6 @@ export default { ...@@ -94,7 +89,6 @@ export default {
:is-bottom="isBottom" :is-bottom="isBottom"
:is-hover="isHover" :is-hover="isHover"
:show-comment-button="true" :show-comment-button="true"
:discussions="discussions"
class="diff-line-num old_line" class="diff-line-num old_line"
/> />
<diff-table-cell <diff-table-cell
...@@ -104,7 +98,6 @@ export default { ...@@ -104,7 +98,6 @@ export default {
:line-type="newLineType" :line-type="newLineType"
:is-bottom="isBottom" :is-bottom="isBottom"
:is-hover="isHover" :is-hover="isHover"
:discussions="discussions"
class="diff-line-num new_line" class="diff-line-num new_line"
/> />
<td <td
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
import { mapGetters, mapState } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import inlineDiffTableRow from './inline_diff_table_row.vue'; import inlineDiffTableRow from './inline_diff_table_row.vue';
import inlineDiffCommentRow from './inline_diff_comment_row.vue'; import inlineDiffCommentRow from './inline_diff_comment_row.vue';
import { trimFirstCharOfLineContent } from '../store/utils';
export default { export default {
components: { components: {
...@@ -20,29 +19,17 @@ export default { ...@@ -20,29 +19,17 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters('diffs', [ ...mapGetters('diffs', ['commitId', 'shouldRenderInlineCommentRow']),
'commitId',
'shouldRenderInlineCommentRow',
'singleDiscussionByLineCode',
]),
...mapState({ ...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms, diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}), }),
normalizedDiffLines() {
return this.diffLines.map(line => (line.richText ? trimFirstCharOfLineContent(line) : line));
},
diffLinesLength() { diffLinesLength() {
return this.normalizedDiffLines.length; return this.diffLines.length;
}, },
userColorScheme() { userColorScheme() {
return window.gon.user_color_scheme; return window.gon.user_color_scheme;
}, },
}, },
methods: {
discussionsList(line) {
return line.lineCode !== undefined ? this.singleDiscussionByLineCode(line.lineCode) : [];
},
},
}; };
</script> </script>
...@@ -53,7 +40,7 @@ export default { ...@@ -53,7 +40,7 @@ export default {
class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view"> class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view">
<tbody> <tbody>
<template <template
v-for="(line, index) in normalizedDiffLines" v-for="(line, index) in diffLines"
> >
<inline-diff-table-row <inline-diff-table-row
:file-hash="diffFile.fileHash" :file-hash="diffFile.fileHash"
...@@ -61,7 +48,6 @@ export default { ...@@ -61,7 +48,6 @@ export default {
:line="line" :line="line"
:is-bottom="index + 1 === diffLinesLength" :is-bottom="index + 1 === diffLinesLength"
:key="line.lineCode" :key="line.lineCode"
:discussions="discussionsList(line)"
/> />
<inline-diff-comment-row <inline-diff-comment-row
v-if="shouldRenderInlineCommentRow(line)" v-if="shouldRenderInlineCommentRow(line)"
...@@ -69,7 +55,6 @@ export default { ...@@ -69,7 +55,6 @@ export default {
:line="line" :line="line"
:line-index="index" :line-index="index"
:key="index" :key="index"
:discussions="discussionsList(line)"
/> />
</template> </template>
</tbody> </tbody>
......
...@@ -21,51 +21,49 @@ export default { ...@@ -21,51 +21,49 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
leftDiscussions: {
type: Array,
required: false,
default: () => [],
},
rightDiscussions: {
type: Array,
required: false,
default: () => [],
},
}, },
computed: { computed: {
...mapState({ ...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms, diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}), }),
leftLineCode() { leftLineCode() {
return this.line.left.lineCode; return this.line.left && this.line.left.lineCode;
}, },
rightLineCode() { rightLineCode() {
return this.line.right.lineCode; return this.line.right && this.line.right.lineCode;
}, },
hasExpandedDiscussionOnLeft() { hasExpandedDiscussionOnLeft() {
const discussions = this.leftDiscussions; return this.line.left && this.line.left.discussions
? this.line.left.discussions.every(discussion => discussion.expanded)
return discussions ? discussions.every(discussion => discussion.expanded) : false; : false;
}, },
hasExpandedDiscussionOnRight() { hasExpandedDiscussionOnRight() {
const discussions = this.rightDiscussions; return this.line.right && this.line.right.discussions
? this.line.right.discussions.every(discussion => discussion.expanded)
return discussions ? discussions.every(discussion => discussion.expanded) : false; : false;
}, },
hasAnyExpandedDiscussion() { hasAnyExpandedDiscussion() {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight; return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
}, },
shouldRenderDiscussionsOnLeft() { shouldRenderDiscussionsOnLeft() {
return this.leftDiscussions && this.hasExpandedDiscussionOnLeft; return this.line.left && this.line.left.discussions && this.hasExpandedDiscussionOnLeft;
}, },
shouldRenderDiscussionsOnRight() { shouldRenderDiscussionsOnRight() {
return this.rightDiscussions && this.hasExpandedDiscussionOnRight && this.line.right.type; return (
this.line.right &&
this.line.right.discussions &&
this.hasExpandedDiscussionOnRight &&
this.line.right.type
);
}, },
showRightSideCommentForm() { showRightSideCommentForm() {
return this.line.right.type && this.diffLineCommentForms[this.rightLineCode]; return (
this.line.right && this.line.right.type && this.diffLineCommentForms[this.rightLineCode]
);
}, },
className() { className() {
return this.leftDiscussions.length > 0 || this.rightDiscussions.length > 0 return (this.left && this.line.left.discussions.length > 0) ||
(this.right && this.line.right.discussions.length > 0)
? '' ? ''
: 'js-temp-notes-holder'; : 'js-temp-notes-holder';
}, },
...@@ -85,8 +83,8 @@ export default { ...@@ -85,8 +83,8 @@ export default {
class="content" class="content"
> >
<diff-discussions <diff-discussions
v-if="leftDiscussions.length" v-if="line.left.discussions.length"
:discussions="leftDiscussions" :discussions="line.left.discussions"
/> />
</div> </div>
<diff-line-note-form <diff-line-note-form
...@@ -104,8 +102,8 @@ export default { ...@@ -104,8 +102,8 @@ export default {
class="content" class="content"
> >
<diff-discussions <diff-discussions
v-if="rightDiscussions.length" v-if="line.right.discussions.length"
:discussions="rightDiscussions" :discussions="line.right.discussions"
/> />
</div> </div>
<diff-line-note-form <diff-line-note-form
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { mapGetters } from 'vuex';
import DiffTableCell from './diff_table_cell.vue'; import DiffTableCell from './diff_table_cell.vue';
import { import {
NEW_LINE_TYPE, NEW_LINE_TYPE,
...@@ -10,8 +9,7 @@ import { ...@@ -10,8 +9,7 @@ import {
OLD_NO_NEW_LINE_TYPE, OLD_NO_NEW_LINE_TYPE,
PARALLEL_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE,
NEW_NO_NEW_LINE_TYPE, NEW_NO_NEW_LINE_TYPE,
LINE_POSITION_LEFT, EMPTY_CELL_TYPE,
LINE_POSITION_RIGHT,
} from '../constants'; } from '../constants';
export default { export default {
...@@ -36,16 +34,6 @@ export default { ...@@ -36,16 +34,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
leftDiscussions: {
type: Array,
required: false,
default: () => [],
},
rightDiscussions: {
type: Array,
required: false,
default: () => [],
},
}, },
data() { data() {
return { return {
...@@ -54,29 +42,26 @@ export default { ...@@ -54,29 +42,26 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters('diffs', ['isParallelView']),
isContextLine() { isContextLine() {
return this.line.left.type === CONTEXT_LINE_TYPE; return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE;
}, },
classNameMap() { classNameMap() {
return { return {
[CONTEXT_LINE_CLASS_NAME]: this.isContextLine, [CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
[PARALLEL_DIFF_VIEW_TYPE]: this.isParallelView, [PARALLEL_DIFF_VIEW_TYPE]: true,
}; };
}, },
parallelViewLeftLineType() { parallelViewLeftLineType() {
if (this.line.right.type === NEW_NO_NEW_LINE_TYPE) { if (this.line.right && this.line.right.type === NEW_NO_NEW_LINE_TYPE) {
return OLD_NO_NEW_LINE_TYPE; return OLD_NO_NEW_LINE_TYPE;
} }
return this.line.left.type; return this.line.left ? this.line.left.type : EMPTY_CELL_TYPE;
}, },
}, },
created() { created() {
this.newLineType = NEW_LINE_TYPE; this.newLineType = NEW_LINE_TYPE;
this.oldLineType = OLD_LINE_TYPE; this.oldLineType = OLD_LINE_TYPE;
this.linePositionLeft = LINE_POSITION_LEFT;
this.linePositionRight = LINE_POSITION_RIGHT;
this.parallelDiffViewType = PARALLEL_DIFF_VIEW_TYPE; this.parallelDiffViewType = PARALLEL_DIFF_VIEW_TYPE;
}, },
methods: { methods: {
...@@ -116,47 +101,57 @@ export default { ...@@ -116,47 +101,57 @@ export default {
@mouseover="handleMouseMove" @mouseover="handleMouseMove"
@mouseout="handleMouseMove" @mouseout="handleMouseMove"
> >
<diff-table-cell <template v-if="line.left">
:file-hash="fileHash" <diff-table-cell
:context-lines-path="contextLinesPath" :file-hash="fileHash"
:line="line" :context-lines-path="contextLinesPath"
:line-type="oldLineType" :line="line.left"
:line-position="linePositionLeft" :line-type="oldLineType"
:is-bottom="isBottom" :is-bottom="isBottom"
:is-hover="isLeftHover" :is-hover="isLeftHover"
:show-comment-button="true" :show-comment-button="true"
:diff-view-type="parallelDiffViewType" :diff-view-type="parallelDiffViewType"
:discussions="leftDiscussions" line-position="left"
class="diff-line-num old_line" class="diff-line-num old_line"
/> />
<td <td
:id="line.left.lineCode" :id="line.left.lineCode"
:class="parallelViewLeftLineType" :class="parallelViewLeftLineType"
class="line_content parallel left-side" class="line_content parallel left-side"
@mousedown.native="handleParallelLineMouseDown" @mousedown.native="handleParallelLineMouseDown"
v-html="line.left.richText" v-html="line.left.richText"
> >
</td> </td>
<diff-table-cell </template>
:file-hash="fileHash" <template v-else>
:context-lines-path="contextLinesPath" <td class="diff-line-num old_line empty-cell"></td>
:line="line" <td class="line_content parallel left-side empty-cell"></td>
:line-type="newLineType" </template>
:line-position="linePositionRight" <template v-if="line.right">
:is-bottom="isBottom" <diff-table-cell
:is-hover="isRightHover" :file-hash="fileHash"
:show-comment-button="true" :context-lines-path="contextLinesPath"
:diff-view-type="parallelDiffViewType" :line="line.right"
:discussions="rightDiscussions" :line-type="newLineType"
class="diff-line-num new_line" :is-bottom="isBottom"
/> :is-hover="isRightHover"
<td :show-comment-button="true"
:id="line.right.lineCode" :diff-view-type="parallelDiffViewType"
:class="line.right.type" line-position="right"
class="line_content parallel right-side" class="diff-line-num new_line"
@mousedown.native="handleParallelLineMouseDown" />
v-html="line.right.richText" <td
> :id="line.right.lineCode"
</td> :class="line.right.type"
class="line_content parallel right-side"
@mousedown.native="handleParallelLineMouseDown"
v-html="line.right.richText"
>
</td>
</template>
<template v-else>
<td class="diff-line-num old_line empty-cell"></td>
<td class="line_content parallel right-side empty-cell"></td>
</template>
</tr> </tr>
</template> </template>
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import parallelDiffTableRow from './parallel_diff_table_row.vue'; import parallelDiffTableRow from './parallel_diff_table_row.vue';
import parallelDiffCommentRow from './parallel_diff_comment_row.vue'; import parallelDiffCommentRow from './parallel_diff_comment_row.vue';
import { EMPTY_CELL_TYPE } from '../constants';
import { trimFirstCharOfLineContent } from '../store/utils';
export default { export default {
components: { components: {
...@@ -21,46 +19,17 @@ export default { ...@@ -21,46 +19,17 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters('diffs', [ ...mapGetters('diffs', ['commitId', 'shouldRenderParallelCommentRow']),
'commitId',
'singleDiscussionByLineCode',
'shouldRenderParallelCommentRow',
]),
...mapState({ ...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms, diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}), }),
parallelDiffLines() {
return this.diffLines.map(line => {
const parallelLine = Object.assign({}, line);
if (line.left) {
parallelLine.left = trimFirstCharOfLineContent(line.left);
} else {
parallelLine.left = { type: EMPTY_CELL_TYPE };
}
if (line.right) {
parallelLine.right = trimFirstCharOfLineContent(line.right);
} else {
parallelLine.right = { type: EMPTY_CELL_TYPE };
}
return parallelLine;
});
},
diffLinesLength() { diffLinesLength() {
return this.parallelDiffLines.length; return this.diffLines.length;
}, },
userColorScheme() { userColorScheme() {
return window.gon.user_color_scheme; return window.gon.user_color_scheme;
}, },
}, },
methods: {
discussionsByLine(line, leftOrRight) {
return line[leftOrRight] && line[leftOrRight].lineCode !== undefined ?
this.singleDiscussionByLineCode(line[leftOrRight].lineCode) : [];
},
},
}; };
</script> </script>
...@@ -73,7 +42,7 @@ export default { ...@@ -73,7 +42,7 @@ export default {
<table> <table>
<tbody> <tbody>
<template <template
v-for="(line, index) in parallelDiffLines" v-for="(line, index) in diffLines"
> >
<parallel-diff-table-row <parallel-diff-table-row
:file-hash="diffFile.fileHash" :file-hash="diffFile.fileHash"
...@@ -81,8 +50,6 @@ export default { ...@@ -81,8 +50,6 @@ export default {
:line="line" :line="line"
:is-bottom="index + 1 === diffLinesLength" :is-bottom="index + 1 === diffLinesLength"
:key="index" :key="index"
:left-discussions="discussionsByLine(line, 'left')"
:right-discussions="discussionsByLine(line, 'right')"
/> />
<parallel-diff-comment-row <parallel-diff-comment-row
v-if="shouldRenderParallelCommentRow(line)" v-if="shouldRenderParallelCommentRow(line)"
...@@ -90,8 +57,6 @@ export default { ...@@ -90,8 +57,6 @@ export default {
:line="line" :line="line"
:diff-file-hash="diffFile.fileHash" :diff-file-hash="diffFile.fileHash"
:line-index="index" :line-index="index"
:left-discussions="discussionsByLine(line, 'left')"
:right-discussions="discussionsByLine(line, 'right')"
/> />
</template> </template>
</tbody> </tbody>
......
...@@ -3,6 +3,7 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -3,6 +3,7 @@ import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils'; import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mergeUrlParams } from '~/lib/utils/url_utility';
import { getDiffPositionByLineCode } from './utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { import {
PARALLEL_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE,
...@@ -29,25 +30,53 @@ export const fetchDiffFiles = ({ state, commit }) => { ...@@ -29,25 +30,53 @@ export const fetchDiffFiles = ({ state, commit }) => {
.then(handleLocationHash); .then(handleLocationHash);
}; };
export const startRenderDiffsQueue = ({ state, commit }) => { // This is adding line discussions to the actual lines in the diff tree
const checkItem = () => { // once for parallel and once for inline mode
const nextFile = state.diffFiles.find( export const assignDiscussionsToDiff = ({ state, commit }, allLineDiscussions) => {
file => !file.renderIt && (!file.collapsed || !file.text), const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
);
if (nextFile) { Object.values(allLineDiscussions).forEach(discussions => {
requestAnimationFrame(() => { if (discussions.length > 0) {
commit(types.RENDER_FILE, nextFile); const { fileHash } = discussions[0];
commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
fileHash,
discussions,
diffPositionByLineCode,
}); });
requestIdleCallback(
() => {
checkItem();
},
{ timeout: 1000 },
);
} }
}; });
};
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();
}
});
checkItem(); return checkItem();
}; };
export const setInlineDiffViewType = ({ commit }) => { export const setInlineDiffViewType = ({ commit }) => {
......
...@@ -17,7 +17,10 @@ export const commitId = state => (state.commit && state.commit.id ? state.commit ...@@ -17,7 +17,10 @@ export const commitId = state => (state.commit && state.commit.id ? state.commit
export const diffHasAllExpandedDiscussions = (state, getters) => diff => { export const diffHasAllExpandedDiscussions = (state, getters) => diff => {
const discussions = getters.getDiffFileDiscussions(diff); const discussions = getters.getDiffFileDiscussions(diff);
return (discussions.length && discussions.every(discussion => discussion.expanded)) || false; return (
(discussions && discussions.length && discussions.every(discussion => discussion.expanded)) ||
false
);
}; };
/** /**
...@@ -28,7 +31,10 @@ export const diffHasAllExpandedDiscussions = (state, getters) => diff => { ...@@ -28,7 +31,10 @@ export const diffHasAllExpandedDiscussions = (state, getters) => diff => {
export const diffHasAllCollpasedDiscussions = (state, getters) => diff => { export const diffHasAllCollpasedDiscussions = (state, getters) => diff => {
const discussions = getters.getDiffFileDiscussions(diff); const discussions = getters.getDiffFileDiscussions(diff);
return (discussions.length && discussions.every(discussion => !discussion.expanded)) || false; return (
(discussions && discussions.length && discussions.every(discussion => !discussion.expanded)) ||
false
);
}; };
/** /**
...@@ -40,7 +46,9 @@ export const diffHasExpandedDiscussions = (state, getters) => diff => { ...@@ -40,7 +46,9 @@ export const diffHasExpandedDiscussions = (state, getters) => diff => {
const discussions = getters.getDiffFileDiscussions(diff); const discussions = getters.getDiffFileDiscussions(diff);
return ( return (
(discussions.length && discussions.find(discussion => discussion.expanded) !== undefined) || (discussions &&
discussions.length &&
discussions.find(discussion => discussion.expanded) !== undefined) ||
false false
); );
}; };
...@@ -64,45 +72,38 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) = ...@@ -64,45 +72,38 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) =
discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash), discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash),
) || []; ) || [];
export const singleDiscussionByLineCode = (state, getters, rootState, rootGetters) => lineCode => { export const shouldRenderParallelCommentRow = state => line => {
if (!lineCode || lineCode === undefined) return []; const hasDiscussion =
const discussions = rootGetters.discussionsByLineCode; (line.left && line.left.discussions && line.left.discussions.length) ||
return discussions[lineCode] || []; (line.right && line.right.discussions && line.right.discussions.length);
};
export const shouldRenderParallelCommentRow = (state, getters) => line => {
const 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 const hasExpandedDiscussionOnLeft =
? leftDiscussions.every(discussion => discussion.expanded) line.left && line.left.discussions && line.left.discussions.length
: false; ? line.left.discussions.every(discussion => discussion.expanded)
const hasExpandedDiscussionOnRight = rightDiscussions.length : false;
? rightDiscussions.every(discussion => discussion.expanded) const hasExpandedDiscussionOnRight =
: false; line.right && line.right.discussions && line.right.discussions.length
? line.right.discussions.every(discussion => discussion.expanded)
: false;
if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) { if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
return true; return true;
} }
const hasCommentFormOnLeft = state.diffLineCommentForms[leftLineCode]; const hasCommentFormOnLeft = line.left && state.diffLineCommentForms[line.left.lineCode];
const hasCommentFormOnRight = state.diffLineCommentForms[rightLineCode]; const hasCommentFormOnRight = line.right && state.diffLineCommentForms[line.right.lineCode];
return hasCommentFormOnLeft || hasCommentFormOnRight; return hasCommentFormOnLeft || hasCommentFormOnRight;
}; };
export const shouldRenderInlineCommentRow = (state, getters) => line => { export const shouldRenderInlineCommentRow = state => line => {
if (state.diffLineCommentForms[line.lineCode]) return true; if (state.diffLineCommentForms[line.lineCode]) return true;
const lineDiscussions = getters.singleDiscussionByLineCode(line.lineCode); if (!line.discussions || line.discussions.length === 0) {
if (lineDiscussions.length === 0) {
return false; return false;
} }
return lineDiscussions.every(discussion => discussion.expanded); return line.discussions.every(discussion => discussion.expanded);
}; };
// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests // prevent babel-plugin-rewire from generating an invalid default during karma∂ tests
......
...@@ -9,3 +9,5 @@ export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES'; ...@@ -9,3 +9,5 @@ export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES';
export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS'; export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS';
export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES'; export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
export const RENDER_FILE = 'RENDER_FILE'; export const RENDER_FILE = 'RENDER_FILE';
export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE';
export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE';
import Vue from 'vue'; import Vue from 'vue';
import _ from 'underscore';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { findDiffFile, addLineReferences, removeMatchLine, addContextLines } from './utils'; import {
import { LINES_TO_BE_RENDERED_DIRECTLY, MAX_LINES_TO_BE_RENDERED } from '../constants'; findDiffFile,
addLineReferences,
removeMatchLine,
addContextLines,
prepareDiffData,
isDiscussionApplicableToLine,
} from './utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
...@@ -17,38 +22,7 @@ export default { ...@@ -17,38 +22,7 @@ export default {
[types.SET_DIFF_DATA](state, data) { [types.SET_DIFF_DATA](state, data) {
const diffData = convertObjectPropsToCamelCase(data, { deep: true }); const diffData = convertObjectPropsToCamelCase(data, { deep: true });
let showingLines = 0; prepareDiffData(diffData);
const filesLength = diffData.diffFiles.length;
let i;
for (i = 0; i < filesLength; i += 1) {
const file = diffData.diffFiles[i];
if (file.parallelDiffLines) {
const linesLength = file.parallelDiffLines.length;
let u = 0;
for (u = 0; u < linesLength; u += 1) {
const line = file.parallelDiffLines[u];
if (line.left) delete line.left.text;
if (line.right) delete line.right.text;
}
}
if (file.highlightedDiffLines) {
const linesLength = file.highlightedDiffLines.length;
let u;
for (u = 0; u < linesLength; u += 1) {
const line = file.highlightedDiffLines[u];
delete line.text;
}
}
if (file.highlightedDiffLines) {
showingLines += file.parallelDiffLines.length;
}
Object.assign(file, {
renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED,
});
}
Object.assign(state, { Object.assign(state, {
...diffData, ...diffData,
...@@ -98,12 +72,10 @@ export default { ...@@ -98,12 +72,10 @@ export default {
[types.ADD_COLLAPSED_DIFFS](state, { file, data }) { [types.ADD_COLLAPSED_DIFFS](state, { file, data }) {
const normalizedData = convertObjectPropsToCamelCase(data, { deep: true }); const normalizedData = convertObjectPropsToCamelCase(data, { deep: true });
prepareDiffData(normalizedData);
const [newFileData] = normalizedData.diffFiles.filter(f => f.fileHash === file.fileHash); const [newFileData] = normalizedData.diffFiles.filter(f => f.fileHash === file.fileHash);
const selectedFile = state.diffFiles.find(f => f.fileHash === file.fileHash);
if (newFileData) { Object.assign(selectedFile, { ...newFileData });
const index = _.findIndex(state.diffFiles, f => f.fileHash === file.fileHash);
state.diffFiles.splice(index, 1, newFileData);
}
}, },
[types.EXPAND_ALL_FILES](state) { [types.EXPAND_ALL_FILES](state) {
...@@ -112,4 +84,81 @@ export default { ...@@ -112,4 +84,81 @@ export default {
collapsed: false, collapsed: false,
})); }));
}, },
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, discussions, diffPositionByLineCode }) {
const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash);
const firstDiscussion = discussions[0];
const isDiffDiscussion = firstDiscussion.diff_discussion;
const hasLineCode = firstDiscussion.line_code;
const isResolvable = firstDiscussion.resolvable;
const diffPosition = diffPositionByLineCode[firstDiscussion.line_code];
if (
selectedFile &&
isDiffDiscussion &&
hasLineCode &&
isResolvable &&
diffPosition &&
isDiscussionApplicableToLine(firstDiscussion, diffPosition)
) {
const targetLine = selectedFile.parallelDiffLines.find(
line =>
(line.left && line.left.lineCode === firstDiscussion.line_code) ||
(line.right && line.right.lineCode === firstDiscussion.line_code),
);
if (targetLine) {
if (targetLine.left && targetLine.left.lineCode === firstDiscussion.line_code) {
Object.assign(targetLine.left, {
discussions,
});
} else {
Object.assign(targetLine.right, {
discussions,
});
}
}
if (selectedFile.highlightedDiffLines) {
const targetInlineLine = selectedFile.highlightedDiffLines.find(
line => line.lineCode === firstDiscussion.line_code,
);
if (targetInlineLine) {
Object.assign(targetInlineLine, {
discussions,
});
}
}
}
},
[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash);
if (selectedFile) {
const targetLine = selectedFile.parallelDiffLines.find(
line =>
(line.left && line.left.lineCode === lineCode) ||
(line.right && line.right.lineCode === lineCode),
);
if (targetLine) {
const side = targetLine.left && targetLine.left.lineCode === lineCode ? 'left' : 'right';
Object.assign(targetLine[side], {
discussions: [],
});
}
if (selectedFile.highlightedDiffLines) {
const targetInlineLine = selectedFile.highlightedDiffLines.find(
line => line.lineCode === lineCode,
);
if (targetInlineLine) {
Object.assign(targetInlineLine, {
discussions: [],
});
}
}
}
},
}; };
...@@ -8,6 +8,8 @@ import { ...@@ -8,6 +8,8 @@ import {
NEW_LINE_TYPE, NEW_LINE_TYPE,
OLD_LINE_TYPE, OLD_LINE_TYPE,
MATCH_LINE_TYPE, MATCH_LINE_TYPE,
LINES_TO_BE_RENDERED_DIRECTLY,
MAX_LINES_TO_BE_RENDERED,
} from '../constants'; } from '../constants';
export function findDiffFile(files, hash) { export function findDiffFile(files, hash) {
...@@ -161,6 +163,11 @@ export function addContextLines(options) { ...@@ -161,6 +163,11 @@ export function addContextLines(options) {
* @returns {Object} * @returns {Object}
*/ */
export function trimFirstCharOfLineContent(line = {}) { export function trimFirstCharOfLineContent(line = {}) {
// eslint-disable-next-line no-param-reassign
delete line.text;
// eslint-disable-next-line no-param-reassign
line.discussions = [];
const parsedLine = Object.assign({}, line); const parsedLine = Object.assign({}, line);
if (line.richText) { if (line.richText) {
...@@ -174,7 +181,43 @@ export function trimFirstCharOfLineContent(line = {}) { ...@@ -174,7 +181,43 @@ export function trimFirstCharOfLineContent(line = {}) {
return parsedLine; return parsedLine;
} }
export function getDiffRefsByLineCode(diffFiles) { // This prepares and optimizes the incoming diff data from the server
// by setting up incremental rendering and removing unneeded data
export function prepareDiffData(diffData) {
const filesLength = diffData.diffFiles.length;
let showingLines = 0;
for (let i = 0; i < filesLength; i += 1) {
const file = diffData.diffFiles[i];
if (file.parallelDiffLines) {
const linesLength = file.parallelDiffLines.length;
for (let u = 0; u < linesLength; u += 1) {
const line = file.parallelDiffLines[u];
if (line.left) {
line.left = trimFirstCharOfLineContent(line.left);
}
if (line.right) {
line.right = trimFirstCharOfLineContent(line.right);
}
}
}
if (file.highlightedDiffLines) {
const linesLength = file.highlightedDiffLines.length;
for (let u = 0; u < linesLength; u += 1) {
trimFirstCharOfLineContent(file.highlightedDiffLines[u]);
}
showingLines += file.parallelDiffLines.length;
}
Object.assign(file, {
renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED,
});
}
}
export function getDiffPositionByLineCode(diffFiles) {
return diffFiles.reduce((acc, diffFile) => { return diffFiles.reduce((acc, diffFile) => {
const { baseSha, headSha, startSha } = diffFile.diffRefs; const { baseSha, headSha, startSha } = diffFile.diffRefs;
const { newPath, oldPath } = diffFile; const { newPath, oldPath } = diffFile;
...@@ -194,3 +237,12 @@ export function getDiffRefsByLineCode(diffFiles) { ...@@ -194,3 +237,12 @@ export function getDiffRefsByLineCode(diffFiles) {
return acc; return acc;
}, {}); }, {});
} }
// This method will check whether the discussion is still applicable
// to the diff line in question regarding different versions of the MR
export function isDiscussionApplicableToLine(discussion, diffPosition) {
const originalRefs = convertObjectPropsToCamelCase(discussion.original_position.formatter);
const refs = convertObjectPropsToCamelCase(discussion.position.formatter);
return _.isEqual(refs, diffPosition) || _.isEqual(originalRefs, diffPosition);
}
...@@ -100,7 +100,7 @@ export default { ...@@ -100,7 +100,7 @@ export default {
</div> </div>
<div <div
:class="{ 'content-loading': group.isChildrenLoading }" :class="{ 'content-loading': group.isChildrenLoading }"
class="avatar-container s24 d-none d-sm-block" class="avatar-container s24 d-none d-sm-flex"
> >
<a <a
:href="group.relativePath" :href="group.relativePath"
......
...@@ -154,7 +154,11 @@ export default class Notes { ...@@ -154,7 +154,11 @@ export default class Notes {
this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList); this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff); this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff-retry-button', this.onClickRetryLazyLoad.bind(this)); this.$wrapperEl.on(
'click',
'.js-toggle-lazy-diff-retry-button',
this.onClickRetryLazyLoad.bind(this),
);
// fetch notes when tab becomes visible // fetch notes when tab becomes visible
this.$wrapperEl.on('visibilitychange', this.visibilityChange); this.$wrapperEl.on('visibilitychange', this.visibilityChange);
...@@ -252,9 +256,7 @@ export default class Notes { ...@@ -252,9 +256,7 @@ export default class Notes {
discussionNoteForm = $textarea.closest('.js-discussion-note-form'); discussionNoteForm = $textarea.closest('.js-discussion-note-form');
if (discussionNoteForm.length) { if (discussionNoteForm.length) {
if ($textarea.val() !== '') { if ($textarea.val() !== '') {
if ( if (!window.confirm('Are you sure you want to cancel creating this comment?')) {
!window.confirm('Are you sure you want to cancel creating this comment?')
) {
return; return;
} }
} }
...@@ -266,9 +268,7 @@ export default class Notes { ...@@ -266,9 +268,7 @@ export default class Notes {
originalText = $textarea.closest('form').data('originalNote'); originalText = $textarea.closest('form').data('originalNote');
newText = $textarea.val(); newText = $textarea.val();
if (originalText !== newText) { if (originalText !== newText) {
if ( if (!window.confirm('Are you sure you want to cancel editing this comment?')) {
!window.confirm('Are you sure you want to cancel editing this comment?')
) {
return; return;
} }
} }
...@@ -1316,8 +1316,7 @@ export default class Notes { ...@@ -1316,8 +1316,7 @@ export default class Notes {
$retryButton.prop('disabled', true); $retryButton.prop('disabled', true);
return this.loadLazyDiff(e) return this.loadLazyDiff(e).then(() => {
.then(() => {
$retryButton.prop('disabled', false); $retryButton.prop('disabled', false);
}); });
} }
...@@ -1343,18 +1342,18 @@ export default class Notes { ...@@ -1343,18 +1342,18 @@ export default class Notes {
*/ */
if (url) { if (url) {
return axios return axios
.get(url) .get(url)
.then(({ data }) => { .then(({ data }) => {
// Reset state in case last request returned error // Reset state in case last request returned error
$successContainer.removeClass('hidden'); $successContainer.removeClass('hidden');
$errorContainer.addClass('hidden'); $errorContainer.addClass('hidden');
Notes.renderDiffContent($container, data); Notes.renderDiffContent($container, data);
}) })
.catch(() => { .catch(() => {
$successContainer.addClass('hidden'); $successContainer.addClass('hidden');
$errorContainer.removeClass('hidden'); $errorContainer.removeClass('hidden');
}); });
} }
return Promise.resolve(); return Promise.resolve();
} }
...@@ -1545,12 +1544,8 @@ export default class Notes { ...@@ -1545,12 +1544,8 @@ export default class Notes {
<div class="note-header"> <div class="note-header">
<div class="note-header-info"> <div class="note-header-info">
<a href="/${_.escape(currentUsername)}"> <a href="/${_.escape(currentUsername)}">
<span class="d-none d-sm-inline-block">${_.escape( <span class="d-none d-sm-inline-block">${_.escape(currentUsername)}</span>
currentUsername, <span class="note-headline-light">${_.escape(currentUsername)}</span>
)}</span>
<span class="note-headline-light">${_.escape(
currentUsername,
)}</span>
</a> </a>
</div> </div>
</div> </div>
...@@ -1565,9 +1560,7 @@ export default class Notes { ...@@ -1565,9 +1560,7 @@ export default class Notes {
); );
$tempNote.find('.d-none.d-sm-inline-block').text(_.escape(currentUserFullname)); $tempNote.find('.d-none.d-sm-inline-block').text(_.escape(currentUserFullname));
$tempNote $tempNote.find('.note-headline-light').text(`@${_.escape(currentUsername)}`);
.find('.note-headline-light')
.text(`@${_.escape(currentUsername)}`);
return $tempNote; return $tempNote;
} }
......
...@@ -137,8 +137,10 @@ export default { ...@@ -137,8 +137,10 @@ export default {
return this.unresolvedDiscussions.length > 1; return this.unresolvedDiscussions.length > 1;
}, },
showJumpToNextDiscussion() { showJumpToNextDiscussion() {
return this.hasMultipleUnresolvedDiscussions && return (
!this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder); this.hasMultipleUnresolvedDiscussions &&
!this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder)
);
}, },
shouldRenderDiffs() { shouldRenderDiffs() {
const { diffDiscussion, diffFile } = this.transformedDiscussion; const { diffDiscussion, diffFile } = this.transformedDiscussion;
...@@ -256,11 +258,16 @@ Please check your network connection and try again.`; ...@@ -256,11 +258,16 @@ Please check your network connection and try again.`;
}); });
}, },
jumpToNextDiscussion() { jumpToNextDiscussion() {
const nextId = const nextId = this.nextUnresolvedDiscussionId(
this.nextUnresolvedDiscussionId(this.discussion.id, this.discussionsByDiffOrder); this.discussion.id,
this.discussionsByDiffOrder,
);
this.jumpToDiscussion(nextId); this.jumpToDiscussion(nextId);
}, },
deleteNoteHandler(note) {
this.$emit('noteDeleted', this.discussion, note);
},
}, },
}; };
</script> </script>
...@@ -270,6 +277,7 @@ Please check your network connection and try again.`; ...@@ -270,6 +277,7 @@ Please check your network connection and try again.`;
<div class="timeline-entry-inner"> <div class="timeline-entry-inner">
<div class="timeline-icon"> <div class="timeline-icon">
<user-avatar-link <user-avatar-link
v-if="author"
:link-href="author.path" :link-href="author.path"
:img-src="author.avatar_url" :img-src="author.avatar_url"
:img-alt="author.name" :img-alt="author.name"
...@@ -344,6 +352,7 @@ Please check your network connection and try again.`; ...@@ -344,6 +352,7 @@ Please check your network connection and try again.`;
:is="componentName(note)" :is="componentName(note)"
:note="componentData(note)" :note="componentData(note)"
:key="note.id" :key="note.id"
@handleDeleteNote="deleteNoteHandler"
/> />
</ul> </ul>
<div <div
......
...@@ -86,6 +86,7 @@ export default { ...@@ -86,6 +86,7 @@ export default {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (window.confirm('Are you sure you want to delete this comment?')) { if (window.confirm('Are you sure you want to delete this comment?')) {
this.isDeleting = true; this.isDeleting = true;
this.$emit('handleDeleteNote', this.note);
this.deleteNote(this.note) this.deleteNote(this.note)
.then(() => { .then(() => {
......
...@@ -138,6 +138,7 @@ export default { ...@@ -138,6 +138,7 @@ export default {
.then(() => { .then(() => {
this.isLoading = false; this.isLoading = false;
this.setNotesFetchedState(true); this.setNotesFetchedState(true);
eventHub.$emit('fetchedNotesData');
}) })
.then(() => this.$nextTick()) .then(() => this.$nextTick())
.then(() => this.checkLocationHash()) .then(() => this.checkLocationHash())
......
...@@ -43,14 +43,23 @@ export const fetchDiscussions = ({ commit }, path) => ...@@ -43,14 +43,23 @@ export const fetchDiscussions = ({ commit }, path) =>
commit(types.SET_INITIAL_DISCUSSIONS, discussions); commit(types.SET_INITIAL_DISCUSSIONS, discussions);
}); });
export const refetchDiscussionById = ({ commit }, { path, discussionId }) => export const refetchDiscussionById = ({ commit, state }, { path, discussionId }) =>
service new Promise(resolve => {
.fetchDiscussions(path) service
.then(res => res.json()) .fetchDiscussions(path)
.then(discussions => { .then(res => res.json())
const selectedDiscussion = discussions.find(discussion => discussion.id === discussionId); .then(discussions => {
if (selectedDiscussion) commit(types.UPDATE_DISCUSSION, selectedDiscussion); const selectedDiscussion = discussions.find(discussion => discussion.id === discussionId);
}); if (selectedDiscussion) {
commit(types.UPDATE_DISCUSSION, selectedDiscussion);
// We need to refetch as it is now the transformed one in state
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
resolve(discussion);
}
})
.catch(() => {});
});
export const deleteNote = ({ commit }, note) => export const deleteNote = ({ commit }, note) =>
service.deleteNote(note.path).then(() => { service.deleteNote(note.path).then(() => {
...@@ -152,26 +161,28 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -152,26 +161,28 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
const replyId = noteData.data.in_reply_to_discussion_id; const replyId = noteData.data.in_reply_to_discussion_id;
const methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote'; const methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote';
commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders
$('.notes-form .flash-container').hide(); // hide previous flash notification $('.notes-form .flash-container').hide(); // hide previous flash notification
commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders
if (hasQuickActions) { if (replyId) {
placeholderText = utils.stripQuickActions(placeholderText); if (hasQuickActions) {
} placeholderText = utils.stripQuickActions(placeholderText);
}
if (placeholderText.length) { if (placeholderText.length) {
commit(types.SHOW_PLACEHOLDER_NOTE, { commit(types.SHOW_PLACEHOLDER_NOTE, {
noteBody: placeholderText, noteBody: placeholderText,
replyId, replyId,
}); });
} }
if (hasQuickActions) { if (hasQuickActions) {
commit(types.SHOW_PLACEHOLDER_NOTE, { commit(types.SHOW_PLACEHOLDER_NOTE, {
isSystemNote: true, isSystemNote: true,
noteBody: utils.getQuickActionText(note), noteBody: utils.getQuickActionText(note),
replyId, replyId,
}); });
}
} }
return dispatch(methodToDispatch, noteData).then(res => { return dispatch(methodToDispatch, noteData).then(res => {
...@@ -211,7 +222,9 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -211,7 +222,9 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
if (errors && errors.commands_only) { if (errors && errors.commands_only) {
Flash(errors.commands_only, 'notice', noteData.flashContainer); Flash(errors.commands_only, 'notice', noteData.flashContainer);
} }
commit(types.REMOVE_PLACEHOLDER_NOTES); if (replyId) {
commit(types.REMOVE_PLACEHOLDER_NOTES);
}
return res; return res;
}); });
......
import _ from 'underscore'; import _ from 'underscore';
import * as constants from '../constants'; import * as constants from '../constants';
import { reduceDiscussionsToLineCodes } from './utils';
import { collapseSystemNotes } from './collapse_utils'; import { collapseSystemNotes } from './collapse_utils';
export const discussions = state => collapseSystemNotes(state.discussions); export const discussions = state => collapseSystemNotes(state.discussions);
...@@ -28,17 +29,8 @@ export const notesById = state => ...@@ -28,17 +29,8 @@ export const notesById = state =>
return acc; return acc;
}, {}); }, {});
export const discussionsByLineCode = state => export const discussionsStructuredByLineCode = state =>
state.discussions.reduce((acc, note) => { reduceDiscussionsToLineCodes(state.discussions);
if (note.diff_discussion && note.line_code && note.resolvable) {
// For context about line notes: there might be multiple notes with the same line code
const items = acc[note.line_code] || [];
items.push(note);
Object.assign(acc, { [note.line_code]: items });
}
return acc;
}, {});
export const noteableType = state => { export const noteableType = state => {
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants; const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
......
...@@ -54,13 +54,12 @@ export default { ...@@ -54,13 +54,12 @@ export default {
[types.EXPAND_DISCUSSION](state, { discussionId }) { [types.EXPAND_DISCUSSION](state, { discussionId }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId); const discussion = utils.findNoteObjectById(state.discussions, discussionId);
Object.assign(discussion, { expanded: true });
discussion.expanded = true;
}, },
[types.COLLAPSE_DISCUSSION](state, { discussionId }) { [types.COLLAPSE_DISCUSSION](state, { discussionId }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId); const discussion = utils.findNoteObjectById(state.discussions, discussionId);
discussion.expanded = false; Object.assign(discussion, { expanded: false });
}, },
[types.REMOVE_PLACEHOLDER_NOTES](state) { [types.REMOVE_PLACEHOLDER_NOTES](state) {
...@@ -95,10 +94,15 @@ export default { ...@@ -95,10 +94,15 @@ export default {
[types.SET_USER_DATA](state, data) { [types.SET_USER_DATA](state, data) {
Object.assign(state, { userData: data }); Object.assign(state, { userData: data });
}, },
[types.SET_INITIAL_DISCUSSIONS](state, discussionsData) { [types.SET_INITIAL_DISCUSSIONS](state, discussionsData) {
const discussions = []; const discussions = [];
discussionsData.forEach(discussion => { discussionsData.forEach(discussion => {
if (discussion.diff_file) {
Object.assign(discussion, { fileHash: discussion.diff_file.file_hash });
}
// To support legacy notes, should be very rare case. // To support legacy notes, should be very rare case.
if (discussion.individual_note && discussion.notes.length > 1) { if (discussion.individual_note && discussion.notes.length > 1) {
discussion.notes.forEach(n => { discussion.notes.forEach(n => {
...@@ -168,8 +172,7 @@ export default { ...@@ -168,8 +172,7 @@ export default {
[types.TOGGLE_DISCUSSION](state, { discussionId }) { [types.TOGGLE_DISCUSSION](state, { discussionId }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId); const discussion = utils.findNoteObjectById(state.discussions, discussionId);
Object.assign(discussion, { expanded: !discussion.expanded });
discussion.expanded = !discussion.expanded;
}, },
[types.UPDATE_NOTE](state, note) { [types.UPDATE_NOTE](state, note) {
...@@ -185,16 +188,12 @@ export default { ...@@ -185,16 +188,12 @@ export default {
[types.UPDATE_DISCUSSION](state, noteData) { [types.UPDATE_DISCUSSION](state, noteData) {
const note = noteData; const note = noteData;
let index = 0; const selectedDiscussion = state.discussions.find(disc => disc.id === note.id);
state.discussions.forEach((n, i) => {
if (n.id === note.id) {
index = i;
}
});
note.expanded = true; // override expand flag to prevent collapse note.expanded = true; // override expand flag to prevent collapse
state.discussions.splice(index, 1, note); if (note.diff_file) {
Object.assign(note, { fileHash: note.diff_file.file_hash });
}
Object.assign(selectedDiscussion, { ...note });
}, },
[types.CLOSE_ISSUE](state) { [types.CLOSE_ISSUE](state) {
......
...@@ -2,13 +2,11 @@ import AjaxCache from '~/lib/utils/ajax_cache'; ...@@ -2,13 +2,11 @@ import AjaxCache from '~/lib/utils/ajax_cache';
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export const findNoteObjectById = (notes, id) => export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0];
notes.filter(n => n.id === id)[0];
export const getQuickActionText = note => { export const getQuickActionText = note => {
let text = 'Applying command'; let text = 'Applying command';
const quickActions = const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
const executedCommands = quickActions.filter(command => { const executedCommands = quickActions.filter(command => {
const commandRegex = new RegExp(`/${command.name}`); const commandRegex = new RegExp(`/${command.name}`);
...@@ -27,7 +25,18 @@ export const getQuickActionText = note => { ...@@ -27,7 +25,18 @@ export const getQuickActionText = note => {
return text; return text;
}; };
export const reduceDiscussionsToLineCodes = selectedDiscussions =>
selectedDiscussions.reduce((acc, note) => {
if (note.diff_discussion && note.line_code && note.resolvable) {
// For context about line notes: there might be multiple notes with the same line code
const items = acc[note.line_code] || [];
items.push(note);
Object.assign(acc, { [note.line_code]: items });
}
return acc;
}, {});
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note); export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
export const stripQuickActions = note => export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
note.replace(REGEX_QUICK_ACTIONS, '').trim();
...@@ -25,6 +25,9 @@ export default { ...@@ -25,6 +25,9 @@ export default {
}, },
}, },
computed: { computed: {
modalId() {
return 'delete-wiki-modal';
},
message() { message() {
return s__('WikiPageConfirmDelete|Are you sure you want to delete this page?'); return s__('WikiPageConfirmDelete|Are you sure you want to delete this page?');
}, },
...@@ -47,31 +50,41 @@ export default { ...@@ -47,31 +50,41 @@ export default {
</script> </script>
<template> <template>
<gl-modal <div class="d-inline-block">
id="delete-wiki-modal" <button
:header-title-text="title" v-gl-modal="modalId"
:footer-primary-button-text="s__('WikiPageConfirmDelete|Delete page')" type="button"
footer-primary-button-variant="danger" class="btn btn-danger"
@submit="onSubmit" >
> {{ __('Delete') }}
{{ message }} </button>
<form <gl-ui-modal
ref="form" :title="title"
:action="deleteWikiUrl" :ok-title="s__('WikiPageConfirmDelete|Delete page')"
method="post" :modal-id="modalId"
class="js-requires-input" title-tag="h4"
ok-variant="danger"
@ok="onSubmit"
> >
<input {{ message }}
ref="method" <form
type="hidden" ref="form"
name="_method" :action="deleteWikiUrl"
value="delete" method="post"
/> class="js-requires-input"
<input >
:value="csrfToken" <input
type="hidden" ref="method"
name="authenticity_token" type="hidden"
/> name="_method"
</form> value="delete"
</gl-modal> />
<input
:value="csrfToken"
type="hidden"
name="authenticity_token"
/>
</form>
</gl-ui-modal>
</div>
</template> </template>
...@@ -14,15 +14,15 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -14,15 +14,15 @@ document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new
new GLForm($('.wiki-form')); // eslint-disable-line no-new new GLForm($('.wiki-form')); // eslint-disable-line no-new
const deleteWikiButton = document.getElementById('delete-wiki-button'); const deleteWikiModalWrapperEl = document.getElementById('delete-wiki-modal-wrapper');
if (deleteWikiButton) { if (deleteWikiModalWrapperEl) {
Vue.use(Translate); Vue.use(Translate);
const { deleteWikiUrl, pageTitle } = deleteWikiButton.dataset; const { deleteWikiUrl, pageTitle } = deleteWikiModalWrapperEl.dataset;
const deleteWikiModalEl = document.getElementById('delete-wiki-modal');
const deleteModal = new Vue({ // eslint-disable-line new Vue({ // eslint-disable-line no-new
el: deleteWikiModalEl, el: deleteWikiModalWrapperEl,
data: { data: {
deleteWikiUrl: '', deleteWikiUrl: '',
}, },
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import tableRegistry from './table_registry.vue'; import tableRegistry from './table_registry.vue';
import { errorMessages, errorMessagesTypes } from '../constants'; import { errorMessages, errorMessagesTypes } from '../constants';
import { __ } from '../../locale';
export default { export default {
name: 'CollapsibeContainerRegisty', name: 'CollapsibeContainerRegisty',
...@@ -46,7 +47,10 @@ ...@@ -46,7 +47,10 @@
handleDeleteRepository() { handleDeleteRepository() {
this.deleteRepo(this.repo) this.deleteRepo(this.repo)
.then(() => this.fetchRepos()) .then(() => {
Flash(__('This container registry has been scheduled for deletion.'), 'notice');
this.fetchRepos();
})
.catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); .catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
}, },
......
...@@ -108,6 +108,7 @@ ...@@ -108,6 +108,7 @@
a { a {
width: 100%; width: 100%;
height: 100%;
display: flex; display: flex;
} }
......
...@@ -2,7 +2,6 @@ class Groups::LabelsController < Groups::ApplicationController ...@@ -2,7 +2,6 @@ class Groups::LabelsController < Groups::ApplicationController
include ToggleSubscriptionAction include ToggleSubscriptionAction
before_action :label, only: [:edit, :update, :destroy] before_action :label, only: [:edit, :update, :destroy]
before_action :available_labels, only: [:index]
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy] before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy]
before_action :save_previous_label_path, only: [:edit] before_action :save_previous_label_path, only: [:edit]
...@@ -11,10 +10,12 @@ class Groups::LabelsController < Groups::ApplicationController ...@@ -11,10 +10,12 @@ class Groups::LabelsController < Groups::ApplicationController
def index def index
respond_to do |format| respond_to do |format|
format.html do format.html do
@labels = @available_labels.page(params[:page]) @labels = @group.labels
.optionally_search(params[:search])
.page(params[:page])
end end
format.json do format.json do
render json: LabelSerializer.new.represent_appearance(@available_labels) render json: LabelSerializer.new.represent_appearance(available_labels)
end end
end end
end end
......
...@@ -18,14 +18,10 @@ module Projects ...@@ -18,14 +18,10 @@ module Projects
end end
def destroy def destroy
if image.destroy DeleteContainerRepositoryWorker.perform_async(current_user.id, image.id)
respond_to do |format|
format.json { head :no_content } respond_to do |format|
end format.json { head :no_content }
else
respond_to do |format|
format.json { head :bad_request }
end
end end
end end
...@@ -41,10 +37,10 @@ module Projects ...@@ -41,10 +37,10 @@ module Projects
# Needed to maintain a backwards compatibility. # Needed to maintain a backwards compatibility.
# #
def ensure_root_container_repository! def ensure_root_container_repository!
ContainerRegistry::Path.new(@project.full_path).tap do |path| ::ContainerRegistry::Path.new(@project.full_path).tap do |path|
break if path.has_repository? break if path.has_repository?
ContainerRepository.build_from_path(path).tap do |repository| ::ContainerRepository.build_from_path(path).tap do |repository|
repository.save! if repository.has_tags? repository.save! if repository.has_tags?
end end
end end
......
...@@ -5,6 +5,7 @@ class Label < ActiveRecord::Base ...@@ -5,6 +5,7 @@ class Label < ActiveRecord::Base
include Referable include Referable
include Subscribable include Subscribable
include Gitlab::SQL::Pattern include Gitlab::SQL::Pattern
include OptionallySearch
# Represents a "No Label" state used for filtering Issues and Merge # Represents a "No Label" state used for filtering Issues and Merge
# Requests that have no label assigned. # Requests that have no label assigned.
......
...@@ -6,6 +6,7 @@ class DiscussionEntity < Grape::Entity ...@@ -6,6 +6,7 @@ class DiscussionEntity < Grape::Entity
expose :id, :reply_id expose :id, :reply_id
expose :position, if: -> (d, _) { d.diff_discussion? && !d.legacy_diff_discussion? } expose :position, if: -> (d, _) { d.diff_discussion? && !d.legacy_diff_discussion? }
expose :original_position, if: -> (d, _) { d.diff_discussion? && !d.legacy_diff_discussion? }
expose :line_code, if: -> (d, _) { d.diff_discussion? } expose :line_code, if: -> (d, _) { d.diff_discussion? }
expose :expanded?, as: :expanded expose :expanded?, as: :expanded
expose :active?, as: :active, if: -> (d, _) { d.diff_discussion? } expose :active?, as: :active, if: -> (d, _) { d.diff_discussion? }
......
# frozen_string_literal: true
module Projects
module ContainerRepository
class DestroyService < BaseService
def execute(container_repository)
return false unless can?(current_user, :update_container_image, project)
container_repository.destroy
end
end
end
end
...@@ -160,7 +160,7 @@ module Projects ...@@ -160,7 +160,7 @@ module Projects
def remove_legacy_registry_tags def remove_legacy_registry_tags
return true unless Gitlab.config.registry.enabled return true unless Gitlab.config.registry.enabled
ContainerRepository.build_root_repository(project).tap do |repository| ::ContainerRepository.build_root_repository(project).tap do |repository|
break repository.has_tags? ? repository.delete_tags! : true break repository.has_tags? ? repository.delete_tags! : true
end end
end end
......
...@@ -28,16 +28,8 @@ ...@@ -28,16 +28,8 @@
= link_to project_wiki_history_path(@project, @page), class: "btn" do = link_to project_wiki_history_path(@project, @page), class: "btn" do
= s_("Wiki|Page history") = s_("Wiki|Page history")
- if can?(current_user, :admin_wiki, @project) - if can?(current_user, :admin_wiki, @project)
%button.btn.btn-danger{ data: { toggle: 'modal', #delete-wiki-modal-wrapper{ data: { delete_wiki_url: project_wiki_path(@project, @page), page_title: @page.title.capitalize } }
target: '#delete-wiki-modal',
delete_wiki_url: project_wiki_path(@project, @page),
page_title: @page.title.capitalize },
id: 'delete-wiki-button',
type: 'button' }
= _('Delete')
= render 'form', uploads_path: wiki_attachment_upload_url = render 'form', uploads_path: wiki_attachment_upload_url
= render 'sidebar' = render 'sidebar'
#delete-wiki-modal.modal.fade
...@@ -90,6 +90,7 @@ ...@@ -90,6 +90,7 @@
- authorized_projects - authorized_projects
- background_migration - background_migration
- create_gpg_signature - create_gpg_signature
- delete_container_repository
- delete_merged_branches - delete_merged_branches
- delete_user - delete_user
- email_receiver - email_receiver
......
# frozen_string_literal: true
class DeleteContainerRepositoryWorker
include ApplicationWorker
include ExclusiveLeaseGuard
LEASE_TIMEOUT = 1.hour
attr_reader :container_repository
def perform(current_user_id, container_repository_id)
current_user = User.find_by(id: current_user_id)
@container_repository = ContainerRepository.find_by(id: container_repository_id)
project = container_repository&.project
return unless current_user && container_repository && project
# If a user accidentally attempts to delete the same container registry in quick succession,
# this can lead to orphaned tags.
try_obtain_lease do
Projects::ContainerRepository::DestroyService.new(project, current_user).execute(container_repository)
end
end
# For ExclusiveLeaseGuard concern
def lease_key
@lease_key ||= "container_repository:delete:#{container_repository.id}"
end
# For ExclusiveLeaseGuard concern
def lease_timeout
LEASE_TIMEOUT
end
end
---
title: Fix outdated discussions being shown on Merge Request Changes tab
merge_request: 21543
author:
type: fixed
---
title: Update ffi to 1.9.25
merge_request: 21561
author: Takuya Noguchi
type: other
---
title: Delete a container registry asynchronously
merge_request: 21553
author:
type: fixed
...@@ -46,6 +46,7 @@ ...@@ -46,6 +46,7 @@
- [project_service, 1] - [project_service, 1]
- [delete_user, 1] - [delete_user, 1]
- [todos_destroyer, 1] - [todos_destroyer, 1]
- [delete_container_repository, 1]
- [delete_merged_branches, 1] - [delete_merged_branches, 1]
- [authorized_projects, 1] - [authorized_projects, 1]
- [expire_build_instance_artifacts, 1] - [expire_build_instance_artifacts, 1]
......
# frozen_string_literal: true
# All the files/directories that should be reviewed by the Docs team.
DOCS_FILES = [
'doc/'
].freeze
def docs_paths_requiring_review(files)
files.select do |file|
DOCS_FILES.any? { |pattern| file.start_with?(pattern) }
end
end
all_files = git.added_files + git.modified_files
docs_paths_to_review = docs_paths_requiring_review(all_files)
unless docs_paths_to_review.empty?
message 'This merge request adds or changes files that require a ' \
'review from the docs team.'
markdown(<<~MARKDOWN)
## Docs Review
The following files require a review from the Documentation team:
* #{docs_paths_to_review.map { |path| "`#{path}`" }.join("\n* ")}
To make sure these changes are reviewed, mention `@gl-docsteam` in a separate
comment, and explain what needs to be reviewed by the team. Please don't mention
the team until your changes are ready for review.
MARKDOWN
unless gitlab.mr_labels.include?('Documentation')
warn 'This merge request is missing the ~Documentation label.'
end
end
...@@ -1638,7 +1638,9 @@ There are three possible values: `none`, `normal`, and `recursive`: ...@@ -1638,7 +1638,9 @@ There are three possible values: `none`, `normal`, and `recursive`:
``` ```
- `recursive` means that all submodules (including submodules of submodules) - `recursive` means that all submodules (including submodules of submodules)
will be included. It is equivalent to: will be included. This feature needs Git v1.8.1 and later. When using a
GitLab Runner with an executor not based on Docker, make sure the Git version
meets that requirement. It is equivalent to:
``` ```
git submodule sync --recursive git submodule sync --recursive
......
...@@ -257,6 +257,15 @@ choices: ...@@ -257,6 +257,15 @@ choices:
If your branch name matches any of the above, it will run only the docs If your branch name matches any of the above, it will run only the docs
tests. If it doesn't, the whole test suite will run (including docs). tests. If it doesn't, the whole test suite will run (including docs).
## Danger bot
GitLab uses [danger bot](https://github.com/danger/danger) for some elements in
code review. For docs changes in merge requests, the following actions are taken:
1. Whenever a change under `/doc` is made, the bot leaves a comment for the
author to mention `@gl-docsteam`, so that the docs can be properly
reviewed.
## Merge requests for GitLab documentation ## Merge requests for GitLab documentation
Before getting started, make sure you read the introductory section Before getting started, make sure you read the introductory section
......
...@@ -13,10 +13,10 @@ Check the GitLab handbook for the [writing styles guidelines](https://about.gitl ...@@ -13,10 +13,10 @@ Check the GitLab handbook for the [writing styles guidelines](https://about.gitl
## Files ## Files
- [Directory structure](index.md#location-and-naming-documents): place the docs - [Directory structure](index.md#location-and-naming-documents): place the docs
in the correct location in the correct location.
- [Documentation files](index.md#documentation-files): name the files accordingly - [Documentation files](index.md#documentation-files): name the files accordingly.
- [Markdown](../../user/markdown.md): use the GitLab Flavored Markdown in the - [Markdown](../../user/markdown.md): use the GitLab Flavored Markdown in the
documentation documentation.
NOTE: **Note:** NOTE: **Note:**
**Do not** use capital letters, spaces, or special chars in file names, **Do not** use capital letters, spaces, or special chars in file names,
...@@ -30,17 +30,17 @@ a test that will fail if it spots a new `README.md` file. ...@@ -30,17 +30,17 @@ a test that will fail if it spots a new `README.md` file.
- Split up long lines (wrap text), this makes it much easier to review and edit. Only - Split up long lines (wrap text), this makes it much easier to review and edit. Only
double line breaks are shown as a full line break in [GitLab markdown][gfm]. double line breaks are shown as a full line break in [GitLab markdown][gfm].
80-100 characters is a good line length 80-100 characters is a good line length.
- Make sure that the documentation is added in the correct - Make sure that the documentation is added in the correct
[directory](index.md#documentation-directory-structure) and that [directory](index.md#documentation-directory-structure) and that
there's a link to it somewhere useful there's a link to it somewhere useful.
- Do not duplicate information - Do not duplicate information.
- Be brief and clear - Be brief and clear.
- Unless there's a logical reason not to, add documents in alphabetical order - Unless there's a logical reason not to, add documents in alphabetical order.
- Write in US English - Write in US English.
- Use [single spaces][] instead of double spaces - Use [single spaces][] instead of double spaces.
- Jump a line between different markups (e.g., after every paragraph, header, list, etc) - Jump a line between different markups (e.g., after every paragraph, header, list, etc)
- Capitalize "G" and "L" in GitLab - Capitalize "G" and "L" in GitLab.
- Use sentence case for titles, headings, labels, menu items, and buttons. - Use sentence case for titles, headings, labels, menu items, and buttons.
- Use title case when referring to [features](https://about.gitlab.com/features/) or - Use title case when referring to [features](https://about.gitlab.com/features/) or
[products](https://about.gitlab.com/pricing/) (e.g., GitLab Runner, Geo, [products](https://about.gitlab.com/pricing/) (e.g., GitLab Runner, Geo,
...@@ -50,10 +50,9 @@ some features are also objects (e.g. "Merge Requests" and "merge requests"). ...@@ -50,10 +50,9 @@ some features are also objects (e.g. "Merge Requests" and "merge requests").
## Formatting ## Formatting
- Use double asterisks (`**`) to mark a word or text in bold (`**bold**`) - Use double asterisks (`**`) to mark a word or text in bold (`**bold**`).
- Use undescore (`_`) for text in italics (`_italic_`) - Use undescore (`_`) for text in italics (`_italic_`).
- Jump a line between different markups, for example: - Put an empty line between different markups. For example:
```md ```md
## Header ## Header
...@@ -69,9 +68,16 @@ For punctuation rules, please refer to the [GitLab UX guide](https://design.gitl ...@@ -69,9 +68,16 @@ For punctuation rules, please refer to the [GitLab UX guide](https://design.gitl
### Ordered and unordered lists ### Ordered and unordered lists
- Use dashes (`-`) for unordered lists instead of asterisks (`*`) - Use dashes (`-`) for unordered lists instead of asterisks (`*`).
- Use the number one (`1`) for ordered lists - Use the number one (`1`) for ordered lists.
- For punctuation in bullet lists, please refer to the [GitLab UX guide](https://design.gitlab.com/content/punctuation/) - Separate list items from explanatory text with a colon (`:`). For example:
```md
The list is as follows:
- First item: This explains the first item.
- Second item: This explains the second item.
```
- For further guidance on punctuation in bullet lists, please refer to the [GitLab UX guide](https://design.gitlab.com/content/punctuation/).
## Headings ## Headings
...@@ -82,7 +88,7 @@ For punctuation rules, please refer to the [GitLab UX guide](https://design.gitl ...@@ -82,7 +88,7 @@ For punctuation rules, please refer to the [GitLab UX guide](https://design.gitl
- Avoid putting numbers in headings. Numbers shift, hence documentation anchor - Avoid putting numbers in headings. Numbers shift, hence documentation anchor
links shift too, which eventually leads to dead links. If you think it is links shift too, which eventually leads to dead links. If you think it is
compelling to add numbers in headings, make sure to at least discuss it with compelling to add numbers in headings, make sure to at least discuss it with
someone in the Merge Request someone in the Merge Request.
- [Avoid using symbols and special chars](https://gitlab.com/gitlab-com/gitlab-docs/issues/84) - [Avoid using symbols and special chars](https://gitlab.com/gitlab-com/gitlab-docs/issues/84)
in headers. Whenever possible, they should be plain and short text. in headers. Whenever possible, they should be plain and short text.
- Avoid adding things that show ephemeral statuses. For example, if a feature is - Avoid adding things that show ephemeral statuses. For example, if a feature is
...@@ -92,8 +98,8 @@ For punctuation rules, please refer to the [GitLab UX guide](https://design.gitl ...@@ -92,8 +98,8 @@ For punctuation rules, please refer to the [GitLab UX guide](https://design.gitl
of the following GitLab members for a review: `@axil` or `@marcia`. of the following GitLab members for a review: `@axil` or `@marcia`.
This is to ensure that no document with wrong heading is going This is to ensure that no document with wrong heading is going
live without an audit, thus preventing dead links and redirection issues when live without an audit, thus preventing dead links and redirection issues when
corrected corrected.
- Leave exactly one new line after a heading - Leave exactly one new line after a heading.
## Links ## Links
...@@ -120,11 +126,11 @@ For punctuation rules, please refer to the [GitLab UX guide](https://design.gitl ...@@ -120,11 +126,11 @@ For punctuation rules, please refer to the [GitLab UX guide](https://design.gitl
To indicate the steps of navigation through the UI: To indicate the steps of navigation through the UI:
- Use the exact word as shown in the UI, including any capital letters as-is - Use the exact word as shown in the UI, including any capital letters as-is.
- Use bold text for navigation items and the char `>` as separator - Use bold text for navigation items and the char `>` as separator
(e.g., `Navigate to your project's **Settings > CI/CD**` ) (e.g., `Navigate to your project's **Settings > CI/CD**` ).
- If there are any expandable menus, make sure to mention that the user - If there are any expandable menus, make sure to mention that the user
needs to expand the tab to find the settings you're referring to needs to expand the tab to find the settings you're referring to.
## Images ## Images
...@@ -149,12 +155,12 @@ Inside the document: ...@@ -149,12 +155,12 @@ Inside the document:
`![Proper description what the image is about](img/document_image_title.png)` `![Proper description what the image is about](img/document_image_title.png)`
- Always use a proper description for what the image is about. That way, when a - Always use a proper description for what the image is about. That way, when a
browser fails to show the image, this text will be used as an alternative browser fails to show the image, this text will be used as an alternative
description description.
- If there are consecutive images with little text between them, always add - If there are consecutive images with little text between them, always add
three dashes (`---`) between the image and the text to create a horizontal three dashes (`---`) between the image and the text to create a horizontal
line for better clarity line for better clarity.
- If a heading is placed right after an image, always add three dashes (`---`) - If a heading is placed right after an image, always add three dashes (`---`)
between the image and the heading between the image and the heading.
## Alert boxes ## Alert boxes
...@@ -262,18 +268,18 @@ below. ...@@ -262,18 +268,18 @@ below.
When a feature is available in EE-only tiers, add the corresponding tier according to the When a feature is available in EE-only tiers, add the corresponding tier according to the
feature availability: feature availability:
- For GitLab Starter and GitLab.com Bronze: `**[STARTER]**` - For GitLab Starter and GitLab.com Bronze: `**[STARTER]**`.
- For GitLab Premium and GitLab.com Silver: `**[PREMIUM]**` - For GitLab Premium and GitLab.com Silver: `**[PREMIUM]**`.
- For GitLab Ultimate and GitLab.com Gold: `**[ULTIMATE]**` - For GitLab Ultimate and GitLab.com Gold: `**[ULTIMATE]**`.
- For GitLab Core and GitLab.com Free: `**[CORE]**` - For GitLab Core and GitLab.com Free: `**[CORE]**`.
To exclude GitLab.com tiers (when the feature is not available in GitLab.com), add the To exclude GitLab.com tiers (when the feature is not available in GitLab.com), add the
keyword "only": keyword "only":
- For GitLab Starter: `**[STARTER ONLY]**` - For GitLab Starter: `**[STARTER ONLY]**`.
- For GitLab Premium: `**[PREMIUM ONLY]**` - For GitLab Premium: `**[PREMIUM ONLY]**`.
- For GitLab Ultimate: `**[ULTIMATE ONLY]**` - For GitLab Ultimate: `**[ULTIMATE ONLY]**`.
- For GitLab Core: `**[CORE ONLY]**` - For GitLab Core: `**[CORE ONLY]**`.
The tier should be ideally added to headers, so that the full badge will be displayed. The tier should be ideally added to headers, so that the full badge will be displayed.
But it can be also mentioned from paragraphs, list items, and table cells. For these cases, But it can be also mentioned from paragraphs, list items, and table cells. For these cases,
...@@ -328,8 +334,8 @@ prefer to document it in the CE docs to avoid duplication. ...@@ -328,8 +334,8 @@ prefer to document it in the CE docs to avoid duplication.
Configuration settings include: Configuration settings include:
- settings that touch configuration files in `config/` - Settings that touch configuration files in `config/`.
- NGINX settings and settings in `lib/support/` in general - NGINX settings and settings in `lib/support/` in general.
When there is a list of steps to perform, usually that entails editing the When there is a list of steps to perform, usually that entails editing the
configuration file and reconfiguring/restarting GitLab. In such case, follow configuration file and reconfiguring/restarting GitLab. In such case, follow
......
...@@ -238,6 +238,12 @@ There is also a feature flag to enable Auto DevOps to a percentage of projects ...@@ -238,6 +238,12 @@ There is also a feature flag to enable Auto DevOps to a percentage of projects
which can be enabled from the console with which can be enabled from the console with
`Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(10)`. `Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(10)`.
NOTE: **Enabled by default:**
Starting with GitLab 11.3, the Auto DevOps pipeline will be enabled by default for all
projects. If it's not explicitly enabled for the project, Auto DevOps will be automatically
disabled on the first pipeline failure. Your project will continue to use an alternative
[CI/CD configuration file](../../ci/yaml/README.md) if one is found.
### Deployment strategy ### Deployment strategy
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0. > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0.
......
...@@ -119,5 +119,33 @@ describe Admin::ApplicationSettingsController do ...@@ -119,5 +119,33 @@ describe Admin::ApplicationSettingsController do
expect(response).to redirect_to(admin_application_settings_path) expect(response).to redirect_to(admin_application_settings_path)
expect(ApplicationSetting.current.default_project_creation).to eq(::EE::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) expect(ApplicationSetting.current.default_project_creation).to eq(::EE::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
end end
it 'updates repository_size_limit' do
put :update, application_setting: { repository_size_limit: '100' }
expect(response).to redirect_to(admin_application_settings_path)
expect(response).to set_flash[:notice].to('Application settings saved successfully')
end
it 'does not accept negative repository_size_limit' do
put :update, application_setting: { repository_size_limit: '-100' }
expect(response).to render_template(:show)
expect(assigns(:application_setting).errors[:repository_size_limit]).to be_present
end
it 'does not accept invalid repository_size_limit' do
put :update, application_setting: { repository_size_limit: 'one thousand' }
expect(response).to render_template(:show)
expect(assigns(:application_setting).errors[:repository_size_limit]).to be_present
end
it 'does not accept empty repository_size_limit' do
put :update, application_setting: { repository_size_limit: '' }
expect(response).to render_template(:show)
expect(assigns(:application_setting).errors[:repository_size_limit]).to be_present
end
end end
end end
...@@ -10,11 +10,12 @@ module Banzai ...@@ -10,11 +10,12 @@ module Banzai
# #
# CommonMark does not allow spaces in the url portion of a link/url. # CommonMark does not allow spaces in the url portion of a link/url.
# For example, `[example](page slug)` is not valid. # For example, `[example](page slug)` is not valid.
# Neither is `![example](test image.jpg)`. However, # Neither is `![example](test image.jpg)`. However, particularly
# in our wikis, we support (via RedCarpet) this type of link, allowing # in our wikis, we support (via RedCarpet) this type of link, allowing
# wiki pages to be easily linked by their title. This filter adds that functionality. # wiki pages to be easily linked by their title. This filter adds that functionality.
# The intent is for this to only be used in Wikis - in general, we want #
# to adhere to CommonMark's spec. # This is a small extension to the CommonMark spec. If they start allowing
# spaces in urls, we could then remove this filter.
# #
class SpacedLinkFilter < HTML::Pipeline::Filter class SpacedLinkFilter < HTML::Pipeline::Filter
include ActionView::Helpers::TagHelper include ActionView::Helpers::TagHelper
......
...@@ -18,6 +18,7 @@ module Banzai ...@@ -18,6 +18,7 @@ module Banzai
Filter::MathFilter, Filter::MathFilter,
Filter::ColorFilter, Filter::ColorFilter,
Filter::MermaidFilter, Filter::MermaidFilter,
Filter::SpacedLinkFilter,
Filter::VideoLinkFilter, Filter::VideoLinkFilter,
Filter::ImageLazyLoadFilter, Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter, Filter::ImageLinkFilter,
......
...@@ -5,7 +5,6 @@ module Banzai ...@@ -5,7 +5,6 @@ module Banzai
@filters ||= begin @filters ||= begin
super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter) super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter)
.insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter) .insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter)
.insert_before(Filter::VideoLinkFilter, Filter::SpacedLinkFilter)
end end
end end
end end
......
...@@ -7443,6 +7443,9 @@ msgstr "" ...@@ -7443,6 +7443,9 @@ msgstr ""
msgid "This branch has changed since you started editing. Would you like to create a new branch?" msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr "" msgstr ""
msgid "This container registry has been scheduled for deletion."
msgstr ""
msgid "This date is after the planned finish date, so this epic won't appear in the roadmap." msgid "This date is after the planned finish date, so this epic won't appear in the roadmap."
msgstr "" msgstr ""
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
}, },
"dependencies": { "dependencies": {
"@gitlab-org/gitlab-svgs": "^1.29.0", "@gitlab-org/gitlab-svgs": "^1.29.0",
"@gitlab-org/gitlab-ui": "1.0.5", "@gitlab-org/gitlab-ui": "^1.1.0",
"autosize": "^4.0.0", "autosize": "^4.0.0",
"axios": "^0.17.1", "axios": "^0.17.1",
"babel-core": "^6.26.3", "babel-core": "^6.26.3",
......
...@@ -32,7 +32,7 @@ GEM ...@@ -32,7 +32,7 @@ GEM
diff-lcs (1.3) diff-lcs (1.3)
domain_name (0.5.20170404) domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
ffi (1.9.18) ffi (1.9.25)
http-cookie (1.0.3) http-cookie (1.0.3)
domain_name (~> 0.5) domain_name (~> 0.5)
i18n (0.9.1) i18n (0.9.1)
......
...@@ -79,68 +79,6 @@ describe Admin::ApplicationSettingsController do ...@@ -79,68 +79,6 @@ describe Admin::ApplicationSettingsController do
expect(ApplicationSetting.current.restricted_visibility_levels).to be_empty expect(ApplicationSetting.current.restricted_visibility_levels).to be_empty
end end
it 'updates repository_size_limit' do
put :update, application_setting: { repository_size_limit: '100' }
expect(response).to redirect_to(admin_application_settings_path)
expect(response).to set_flash[:notice].to('Application settings saved successfully')
end
it 'does not accept negative repository_size_limit' do
put :update, application_setting: { repository_size_limit: '-100' }
expect(response).to render_template(:show)
expect(assigns(:application_setting).errors[:repository_size_limit]).to be_present
end
it 'does not accept invalid repository_size_limit' do
put :update, application_setting: { repository_size_limit: 'one thousand' }
expect(response).to render_template(:show)
expect(assigns(:application_setting).errors[:repository_size_limit]).to be_present
end
it 'does not accept empty repository_size_limit' do
put :update, application_setting: { repository_size_limit: '' }
expect(response).to render_template(:show)
expect(assigns(:application_setting).errors[:repository_size_limit]).to be_present
end
end
describe 'GET #usage_data with no access' do
before do
sign_in(user)
end
it 'returns 404' do
get :usage_data, format: :html
expect(response.status).to eq(404)
end
end
describe 'GET #usage_data' do
before do
sign_in(admin)
end
it 'returns HTML data' do
get :usage_data, format: :html
expect(response.body).to start_with('<span')
expect(response.status).to eq(200)
end
it 'returns JSON data' do
get :usage_data, format: :json
body = JSON.parse(response.body)
expect(body["version"]).to eq(Gitlab::VERSION)
expect(body).to include('counts')
expect(response.status).to eq(200)
end
it 'updates the receive_max_input_size setting' do it 'updates the receive_max_input_size setting' do
put :update, application_setting: { receive_max_input_size: "1024" } put :update, application_setting: { receive_max_input_size: "1024" }
......
...@@ -86,9 +86,10 @@ describe Projects::Registry::RepositoriesController do ...@@ -86,9 +86,10 @@ describe Projects::Registry::RepositoriesController do
stub_container_registry_tags(repository: :any, tags: []) stub_container_registry_tags(repository: :any, tags: [])
end end
it 'deletes a repository' do it 'schedules a job to delete a repository' do
expect { delete_repository(repository) }.to change { ContainerRepository.all.count }.by(-1) expect(DeleteContainerRepositoryWorker).to receive(:perform_async).with(user.id, repository.id)
delete_repository(repository)
expect(response).to have_gitlab_http_status(:no_content) expect(response).to have_gitlab_http_status(:no_content)
end end
end end
......
...@@ -186,11 +186,8 @@ describe 'Merge request > User posts diff notes', :js do ...@@ -186,11 +186,8 @@ describe 'Merge request > User posts diff notes', :js do
describe 'posting a note' do describe 'posting a note' do
it 'adds as discussion' do it 'adds as discussion' do
expect(page).to have_css('.js-temp-notes-holder', count: 2)
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'), asset_form_reset: false) should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'), asset_form_reset: false)
expect(page).to have_css('.notes_holder .note.note-discussion', count: 1) expect(page).to have_css('.notes_holder .note.note-discussion', count: 1)
expect(page).to have_css('.js-temp-notes-holder', count: 1)
expect(page).to have_button('Reply...') expect(page).to have_button('Reply...')
end end
end end
...@@ -267,7 +264,7 @@ describe 'Merge request > User posts diff notes', :js do ...@@ -267,7 +264,7 @@ describe 'Merge request > User posts diff notes', :js do
def assert_comment_persistence(line_holder, asset_form_reset:) def assert_comment_persistence(line_holder, asset_form_reset:)
notes_holder_saved = line_holder.find(:xpath, notes_holder_input_xpath) notes_holder_saved = line_holder.find(:xpath, notes_holder_input_xpath)
expect(notes_holder_saved[:class]).not_to include(notes_holder_input_class) expect(notes_holder_saved[:class]).not_to include('note-edit-form')
expect(notes_holder_saved).to have_content test_note_comment expect(notes_holder_saved).to have_content test_note_comment
assert_form_is_reset if asset_form_reset assert_form_is_reset if asset_form_reset
...@@ -281,6 +278,6 @@ describe 'Merge request > User posts diff notes', :js do ...@@ -281,6 +278,6 @@ describe 'Merge request > User posts diff notes', :js do
end end
def assert_form_is_reset def assert_form_is_reset
expect(page).to have_no_css('.js-temp-notes-holder') expect(page).to have_no_css('.note-edit-form')
end end
end end
...@@ -13,7 +13,7 @@ describe 'User deletes wiki page', :js do ...@@ -13,7 +13,7 @@ describe 'User deletes wiki page', :js do
it 'deletes a page' do it 'deletes a page' do
click_on('Edit') click_on('Edit')
click_on('Delete') click_on('Delete')
find('.js-modal-primary-action').click find('.modal-footer .btn-danger').click
expect(page).to have_content('Page was successfully deleted') expect(page).to have_content('Page was successfully deleted')
end end
......
...@@ -185,7 +185,7 @@ describe IssuablesHelper do ...@@ -185,7 +185,7 @@ describe IssuablesHelper do
issuableRef: "##{issue.iid}", issuableRef: "##{issue.iid}",
markdownPreviewPath: "/#{@project.full_path}/preview_markdown", markdownPreviewPath: "/#{@project.full_path}/preview_markdown",
markdownDocsPath: '/help/user/markdown', markdownDocsPath: '/help/user/markdown',
markdownVersion: 11, markdownVersion: CacheMarkdownField::CACHE_COMMONMARK_VERSION,
issuableTemplates: [], issuableTemplates: [],
projectPath: @project.path, projectPath: @project.path,
projectNamespace: @project.namespace.path, projectNamespace: @project.namespace.path,
......
...@@ -51,7 +51,9 @@ describe('DiffFile', () => { ...@@ -51,7 +51,9 @@ describe('DiffFile', () => {
}); });
it('should have collapsed text and link', done => { it('should have collapsed text and link', done => {
vm.file.collapsed = true; vm.file.renderIt = true;
vm.file.collapsed = false;
vm.file.highlightedDiffLines = null;
vm.$nextTick(() => { vm.$nextTick(() => {
expect(vm.$el.innerText).toContain('This diff is collapsed'); expect(vm.$el.innerText).toContain('This diff is collapsed');
......
...@@ -6,61 +6,61 @@ import discussionsMockData from '../mock_data/diff_discussions'; ...@@ -6,61 +6,61 @@ import discussionsMockData from '../mock_data/diff_discussions';
import diffFileMockData from '../mock_data/diff_file'; import diffFileMockData from '../mock_data/diff_file';
describe('DiffLineGutterContent', () => { describe('DiffLineGutterContent', () => {
const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
const getDiffFileMock = () => Object.assign({}, diffFileMockData); const getDiffFileMock = () => Object.assign({}, diffFileMockData);
const createComponent = (options = {}) => { const createComponent = (options = {}) => {
const cmp = Vue.extend(DiffLineGutterContent); const cmp = Vue.extend(DiffLineGutterContent);
const props = Object.assign({}, options); const props = Object.assign({}, options);
props.line = {
lineCode: 'LC_42',
type: 'new',
oldLine: null,
newLine: 1,
discussions: [],
text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
richText: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
metaData: null,
};
props.fileHash = getDiffFileMock().fileHash; props.fileHash = getDiffFileMock().fileHash;
props.contextLinesPath = '/context/lines/path'; props.contextLinesPath = '/context/lines/path';
return createComponentWithStore(cmp, store, props).$mount(); return createComponentWithStore(cmp, store, props).$mount();
}; };
const setDiscussions = component => {
component.$store.dispatch('setInitialNotes', getDiscussionsMockData());
};
const resetDiscussions = component => {
component.$store.dispatch('setInitialNotes', []);
};
describe('computed', () => { describe('computed', () => {
describe('lineHref', () => { describe('lineHref', () => {
it('should prepend # to lineCode', () => { it('should prepend # to lineCode', () => {
const lineCode = 'LC_42'; const lineCode = 'LC_42';
const component = createComponent({ lineCode }); const component = createComponent();
expect(component.lineHref).toEqual(`#${lineCode}`); expect(component.lineHref).toEqual(`#${lineCode}`);
}); });
it('should return # if there is no lineCode', () => { it('should return # if there is no lineCode', () => {
const component = createComponent({ lineCode: null }); const component = createComponent();
component.line.lineCode = '';
expect(component.lineHref).toEqual('#'); expect(component.lineHref).toEqual('#');
}); });
}); });
describe('discussions, hasDiscussions, shouldShowAvatarsOnGutter', () => { describe('discussions, hasDiscussions, shouldShowAvatarsOnGutter', () => {
it('should return empty array when there is no discussion', () => { it('should return empty array when there is no discussion', () => {
const component = createComponent({ lineCode: 'LC_42' }); const component = createComponent();
expect(component.discussions).toEqual([]);
expect(component.hasDiscussions).toEqual(false); expect(component.hasDiscussions).toEqual(false);
expect(component.shouldShowAvatarsOnGutter).toEqual(false); expect(component.shouldShowAvatarsOnGutter).toEqual(false);
}); });
it('should return discussions for the given lineCode', () => { it('should return discussions for the given lineCode', () => {
const { lineCode } = getDiffFileMock().highlightedDiffLines[1]; const cmp = Vue.extend(DiffLineGutterContent);
const component = createComponent({ const props = {
lineCode, line: getDiffFileMock().highlightedDiffLines[1],
fileHash: getDiffFileMock().fileHash,
showCommentButton: true, showCommentButton: true,
discussions: getDiscussionsMockData(), contextLinesPath: '/context/lines/path',
}); };
props.line.discussions = [Object.assign({}, discussionsMockData)];
const component = createComponentWithStore(cmp, store, props).$mount();
setDiscussions(component);
expect(component.discussions).toEqual(getDiscussionsMockData());
expect(component.hasDiscussions).toEqual(true); expect(component.hasDiscussions).toEqual(true);
expect(component.shouldShowAvatarsOnGutter).toEqual(true); expect(component.shouldShowAvatarsOnGutter).toEqual(true);
resetDiscussions(component);
}); });
}); });
}); });
...@@ -104,9 +104,7 @@ describe('DiffLineGutterContent', () => { ...@@ -104,9 +104,7 @@ describe('DiffLineGutterContent', () => {
lineCode: getDiffFileMock().highlightedDiffLines[1].lineCode, lineCode: getDiffFileMock().highlightedDiffLines[1].lineCode,
}); });
setDiscussions(component);
expect(component.$el.querySelector('.diff-comment-avatar-holders')).toBeDefined(); expect(component.$el.querySelector('.diff-comment-avatar-holders')).toBeDefined();
resetDiscussions(component);
}); });
}); });
}); });
...@@ -18,11 +18,11 @@ describe('ParallelDiffView', () => { ...@@ -18,11 +18,11 @@ describe('ParallelDiffView', () => {
}).$mount(); }).$mount();
}); });
describe('computed', () => { describe('assigned', () => {
describe('parallelDiffLines', () => { describe('diffLines', () => {
it('should normalize lines for empty cells', () => { it('should normalize lines for empty cells', () => {
expect(component.parallelDiffLines[0].left.type).toEqual(constants.EMPTY_CELL_TYPE); expect(component.diffLines[0].left.type).toEqual(constants.EMPTY_CELL_TYPE);
expect(component.parallelDiffLines[1].left.type).toEqual(constants.EMPTY_CELL_TYPE); expect(component.diffLines[1].left.type).toEqual(constants.EMPTY_CELL_TYPE);
}); });
}); });
}); });
......
...@@ -49,6 +49,7 @@ export default { ...@@ -49,6 +49,7 @@ export default {
type: 'new', type: 'new',
oldLine: null, oldLine: null,
newLine: 1, newLine: 1,
discussions: [],
text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
richText: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', richText: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
metaData: null, metaData: null,
...@@ -58,6 +59,7 @@ export default { ...@@ -58,6 +59,7 @@ export default {
type: 'new', type: 'new',
oldLine: null, oldLine: null,
newLine: 2, newLine: 2,
discussions: [],
text: '+<span id="LC2" class="line" lang="plaintext"></span>\n', text: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
richText: '+<span id="LC2" class="line" lang="plaintext"></span>\n', richText: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
metaData: null, metaData: null,
...@@ -67,6 +69,7 @@ export default { ...@@ -67,6 +69,7 @@ export default {
type: null, type: null,
oldLine: 1, oldLine: 1,
newLine: 3, newLine: 3,
discussions: [],
text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
richText: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', richText: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
metaData: null, metaData: null,
...@@ -76,6 +79,7 @@ export default { ...@@ -76,6 +79,7 @@ export default {
type: null, type: null,
oldLine: 2, oldLine: 2,
newLine: 4, newLine: 4,
discussions: [],
text: ' <span id="LC4" class="line" lang="plaintext"></span>\n', text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
richText: ' <span id="LC4" class="line" lang="plaintext"></span>\n', richText: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
metaData: null, metaData: null,
...@@ -85,6 +89,7 @@ export default { ...@@ -85,6 +89,7 @@ export default {
type: null, type: null,
oldLine: 3, oldLine: 3,
newLine: 5, newLine: 5,
discussions: [],
text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
richText: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', richText: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
metaData: null, metaData: null,
...@@ -94,6 +99,7 @@ export default { ...@@ -94,6 +99,7 @@ export default {
type: 'match', type: 'match',
oldLine: null, oldLine: null,
newLine: null, newLine: null,
discussions: [],
text: '', text: '',
richText: '', richText: '',
metaData: { metaData: {
...@@ -112,6 +118,7 @@ export default { ...@@ -112,6 +118,7 @@ export default {
type: 'new', type: 'new',
oldLine: null, oldLine: null,
newLine: 1, newLine: 1,
discussions: [],
text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
richText: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n', richText: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
metaData: null, metaData: null,
...@@ -126,6 +133,7 @@ export default { ...@@ -126,6 +133,7 @@ export default {
type: 'new', type: 'new',
oldLine: null, oldLine: null,
newLine: 2, newLine: 2,
discussions: [],
text: '+<span id="LC2" class="line" lang="plaintext"></span>\n', text: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
richText: '<span id="LC2" class="line" lang="plaintext"></span>\n', richText: '<span id="LC2" class="line" lang="plaintext"></span>\n',
metaData: null, metaData: null,
...@@ -137,6 +145,7 @@ export default { ...@@ -137,6 +145,7 @@ export default {
type: null, type: null,
oldLine: 1, oldLine: 1,
newLine: 3, newLine: 3,
discussions: [],
text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
richText: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', richText: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
metaData: null, metaData: null,
...@@ -146,6 +155,7 @@ export default { ...@@ -146,6 +155,7 @@ export default {
type: null, type: null,
oldLine: 1, oldLine: 1,
newLine: 3, newLine: 3,
discussions: [],
text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
richText: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n', richText: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
metaData: null, metaData: null,
...@@ -157,6 +167,7 @@ export default { ...@@ -157,6 +167,7 @@ export default {
type: null, type: null,
oldLine: 2, oldLine: 2,
newLine: 4, newLine: 4,
discussions: [],
text: ' <span id="LC4" class="line" lang="plaintext"></span>\n', text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
richText: '<span id="LC4" class="line" lang="plaintext"></span>\n', richText: '<span id="LC4" class="line" lang="plaintext"></span>\n',
metaData: null, metaData: null,
...@@ -166,6 +177,7 @@ export default { ...@@ -166,6 +177,7 @@ export default {
type: null, type: null,
oldLine: 2, oldLine: 2,
newLine: 4, newLine: 4,
discussions: [],
text: ' <span id="LC4" class="line" lang="plaintext"></span>\n', text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
richText: '<span id="LC4" class="line" lang="plaintext"></span>\n', richText: '<span id="LC4" class="line" lang="plaintext"></span>\n',
metaData: null, metaData: null,
...@@ -177,6 +189,7 @@ export default { ...@@ -177,6 +189,7 @@ export default {
type: null, type: null,
oldLine: 3, oldLine: 3,
newLine: 5, newLine: 5,
discussions: [],
text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
richText: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', richText: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
metaData: null, metaData: null,
...@@ -186,6 +199,7 @@ export default { ...@@ -186,6 +199,7 @@ export default {
type: null, type: null,
oldLine: 3, oldLine: 3,
newLine: 5, newLine: 5,
discussions: [],
text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
richText: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n', richText: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
metaData: null, metaData: null,
...@@ -197,6 +211,7 @@ export default { ...@@ -197,6 +211,7 @@ export default {
type: 'match', type: 'match',
oldLine: null, oldLine: null,
newLine: null, newLine: null,
discussions: [],
text: '', text: '',
richText: '', richText: '',
metaData: { metaData: {
...@@ -209,6 +224,7 @@ export default { ...@@ -209,6 +224,7 @@ export default {
type: 'match', type: 'match',
oldLine: null, oldLine: null,
newLine: null, newLine: null,
discussions: [],
text: '', text: '',
richText: '', richText: '',
metaData: { metaData: {
......
...@@ -7,10 +7,30 @@ import { ...@@ -7,10 +7,30 @@ import {
} from '~/diffs/constants'; } from '~/diffs/constants';
import * as actions from '~/diffs/store/actions'; import * as actions from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types'; import * as types from '~/diffs/store/mutation_types';
import { reduceDiscussionsToLineCodes } from '~/notes/stores/utils';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import testAction from '../../helpers/vuex_action_helper'; import testAction from '../../helpers/vuex_action_helper';
describe('DiffsStoreActions', () => { describe('DiffsStoreActions', () => {
const originalMethods = {
requestAnimationFrame: global.requestAnimationFrame,
requestIdleCallback: global.requestIdleCallback,
};
beforeEach(() => {
['requestAnimationFrame', 'requestIdleCallback'].forEach(method => {
global[method] = cb => {
cb();
};
});
});
afterEach(() => {
['requestAnimationFrame', 'requestIdleCallback'].forEach(method => {
global[method] = originalMethods[method];
});
});
describe('setBaseConfig', () => { describe('setBaseConfig', () => {
it('should set given endpoint and project path', done => { it('should set given endpoint and project path', done => {
const endpoint = '/diffs/set/endpoint'; const endpoint = '/diffs/set/endpoint';
...@@ -53,6 +73,198 @@ describe('DiffsStoreActions', () => { ...@@ -53,6 +73,198 @@ describe('DiffsStoreActions', () => {
}); });
}); });
describe('assignDiscussionsToDiff', () => {
it('should merge discussions into diffs', done => {
const state = {
diffFiles: [
{
fileHash: 'ABC',
parallelDiffLines: [
{
left: {
lineCode: 'ABC_1_1',
discussions: [],
},
right: {
lineCode: 'ABC_1_1',
discussions: [],
},
},
],
highlightedDiffLines: [
{
lineCode: 'ABC_1_1',
discussions: [],
oldLine: 5,
newLine: null,
},
],
diffRefs: {
baseSha: 'abc',
headSha: 'def',
startSha: 'ghi',
},
newPath: 'file1',
oldPath: 'file2',
},
],
};
const diffPosition = {
baseSha: 'abc',
headSha: 'def',
startSha: 'ghi',
newLine: null,
newPath: 'file1',
oldLine: 5,
oldPath: 'file2',
};
const singleDiscussion = {
line_code: 'ABC_1_1',
diff_discussion: {},
diff_file: {
file_hash: 'ABC',
},
fileHash: 'ABC',
resolvable: true,
position: {
formatter: diffPosition,
},
original_position: {
formatter: diffPosition,
},
};
const discussions = reduceDiscussionsToLineCodes([singleDiscussion]);
testAction(
actions.assignDiscussionsToDiff,
discussions,
state,
[
{
type: types.SET_LINE_DISCUSSIONS_FOR_FILE,
payload: {
fileHash: 'ABC',
discussions: [singleDiscussion],
diffPositionByLineCode: {
ABC_1_1: {
baseSha: 'abc',
headSha: 'def',
startSha: 'ghi',
newLine: null,
newPath: 'file1',
oldLine: 5,
oldPath: 'file2',
},
},
},
},
],
[],
() => {
done();
},
);
});
});
describe('removeDiscussionsFromDiff', () => {
it('should remove discussions from diffs', done => {
const state = {
diffFiles: [
{
fileHash: 'ABC',
parallelDiffLines: [
{
left: {
lineCode: 'ABC_1_1',
discussions: [
{
id: 1,
},
],
},
right: {
lineCode: 'ABC_1_1',
discussions: [],
},
},
],
highlightedDiffLines: [
{
lineCode: 'ABC_1_1',
discussions: [],
},
],
},
],
};
const singleDiscussion = {
fileHash: 'ABC',
line_code: 'ABC_1_1',
};
testAction(
actions.removeDiscussionsFromDiff,
singleDiscussion,
state,
[
{
type: types.REMOVE_LINE_DISCUSSIONS_FOR_FILE,
payload: {
fileHash: 'ABC',
lineCode: 'ABC_1_1',
},
},
],
[],
() => {
done();
},
);
});
});
describe('startRenderDiffsQueue', () => {
it('should set all files to RENDER_FILE', done => {
const state = {
diffFiles: [
{
id: 1,
renderIt: false,
collapsed: false,
},
{
id: 2,
renderIt: false,
collapsed: false,
},
],
};
const pseudoCommit = (commitType, file) => {
expect(commitType).toBe(types.RENDER_FILE);
Object.assign(file, {
renderIt: true,
});
};
actions
.startRenderDiffsQueue({ state, commit: pseudoCommit })
.then(() => {
expect(state.diffFiles[0].renderIt).toBeTruthy();
expect(state.diffFiles[1].renderIt).toBeTruthy();
done();
})
.catch(() => {
done.fail();
});
});
});
describe('setInlineDiffViewType', () => { describe('setInlineDiffViewType', () => {
it('should set diff view type to inline and also set the cookie properly', done => { it('should set diff view type to inline and also set the cookie properly', done => {
testAction( testAction(
...@@ -204,7 +416,11 @@ describe('DiffsStoreActions', () => { ...@@ -204,7 +416,11 @@ describe('DiffsStoreActions', () => {
actions.toggleFileDiscussions({ getters, dispatch }); actions.toggleFileDiscussions({ getters, dispatch });
expect(dispatch).toHaveBeenCalledWith('collapseDiscussion', { discussionId: 1 }, { root: true }); expect(dispatch).toHaveBeenCalledWith(
'collapseDiscussion',
{ discussionId: 1 },
{ root: true },
);
}); });
it('should dispatch expandDiscussion when all discussions are collapsed', () => { it('should dispatch expandDiscussion when all discussions are collapsed', () => {
...@@ -218,7 +434,11 @@ describe('DiffsStoreActions', () => { ...@@ -218,7 +434,11 @@ describe('DiffsStoreActions', () => {
actions.toggleFileDiscussions({ getters, dispatch }); actions.toggleFileDiscussions({ getters, dispatch });
expect(dispatch).toHaveBeenCalledWith('expandDiscussion', { discussionId: 1 }, { root: true }); expect(dispatch).toHaveBeenCalledWith(
'expandDiscussion',
{ discussionId: 1 },
{ root: true },
);
}); });
it('should dispatch expandDiscussion when some discussions are collapsed and others are expanded for the collapsed discussion', () => { it('should dispatch expandDiscussion when some discussions are collapsed and others are expanded for the collapsed discussion', () => {
...@@ -232,7 +452,11 @@ describe('DiffsStoreActions', () => { ...@@ -232,7 +452,11 @@ describe('DiffsStoreActions', () => {
actions.toggleFileDiscussions({ getters, dispatch }); actions.toggleFileDiscussions({ getters, dispatch });
expect(dispatch).toHaveBeenCalledWith('expandDiscussion', { discussionId: 1 }, { root: true }); expect(dispatch).toHaveBeenCalledWith(
'expandDiscussion',
{ discussionId: 1 },
{ root: true },
);
}); });
}); });
}); });
...@@ -184,101 +184,73 @@ describe('Diffs Module Getters', () => { ...@@ -184,101 +184,73 @@ describe('Diffs Module Getters', () => {
}); });
}); });
describe('singleDiscussionByLineCode', () => {
it('returns found discussion per line Code', () => {
const discussionsMock = {};
discussionsMock.ABC = discussionMock;
expect(
getters.singleDiscussionByLineCode(localState, {}, null, {
discussionsByLineCode: () => discussionsMock,
})('DEF'),
).toEqual([]);
});
it('returns empty array when no discussions match', () => {
expect(
getters.singleDiscussionByLineCode(localState, {}, null, {
discussionsByLineCode: () => {},
})('DEF'),
).toEqual([]);
});
});
describe('shouldRenderParallelCommentRow', () => { describe('shouldRenderParallelCommentRow', () => {
let line; let line;
beforeEach(() => { beforeEach(() => {
line = {}; line = {};
discussionMock.expanded = true;
line.left = { line.left = {
lineCode: 'ABC', lineCode: 'ABC',
discussions: [discussionMock],
}; };
line.right = { line.right = {
lineCode: 'DEF', lineCode: 'DEF',
discussions: [discussionMock1],
}; };
}); });
it('returns true when discussion is expanded', () => { it('returns true when discussion is expanded', () => {
discussionMock.expanded = true; expect(getters.shouldRenderParallelCommentRow(localState)(line)).toEqual(true);
expect(
getters.shouldRenderParallelCommentRow(localState, {
singleDiscussionByLineCode: () => [discussionMock],
})(line),
).toEqual(true);
}); });
it('returns false when no discussion was found', () => { it('returns false when no discussion was found', () => {
line.left.discussions = [];
line.right.discussions = [];
localState.diffLineCommentForms.ABC = false; localState.diffLineCommentForms.ABC = false;
localState.diffLineCommentForms.DEF = false; localState.diffLineCommentForms.DEF = false;
expect( expect(getters.shouldRenderParallelCommentRow(localState)(line)).toEqual(false);
getters.shouldRenderParallelCommentRow(localState, {
singleDiscussionByLineCode: () => [],
})(line),
).toEqual(false);
}); });
it('returns true when discussionForm was found', () => { it('returns true when discussionForm was found', () => {
localState.diffLineCommentForms.ABC = {}; localState.diffLineCommentForms.ABC = {};
expect( expect(getters.shouldRenderParallelCommentRow(localState)(line)).toEqual(true);
getters.shouldRenderParallelCommentRow(localState, {
singleDiscussionByLineCode: () => [discussionMock],
})(line),
).toEqual(true);
}); });
}); });
describe('shouldRenderInlineCommentRow', () => { describe('shouldRenderInlineCommentRow', () => {
let line;
beforeEach(() => {
discussionMock.expanded = true;
line = {
lineCode: 'ABC',
discussions: [discussionMock],
};
});
it('returns true when diffLineCommentForms has form', () => { it('returns true when diffLineCommentForms has form', () => {
localState.diffLineCommentForms.ABC = {}; localState.diffLineCommentForms.ABC = {};
expect( expect(getters.shouldRenderInlineCommentRow(localState)(line)).toEqual(true);
getters.shouldRenderInlineCommentRow(localState)({
lineCode: 'ABC',
}),
).toEqual(true);
}); });
it('returns false when no line discussions were found', () => { it('returns false when no line discussions were found', () => {
expect( line.discussions = [];
getters.shouldRenderInlineCommentRow(localState, { expect(getters.shouldRenderInlineCommentRow(localState)(line)).toEqual(false);
singleDiscussionByLineCode: () => [],
})('DEF'),
).toEqual(false);
}); });
it('returns true if all found discussions are expanded', () => { it('returns true if all found discussions are expanded', () => {
discussionMock.expanded = true; discussionMock.expanded = true;
expect( expect(getters.shouldRenderInlineCommentRow(localState)(line)).toEqual(true);
getters.shouldRenderInlineCommentRow(localState, {
singleDiscussionByLineCode: () => [discussionMock],
})('ABC'),
).toEqual(true);
}); });
}); });
......
...@@ -138,10 +138,9 @@ describe('DiffsStoreMutations', () => { ...@@ -138,10 +138,9 @@ describe('DiffsStoreMutations', () => {
const fileHash = 123; const fileHash = 123;
const state = { diffFiles: [{}, { fileHash, existingField: 0 }] }; const state = { diffFiles: [{}, { fileHash, existingField: 0 }] };
const file = { fileHash };
const data = { diff_files: [{ file_hash: fileHash, extra_field: 1, existingField: 1 }] }; const data = { diff_files: [{ file_hash: fileHash, extra_field: 1, existingField: 1 }] };
mutations[types.ADD_COLLAPSED_DIFFS](state, { file, data }); mutations[types.ADD_COLLAPSED_DIFFS](state, { file: state.diffFiles[1], data });
expect(spy).toHaveBeenCalledWith(data, { deep: true }); expect(spy).toHaveBeenCalledWith(data, { deep: true });
expect(state.diffFiles[1].fileHash).toEqual(fileHash); expect(state.diffFiles[1].fileHash).toEqual(fileHash);
...@@ -149,4 +148,141 @@ describe('DiffsStoreMutations', () => { ...@@ -149,4 +148,141 @@ describe('DiffsStoreMutations', () => {
expect(state.diffFiles[1].extraField).toEqual(1); expect(state.diffFiles[1].extraField).toEqual(1);
}); });
}); });
describe('SET_LINE_DISCUSSIONS_FOR_FILE', () => {
it('should add discussions to the given line', () => {
const diffPosition = {
baseSha: 'ed13df29948c41ba367caa757ab3ec4892509910',
headSha: 'b921914f9a834ac47e6fd9420f78db0f83559130',
newLine: null,
newPath: '500-lines-4.txt',
oldLine: 5,
oldPath: '500-lines-4.txt',
startSha: 'ed13df29948c41ba367caa757ab3ec4892509910',
};
const state = {
diffFiles: [
{
fileHash: 'ABC',
parallelDiffLines: [
{
left: {
lineCode: 'ABC_1',
discussions: [],
},
right: {
lineCode: 'ABC_1',
discussions: [],
},
},
],
highlightedDiffLines: [
{
lineCode: 'ABC_1',
discussions: [],
},
],
},
],
};
const discussions = [
{
id: 1,
line_code: 'ABC_1',
diff_discussion: true,
resolvable: true,
original_position: {
formatter: diffPosition,
},
position: {
formatter: diffPosition,
},
},
{
id: 2,
line_code: 'ABC_1',
diff_discussion: true,
resolvable: true,
original_position: {
formatter: diffPosition,
},
position: {
formatter: diffPosition,
},
},
];
const diffPositionByLineCode = {
ABC_1: diffPosition,
};
mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, {
fileHash: 'ABC',
discussions,
diffPositionByLineCode,
});
expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(2);
expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[1].id).toEqual(2);
expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(2);
expect(state.diffFiles[0].highlightedDiffLines[0].discussions[1].id).toEqual(2);
});
});
describe('REMOVE_LINE_DISCUSSIONS', () => {
it('should remove the existing discussions on the given line', () => {
const state = {
diffFiles: [
{
fileHash: 'ABC',
parallelDiffLines: [
{
left: {
lineCode: 'ABC_1',
discussions: [
{
id: 1,
line_code: 'ABC_1',
},
{
id: 2,
line_code: 'ABC_1',
},
],
},
right: {
lineCode: 'ABC_1',
discussions: [],
},
},
],
highlightedDiffLines: [
{
lineCode: 'ABC_1',
discussions: [
{
id: 1,
line_code: 'ABC_1',
},
{
id: 2,
line_code: 'ABC_1',
},
],
},
],
},
],
};
mutations[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, {
fileHash: 'ABC',
lineCode: 'ABC_1',
});
expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(0);
expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(0);
});
});
}); });
...@@ -179,32 +179,117 @@ describe('DiffsStoreUtils', () => { ...@@ -179,32 +179,117 @@ describe('DiffsStoreUtils', () => {
describe('trimFirstCharOfLineContent', () => { describe('trimFirstCharOfLineContent', () => {
it('trims the line when it starts with a space', () => { it('trims the line when it starts with a space', () => {
expect(utils.trimFirstCharOfLineContent({ richText: ' diff' })).toEqual({ richText: 'diff' }); expect(utils.trimFirstCharOfLineContent({ richText: ' diff' })).toEqual({
discussions: [],
richText: 'diff',
});
}); });
it('trims the line when it starts with a +', () => { it('trims the line when it starts with a +', () => {
expect(utils.trimFirstCharOfLineContent({ richText: '+diff' })).toEqual({ richText: 'diff' }); expect(utils.trimFirstCharOfLineContent({ richText: '+diff' })).toEqual({
discussions: [],
richText: 'diff',
});
}); });
it('trims the line when it starts with a -', () => { it('trims the line when it starts with a -', () => {
expect(utils.trimFirstCharOfLineContent({ richText: '-diff' })).toEqual({ richText: 'diff' }); expect(utils.trimFirstCharOfLineContent({ richText: '-diff' })).toEqual({
discussions: [],
richText: 'diff',
});
}); });
it('does not trims the line when it starts with a letter', () => { it('does not trims the line when it starts with a letter', () => {
expect(utils.trimFirstCharOfLineContent({ richText: 'diff' })).toEqual({ richText: 'diff' }); expect(utils.trimFirstCharOfLineContent({ richText: 'diff' })).toEqual({
discussions: [],
richText: 'diff',
});
}); });
it('does not modify the provided object', () => { it('does not modify the provided object', () => {
const lineObj = { const lineObj = {
discussions: [],
richText: ' diff', richText: ' diff',
}; };
utils.trimFirstCharOfLineContent(lineObj); utils.trimFirstCharOfLineContent(lineObj);
expect(lineObj).toEqual({ richText: ' diff' }); expect(lineObj).toEqual({ discussions: [], richText: ' diff' });
}); });
it('handles a undefined or null parameter', () => { it('handles a undefined or null parameter', () => {
expect(utils.trimFirstCharOfLineContent()).toEqual({}); expect(utils.trimFirstCharOfLineContent()).toEqual({ discussions: [] });
});
});
describe('prepareDiffData', () => {
it('sets the renderIt and collapsed attribute on files', () => {
const preparedDiff = { diffFiles: [getDiffFileMock()] };
utils.prepareDiffData(preparedDiff);
const firstParallelDiffLine = preparedDiff.diffFiles[0].parallelDiffLines[2];
expect(firstParallelDiffLine.left.discussions.length).toBe(0);
expect(firstParallelDiffLine.left).not.toHaveAttr('text');
expect(firstParallelDiffLine.right.discussions.length).toBe(0);
expect(firstParallelDiffLine.right).not.toHaveAttr('text');
expect(preparedDiff.diffFiles[0].highlightedDiffLines[0].discussions.length).toBe(0);
expect(preparedDiff.diffFiles[0].highlightedDiffLines[0]).not.toHaveAttr('text');
expect(preparedDiff.diffFiles[0].renderIt).toBeTruthy();
expect(preparedDiff.diffFiles[0].collapsed).toBeFalsy();
});
});
describe('isDiscussionApplicableToLine', () => {
const diffPosition = {
baseSha: 'ed13df29948c41ba367caa757ab3ec4892509910',
headSha: 'b921914f9a834ac47e6fd9420f78db0f83559130',
newLine: null,
newPath: '500-lines-4.txt',
oldLine: 5,
oldPath: '500-lines-4.txt',
startSha: 'ed13df29948c41ba367caa757ab3ec4892509910',
};
const wrongDiffPosition = {
baseSha: 'wrong',
headSha: 'wrong',
newLine: null,
newPath: '500-lines-4.txt',
oldLine: 5,
oldPath: '500-lines-4.txt',
startSha: 'wrong',
};
const discussions = {
upToDateDiscussion1: {
original_position: {
formatter: diffPosition,
},
position: {
formatter: wrongDiffPosition,
},
},
outDatedDiscussion1: {
original_position: {
formatter: wrongDiffPosition,
},
position: {
formatter: wrongDiffPosition,
},
},
};
it('returns true when the discussion is up to date', () => {
expect(
utils.isDiscussionApplicableToLine(discussions.upToDateDiscussion1, diffPosition),
).toBe(true);
});
it('returns false when the discussion is not up to date', () => {
expect(
utils.isDiscussionApplicableToLine(discussions.outDatedDiscussion1, diffPosition),
).toBe(false);
}); });
}); });
}); });
...@@ -2,10 +2,10 @@ import LazyLoader from '~/lazy_loader'; ...@@ -2,10 +2,10 @@ import LazyLoader from '~/lazy_loader';
let lazyLoader = null; let lazyLoader = null;
describe('LazyLoader', function () { describe('LazyLoader', function() {
preloadFixtures('issues/issue_with_comment.html.raw'); preloadFixtures('issues/issue_with_comment.html.raw');
beforeEach(function () { beforeEach(function() {
loadFixtures('issues/issue_with_comment.html.raw'); loadFixtures('issues/issue_with_comment.html.raw');
lazyLoader = new LazyLoader({ lazyLoader = new LazyLoader({
observerNode: 'body', observerNode: 'body',
...@@ -13,8 +13,8 @@ describe('LazyLoader', function () { ...@@ -13,8 +13,8 @@ describe('LazyLoader', function () {
// Doing everything that happens normally in onload // Doing everything that happens normally in onload
lazyLoader.loadCheck(); lazyLoader.loadCheck();
}); });
describe('behavior', function () { describe('behavior', function() {
it('should copy value from data-src to src for img 1', function (done) { it('should copy value from data-src to src for img 1', function(done) {
const img = document.querySelectorAll('img[data-src]')[0]; const img = document.querySelectorAll('img[data-src]')[0];
const originalDataSrc = img.getAttribute('data-src'); const originalDataSrc = img.getAttribute('data-src');
img.scrollIntoView(); img.scrollIntoView();
...@@ -26,7 +26,7 @@ describe('LazyLoader', function () { ...@@ -26,7 +26,7 @@ describe('LazyLoader', function () {
}, 100); }, 100);
}); });
it('should lazy load dynamically added data-src images', function (done) { it('should lazy load dynamically added data-src images', function(done) {
const newImg = document.createElement('img'); const newImg = document.createElement('img');
const testPath = '/img/testimg.png'; const testPath = '/img/testimg.png';
newImg.className = 'lazy'; newImg.className = 'lazy';
...@@ -41,7 +41,7 @@ describe('LazyLoader', function () { ...@@ -41,7 +41,7 @@ describe('LazyLoader', function () {
}, 100); }, 100);
}); });
it('should not alter normal images', function (done) { it('should not alter normal images', function(done) {
const newImg = document.createElement('img'); const newImg = document.createElement('img');
const testPath = '/img/testimg.png'; const testPath = '/img/testimg.png';
newImg.setAttribute('src', testPath); newImg.setAttribute('src', testPath);
......
...@@ -2,4 +2,4 @@ export const faviconDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACA ...@@ -2,4 +2,4 @@ export const faviconDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACA
export const overlayDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAA85JREFUWAntVllIVGEUPv/9b46O41KplYN7PeRkti8TjQlhCUGh3MmeQugpIsGKAi2soIcIooiohxYKK2daqDAlIpIiWwxtQaJcaHE0d5tMrbn37z9XRqfR0TvVW56Hudf//uec72zfEWBCJjIwkYGJDPzvGSD/KgExN3Oi2Q+2DJgSDYQEMwItVGH1iZGmJw/Si1y+/PwVAMYYib22MYc/8hVQFgKDEfYoId0KYzagAQebsos/ewMZoeB9wdffcTYpQSaCTWHKoqSQaDk7zkIt0+aCUR8BelEHrf3dUNv9AcqbnsHtT5UKB/hTASh0SLYjnjb/CIDRJi0XiFAaJOpCD8zLpdb4NB66b1OfelthX815dtdRRfiti2aAXLvVLiMQ6olGyztGDkSo4JGGXk8/QFdGpYzpHG2GBQTDhtgVhPEaVbbVpvI6GJz22rv4TcAfrYI1x7Rj5MWWAppomKFVVb2302SFzUkZHAbkG+0b1+Gh77yNYjrmqnWTrLBLRxdvBWv8qlFujH/kYjJYyvLkj71t78zAUvzMAMnHhpN4zf9UREJhd8omyssxu1IgazQDwDnHUcNuH6vhPIE1fmuBzHt74Hn7W89jWGtcAjoaIDOFrdcMYJBkgOCoaRF0Lj0oglddDbCj6tRvKjphEpgjkzEQs2YAKsNxMzjn3nKurhzK+Ly7xe28ua8TwgMMcHJZnvvT0BPtEEKM4tDJ+C8GvIIk4ylINIXVZ0EUKJxYuh3mhCeokbudl6TtVc88dfBdLwbyaWB6zQCYQJpBYSrDGQxBQ/ZWRM2B+VNmQnVnHWx7elyNuL2/R336co7KyJR8CL9oLgEuFlREevWUkEl6uGwpVEG4FBm0OEf9N10NMgPlvWYAuNVwsWDKvcUNYsHUWTCZ13ysyFEXe6TO6aC8CUr9IiK+A05TQrc8yjwmxARHeeMAPlfQJw+AQRwu0YhL/GDXi9NwufG+S8dYkuYMqIb4SsWthotlNMOUCOM6r+G9cqXxPmd1dqrBav/o1zJy2l5/NUjJA/VORwYuFnOUaTQcPs9wMqwV++Xv8oADxKAcZ8nLPr8AoGW+xR6HSqYk3GodAz2QNj0V+Gr26dT9ASNH5239Pf0gktVNWZca8ZvfAFBprWS6hSu1pqt++Y0PD+WIwDAhIWQGtzvSHDbcodfFUFB9hg1Gjs5LXqIdFL+acFBl+FddqYwdxsWC3I70OvgfUaA65zhq2O2c8VxYcyIGFTVlXegYtvCXANCQZJMobjVcLMjtSK/IcEgyOOe8Ve5w7ryKDefp2P3+C/5ohv8HZmVLAAAAAElFTkSuQmCC'; export const overlayDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAA85JREFUWAntVllIVGEUPv/9b46O41KplYN7PeRkti8TjQlhCUGh3MmeQugpIsGKAi2soIcIooiohxYKK2daqDAlIpIiWwxtQaJcaHE0d5tMrbn37z9XRqfR0TvVW56Hudf//uec72zfEWBCJjIwkYGJDPzvGSD/KgExN3Oi2Q+2DJgSDYQEMwItVGH1iZGmJw/Si1y+/PwVAMYYib22MYc/8hVQFgKDEfYoId0KYzagAQebsos/ewMZoeB9wdffcTYpQSaCTWHKoqSQaDk7zkIt0+aCUR8BelEHrf3dUNv9AcqbnsHtT5UKB/hTASh0SLYjnjb/CIDRJi0XiFAaJOpCD8zLpdb4NB66b1OfelthX815dtdRRfiti2aAXLvVLiMQ6olGyztGDkSo4JGGXk8/QFdGpYzpHG2GBQTDhtgVhPEaVbbVpvI6GJz22rv4TcAfrYI1x7Rj5MWWAppomKFVVb2302SFzUkZHAbkG+0b1+Gh77yNYjrmqnWTrLBLRxdvBWv8qlFujH/kYjJYyvLkj71t78zAUvzMAMnHhpN4zf9UREJhd8omyssxu1IgazQDwDnHUcNuH6vhPIE1fmuBzHt74Hn7W89jWGtcAjoaIDOFrdcMYJBkgOCoaRF0Lj0oglddDbCj6tRvKjphEpgjkzEQs2YAKsNxMzjn3nKurhzK+Ly7xe28ua8TwgMMcHJZnvvT0BPtEEKM4tDJ+C8GvIIk4ylINIXVZ0EUKJxYuh3mhCeokbudl6TtVc88dfBdLwbyaWB6zQCYQJpBYSrDGQxBQ/ZWRM2B+VNmQnVnHWx7elyNuL2/R336co7KyJR8CL9oLgEuFlREevWUkEl6uGwpVEG4FBm0OEf9N10NMgPlvWYAuNVwsWDKvcUNYsHUWTCZ13ysyFEXe6TO6aC8CUr9IiK+A05TQrc8yjwmxARHeeMAPlfQJw+AQRwu0YhL/GDXi9NwufG+S8dYkuYMqIb4SsWthotlNMOUCOM6r+G9cqXxPmd1dqrBav/o1zJy2l5/NUjJA/VORwYuFnOUaTQcPs9wMqwV++Xv8oADxKAcZ8nLPr8AoGW+xR6HSqYk3GodAz2QNj0V+Gr26dT9ASNH5239Pf0gktVNWZca8ZvfAFBprWS6hSu1pqt++Y0PD+WIwDAhIWQGtzvSHDbcodfFUFB9hg1Gjs5LXqIdFL+acFBl+FddqYwdxsWC3I70OvgfUaA65zhq2O2c8VxYcyIGFTVlXegYtvCXANCQZJMobjVcLMjtSK/IcEgyOOe8Ve5w7ryKDefp2P3+C/5ohv8HZmVLAAAAAElFTkSuQmCC';
export const faviconWithOverlayDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAGt0lEQVRYR8WWf3DT5R3H35/vN0lDiCztSuiPAEnTFhSOUamIzFGokwMH55g0E845yjbP6+4qIoiHY6JjnHLeRI6h9jgQpQcD3ImH2u00eHBwjGthKLI1TSm26S8oKYVS0vT7/X52z7ckTUhqC/yx55/kks/zeb+ez6/nIWYm/B8XJQB4SGq6lL+CJA47vvRtvWs2D0nNl/Kf0qBZxx6u23arv0QAAIHivK8BynB4ffa7BgDQXJx/ngGnw+uThgTwP5ZnMocoJAxVxZDVZ+0L5n5WF75TkMafjLdJxpSg2E+gqW1X7zk3rbpaifhLiEBgTv4mEFbpBoTyu01DoDj/dQAv9rvjtdnp/k3Yx9rgAMV5QYCsAAwAgg6vL/1OTy/2BYrzzwLIBWACuNHhrXPG+otGoKaw0JA58kqGJtOFfgPS8yWT4sz88nzj7UIIfz+wd0mRdEZPLMnp2V/8R0+JrhLbBYFHJvwWzBUxYgqYNzhG+zfEhm24MIE5ectBtP0W+y0Or29FcoDifHFSRxwAcMrh9c0YrmisXaA4r0V0U8xvopgDDq9PpCQ+Ag0/zbEbNUNbMiG9fTwkDTsKHpJa2t1Zmiw1AqLg+tMZ+R6WVVtnZ2qP6Ib+FIjh05G3lsDrB4xjUIbRDeM+WZLJYZ4B1rKMzKPm/fdyzs9qg6WT225IMnPcuYjxbPZhn57qaA00zc4/QYT7b1b/wAZmDYSLjsN1WcmiM+6jXz7JTCs1aNPASBjrtrCGOXVBLK9ph72772bc0REZcsQlkEVoOxblhaFBH0Bxi6GBWFNC8gpV0XqYSe/hI85R9o1zxr/QaZbdbmuW9oRzljRrzBRkW9JhMaTgYugKzl35DlXNJ/Fp43FImoZnz7T0ln7bLihM0g85N627vkWPgLrbvYyCvAP1+rRIWETA5QsyQlcJYOCbMRasWpALtljwSsFyeJxFYsoNWqdN1y/ildM78Y/WGjxx8TL+ol3oluy8VupKe7cfoNLdCJkdqEUPOmBJ5ksJoae91mBps5lQ6pkIm20MPiz6A3KsmcNukDe/3Ye3zh3A77Q2XqcGjslLz88i/nB8pkpSoL8nAFSTBpUN4qSxS5KB5jOGUOniCebmzFQcevSN2xKP+Fp7ajt21f8TOxU/5i45JZFS6XwcTB9HxZgUnGTRNgk31x5jet+aGU7jWw+UweOcPeyTxxoqrGL25+UwdjehSvnmOVIqcz4C8y8GAABcQwjnYI5NheikhQWT+EZmDh2ev/l7cz4U2cGmYyg78TYqVH87Kbtd1wFY4hsVQAt14zu2RiDaTUZMf/BHWD35STx37wDv94k1dLeh7MRmvDZ1GR5Inxg17dX6MPnjZfh5X6tGSqXrV2B8ACIx98UNGOlV4CxCuA6zqIeq9FQ8c68bhx7ZiIK06CQdVF+Il3y1Hq03gnDfk4Uj8zbH2T51dCPOtlW39Q+iPTl2VSMfwKPiKw8aTuhgpl1Zdqxzj8PphRWwm21xZjv9VcgYkYb52dP132PFbSYr/la0DpNtrrg9a2oqsKfB2zlwG+4nSe1z7QDjaQBi2Eh6J4QRwimYt43LwOsuB2oX7YLVMCLqTAya3xx/EwZJxtYHy3WhyMkHExebXz3zAbbXfdo7AFBRaMAz1Ypa6XoaoPejKRGteZm6D3SlWVdOcOHo/Lfj2u9aXw+WHNmA00G/DiFEO0Jd+meyk0fIf/+vLfik6Xhj4qN0v7i5HCY1bBQPk+ij9GSzNbzYNdH03kMrscARfzvHQgiBocSFTVHVCrW+u+WrpK9iCIgS1rRK93oG/1GkRJVIup8KMNs1Sw/1rUtALD36ZzRca8XeJDmPtRc18vDn5SCJViYHENY3IZTK3JkE7RAYtpdkp3bAaJeOzN+CsSMTX+wqa7ih9sbVSLI2WV3znihAJYXZPThA7M6KQoM2MniyhUxTioxTpKLMadjx8Jqh5k3S//8d9GOh92XWmP/aXLKvfHgA0ZTklL0jj9m6UR6L5+9bjFWTPLcFIWbCY1+8pHb0drWybJ4aWLQrODyAWJndzoyylNyGg0hL+bV7Ll4rKIWB5CFBxMlLj21SL4W6QjDQjwOL9n4tNt0+AADPfo+UqgXPHJLSJrkso7F6ylLMy56OFMmYACIKblvtQext8Iqp0swyLYiI3zEAbs6Ml3cXv/p3Y+ryq5KcnSKb1Jmj75P7X0Rm/UV0tvO86r/WIhORwszvkmHEehH2WMo7ikDUQUWhoaIG+NNc96Os8eMEmklE2Qy2ANTO0OrA+CwFOFBfsq8pWZ7+B25aDBxvPp+QAAAAAElFTkSuQmCC'; export const faviconWithOverlayDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAGtElEQVRYR8WXf3CT9R3H35/nSdIQIktrCf0RStI0FYRjVBAccxTq5MDBKUoz4ZyjbPO87q4yBsPDMdExTjlvIsdQexyI0oMBeuKhdjsNHhwcMgpjIlublLIm/UlJKZSSJs/z/e6+T5v0CQ22wB/7/pPck8/383l9fj6fEOec8H88NAjAS1LwknsFSVLU8WXd1rtm85LUeKnwGQKzjj3s33azvsEAAEIlnn8ByHL4/Pa7BgAQLCm8QOBOh88vDQkQeMxjMkcQEYKqYsyJWWPhgs/80TsFafzROJtkNIXFfYI0pfXqPeennjqlxPUNikBoTuEmEF+lCRBV3G0aQiWFrwH8d30AWJubGdiEfZzdGqDEEwbICnADQGGHry7zTr0X94IlnnMACggwAWh0+PxOvb5EBGqmTTNkj7ySxWS62C+g5Usm1Zn95YXG24UQ+r5n75Li6Ux4LBkyc7/4t5YSLSr6Lgg9UvBLcKocMEYKON/gGB3YoA/bcGFCczzLQdieLE9bHL66FakBSjzCU0cSAHDa4at7aLhG9XLBEk8zAVnxZxyIEhBy+PwFgwAafpxvNzK5NZUhrX28JA07Cl6SmtvcOUwm4ZAouHj7ad+jMrN1dqb3iG7oS4EYPh2etQS+XiesC8TQ3ZD3yZJsHuUPgbMcI+ej5v3ncv5PasNlk1p7JJnzJL+I0/O5h+u0VCdqIDi78AQRHuirft3hYJzQPvawPydVdPI+/OnTnNNKBjYVXHRa8rFFGeb4w1he0wZ7d/84IXTEhxzxUsgitB2LPFGwvgGUfLSeZUpEXqEqrIdz0nr4iHOUfeOccb/tNMtutzWHPeWcJc0aMxm5lkxYDGloj1zB+Sv/RXXTSXzaeBwSY3j+bHNv2bdtMYCbpHtRkNFd36xFQN3tXkZhvgP1fdPi5kMEXL4oIXKVAA58M8aCVQs84BYLXi5aDq+zGJTqYr+i4PV2vHxmJ/7WUoOn2i/jz6yhW7JjrdSV8U4fQFV+I2Q4UIsedMCSSlcsgp72WtnSajOhzDsBNtsYfFD8e+Rbs4fdIG98uw9vnj+AX7FWvk4NHZOXXphF/INx2SpJIU2L8L4GDAoMwlP9kWSg6awcKVs83tyUnY5Dj75+W8bjutae3o5d9X/HTiWAuUtOS6RUOR8Hp48TxjgU/AMSeKJ1Ej/tMWXG1sxwGt98sBxe5+xhe64XVLiK2Z9XwNgdRLXyzQsC4ENwelIHAFxDBOdh1qdCdNLCoon8RnY+HZ6/+TtzPhTZweAxlJ94C5VqoI2U3a7rACzJjQqgBd24CGscos1kxPQZ38fqSU/jhQkDvN9lrKG7FeUnNuPVKcvwYOb4hGgvi2HSx8vwRKyJkVLl+hk43gdBAcfADBD1cA4RXIdZ1EN1Zjqem+DGoUc2oigjMUlvaV8YL/1qPVpuhOG+JwdH5m1Okn3m6Eacaz3V2jeI9uTbVYY6AKOSKw8MX0MBg2lXjh3r3Hk4s7ASdrMtSWxnoBpZIzIwP3e69lxv3Gay4q/F6zDJ5kq6s6amEnsafJ0Db8P9JKkx1w5wPJuY36IToojgNMzb8rLwmsuB2kW7YDWMSCgTg+YXx9+AQZKxdUaFZiju+a2Mi8uvnH0f2/2f9g4AVE4z4LlTilrlehag9xIpEam4jO4DXfdaV97nwtH5byW137VYD5Yc2YAz4YAGIYx2RLq0z1Sex8l//fUWfBI83jh4Kd1PEuAwqVGjWEwSS+nJJmt0sWu86d0frMQCR/LbWQ8hDAxlXMgUV69Q67ubv0q5FUNAlHKmVLnXE/gfREpUiaQHqAizXbO0UN98BMTSo39Cw7UW7E2Rc728qJGHP68ASbQyNYCQTkAUzCSwQ+CwvSjnsQPGLOnI/C0YO3Lwxq5yhhtqb1KNpGqT1TXvigJU0jh33xpAf7NymoGNDJ9sJtPkYuNkqTh7KnY8vGaoeZPy93+GA1joe4kzzv/SVLqvYngA/dFgVfnlb8tjtm6Ux+I39y/Gqone24IQM+GxL15UO3q7WrhsnhJatCs8PAC9md3OrPK0goaDyEj7uXsuXi0qg4HkIUGE52XHNqmXIl0RGOiHoUV7xb+v5K14SC39At79Ximdhc8ekjImuiyjsXryUszLnY40yThIhSi4bbUHsbfBJ6ZKE5dpQdz4HQOgf2a8tLvklY+M6cuvSnJummxSZ46+X+7biMzaRnSu84IauNYsE5HCOX+HDCPWi7DrKW8/BTcVZ2UN8Me57kc5448TaCYR5XJwC0BtHMwPjs/SgAP1pfuCqSL8Pxhr/wunLWAOAAAAAElFTkSuQmCC';
...@@ -87,4 +87,22 @@ describe Banzai::Pipeline::GfmPipeline do ...@@ -87,4 +87,22 @@ describe Banzai::Pipeline::GfmPipeline do
end end
end end
end end
describe 'markdown link or image urls having spaces' do
let(:project) { create(:project, :public) }
it 'rewrites links with spaces in url' do
markdown = "[Link to Page](page slug)"
output = described_class.to_html(markdown, project: project)
expect(output).to include("href=\"page%20slug\"")
end
it 'rewrites images with spaces in url' do
markdown = "![My Image](test image.png)"
output = described_class.to_html(markdown, project: project)
expect(output).to include("src=\"test%20image.png\"")
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Projects::ContainerRepository::DestroyService do
set(:user) { create(:user) }
set(:project) { create(:project, :private) }
subject { described_class.new(project, user) }
before do
stub_container_registry_config(enabled: true)
end
context 'when user does not have access to registry' do
let!(:repository) { create(:container_repository, :root, project: project) }
it 'does not delete a repository' do
expect { subject.execute(repository) }.not_to change { ContainerRepository.all.count }
end
end
context 'when user has access to registry' do
before do
project.add_developer(user)
end
context 'when root container repository exists' do
let!(:repository) { create(:container_repository, :root, project: project) }
before do
stub_container_registry_tags(repository: :any, tags: [])
end
it 'deletes the repository' do
expect { described_class.new(project, user).execute(repository) }.to change { ContainerRepository.all.count }.by(-1)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe DeleteContainerRepositoryWorker do
let(:registry) { create(:container_repository) }
let(:project) { registry.project }
let(:user) { project.owner }
subject { described_class.new }
describe '#perform' do
it 'executes the destroy service' do
service = instance_double(Projects::ContainerRepository::DestroyService)
expect(service).to receive(:execute)
expect(Projects::ContainerRepository::DestroyService).to receive(:new).with(project, user).and_return(service)
subject.perform(user.id, registry.id)
end
it 'does not raise error when user could not be found' do
expect do
subject.perform(-1, registry.id)
end.not_to raise_error
end
it 'does not raise error when registry could not be found' do
expect do
subject.perform(user.id, -1)
end.not_to raise_error
end
end
end
...@@ -82,9 +82,9 @@ ...@@ -82,9 +82,9 @@
version "1.29.0" version "1.29.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.29.0.tgz#03b65b513f9099bbda6ecf94d673a2952f8c6c70" resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.29.0.tgz#03b65b513f9099bbda6ecf94d673a2952f8c6c70"
"@gitlab-org/gitlab-ui@1.0.5": "@gitlab-org/gitlab-ui@^1.1.0":
version "1.0.5" version "1.1.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.0.5.tgz#a64b402650494115c8b494a44b72c2d6fbf33fff" resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.1.0.tgz#4216b84c142e37653666da6a088384a44c9d5727"
dependencies: dependencies:
"@gitlab-org/gitlab-svgs" "^1.23.0" "@gitlab-org/gitlab-svgs" "^1.23.0"
bootstrap-vue "^2.0.0-rc.11" bootstrap-vue "^2.0.0-rc.11"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment