Commit f3a427b5 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'master' into pawel/prometheus-business-metrics-ee-2273

parents 9fb3d302 6d49d141
...@@ -11,8 +11,8 @@ engines: ...@@ -11,8 +11,8 @@ engines:
exclude_paths: exclude_paths:
- "lib/api/v3/*" - "lib/api/v3/*"
eslint: eslint:
# eslint-plugin-vue is locked to version 2 in codeclimate, we need version 4 enabled: true
enabled: false channel: "eslint-4"
rubocop: rubocop:
enabled: true enabled: true
channel: "gitlab-rubocop-0-52-1" channel: "gitlab-rubocop-0-52-1"
...@@ -45,3 +45,4 @@ exclude_paths: ...@@ -45,3 +45,4 @@ exclude_paths:
- log/ - log/
- backups/ - backups/
- coverage-javascript/ - coverage-javascript/
- plugins/
...@@ -67,3 +67,4 @@ eslint-report.html ...@@ -67,3 +67,4 @@ eslint-report.html
/locale/**/LC_MESSAGES /locale/**/LC_MESSAGES
/locale/**/*.time_stamp /locale/**/*.time_stamp
/.rspec /.rspec
/plugins/*
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6" image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
.dedicated-runner: &dedicated-runner .dedicated-runner: &dedicated-runner
retry: 1 retry: 1
......
<!---
Please read this! Please read this!
Before opening a new issue, make sure to search for keywords in the issues Before opening a new issue, make sure to search for keywords in the issues
...@@ -14,10 +15,7 @@ For the Enterprise Edition issue tracker: ...@@ -14,10 +15,7 @@ For the Enterprise Edition issue tracker:
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=bug - https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=bug
and verify the issue you're about to submit isn't a duplicate. and verify the issue you're about to submit isn't a duplicate.
--->
Please remove this notice if you're confident your issue isn't a duplicate.
------
### Summary ### Summary
......
...@@ -17,6 +17,7 @@ AllCops: ...@@ -17,6 +17,7 @@ AllCops:
- 'bin/**/*' - 'bin/**/*'
- 'generator_templates/**/*' - 'generator_templates/**/*'
- 'builds/**/*' - 'builds/**/*'
- 'plugins/**/*'
CacheRootDirectory: tmp CacheRootDirectory: tmp
# This cop checks whether some constant value isn't a # This cop checks whether some constant value isn't a
......
...@@ -124,8 +124,8 @@ Lint/DuplicateMethods: ...@@ -124,8 +124,8 @@ Lint/DuplicateMethods:
- 'lib/gitlab/git/repository.rb' - 'lib/gitlab/git/repository.rb'
- 'lib/gitlab/git/tree.rb' - 'lib/gitlab/git/tree.rb'
- 'lib/gitlab/git/wiki_page.rb' - 'lib/gitlab/git/wiki_page.rb'
- 'lib/gitlab/ldap/person.rb' - 'lib/gitlab/auth/ldap/person.rb'
- 'lib/gitlab/o_auth/user.rb' - 'lib/gitlab/auth/o_auth/user.rb'
# Offense count: 4 # Offense count: 4
Lint/InterpolationCheck: Lint/InterpolationCheck:
...@@ -812,7 +812,7 @@ Style/TrivialAccessors: ...@@ -812,7 +812,7 @@ Style/TrivialAccessors:
Exclude: Exclude:
- 'app/models/external_issue.rb' - 'app/models/external_issue.rb'
- 'app/serializers/base_serializer.rb' - 'app/serializers/base_serializer.rb'
- 'lib/gitlab/ldap/person.rb' - 'lib/gitlab/auth/ldap/person.rb'
- 'lib/system_check/base_check.rb' - 'lib/system_check/base_check.rb'
# Offense count: 4 # Offense count: 4
......
...@@ -1064,7 +1064,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -1064,7 +1064,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Adds abitlity to render deploy boards in the frontend side. !1233 - Adds abitlity to render deploy boards in the frontend side. !1233
- Add filtered search to MR page. !1243 - Add filtered search to MR page. !1243
- Update project list API returns with approvals_before_merge attribute. !1245 (Geoff Webster) - Update project list API returns with approvals_before_merge attribute. !1245 (Geoff Webster)
- Catch Net::LDAP::DN exceptions in EE::Gitlab::LDAP::Group. !1260 - Catch Net::LDAP::DN exceptions in EE::Gitlab::Auth::LDAP::Group. !1260
- API: Use `post ":id/#{type}/:subscribable_id/subscribe"` to subscribe and `post ":id/#{type}/:subscribable_id/unsubscribe"` to unsubscribe from a resource. !1274 (Robert Schilling) - API: Use `post ":id/#{type}/:subscribable_id/subscribe"` to subscribe and `post ":id/#{type}/:subscribable_id/unsubscribe"` to unsubscribe from a resource. !1274 (Robert Schilling)
- API: Remove deprecated fields Notes#upvotes and Notes#downvotes. !1275 (Robert Schilling) - API: Remove deprecated fields Notes#upvotes and Notes#downvotes. !1275 (Robert Schilling)
- Deploy board backend. !1278 - Deploy board backend. !1278
......
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
import AccessorUtilities from './lib/utils/accessor'; import AccessorUtilities from './lib/utils/accessor';
export default class Autosave { export default class Autosave {
constructor(field, key, resource) { constructor(field, key) {
this.field = field; this.field = field;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.resource = resource;
if (key.join != null) { if (key.join != null) {
key = key.join('/'); key = key.join('/');
} }
...@@ -17,31 +17,27 @@ export default class Autosave { ...@@ -17,31 +17,27 @@ export default class Autosave {
} }
restore() { restore() {
var text;
if (!this.isLocalStorageAvailable) return; if (!this.isLocalStorageAvailable) return;
if (!this.field.length) return;
text = window.localStorage.getItem(this.key); const text = window.localStorage.getItem(this.key);
if ((text != null ? text.length : void 0) > 0) { if ((text != null ? text.length : void 0) > 0) {
this.field.val(text); this.field.val(text);
} }
if (!this.resource && this.resource !== 'issue') {
this.field.trigger('input'); this.field.trigger('input');
} else { // v-model does not update with jQuery trigger
// v-model does not update with jQuery trigger // https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
// https://github.com/vuejs/vue/issues/2804#issuecomment-216968137 const event = new Event('change', { bubbles: true, cancelable: false });
const event = new Event('change', { bubbles: true, cancelable: false }); const field = this.field.get(0);
const field = this.field.get(0); field.dispatchEvent(event);
if (field) {
field.dispatchEvent(event);
}
}
} }
save() { save() {
var text; if (!this.field.length) return;
text = this.field.val();
const text = this.field.val();
if (this.isLocalStorageAvailable && (text != null ? text.length : void 0) > 0) { if (this.isLocalStorageAvailable && (text != null ? text.length : void 0) > 0) {
return window.localStorage.setItem(this.key, text); return window.localStorage.setItem(this.key, text);
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { __ } from './locale'; import { __ } from './locale';
import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils'; import { isInIssuePage, isInMRPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils';
import flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
...@@ -239,9 +239,9 @@ class AwardsHandler { ...@@ -239,9 +239,9 @@ class AwardsHandler {
} }
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
const isMainAwardsBlock = votesBlock.closest('.js-issue-note-awards').length; const isMainAwardsBlock = votesBlock.closest('.js-noteable-awards').length;
if (isInIssuePage() && !isMainAwardsBlock) { if (this.isInVueNoteablePage() && !isMainAwardsBlock) {
const id = votesBlock.attr('id').replace('note_', ''); const id = votesBlock.attr('id').replace('note_', '');
this.hideMenuElement($('.emoji-menu')); this.hideMenuElement($('.emoji-menu'));
...@@ -293,8 +293,16 @@ class AwardsHandler { ...@@ -293,8 +293,16 @@ class AwardsHandler {
} }
} }
isVueMRDiscussions() {
return isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
}
isInVueNoteablePage() {
return isInIssuePage() || this.isVueMRDiscussions();
}
getVotesBlock() { getVotesBlock() {
if (isInIssuePage()) { if (this.isInVueNoteablePage()) {
const $el = $('.js-add-award.is-active').closest('.note.timeline-entry'); const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
if ($el.length) { if ($el.length) {
......
...@@ -7,7 +7,7 @@ function onError() { ...@@ -7,7 +7,7 @@ function onError() {
return flash; return flash;
} }
function loadBalsamiqFile() { export default function loadBalsamiqFile() {
const viewer = document.getElementById('js-balsamiq-viewer'); const viewer = document.getElementById('js-balsamiq-viewer');
if (!(viewer instanceof Element)) return; if (!(viewer instanceof Element)) return;
...@@ -17,5 +17,3 @@ function loadBalsamiqFile() { ...@@ -17,5 +17,3 @@ function loadBalsamiqFile() {
const balsamiqViewer = new BalsamiqViewer(viewer); const balsamiqViewer = new BalsamiqViewer(viewer);
balsamiqViewer.loadFile(endpoint).catch(onError); balsamiqViewer.loadFile(endpoint).catch(onError);
} }
$(loadBalsamiqFile);
import renderNotebook from './notebook'; import renderNotebook from './notebook';
document.addEventListener('DOMContentLoaded', renderNotebook); export default renderNotebook;
import renderPDF from './pdf'; import renderPDF from './pdf';
document.addEventListener('DOMContentLoaded', renderPDF); export default renderPDF;
/* eslint-disable no-new */ /* eslint-disable no-new */
import SketchLoader from './sketch'; import SketchLoader from './sketch';
document.addEventListener('DOMContentLoaded', () => { export default () => {
const el = document.getElementById('js-sketch-viewer'); const el = document.getElementById('js-sketch-viewer');
new SketchLoader(el); new SketchLoader(el);
}); };
import Renderer from './3d_viewer'; import Renderer from './3d_viewer';
document.addEventListener('DOMContentLoaded', () => { export default () => {
const viewer = new Renderer(document.getElementById('js-stl-viewer')); const viewer = new Renderer(document.getElementById('js-stl-viewer'));
[].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => { [].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => {
...@@ -16,4 +16,4 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -16,4 +16,4 @@ document.addEventListener('DOMContentLoaded', () => {
viewer.changeObjectMaterials(target.dataset.type); viewer.changeObjectMaterials(target.dataset.type);
}); });
}); });
}); };
...@@ -5,6 +5,7 @@ import axios from '../../lib/utils/axios_utils'; ...@@ -5,6 +5,7 @@ import axios from '../../lib/utils/axios_utils';
export default class BlobViewer { export default class BlobViewer {
constructor() { constructor() {
BlobViewer.initAuxiliaryViewer(); BlobViewer.initAuxiliaryViewer();
BlobViewer.initRichViewer();
this.initMainViewers(); this.initMainViewers();
} }
...@@ -16,6 +17,38 @@ export default class BlobViewer { ...@@ -16,6 +17,38 @@ export default class BlobViewer {
BlobViewer.loadViewer(auxiliaryViewer); BlobViewer.loadViewer(auxiliaryViewer);
} }
static initRichViewer() {
const viewer = document.querySelector('.blob-viewer[data-type="rich"]');
if (!viewer || !viewer.dataset.richType) return;
const initViewer = promise => promise
.then(module => module.default(viewer))
.catch((error) => {
Flash('Error loading file viewer.');
throw error;
});
switch (viewer.dataset.richType) {
case 'balsamiq':
initViewer(import(/* webpackChunkName: 'balsamiq_viewer' */ '../balsamiq_viewer'));
break;
case 'notebook':
initViewer(import(/* webpackChunkName: 'notebook_viewer' */ '../notebook_viewer'));
break;
case 'pdf':
initViewer(import(/* webpackChunkName: 'pdf_viewer' */ '../pdf_viewer'));
break;
case 'sketch':
initViewer(import(/* webpackChunkName: 'sketch_viewer' */ '../sketch_viewer'));
break;
case 'stl':
initViewer(import(/* webpackChunkName: 'stl_viewer' */ '../stl_viewer'));
break;
default:
break;
}
}
initMainViewers() { initMainViewers() {
this.$fileHolder = $('.file-holder'); this.$fileHolder = $('.file-holder');
if (!this.$fileHolder.length) return; if (!this.$fileHolder.length) return;
......
...@@ -7,7 +7,7 @@ import { __ } from '../../locale'; ...@@ -7,7 +7,7 @@ import { __ } from '../../locale';
import Sidebar from '../../right_sidebar'; import Sidebar from '../../right_sidebar';
import eventHub from '../../sidebar/event_hub'; import eventHub from '../../sidebar/event_hub';
import assigneeTitle from '../../sidebar/components/assignees/assignee_title'; import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
import assignees from '../../sidebar/components/assignees/assignees'; import assignees from '../../sidebar/components/assignees/assignees.vue';
import DueDateSelectors from '../../due_date_select'; import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue'; import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context'; import IssuableContext from '../../issuable_context';
......
...@@ -14,10 +14,10 @@ import CycleAnalyticsStore from './cycle_analytics_store'; ...@@ -14,10 +14,10 @@ import CycleAnalyticsStore from './cycle_analytics_store';
Vue.use(Translate); Vue.use(Translate);
$(() => { export default () => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
gl.cycleAnalyticsApp = new Vue({ new Vue({ // eslint-disable-line no-new
el: '#cycle-analytics', el: '#cycle-analytics',
name: 'CycleAnalytics', name: 'CycleAnalytics',
components: { components: {
...@@ -132,4 +132,4 @@ $(() => { ...@@ -132,4 +132,4 @@ $(() => {
}, },
}, },
}); });
}); };
...@@ -197,7 +197,7 @@ const JumpToDiscussion = Vue.extend({ ...@@ -197,7 +197,7 @@ const JumpToDiscussion = Vue.extend({
} }
$.scrollTo($target, { $.scrollTo($target, {
offset: 0 offset: -150
}); });
} }
}, },
......
...@@ -87,6 +87,7 @@ const ResolveBtn = Vue.extend({ ...@@ -87,6 +87,7 @@ const ResolveBtn = Vue.extend({
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by); CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
this.discussion.updateHeadline(data); this.discussion.updateHeadline(data);
gl.mrWidget.checkStatus(); gl.mrWidget.checkStatus();
document.dispatchEvent(new CustomEvent('refreshVueNotes'));
this.updateTooltip(); this.updateTooltip();
}) })
......
...@@ -14,6 +14,7 @@ import './components/resolve_count'; ...@@ -14,6 +14,7 @@ import './components/resolve_count';
import './components/resolve_discussion_btn'; import './components/resolve_discussion_btn';
import './components/diff_note_avatars'; import './components/diff_note_avatars';
import './components/new_issue_for_discussion'; import './components/new_issue_for_discussion';
import { hasVueMRDiscussionsCookie } from '../lib/utils/common_utils';
export default () => { export default () => {
const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box'); const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box');
...@@ -67,12 +68,14 @@ export default () => { ...@@ -67,12 +68,14 @@ export default () => {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
new Vue({ if (!hasVueMRDiscussionsCookie()) {
el: '#resolve-count-app', new Vue({
components: { el: '#resolve-count-app',
'resolve-count': ResolveCount components: {
}, 'resolve-count': ResolveCount
}); },
});
}
$(window).trigger('resize.nav'); $(window).trigger('resize.nav');
}; };
...@@ -8,8 +8,8 @@ window.gl = window.gl || {}; ...@@ -8,8 +8,8 @@ window.gl = window.gl || {};
class ResolveServiceClass { class ResolveServiceClass {
constructor(root) { constructor(root) {
this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`); this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`);
this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`); this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`);
} }
resolve(noteId) { resolve(noteId) {
...@@ -45,6 +45,7 @@ class ResolveServiceClass { ...@@ -45,6 +45,7 @@ class ResolveServiceClass {
if (gl.mrWidget) gl.mrWidget.checkStatus(); if (gl.mrWidget) gl.mrWidget.checkStatus();
discussion.updateHeadline(data); discussion.updateHeadline(data);
document.dispatchEvent(new CustomEvent('refreshVueNotes'));
}) })
.catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.')); .catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.'));
} }
......
...@@ -5,7 +5,7 @@ import Translate from '../vue_shared/translate'; ...@@ -5,7 +5,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate); Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({ export default () => new Vue({
el: '#environments-list-view', el: '#environments-list-view',
components: { components: {
environmentsComponent, environmentsComponent,
...@@ -36,4 +36,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -36,4 +36,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
}, },
}); });
}, },
})); });
import './dropdown_emoji';
import './dropdown_hint';
import './dropdown_non_user';
import './dropdown_user';
import './dropdown_utils';
import './filtered_search_dropdown_manager';
import './filtered_search_dropdown';
import './filtered_search_manager';
import './filtered_search_tokenizer';
import './filtered_search_visual_tokens';
...@@ -10,14 +10,22 @@ import DropdownUser from './dropdown_user'; ...@@ -10,14 +10,22 @@ import DropdownUser from './dropdown_user';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
export default class FilteredSearchDropdownManager { export default class FilteredSearchDropdownManager {
constructor(baseEndpoint = '', tokenizer, page, isGroup, filteredSearchTokenKeys) { constructor({
baseEndpoint = '',
tokenizer,
page,
isGroup,
isGroupAncestor,
filteredSearchTokenKeys,
}) {
this.container = FilteredSearchContainer.container; this.container = FilteredSearchContainer.container;
this.baseEndpoint = baseEndpoint.replace(/\/$/, ''); this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = tokenizer; this.tokenizer = tokenizer;
this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys; this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys;
this.filteredSearchInput = this.container.querySelector('.filtered-search'); this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.page = page; this.page = page;
this.groupsOnly = page === 'boards' && isGroup; this.groupsOnly = isGroup;
this.groupAncestor = isGroupAncestor;
this.setupMapping(); this.setupMapping();
...@@ -60,7 +68,7 @@ export default class FilteredSearchDropdownManager { ...@@ -60,7 +68,7 @@ export default class FilteredSearchDropdownManager {
reference: null, reference: null,
gl: DropdownNonUser, gl: DropdownNonUser,
extraArguments: { extraArguments: {
endpoint: `${this.baseEndpoint}/milestones.json${this.groupsOnly ? '?only_group_milestones=true' : ''}`, endpoint: this.getMilestoneEndpoint(),
symbol: '%', symbol: '%',
}, },
element: this.container.querySelector('#js-dropdown-milestone'), element: this.container.querySelector('#js-dropdown-milestone'),
...@@ -69,7 +77,7 @@ export default class FilteredSearchDropdownManager { ...@@ -69,7 +77,7 @@ export default class FilteredSearchDropdownManager {
reference: null, reference: null,
gl: DropdownNonUser, gl: DropdownNonUser,
extraArguments: { extraArguments: {
endpoint: `${this.baseEndpoint}/labels.json${this.groupsOnly ? '?only_group_labels=true' : ''}`, endpoint: this.getLabelsEndpoint(),
symbol: '~', symbol: '~',
preprocessing: DropdownUtils.duplicateLabelPreprocessing, preprocessing: DropdownUtils.duplicateLabelPreprocessing,
}, },
...@@ -96,6 +104,28 @@ export default class FilteredSearchDropdownManager { ...@@ -96,6 +104,28 @@ export default class FilteredSearchDropdownManager {
this.mapping = allowedMappings; this.mapping = allowedMappings;
} }
getMilestoneEndpoint() {
let endpoint = `${this.baseEndpoint}/milestones.json`;
// EE-only
if (this.groupsOnly) {
endpoint = `${endpoint}?only_group_milestones=true`;
}
return endpoint;
}
getLabelsEndpoint() {
let endpoint = `${this.baseEndpoint}/labels.json`;
// EE-only
if (this.groupsOnly) {
endpoint = `${endpoint}?only_group_labels=true`;
}
return endpoint;
}
static addWordToInput(tokenName, tokenValue = '', clicked = false) { static addWordToInput(tokenName, tokenValue = '', clicked = false) {
const input = FilteredSearchContainer.container.querySelector('.filtered-search'); const input = FilteredSearchContainer.container.querySelector('.filtered-search');
......
...@@ -20,10 +20,13 @@ import DropdownUtils from './dropdown_utils'; ...@@ -20,10 +20,13 @@ import DropdownUtils from './dropdown_utils';
export default class FilteredSearchManager { export default class FilteredSearchManager {
constructor({ constructor({
page, page,
isGroup = false,
isGroupAncestor = false,
filteredSearchTokenKeys = FilteredSearchTokenKeys, filteredSearchTokenKeys = FilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters', stateFiltersSelector = '.issues-state-filters',
}) { }) {
this.isGroup = false; this.isGroup = isGroup;
this.isGroupAncestor = isGroupAncestor;
this.states = ['opened', 'closed', 'merged', 'all']; this.states = ['opened', 'closed', 'merged', 'all'];
this.page = page; this.page = page;
...@@ -98,13 +101,14 @@ export default class FilteredSearchManager { ...@@ -98,13 +101,14 @@ export default class FilteredSearchManager {
if (this.filteredSearchInput) { if (this.filteredSearchInput) {
this.tokenizer = FilteredSearchTokenizer; this.tokenizer = FilteredSearchTokenizer;
this.dropdownManager = new FilteredSearchDropdownManager( this.dropdownManager = new FilteredSearchDropdownManager({
this.filteredSearchInput.getAttribute('data-base-endpoint') || '', baseEndpoint: this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
this.tokenizer, tokenizer: this.tokenizer,
this.page, page: this.page,
this.isGroup, isGroup: this.isGroup,
this.filteredSearchTokenKeys, isGroupAncestor: this.isGroupAncestor,
); filteredSearchTokenKeys: this.filteredSearchTokenKeys,
});
this.recentSearchesRoot = new RecentSearchesRoot( this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore, this.recentSearchesStore,
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import router from '~/ide/ide_router';
import icon from '../../../vue_shared/components/icon.vue'; import icon from '../../../vue_shared/components/icon.vue';
export default { export default {
...@@ -24,20 +25,27 @@ ...@@ -24,20 +25,27 @@
...mapActions([ ...mapActions([
'discardFileChanges', 'discardFileChanges',
]), ]),
openFileInEditor(file) {
router.push(`/project${file.url}`);
},
}, },
}; };
</script> </script>
<template> <template>
<div class="multi-file-commit-list-item"> <div class="multi-file-commit-list-item">
<icon <button
:name="iconName" type="button"
:size="16" class="multi-file-commit-list-path"
:css-classes="iconClass" @click="openFileInEditor(file)">
/> <span class="multi-file-commit-list-file-path">
<span class="multi-file-commit-list-path"> <icon
{{ file.path }} :name="iconName"
</span> :size="16"
:css-classes="iconClass"
/>{{ file.path }}
</span>
</button>
<button <button
type="button" type="button"
class="btn btn-blank multi-file-discard-btn" class="btn btn-blank multi-file-discard-btn"
......
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue'; import repoCommitSection from './repo_commit_section.vue';
import icon from '../../vue_shared/components/icon.vue';
import panelResizer from '../../vue_shared/components/panel_resizer.vue';
export default { export default {
components: { components: {
......
<script> <script>
import icon from '~/vue_shared/components/icon.vue';
import repoTree from './ide_repo_tree.vue'; import repoTree from './ide_repo_tree.vue';
import icon from '../../vue_shared/components/icon.vue';
import newDropdown from './new_dropdown/index.vue'; import newDropdown from './new_dropdown/index.vue';
export default { export default {
......
<script> <script>
import projectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import branchesTree from './ide_project_branches_tree.vue'; import branchesTree from './ide_project_branches_tree.vue';
import projectAvatarImage from '../../vue_shared/components/project_avatar/image.vue';
export default { export default {
components: { components: {
......
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import repoPreviousDirectory from './repo_prev_directory.vue'; import repoPreviousDirectory from './repo_prev_directory.vue';
import repoFile from './repo_file.vue'; import repoFile from './repo_file.vue';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
import { treeList } from '../stores/utils'; import { treeList } from '../stores/utils';
export default { export default {
......
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import projectTree from './ide_project_tree.vue'; import projectTree from './ide_project_tree.vue';
import icon from '../../vue_shared/components/icon.vue';
import panelResizer from '../../vue_shared/components/panel_resizer.vue';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
export default { export default {
components: { components: {
......
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import icon from '../../vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '../../vue_shared/mixins/timeago'; import timeAgoMixin from '~/vue_shared/mixins/timeago';
export default { export default {
components: { components: {
......
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import flash, { hideFlash } from '../../flash'; import flash, { hideFlash } from '~/flash';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
export default { export default {
components: { components: {
......
<script> <script>
import { mapState, mapActions, mapGetters } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import * as consts from '../stores/modules/commit/constants'; import tooltip from '~/vue_shared/directives/tooltip';
import tooltip from '../../vue_shared/directives/tooltip'; import icon from '~/vue_shared/components/icon.vue';
import icon from '../../vue_shared/components/icon.vue'; import modal from '~/vue_shared/components/modal.vue';
import modal from '../../vue_shared/components/modal.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import commitFilesList from './commit_sidebar/list.vue'; import commitFilesList from './commit_sidebar/list.vue';
import Actions from './commit_sidebar/actions.vue';
import LoadingButton from '../../vue_shared/components/loading_button.vue'; import * as consts from 'ee/ide/stores/modules/commit/constants'; // eslint-disable-line import/first
import Actions from 'ee/ide/components/commit_sidebar/actions.vue'; // eslint-disable-line import/first
export default { export default {
components: { components: {
......
<script> <script>
import { mapGetters, mapActions, mapState } from 'vuex'; import { mapGetters, mapActions, mapState } from 'vuex';
import modal from '../../vue_shared/components/modal.vue'; import modal from '~/vue_shared/components/modal.vue';
export default { export default {
components: { components: {
......
<script> <script>
/* global monaco */ /* global monaco */
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '../../flash'; import flash from '~/flash';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
......
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
export default { export default {
components: { components: {
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import LineHighlighter from '../../line_highlighter'; import LineHighlighter from '~/line_highlighter';
import syntaxHighlight from '../../syntax_highlight'; import syntaxHighlight from '~/syntax_highlight';
export default { export default {
computed: { computed: {
......
/* global monaco */ /* global monaco */
import Disposable from './disposable'; import Disposable from './disposable';
import eventHub from '../../eventhub';
import eventHub from 'ee/ide/eventhub'; // eslint-disable-line import/first
export default class Model { export default class Model {
constructor(monaco, file) { constructor(monaco, file) {
......
import Vue from 'vue'; import Vue from 'vue';
import { visitUrl } from '../../lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const redirectToUrl = (_, url) => visitUrl(url); export const redirectToUrl = (_, url) => visitUrl(url);
......
...@@ -4,7 +4,8 @@ import state from './state'; ...@@ -4,7 +4,8 @@ import state from './state';
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters'; import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
import commitModule from './modules/commit';
import commitModule from 'ee/ide/stores/modules/commit'; // eslint-disable-line import/first
Vue.use(Vuex); Vue.use(Vuex);
......
...@@ -21,7 +21,7 @@ export default class LabelsSelect { ...@@ -21,7 +21,7 @@ export default class LabelsSelect {
} }
$els.each(function(i, dropdown) { $els.each(function(i, dropdown) {
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
$dropdown = $(dropdown); $dropdown = $(dropdown);
$dropdownContainer = $dropdown.closest('.labels-filter'); $dropdownContainer = $dropdown.closest('.labels-filter');
$toggleText = $dropdown.find('.dropdown-toggle-text'); $toggleText = $dropdown.find('.dropdown-toggle-text');
...@@ -53,13 +53,6 @@ export default class LabelsSelect { ...@@ -53,13 +53,6 @@ export default class LabelsSelect {
.map(function () { .map(function () {
return this.value; return this.value;
}).get(); }).get();
if (issueUpdateURL != null) {
issueURLSplit = issueUpdateURL.split('/');
}
if (issueUpdateURL) {
labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
labelNoneHTMLTemplate = '<span class="no-value">None</span>';
}
const handleClick = options.handleClick; const handleClick = options.handleClick;
$sidebarLabelTooltip.tooltip(); $sidebarLabelTooltip.tooltip();
...@@ -91,14 +84,17 @@ export default class LabelsSelect { ...@@ -91,14 +84,17 @@ export default class LabelsSelect {
$loading.fadeOut(); $loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown'); $dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide(); $selectbox.hide();
data.issueURLSplit = issueURLSplit; data.issueUpdateURL = issueUpdateURL;
labelCount = 0; labelCount = 0;
if (data.labels.length) { if (data.labels.length && issueUpdateURL) {
template = labelHTMLTemplate(data); template = LabelsSelect.getLabelTemplate({
labels: data.labels,
issueUpdateURL,
});
labelCount = data.labels.length; labelCount = data.labels.length;
} }
else { else {
template = labelNoneHTMLTemplate; template = '<span class="no-value">None</span>';
} }
$value.removeAttr('style').html(template); $value.removeAttr('style').html(template);
$sidebarCollapsedValue.text(labelCount); $sidebarCollapsedValue.text(labelCount);
...@@ -242,10 +238,16 @@ export default class LabelsSelect { ...@@ -242,10 +238,16 @@ export default class LabelsSelect {
filterable: true, filterable: true,
selected: $dropdown.data('selected') || [], selected: $dropdown.data('selected') || [],
toggleLabel: function(selected, el) { toggleLabel: function(selected, el) {
var $dropdownParent = $dropdown.parent();
var $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
var isSelected = el !== null ? el.hasClass('is-active') : false; var isSelected = el !== null ? el.hasClass('is-active') : false;
var title = selected.title; var title = selected.title;
var selectedLabels = this.selected; var selectedLabels = this.selected;
if ($dropdownInputField.length && $dropdownInputField.val().length) {
$dropdownParent.find('.dropdown-input-clear').trigger('click');
}
if (selected.id === 0) { if (selected.id === 0) {
this.selected = []; this.selected = [];
return 'No Label'; return 'No Label';
...@@ -412,6 +414,26 @@ export default class LabelsSelect { ...@@ -412,6 +414,26 @@ export default class LabelsSelect {
this.bindEvents(); this.bindEvents();
} }
static getLabelTemplate(tplData) {
// We could use ES6 template string here
// and properly indent markup for readability
// but that also introduces unintended white-space
// so best approach is to use traditional way of
// concatenation
// see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays
const tpl = _.template([
'<% _.each(labels, function(label){ %>',
'<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
'<span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
'<%- label.title %>',
'</span>',
'</a>',
'<% }); %>',
].join(''));
return tpl(tplData);
}
bindEvents() { bindEvents() {
return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue); return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue);
} }
......
import jQuery from 'jquery';
import Cookies from 'js-cookie';
import axios from './axios_utils'; import axios from './axios_utils';
import { getLocationHash } from './url_utility'; import { getLocationHash } from './url_utility';
import { convertToCamelCase } from './text_utility'; import { convertToCamelCase } from './text_utility';
...@@ -22,13 +24,18 @@ export const getGroupSlug = () => { ...@@ -22,13 +24,18 @@ export const getGroupSlug = () => {
return null; return null;
}; };
export const isInIssuePage = () => { export const checkPageAndAction = (page, action) => {
const page = getPagePath(1); const pagePath = getPagePath(1);
const action = getPagePath(2); const actionPath = getPagePath(2);
return page === 'issues' && action === 'show'; return pagePath === page && actionPath === action;
}; };
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
export const isInNoteablePage = () => isInIssuePage() || isInMRPage();
export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions');
export const ajaxGet = url => axios.get(url, { export const ajaxGet = url => axios.get(url, {
params: { format: 'js' }, params: { format: 'js' },
responseType: 'text', responseType: 'text',
...@@ -133,7 +140,11 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; ...@@ -133,7 +140,11 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
// 3) Middle-click or Mouse Wheel Click (e.which is 2) // 3) Middle-click or Mouse Wheel Click (e.which is 2)
export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2; export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2;
export const scrollToElement = ($el) => { export const scrollToElement = (element) => {
let $el = element;
if (!(element instanceof jQuery)) {
$el = $(element);
}
const top = $el.offset().top; const top = $el.offset().top;
const mrTabsHeight = $('.merge-request-tabs').height() || 0; const mrTabsHeight = $('.merge-request-tabs').height() || 0;
const headerHeight = $('.navbar-gitlab').height() || 0; const headerHeight = $('.navbar-gitlab').height() || 0;
......
...@@ -65,6 +65,20 @@ export function capitalizeFirstCharacter(text) { ...@@ -65,6 +65,20 @@ export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`; return `${text[0].toUpperCase()}${text.slice(1)}`;
} }
export function camelCase(str) {
return str.replace(/_+([a-z])/gi, ($1, $2) => $2.toUpperCase());
}
export function camelCaseKeys(obj = {}) {
return Object.keys(obj).reduce((acc, key) => {
const camelKey = camelCase(key);
return {
...acc,
[camelKey]: obj[key],
};
}, {});
}
/** /**
* Replaces all html tags from a string with the given replacement. * Replaces all html tags from a string with the given replacement.
* *
......
...@@ -12,7 +12,7 @@ import './components/inline_conflict_lines'; ...@@ -12,7 +12,7 @@ import './components/inline_conflict_lines';
import './components/parallel_conflict_lines'; import './components/parallel_conflict_lines';
import syntaxHighlight from '../syntax_highlight'; import syntaxHighlight from '../syntax_highlight';
$(() => { export default function initMergeConflicts() {
const INTERACTIVE_RESOLVE_MODE = 'interactive'; const INTERACTIVE_RESOLVE_MODE = 'interactive';
const conflictsEl = document.querySelector('#conflicts'); const conflictsEl = document.querySelector('#conflicts');
const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore; const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore;
...@@ -91,4 +91,4 @@ $(() => { ...@@ -91,4 +91,4 @@ $(() => {
} }
} }
}); });
}); }
...@@ -241,6 +241,10 @@ export default class MergeRequestTabs { ...@@ -241,6 +241,10 @@ export default class MergeRequestTabs {
return newState; return newState;
} }
getCurrentAction() {
return this.currentAction;
}
loadCommits(source) { loadCommits(source) {
if (this.commitsLoaded) { if (this.commitsLoaded) {
return; return;
......
...@@ -76,7 +76,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom ...@@ -76,7 +76,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
metricTag = seriesCustomizationData.value || timeSeriesMetricLabel; metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
[lineColor, areaColor] = pickColor(seriesCustomizationData.color); [lineColor, areaColor] = pickColor(seriesCustomizationData.color);
} else { } else {
metricTag = timeSeriesMetricLabel || `series ${timeSeriesNumber + 1}`; metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`;
[lineColor, areaColor] = pickColor(); [lineColor, areaColor] = pickColor();
} }
......
import Vue from 'vue';
import notesApp from '../notes/components/notes_app.vue';
import discussionCounter from '../notes/components/discussion_counter.vue';
import store from '../notes/stores';
document.addEventListener('DOMContentLoaded', () => {
new Vue({ // eslint-disable-line
el: '#js-vue-mr-discussions',
components: {
notesApp,
},
data() {
const notesDataset = document.getElementById('js-vue-mr-discussions').dataset;
return {
noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData),
notesData: JSON.parse(notesDataset.notesData),
};
},
render(createElement) {
return createElement('notes-app', {
props: {
noteableData: this.noteableData,
notesData: this.notesData,
userData: this.currentUserData,
},
});
},
});
new Vue({ // eslint-disable-line
el: '#js-vue-discussion-counter',
components: {
discussionCounter,
},
store,
render(createElement) {
return createElement('discussion-counter');
},
});
});
This diff is collapsed.
...@@ -2,10 +2,11 @@ ...@@ -2,10 +2,11 @@
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import Autosize from 'autosize'; import Autosize from 'autosize';
import { __ } from '~/locale'; import { __, sprintf } from '~/locale';
import Flash from '../../flash'; import Flash from '../../flash';
import Autosave from '../../autosave'; import Autosave from '../../autosave';
import TaskList from '../../task_list'; import TaskList from '../../task_list';
import { capitalizeFirstCharacter, convertToCamelCase } from '../../lib/utils/text_utility';
import * as constants from '../constants'; import * as constants from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
...@@ -29,6 +30,12 @@ ...@@ -29,6 +30,12 @@
mixins: [ mixins: [
issuableStateMixin, issuableStateMixin,
], ],
props: {
noteableType: {
type: String,
required: true,
},
},
data() { data() {
return { return {
note: '', note: '',
...@@ -43,37 +50,51 @@ ...@@ -43,37 +50,51 @@
'getUserData', 'getUserData',
'getNoteableData', 'getNoteableData',
'getNotesData', 'getNotesData',
'issueState', 'openState',
]), ]),
noteableDisplayName() {
return this.noteableType.replace(/_/g, ' ');
},
isLoggedIn() { isLoggedIn() {
return this.getUserData.id; return this.getUserData.id;
}, },
commentButtonTitle() { commentButtonTitle() {
return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion'; return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion';
}, },
isIssueOpen() { isOpen() {
return this.issueState === constants.OPENED || this.issueState === constants.REOPENED; return this.openState === constants.OPENED || this.openState === constants.REOPENED;
}, },
canCreateNote() { canCreateNote() {
return this.getNoteableData.current_user.can_create_note; return this.getNoteableData.current_user.can_create_note;
}, },
issueActionButtonTitle() { issueActionButtonTitle() {
if (this.note.length) { const openOrClose = this.isOpen ? 'close' : 'reopen';
const actionText = this.isIssueOpen ? 'close' : 'reopen';
return this.noteType === constants.COMMENT ? if (this.note.length) {
`Comment & ${actionText} issue` : return sprintf(
`Start discussion & ${actionText} issue`; __('%{actionText} & %{openOrClose} %{noteable}'),
{
actionText: this.commentButtonTitle,
openOrClose,
noteable: this.noteableDisplayName,
},
);
} }
return this.isIssueOpen ? 'Close issue' : 'Reopen issue'; return sprintf(
__('%{openOrClose} %{noteable}'),
{
openOrClose: capitalizeFirstCharacter(openOrClose),
noteable: this.noteableDisplayName,
},
);
}, },
actionButtonClassNames() { actionButtonClassNames() {
return { return {
'btn-reopen': !this.isIssueOpen, 'btn-reopen': !this.isOpen,
'btn-close': this.isIssueOpen, 'btn-close': this.isOpen,
'js-note-target-close': this.isIssueOpen, 'js-note-target-close': this.isOpen,
'js-note-target-reopen': !this.isIssueOpen, 'js-note-target-reopen': !this.isOpen,
}; };
}, },
markdownDocsPath() { markdownDocsPath() {
...@@ -138,7 +159,7 @@ ...@@ -138,7 +159,7 @@
flashContainer: this.$el, flashContainer: this.$el,
data: { data: {
note: { note: {
noteable_type: constants.NOTEABLE_TYPE, noteable_type: this.noteableType,
noteable_id: this.getNoteableData.id, noteable_id: this.getNoteableData.id,
note: this.note, note: this.note,
}, },
...@@ -193,19 +214,29 @@ Please check your network connection and try again.`; ...@@ -193,19 +214,29 @@ Please check your network connection and try again.`;
this.isSubmitting = false; this.isSubmitting = false;
}, },
toggleIssueState() { toggleIssueState() {
if (this.isIssueOpen) { if (this.isOpen) {
this.closeIssue() this.closeIssue()
.then(() => this.enableButton()) .then(() => this.enableButton())
.catch(() => { .catch(() => {
this.enableButton(); this.enableButton();
Flash(__('Something went wrong while closing the issue. Please try again later')); Flash(
sprintf(
__('Something went wrong while closing the %{issuable}. Please try again later'),
{ issuable: this.noteableDisplayName },
),
);
}); });
} else { } else {
this.reopenIssue() this.reopenIssue()
.then(() => this.enableButton()) .then(() => this.enableButton())
.catch(() => { .catch(() => {
this.enableButton(); this.enableButton();
Flash(__('Something went wrong while reopening the issue. Please try again later')); Flash(
sprintf(
__('Something went wrong while reopening the %{issuable}. Please try again later'),
{ issuable: this.noteableDisplayName },
),
);
}); });
} }
}, },
...@@ -221,7 +252,6 @@ Please check your network connection and try again.`; ...@@ -221,7 +252,6 @@ Please check your network connection and try again.`;
this.$refs.markdownField.previewMarkdown = false; this.$refs.markdownField.previewMarkdown = false;
} }
// reset autostave
this.autosave.reset(); this.autosave.reset();
}, },
setNoteType(type) { setNoteType(type) {
...@@ -240,10 +270,11 @@ Please check your network connection and try again.`; ...@@ -240,10 +270,11 @@ Please check your network connection and try again.`;
}, },
initAutoSave() { initAutoSave() {
if (this.isLoggedIn) { if (this.isLoggedIn) {
const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType));
this.autosave = new Autosave( this.autosave = new Autosave(
$(this.$refs.textarea), $(this.$refs.textarea),
['Note', 'Issue', this.getNoteableData.id], ['Note', noteableType, this.getNoteableData.id],
'issue',
); );
} }
}, },
...@@ -331,7 +362,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" ...@@ -331,7 +362,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
:disabled="isSubmitButtonDisabled" :disabled="isSubmitButtonDisabled"
class="btn btn-create comment-btn js-comment-button js-comment-submit-button" class="btn btn-create comment-btn js-comment-button js-comment-submit-button"
type="submit"> type="submit">
{{ commentButtonTitle }} {{ __(commentButtonTitle) }}
</button> </button>
<button <button
:disabled="isSubmitButtonDisabled" :disabled="isSubmitButtonDisabled"
...@@ -359,7 +390,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" ...@@ -359,7 +390,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<div class="description"> <div class="description">
<strong>Comment</strong> <strong>Comment</strong>
<p> <p>
Add a general comment to this issue. Add a general comment to this {{ noteableDisplayName }}.
</p> </p>
</div> </div>
</button> </button>
......
<script>
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
ClipboardButton,
Icon,
},
props: {
diffFile: {
type: Object,
required: true,
},
},
computed: {
titleTag() {
return this.diffFile.discussionPath ? 'a' : 'span';
},
},
};
</script>
<template>
<div class="file-header-content">
<div
v-if="diffFile.submodule"
>
<span>
<icon name="archive" />
<strong
v-html="diffFile.submoduleLink"
class="file-title-name"
></strong>
<clipboard-button
title="Copy file path to clipboard"
:text="diffFile.submoduleLink"
/>
</span>
</div>
<template v-else>
<component
ref="titleWrapper"
:is="titleTag"
:href="diffFile.discussionPath"
>
<span v-html="diffFile.blobIcon"></span>
<span v-if="diffFile.renamedFile">
<strong
class="file-title-name has-tooltip"
:title="diffFile.oldPath"
data-container="body"
>
{{ diffFile.oldPath }}
</strong>
&rarr;
<strong
class="file-title-name has-tooltip"
:title="diffFile.newPath"
data-container="body"
>
{{ diffFile.newPath }}
</strong>
</span>
<strong
v-else
class="file-title-name has-tooltip"
:title="diffFile.oldPath"
data-container="body"
>
{{ diffFile.filePath }}
<span v-if="diffFile.deletedFile">
deleted
</span>
</strong>
</component>
<clipboard-button
title="Copy file path to clipboard"
:text="diffFile.filePath"
/>
<small
v-if="diffFile.modeChanged"
ref="fileMode"
>
{{ diffFile.aMode }}{{ diffFile.bMode }}
</small>
</template>
</div>
</template>
<script>
import syntaxHighlight from '~/syntax_highlight';
import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from './diff_file_header.vue';
export default {
components: {
DiffFileHeader,
},
props: {
discussion: {
type: Object,
required: true,
},
},
computed: {
isImageDiff() {
return !this.diffFile.text;
},
diffFileClass() {
const { text } = this.diffFile;
return text ? 'text-file' : 'js-image-file';
},
diffRows() {
return $(this.discussion.truncatedDiffLines);
},
diffFile() {
return convertObjectPropsToCamelCase(this.discussion.diffFile);
},
imageDiffHtml() {
return this.discussion.imageDiffHtml;
},
},
mounted() {
if (this.isImageDiff) {
const canCreateNote = false;
const renderCommentBadge = true;
imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
} else {
const fileHolder = $(this.$refs.fileHolder);
this.$nextTick(() => {
syntaxHighlight(fileHolder);
});
}
},
methods: {
rowTag(html) {
return html.outerHTML ? 'tr' : 'template';
},
},
};
</script>
<template>
<div
ref="fileHolder"
class="diff-file file-holder"
:class="diffFileClass"
>
<div class="js-file-title file-title file-title-flex-parent">
<diff-file-header
:diff-file="diffFile"
/>
</div>
<div
v-if="diffFile.text"
class="diff-content code js-syntax-highlight"
>
<table>
<component
:is="rowTag(html)"
:class="html.className"
v-for="(html, index) in diffRows"
v-html="html.outerHTML"
:key="index"
/>
<tr class="notes_holder">
<td
class="notes_line"
colspan="2"
></td>
<td class="notes_content">
<slot></slot>
</td>
</tr>
</table>
</div>
<div
v-else
>
<div v-html="imageDiffHtml"></div>
<slot></slot>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import resolveSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedSvg from 'icons/_icon_status_success_solid.svg';
import mrIssueSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionSvg from 'icons/_next_discussion.svg';
import { pluralize } from '../../lib/utils/text_utility';
import { scrollToElement } from '../../lib/utils/common_utils';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
},
computed: {
...mapGetters([
'getUserData',
'getNoteableData',
'discussionCount',
'unresolvedDiscussions',
'resolvedDiscussionCount',
]),
isLoggedIn() {
return this.getUserData.id;
},
hasNextButton() {
return this.isLoggedIn && !this.allResolved;
},
countText() {
return pluralize('discussion', this.discussionCount);
},
allResolved() {
return this.resolvedDiscussionCount === this.discussionCount;
},
resolveAllDiscussionsIssuePath() {
return this.getNoteableData.create_issue_to_resolve_discussions_path;
},
firstUnresolvedDiscussionId() {
const item = this.unresolvedDiscussions[0] || {};
return item.id;
},
},
created() {
this.resolveSvg = resolveSvg;
this.resolvedSvg = resolvedSvg;
this.mrIssueSvg = mrIssueSvg;
this.nextDiscussionSvg = nextDiscussionSvg;
},
methods: {
jumpToFirstDiscussion() {
const el = document.querySelector(`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`);
const activeTab = window.mrTabs.currentAction;
if (activeTab === 'commits' || activeTab === 'pipelines') {
window.mrTabs.activateTab('show');
}
if (el) {
scrollToElement(el);
}
},
},
};
</script>
<template>
<div class="line-resolve-all-container prepend-top-10">
<div>
<div
v-if="discussionCount > 0"
:class="{ 'has-next-btn': hasNextButton }"
class="line-resolve-all">
<span
:class="{ 'is-active': allResolved }"
class="line-resolve-btn is-disabled"
type="button">
<span
v-if="allResolved"
v-html="resolvedSvg"
></span>
<span
v-else
v-html="resolveSvg"
></span>
</span>
<span class=".line-resolve-text">
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
</span>
</div>
<div
v-if="resolveAllDiscussionsIssuePath && !allResolved"
class="btn-group"
role="group">
<a
:href="resolveAllDiscussionsIssuePath"
v-tooltip
title="Resolve all discussions in new issue"
data-container="body"
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn">
<span v-html="mrIssueSvg"></span>
</a>
</div>
<div
v-if="isLoggedIn && !allResolved"
class="btn-group"
role="group">
<button
@click="jumpToFirstDiscussion"
v-tooltip
title="Jump to first unresolved discussion"
data-container="body"
class="btn btn-default discussion-next-btn">
<span v-html="nextDiscussionSvg"></span>
</button>
</div>
</div>
</div>
</template>
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg';
import editSvg from 'icons/_icon_pencil.svg'; import editSvg from 'icons/_icon_pencil.svg';
import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
import ellipsisSvg from 'icons/_ellipsis_v.svg'; import ellipsisSvg from 'icons/_ellipsis_v.svg';
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
...@@ -42,6 +44,26 @@ ...@@ -42,6 +44,26 @@
type: Boolean, type: Boolean,
required: true, required: true,
}, },
resolvable: {
type: Boolean,
required: false,
default: false,
},
isResolved: {
type: Boolean,
required: false,
default: false,
},
isResolving: {
type: Boolean,
required: false,
default: false,
},
resolvedBy: {
type: Object,
required: false,
default: () => ({}),
},
canReportAsAbuse: { canReportAsAbuse: {
type: Boolean, type: Boolean,
required: true, required: true,
...@@ -63,6 +85,15 @@ ...@@ -63,6 +85,15 @@
currentUserId() { currentUserId() {
return this.getUserDataByProp('id'); return this.getUserDataByProp('id');
}, },
resolveButtonTitle() {
let title = 'Mark as resolved';
if (this.resolvedBy) {
title = `Resolved by ${this.resolvedBy.name}`;
}
return title;
},
}, },
created() { created() {
this.emojiSmiling = emojiSmiling; this.emojiSmiling = emojiSmiling;
...@@ -70,6 +101,8 @@ ...@@ -70,6 +101,8 @@
this.emojiSmiley = emojiSmiley; this.emojiSmiley = emojiSmiley;
this.editSvg = editSvg; this.editSvg = editSvg;
this.ellipsisSvg = ellipsisSvg; this.ellipsisSvg = ellipsisSvg;
this.resolveDiscussionSvg = resolveDiscussionSvg;
this.resolvedDiscussionSvg = resolvedDiscussionSvg;
}, },
methods: { methods: {
onEdit() { onEdit() {
...@@ -78,6 +111,9 @@ ...@@ -78,6 +111,9 @@
onDelete() { onDelete() {
this.$emit('handleDelete'); this.$emit('handleDelete');
}, },
onResolve() {
this.$emit('handleResolve');
},
}, },
}; };
</script> </script>
...@@ -89,6 +125,31 @@ ...@@ -89,6 +125,31 @@
class="note-role user-access-role"> class="note-role user-access-role">
{{ accessLevel }} {{ accessLevel }}
</span> </span>
<div
v-if="resolvable"
class="note-actions-item">
<button
v-tooltip
@click="onResolve"
:class="{ 'is-disabled': !resolvable, 'is-active': isResolved }"
:title="resolveButtonTitle"
:aria-label="resolveButtonTitle"
type="button"
class="line-resolve-btn note-action-button">
<template v-if="!isResolving">
<div
v-if="isResolved"
v-html="resolvedDiscussionSvg"></div>
<div
v-else
v-html="resolveDiscussionSvg"></div>
</template>
<loading-icon
v-else
:inline="true"
/>
</button>
</div>
<div <div
v-if="canAddAwardEmoji" v-if="canAddAwardEmoji"
class="note-actions-item"> class="note-actions-item">
......
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
this.initTaskList(); this.initTaskList();
if (this.isEditing) { if (this.isEditing) {
this.initAutoSave(); this.initAutoSave(this.note.noteable_type);
} }
}, },
updated() { updated() {
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
if (this.isEditing) { if (this.isEditing) {
if (!this.autosave) { if (!this.autosave) {
this.initAutoSave(); this.initAutoSave(this.note.noteable_type);
} else { } else {
this.setAutoSave(); this.setAutoSave();
} }
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state'; import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable';
export default { export default {
name: 'IssueNoteForm', name: 'IssueNoteForm',
...@@ -13,6 +14,7 @@ ...@@ -13,6 +14,7 @@
}, },
mixins: [ mixins: [
issuableStateMixin, issuableStateMixin,
resolvable,
], ],
props: { props: {
noteBody: { noteBody: {
...@@ -30,7 +32,7 @@ ...@@ -30,7 +32,7 @@
required: false, required: false,
default: 'Save comment', default: 'Save comment',
}, },
discussion: { note: {
type: Object, type: Object,
required: false, required: false,
default: () => ({}), default: () => ({}),
...@@ -42,9 +44,11 @@ ...@@ -42,9 +44,11 @@
}, },
data() { data() {
return { return {
note: this.noteBody, updatedNoteBody: this.noteBody,
conflictWhileEditing: false, conflictWhileEditing: false,
isSubmitting: false, isSubmitting: false,
isResolving: false,
resolveAsThread: true,
}; };
}, },
computed: { computed: {
...@@ -71,13 +75,13 @@ ...@@ -71,13 +75,13 @@
return this.getUserDataByProp('id'); return this.getUserDataByProp('id');
}, },
isDisabled() { isDisabled() {
return !this.note.length || this.isSubmitting; return !this.updatedNoteBody.length || this.isSubmitting;
}, },
}, },
watch: { watch: {
noteBody() { noteBody() {
if (this.note === this.noteBody) { if (this.updatedNoteBody === this.noteBody) {
this.note = this.noteBody; this.updatedNoteBody = this.noteBody;
} else { } else {
this.conflictWhileEditing = true; this.conflictWhileEditing = true;
} }
...@@ -87,16 +91,24 @@ ...@@ -87,16 +91,24 @@
this.$refs.textarea.focus(); this.$refs.textarea.focus();
}, },
methods: { methods: {
handleUpdate() { ...mapActions([
'toggleResolveNote',
]),
handleUpdate(shouldResolve) {
const beforeSubmitDiscussionState = this.discussionResolved;
this.isSubmitting = true; this.isSubmitting = true;
this.$emit('handleFormUpdate', this.note, this.$refs.editNoteForm, () => { this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => {
this.isSubmitting = false; this.isSubmitting = false;
if (shouldResolve) {
this.resolveHandler(beforeSubmitDiscussionState);
}
}); });
}, },
editMyLastNote() { editMyLastNote() {
if (this.note === '') { if (this.updatedNoteBody === '') {
const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion); const lastNoteInDiscussion = this.getDiscussionLastNote(this.updatedNoteBody);
if (lastNoteInDiscussion) { if (lastNoteInDiscussion) {
eventHub.$emit('enterEditMode', { eventHub.$emit('enterEditMode', {
...@@ -107,7 +119,7 @@ ...@@ -107,7 +119,7 @@
}, },
cancelHandler(shouldConfirm = false) { cancelHandler(shouldConfirm = false) {
// Sends information about confirm message and if the textarea has changed // Sends information about confirm message and if the textarea has changed
this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note); this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.updatedNoteBody);
}, },
}, },
}; };
...@@ -150,7 +162,7 @@ ...@@ -150,7 +162,7 @@
js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
:data-supports-quick-actions="!isEditing" :data-supports-quick-actions="!isEditing"
aria-label="Description" aria-label="Description"
v-model="note" v-model="updatedNoteBody"
ref="textarea" ref="textarea"
slot="textarea" slot="textarea"
placeholder="Write a comment or drag your files here..." placeholder="Write a comment or drag your files here..."
...@@ -168,6 +180,13 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" ...@@ -168,6 +180,13 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
class="js-vue-issue-save btn btn-save"> class="js-vue-issue-save btn btn-save">
{{ saveButtonTitle }} {{ saveButtonTitle }}
</button> </button>
<button
v-if="note.resolvable"
@click.prevent="handleUpdate(true)"
class="btn btn-nr btn-default append-right-10 js-comment-resolve-button"
>
{{ resolveButtonTitle }}
</button>
<button <button
@click="cancelHandler()" @click="cancelHandler()"
class="btn btn-cancel note-edit-cancel" class="btn btn-cancel note-edit-cancel"
......
...@@ -34,15 +34,15 @@ ...@@ -34,15 +34,15 @@
required: false, required: false,
default: false, default: false,
}, },
}, expanded: {
data() { type: Boolean,
return { required: false,
isExpanded: true, default: true,
}; },
}, },
computed: { computed: {
toggleChevronClass() { toggleChevronClass() {
return this.isExpanded ? 'fa-chevron-up' : 'fa-chevron-down'; return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down';
}, },
noteTimestampLink() { noteTimestampLink() {
return `#note_${this.noteId}`; return `#note_${this.noteId}`;
...@@ -53,7 +53,6 @@ ...@@ -53,7 +53,6 @@
'setTargetNoteHash', 'setTargetNoteHash',
]), ]),
handleToggle() { handleToggle() {
this.isExpanded = !this.isExpanded;
this.$emit('toggleHandler'); this.$emit('toggleHandler');
}, },
updateTargetNoteHash() { updateTargetNoteHash() {
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
import noteActions from './note_actions.vue'; import noteActions from './note_actions.vue';
import noteBody from './note_body.vue'; import noteBody from './note_body.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
export default { export default {
components: { components: {
...@@ -15,6 +17,10 @@ ...@@ -15,6 +17,10 @@
noteActions, noteActions,
noteBody, noteBody,
}, },
mixins: [
noteable,
resolvable,
],
props: { props: {
note: { note: {
type: Object, type: Object,
...@@ -26,6 +32,7 @@ ...@@ -26,6 +32,7 @@
isEditing: false, isEditing: false,
isDeleting: false, isDeleting: false,
isRequesting: false, isRequesting: false,
isResolving: false,
}; };
}, },
computed: { computed: {
...@@ -65,6 +72,7 @@ ...@@ -65,6 +72,7 @@
...mapActions([ ...mapActions([
'deleteNote', 'deleteNote',
'updateNote', 'updateNote',
'toggleResolveNote',
'scrollToNoteIfNeeded', 'scrollToNoteIfNeeded',
]), ]),
editHandler() { editHandler() {
...@@ -89,7 +97,7 @@ ...@@ -89,7 +97,7 @@
const data = { const data = {
endpoint: this.note.path, endpoint: this.note.path,
note: { note: {
target_type: 'issue', target_type: this.noteableType,
target_id: this.note.noteable_id, target_id: this.note.noteable_id,
note: { note: noteText }, note: { note: noteText },
}, },
...@@ -134,7 +142,7 @@ ...@@ -134,7 +142,7 @@
// we need to do this to prevent noteForm inconsistent content warning // we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content // this is something we intentionally do so we need to recover the content
this.note.note = noteText; this.note.note = noteText;
this.$refs.noteBody.$refs.noteForm.note = noteText; this.$refs.noteBody.$refs.noteForm.note.note = noteText;
}, },
}, },
}; };
...@@ -171,8 +179,13 @@ ...@@ -171,8 +179,13 @@
:can-delete="note.current_user.can_edit" :can-delete="note.current_user.can_edit"
:can-report-as-abuse="canReportAsAbuse" :can-report-as-abuse="canReportAsAbuse"
:report-abuse-path="note.report_abuse_path" :report-abuse-path="note.report_abuse_path"
:resolvable="note.resolvable"
:is-resolved="note.resolved"
:is-resolving="isResolving"
:resolved-by="note.resolved_by"
@handleEdit="editHandler" @handleEdit="editHandler"
@handleDelete="deleteHandler" @handleDelete="deleteHandler"
@handleResolve="resolveHandler"
/> />
</div> </div>
<note-body <note-body
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
export default { export default {
name: 'NotesApp', name: 'NotesApp',
...@@ -48,7 +49,24 @@ ...@@ -48,7 +49,24 @@
...mapGetters([ ...mapGetters([
'notes', 'notes',
'getNotesDataByProp', 'getNotesDataByProp',
'discussionCount',
]), ]),
noteableType() {
// FIXME -- @fatihacet Get this from JSON data.
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants;
return this.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE : ISSUE_NOTEABLE_TYPE;
},
allNotes() {
if (this.isLoading) {
const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0;
return new Array(totalNotes).fill({
isSkeletonNote: true,
});
}
return this.notes;
},
}, },
created() { created() {
this.setNotesData(this.notesData); this.setNotesData(this.notesData);
...@@ -67,6 +85,10 @@ ...@@ -67,6 +85,10 @@
this.actionToggleAward({ awardName, noteId }); this.actionToggleAward({ awardName, noteId });
}); });
} }
document.addEventListener('refreshVueNotes', this.fetchNotes);
},
beforeDestroy() {
document.removeEventListener('refreshVueNotes', this.fetchNotes);
}, },
methods: { methods: {
...mapActions({ ...mapActions({
...@@ -81,6 +103,9 @@ ...@@ -81,6 +103,9 @@
setTargetNoteHash: 'setTargetNoteHash', setTargetNoteHash: 'setTargetNoteHash',
}), }),
getComponentName(note) { getComponentName(note) {
if (note.isSkeletonNote) {
return skeletonLoadingContainer;
}
if (note.isPlaceholderNote) { if (note.isPlaceholderNote) {
if (note.placeholderType === constants.SYSTEM_NOTE) { if (note.placeholderType === constants.SYSTEM_NOTE) {
return placeholderSystemNote; return placeholderSystemNote;
...@@ -109,9 +134,14 @@ ...@@ -109,9 +134,14 @@
}); });
}, },
initPolling() { initPolling() {
if (this.isPollingInitialized) {
return;
}
this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt')); this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt'));
this.poll(); this.poll();
this.isPollingInitialized = true;
}, },
checkLocationHash() { checkLocationHash() {
const hash = getLocationHash(); const hash = getLocationHash();
...@@ -128,25 +158,20 @@ ...@@ -128,25 +158,20 @@
<template> <template>
<div id="notes"> <div id="notes">
<div
v-if="isLoading"
class="js-loading loading">
<loading-icon />
</div>
<ul <ul
v-if="!isLoading"
id="notes-list" id="notes-list"
class="notes main-notes-list timeline"> class="notes main-notes-list timeline">
<component <component
v-for="note in notes" v-for="note in allNotes"
:is="getComponentName(note)" :is="getComponentName(note)"
:note="getComponentData(note)" :note="getComponentData(note)"
:key="note.id" :key="note.id"
/> />
</ul> </ul>
<comment-form /> <comment-form
:noteable-type="noteableType"
/>
</div> </div>
</template> </template>
export const DISCUSSION_NOTE = 'DiscussionNote'; export const DISCUSSION_NOTE = 'DiscussionNote';
export const DIFF_NOTE = 'DiffNote';
export const DISCUSSION = 'discussion'; export const DISCUSSION = 'discussion';
export const NOTE = 'note'; export const NOTE = 'note';
export const SYSTEM_NOTE = 'systemNote'; export const SYSTEM_NOTE = 'systemNote';
...@@ -8,4 +9,7 @@ export const REOPENED = 'reopened'; ...@@ -8,4 +9,7 @@ export const REOPENED = 'reopened';
export const CLOSED = 'closed'; export const CLOSED = 'closed';
export const EMOJI_THUMBSUP = 'thumbsup'; export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown'; export const EMOJI_THUMBSDOWN = 'thumbsdown';
export const NOTEABLE_TYPE = 'Issue'; export const ISSUE_NOTEABLE_TYPE = 'issue';
export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post';
...@@ -20,17 +20,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -20,17 +20,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
return { return {
noteableData: JSON.parse(notesDataset.noteableData), noteableData: JSON.parse(notesDataset.noteableData),
currentUserData, currentUserData,
notesData: { notesData: JSON.parse(notesDataset.notesData),
lastFetchedAt: notesDataset.lastFetchedAt,
discussionsPath: notesDataset.discussionsPath,
newSessionPath: notesDataset.newSessionPath,
registerPath: notesDataset.registerPath,
notesPath: notesDataset.notesPath,
markdownDocsPath: notesDataset.markdownDocsPath,
quickActionsDocsPath: notesDataset.quickActionsDocsPath,
closeIssuePath: notesDataset.closeIssuePath,
reopenIssuePath: notesDataset.reopenIssuePath,
},
}; };
}, },
render(createElement) { render(createElement) {
......
import Autosave from '../../autosave'; import Autosave from '../../autosave';
import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export default { export default {
methods: { methods: {
initAutoSave() { initAutoSave(noteableType) {
this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', 'Issue', this.note.id], 'issue'); this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', capitalizeFirstCharacter(noteableType), this.note.id]);
}, },
resetAutoSave() { resetAutoSave() {
this.autosave.reset(); this.autosave.reset();
......
import * as constants from '../constants';
export default {
props: {
note: {
type: Object,
required: true,
},
},
computed: {
noteableType() {
switch (this.note.noteable_type) {
case 'MergeRequest':
return constants.MERGE_REQUEST_NOTEABLE_TYPE;
case 'Issue':
return constants.ISSUE_NOTEABLE_TYPE;
default:
return '';
}
},
},
};
import Flash from '~/flash';
import { __ } from '~/locale';
export default {
props: {
note: {
type: Object,
required: true,
},
},
computed: {
discussionResolved() {
const { notes, resolved } = this.note;
if (notes) { // Decide resolved state using store. Only valid for discussions.
return notes.every(note => note.resolved && !note.system);
}
return resolved;
},
resolveButtonTitle() {
if (this.updatedNoteBody) {
if (this.discussionResolved) {
return __('Comment and unresolve discussion');
}
return __('Comment and resolve discussion');
}
return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion');
},
},
methods: {
resolveHandler(resolvedState = false) {
this.isResolving = true;
const endpoint = this.note.resolve_path || `${this.note.path}/resolve`;
const isResolved = this.discussionResolved || resolvedState;
const discussion = this.resolveAsThread;
this.toggleResolveNote({ endpoint, isResolved, discussion })
.then(() => {
this.isResolving = false;
})
.catch(() => {
this.isResolving = false;
const msg = __('Something went wrong while resolving this discussion. Please try again.');
Flash(msg, 'alert', this.$el);
});
},
},
};
import Vue from 'vue'; import Vue from 'vue';
import VueResource from 'vue-resource'; import VueResource from 'vue-resource';
import * as constants from '../constants';
Vue.use(VueResource); Vue.use(VueResource);
...@@ -19,6 +20,12 @@ export default { ...@@ -19,6 +20,12 @@ export default {
createNewNote(endpoint, data) { createNewNote(endpoint, data) {
return Vue.http.post(endpoint, data, { emulateJSON: true }); return Vue.http.post(endpoint, data, { emulateJSON: true });
}, },
toggleResolveNote(endpoint, isResolved) {
const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants;
const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME;
return Vue.http[method](endpoint);
},
poll(data = {}) { poll(data = {}) {
const { endpoint, lastFetchedAt } = data; const { endpoint, lastFetchedAt } = data;
const options = { const options = {
......
...@@ -61,8 +61,17 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service ...@@ -61,8 +61,17 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service
export const removePlaceholderNotes = ({ commit }) => export const removePlaceholderNotes = ({ commit }) =>
commit(types.REMOVE_PLACEHOLDER_NOTES); commit(types.REMOVE_PLACEHOLDER_NOTES);
export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) => service
.toggleResolveNote(endpoint, isResolved)
.then(res => res.json())
.then((res) => {
const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
commit(mutationType, res);
});
export const closeIssue = ({ commit, dispatch, state }) => service export const closeIssue = ({ commit, dispatch, state }) => service
.toggleIssueState(state.notesData.closeIssuePath) .toggleIssueState(state.notesData.closePath)
.then(res => res.json()) .then(res => res.json())
.then((data) => { .then((data) => {
commit(types.CLOSE_ISSUE); commit(types.CLOSE_ISSUE);
...@@ -70,7 +79,7 @@ export const closeIssue = ({ commit, dispatch, state }) => service ...@@ -70,7 +79,7 @@ export const closeIssue = ({ commit, dispatch, state }) => service
}); });
export const reopenIssue = ({ commit, dispatch, state }) => service export const reopenIssue = ({ commit, dispatch, state }) => service
.toggleIssueState(state.notesData.reopenIssuePath) .toggleIssueState(state.notesData.reopenPath)
.then(res => res.json()) .then(res => res.json())
.then((data) => { .then((data) => {
commit(types.REOPEN_ISSUE); commit(types.REOPEN_ISSUE);
...@@ -80,7 +89,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => service ...@@ -80,7 +89,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => service
export const emitStateChangedEvent = ({ commit, getters }, data) => { export const emitStateChangedEvent = ({ commit, getters }, data) => {
const event = new CustomEvent('issuable_vue_app:change', { detail: { const event = new CustomEvent('issuable_vue_app:change', { detail: {
data, data,
isClosed: getters.issueState === constants.CLOSED, isClosed: getters.openState === constants.CLOSED,
} }); } });
document.dispatchEvent(event); document.dispatchEvent(event);
...@@ -174,7 +183,7 @@ const pollSuccessCallBack = (resp, commit, state, getters) => { ...@@ -174,7 +183,7 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
resp.notes.forEach((note) => { resp.notes.forEach((note) => {
if (notesById[note.id]) { if (notesById[note.id]) {
commit(types.UPDATE_NOTE, note); commit(types.UPDATE_NOTE, note);
} else if (note.type === constants.DISCUSSION_NOTE) { } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
const discussion = utils.findNoteObjectById(state.notes, note.discussion_id); const discussion = utils.findNoteObjectById(state.notes, note.discussion_id);
if (discussion) { if (discussion) {
......
...@@ -8,7 +8,7 @@ export const getNotesDataByProp = state => prop => state.notesData[prop]; ...@@ -8,7 +8,7 @@ export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getNoteableData = state => state.noteableData; export const getNoteableData = state => state.noteableData;
export const getNoteableDataByProp = state => prop => state.noteableData[prop]; export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export const issueState = state => state.noteableData.state; export const openState = state => state.noteableData.state;
export const getUserData = state => state.userData || {}; export const getUserData = state => state.userData || {};
export const getUserDataByProp = state => prop => state.userData && state.userData[prop]; export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
...@@ -30,3 +30,37 @@ export const getCurrentUserLastNote = state => _.flatten( ...@@ -30,3 +30,37 @@ export const getCurrentUserLastNote = state => _.flatten(
export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes) export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes)
.find(el => isLastNote(el, state)); .find(el => isLastNote(el, state));
export const discussionCount = (state) => {
const discussions = state.notes.filter(n => !n.individual_note);
return discussions.length;
};
export const unresolvedDiscussions = (state, getters) => {
const resolvedMap = getters.resolvedDiscussionsById;
return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]);
};
export const resolvedDiscussionsById = (state) => {
const map = {};
state.notes.forEach((n) => {
if (n.notes) {
const resolved = n.notes.every(note => note.resolved && !note.system);
if (resolved) {
map[n.id] = n;
}
}
});
return map;
};
export const resolvedDiscussionCount = (state, getters) => {
const resolvedMap = getters.resolvedDiscussionsById;
return Object.keys(resolvedMap).length;
};
...@@ -12,6 +12,7 @@ export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE'; ...@@ -12,6 +12,7 @@ export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
export const TOGGLE_AWARD = 'TOGGLE_AWARD'; export const TOGGLE_AWARD = 'TOGGLE_AWARD';
export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION'; export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
export const UPDATE_NOTE = 'UPDATE_NOTE'; export const UPDATE_NOTE = 'UPDATE_NOTE';
export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
// Issue // Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE'; export const CLOSE_ISSUE = 'CLOSE_ISSUE';
......
import * as utils from './utils'; import * as utils from './utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
import * as constants from '../constants'; import * as constants from '../constants';
import { isInMRPage } from '../../lib/utils/common_utils';
export default { export default {
[types.ADD_NEW_NOTE](state, note) { [types.ADD_NEW_NOTE](state, note) {
const { discussion_id, type } = note; const { discussion_id, type } = note;
const [exists] = state.notes.filter(n => n.id === note.discussion_id); const [exists] = state.notes.filter(n => n.id === note.discussion_id);
const isDiscussion = (type === constants.DISCUSSION_NOTE);
if (!exists) { if (!exists) {
const noteData = { const noteData = {
expanded: true, expanded: true,
id: discussion_id, id: discussion_id,
individual_note: !(type === constants.DISCUSSION_NOTE), individual_note: !isDiscussion,
notes: [note], notes: [note],
reply_id: discussion_id, reply_id: discussion_id,
}; };
if (isDiscussion && isInMRPage()) {
noteData.resolvable = note.resolvable;
noteData.resolved = false;
noteData.resolve_path = note.resolve_path;
noteData.resolve_with_issue_path = note.resolve_with_issue_path;
}
state.notes.push(noteData); state.notes.push(noteData);
document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
} }
}, },
...@@ -25,6 +35,7 @@ export default { ...@@ -25,6 +35,7 @@ export default {
if (noteObj) { if (noteObj) {
noteObj.notes.push(note); noteObj.notes.push(note);
document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
} }
}, },
...@@ -41,6 +52,8 @@ export default { ...@@ -41,6 +52,8 @@ export default {
state.notes.splice(state.notes.indexOf(noteObj), 1); state.notes.splice(state.notes.indexOf(noteObj), 1);
} }
} }
document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
}, },
[types.REMOVE_PLACEHOLDER_NOTES](state) { [types.REMOVE_PLACEHOLDER_NOTES](state) {
...@@ -77,15 +90,19 @@ export default { ...@@ -77,15 +90,19 @@ export default {
const notes = []; const notes = [];
notesData.forEach((note) => { notesData.forEach((note) => {
const nn = Object.assign({}, note);
// To support legacy notes, should be very rare case. // To support legacy notes, should be very rare case.
if (note.individual_note && note.notes.length > 1) { if (note.individual_note && note.notes.length > 1) {
note.notes.forEach((n) => { note.notes.forEach((n) => {
const nn = Object.assign({}, note);
nn.notes = [n]; // override notes array to only have one item to mimick individual_note nn.notes = [n]; // override notes array to only have one item to mimick individual_note
notes.push(nn); notes.push(nn);
}); });
} else { } else {
notes.push(note); const oldNote = utils.findNoteObjectById(state.notes, note.id);
nn.expanded = oldNote ? oldNote.expanded : note.expanded;
notes.push(nn);
} }
}); });
...@@ -134,6 +151,8 @@ export default { ...@@ -134,6 +151,8 @@ export default {
user: { id, name, username }, user: { id, name, username },
}); });
} }
document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
}, },
[types.TOGGLE_DISCUSSION](state, { discussionId }) { [types.TOGGLE_DISCUSSION](state, { discussionId }) {
...@@ -151,6 +170,24 @@ export default { ...@@ -151,6 +170,24 @@ export default {
const comment = utils.findNoteObjectById(noteObj.notes, note.id); const comment = utils.findNoteObjectById(noteObj.notes, note.id);
noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note); noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
} }
// document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
},
[types.UPDATE_DISCUSSION](state, noteData) {
const note = noteData;
let index = 0;
state.notes.forEach((n, i) => {
if (n.id === note.id) {
index = i;
}
});
note.expanded = true; // override expand flag to prevent collapse
state.notes.splice(index, 1, note);
document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
}, },
[types.CLOSE_ISSUE](state) { [types.CLOSE_ISSUE](state) {
......
...@@ -28,4 +28,3 @@ export const getQuickActionText = (note) => { ...@@ -28,4 +28,3 @@ 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();
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import UsersSelect from '~/users_select'; import UsersSelect from '~/users_select';
import { isMetaClick } from '~/lib/utils/common_utils'; import { isMetaClick } from '~/lib/utils/common_utils';
import { __ } from '../../../../locale'; import { __ } from '~/locale';
import flash from '../../../../flash'; import flash from '~/flash';
import axios from '../../../../lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
export default class Todos { export default class Todos {
constructor() { constructor() {
......
import '~/profile/gl_crop';
import Profile from '~/profile/profile';
document.addEventListener('DOMContentLoaded', () => {
$(document).on('input.ssh_key', '#key_key', function () { // eslint-disable-line func-names
const $title = $('#key_title');
const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
// Extract the SSH Key title from its comment
if (comment && comment.length > 1) {
$title.val(comment[1]).change();
}
});
new Profile(); // eslint-disable-line no-new
});
import initCycleAnalytics from '~/cycle_analytics/cycle_analytics_bundle';
document.addEventListener('DOMContentLoaded', initCycleAnalytics);
import initEnviroments from '~/environments/';
document.addEventListener('DOMContentLoaded', initEnviroments);
import initIssuableSidebar from '~/init_issuable_sidebar';
import Issue from '~/issue';
import ShortcutsIssuable from '~/shortcuts_issuable';
import ZenMode from '~/zen_mode';
import '~/notes/index';
import '~/issue_show/index';
export default function () {
new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar();
}
import initIssuableSidebar from '~/init_issuable_sidebar'; import initSidebarBundle from '~/sidebar/sidebar_bundle';
import Issue from '~/issue'; import initShow from '../show';
import ShortcutsIssuable from '~/shortcuts_issuable';
import ZenMode from '~/zen_mode';
import '~/notes/index';
import '~/issue_show/index';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new Issue(); // eslint-disable-line no-new initShow();
new ShortcutsIssuable(); // eslint-disable-line no-new initSidebarBundle();
new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar();
}); });
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initMergeConflicts from '~/merge_conflicts/merge_conflicts_bundle';
document.addEventListener('DOMContentLoaded', () => {
initSidebarBundle();
initMergeConflicts();
});
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initMergeConflicts from '~/merge_conflicts/merge_conflicts_bundle';
document.addEventListener('DOMContentLoaded', () => {
initSidebarBundle();
initMergeConflicts();
});
import MergeRequest from '~/merge_request';
import ZenMode from '~/zen_mode';
import initNotes from '~/init_notes';
import initIssuableSidebar from '~/init_issuable_sidebar';
import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import ShortcutsIssuable from '~/shortcuts_issuable';
import Diff from '~/diff';
import { handleLocationHash } from '~/lib/utils/common_utils';
import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initWidget from '../../../vue_merge_request_widget';
export default function () {
new Diff(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar();
initNotes();
initDiffNotes();
initPipelines();
const mrShowNode = document.querySelector('.merge-request');
window.mergeRequest = new MergeRequest({
action: mrShowNode.dataset.mrAction,
});
new ShortcutsIssuable(true); // eslint-disable-line no-new
handleLocationHash();
howToMerge();
initWidget();
}
import MergeRequest from '~/merge_request'; import initSidebarBundle from '~/sidebar/sidebar_bundle';
import ZenMode from '~/zen_mode'; import initShow from '../init_merge_request_show';
import initNotes from '~/init_notes';
import initIssuableSidebar from '~/init_issuable_sidebar';
import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import ShortcutsIssuable from '~/shortcuts_issuable';
import Diff from '~/diff';
import { handleLocationHash } from '~/lib/utils/common_utils';
import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initWidget from '../../../../vue_merge_request_widget';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new initShow();
new ZenMode(); // eslint-disable-line no-new initSidebarBundle();
initIssuableSidebar();
initNotes();
initDiffNotes();
initPipelines();
const mrShowNode = document.querySelector('.merge-request');
window.mergeRequest = new MergeRequest({
action: mrShowNode.dataset.mrAction,
});
new ShortcutsIssuable(true); // eslint-disable-line no-new
handleLocationHash();
howToMerge();
initWidget();
}); });
import initPipelineDetails from '~/pipelines/pipeline_details_bundle';
import initPipelines from '../init_pipelines'; import initPipelines from '../init_pipelines';
document.addEventListener('DOMContentLoaded', initPipelines); document.addEventListener('DOMContentLoaded', () => {
initPipelines();
initPipelineDetails();
});
import initPipelines from '../init_pipelines';
document.addEventListener('DOMContentLoaded', initPipelines);
import initPipelineDetails from '~/pipelines/pipeline_details_bundle';
import initPipelines from '../init_pipelines'; import initPipelines from '../init_pipelines';
document.addEventListener('DOMContentLoaded', initPipelines); document.addEventListener('DOMContentLoaded', () => {
initPipelines();
initPipelineDetails();
});
import initRegistryImages from '~/registry/index';
document.addEventListener('DOMContentLoaded', initRegistryImages);
/* eslint-disable no-new */
import ProtectedTagCreate from '~/protected_tags/protected_tag_create';
import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
import initDeployKeys from '~/deploy_keys'; import initDeployKeys from '~/deploy_keys';
import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new ProtectedTagCreate();
new ProtectedTagEditList();
initDeployKeys(); initDeployKeys();
initSettingsPanels(); initSettingsPanels();
new ProtectedBranchCreate(); // eslint-disable-line no-new
new ProtectedBranchEditList(); // eslint-disable-line no-new
}); });
import initSnippet from '~/snippet/snippet_bundle';
import initForm from '~/pages/projects/init_form'; import initForm from '~/pages/projects/init_form';
document.addEventListener('DOMContentLoaded', () => initForm($('.snippet-form'))); document.addEventListener('DOMContentLoaded', () => {
initSnippet();
initForm($('.snippet-form'));
});
import initSnippet from '~/snippet/snippet_bundle';
import initForm from '~/pages/projects/init_form'; import initForm from '~/pages/projects/init_form';
document.addEventListener('DOMContentLoaded', () => initForm($('.snippet-form'))); document.addEventListener('DOMContentLoaded', () => {
initSnippet();
initForm($('.snippet-form'));
});
import FilteredSearchManager from '~/filtered_search/filtered_search_manager'; import FilteredSearchManager from '~/filtered_search/filtered_search_manager';
export default ({ page, filteredSearchTokenKeys, stateFiltersSelector }) => { export default ({
page,
filteredSearchTokenKeys,
isGroup,
isGroupAncestor,
stateFiltersSelector,
}) => {
const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search'); const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search');
if (filteredSearchEnabled) { if (filteredSearchEnabled) {
const filteredSearchManager = new FilteredSearchManager({ const filteredSearchManager = new FilteredSearchManager({
page, page,
isGroup,
isGroupAncestor,
filteredSearchTokenKeys, filteredSearchTokenKeys,
stateFiltersSelector, stateFiltersSelector,
}); });
......
import initSnippet from '~/snippet/snippet_bundle';
import form from '../form'; import form from '../form';
document.addEventListener('DOMContentLoaded', form); document.addEventListener('DOMContentLoaded', () => {
initSnippet();
form();
});
import initSnippet from '~/snippet/snippet_bundle';
import form from '../form'; import form from '../form';
document.addEventListener('DOMContentLoaded', form); document.addEventListener('DOMContentLoaded', () => {
initSnippet();
form();
});
...@@ -316,7 +316,7 @@ ...@@ -316,7 +316,7 @@
v-if="pipeline.flags.cancelable" v-if="pipeline.flags.cancelable"
:endpoint="pipeline.cancel_path" :endpoint="pipeline.cancel_path"
css-class="js-pipelines-cancel-button btn-remove" css-class="js-pipelines-cancel-button btn-remove"
title="Cancel" title="Stop"
icon="close" icon="close"
:pipeline-id="pipeline.id" :pipeline-id="pipeline.id"
data-toggle="modal" data-toggle="modal"
......
...@@ -6,14 +6,13 @@ import PipelinesMediator from './pipeline_details_mediator'; ...@@ -6,14 +6,13 @@ import PipelinesMediator from './pipeline_details_mediator';
import pipelineGraph from './components/graph/graph_component.vue'; import pipelineGraph from './components/graph/graph_component.vue';
import pipelineHeader from './components/header_component.vue'; import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub'; import eventHub from './event_hub';
import SecurityReportApp from './components/security_reports/security_report_app.vue';
import SastSummaryWidget from './components/security_reports/sast_report_summary_widget.vue';
Vue.use(Translate); import SecurityReportApp from 'ee/pipelines/components/security_reports/security_report_app.vue'; // eslint-disable-line import/first
import SastSummaryWidget from 'ee/pipelines/components/security_reports/sast_report_summary_widget.vue'; // eslint-disable-line import/first
Vue.use(Translate); Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => { export default () => {
const dataset = document.querySelector('.js-pipeline-details-vue').dataset; const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint }); const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
...@@ -143,4 +142,4 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -143,4 +142,4 @@ document.addEventListener('DOMContentLoaded', () => {
}, },
}); });
} }
}); };
/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */ /* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { getPagePath } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import flash from '../flash'; import flash from '../flash';
((global) => { export default class Profile {
class Profile { constructor({ form } = {}) {
constructor({ form } = {}) { this.onSubmitForm = this.onSubmitForm.bind(this);
this.onSubmitForm = this.onSubmitForm.bind(this); this.form = form || $('.edit-user');
this.form = form || $('.edit-user'); this.newRepoActivated = Cookies.get('new_repo');
this.newRepoActivated = Cookies.get('new_repo'); this.setRepoRadio();
this.setRepoRadio(); this.bindEvents();
this.bindEvents(); this.initAvatarGlCrop();
this.initAvatarGlCrop(); }
}
initAvatarGlCrop() {
const cropOpts = {
filename: '.js-avatar-filename',
previewImage: '.avatar-image .avatar',
modalCrop: '.modal-profile-crop',
pickImageEl: '.js-choose-user-avatar-button',
uploadImageBtn: '.js-upload-user-avatar',
modalCropImg: '.modal-profile-crop-image'
};
this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
}
bindEvents() { initAvatarGlCrop() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); const cropOpts = {
$('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie); filename: '.js-avatar-filename',
$('#user_notification_email').on('change', this.submitForm); previewImage: '.avatar-image .avatar',
$('#user_notified_of_own_activity').on('change', this.submitForm); modalCrop: '.modal-profile-crop',
this.form.on('submit', this.onSubmitForm); pickImageEl: '.js-choose-user-avatar-button',
} uploadImageBtn: '.js-upload-user-avatar',
modalCropImg: '.modal-profile-crop-image'
};
this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
}
submitForm() { bindEvents() {
return $(this).parents('form').submit(); $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
} $('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie);
$('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
this.form.on('submit', this.onSubmitForm);
}
onSubmitForm(e) { submitForm() {
e.preventDefault(); return $(this).parents('form').submit();
return this.saveForm(); }
}
saveForm() { onSubmitForm(e) {
const self = this; e.preventDefault();
const formData = new FormData(this.form[0]); return this.saveForm();
const avatarBlob = this.avatarGlCrop.getBlob(); }
if (avatarBlob != null) { saveForm() {
formData.append('user[avatar]', avatarBlob, 'avatar.png'); const self = this;
} const formData = new FormData(this.form[0]);
const avatarBlob = this.avatarGlCrop.getBlob();
axios({ if (avatarBlob != null) {
method: this.form.attr('method'), formData.append('user[avatar]', avatarBlob, 'avatar.png');
url: this.form.attr('action'),
data: formData,
})
.then(({ data }) => flash(data.message, 'notice'))
.then(() => {
window.scrollTo(0, 0);
// Enable submit button after requests ends
self.form.find(':input[disabled]').enable();
})
.catch(error => flash(error.message));
} }
setNewRepoCookie() { axios({
if (this.value === 'off') { method: this.form.attr('method'),
Cookies.remove('new_repo'); url: this.form.attr('action'),
} else { data: formData,
Cookies.set('new_repo', true, { expires_in: 365 }); })
} .then(({ data }) => flash(data.message, 'notice'))
} .then(() => {
window.scrollTo(0, 0);
// Enable submit button after requests ends
self.form.find(':input[disabled]').enable();
})
.catch(error => flash(error.message));
}
setRepoRadio() { setNewRepoCookie() {
const multiEditRadios = $('input[name="user[multi_file]"]'); if (this.value === 'off') {
if (this.newRepoActivated || this.newRepoActivated === 'true') { Cookies.remove('new_repo');
multiEditRadios.filter('[value=on]').prop('checked', true); } else {
} else { Cookies.set('new_repo', true, { expires_in: 365 });
multiEditRadios.filter('[value=off]').prop('checked', true);
}
} }
} }
$(function() { setRepoRadio() {
$(document).on('input.ssh_key', '#key_key', function() { const multiEditRadios = $('input[name="user[multi_file]"]');
const $title = $('#key_title'); if (this.newRepoActivated || this.newRepoActivated === 'true') {
const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); multiEditRadios.filter('[value=on]').prop('checked', true);
} else {
// Extract the SSH Key title from its comment multiEditRadios.filter('[value=off]').prop('checked', true);
if (comment && comment.length > 1) {
return $title.val(comment[1]).change();
}
});
if (getPagePath() === 'profiles') {
return new Profile();
} }
}); }
})(window.gl || (window.gl = {})); }
import './gl_crop';
import './profile';
/* eslint-disable no-unused-vars */
import ProtectedBranchCreate from './protected_branch_create';
import ProtectedBranchEditList from './protected_branch_edit_list';
$(() => {
const protectedBranchCreate = new ProtectedBranchCreate();
const protectedBranchEditList = new ProtectedBranchEditList();
});
/* eslint-disable no-unused-vars */
import ProtectedTagCreate from './protected_tag_create';
import ProtectedTagEditList from './protected_tag_edit_list';
$(() => {
const protectedtTagCreate = new ProtectedTagCreate();
const protectedtTagEditList = new ProtectedTagEditList();
});
...@@ -4,7 +4,7 @@ import Translate from '../vue_shared/translate'; ...@@ -4,7 +4,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate); Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({ export default () => new Vue({
el: '#js-vue-registry-images', el: '#js-vue-registry-images',
components: { components: {
registryApp, registryApp,
...@@ -22,4 +22,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -22,4 +22,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
}, },
}); });
}, },
})); });
<script>
export default { export default {
name: 'Assignees', name: 'Assignees',
data() {
return {
defaultRenderCount: 5,
defaultMaxCounter: 99,
showLess: true,
};
},
props: { props: {
rootPath: { rootPath: {
type: String, type: String,
...@@ -21,6 +15,13 @@ export default { ...@@ -21,6 +15,13 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
defaultRenderCount: 5,
defaultMaxCounter: 99,
showLess: true,
};
},
computed: { computed: {
firstUser() { firstUser() {
return this.users[0]; return this.users[0];
...@@ -101,125 +102,130 @@ export default { ...@@ -101,125 +102,130 @@ export default {
return index === 0 || firstTwo; return index === 0 || firstTwo;
}, },
}, },
template: ` };
<div> </script>
<div
class="sidebar-collapsed-icon sidebar-collapsed-user" <template>
:class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }" <div>
data-container="body" <div
data-placement="left" class="sidebar-collapsed-icon sidebar-collapsed-user"
:title="collapsedTooltipTitle" :class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
data-container="body"
data-placement="left"
:title="collapsedTooltipTitle"
>
<i
v-if="hasNoUsers"
aria-label="No Assignee"
class="fa fa-user"
> >
<i </i>
v-if="hasNoUsers" <button
aria-label="No Assignee" type="button"
class="fa fa-user" class="btn-link"
v-for="(user, index) in users"
v-if="shouldRenderCollapsedAssignee(index)"
:key="user.id"
>
<img
width="24"
class="avatar avatar-inline s24"
:alt="assigneeAlt(user)"
:src="avatarUrl(user)"
/> />
<button <span class="author">
type="button" {{ user.name }}
class="btn-link" </span>
v-for="(user, index) in users" </button>
v-if="shouldRenderCollapsedAssignee(index)" <button
v-if="hasMoreThanTwoAssignees"
class="btn-link"
type="button"
>
<span
class="avatar-counter sidebar-avatar-counter"
>
{{ sidebarAvatarCounter }}
</span>
</button>
</div>
<div class="value hide-collapsed">
<template v-if="hasNoUsers">
<span class="assign-yourself no-value">
No assignee
<template v-if="editable">
-
<button
type="button"
class="btn-link"
@click="assignSelf"
>
assign yourself
</button>
</template>
</span>
</template>
<template v-else-if="hasOneUser">
<a
class="author_link bold"
:href="assigneeUrl(firstUser)"
> >
<img <img
width="24" width="32"
class="avatar avatar-inline s24" class="avatar avatar-inline s32"
:alt="assigneeAlt(user)" :alt="assigneeAlt(firstUser)"
:src="avatarUrl(user)" :src="avatarUrl(firstUser)"
/> />
<span class="author"> <span class="author">
{{ user.name }} {{ firstUser.name }}
</span>
</button>
<button
v-if="hasMoreThanTwoAssignees"
class="btn-link"
type="button"
>
<span
class="avatar-counter sidebar-avatar-counter"
>
{{ sidebarAvatarCounter }}
</span> </span>
</button> <span class="username">
</div> {{ assigneeUsername(firstUser) }}
<div class="value hide-collapsed">
<template v-if="hasNoUsers">
<span class="assign-yourself no-value">
No assignee
<template v-if="editable">
-
<button
type="button"
class="btn-link"
@click="assignSelf"
>
assign yourself
</button>
</template>
</span> </span>
</template> </a>
<template v-else-if="hasOneUser"> </template>
<a <template v-else>
class="author_link bold" <div class="user-list">
:href="assigneeUrl(firstUser)"
>
<img
width="32"
class="avatar avatar-inline s32"
:alt="assigneeAlt(firstUser)"
:src="avatarUrl(firstUser)"
/>
<span class="author">
{{ firstUser.name }}
</span>
<span class="username">
{{ assigneeUsername(firstUser) }}
</span>
</a>
</template>
<template v-else>
<div class="user-list">
<div
class="user-item"
v-for="(user, index) in users"
v-if="renderAssignee(index)"
>
<a
class="user-link has-tooltip"
data-container="body"
data-placement="bottom"
:href="assigneeUrl(user)"
:data-title="user.name"
>
<img
width="32"
class="avatar avatar-inline s32"
:alt="assigneeAlt(user)"
:src="avatarUrl(user)"
/>
</a>
</div>
</div>
<div <div
v-if="renderShowMoreSection" class="user-item"
class="user-list-more" v-for="(user, index) in users"
v-if="renderAssignee(index)"
:key="user.id"
> >
<button <a
type="button" class="user-link has-tooltip"
class="btn-link" data-container="body"
@click="toggleShowLess" data-placement="bottom"
:href="assigneeUrl(user)"
:data-title="user.name"
> >
<template v-if="showLess"> <img
{{ hiddenAssigneesLabel }} width="32"
</template> class="avatar avatar-inline s32"
<template v-else> :alt="assigneeAlt(user)"
- show less :src="avatarUrl(user)"
</template> />
</button> </a>
</div> </div>
</template> </div>
</div> <div
v-if="renderShowMoreSection"
class="user-list-more"
>
<button
type="button"
class="btn-link"
@click="toggleShowLess"
>
<template v-if="showLess">
{{ hiddenAssigneesLabel }}
</template>
<template v-else>
- show less
</template>
</button>
</div>
</template>
</div> </div>
`, </div>
}; </template>
import Flash from '../../../flash'; import Flash from '../../../flash';
import AssigneeTitle from './assignee_title'; import AssigneeTitle from './assignee_title';
import Assignees from './assignees'; import Assignees from './assignees.vue';
import Store from '../../stores/sidebar_store'; import Store from '../../stores/sidebar_store';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
...@@ -28,8 +28,8 @@ export default { ...@@ -28,8 +28,8 @@ export default {
}, },
}, },
components: { components: {
'assignee-title': AssigneeTitle, AssigneeTitle,
assignees: Assignees, Assignees,
}, },
methods: { methods: {
assignSelf() { assignSelf() {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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