Commit 024e3860 authored by Drew Blessing's avatar Drew Blessing

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

parents f0f3821b c60cb393
...@@ -2,6 +2,19 @@ ...@@ -2,6 +2,19 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 11.1.4 (2018-07-30)
### Fixed (4 changes, 1 of them is from the community)
- Rework some projects table indexes around repository_storage field. !20377
- Don't overflow project/group dropdown results. !20704 (gfyoung)
- Fixed IDE not opening JSON files. !20798
- Disable Gitaly timeouts when creating or restoring backups. !20810
## 11.1.3 (2018-07-27)
- Not released.
## 11.1.2 (2018-07-26) ## 11.1.2 (2018-07-26)
### Security (4 changes) ### Security (4 changes)
......
...@@ -306,7 +306,7 @@ group :metrics do ...@@ -306,7 +306,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false gem 'influxdb', '~> 0.2', require: false
# Prometheus # Prometheus
gem 'prometheus-client-mmap', '~> 0.9.3' gem 'prometheus-client-mmap', '~> 0.9.4'
gem 'raindrops', '~> 0.18' gem 'raindrops', '~> 0.18'
end end
......
...@@ -635,7 +635,7 @@ GEM ...@@ -635,7 +635,7 @@ GEM
parser parser
unparser unparser
procto (0.0.3) procto (0.0.3)
prometheus-client-mmap (0.9.3) prometheus-client-mmap (0.9.4)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
...@@ -1126,7 +1126,7 @@ DEPENDENCIES ...@@ -1126,7 +1126,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3) peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2) pg (~> 0.18.2)
premailer-rails (~> 1.9.7) premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.9.3) prometheus-client-mmap (~> 0.9.4)
pry-byebug (~> 3.4.1) pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1) rack-attack (~> 4.4.1)
......
...@@ -639,7 +639,7 @@ GEM ...@@ -639,7 +639,7 @@ GEM
parser parser
unparser unparser
procto (0.0.3) procto (0.0.3)
prometheus-client-mmap (0.9.3) prometheus-client-mmap (0.9.4)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
...@@ -1136,7 +1136,7 @@ DEPENDENCIES ...@@ -1136,7 +1136,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3) peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2) pg (~> 0.18.2)
premailer-rails (~> 1.9.7) premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.9.3) prometheus-client-mmap (~> 0.9.4)
pry-byebug (~> 3.4.1) pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1) rack-attack (~> 4.4.1)
......
...@@ -53,8 +53,4 @@ export default class Autosave { ...@@ -53,8 +53,4 @@ export default class Autosave {
return window.localStorage.removeItem(this.key); return window.localStorage.removeItem(this.key);
} }
dispose() {
this.field.off('input');
}
} }
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import Vue from 'vue'; import Vue from 'vue';
import { n__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
import AccessorUtilities from '../../lib/utils/accessor'; import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list.vue'; import boardList from './board_list.vue';
import BoardBlankState from './board_blank_state.vue'; import BoardBlankState from './board_blank_state.vue';
...@@ -17,6 +20,10 @@ gl.issueBoards.Board = Vue.extend({ ...@@ -17,6 +20,10 @@ gl.issueBoards.Board = Vue.extend({
boardList, boardList,
'board-delete': gl.issueBoards.BoardDelete, 'board-delete': gl.issueBoards.BoardDelete,
BoardBlankState, BoardBlankState,
Icon,
},
directives: {
Tooltip,
}, },
props: { props: {
list: { list: {
...@@ -46,6 +53,12 @@ gl.issueBoards.Board = Vue.extend({ ...@@ -46,6 +53,12 @@ gl.issueBoards.Board = Vue.extend({
filter: Store.filter, filter: Store.filter,
}; };
}, },
computed: {
counterTooltip() {
const { issuesSize } = this.list;
return `${n__('%d issue', '%d issues', issuesSize)}`;
},
},
watch: { watch: {
filter: { filter: {
handler() { handler() {
......
...@@ -115,6 +115,7 @@ export default { ...@@ -115,6 +115,7 @@ export default {
:id="list.id + '-title'" :id="list.id + '-title'"
class="form-control" class="form-control"
type="text" type="text"
name="issue_title"
autocomplete="off" autocomplete="off"
/> />
<project-select <project-select
......
...@@ -46,7 +46,7 @@ export default { ...@@ -46,7 +46,7 @@ export default {
selectable: true, selectable: true,
data: (term, callback) => { data: (term, callback) => {
this.loading = true; this.loading = true;
return Api.groupProjects(this.groupId, term, projects => { return Api.groupProjects(this.groupId, term, {}, projects => {
this.loading = false; this.loading = false;
callback(projects); callback(projects);
}); });
......
...@@ -136,6 +136,8 @@ class List { ...@@ -136,6 +136,8 @@ class List {
} }
this.createIssues(data.issues); this.createIssues(data.issues);
return data;
}); });
} }
......
...@@ -125,11 +125,17 @@ gl.issueBoards.BoardsStore = { ...@@ -125,11 +125,17 @@ gl.issueBoards.BoardsStore = {
} else if (listTo.type === 'backlog' && listFrom.type === 'assignee') { } else if (listTo.type === 'backlog' && listFrom.type === 'assignee') {
issue.removeAssignee(listFrom.assignee); issue.removeAssignee(listFrom.assignee);
listFrom.removeIssue(issue); listFrom.removeIssue(issue);
} else if ((listTo.type !== 'label' && listFrom.type === 'assignee') || } else if (this.shouldRemoveIssue(listFrom, listTo)) {
(listTo.type !== 'assignee' && listFrom.type === 'label')) {
listFrom.removeIssue(issue); listFrom.removeIssue(issue);
} }
}, },
shouldRemoveIssue(listFrom, listTo) {
return (
(listTo.type !== 'label' && listFrom.type === 'assignee') ||
(listTo.type !== 'assignee' && listFrom.type === 'label') ||
(listFrom.type === 'backlog')
);
},
moveIssueInList (list, issue, oldIndex, newIndex, idArray) { moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null; const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + 1], 10) || null; const afterId = parseInt(idArray[newIndex + 1], 10) || null;
......
...@@ -30,7 +30,6 @@ export default { ...@@ -30,7 +30,6 @@ export default {
:render-header="false" :render-header="false"
:render-diff-file="false" :render-diff-file="false"
:always-expanded="true" :always-expanded="true"
:discussions-by-diff-order="true"
/> />
</ul> </ul>
</div> </div>
......
...@@ -50,7 +50,7 @@ export default { ...@@ -50,7 +50,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters('diffs', ['diffHasExpandedDiscussions']), ...mapGetters('diffs', ['diffHasExpandedDiscussions', 'diffHasDiscussions']),
hasExpandedDiscussions() { hasExpandedDiscussions() {
return this.diffHasExpandedDiscussions(this.diffFile); return this.diffHasExpandedDiscussions(this.diffFile);
}, },
...@@ -108,6 +108,9 @@ export default { ...@@ -108,6 +108,9 @@ export default {
false, false,
); );
}, },
gfmCopyText() {
return `\`${this.diffFile.filePath}\``;
},
}, },
methods: { methods: {
...mapActions('diffs', ['toggleFileDiscussions']), ...mapActions('diffs', ['toggleFileDiscussions']),
...@@ -191,6 +194,7 @@ export default { ...@@ -191,6 +194,7 @@ export default {
<clipboard-button <clipboard-button
:title="__('Copy file path to clipboard')" :title="__('Copy file path to clipboard')"
:text="diffFile.filePath" :text="diffFile.filePath"
:gfm="gfmCopyText"
css-class="btn-default btn-transparent btn-clipboard" css-class="btn-default btn-transparent btn-clipboard"
/> />
...@@ -217,6 +221,7 @@ export default { ...@@ -217,6 +221,7 @@ export default {
v-if="diffFile.blob && diffFile.blob.readableText" v-if="diffFile.blob && diffFile.blob.readableText"
> >
<button <button
:disabled="!diffHasDiscussions(diffFile)"
:class="{ active: hasExpandedDiscussions }" :class="{ active: hasExpandedDiscussions }"
:title="s__('MergeRequests|Toggle comments for this file')" :title="s__('MergeRequests|Toggle comments for this file')"
class="js-btn-vue-toggle-comments btn" class="js-btn-vue-toggle-comments btn"
......
...@@ -71,18 +71,13 @@ export default { ...@@ -71,18 +71,13 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
discussions: {
type: Array,
required: false,
default: () => [],
},
}, },
computed: { computed: {
...mapState({ ...mapState({
diffViewType: state => state.diffs.diffViewType, diffViewType: state => state.diffs.diffViewType,
diffFiles: state => state.diffs.diffFiles, diffFiles: state => state.diffs.diffFiles,
}), }),
...mapGetters(['isLoggedIn']), ...mapGetters(['isLoggedIn', 'discussionsByLineCode']),
lineHref() { lineHref() {
return this.lineCode ? `#${this.lineCode}` : '#'; return this.lineCode ? `#${this.lineCode}` : '#';
}, },
...@@ -92,19 +87,24 @@ export default { ...@@ -92,19 +87,24 @@ export default {
this.showCommentButton && this.showCommentButton &&
!this.isMatchLine && !this.isMatchLine &&
!this.isContextLine && !this.isContextLine &&
!this.isMetaLine && !this.hasDiscussions &&
!this.hasDiscussions !this.isMetaLine
); );
}, },
discussions() {
return this.discussionsByLineCode[this.lineCode] || [];
},
hasDiscussions() { hasDiscussions() {
return this.discussions.length > 0; return this.discussions.length > 0;
}, },
shouldShowAvatarsOnGutter() { shouldShowAvatarsOnGutter() {
let render = this.hasDiscussions && this.showCommentButton;
if (!this.lineType && this.linePosition === LINE_POSITION_RIGHT) { if (!this.lineType && this.linePosition === LINE_POSITION_RIGHT) {
return false; render = false;
} }
return this.hasDiscussions && this.showCommentButton; return render;
}, },
}, },
methods: { methods: {
...@@ -189,6 +189,7 @@ export default { ...@@ -189,6 +189,7 @@ export default {
</button> </button>
<a <a
v-if="lineNumber" v-if="lineNumber"
v-once
:data-linenumber="lineNumber" :data-linenumber="lineNumber"
:href="lineHref" :href="lineHref"
> >
......
<script> <script>
import $ from 'jquery';
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import noteForm from '../../notes/components/note_form.vue'; 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 '../../autosave';
import { DIFF_NOTE_TYPE } from '../constants'; import { DIFF_NOTE_TYPE, NOTE_TYPE } from '../constants';
export default { export default {
components: { components: {
noteForm, noteForm,
}, },
mixins: [autosave],
props: { props: {
diffFileHash: { diffFileHash: {
type: String, type: String,
...@@ -41,35 +41,28 @@ export default { ...@@ -41,35 +41,28 @@ export default {
}, },
mounted() { mounted() {
if (this.isLoggedIn) { if (this.isLoggedIn) {
const noteableData = this.getNoteableData;
const keys = [ const keys = [
this.noteableData.diff_head_sha, NOTE_TYPE,
this.noteableType,
noteableData.id,
noteableData.diff_head_sha,
DIFF_NOTE_TYPE, DIFF_NOTE_TYPE,
this.noteableData.source_project_id, noteableData.source_project_id,
this.line.lineCode, this.line.lineCode,
]; ];
this.initAutoSave(this.noteableData, keys); this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), keys);
} }
}, },
methods: { methods: {
...mapActions('diffs', ['cancelCommentForm']), ...mapActions('diffs', ['cancelCommentForm']),
...mapActions(['saveNote', 'refetchDiscussionById']), ...mapActions(['saveNote', 'refetchDiscussionById']),
handleCancelCommentForm(shouldConfirm, isDirty) { handleCancelCommentForm() {
if (shouldConfirm && isDirty) { this.autosave.reset();
const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
// eslint-disable-next-line no-alert
if (!window.confirm(msg)) {
return;
}
}
this.cancelCommentForm({ this.cancelCommentForm({
lineCode: this.line.lineCode, lineCode: this.line.lineCode,
}); });
this.$nextTick(() => {
this.resetAutoSave();
});
}, },
handleSaveNote(note) { handleSaveNote(note) {
const selectedDiffFile = this.getDiffFileByHash(this.diffFileHash); const selectedDiffFile = this.getDiffFileByHash(this.diffFileHash);
......
...@@ -67,11 +67,6 @@ export default { ...@@ -67,11 +67,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
discussions: {
type: Array,
required: false,
default: () => [],
},
}, },
computed: { computed: {
...mapGetters(['isLoggedIn']), ...mapGetters(['isLoggedIn']),
...@@ -141,7 +136,6 @@ export default { ...@@ -141,7 +136,6 @@ export default {
: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>
<script> <script>
import { mapState } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import diffDiscussions from './diff_discussions.vue'; import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue'; import diffLineNoteForm from './diff_line_note_form.vue';
...@@ -21,22 +21,18 @@ export default { ...@@ -21,22 +21,18 @@ 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,
}), }),
...mapGetters(['discussionsByLineCode']),
discussions() {
return this.discussionsByLineCode[this.line.lineCode] || [];
},
className() { className() {
return this.discussions.length ? '' : 'js-temp-notes-holder'; return this.discussions.length ? '' : 'js-temp-notes-holder';
}, },
hasCommentForm() {
return this.diffLineCommentForms[this.line.lineCode];
},
}, },
}; };
</script> </script>
...@@ -57,7 +53,7 @@ export default { ...@@ -57,7 +53,7 @@ export default {
:discussions="discussions" :discussions="discussions"
/> />
<diff-line-note-form <diff-line-note-form
v-if="hasCommentForm" v-if="diffLineCommentForms[line.lineCode]"
:diff-file-hash="diffFileHash" :diff-file-hash="diffFileHash"
:line="line" :line="line"
:note-target-line="line" :note-target-line="line"
......
...@@ -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,10 +98,10 @@ export default { ...@@ -104,10 +98,10 @@ 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
v-once
:class="line.type" :class="line.type"
class="line_content" class="line_content"
v-html="line.richText" v-html="line.richText"
......
...@@ -20,11 +20,8 @@ export default { ...@@ -20,11 +20,8 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters('diffs', [ ...mapGetters('diffs', ['commitId']),
'commitId', ...mapGetters(['discussionsByLineCode']),
'shouldRenderInlineCommentRow',
'singleDiscussionByLineCode',
]),
...mapState({ ...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms, diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}), }),
...@@ -38,7 +35,18 @@ export default { ...@@ -38,7 +35,18 @@ export default {
return window.gon.user_color_scheme; return window.gon.user_color_scheme;
}, },
}, },
methods: {}, methods: {
shouldRenderCommentRow(line) {
if (this.diffLineCommentForms[line.lineCode]) return true;
const lineDiscussions = this.discussionsByLineCode[line.lineCode];
if (lineDiscussions === undefined) {
return false;
}
return lineDiscussions.every(discussion => discussion.expanded);
},
},
}; };
</script> </script>
...@@ -57,15 +65,13 @@ export default { ...@@ -57,15 +65,13 @@ export default {
:line="line" :line="line"
:is-bottom="index + 1 === diffLinesLength" :is-bottom="index + 1 === diffLinesLength"
:key="line.lineCode" :key="line.lineCode"
:discussions="singleDiscussionByLineCode(line.lineCode)"
/> />
<inline-diff-comment-row <inline-diff-comment-row
v-if="shouldRenderInlineCommentRow(line)" v-if="shouldRenderCommentRow(line)"
:diff-file-hash="diffFile.fileHash" :diff-file-hash="diffFile.fileHash"
:line="line" :line="line"
:line-index="index" :line-index="index"
:key="index" :key="index"
:discussions="singleDiscussionByLineCode(line.lineCode)"
/> />
</template> </template>
</tbody> </tbody>
......
<script> <script>
import { mapState } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import diffDiscussions from './diff_discussions.vue'; import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue'; import diffLineNoteForm from './diff_line_note_form.vue';
...@@ -21,51 +21,48 @@ export default { ...@@ -21,51 +21,48 @@ 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,
}), }),
...mapGetters(['discussionsByLineCode']),
leftLineCode() { leftLineCode() {
return this.line.left.lineCode; return this.line.left.lineCode;
}, },
rightLineCode() { rightLineCode() {
return this.line.right.lineCode; return this.line.right.lineCode;
}, },
hasDiscussion() {
const discussions = this.discussionsByLineCode;
return discussions[this.leftLineCode] || discussions[this.rightLineCode];
},
hasExpandedDiscussionOnLeft() { hasExpandedDiscussionOnLeft() {
const discussions = this.leftDiscussions; const discussions = this.discussionsByLineCode[this.leftLineCode];
return discussions ? discussions.every(discussion => discussion.expanded) : false; return discussions ? discussions.every(discussion => discussion.expanded) : false;
}, },
hasExpandedDiscussionOnRight() { hasExpandedDiscussionOnRight() {
const discussions = this.rightDiscussions; const discussions = this.discussionsByLineCode[this.rightLineCode];
return discussions ? discussions.every(discussion => discussion.expanded) : false; return discussions ? discussions.every(discussion => discussion.expanded) : false;
}, },
hasAnyExpandedDiscussion() { hasAnyExpandedDiscussion() {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight; return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
}, },
shouldRenderDiscussionsOnLeft() { shouldRenderDiscussionsOnLeft() {
return this.leftDiscussions && this.hasExpandedDiscussionOnLeft; return this.discussionsByLineCode[this.leftLineCode] && this.hasExpandedDiscussionOnLeft;
}, },
shouldRenderDiscussionsOnRight() { shouldRenderDiscussionsOnRight() {
return this.rightDiscussions && this.hasExpandedDiscussionOnRight && this.line.right.type; return (
}, this.discussionsByLineCode[this.rightLineCode] &&
showRightSideCommentForm() { this.hasExpandedDiscussionOnRight &&
return this.line.right.type && this.diffLineCommentForms[this.rightLineCode]; this.line.right.type
);
}, },
className() { className() {
return this.leftDiscussions.length > 0 || this.rightDiscussions.length > 0 return this.hasDiscussion ? '' : 'js-temp-notes-holder';
? ''
: 'js-temp-notes-holder';
}, },
}, },
}; };
...@@ -83,12 +80,13 @@ export default { ...@@ -83,12 +80,13 @@ export default {
class="content" class="content"
> >
<diff-discussions <diff-discussions
v-if="leftDiscussions.length" v-if="discussionsByLineCode[leftLineCode].length"
:discussions="leftDiscussions" :discussions="discussionsByLineCode[leftLineCode]"
/> />
</div> </div>
<diff-line-note-form <diff-line-note-form
v-if="diffLineCommentForms[leftLineCode]" v-if="diffLineCommentForms[leftLineCode] &&
diffLineCommentForms[leftLineCode]"
:diff-file-hash="diffFileHash" :diff-file-hash="diffFileHash"
:line="line.left" :line="line.left"
:note-target-line="line.left" :note-target-line="line.left"
...@@ -102,12 +100,13 @@ export default { ...@@ -102,12 +100,13 @@ export default {
class="content" class="content"
> >
<diff-discussions <diff-discussions
v-if="rightDiscussions.length" v-if="discussionsByLineCode[rightLineCode].length"
:discussions="rightDiscussions" :discussions="discussionsByLineCode[rightLineCode]"
/> />
</div> </div>
<diff-line-note-form <diff-line-note-form
v-if="showRightSideCommentForm" v-if="diffLineCommentForms[rightLineCode] &&
diffLineCommentForms[rightLineCode] && line.right.type"
:diff-file-hash="diffFileHash" :diff-file-hash="diffFileHash"
:line="line.right" :line="line.right"
:note-target-line="line.right" :note-target-line="line.right"
......
...@@ -36,16 +36,6 @@ export default { ...@@ -36,16 +36,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 {
...@@ -126,10 +116,10 @@ export default { ...@@ -126,10 +116,10 @@ export default {
:is-hover="isLeftHover" :is-hover="isLeftHover"
:show-comment-button="true" :show-comment-button="true"
:diff-view-type="parallelDiffViewType" :diff-view-type="parallelDiffViewType"
:discussions="leftDiscussions"
class="diff-line-num old_line" class="diff-line-num old_line"
/> />
<td <td
v-once
:id="line.left.lineCode" :id="line.left.lineCode"
:class="parallelViewLeftLineType" :class="parallelViewLeftLineType"
class="line_content parallel left-side" class="line_content parallel left-side"
...@@ -147,10 +137,10 @@ export default { ...@@ -147,10 +137,10 @@ export default {
:is-hover="isRightHover" :is-hover="isRightHover"
:show-comment-button="true" :show-comment-button="true"
:diff-view-type="parallelDiffViewType" :diff-view-type="parallelDiffViewType"
:discussions="rightDiscussions"
class="diff-line-num new_line" class="diff-line-num new_line"
/> />
<td <td
v-once
:id="line.right.lineCode" :id="line.right.lineCode"
:class="line.right.type" :class="line.right.type"
class="line_content parallel right-side" class="line_content parallel right-side"
......
...@@ -21,11 +21,8 @@ export default { ...@@ -21,11 +21,8 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters('diffs', [ ...mapGetters('diffs', ['commitId']),
'commitId', ...mapGetters(['discussionsByLineCode']),
'singleDiscussionByLineCode',
'shouldRenderParallelCommentRow',
]),
...mapState({ ...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms, diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}), }),
...@@ -55,6 +52,32 @@ export default { ...@@ -55,6 +52,32 @@ export default {
return window.gon.user_color_scheme; return window.gon.user_color_scheme;
}, },
}, },
methods: {
shouldRenderCommentRow(line) {
const leftLineCode = line.left.lineCode;
const rightLineCode = line.right.lineCode;
const discussions = this.discussionsByLineCode;
const leftDiscussions = discussions[leftLineCode];
const rightDiscussions = discussions[rightLineCode];
const hasDiscussion = leftDiscussions || rightDiscussions;
const hasExpandedDiscussionOnLeft = leftDiscussions
? leftDiscussions.every(discussion => discussion.expanded)
: false;
const hasExpandedDiscussionOnRight = rightDiscussions
? rightDiscussions.every(discussion => discussion.expanded)
: false;
if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
return true;
}
const hasCommentFormOnLeft = this.diffLineCommentForms[leftLineCode];
const hasCommentFormOnRight = this.diffLineCommentForms[rightLineCode];
return hasCommentFormOnLeft || hasCommentFormOnRight;
},
},
}; };
</script> </script>
...@@ -75,17 +98,13 @@ export default { ...@@ -75,17 +98,13 @@ export default {
:line="line" :line="line"
:is-bottom="index + 1 === diffLinesLength" :is-bottom="index + 1 === diffLinesLength"
:key="index" :key="index"
:left-discussions="singleDiscussionByLineCode(line.left.lineCode)"
:right-discussions="singleDiscussionByLineCode(line.right.lineCode)"
/> />
<parallel-diff-comment-row <parallel-diff-comment-row
v-if="shouldRenderParallelCommentRow(line)" v-if="shouldRenderCommentRow(line)"
:key="`dcr-${index}`" :key="`dcr-${index}`"
:line="line" :line="line"
:diff-file-hash="diffFile.fileHash" :diff-file-hash="diffFile.fileHash"
:line-index="index" :line-index="index"
:left-discussions="singleDiscussionByLineCode(line.left.lineCode)"
:right-discussions="singleDiscussionByLineCode(line.right.lineCode)"
/> />
</template> </template>
</tbody> </tbody>
......
import _ from 'underscore'; import _ from 'underscore';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants'; import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
import { getDiffRefsByLineCode } from './utils';
export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE; export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
...@@ -47,6 +45,14 @@ export const diffHasExpandedDiscussions = (state, getters) => diff => { ...@@ -47,6 +45,14 @@ export const diffHasExpandedDiscussions = (state, getters) => diff => {
); );
}; };
/**
* Checks if the diff has any discussion
* @param {Boolean} diff
* @returns {Boolean}
*/
export const diffHasDiscussions = (state, getters) => diff =>
getters.getDiffFileDiscussions(diff).length > 0;
/** /**
* Returns an array with the discussions of the given diff * Returns an array with the discussions of the given diff
* @param {Object} diff * @param {Object} diff
...@@ -58,87 +64,6 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) = ...@@ -58,87 +64,6 @@ 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),
) || []; ) || [];
/**
* Returns an Object with discussions by their diff line code
* To avoid rendering outdated discussions on the Changes tab we should do a bunch of SHA
* comparisions. `note.position.formatter` have the current version diff refs but
* `note.original_position.formatter` will have the first version's diff refs.
* If line diff refs matches with one of them, we should render it as a discussion on Changes tab.
*
* @param {Object} diff
* @returns {Array}
*/
export const discussionsByLineCode = (state, getters, rootState, rootGetters) => {
const diffRefsByLineCode = getDiffRefsByLineCode(state.diffFiles);
return rootGetters.discussions.reduce((acc, note) => {
const isDiffDiscussion = note.diff_discussion;
const hasLineCode = note.line_code;
const isResolvable = note.resolvable;
if (isDiffDiscussion && hasLineCode && isResolvable) {
const diffRefs = diffRefsByLineCode[note.line_code];
if (diffRefs) {
const refs = convertObjectPropsToCamelCase(note.position.formatter);
const originalRefs = convertObjectPropsToCamelCase(note.original_position.formatter);
if (_.isEqual(refs, diffRefs) || _.isEqual(originalRefs, diffRefs)) {
const lineCode = note.line_code;
if (acc[lineCode]) {
acc[lineCode].push(note);
} else {
acc[lineCode] = [note];
}
}
}
}
return acc;
}, {});
};
export const singleDiscussionByLineCode = (state, getters) => lineCode => {
if (!lineCode) return [];
const discussions = getters.discussionsByLineCode;
return discussions[lineCode] || [];
};
export const shouldRenderParallelCommentRow = (state, getters) => line => {
const leftLineCode = line.left.lineCode;
const rightLineCode = line.right.lineCode;
const leftDiscussions = getters.singleDiscussionByLineCode(leftLineCode);
const rightDiscussions = getters.singleDiscussionByLineCode(rightLineCode);
const hasDiscussion = leftDiscussions.length || rightDiscussions.length;
const hasExpandedDiscussionOnLeft = leftDiscussions.length
? leftDiscussions.every(discussion => discussion.expanded)
: false;
const hasExpandedDiscussionOnRight = rightDiscussions.length
? rightDiscussions.every(discussion => discussion.expanded)
: false;
if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
return true;
}
const hasCommentFormOnLeft = state.diffLineCommentForms[leftLineCode];
const hasCommentFormOnRight = state.diffLineCommentForms[rightLineCode];
return hasCommentFormOnLeft || hasCommentFormOnRight;
};
export const shouldRenderInlineCommentRow = (state, getters) => line => {
if (state.diffLineCommentForms[line.lineCode]) return true;
const lineDiscussions = getters.singleDiscussionByLineCode(line.lineCode);
if (lineDiscussions.length === 0) {
return false;
}
return lineDiscussions.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
export const getDiffFileByHash = state => fileHash => export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.fileHash === fileHash); state.diffFiles.find(file => file.fileHash === fileHash);
......
...@@ -173,24 +173,3 @@ export function trimFirstCharOfLineContent(line = {}) { ...@@ -173,24 +173,3 @@ export function trimFirstCharOfLineContent(line = {}) {
return parsedLine; return parsedLine;
} }
export function getDiffRefsByLineCode(diffFiles) {
return diffFiles.reduce((acc, diffFile) => {
const { baseSha, headSha, startSha } = diffFile.diffRefs;
const { newPath, oldPath } = diffFile;
// We can only use highlightedDiffLines to create the map of diff lines because
// highlightedDiffLines will also include every parallel diff line in it.
if (diffFile.highlightedDiffLines) {
diffFile.highlightedDiffLines.forEach(line => {
const { lineCode, oldLine, newLine } = line;
if (lineCode) {
acc[lineCode] = { baseSha, headSha, startSha, newPath, oldPath, oldLine, newLine };
}
});
}
return acc;
}, {});
}
import _ from 'underscore';
import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
export const DEFAULT_SIZE_CLASS = 's40';
export const IDENTICON_BG_COUNT = 7;
export function getIdenticonBackgroundClass(entityId) {
const type = (entityId % IDENTICON_BG_COUNT) + 1;
return `bg${type}`;
}
export function getIdenticonTitle(entityName) {
return getFirstCharacterCapitalized(entityName) || ' ';
}
export function renderIdenticon(entity, options = {}) {
const { sizeClass = DEFAULT_SIZE_CLASS } = options;
const bgClass = getIdenticonBackgroundClass(entity.id);
const title = getIdenticonTitle(entity.name);
return `<div class="avatar identicon ${_.escape(sizeClass)} ${_.escape(bgClass)}">${_.escape(title)}</div>`;
}
export function renderAvatar(entity, options = {}) {
if (!entity.avatar_url) {
return renderIdenticon(entity, options);
}
const { sizeClass = DEFAULT_SIZE_CLASS } = options;
return `<img src="${_.escape(entity.avatar_url)}" class="avatar ${_.escape(sizeClass)}" />`;
}
<script> <script>
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale';
import NewModal from './new_dropdown/modal.vue'; import NewModal from './new_dropdown/modal.vue';
import IdeSidebar from './ide_side_bar.vue'; import IdeSidebar from './ide_side_bar.vue';
import RepoTabs from './repo_tabs.vue'; import RepoTabs from './repo_tabs.vue';
...@@ -25,7 +26,6 @@ export default { ...@@ -25,7 +26,6 @@ export default {
}, },
computed: { computed: {
...mapState([ ...mapState([
'changedFiles',
'openFiles', 'openFiles',
'viewer', 'viewer',
'currentMergeRequestId', 'currentMergeRequestId',
...@@ -34,18 +34,10 @@ export default { ...@@ -34,18 +34,10 @@ export default {
'currentProjectId', 'currentProjectId',
'errorMessage', 'errorMessage',
]), ]),
...mapGetters(['activeFile', 'hasChanges']), ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges']),
}, },
mounted() { mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?'; window.onbeforeunload = e => this.onBeforeUnload(e);
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, {
returnValue,
});
return returnValue;
};
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => { Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
if (e.preventDefault) { if (e.preventDefault) {
...@@ -59,6 +51,16 @@ export default { ...@@ -59,6 +51,16 @@ export default {
}, },
methods: { methods: {
...mapActions(['toggleFileFinder']), ...mapActions(['toggleFileFinder']),
onBeforeUnload(e = {}) {
const returnValue = __('Are you sure you want to lose unsaved changes?');
if (!this.someUncommitedChanges) return undefined;
Object.assign(e, {
returnValue,
});
return returnValue;
},
mousetrapStopCallback(e, el, combo) { mousetrapStopCallback(e, el, combo) {
if ( if (
(combo === 't' && el.classList.contains('dropdown-input-field')) || (combo === 't' && el.classList.contains('dropdown-input-field')) ||
......
...@@ -35,7 +35,9 @@ export default { ...@@ -35,7 +35,9 @@ export default {
watch: { watch: {
dropdownOpen() { dropdownOpen() {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.dropdownMenu.scrollIntoView(); this.$refs.dropdownMenu.scrollIntoView({
block: 'nearest',
});
}); });
}, },
mouseOver() { mouseOver() {
......
...@@ -2,11 +2,34 @@ ...@@ -2,11 +2,34 @@
* exports HTTP status codes * exports HTTP status codes
*/ */
export default { const httpStatusCodes = {
ABORTED: 0, ABORTED: 0,
NO_CONTENT: 204,
OK: 200, OK: 200,
CREATED: 201,
ACCEPTED: 202,
NON_AUTHORITATIVE_INFORMATION: 203,
NO_CONTENT: 204,
RESET_CONTENT: 205,
PARTIAL_CONTENT: 206,
MULTI_STATUS: 207,
ALREADY_REPORTED: 208,
IM_USED: 226,
MULTIPLE_CHOICES: 300, MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400, BAD_REQUEST: 400,
NOT_FOUND: 404, NOT_FOUND: 404,
}; };
export const successCodes = [
httpStatusCodes.OK,
httpStatusCodes.CREATED,
httpStatusCodes.ACCEPTED,
httpStatusCodes.NON_AUTHORITATIVE_INFORMATION,
httpStatusCodes.NO_CONTENT,
httpStatusCodes.RESET_CONTENT,
httpStatusCodes.PARTIAL_CONTENT,
httpStatusCodes.MULTI_STATUS,
httpStatusCodes.ALREADY_REPORTED,
httpStatusCodes.IM_USED,
];
export default httpStatusCodes;
import httpStatusCodes from './http_status'; import httpStatusCodes, { successCodes } from './http_status';
import { normalizeHeaders } from './common_utils'; import { normalizeHeaders } from './common_utils';
/** /**
...@@ -62,8 +62,7 @@ export default class Poll { ...@@ -62,8 +62,7 @@ export default class Poll {
checkConditions(response) { checkConditions(response) {
const headers = normalizeHeaders(response.headers); const headers = normalizeHeaders(response.headers);
const pollInterval = parseInt(headers[this.intervalHeader], 10); const pollInterval = parseInt(headers[this.intervalHeader], 10);
if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) { if (pollInterval > 0 && successCodes.indexOf(response.status) !== -1 && this.canPoll) {
clearTimeout(this.timeoutID);
this.timeoutID = setTimeout(() => { this.timeoutID = setTimeout(() => {
this.makeRequest(); this.makeRequest();
}, pollInterval); }, pollInterval);
......
...@@ -75,6 +75,20 @@ export function capitalizeFirstCharacter(text) { ...@@ -75,6 +75,20 @@ export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`; return `${text[0].toUpperCase()}${text.slice(1)}`;
} }
/**
* Returns the first character capitalized
*
* If falsey, returns empty string.
*
* @param {String} text
* @return {String}
*/
export function getFirstCharacterCapitalized(text) {
return text
? text.charAt(0).toUpperCase()
: '';
}
/** /**
* Replaces all html tags from a string with the given replacement. * Replaces all html tags from a string with the given replacement.
* *
......
...@@ -5,20 +5,19 @@ import resolvedSvg from 'icons/_icon_status_success_solid.svg'; ...@@ -5,20 +5,19 @@ import resolvedSvg from 'icons/_icon_status_success_solid.svg';
import mrIssueSvg from 'icons/_icon_mr_issue.svg'; import mrIssueSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionSvg from 'icons/_next_discussion.svg'; import nextDiscussionSvg from 'icons/_next_discussion.svg';
import { pluralize } from '../../lib/utils/text_utility'; import { pluralize } from '../../lib/utils/text_utility';
import discussionNavigation from '../mixins/discussion_navigation'; import { scrollToElement } from '../../lib/utils/common_utils';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
mixins: [discussionNavigation],
computed: { computed: {
...mapGetters([ ...mapGetters([
'getUserData', 'getUserData',
'getNoteableData', 'getNoteableData',
'discussionCount', 'discussionCount',
'firstUnresolvedDiscussionId', 'unresolvedDiscussions',
'resolvedDiscussionCount', 'resolvedDiscussionCount',
]), ]),
isLoggedIn() { isLoggedIn() {
...@@ -36,6 +35,11 @@ export default { ...@@ -36,6 +35,11 @@ export default {
resolveAllDiscussionsIssuePath() { resolveAllDiscussionsIssuePath() {
return this.getNoteableData.create_issue_to_resolve_discussions_path; return this.getNoteableData.create_issue_to_resolve_discussions_path;
}, },
firstUnresolvedDiscussionId() {
const item = this.unresolvedDiscussions[0] || {};
return item.id;
},
}, },
created() { created() {
this.resolveSvg = resolveSvg; this.resolveSvg = resolveSvg;
...@@ -46,10 +50,22 @@ export default { ...@@ -46,10 +50,22 @@ export default {
methods: { methods: {
...mapActions(['expandDiscussion']), ...mapActions(['expandDiscussion']),
jumpToFirstUnresolvedDiscussion() { jumpToFirstUnresolvedDiscussion() {
const diffTab = window.mrTabs.currentAction === 'diffs'; const discussionId = this.firstUnresolvedDiscussionId;
const discussionId = this.firstUnresolvedDiscussionId(diffTab); if (!discussionId) {
return;
}
const el = document.querySelector(`[data-discussion-id="${discussionId}"]`);
const activeTab = window.mrTabs.currentAction;
if (activeTab === 'commits' || activeTab === 'pipelines') {
window.mrTabs.activateTab('show');
}
this.jumpToDiscussion(discussionId); if (el) {
this.expandDiscussion({ discussionId });
scrollToElement(el);
}
}, },
}, },
}; };
......
...@@ -7,7 +7,7 @@ import issuableStateMixin from '../mixins/issuable_state'; ...@@ -7,7 +7,7 @@ import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
export default { export default {
name: 'NoteForm', name: 'IssueNoteForm',
components: { components: {
issueWarning, issueWarning,
markdownField, markdownField,
......
...@@ -74,6 +74,9 @@ export default { ...@@ -74,6 +74,9 @@ export default {
</div> </div>
<a :href="author.path"> <a :href="author.path">
<span class="note-header-author-name">{{ author.name }}</span> <span class="note-header-author-name">{{ author.name }}</span>
<span
v-if="author.status_tooltip_html"
v-html="author.status_tooltip_html"></span>
<span class="note-headline-light"> <span class="note-headline-light">
@{{ author.username }} @{{ author.username }}
</span> </span>
......
<script> <script>
import _ from 'underscore';
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg'; import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionsSvg from 'icons/_next_discussion.svg'; import nextDiscussionsSvg from 'icons/_next_discussion.svg';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase, scrollToElement } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility'; import { truncateSha } from '~/lib/utils/text_utility';
import systemNote from '~/vue_shared/components/notes/system_note.vue'; import systemNote from '~/vue_shared/components/notes/system_note.vue';
import { s__ } from '~/locale';
import Flash from '../../flash'; import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants'; import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
...@@ -20,7 +20,6 @@ import placeholderSystemNote from '../../vue_shared/components/notes/placeholder ...@@ -20,7 +20,6 @@ import placeholderSystemNote from '../../vue_shared/components/notes/placeholder
import autosave from '../mixins/autosave'; import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable'; import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
import discussionNavigation from '../mixins/discussion_navigation';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
...@@ -40,7 +39,7 @@ export default { ...@@ -40,7 +39,7 @@ export default {
directives: { directives: {
tooltip, tooltip,
}, },
mixins: [autosave, noteable, resolvable, discussionNavigation], mixins: [autosave, noteable, resolvable],
props: { props: {
discussion: { discussion: {
type: Object, type: Object,
...@@ -61,11 +60,6 @@ export default { ...@@ -61,11 +60,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
discussionsByDiffOrder: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -80,12 +74,7 @@ export default { ...@@ -80,12 +74,7 @@ export default {
'discussionCount', 'discussionCount',
'resolvedDiscussionCount', 'resolvedDiscussionCount',
'allDiscussions', 'allDiscussions',
'unresolvedDiscussionsIdsByDiff',
'unresolvedDiscussionsIdsByDate',
'unresolvedDiscussions', 'unresolvedDiscussions',
'unresolvedDiscussionsIdsOrdered',
'nextUnresolvedDiscussionId',
'isLastUnresolvedDiscussion',
]), ]),
transformedDiscussion() { transformedDiscussion() {
return { return {
...@@ -136,10 +125,6 @@ export default { ...@@ -136,10 +125,6 @@ export default {
hasMultipleUnresolvedDiscussions() { hasMultipleUnresolvedDiscussions() {
return this.unresolvedDiscussions.length > 1; return this.unresolvedDiscussions.length > 1;
}, },
showJumpToNextDiscussion() {
return this.hasMultipleUnresolvedDiscussions &&
!this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder);
},
shouldRenderDiffs() { shouldRenderDiffs() {
const { diffDiscussion, diffFile } = this.transformedDiscussion; const { diffDiscussion, diffFile } = this.transformedDiscussion;
...@@ -159,17 +144,19 @@ export default { ...@@ -159,17 +144,19 @@ export default {
return this.isDiffDiscussion ? '' : 'card discussion-wrapper'; return this.isDiffDiscussion ? '' : 'card discussion-wrapper';
}, },
}, },
watch: { mounted() {
isReplying() { if (this.isReplying) {
if (this.isReplying) { this.initAutoSave(this.transformedDiscussion);
this.$nextTick(() => { }
// Pass an extra key to separate reply and note edit forms },
this.initAutoSave(this.transformedDiscussion, ['Reply']); updated() {
}); if (this.isReplying) {
if (!this.autosave) {
this.initAutoSave(this.transformedDiscussion);
} else { } else {
this.disposeAutoSave(); this.setAutoSave();
} }
}, }
}, },
created() { created() {
this.resolveDiscussionsSvg = resolveDiscussionsSvg; this.resolveDiscussionsSvg = resolveDiscussionsSvg;
...@@ -207,18 +194,16 @@ export default { ...@@ -207,18 +194,16 @@ export default {
showReplyForm() { showReplyForm() {
this.isReplying = true; this.isReplying = true;
}, },
cancelReplyForm(shouldConfirm, isDirty) { cancelReplyForm(shouldConfirm) {
if (shouldConfirm && isDirty) { if (shouldConfirm && this.$refs.noteForm.isDirty) {
const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (!window.confirm(msg)) { if (!window.confirm('Are you sure you want to cancel creating this comment?')) {
return; return;
} }
} }
this.isReplying = false;
this.resetAutoSave(); this.resetAutoSave();
this.isReplying = false;
}, },
saveReply(noteText, form, callback) { saveReply(noteText, form, callback) {
const postData = { const postData = {
...@@ -256,10 +241,21 @@ Please check your network connection and try again.`; ...@@ -256,10 +241,21 @@ Please check your network connection and try again.`;
}); });
}, },
jumpToNextDiscussion() { jumpToNextDiscussion() {
const nextId = const discussionIds = this.allDiscussions.map(d => d.id);
this.nextUnresolvedDiscussionId(this.discussion.id, this.discussionsByDiffOrder); const unresolvedIds = this.unresolvedDiscussions.map(d => d.id);
const currentIndex = discussionIds.indexOf(this.discussion.id);
const remainingAfterCurrent = discussionIds.slice(currentIndex + 1);
const nextIndex = _.findIndex(remainingAfterCurrent, id => unresolvedIds.indexOf(id) > -1);
if (nextIndex > -1) {
const nextId = remainingAfterCurrent[nextIndex];
const el = document.querySelector(`[data-discussion-id="${nextId}"]`);
this.jumpToDiscussion(nextId); if (el) {
this.expandDiscussion({ discussionId: nextId });
scrollToElement(el);
}
}
}, },
}, },
}; };
...@@ -401,7 +397,7 @@ Please check your network connection and try again.`; ...@@ -401,7 +397,7 @@ Please check your network connection and try again.`;
</a> </a>
</div> </div>
<div <div
v-if="showJumpToNextDiscussion" v-if="hasMultipleUnresolvedDiscussions"
class="btn-group" class="btn-group"
role="group"> role="group">
<button <button
...@@ -424,8 +420,7 @@ Please check your network connection and try again.`; ...@@ -424,8 +420,7 @@ Please check your network connection and try again.`;
:is-editing="false" :is-editing="false"
save-button-title="Comment" save-button-title="Comment"
@handleFormUpdate="saveReply" @handleFormUpdate="saveReply"
@cancelForm="cancelReplyForm" @cancelForm="cancelReplyForm" />
/>
<note-signed-out-widget v-if="!canReply" /> <note-signed-out-widget v-if="!canReply" />
</div> </div>
</div> </div>
......
...@@ -4,18 +4,12 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility'; ...@@ -4,18 +4,12 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export default { export default {
methods: { methods: {
initAutoSave(noteable, extraKeys = []) { initAutoSave(noteable) {
let keys = [ this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), [
'Note', 'Note',
capitalizeFirstCharacter(noteable.noteable_type || noteable.noteableType), capitalizeFirstCharacter(noteable.noteable_type),
noteable.id, noteable.id,
]; ]);
if (extraKeys) {
keys = keys.concat(extraKeys);
}
this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), keys);
}, },
resetAutoSave() { resetAutoSave() {
this.autosave.reset(); this.autosave.reset();
...@@ -23,8 +17,5 @@ export default { ...@@ -23,8 +17,5 @@ export default {
setAutoSave() { setAutoSave() {
this.autosave.save(); this.autosave.save();
}, },
disposeAutoSave() {
this.autosave.dispose();
},
}, },
}; };
import { scrollToElement } from '~/lib/utils/common_utils';
export default {
methods: {
jumpToDiscussion(id) {
if (id) {
const activeTab = window.mrTabs.currentAction;
const selector =
activeTab === 'diffs'
? `ul.notes[data-discussion-id="${id}"]`
: `div.discussion[data-discussion-id="${id}"]`;
const el = document.querySelector(selector);
if (activeTab === 'commits' || activeTab === 'pipelines') {
window.mrTabs.activateTab('show');
}
if (el) {
this.expandDiscussion({ discussionId: id });
scrollToElement(el);
return true;
}
}
return false;
},
},
};
...@@ -28,6 +28,18 @@ export const notesById = state => ...@@ -28,6 +28,18 @@ export const notesById = state =>
return acc; return acc;
}, {}); }, {});
export const discussionsByLineCode = state =>
state.discussions.reduce((acc, note) => {
if (note.diff_discussion && note.line_code && note.resolvable) {
// For context about line notes: there might be multiple notes with the same line code
const items = acc[note.line_code] || [];
items.push(note);
Object.assign(acc, { [note.line_code]: items });
}
return acc;
}, {});
export const 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;
...@@ -70,9 +82,6 @@ export const allDiscussions = (state, getters) => { ...@@ -70,9 +82,6 @@ export const allDiscussions = (state, getters) => {
return Object.values(resolved).concat(unresolved); return Object.values(resolved).concat(unresolved);
}; };
export const allResolvableDiscussions = (state, getters) =>
getters.allDiscussions.filter(d => !d.individual_note && d.resolvable);
export const resolvedDiscussionsById = state => { export const resolvedDiscussionsById = state => {
const map = {}; const map = {};
...@@ -89,51 +98,6 @@ export const resolvedDiscussionsById = state => { ...@@ -89,51 +98,6 @@ export const resolvedDiscussionsById = state => {
return map; return map;
}; };
// Gets Discussions IDs ordered by the date of their initial note
export const unresolvedDiscussionsIdsByDate = (state, getters) =>
getters.allResolvableDiscussions
.filter(d => !d.resolved)
.sort((a, b) => {
const aDate = new Date(a.notes[0].created_at);
const bDate = new Date(b.notes[0].created_at);
if (aDate < bDate) {
return -1;
}
return aDate === bDate ? 0 : 1;
})
.map(d => d.id);
// Gets Discussions IDs ordered by their position in the diff
//
// Sorts the array of resolvable yet unresolved discussions by
// comparing file names first. If file names are the same, compares
// line numbers.
export const unresolvedDiscussionsIdsByDiff = (state, getters) =>
getters.allResolvableDiscussions
.filter(d => !d.resolved)
.sort((a, b) => {
if (!a.diff_file || !b.diff_file) {
return 0;
}
// Get file names comparison result
const filenameComparison = a.diff_file.file_path.localeCompare(b.diff_file.file_path);
// Get the line numbers, to compare within the same file
const aLines = [a.position.formatter.new_line, a.position.formatter.old_line];
const bLines = [b.position.formatter.new_line, b.position.formatter.old_line];
return filenameComparison < 0 ||
(filenameComparison === 0 &&
// .max() because one of them might be zero (if removed/added)
Math.max(aLines[0], aLines[1]) < Math.max(bLines[0], bLines[1]))
? -1
: 1;
})
.map(d => d.id);
export const resolvedDiscussionCount = (state, getters) => { export const resolvedDiscussionCount = (state, getters) => {
const resolvedMap = getters.resolvedDiscussionsById; const resolvedMap = getters.resolvedDiscussionsById;
...@@ -150,42 +114,5 @@ export const discussionTabCounter = state => { ...@@ -150,42 +114,5 @@ export const discussionTabCounter = state => {
return all.length; return all.length;
}; };
// Returns the list of discussion IDs ordered according to given parameter
// @param {Boolean} diffOrder - is ordered by diff?
export const unresolvedDiscussionsIdsOrdered = (state, getters) => diffOrder => {
if (diffOrder) {
return getters.unresolvedDiscussionsIdsByDiff;
}
return getters.unresolvedDiscussionsIdsByDate;
};
// Checks if a given discussion is the last in the current order (diff or date)
// @param {Boolean} discussionId - id of the discussion
// @param {Boolean} diffOrder - is ordered by diff?
export const isLastUnresolvedDiscussion = (state, getters) => (discussionId, diffOrder) => {
const idsOrdered = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
const lastDiscussionId = idsOrdered[idsOrdered.length - 1];
return lastDiscussionId === discussionId;
};
// Gets the ID of the discussion following the one provided, respecting order (diff or date)
// @param {Boolean} discussionId - id of the current discussion
// @param {Boolean} diffOrder - is ordered by diff?
export const nextUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) => {
const idsOrdered = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
const currentIndex = idsOrdered.indexOf(discussionId);
return idsOrdered.slice(currentIndex + 1, currentIndex + 2)[0];
};
// @param {Boolean} diffOrder - is ordered by diff?
export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => {
if (diffOrder) {
return getters.unresolvedDiscussionsIdsByDiff[0];
}
return getters.unresolvedDiscussionsIdsByDate[0];
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -174,19 +174,27 @@ export default { ...@@ -174,19 +174,27 @@ export default {
[types.UPDATE_NOTE](state, note) { [types.UPDATE_NOTE](state, note) {
const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id); const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (noteObj.individual_note) { if (noteObj.individual_note) {
noteObj.notes.splice(0, 1, note); noteObj.notes.splice(0, 1, note);
} else { } else {
const comment = utils.findNoteObjectById(noteObj.notes, note.id); const comment = utils.findNoteObjectById(noteObj.notes, note.id);
Object.assign(comment, note); noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
} }
}, },
[types.UPDATE_DISCUSSION](state, noteData) { [types.UPDATE_DISCUSSION](state, noteData) {
const note = noteData; const note = noteData;
const selectedDiscussion = state.discussions.find(n => n.id === note.id); let index = 0;
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
Object.assign(selectedDiscussion, note); state.discussions.splice(index, 1, note);
}, },
[types.CLOSE_ISSUE](state) { [types.CLOSE_ISSUE](state) {
...@@ -207,9 +215,12 @@ export default { ...@@ -207,9 +215,12 @@ export default {
[types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) { [types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId); const discussion = utils.findNoteObjectById(state.discussions, discussionId);
const index = state.discussions.indexOf(discussion);
Object.assign(discussion, { const discussionWithDiffLines = Object.assign({}, discussion, {
truncated_diff_lines: diffLines, truncated_diff_lines: diffLines,
}); });
state.discussions.splice(index, 1, discussionWithDiffLines);
}, },
}; };
...@@ -2,11 +2,13 @@ import AjaxCache from '~/lib/utils/ajax_cache'; ...@@ -2,11 +2,13 @@ import AjaxCache from '~/lib/utils/ajax_cache';
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export const findNoteObjectById = (notes, id) => notes.find(n => n.id === id); export const findNoteObjectById = (notes, id) =>
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 = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || []; const quickActions =
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,4 +29,5 @@ export const getQuickActionText = note => { ...@@ -27,4 +29,5 @@ export const getQuickActionText = note => {
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note); export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim(); export const stripQuickActions = note =>
note.replace(REGEX_QUICK_ACTIONS, '').trim();
...@@ -14,7 +14,7 @@ import tooltip from '../../../vue_shared/directives/tooltip'; ...@@ -14,7 +14,7 @@ import tooltip from '../../../vue_shared/directives/tooltip';
* "id": 4256, * "id": 4256,
* "name": "test", * "name": "test",
* "status": { * "status": {
* "icon": "icon_status_success", * "icon": "status_success",
* "text": "passed", * "text": "passed",
* "label": "passed", * "label": "passed",
* "group": "success", * "group": "success",
......
...@@ -13,7 +13,7 @@ import tooltip from '../../../vue_shared/directives/tooltip'; ...@@ -13,7 +13,7 @@ import tooltip from '../../../vue_shared/directives/tooltip';
* "id": 4256, * "id": 4256,
* "name": "test", * "name": "test",
* "status": { * "status": {
* "icon": "icon_status_success", * "icon": "status_success",
* "text": "passed", * "text": "passed",
* "label": "passed", * "label": "passed",
* "group": "success", * "group": "success",
......
...@@ -137,7 +137,11 @@ export default class SearchAutocomplete { ...@@ -137,7 +137,11 @@ export default class SearchAutocomplete {
if (!term) { if (!term) {
const contents = this.getCategoryContents(); const contents = this.getCategoryContents();
if (contents) { if (contents) {
this.searchInput.data('glDropdown').filter.options.callback(contents); const glDropdownInstance = this.searchInput.data('glDropdown');
if (glDropdownInstance) {
glDropdownInstance.filter.options.callback(contents);
}
this.enableAutocomplete(); this.enableAutocomplete();
} }
return; return;
......
...@@ -31,6 +31,11 @@ export default { ...@@ -31,6 +31,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
gfm: {
type: String,
required: false,
default: null,
},
title: { title: {
type: String, type: String,
required: true, required: true,
...@@ -51,6 +56,14 @@ export default { ...@@ -51,6 +56,14 @@ export default {
default: 'btn-default', default: 'btn-default',
}, },
}, },
computed: {
clipboardText() {
if (this.gfm !== null) {
return JSON.stringify({ text: this.text, gfm: this.gfm });
}
return this.text;
},
},
}; };
</script> </script>
...@@ -59,7 +72,7 @@ export default { ...@@ -59,7 +72,7 @@ export default {
v-tooltip v-tooltip
:class="cssClass" :class="cssClass"
:title="title" :title="title"
:data-clipboard-text="text" :data-clipboard-text="clipboardText"
:data-container="tooltipContainer" :data-container="tooltipContainer"
:data-placement="tooltipPlacement" :data-placement="tooltipPlacement"
type="button" type="button"
......
...@@ -113,6 +113,9 @@ export default { ...@@ -113,6 +113,9 @@ export default {
{{ user.name }} {{ user.name }}
</a> </a>
<span
v-if="user.status_tooltip_html"
v-html="user.status_tooltip_html"></span>
</template> </template>
</section> </section>
......
<script> <script>
import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
export default { export default {
props: { props: {
entityId: { entityId: {
...@@ -16,26 +18,11 @@ export default { ...@@ -16,26 +18,11 @@ export default {
}, },
}, },
computed: { computed: {
/** identiconBackgroundClass() {
* This method is based on app/helpers/avatars_helper.rb#project_identicon return getIdenticonBackgroundClass(this.entityId);
*/
identiconStyles() {
const allowedColors = [
'#FFEBEE',
'#F3E5F5',
'#E8EAF6',
'#E3F2FD',
'#E0F2F1',
'#FBE9E7',
'#EEEEEE',
];
const backgroundColor = allowedColors[this.entityId % 7];
return `background-color: ${backgroundColor}; color: #555;`;
}, },
identiconTitle() { identiconTitle() {
return this.entityName.charAt(0).toUpperCase(); return getIdenticonTitle(this.entityName);
}, },
}, },
}; };
...@@ -43,8 +30,7 @@ export default { ...@@ -43,8 +30,7 @@ export default {
<template> <template>
<div <div
:class="sizeClass" :class="[sizeClass, identiconBackgroundClass]"
:style="identiconStyles"
class="avatar identicon"> class="avatar identicon">
{{ identiconTitle }} {{ identiconTitle }}
</div> </div>
......
...@@ -69,7 +69,10 @@ ...@@ -69,7 +69,10 @@
.identicon { .identicon {
text-align: center; text-align: center;
vertical-align: top; vertical-align: top;
color: $identicon-fg-color;
background-color: $identicon-gray;
// Sizes
&.s16 { font-size: 12px; line-height: 1.33; } &.s16 { font-size: 12px; line-height: 1.33; }
&.s24 { font-size: 13px; line-height: 1.8; } &.s24 { font-size: 13px; line-height: 1.8; }
&.s26 { font-size: 20px; line-height: 1.33; } &.s26 { font-size: 20px; line-height: 1.33; }
...@@ -82,6 +85,15 @@ ...@@ -82,6 +85,15 @@
&.s110 { font-size: 40px; line-height: 108px; font-weight: $gl-font-weight-normal; } &.s110 { font-size: 40px; line-height: 108px; font-weight: $gl-font-weight-normal; }
&.s140 { font-size: 72px; line-height: 138px; } &.s140 { font-size: 72px; line-height: 138px; }
&.s160 { font-size: 96px; line-height: 158px; } &.s160 { font-size: 96px; line-height: 158px; }
// Background colors
&.bg1 { background-color: $identicon-red; }
&.bg2 { background-color: $identicon-purple; }
&.bg3 { background-color: $identicon-indigo; }
&.bg4 { background-color: $identicon-blue; }
&.bg5 { background-color: $identicon-teal; }
&.bg6 { background-color: $identicon-orange; }
&.bg7 { background-color: $identicon-gray; }
} }
.avatar-container { .avatar-container {
......
...@@ -444,3 +444,5 @@ textarea { ...@@ -444,3 +444,5 @@ textarea {
color: $placeholder-text-color; color: $placeholder-text-color;
} }
} }
.lh-100 { line-height: 1; }
...@@ -486,6 +486,18 @@ $note-icon-gutter-width: 55px; ...@@ -486,6 +486,18 @@ $note-icon-gutter-width: 55px;
*/ */
$zen-control-color: #555; $zen-control-color: #555;
/*
* Identicon
*/
$identicon-red: #ffebee;
$identicon-purple: #f3e5f5;
$identicon-indigo: #e8eaf6;
$identicon-blue: #e3f2fd;
$identicon-teal: #e0f2f1;
$identicon-orange: #fbe9e7;
$identicon-gray: $gray-darker;
$identicon-fg-color: #555555;
/* /*
* Calendar * Calendar
*/ */
......
...@@ -205,7 +205,7 @@ ...@@ -205,7 +205,7 @@
.board-title { .board-title {
margin: 0; margin: 0;
padding: 12px $gl-padding; padding: $gl-padding-8 $gl-padding;
font-size: 1em; font-size: 1em;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
display: flex; display: flex;
......
.issue-count-badge { .issue-count-badge {
display: inline-flex; display: inline-flex;
align-items: stretch;
height: 24px;
}
.issue-count-badge-count {
display: flex;
align-items: center;
padding-right: 10px;
padding-left: 10px;
border: 1px solid $border-color;
border-radius: $border-radius-base; border-radius: $border-radius-base;
line-height: 1; border: 1px solid $border-color;
padding: 5px $gl-padding-8;
&.has-btn {
border-right: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
} }
.issue-count-badge-add-button { .issue-count-badge-count {
display: flex; display: inline-flex;
align-items: center; align-items: center;
border: 1px solid $border-color;
border-radius: 0 $border-radius-base $border-radius-base 0;
line-height: 1;
} }
...@@ -754,6 +754,11 @@ ...@@ -754,6 +754,11 @@
} }
} }
.repository-languages-bar {
height: 6px;
margin-bottom: 8px;
}
pre.light-well { pre.light-well {
border-color: $well-light-border; border-color: $well-light-border;
} }
......
...@@ -75,31 +75,27 @@ ...@@ -75,31 +75,27 @@
margin: 0; margin: 0;
.license-item { .license-item {
line-height: $gl-padding-24; line-height: $gl-padding-32;
.license-dependencies { .license-packages {
color: $gl-text-color-tertiary; font-size: $label-font-size;
} }
.btn-show-all-packages {
line-height: $gl-btn-line-height;
margin-bottom: 2px;
}
} }
} }
.report-block-list-icon { .report-block-list-icon {
display: flex; display: flex;
&.failed { &.failed svg {
color: $red-500; color: $red-500;
} }
&.success { &.success svg {
color: $green-500; color: $green-500;
} }
&.neutral { &.neutral svg {
color: $theme-gray-700; color: $theme-gray-700;
} }
......
...@@ -2,7 +2,7 @@ class Admin::JobsController < Admin::ApplicationController ...@@ -2,7 +2,7 @@ class Admin::JobsController < Admin::ApplicationController
def index def index
@scope = params[:scope] @scope = params[:scope]
@all_builds = Ci::Build @all_builds = Ci::Build
@builds = @all_builds.order('created_at DESC') @builds = @all_builds.order('id DESC')
@builds = @builds =
case @scope case @scope
when 'pending' when 'pending'
......
# frozen_string_literal: true
class Admin::ServicesController < Admin::ApplicationController class Admin::ServicesController < Admin::ApplicationController
include ServiceParams include ServiceParams
...@@ -30,7 +32,7 @@ class Admin::ServicesController < Admin::ApplicationController ...@@ -30,7 +32,7 @@ class Admin::ServicesController < Admin::ApplicationController
def services_templates def services_templates
Service.available_services_names.map do |service_name| Service.available_services_names.map do |service_name|
service_template = service_name.concat("_service").camelize.constantize service_template = "#{service_name}_service".camelize.constantize
service_template.where(template: true).first_or_create service_template.where(template: true).first_or_create
end end
end end
......
...@@ -397,7 +397,7 @@ class ApplicationController < ActionController::Base ...@@ -397,7 +397,7 @@ class ApplicationController < ActionController::Base
# actually stored in the session and a token is needed # actually stored in the session and a token is needed
# for every request. If you want the token to work as a # for every request. If you want the token to work as a
# sign in token, you can simply remove store: false. # sign in token, you can simply remove store: false.
sign_in user, store: false sign_in(user, store: false, message: :sessionless_sign_in)
end end
end end
......
...@@ -12,8 +12,9 @@ module Boards ...@@ -12,8 +12,9 @@ module Boards
skip_before_action :authenticate_user!, only: [:index] skip_before_action :authenticate_user!, only: [:index]
def index def index
issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
issues = issues.page(params[:page]).per(params[:per] || 20) issues = list_service.execute
issues = issues.page(params[:page]).per(params[:per] || 20).without_count
make_sure_position_is_set(issues) if Gitlab::Database.read_write? make_sure_position_is_set(issues) if Gitlab::Database.read_write?
issues = issues.preload(:project, issues = issues.preload(:project,
:milestone, :milestone,
...@@ -22,10 +23,7 @@ module Boards ...@@ -22,10 +23,7 @@ module Boards
notes: [:award_emoji, :author] notes: [:award_emoji, :author]
) )
render json: { render_issues(issues, list_service.metadata)
issues: serialize_as_json(issues),
size: issues.total_count
}
end end
def create def create
...@@ -51,6 +49,13 @@ module Boards ...@@ -51,6 +49,13 @@ module Boards
private private
def render_issues(issues, metadata)
data = { issues: serialize_as_json(issues) }
data.merge!(metadata)
render json: data
end
def make_sure_position_is_set(issues) def make_sure_position_is_set(issues)
issues.each do |issue| issues.each do |issue|
issue.move_to_end && issue.save unless issue.relative_position issue.move_to_end && issue.save unless issue.relative_position
......
...@@ -60,7 +60,7 @@ module AuthenticatesWithTwoFactor ...@@ -60,7 +60,7 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
user.save! user.save!
sign_in(user) sign_in(user, message: :two_factor_authenticated)
else else
user.increment_failed_attempts! user.increment_failed_attempts!
Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=OTP") Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=OTP")
...@@ -77,7 +77,7 @@ module AuthenticatesWithTwoFactor ...@@ -77,7 +77,7 @@ module AuthenticatesWithTwoFactor
session.delete(:challenge) session.delete(:challenge)
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
sign_in(user) sign_in(user, message: :two_factor_authenticated)
else else
user.increment_failed_attempts! user.increment_failed_attempts!
Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=U2F") Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=U2F")
......
...@@ -2,10 +2,18 @@ module MembersPresentation ...@@ -2,10 +2,18 @@ module MembersPresentation
extend ActiveSupport::Concern extend ActiveSupport::Concern
def present_members(members) def present_members(members)
preload_associations(members)
Gitlab::View::Presenter::Factory.new( Gitlab::View::Presenter::Factory.new(
members, members,
current_user: current_user, current_user: current_user,
presenter_class: MembersPresenter presenter_class: MembersPresenter
).fabricate! ).fabricate!
end end
def preload_associations(members)
ActiveRecord::Associations::Preloader.new.preload(members, :user)
ActiveRecord::Associations::Preloader.new.preload(members, :source)
ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status)
ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations)
end
end end
module MembershipActions module MembershipActions
include MembersPresentation
extend ActiveSupport::Concern extend ActiveSupport::Concern
def create def create
...@@ -20,6 +21,7 @@ module MembershipActions ...@@ -20,6 +21,7 @@ module MembershipActions
.execute(member) .execute(member)
.present(current_user: current_user) .present(current_user: current_user)
present_members([member])
respond_to do |format| respond_to do |format|
format.js { render 'shared/members/update', locals: { member: member } } format.js { render 'shared/members/update', locals: { member: member } }
end end
......
...@@ -41,7 +41,7 @@ module NotesActions ...@@ -41,7 +41,7 @@ module NotesActions
@note = Notes::CreateService.new(note_project, current_user, create_params).execute @note = Notes::CreateService.new(note_project, current_user, create_params).execute
if @note.is_a?(Note) if @note.is_a?(Note)
Notes::RenderService.new(current_user).execute([@note]) prepare_notes_for_rendering([@note], noteable)
end end
respond_to do |format| respond_to do |format|
...@@ -56,7 +56,7 @@ module NotesActions ...@@ -56,7 +56,7 @@ module NotesActions
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note) @note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note) if @note.is_a?(Note)
Notes::RenderService.new(current_user).execute([@note]) prepare_notes_for_rendering([@note])
end end
respond_to do |format| respond_to do |format|
......
...@@ -4,6 +4,7 @@ module RendersNotes ...@@ -4,6 +4,7 @@ module RendersNotes
preload_noteable_for_regular_notes(notes) preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project) preload_max_access_for_authors(notes, @project)
preload_first_time_contribution_for_authors(noteable, notes) preload_first_time_contribution_for_authors(noteable, notes)
preload_author_status(notes)
Notes::RenderService.new(current_user).execute(notes) Notes::RenderService.new(current_user).execute(notes)
notes notes
...@@ -28,4 +29,8 @@ module RendersNotes ...@@ -28,4 +29,8 @@ module RendersNotes
notes.each {|n| n.specialize_for_first_contribution!(noteable)} notes.each {|n| n.specialize_for_first_contribution!(noteable)}
end end
def preload_author_status(notes)
ActiveRecord::Associations::Preloader.new.preload(notes, { author: :status })
end
end end
...@@ -30,7 +30,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -30,7 +30,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end end
@members = @members.page(params[:page]).per(50) @members = @members.page(params[:page]).per(50)
@members = present_members(@members.includes(:user)) @members = present_members(@members)
@requesters = present_members( @requesters = present_members(
AccessRequestsFinder.new(@group).execute(current_user)) AccessRequestsFinder.new(@group).execute(current_user))
......
# frozen_string_literal: true
class InstanceStatistics::ApplicationController < ApplicationController
before_action :authorize_read_instance_statistics!
layout 'instance_statistics'
def authorize_read_instance_statistics!
render_404 unless can?(current_user, :read_instance_statistics)
end
end
class Admin::CohortsController < Admin::ApplicationController # frozen_string_literal: true
class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationController
def index def index
if Gitlab::CurrentSettings.usage_ping_enabled if Gitlab::CurrentSettings.usage_ping_enabled
cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
......
class Admin::ConversationalDevelopmentIndexController < Admin::ApplicationController # frozen_string_literal: true
def show
class InstanceStatistics::ConversationalDevelopmentIndexController < InstanceStatistics::ApplicationController
def index
@metric = ConversationalDevelopmentIndex::Metric.order(:created_at).last&.present @metric = ConversationalDevelopmentIndex::Metric.order(:created_at).last&.present
end end
end end
...@@ -54,6 +54,22 @@ class JwtController < ApplicationController ...@@ -54,6 +54,22 @@ class JwtController < ApplicationController
end end
def auth_params def auth_params
params.permit(:service, :scope, :account, :client_id) params.permit(:service, :account, :client_id)
.merge(additional_params)
end
def additional_params
{ scopes: scopes_param }.compact
end
# We have to parse scope here, because Docker Client does not send an array of scopes,
# but rather a flat list and we loose second scope when being processed by Rails:
# scope=scopeA&scope=scopeB
#
# This method makes to always return an array of scopes
def scopes_param
return unless params[:scope].present?
Array(Rack::Utils.parse_query(request.query_string)['scope'])
end end
end end
...@@ -100,7 +100,8 @@ class ProfilesController < Profiles::ApplicationController ...@@ -100,7 +100,8 @@ class ProfilesController < Profiles::ApplicationController
:website_url, :website_url,
:organization, :organization,
:preferred_language, :preferred_language,
:private_profile :private_profile,
status: [:emoji, :message]
) )
end end
end end
...@@ -22,7 +22,9 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -22,7 +22,9 @@ class Projects::CommitController < Projects::ApplicationController
apply_diff_view_cookie! apply_diff_view_cookie!
respond_to do |format| respond_to do |format|
format.html { render } format.html do
render
end
format.diff do format.diff do
send_git_diff(@project.repository, @commit.diff_refs) send_git_diff(@project.repository, @commit.diff_refs)
end end
...@@ -124,7 +126,10 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -124,7 +126,10 @@ class Projects::CommitController < Projects::ApplicationController
end end
def commit def commit
@noteable = @commit ||= @project.commit_by(oid: params[:id]) @noteable = @commit ||= @project.commit_by(oid: params[:id]).tap do |commit|
# preload author and their status for rendering
commit&.author&.status
end
end end
def define_commit_vars def define_commit_vars
......
...@@ -165,7 +165,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -165,7 +165,7 @@ class Projects::IssuesController < Projects::ApplicationController
return @issue if defined?(@issue) return @issue if defined?(@issue)
# The Sortable default scope causes performance issues when used with find_by # The Sortable default scope causes performance issues when used with find_by
@issuable = @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take! @issuable = @noteable = @issue ||= @project.issues.includes(author: :status).where(iid: params[:id]).reorder(nil).take!
@note = @project.notes.new(noteable: @issuable) @note = @project.notes.new(noteable: @issuable)
return render_404 unless can?(current_user, :read_issue, @issue) return render_404 unless can?(current_user, :read_issue, @issue)
......
...@@ -160,7 +160,10 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -160,7 +160,10 @@ class Projects::LabelsController < Projects::ApplicationController
def find_labels def find_labels
@available_labels ||= @available_labels ||=
LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: params[:include_ancestor_groups]).execute LabelsFinder.new(current_user,
project_id: @project.id,
include_ancestor_groups: params[:include_ancestor_groups],
search: params[:search]).execute
end end
def authorize_admin_labels! def authorize_admin_labels!
......
class Projects::LfsApiController < Projects::GitHttpClientController class Projects::LfsApiController < Projects::GitHttpClientController
include LfsRequest include LfsRequest
LFS_TRANSFER_CONTENT_TYPE = 'application/octet-stream'.freeze
skip_before_action :lfs_check_access!, only: [:deprecated] skip_before_action :lfs_check_access!, only: [:deprecated]
before_action :lfs_check_batch_operation!, only: [:batch] before_action :lfs_check_batch_operation!, only: [:batch]
...@@ -86,7 +88,10 @@ class Projects::LfsApiController < Projects::GitHttpClientController ...@@ -86,7 +88,10 @@ class Projects::LfsApiController < Projects::GitHttpClientController
upload: { upload: {
href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}", href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}",
header: { header: {
Authorization: request.headers['Authorization'] Authorization: request.headers['Authorization'],
# git-lfs v2.5.0 sets the Content-Type based on the uploaded file. This
# ensures that Workhorse can intercept the request.
'Content-Type': LFS_TRANSFER_CONTENT_TYPE
}.compact }.compact
} }
} }
......
...@@ -6,7 +6,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont ...@@ -6,7 +6,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
private private
def merge_request def merge_request
@issuable = @merge_request ||= @project.merge_requests.find_by!(iid: params[:id]) @issuable = @merge_request ||= @project.merge_requests.includes(author: :status).find_by!(iid: params[:id])
end end
def merge_request_params def merge_request_params
......
...@@ -13,7 +13,9 @@ class Projects::MirrorsController < Projects::ApplicationController ...@@ -13,7 +13,9 @@ class Projects::MirrorsController < Projects::ApplicationController
end end
def update def update
if project.update(mirror_params) result = ::Projects::UpdateService.new(project, current_user, mirror_params).execute
if result[:status] == :success
flash[:notice] = 'Mirroring settings were successfully updated.' flash[:notice] = 'Mirroring settings were successfully updated.'
else else
flash[:alert] = project.errors.full_messages.join(', ').html_safe flash[:alert] = project.errors.full_messages.join(', ').html_safe
......
class Projects::NotesController < Projects::ApplicationController class Projects::NotesController < Projects::ApplicationController
include RendersNotes
include NotesActions include NotesActions
include NotesHelper include NotesHelper
include ToggleAwardEmoji include ToggleAwardEmoji
...@@ -53,7 +54,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -53,7 +54,7 @@ class Projects::NotesController < Projects::ApplicationController
private private
def render_json_with_notes_serializer def render_json_with_notes_serializer
Notes::RenderService.new(current_user).execute([note]) prepare_notes_for_rendering([note])
render json: note_serializer.represent(note) render json: note_serializer.represent(note)
end end
......
class Projects::PipelinesController < Projects::ApplicationController class Projects::PipelinesController < Projects::ApplicationController
before_action :whitelist_query_limiting, only: [:create, :retry] before_action :whitelist_query_limiting, only: [:create, :retry]
before_action :pipeline, except: [:index, :new, :create, :charts] before_action :pipeline, except: [:index, :new, :create, :charts]
before_action :commit, only: [:show, :builds, :failures]
before_action :authorize_read_pipeline! before_action :authorize_read_pipeline!
before_action :authorize_create_pipeline!, only: [:new, :create] before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel] before_action :authorize_update_pipeline!, only: [:retry, :cancel]
...@@ -161,11 +160,11 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -161,11 +160,11 @@ class Projects::PipelinesController < Projects::ApplicationController
end end
def pipeline def pipeline
@pipeline ||= project.pipelines.find_by!(id: params[:id]).present(current_user: current_user) @pipeline ||= project
end .pipelines
.includes(user: :status)
def commit .find_by!(id: params[:id])
@commit ||= @pipeline.commit .present(current_user: current_user)
end end
def whitelist_query_limiting def whitelist_query_limiting
......
...@@ -88,7 +88,7 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -88,7 +88,7 @@ class Projects::SnippetsController < Projects::ApplicationController
protected protected
def snippet def snippet
@snippet ||= @project.snippets.find(params[:id]) @snippet ||= @project.snippets.inc_relations_for_view.find(params[:id])
end end
alias_method :awardable, :snippet alias_method :awardable, :snippet
alias_method :spammable, :snippet alias_method :spammable, :snippet
......
class Projects::WikisController < Projects::ApplicationController class Projects::WikisController < Projects::ApplicationController
include PreviewMarkdown include PreviewMarkdown
include Gitlab::Utils::StrongMemoize
before_action :authorize_read_wiki! before_action :authorize_read_wiki!
before_action :authorize_create_wiki!, only: [:edit, :create, :history] before_action :authorize_create_wiki!, only: [:edit, :create, :history]
before_action :authorize_admin_wiki!, only: :destroy before_action :authorize_admin_wiki!, only: :destroy
before_action :load_project_wiki before_action :load_project_wiki
before_action :load_page, only: [:show, :edit, :update, :history, :destroy]
before_action :valid_encoding?, only: [:show, :edit, :update], if: :load_page
before_action only: [:edit, :update], unless: :valid_encoding? do
redirect_to(project_wiki_path(@project, @page))
end
def pages def pages
@wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]) @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page])
...@@ -12,11 +18,11 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -12,11 +18,11 @@ class Projects::WikisController < Projects::ApplicationController
end end
def show def show
@page = @project_wiki.find_page(params[:id], params[:version_id])
view_param = @project_wiki.empty? ? params[:view] : 'create' view_param = @project_wiki.empty? ? params[:view] : 'create'
if @page if @page
set_encoding_error unless valid_encoding?
render 'show' render 'show'
elsif file = @project_wiki.find_file(params[:id], params[:version_id]) elsif file = @project_wiki.find_file(params[:id], params[:version_id])
response.headers['Content-Security-Policy'] = "default-src 'none'" response.headers['Content-Security-Policy'] = "default-src 'none'"
...@@ -38,13 +44,11 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -38,13 +44,11 @@ class Projects::WikisController < Projects::ApplicationController
end end
def edit def edit
@page = @project_wiki.find_page(params[:id])
end end
def update def update
return render('empty') unless can?(current_user, :create_wiki, @project) return render('empty') unless can?(current_user, :create_wiki, @project)
@page = @project_wiki.find_page(params[:id])
@page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page) @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
if @page.valid? if @page.valid?
...@@ -79,8 +83,6 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -79,8 +83,6 @@ class Projects::WikisController < Projects::ApplicationController
end end
def history def history
@page = @project_wiki.find_page(params[:id])
if @page if @page
@page_versions = Kaminari.paginate_array(@page.versions(page: params[:page].to_i), @page_versions = Kaminari.paginate_array(@page.versions(page: params[:page].to_i),
total_count: @page.count_versions) total_count: @page.count_versions)
...@@ -94,8 +96,6 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -94,8 +96,6 @@ class Projects::WikisController < Projects::ApplicationController
end end
def destroy def destroy
@page = @project_wiki.find_page(params[:id])
WikiPages::DestroyService.new(@project, current_user).execute(@page) WikiPages::DestroyService.new(@project, current_user).execute(@page)
redirect_to project_wiki_path(@project, :home), redirect_to project_wiki_path(@project, :home),
...@@ -141,4 +141,25 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -141,4 +141,25 @@ class Projects::WikisController < Projects::ApplicationController
page.update_attributes(args) # rubocop:disable Rails/ActiveRecordAliases page.update_attributes(args) # rubocop:disable Rails/ActiveRecordAliases
end end
end end
def load_page
@page ||= @project_wiki.find_page(*page_params)
end
def page_params
keys = [:id]
keys << :version_id if params[:action] == 'show'
params.values_at(*keys)
end
def valid_encoding?
strong_memoize(:valid_encoding) do
@page.content.encoding == Encoding::UTF_8
end
end
def set_encoding_error
flash.now[:notice] = "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository."
end
end end
...@@ -89,6 +89,14 @@ class SessionsController < Devise::SessionsController ...@@ -89,6 +89,14 @@ class SessionsController < Devise::SessionsController
).increment ).increment
end end
##
# We do have some duplication between lib/gitlab/auth/activity.rb here, but
# leaving this method here because of backwards compatibility.
#
def login_counter
@login_counter ||= Gitlab::Metrics.counter(:user_session_logins_total, 'User sign in count')
end
def log_failed_login def log_failed_login
Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}") Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}")
end end
...@@ -97,10 +105,6 @@ class SessionsController < Devise::SessionsController ...@@ -97,10 +105,6 @@ class SessionsController < Devise::SessionsController
(options = env["warden.options"]) && options[:action] == "unauthenticated" (options = env["warden.options"]) && options[:action] == "unauthenticated"
end end
def login_counter
@login_counter ||= Gitlab::Metrics.counter(:user_session_logins_total, 'User sign in count')
end
# Handle an "initial setup" state, where there's only one user, it's an admin, # Handle an "initial setup" state, where there's only one user, it's an admin,
# and they require a password change. # and they require a password change.
def check_initial_setup def check_initial_setup
......
...@@ -9,7 +9,7 @@ class Snippets::NotesController < ApplicationController ...@@ -9,7 +9,7 @@ class Snippets::NotesController < ApplicationController
private private
def note def note
@note ||= snippet.notes.find(params[:id]) @note ||= snippet.notes.inc_relations_for_view.find(params[:id])
end end
alias_method :awardable, :note alias_method :awardable, :note
......
...@@ -95,7 +95,7 @@ class SnippetsController < ApplicationController ...@@ -95,7 +95,7 @@ class SnippetsController < ApplicationController
protected protected
def snippet def snippet
@snippet ||= PersonalSnippet.find_by(id: params[:id]) @snippet ||= PersonalSnippet.inc_relations_for_view.find_by(id: params[:id])
end end
alias_method :awardable, :snippet alias_method :awardable, :snippet
......
...@@ -130,7 +130,7 @@ class IssuableFinder ...@@ -130,7 +130,7 @@ class IssuableFinder
counts[:all] = counts.values.sum counts[:all] = counts.values.sum
counts counts.with_indifferent_access
end end
def group def group
......
...@@ -14,6 +14,7 @@ class LabelsFinder < UnionFinder ...@@ -14,6 +14,7 @@ class LabelsFinder < UnionFinder
@skip_authorization = skip_authorization @skip_authorization = skip_authorization
items = find_union(label_ids, Label) || Label.none items = find_union(label_ids, Label) || Label.none
items = with_title(items) items = with_title(items)
items = by_search(items)
sort(items) sort(items)
end end
...@@ -63,6 +64,12 @@ class LabelsFinder < UnionFinder ...@@ -63,6 +64,12 @@ class LabelsFinder < UnionFinder
items.where(title: title) items.where(title: title)
end end
def by_search(labels)
return labels unless search?
labels.search(params[:search])
end
# Gets redacted array of group ids # Gets redacted array of group ids
# which can include the ancestors and descendants of the requested group. # which can include the ancestors and descendants of the requested group.
def group_ids_for(group) def group_ids_for(group)
...@@ -106,6 +113,10 @@ class LabelsFinder < UnionFinder ...@@ -106,6 +113,10 @@ class LabelsFinder < UnionFinder
params[:only_group_labels] params[:only_group_labels]
end end
def search?
params[:search].present?
end
def title def title
params[:title] || params[:name] params[:title] || params[:name]
end end
......
...@@ -148,6 +148,7 @@ module ApplicationSettingsHelper ...@@ -148,6 +148,7 @@ module ApplicationSettingsHelper
:after_sign_up_text, :after_sign_up_text,
:akismet_api_key, :akismet_api_key,
:akismet_enabled, :akismet_enabled,
:allow_local_requests_from_hooks_and_services,
:authorized_keys_enabled, :authorized_keys_enabled,
:auto_devops_enabled, :auto_devops_enabled,
:auto_devops_domain, :auto_devops_domain,
...@@ -174,6 +175,7 @@ module ApplicationSettingsHelper ...@@ -174,6 +175,7 @@ module ApplicationSettingsHelper
:ed25519_key_restriction, :ed25519_key_restriction,
:email_author_in_body, :email_author_in_body,
:enabled_git_access_protocol, :enabled_git_access_protocol,
:enforce_terms,
:gitaly_timeout_default, :gitaly_timeout_default,
:gitaly_timeout_medium, :gitaly_timeout_medium,
:gitaly_timeout_fast, :gitaly_timeout_fast,
...@@ -182,6 +184,7 @@ module ApplicationSettingsHelper ...@@ -182,6 +184,7 @@ module ApplicationSettingsHelper
:help_page_hide_commercial_content, :help_page_hide_commercial_content,
:help_page_support_url, :help_page_support_url,
:help_page_text, :help_page_text,
:hide_third_party_offers,
:home_page_url, :home_page_url,
:housekeeping_bitmaps_enabled, :housekeeping_bitmaps_enabled,
:housekeeping_enabled, :housekeeping_enabled,
...@@ -203,6 +206,7 @@ module ApplicationSettingsHelper ...@@ -203,6 +206,7 @@ module ApplicationSettingsHelper
:metrics_port, :metrics_port,
:metrics_sample_interval, :metrics_sample_interval,
:metrics_timeout, :metrics_timeout,
:mirror_available,
:pages_domain_verification_enabled, :pages_domain_verification_enabled,
:password_authentication_enabled_for_web, :password_authentication_enabled_for_web,
:password_authentication_enabled_for_git, :password_authentication_enabled_for_git,
...@@ -233,28 +237,25 @@ module ApplicationSettingsHelper ...@@ -233,28 +237,25 @@ module ApplicationSettingsHelper
:sign_in_text, :sign_in_text,
:signup_enabled, :signup_enabled,
:terminal_max_session_time, :terminal_max_session_time,
:throttle_unauthenticated_enabled, :terms,
:throttle_unauthenticated_requests_per_period,
:throttle_unauthenticated_period_in_seconds,
:throttle_authenticated_web_enabled,
:throttle_authenticated_web_requests_per_period,
:throttle_authenticated_web_period_in_seconds,
:throttle_authenticated_api_enabled, :throttle_authenticated_api_enabled,
:throttle_authenticated_api_requests_per_period,
:throttle_authenticated_api_period_in_seconds, :throttle_authenticated_api_period_in_seconds,
:throttle_authenticated_api_requests_per_period,
:throttle_authenticated_web_enabled,
:throttle_authenticated_web_period_in_seconds,
:throttle_authenticated_web_requests_per_period,
:throttle_unauthenticated_enabled,
:throttle_unauthenticated_period_in_seconds,
:throttle_unauthenticated_requests_per_period,
:two_factor_grace_period, :two_factor_grace_period,
:unique_ips_limit_enabled, :unique_ips_limit_enabled,
:unique_ips_limit_per_user, :unique_ips_limit_per_user,
:unique_ips_limit_time_window, :unique_ips_limit_time_window,
:usage_ping_enabled, :usage_ping_enabled,
:instance_statistics_visibility_private,
:user_default_external, :user_default_external,
:user_oauth_applications, :user_oauth_applications,
:version_check_enabled, :version_check_enabled
:allow_local_requests_from_hooks_and_services,
:hide_third_party_offers,
:enforce_terms,
:terms,
:mirror_available
] ]
end end
end end
...@@ -15,22 +15,12 @@ module AvatarsHelper ...@@ -15,22 +15,12 @@ module AvatarsHelper
end end
def project_identicon(project, options = {}) def project_identicon(project, options = {})
allowed_colors = { bg_key = (project.id % 7) + 1
red: 'FFEBEE',
purple: 'F3E5F5',
indigo: 'E8EAF6',
blue: 'E3F2FD',
teal: 'E0F2F1',
orange: 'FBE9E7',
gray: 'EEEEEE'
}
options[:class] ||= '' options[:class] ||= ''
options[:class] << ' identicon' options[:class] << ' identicon'
bg_key = project.id % 7 options[:class] << " bg#{bg_key}"
style = "background-color: ##{allowed_colors.values[bg_key]}; color: #555"
content_tag(:div, class: options[:class], style: style) do content_tag(:div, class: options[:class]) do
project.name[0, 1].upcase project.name[0, 1].upcase
end end
end end
......
...@@ -159,6 +159,12 @@ module IssuablesHelper ...@@ -159,6 +159,12 @@ module IssuablesHelper
output << content_tag(:strong) do output << content_tag(:strong) do
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline", tooltip: true) author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline", tooltip: true)
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-block d-sm-none") author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-block d-sm-none")
if status = user_status(issuable.author)
author_output << "&ensp; #{status}".html_safe
end
author_output
end end
output << "&ensp;".html_safe output << "&ensp;".html_safe
......
...@@ -9,4 +9,8 @@ module ProfilesHelper ...@@ -9,4 +9,8 @@ module ProfilesHelper
end end
end end
end end
def show_user_status_field?
Feature.enabled?(:user_status_form) || cookies[:feature_user_status_form] == 'true'
end
end end
module RepositoryLanguagesHelper
def repository_languages_bar(languages)
return if languages.none?
content_tag :div, class: 'progress repository-languages-bar' do
safe_join(languages.map { |lang| language_progress(lang) })
end
end
def language_progress(lang)
content_tag :div, nil,
class: "progress-bar has-tooltip",
style: "width: #{lang.share}%; background-color:#{lang.color}",
title: lang.name
end
end
...@@ -39,6 +39,24 @@ module UsersHelper ...@@ -39,6 +39,24 @@ module UsersHelper
"access:#{max_project_member_access(project)}" "access:#{max_project_member_access(project)}"
end end
def user_status(user)
return unless user
unless user.association(:status).loaded?
exception = RuntimeError.new("Status was not preloaded")
Gitlab::Sentry.track_exception(exception, extra: { user: user.inspect })
end
return unless user.status
content_tag :span,
class: 'user-status-emoji has-tooltip',
title: user.status.message_html,
data: { html: true, placement: 'top' } do
emoji_icon user.status.emoji
end
end
private private
def get_profile_tabs def get_profile_tabs
......
# frozen_string_literal: true
require_dependency 'declarative_policy' require_dependency 'declarative_policy'
class Ability class Ability
......
# frozen_string_literal: true
class AbuseReport < ActiveRecord::Base class AbuseReport < ActiveRecord::Base
include CacheMarkdownField include CacheMarkdownField
......
# frozen_string_literal: true
class ActiveSession class ActiveSession
include ActiveModel::Model include ActiveModel::Model
......
# frozen_string_literal: true
class Appearance < ActiveRecord::Base class Appearance < ActiveRecord::Base
include CacheableAttributes include CacheableAttributes
include CacheMarkdownField include CacheMarkdownField
......
# frozen_string_literal: true
class ApplicationSetting < ActiveRecord::Base class ApplicationSetting < ActiveRecord::Base
include CacheableAttributes include CacheableAttributes
include CacheMarkdownField include CacheMarkdownField
...@@ -228,25 +230,27 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -228,25 +230,27 @@ class ApplicationSetting < ActiveRecord::Base
{ {
after_sign_up_text: nil, after_sign_up_text: nil,
akismet_enabled: false, akismet_enabled: false,
allow_local_requests_from_hooks_and_services: false,
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
container_registry_token_expire_delay: 5, container_registry_token_expire_delay: 5,
default_artifacts_expire_in: '30 days', default_artifacts_expire_in: '30 days',
default_branch_protection: Settings.gitlab['default_branch_protection'], default_branch_protection: Settings.gitlab['default_branch_protection'],
default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_projects_limit: Settings.gitlab['default_projects_limit'], default_projects_limit: Settings.gitlab['default_projects_limit'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'],
disabled_oauth_sign_in_sources: [], disabled_oauth_sign_in_sources: [],
domain_whitelist: Settings.gitlab['domain_whitelist'], domain_whitelist: Settings.gitlab['domain_whitelist'],
dsa_key_restriction: 0, dsa_key_restriction: 0,
ecdsa_key_restriction: 0, ecdsa_key_restriction: 0,
ed25519_key_restriction: 0, ed25519_key_restriction: 0,
gitaly_timeout_default: 55,
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
gravatar_enabled: Settings.gravatar['enabled'], gravatar_enabled: Settings.gravatar['enabled'],
help_page_text: nil,
help_page_hide_commercial_content: false, help_page_hide_commercial_content: false,
unique_ips_limit_per_user: 10, help_page_text: nil,
unique_ips_limit_time_window: 3600, hide_third_party_offers: false,
unique_ips_limit_enabled: false,
housekeeping_bitmaps_enabled: true, housekeeping_bitmaps_enabled: true,
housekeeping_enabled: true, housekeeping_enabled: true,
housekeeping_full_repack_period: 50, housekeeping_full_repack_period: 50,
...@@ -257,12 +261,14 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -257,12 +261,14 @@ class ApplicationSetting < ActiveRecord::Base
koding_url: nil, koding_url: nil,
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'], max_attachment_size: Settings.gitlab['max_attachment_size'],
password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'], mirror_available: true,
password_authentication_enabled_for_git: true, password_authentication_enabled_for_git: true,
password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'],
performance_bar_allowed_group_id: nil, performance_bar_allowed_group_id: nil,
rsa_key_restriction: 0, rsa_key_restriction: 0,
plantuml_enabled: false, plantuml_enabled: false,
plantuml_url: nil, plantuml_url: nil,
polling_interval_multiplier: 1,
project_export_enabled: true, project_export_enabled: true,
recaptcha_enabled: false, recaptcha_enabled: false,
repository_checks_enabled: true, repository_checks_enabled: true,
...@@ -277,25 +283,22 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -277,25 +283,22 @@ class ApplicationSetting < ActiveRecord::Base
sign_in_text: nil, sign_in_text: nil,
signup_enabled: Settings.gitlab['signup_enabled'], signup_enabled: Settings.gitlab['signup_enabled'],
terminal_max_session_time: 0, terminal_max_session_time: 0,
throttle_unauthenticated_enabled: false,
throttle_unauthenticated_requests_per_period: 3600,
throttle_unauthenticated_period_in_seconds: 3600,
throttle_authenticated_web_enabled: false,
throttle_authenticated_web_requests_per_period: 7200,
throttle_authenticated_web_period_in_seconds: 3600,
throttle_authenticated_api_enabled: false, throttle_authenticated_api_enabled: false,
throttle_authenticated_api_requests_per_period: 7200,
throttle_authenticated_api_period_in_seconds: 3600, throttle_authenticated_api_period_in_seconds: 3600,
throttle_authenticated_api_requests_per_period: 7200,
throttle_authenticated_web_enabled: false,
throttle_authenticated_web_period_in_seconds: 3600,
throttle_authenticated_web_requests_per_period: 7200,
throttle_unauthenticated_enabled: false,
throttle_unauthenticated_period_in_seconds: 3600,
throttle_unauthenticated_requests_per_period: 3600,
two_factor_grace_period: 48, two_factor_grace_period: 48,
user_default_external: false, unique_ips_limit_enabled: false,
polling_interval_multiplier: 1, unique_ips_limit_per_user: 10,
unique_ips_limit_time_window: 3600,
usage_ping_enabled: Settings.gitlab['usage_ping_enabled'], usage_ping_enabled: Settings.gitlab['usage_ping_enabled'],
gitaly_timeout_fast: 10, instance_statistics_visibility_private: false,
gitaly_timeout_medium: 30, user_default_external: false
gitaly_timeout_default: 55,
allow_local_requests_from_hooks_and_services: false,
hide_third_party_offers: false,
mirror_available: true
} }
end end
......
# frozen_string_literal: true
class AuditEvent < ActiveRecord::Base class AuditEvent < ActiveRecord::Base
serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize
......
# frozen_string_literal: true
class AwardEmoji < ActiveRecord::Base class AwardEmoji < ActiveRecord::Base
DOWNVOTE_NAME = "thumbsdown".freeze DOWNVOTE_NAME = "thumbsdown".freeze
UPVOTE_NAME = "thumbsup".freeze UPVOTE_NAME = "thumbsup".freeze
......
# frozen_string_literal: true
class Badge < ActiveRecord::Base class Badge < ActiveRecord::Base
# This structure sets the placeholders that the urls # This structure sets the placeholders that the urls
# can have. This hash also sets which action to ask when # can have. This hash also sets which action to ask when
......
# frozen_string_literal: true
# Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects # Blob is a Rails-specific wrapper around Gitlab::Git::Blob objects
class Blob < SimpleDelegator class Blob < SimpleDelegator
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
......
# frozen_string_literal: true
class Board < ActiveRecord::Base class Board < ActiveRecord::Base
belongs_to :group belongs_to :group
belongs_to :project belongs_to :project
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment