Commit f4023a69 authored by Jan Provaznik's avatar Jan Provaznik

Merge remote-tracking branch 'origin/master' into dev-master

parents 79911cb4 a0ca86e5
Dangerfile gitlab-language=ruby Dangerfile gitlab-language=ruby
db/schema.rb merge=merge_db_schema
...@@ -694,7 +694,10 @@ gitlab:setup-mysql: ...@@ -694,7 +694,10 @@ gitlab:setup-mysql:
# Frontend-related jobs # Frontend-related jobs
gitlab:assets:compile: gitlab:assets:compile:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job <<: *dedicated-no-docs-and-no-qa-pull-cache-job
image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-git-2.18-chrome-69.0-node-8.x-yarn-1.2-graphicsmagick-1.3.29-docker-18.06.1
dependencies: [] dependencies: []
services:
- docker:stable-dind
variables: variables:
NODE_ENV: "production" NODE_ENV: "production"
RAILS_ENV: "production" RAILS_ENV: "production"
...@@ -703,18 +706,23 @@ gitlab:assets:compile: ...@@ -703,18 +706,23 @@ gitlab:assets:compile:
WEBPACK_REPORT: "true" WEBPACK_REPORT: "true"
# we override the max_old_space_size to prevent OOM errors # we override the max_old_space_size to prevent OOM errors
NODE_OPTIONS: --max_old_space_size=3584 NODE_OPTIONS: --max_old_space_size=3584
DOCKER_DRIVER: overlay2
DOCKER_HOST: tcp://docker:2375
script: script:
- date - date
- yarn install --frozen-lockfile --production --cache-folder .yarn-cache - yarn install --frozen-lockfile --production --cache-folder .yarn-cache
- date - date
- free -m - free -m
- bundle exec rake gitlab:assets:compile - bundle exec rake gitlab:assets:compile
- scripts/build_assets_image
artifacts: artifacts:
name: webpack-report name: webpack-report
expire_in: 31d expire_in: 31d
paths: paths:
- webpack-report/ - webpack-report/
- public/assets/ - public/assets/
tags:
- docker
karma: karma:
<<: *dedicated-no-docs-pull-cache-job <<: *dedicated-no-docs-pull-cache-job
......
# Simple container to store assets for later use
FROM scratch
ADD public/assets /assets/
CMD /bin/true
...@@ -13,7 +13,7 @@ export default () => { ...@@ -13,7 +13,7 @@ export default () => {
if (editBlobForm.length) { if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot'); const urlRoot = editBlobForm.data('relativeUrlRoot');
const assetsPath = editBlobForm.data('assetsPrefix'); const assetsPath = editBlobForm.data('assetsPrefix');
const filePath = editBlobForm.data('blobFilename') const filePath = editBlobForm.data('blobFilename');
const currentAction = $('.js-file-title').data('currentAction'); const currentAction = $('.js-file-title').data('currentAction');
const projectId = editBlobForm.data('project-id'); const projectId = editBlobForm.data('project-id');
......
...@@ -42,7 +42,7 @@ export default Vue.extend({ ...@@ -42,7 +42,7 @@ export default Vue.extend({
required: true, required: true,
}, },
}, },
data () { data() {
return { return {
detailIssue: boardsStore.detail, detailIssue: boardsStore.detail,
filter: boardsStore.filter, filter: boardsStore.filter,
...@@ -55,27 +55,26 @@ export default Vue.extend({ ...@@ -55,27 +55,26 @@ export default Vue.extend({
}, },
isNewIssueShown() { isNewIssueShown() {
return this.list.type === 'backlog' || (!this.disabled && this.list.type !== 'closed'); return this.list.type === 'backlog' || (!this.disabled && this.list.type !== 'closed');
} },
}, },
watch: { watch: {
filter: { filter: {
handler() { handler() {
this.list.page = 1; this.list.page = 1;
this.list.getIssues(true) this.list.getIssues(true).catch(() => {
.catch(() => { // TODO: handle request error
// TODO: handle request error });
});
}, },
deep: true, deep: true,
} },
}, },
mounted () { mounted() {
this.sortableOptions = getBoardSortableDefaultOptions({ this.sortableOptions = getBoardSortableDefaultOptions({
disabled: this.disabled, disabled: this.disabled,
group: 'boards', group: 'boards',
draggable: '.is-draggable', draggable: '.is-draggable',
handle: '.js-board-handle', handle: '.js-board-handle',
onEnd: (e) => { onEnd: e => {
sortableEnd(); sortableEnd();
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
...@@ -86,14 +85,15 @@ export default Vue.extend({ ...@@ -86,14 +85,15 @@ export default Vue.extend({
boardsStore.moveList(list, order); boardsStore.moveList(list, order);
}); });
} }
} },
}); });
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
}, },
created() { created() {
if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) { if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) {
const isCollapsed = localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false'; const isCollapsed =
localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
this.list.isExpanded = !isCollapsed; this.list.isExpanded = !isCollapsed;
} }
...@@ -107,7 +107,10 @@ export default Vue.extend({ ...@@ -107,7 +107,10 @@ export default Vue.extend({
this.list.isExpanded = !this.list.isExpanded; this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) { if (AccessorUtilities.isLocalStorageAccessSafe()) {
localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded); localStorage.setItem(
`boards.${this.boardId}.${this.list.type}.expanded`,
this.list.isExpanded,
);
} }
} }
}, },
......
...@@ -32,18 +32,18 @@ export default { ...@@ -32,18 +32,18 @@ export default {
boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position'); boardsStore.state.lists = _.sortBy(boardsStore.state.lists, 'position');
// Save the labels // Save the labels
gl.boardService.generateDefaultLists() gl.boardService
.generateDefaultLists()
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => {
data.forEach((listObj) => { data.forEach(listObj => {
const list = boardsStore.findList('title', listObj.title); const list = boardsStore.findList('title', listObj.title);
list.id = listObj.id; list.id = listObj.id;
list.label.id = listObj.label.id; list.label.id = listObj.label.id;
list.getIssues() list.getIssues().catch(() => {
.catch(() => { // TODO: handle request error
// TODO: handle request error });
});
}); });
}) })
.catch(() => { .catch(() => {
...@@ -57,7 +57,6 @@ export default { ...@@ -57,7 +57,6 @@ export default {
clearBlankState: boardsStore.removeBlankState.bind(boardsStore), clearBlankState: boardsStore.removeBlankState.bind(boardsStore),
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
import IssueCardInner from './issue_card_inner.vue'; import IssueCardInner from './issue_card_inner.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
export default { export default {
name: 'BoardsIssueCard', name: 'BoardsIssueCard',
components: { components: {
IssueCardInner, IssueCardInner,
},
props: {
list: {
type: Object,
default: () => ({}),
}, },
props: { issue: {
list: { type: Object,
type: Object, default: () => ({}),
default: () => ({}),
},
issue: {
type: Object,
default: () => ({}),
},
issueLinkBase: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
index: {
type: Number,
default: 0,
},
rootPath: {
type: String,
default: '',
},
groupId: {
type: Number,
},
}, },
data() { issueLinkBase: {
return { type: String,
showDetail: false, default: '',
detailIssue: boardsStore.detail,
};
}, },
computed: { disabled: {
issueDetailVisible() { type: Boolean,
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; default: false,
},
}, },
methods: { index: {
mouseDown() { type: Number,
this.showDetail = true; default: 0,
}, },
mouseMove() { rootPath: {
this.showDetail = false; type: String,
}, default: '',
showIssue(e) { },
if (e.target.classList.contains('js-no-trigger')) return; groupId: {
type: Number,
},
},
data() {
return {
showDetail: false,
detailIssue: boardsStore.detail,
};
},
computed: {
issueDetailVisible() {
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
},
},
methods: {
mouseDown() {
this.showDetail = true;
},
mouseMove() {
this.showDetail = false;
},
showIssue(e) {
if (e.target.classList.contains('js-no-trigger')) return;
if (this.showDetail) { if (this.showDetail) {
this.showDetail = false; this.showDetail = false;
if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) { if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) {
eventHub.$emit('clearDetailIssue'); eventHub.$emit('clearDetailIssue');
} else { } else {
eventHub.$emit('newDetailIssue', this.issue); eventHub.$emit('newDetailIssue', this.issue);
boardsStore.detail.list = this.list; boardsStore.detail.list = this.list;
}
} }
}, }
}, },
}; },
};
</script> </script>
<template> <template>
......
...@@ -62,7 +62,8 @@ export default { ...@@ -62,7 +62,8 @@ export default {
eventHub.$emit(`scroll-board-list-${this.list.id}`); eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel(); this.cancel();
return this.list.newIssue(issue) return this.list
.newIssue(issue)
.then(() => { .then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions // Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable(); $(this.$refs.submitButton).enable();
......
...@@ -38,7 +38,7 @@ export default Vue.extend({ ...@@ -38,7 +38,7 @@ export default Vue.extend({
}; };
}, },
computed: { computed: {
showSidebar () { showSidebar() {
return Object.keys(this.issue).length; return Object.keys(this.issue).length;
}, },
milestoneTitle() { milestoneTitle() {
...@@ -51,18 +51,20 @@ export default Vue.extend({ ...@@ -51,18 +51,20 @@ export default Vue.extend({
return this.issue.labels && this.issue.labels.length; return this.issue.labels && this.issue.labels.length;
}, },
labelDropdownTitle() { labelDropdownTitle() {
return this.hasLabels ? sprintf(__('%{firstLabel} +%{labelCount} more'), { return this.hasLabels
firstLabel: this.issue.labels[0].title, ? sprintf(__('%{firstLabel} +%{labelCount} more'), {
labelCount: this.issue.labels.length - 1 firstLabel: this.issue.labels[0].title,
}) : __('Label'); labelCount: this.issue.labels.length - 1,
})
: __('Label');
}, },
selectedLabels() { selectedLabels() {
return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : ''; return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : '';
} },
}, },
watch: { watch: {
detail: { detail: {
handler () { handler() {
if (this.issue.id !== this.detail.issue.id) { if (this.issue.id !== this.detail.issue.id) {
$('.block.assignee') $('.block.assignee')
.find('input:not(.js-vue)[name="issue[assignee_ids][]"]') .find('input:not(.js-vue)[name="issue[assignee_ids][]"]')
...@@ -71,17 +73,19 @@ export default Vue.extend({ ...@@ -71,17 +73,19 @@ export default Vue.extend({
}); });
$('.js-issue-board-sidebar', this.$el).each((i, el) => { $('.js-issue-board-sidebar', this.$el).each((i, el) => {
$(el).data('glDropdown').clearMenu(); $(el)
.data('glDropdown')
.clearMenu();
}); });
} }
this.issue = this.detail.issue; this.issue = this.detail.issue;
this.list = this.detail.list; this.list = this.detail.list;
}, },
deep: true deep: true,
}, },
}, },
created () { created() {
// Get events from glDropdown // Get events from glDropdown
eventHub.$on('sidebar.removeAssignee', this.removeAssignee); eventHub.$on('sidebar.removeAssignee', this.removeAssignee);
eventHub.$on('sidebar.addAssignee', this.addAssignee); eventHub.$on('sidebar.addAssignee', this.addAssignee);
...@@ -94,7 +98,7 @@ export default Vue.extend({ ...@@ -94,7 +98,7 @@ export default Vue.extend({
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees); eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
}, },
mounted () { mounted() {
new IssuableContext(this.currentUser); new IssuableContext(this.currentUser);
new MilestoneSelect(); new MilestoneSelect();
new DueDateSelectors(); new DueDateSelectors();
...@@ -102,29 +106,30 @@ export default Vue.extend({ ...@@ -102,29 +106,30 @@ export default Vue.extend({
new Sidebar(); new Sidebar();
}, },
methods: { methods: {
closeSidebar () { closeSidebar() {
this.detail.issue = {}; this.detail.issue = {};
}, },
assignSelf () { assignSelf() {
// Notify gl dropdown that we are now assigning to current user // Notify gl dropdown that we are now assigning to current user
this.$refs.assigneeBlock.dispatchEvent(new Event('assignYourself')); this.$refs.assigneeBlock.dispatchEvent(new Event('assignYourself'));
this.addAssignee(this.currentUser); this.addAssignee(this.currentUser);
this.saveAssignees(); this.saveAssignees();
}, },
removeAssignee (a) { removeAssignee(a) {
boardsStore.detail.issue.removeAssignee(a); boardsStore.detail.issue.removeAssignee(a);
}, },
addAssignee (a) { addAssignee(a) {
boardsStore.detail.issue.addAssignee(a); boardsStore.detail.issue.addAssignee(a);
}, },
removeAllAssignees () { removeAllAssignees() {
boardsStore.detail.issue.removeAllAssignees(); boardsStore.detail.issue.removeAllAssignees();
}, },
saveAssignees () { saveAssignees() {
this.loadingAssignees = true; this.loadingAssignees = true;
boardsStore.detail.issue.update() boardsStore.detail.issue
.update()
.then(() => { .then(() => {
this.loadingAssignees = false; this.loadingAssignees = false;
}) })
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
export default { export default {
components: { components: {
UserAvatarLink, UserAvatarLink,
Icon, Icon,
}, },
directives: { directives: {
tooltip, tooltip,
}, },
props: { props: {
issue: { issue: {
type: Object, type: Object,
required: true, required: true,
},
issueLinkBase: {
type: String,
required: true,
},
list: {
type: Object,
required: false,
default: () => ({}),
},
rootPath: {
type: String,
required: true,
},
updateFilters: {
type: Boolean,
required: false,
default: false,
},
groupId: {
type: Number,
required: false,
default: null,
},
},
data() {
return {
limitBeforeCounter: 3,
maxRender: 4,
maxCounter: 99,
};
}, },
computed: { issueLinkBase: {
numberOverLimit() { type: String,
return this.issue.assignees.length - this.limitBeforeCounter; required: true,
}, },
assigneeCounterTooltip() { list: {
return `${this.assigneeCounterLabel} more`; type: Object,
}, required: false,
assigneeCounterLabel() { default: () => ({}),
if (this.numberOverLimit > this.maxCounter) { },
return `${this.maxCounter}+`; rootPath: {
} type: String,
required: true,
return `+${this.numberOverLimit}`; },
}, updateFilters: {
shouldRenderCounter() { type: Boolean,
if (this.issue.assignees.length <= this.maxRender) { required: false,
return false; default: false,
} },
groupId: {
type: Number,
required: false,
default: null,
},
},
data() {
return {
limitBeforeCounter: 3,
maxRender: 4,
maxCounter: 99,
};
},
computed: {
numberOverLimit() {
return this.issue.assignees.length - this.limitBeforeCounter;
},
assigneeCounterTooltip() {
return `${this.assigneeCounterLabel} more`;
},
assigneeCounterLabel() {
if (this.numberOverLimit > this.maxCounter) {
return `${this.maxCounter}+`;
}
return this.issue.assignees.length > this.numberOverLimit; return `+${this.numberOverLimit}`;
}, },
issueId() { shouldRenderCounter() {
if (this.issue.iid) { if (this.issue.assignees.length <= this.maxRender) {
return `#${this.issue.iid}`;
}
return false; return false;
}, }
showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
},
},
methods: {
isIndexLessThanlimit(index) {
return index < this.limitBeforeCounter;
},
shouldRenderAssignee(index) {
// Eg. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
// Otherwise render up to the limitBeforeCounter
if (this.issue.assignees.length <= this.maxRender) {
return index < this.maxRender;
}
return index < this.limitBeforeCounter; return this.issue.assignees.length > this.numberOverLimit;
}, },
assigneeUrl(assignee) { issueId() {
return `${this.rootPath}${assignee.username}`; if (this.issue.iid) {
}, return `#${this.issue.iid}`;
assigneeUrlTitle(assignee) { }
return `Assigned to ${assignee.name}`; return false;
}, },
avatarUrlTitle(assignee) { showLabelFooter() {
return `Avatar for ${assignee.name}`; return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
}, },
showLabel(label) { },
if (!label.id) return false; methods: {
return true; isIndexLessThanlimit(index) {
}, return index < this.limitBeforeCounter;
filterByLabel(label, e) { },
if (!this.updateFilters) return; shouldRenderAssignee(index) {
// Eg. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
// Otherwise render up to the limitBeforeCounter
if (this.issue.assignees.length <= this.maxRender) {
return index < this.maxRender;
}
return index < this.limitBeforeCounter;
},
assigneeUrl(assignee) {
return `${this.rootPath}${assignee.username}`;
},
assigneeUrlTitle(assignee) {
return `Assigned to ${assignee.name}`;
},
avatarUrlTitle(assignee) {
return `Avatar for ${assignee.name}`;
},
showLabel(label) {
if (!label.id) return false;
return true;
},
filterByLabel(label, e) {
if (!this.updateFilters) return;
const filterPath = boardsStore.filter.path.split('&'); const filterPath = boardsStore.filter.path.split('&');
const labelTitle = encodeURIComponent(label.title); const labelTitle = encodeURIComponent(label.title);
const param = `label_name[]=${labelTitle}`; const param = `label_name[]=${labelTitle}`;
const labelIndex = filterPath.indexOf(param); const labelIndex = filterPath.indexOf(param);
$(e.currentTarget).tooltip('hide'); $(e.currentTarget).tooltip('hide');
if (labelIndex === -1) { if (labelIndex === -1) {
filterPath.push(param); filterPath.push(param);
} else { } else {
filterPath.splice(labelIndex, 1); filterPath.splice(labelIndex, 1);
} }
boardsStore.filter.path = filterPath.join('&'); boardsStore.filter.path = filterPath.join('&');
boardsStore.updateFiltersUrl(); boardsStore.updateFiltersUrl();
eventHub.$emit('updateTokens'); eventHub.$emit('updateTokens');
}, },
labelStyle(label) { labelStyle(label) {
return { return {
backgroundColor: label.color, backgroundColor: label.color,
color: label.textColor, color: label.textColor,
}; };
}, },
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
......
...@@ -20,7 +20,7 @@ export default { ...@@ -20,7 +20,7 @@ export default {
computed: { computed: {
contents() { contents() {
const obj = { const obj = {
title: 'You haven\'t added any issues to your project yet', title: "You haven't added any issues to your project yet",
content: ` content: `
An issue can be a bug, a todo or a feature request that needs to be An issue can be a bug, a todo or a feature request that needs to be
discussed in a project. Besides, issues are searchable and filterable. discussed in a project. Besides, issues are searchable and filterable.
...@@ -28,7 +28,7 @@ export default { ...@@ -28,7 +28,7 @@ export default {
}; };
if (this.activeTab === 'selected') { if (this.activeTab === 'selected') {
obj.title = 'You haven\'t selected any issues yet'; obj.title = "You haven't selected any issues yet";
obj.content = ` obj.content = `
Go back to <strong>Open issues</strong> and select some issues Go back to <strong>Open issues</strong> and select some issues
to add to your board. to add to your board.
......
...@@ -42,19 +42,17 @@ export default { ...@@ -42,19 +42,17 @@ export default {
const req = this.buildUpdateRequest(list); const req = this.buildUpdateRequest(list);
// Post the data to the backend // Post the data to the backend
gl.boardService gl.boardService.bulkUpdate(issueIds, req).catch(() => {
.bulkUpdate(issueIds, req) Flash(__('Failed to update issues, please try again.'));
.catch(() => {
Flash(__('Failed to update issues, please try again.'));
selectedIssues.forEach((issue) => { selectedIssues.forEach(issue => {
list.removeIssue(issue); list.removeIssue(issue);
list.issuesSize -= 1; list.issuesSize -= 1;
});
}); });
});
// Add the issues on the frontend // Add the issues on the frontend
selectedIssues.forEach((issue) => { selectedIssues.forEach(issue => {
list.addIssue(issue); list.addIssue(issue);
list.issuesSize += 1; list.issuesSize += 1;
}); });
......
<script> <script>
import ModalFilters from './filters'; import ModalFilters from './filters';
import ModalTabs from './tabs.vue'; import ModalTabs from './tabs.vue';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins'; import modalMixin from '../../mixins/modal_mixins';
export default { export default {
components: { components: {
ModalTabs, ModalTabs,
ModalFilters, ModalFilters,
},
mixins: [modalMixin],
props: {
projectId: {
type: Number,
required: true,
}, },
mixins: [modalMixin], milestonePath: {
props: { type: String,
projectId: { required: true,
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
}, },
data() { labelPath: {
return ModalStore.store; type: String,
required: true,
}, },
computed: { },
selectAllText() { data() {
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { return ModalStore.store;
return 'Select all'; },
} computed: {
selectAllText() {
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
return 'Select all';
}
return 'Deselect all'; return 'Deselect all';
},
showSearch() {
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
},
}, },
methods: { showSearch() {
toggleAll() { return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
this.$refs.selectAllBtn.blur(); },
},
methods: {
toggleAll() {
this.$refs.selectAllBtn.blur();
ModalStore.toggleAll(); ModalStore.toggleAll();
},
}, },
}; },
};
</script> </script>
<template> <template>
<div> <div>
......
<script> <script>
/* global ListIssue */ /* global ListIssue */
import { urlParamsToObject } from '~/lib/utils/common_utils'; import { urlParamsToObject } from '~/lib/utils/common_utils';
import ModalHeader from './header.vue'; import ModalHeader from './header.vue';
import ModalList from './list.vue'; import ModalList from './list.vue';
import ModalFooter from './footer.vue'; import ModalFooter from './footer.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
export default { export default {
components: { components: {
EmptyState, EmptyState,
ModalHeader, ModalHeader,
ModalList, ModalList,
ModalFooter, ModalFooter,
},
props: {
newIssuePath: {
type: String,
required: true,
}, },
props: { emptyStateSvg: {
newIssuePath: { type: String,
type: String, required: true,
required: true,
},
emptyStateSvg: {
type: String,
required: true,
},
issueLinkBase: {
type: String,
required: true,
},
rootPath: {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
}, },
data() { issueLinkBase: {
return ModalStore.store; type: String,
required: true,
}, },
computed: { rootPath: {
showList() { type: String,
if (this.activeTab === 'selected') { required: true,
return this.selectedIssues.length > 0; },
} projectId: {
type: Number,
required: true,
},
milestonePath: {
type: String,
required: true,
},
labelPath: {
type: String,
required: true,
},
},
data() {
return ModalStore.store;
},
computed: {
showList() {
if (this.activeTab === 'selected') {
return this.selectedIssues.length > 0;
}
return this.issuesCount > 0; return this.issuesCount > 0;
}, },
showEmptyState() { showEmptyState() {
if (!this.loading && this.issuesCount === 0) { if (!this.loading && this.issuesCount === 0) {
return true; return true;
} }
return this.activeTab === 'selected' && this.selectedIssues.length === 0; return this.activeTab === 'selected' && this.selectedIssues.length === 0;
},
}, },
watch: { },
page() { watch: {
this.loadIssues(); page() {
}, this.loadIssues();
showAddIssuesModal() { },
if (this.showAddIssuesModal && !this.issues.length) { showAddIssuesModal() {
this.loading = true; if (this.showAddIssuesModal && !this.issues.length) {
this.loading = true;
const loadingDone = () => {
this.loading = false;
};
this.loadIssues()
.then(loadingDone)
.catch(loadingDone);
} else if (!this.showAddIssuesModal) {
this.issues = [];
this.selectedIssues = [];
this.issuesCount = false;
}
},
filter: {
handler() {
if (this.$el.tagName) {
this.page = 1;
this.filterLoading = true;
const loadingDone = () => { const loadingDone = () => {
this.loading = false; this.filterLoading = false;
}; };
this.loadIssues() this.loadIssues(true)
.then(loadingDone) .then(loadingDone)
.catch(loadingDone); .catch(loadingDone);
} else if (!this.showAddIssuesModal) {
this.issues = [];
this.selectedIssues = [];
this.issuesCount = false;
} }
}, },
filter: { deep: true,
handler() {
if (this.$el.tagName) {
this.page = 1;
this.filterLoading = true;
const loadingDone = () => {
this.filterLoading = false;
};
this.loadIssues(true)
.then(loadingDone)
.catch(loadingDone);
}
},
deep: true,
},
}, },
created() { },
this.page = 1; created() {
}, this.page = 1;
methods: { },
loadIssues(clearIssues = false) { methods: {
if (!this.showAddIssuesModal) return false; loadIssues(clearIssues = false) {
if (!this.showAddIssuesModal) return false;
return gl.boardService.getBacklog({ return gl.boardService
.getBacklog({
...urlParamsToObject(this.filter.path), ...urlParamsToObject(this.filter.path),
page: this.page, page: this.page,
per: this.perPage, per: this.perPage,
}) })
.then(res => res.data) .then(res => res.data)
.then(data => { .then(data => {
if (clearIssues) { if (clearIssues) {
this.issues = []; this.issues = [];
} }
data.issues.forEach(issueObj => { data.issues.forEach(issueObj => {
const issue = new ListIssue(issueObj); const issue = new ListIssue(issueObj);
const foundSelectedIssue = ModalStore.findSelectedIssue(issue); const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
issue.selected = !!foundSelectedIssue; issue.selected = !!foundSelectedIssue;
this.issues.push(issue); this.issues.push(issue);
}); });
this.loadingNewPage = false; this.loadingNewPage = false;
if (!this.issuesCount) { if (!this.issuesCount) {
this.issuesCount = data.size; this.issuesCount = data.size;
} }
}) })
.catch(() => { .catch(() => {
// TODO: handle request error // TODO: handle request error
}); });
},
}, },
}; },
};
</script> </script>
<template> <template>
<div <div
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import bp from '../../../breakpoints'; import bp from '../../../breakpoints';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import IssueCardInner from '../issue_card_inner.vue'; import IssueCardInner from '../issue_card_inner.vue';
export default { export default {
components: { components: {
IssueCardInner, IssueCardInner,
Icon, Icon,
},
props: {
issueLinkBase: {
type: String,
required: true,
}, },
props: { rootPath: {
issueLinkBase: { type: String,
type: String, required: true,
required: true,
},
rootPath: {
type: String,
required: true,
},
emptyStateSvg: {
type: String,
required: true,
},
}, },
data() { emptyStateSvg: {
return ModalStore.store; type: String,
required: true,
}, },
computed: { },
loopIssues() { data() {
if (this.activeTab === 'all') { return ModalStore.store;
return this.issues; },
} computed: {
loopIssues() {
if (this.activeTab === 'all') {
return this.issues;
}
return this.selectedIssues; return this.selectedIssues;
}, },
groupedIssues() { groupedIssues() {
const groups = []; const groups = [];
this.loopIssues.forEach((issue, i) => { this.loopIssues.forEach((issue, i) => {
const index = i % this.columns; const index = i % this.columns;
if (!groups[index]) { if (!groups[index]) {
groups.push([]); groups.push([]);
} }
groups[index].push(issue); groups[index].push(issue);
}); });
return groups; return groups;
},
}, },
watch: { },
activeTab() { watch: {
if (this.activeTab === 'all') { activeTab() {
ModalStore.purgeUnselectedIssues(); if (this.activeTab === 'all') {
} ModalStore.purgeUnselectedIssues();
}, }
}, },
mounted() { },
this.scrollHandlerWrapper = this.scrollHandler.bind(this); mounted() {
this.setColumnCountWrapper = this.setColumnCount.bind(this); this.scrollHandlerWrapper = this.scrollHandler.bind(this);
this.setColumnCount(); this.setColumnCountWrapper = this.setColumnCount.bind(this);
this.setColumnCount();
this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
window.addEventListener('resize', this.setColumnCountWrapper); window.addEventListener('resize', this.setColumnCountWrapper);
},
beforeDestroy() {
this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
window.removeEventListener('resize', this.setColumnCountWrapper);
},
methods: {
scrollHandler() {
const currentPage = Math.floor(this.issues.length / this.perPage);
if (
this.scrollTop() > this.scrollHeight() - 100 &&
!this.loadingNewPage &&
currentPage === this.page
) {
this.loadingNewPage = true;
this.page += 1;
}
}, },
beforeDestroy() { toggleIssue(e, issue) {
this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); if (e.target.tagName !== 'A') {
window.removeEventListener('resize', this.setColumnCountWrapper); ModalStore.toggleIssue(issue);
}
}, },
methods: { listHeight() {
scrollHandler() { return this.$refs.list.getBoundingClientRect().height;
const currentPage = Math.floor(this.issues.length / this.perPage); },
scrollHeight() {
if ( return this.$refs.list.scrollHeight;
this.scrollTop() > this.scrollHeight() - 100 && },
!this.loadingNewPage && scrollTop() {
currentPage === this.page return this.$refs.list.scrollTop + this.listHeight();
) { },
this.loadingNewPage = true; showIssue(issue) {
this.page += 1; if (this.activeTab === 'all') return true;
}
},
toggleIssue(e, issue) {
if (e.target.tagName !== 'A') {
ModalStore.toggleIssue(issue);
}
},
listHeight() {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
showIssue(issue) {
if (this.activeTab === 'all') return true;
const index = ModalStore.selectedIssueIndex(issue); const index = ModalStore.selectedIssueIndex(issue);
return index !== -1; return index !== -1;
}, },
setColumnCount() { setColumnCount() {
const breakpoint = bp.getBreakpointSize(); const breakpoint = bp.getBreakpointSize();
if (breakpoint === 'lg' || breakpoint === 'md') { if (breakpoint === 'lg' || breakpoint === 'md') {
this.columns = 3; this.columns = 3;
} else if (breakpoint === 'sm') { } else if (breakpoint === 'sm') {
this.columns = 2; this.columns = 2;
} else { } else {
this.columns = 1; this.columns = 1;
} }
},
}, },
}; },
};
</script> </script>
<template> <template>
<section <section
......
<script> <script>
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins'; import modalMixin from '../../mixins/modal_mixins';
export default { export default {
mixins: [modalMixin], mixins: [modalMixin],
data() { data() {
return ModalStore.store; return ModalStore.store;
},
computed: {
selectedCount() {
return ModalStore.selectedCount();
}, },
computed: { },
selectedCount() { destroyed() {
return ModalStore.selectedCount(); this.activeTab = 'all';
}, },
}, };
destroyed() {
this.activeTab = 'all';
},
};
</script> </script>
<template> <template>
<div class="top-area prepend-top-10 append-bottom-10"> <div class="top-area prepend-top-10 append-bottom-10">
......
...@@ -6,36 +6,41 @@ import _ from 'underscore'; ...@@ -6,36 +6,41 @@ import _ from 'underscore';
import CreateLabelDropdown from '../../create_label'; import CreateLabelDropdown from '../../create_label';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
$(document).off('created.label').on('created.label', (e, label) => { $(document)
boardsStore.new({ .off('created.label')
title: label.title, .on('created.label', (e, label) => {
position: boardsStore.state.lists.length - 2, boardsStore.new({
list_type: 'label',
label: {
id: label.id,
title: label.title, title: label.title,
color: label.color, position: boardsStore.state.lists.length - 2,
}, list_type: 'label',
label: {
id: label.id,
title: label.title,
color: label.color,
},
});
}); });
});
export default function initNewListDropdown() { export default function initNewListDropdown() {
$('.js-new-board-list').each(function () { $('.js-new-board-list').each(function() {
const $this = $(this); const $this = $(this);
new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespacePath'), $this.data('projectPath')); new CreateLabelDropdown(
$this.closest('.dropdown').find('.dropdown-new-label'),
$this.data('namespacePath'),
$this.data('projectPath'),
);
$this.glDropdown({ $this.glDropdown({
data(term, callback) { data(term, callback) {
axios.get($this.attr('data-list-labels-path')) axios.get($this.attr('data-list-labels-path')).then(({ data }) => {
.then(({ data }) => { callback(data);
callback(data); });
});
}, },
renderRow (label) { renderRow(label) {
const active = boardsStore.findList('title', label.title); const active = boardsStore.findList('title', label.title);
const $li = $('<li />'); const $li = $('<li />');
const $a = $('<a />', { const $a = $('<a />', {
class: (active ? `is-active js-board-list-${active.id}` : ''), class: active ? `is-active js-board-list-${active.id}` : '',
text: label.title, text: label.title,
href: '#', href: '#',
}); });
...@@ -53,7 +58,7 @@ export default function initNewListDropdown() { ...@@ -53,7 +58,7 @@ export default function initNewListDropdown() {
selectable: true, selectable: true,
multiSelect: true, multiSelect: true,
containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content', containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content',
clicked (options) { clicked(options) {
const { e } = options; const { e } = options;
const label = options.selectedObj; const label = options.selectedObj;
e.preventDefault(); e.preventDefault();
......
...@@ -46,7 +46,7 @@ export default { ...@@ -46,7 +46,7 @@ export default {
selectable: true, selectable: true,
data: (term, callback) => { data: (term, callback) => {
this.loading = true; this.loading = true;
return Api.groupProjects(this.groupId, term, {with_issues_enabled: true}, projects => { return Api.groupProjects(this.groupId, term, { with_issues_enabled: true }, projects => {
this.loading = false; this.loading = false;
callback(projects); callback(projects);
}); });
...@@ -54,7 +54,9 @@ export default { ...@@ -54,7 +54,9 @@ export default {
renderRow(project) { renderRow(project) {
return ` return `
<li> <li>
<a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}"> <a href='#' class='dropdown-menu-link' data-project-id="${
project.id
}" data-project-name="${project.name}">
${_.escape(project.name)} ${_.escape(project.name)}
</a> </a>
</li> </li>
......
<script> <script>
import Vue from 'vue'; import Vue from 'vue';
import Flash from '../../../flash'; import Flash from '../../../flash';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import boardsStore from '../../stores/boards_store'; import boardsStore from '../../stores/boards_store';
export default Vue.extend({ export default Vue.extend({
props: { props: {
issue: { issue: {
type: Object, type: Object,
required: true, required: true,
},
list: {
type: Object,
required: true,
},
}, },
computed: { list: {
updateUrl() { type: Object,
return this.issue.path; required: true,
},
}, },
methods: { },
removeIssue() { computed: {
const { issue } = this; updateUrl() {
const lists = issue.getLists(); return this.issue.path;
const req = this.buildPatchRequest(issue, lists); },
},
const data = { methods: {
issue: this.seedPatchRequest(issue, req), removeIssue() {
}; const { issue } = this;
const lists = issue.getLists();
const req = this.buildPatchRequest(issue, lists);
if (data.issue.label_ids.length === 0) { const data = {
data.issue.label_ids = ['']; issue: this.seedPatchRequest(issue, req),
} };
// Post the remove data if (data.issue.label_ids.length === 0) {
Vue.http.patch(this.updateUrl, data).catch(() => { data.issue.label_ids = [''];
Flash(__('Failed to remove issue from board, please try again.')); }
lists.forEach(list => { // Post the remove data
list.addIssue(issue); Vue.http.patch(this.updateUrl, data).catch(() => {
}); Flash(__('Failed to remove issue from board, please try again.'));
});
// Remove from the frontend store
lists.forEach(list => { lists.forEach(list => {
list.removeIssue(issue); list.addIssue(issue);
}); });
});
boardsStore.detail.issue = {}; // Remove from the frontend store
}, lists.forEach(list => {
/** list.removeIssue(issue);
* Build the default patch request. });
*/
buildPatchRequest(issue, lists) {
const listLabelIds = lists.map(list => list.label.id);
const labelIds = issue.labels boardsStore.detail.issue = {};
.map(label => label.id) },
.filter(id => !listLabelIds.includes(id)); /**
* Build the default patch request.
*/
buildPatchRequest(issue, lists) {
const listLabelIds = lists.map(list => list.label.id);
const labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id));
return { return {
label_ids: labelIds, label_ids: labelIds,
}; };
}, },
/** /**
* Seed the given patch request. * Seed the given patch request.
* *
* (This is overridden in EE) * (This is overridden in EE)
*/ */
seedPatchRequest(issue, req) { seedPatchRequest(issue, req) {
return req; return req;
},
}, },
}); },
});
</script> </script>
<template> <template>
<div <div
......
...@@ -32,7 +32,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager { ...@@ -32,7 +32,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token'); const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token');
// Remove all the tokens as they will be replaced by the search manager // Remove all the tokens as they will be replaced by the search manager
[].forEach.call(tokens, (el) => { [].forEach.call(tokens, el => {
el.parentNode.removeChild(el); el.parentNode.removeChild(el);
}); });
...@@ -50,7 +50,10 @@ export default class FilteredSearchBoards extends FilteredSearchManager { ...@@ -50,7 +50,10 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
canEdit(tokenName, tokenValue) { canEdit(tokenName, tokenValue) {
if (this.cantEdit.includes(tokenName)) return false; if (this.cantEdit.includes(tokenName)) return false;
return this.cantEditWithValue.findIndex(token => token.name === tokenName && return (
token.value === tokenValue) === -1; this.cantEditWithValue.findIndex(
token => token.name === tokenName && token.value === tokenValue,
) === -1
);
} }
} }
...@@ -32,9 +32,9 @@ export default () => { ...@@ -32,9 +32,9 @@ export default () => {
const $boardApp = document.getElementById('board-app'); const $boardApp = document.getElementById('board-app');
// check for browser back and trigger a hard reload to circumvent browser caching. // check for browser back and trigger a hard reload to circumvent browser caching.
window.addEventListener('pageshow', (event) => { window.addEventListener('pageshow', event => {
const isNavTypeBackForward = window.performance && const isNavTypeBackForward =
window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD; window.performance && window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD;
if (event.persisted || isNavTypeBackForward) { if (event.persisted || isNavTypeBackForward) {
window.location.reload(); window.location.reload();
......
...@@ -4,7 +4,8 @@ import $ from 'jquery'; ...@@ -4,7 +4,8 @@ import $ from 'jquery';
import sortableConfig from '../../sortable/sortable_config'; import sortableConfig from '../../sortable/sortable_config';
export function sortableStart() { export function sortableStart() {
$('.has-tooltip').tooltip('hide') $('.has-tooltip')
.tooltip('hide')
.tooltip('disable'); .tooltip('disable');
document.body.classList.add('is-dragging'); document.body.classList.add('is-dragging');
} }
...@@ -15,7 +16,8 @@ export function sortableEnd() { ...@@ -15,7 +16,8 @@ export function sortableEnd() {
} }
export function getBoardSortableDefaultOptions(obj) { export function getBoardSortableDefaultOptions(obj) {
const touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; const touchEnabled =
'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch);
const defaultSortOptions = Object.assign({}, sortableConfig, { const defaultSortOptions = Object.assign({}, sortableConfig, {
filter: '.board-delete, .btn', filter: '.board-delete, .btn',
...@@ -26,6 +28,8 @@ export function getBoardSortableDefaultOptions(obj) { ...@@ -26,6 +28,8 @@ export function getBoardSortableDefaultOptions(obj) {
onEnd: sortableEnd, onEnd: sortableEnd,
}); });
Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; }); Object.keys(obj).forEach(key => {
defaultSortOptions[key] = obj[key];
});
return defaultSortOptions; return defaultSortOptions;
} }
...@@ -9,7 +9,7 @@ import IssueProject from './project'; ...@@ -9,7 +9,7 @@ import IssueProject from './project';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
class ListIssue { class ListIssue {
constructor (obj, defaultAvatar) { constructor(obj, defaultAvatar) {
this.id = obj.id; this.id = obj.id;
this.iid = obj.iid; this.iid = obj.iid;
this.title = obj.title; this.title = obj.title;
...@@ -39,54 +39,54 @@ class ListIssue { ...@@ -39,54 +39,54 @@ class ListIssue {
this.milestone = new ListMilestone(obj.milestone); this.milestone = new ListMilestone(obj.milestone);
} }
obj.labels.forEach((label) => { obj.labels.forEach(label => {
this.labels.push(new ListLabel(label)); this.labels.push(new ListLabel(label));
}); });
this.assignees = obj.assignees.map(a => new ListAssignee(a, defaultAvatar)); this.assignees = obj.assignees.map(a => new ListAssignee(a, defaultAvatar));
} }
addLabel (label) { addLabel(label) {
if (!this.findLabel(label)) { if (!this.findLabel(label)) {
this.labels.push(new ListLabel(label)); this.labels.push(new ListLabel(label));
} }
} }
findLabel (findLabel) { findLabel(findLabel) {
return this.labels.filter(label => label.title === findLabel.title)[0]; return this.labels.filter(label => label.title === findLabel.title)[0];
} }
removeLabel (removeLabel) { removeLabel(removeLabel) {
if (removeLabel) { if (removeLabel) {
this.labels = this.labels.filter(label => removeLabel.title !== label.title); this.labels = this.labels.filter(label => removeLabel.title !== label.title);
} }
} }
removeLabels (labels) { removeLabels(labels) {
labels.forEach(this.removeLabel.bind(this)); labels.forEach(this.removeLabel.bind(this));
} }
addAssignee (assignee) { addAssignee(assignee) {
if (!this.findAssignee(assignee)) { if (!this.findAssignee(assignee)) {
this.assignees.push(new ListAssignee(assignee)); this.assignees.push(new ListAssignee(assignee));
} }
} }
findAssignee (findAssignee) { findAssignee(findAssignee) {
return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0]; return this.assignees.filter(assignee => assignee.id === findAssignee.id)[0];
} }
removeAssignee (removeAssignee) { removeAssignee(removeAssignee) {
if (removeAssignee) { if (removeAssignee) {
this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id); this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id);
} }
} }
removeAllAssignees () { removeAllAssignees() {
this.assignees = []; this.assignees = [];
} }
getLists () { getLists() {
return boardsStore.state.lists.filter(list => list.findIssue(this.id)); return boardsStore.state.lists.filter(list => list.findIssue(this.id));
} }
...@@ -102,14 +102,14 @@ class ListIssue { ...@@ -102,14 +102,14 @@ class ListIssue {
this.isLoading[key] = value; this.isLoading[key] = value;
} }
update () { update() {
const data = { const data = {
issue: { issue: {
milestone_id: this.milestone ? this.milestone.id : null, milestone_id: this.milestone ? this.milestone.id : null,
due_date: this.dueDate, due_date: this.dueDate,
assignee_ids: this.assignees.length > 0 ? this.assignees.map((u) => u.id) : [0], assignee_ids: this.assignees.length > 0 ? this.assignees.map(u => u.id) : [0],
label_ids: this.labels.map((label) => label.id) label_ids: this.labels.map(label => label.id),
} },
}; };
if (!data.issue.label_ids.length) { if (!data.issue.label_ids.length) {
......
...@@ -234,11 +234,11 @@ class List { ...@@ -234,11 +234,11 @@ class List {
}); });
} }
getTypeInfo (type) { getTypeInfo(type) {
return TYPES[type] || {}; return TYPES[type] || {};
} }
onNewIssueResponse (issue, data) { onNewIssueResponse(issue, data) {
issue.id = data.id; issue.id = data.id;
issue.iid = data.iid; issue.iid = data.iid;
issue.project = data.project; issue.project = data.project;
......
...@@ -19,7 +19,9 @@ export default class BoardService { ...@@ -19,7 +19,9 @@ export default class BoardService {
} }
static generateIssuePath(boardId, id) { static generateIssuePath(boardId, id) {
return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`; return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${
id ? `/${id}` : ''
}`;
} }
all() { all() {
...@@ -54,7 +56,9 @@ export default class BoardService { ...@@ -54,7 +56,9 @@ export default class BoardService {
getIssuesForList(id, filter = {}) { getIssuesForList(id, filter = {}) {
const data = { id }; const data = { id };
Object.keys(filter).forEach((key) => { data[key] = filter[key]; }); Object.keys(filter).forEach(key => {
data[key] = filter[key];
});
return axios.get(mergeUrlParams(data, this.generateIssuesPath(id))); return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
} }
...@@ -75,7 +79,9 @@ export default class BoardService { ...@@ -75,7 +79,9 @@ export default class BoardService {
} }
getBacklog(data) { getBacklog(data) {
return axios.get(mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`)); return axios.get(
mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`),
);
} }
bulkUpdate(issueIds, extraData = {}) { bulkUpdate(issueIds, extraData = {}) {
......
...@@ -20,20 +20,20 @@ const boardsStore = { ...@@ -20,20 +20,20 @@ const boardsStore = {
issue: {}, issue: {},
list: {}, list: {},
}, },
create () { create() {
this.state.lists = []; this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&'); this.filter.path = getUrlParamsArray().join('&');
this.detail = { this.detail = {
issue: {}, issue: {},
}; };
}, },
addList (listObj, defaultAvatar) { addList(listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar); const list = new List(listObj, defaultAvatar);
this.state.lists.push(list); this.state.lists.push(list);
return list; return list;
}, },
new (listObj) { new(listObj) {
const list = this.addList(listObj); const list = this.addList(listObj);
const backlogList = this.findList('type', 'backlog', 'backlog'); const backlogList = this.findList('type', 'backlog', 'backlog');
...@@ -50,44 +50,44 @@ const boardsStore = { ...@@ -50,44 +50,44 @@ const boardsStore = {
}); });
this.removeBlankState(); this.removeBlankState();
}, },
updateNewListDropdown (listId) { updateNewListDropdown(listId) {
$(`.js-board-list-${listId}`).removeClass('is-active'); $(`.js-board-list-${listId}`).removeClass('is-active');
}, },
shouldAddBlankState () { shouldAddBlankState() {
// Decide whether to add the blank state // Decide whether to add the blank state
return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0]); return !this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'closed')[0];
}, },
addBlankState () { addBlankState() {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
this.addList({ this.addList({
id: 'blank', id: 'blank',
list_type: 'blank', list_type: 'blank',
title: 'Welcome to your Issue Board!', title: 'Welcome to your Issue Board!',
position: 0 position: 0,
}); });
this.state.lists = _.sortBy(this.state.lists, 'position'); this.state.lists = _.sortBy(this.state.lists, 'position');
}, },
removeBlankState () { removeBlankState() {
this.removeList('blank'); this.removeList('blank');
Cookies.set('issue_board_welcome_hidden', 'true', { Cookies.set('issue_board_welcome_hidden', 'true', {
expires: 365 * 10, expires: 365 * 10,
path: '' path: '',
}); });
}, },
welcomeIsHidden () { welcomeIsHidden() {
return Cookies.get('issue_board_welcome_hidden') === 'true'; return Cookies.get('issue_board_welcome_hidden') === 'true';
}, },
removeList (id, type = 'blank') { removeList(id, type = 'blank') {
const list = this.findList('id', id, type); const list = this.findList('id', id, type);
if (!list) return; if (!list) return;
this.state.lists = this.state.lists.filter(list => list.id !== id); this.state.lists = this.state.lists.filter(list => list.id !== id);
}, },
moveList (listFrom, orderLists) { moveList(listFrom, orderLists) {
orderLists.forEach((id, i) => { orderLists.forEach((id, i) => {
const list = this.findList('id', parseInt(id, 10)); const list = this.findList('id', parseInt(id, 10));
...@@ -95,22 +95,25 @@ const boardsStore = { ...@@ -95,22 +95,25 @@ const boardsStore = {
}); });
listFrom.update(); listFrom.update();
}, },
moveIssueToList (listFrom, listTo, issue, newIndex) { moveIssueToList(listFrom, listTo, issue, newIndex) {
const issueTo = listTo.findIssue(issue.id); const issueTo = listTo.findIssue(issue.id);
const issueLists = issue.getLists(); const issueLists = issue.getLists();
const listLabels = issueLists.map(listIssue => listIssue.label); const listLabels = issueLists.map(listIssue => listIssue.label);
if (!issueTo) { if (!issueTo) {
// Check if target list assignee is already present in this issue // Check if target list assignee is already present in this issue
if ((listTo.type === 'assignee' && listFrom.type === 'assignee') && if (
issue.findAssignee(listTo.assignee)) { listTo.type === 'assignee' &&
listFrom.type === 'assignee' &&
issue.findAssignee(listTo.assignee)
) {
const targetIssue = listTo.findIssue(issue.id); const targetIssue = listTo.findIssue(issue.id);
targetIssue.removeAssignee(listFrom.assignee); targetIssue.removeAssignee(listFrom.assignee);
} else if (listTo.type === 'milestone') { } else if (listTo.type === 'milestone') {
const currentMilestone = issue.milestone; const currentMilestone = issue.milestone;
const currentLists = this.state.lists const currentLists = this.state.lists
.filter(list => (list.type === 'milestone' && list.id !== listTo.id)) .filter(list => list.type === 'milestone' && list.id !== listTo.id)
.filter(list => list.issues.some(listIssue => issue.id === listIssue.id)); .filter(list => list.issues.some(listIssue => issue.id === listIssue.id));
issue.removeMilestone(currentMilestone); issue.removeMilestone(currentMilestone);
issue.addMilestone(listTo.milestone); issue.addMilestone(listTo.milestone);
...@@ -126,7 +129,7 @@ const boardsStore = { ...@@ -126,7 +129,7 @@ const boardsStore = {
} }
if (listTo.type === 'closed' && listFrom.type !== 'backlog') { if (listTo.type === 'closed' && listFrom.type !== 'backlog') {
issueLists.forEach((list) => { issueLists.forEach(list => {
list.removeIssue(issue); list.removeIssue(issue);
}); });
issue.removeLabels(listLabels); issue.removeLabels(listLabels);
...@@ -144,26 +147,28 @@ const boardsStore = { ...@@ -144,26 +147,28 @@ const boardsStore = {
return ( return (
(listTo.type !== 'label' && listFrom.type === 'assignee') || (listTo.type !== 'label' && listFrom.type === 'assignee') ||
(listTo.type !== 'assignee' && listFrom.type === 'label') || (listTo.type !== 'assignee' && listFrom.type === 'label') ||
(listFrom.type === 'backlog') listFrom.type === 'backlog'
); );
}, },
moveIssueInList (list, issue, oldIndex, newIndex, idArray) { moveIssueInList(list, issue, oldIndex, newIndex, idArray) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null; const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + 1], 10) || null; const afterId = parseInt(idArray[newIndex + 1], 10) || null;
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
}, },
findList (key, val, type = 'label') { findList(key, val, type = 'label') {
const filteredList = this.state.lists.filter((list) => { const filteredList = this.state.lists.filter(list => {
const byType = type ? (list.type === type) || (list.type === 'assignee') || (list.type === 'milestone') : true; const byType = type
? list.type === type || list.type === 'assignee' || list.type === 'milestone'
: true;
return list[key] === val && byType; return list[key] === val && byType;
}); });
return filteredList[0]; return filteredList[0];
}, },
updateFiltersUrl () { updateFiltersUrl() {
window.history.pushState(null, null, `?${this.filter.path}`); window.history.pushState(null, null, `?${this.filter.path}`);
} },
}; };
// hacks added in order to allow milestone_select to function properly // hacks added in order to allow milestone_select to function properly
......
...@@ -40,7 +40,7 @@ class ModalStore { ...@@ -40,7 +40,7 @@ class ModalStore {
toggleAll() { toggleAll() {
const select = this.selectedCount() !== this.store.issues.length; const select = this.selectedCount() !== this.store.issues.length;
this.store.issues.forEach((issue) => { this.store.issues.forEach(issue => {
const issueUpdate = issue; const issueUpdate = issue;
if (issueUpdate.selected !== select) { if (issueUpdate.selected !== select) {
...@@ -69,13 +69,14 @@ class ModalStore { ...@@ -69,13 +69,14 @@ class ModalStore {
removeSelectedIssue(issue, forcePurge = false) { removeSelectedIssue(issue, forcePurge = false) {
if (this.store.activeTab === 'all' || forcePurge) { if (this.store.activeTab === 'all' || forcePurge) {
this.store.selectedIssues = this.store.selectedIssues this.store.selectedIssues = this.store.selectedIssues.filter(
.filter(fIssue => fIssue.id !== issue.id); fIssue => fIssue.id !== issue.id,
);
} }
} }
purgeUnselectedIssues() { purgeUnselectedIssues() {
this.store.selectedIssues.forEach((issue) => { this.store.selectedIssues.forEach(issue => {
if (!issue.selected) { if (!issue.selected) {
this.removeSelectedIssue(issue, true); this.removeSelectedIssue(issue, true);
} }
...@@ -87,8 +88,7 @@ class ModalStore { ...@@ -87,8 +88,7 @@ class ModalStore {
} }
findSelectedIssue(issue) { findSelectedIssue(issue) {
return this.store.selectedIssues return this.store.selectedIssues.filter(filteredIssue => filteredIssue.id === issue.id)[0];
.filter(filteredIssue => filteredIssue.id === issue.id)[0];
} }
} }
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import helmInstallIllustration from '@gitlab-org/gitlab-svgs/dist/illustrations/kubernetes-installation.svg'; import helmInstallIllustration from '@gitlab/svgs/dist/illustrations/kubernetes-installation.svg';
import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png'; import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png';
import gitlabLogo from 'images/cluster_app_logos/gitlab.png'; import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
import helmLogo from 'images/cluster_app_logos/helm.png'; import helmLogo from 'images/cluster_app_logos/helm.png';
......
import Vue from 'vue'; import Vue from 'vue';
import { import { GlProgressBar, GlLoadingIcon, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
GlPagination,
GlProgressBar,
GlModal,
GlLoadingIcon,
GlModalDirective,
GlTooltipDirective,
} from '@gitlab-org/gitlab-ui';
Vue.component('gl-pagination', GlPagination);
Vue.component('gl-progress-bar', GlProgressBar); Vue.component('gl-progress-bar', GlProgressBar);
Vue.component('gl-ui-modal', GlModal);
Vue.component('gl-loading-icon', GlLoadingIcon); Vue.component('gl-loading-icon', GlLoadingIcon);
Vue.directive('gl-modal', GlModalDirective);
Vue.directive('gl-tooltip', GlTooltipDirective); Vue.directive('gl-tooltip', GlTooltipDirective);
...@@ -18,8 +18,8 @@ export default { ...@@ -18,8 +18,8 @@ export default {
}, },
data() { data() {
const treeListStored = localStorage.getItem(treeListStorageKey); const treeListStored = localStorage.getItem(treeListStorageKey);
const renderTreeList = treeListStored !== null ? const renderTreeList =
convertPermissionToBoolean(treeListStored) : true; treeListStored !== null ? convertPermissionToBoolean(treeListStored) : true;
return { return {
search: '', search: '',
......
...@@ -15,7 +15,7 @@ import CommitComponent from '../../vue_shared/components/commit.vue'; ...@@ -15,7 +15,7 @@ import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
/** /**
* Envrionment Item Component * Environment Item Component
* *
* Renders a table row for each environment. * Renders a table row for each environment.
*/ */
...@@ -60,7 +60,7 @@ export default { ...@@ -60,7 +60,7 @@ export default {
computed: { computed: {
/** /**
* Verifies if `last_deployment` key exists in the current Envrionment. * Verifies if `last_deployment` key exists in the current Environment.
* This key is required to render most of the html - this method works has * This key is required to render most of the html - this method works has
* an helper. * an helper.
* *
......
<script> <script>
import Flash from '../../flash'; import Flash from '../../flash';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
import emptyState from './empty_state.vue'; import emptyState from './empty_state.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin'; import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from './stop_environment_modal.vue'; import StopEnvironmentModal from './stop_environment_modal.vue';
export default { export default {
components: { components: {
emptyState, emptyState,
StopEnvironmentModal, StopEnvironmentModal,
}, },
mixins: [ mixins: [CIPaginationMixin, environmentsMixin],
CIPaginationMixin,
environmentsMixin,
],
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
required: true, required: true,
},
canCreateEnvironment: {
type: Boolean,
required: true,
},
canCreateDeployment: {
type: Boolean,
required: true,
},
canReadEnvironment: {
type: Boolean,
required: true,
},
cssContainerClass: {
type: String,
required: true,
},
newEnvironmentPath: {
type: String,
required: true,
},
helpPagePath: {
type: String,
required: true,
},
}, },
canCreateEnvironment: {
created() { type: Boolean,
eventHub.$on('toggleFolder', this.toggleFolder); required: true,
}, },
canCreateDeployment: {
beforeDestroy() { type: Boolean,
eventHub.$off('toggleFolder'); required: true,
},
canReadEnvironment: {
type: Boolean,
required: true,
},
cssContainerClass: {
type: String,
required: true,
},
newEnvironmentPath: {
type: String,
required: true,
}, },
helpPagePath: {
type: String,
required: true,
},
},
created() {
eventHub.$on('toggleFolder', this.toggleFolder);
},
methods: { beforeDestroy() {
toggleFolder(folder) { eventHub.$off('toggleFolder');
this.store.toggleFolder(folder); },
if (!folder.isOpen) { methods: {
this.fetchChildEnvironments(folder, true); toggleFolder(folder) {
} this.store.toggleFolder(folder);
},
fetchChildEnvironments(folder, showLoader = false) { if (!folder.isOpen) {
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader); this.fetchChildEnvironments(folder, true);
}
},
this.service.getFolderContent(folder.folder_path) fetchChildEnvironments(folder, showLoader = false) {
.then(response => this.store.setfolderContent(folder, response.data.environments)) this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
.then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
.catch(() => { this.service
Flash(s__('Environments|An error occurred while fetching the environments.')); .getFolderContent(folder.folder_path)
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false); .then(response => this.store.setfolderContent(folder, response.data.environments))
}); .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
}, .catch(() => {
Flash(s__('Environments|An error occurred while fetching the environments.'));
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
});
},
successCallback(resp) { successCallback(resp) {
this.saveData(resp); this.saveData(resp);
// We need to verify if any folder is open to also update it // We need to verify if any folder is open to also update it
const openFolders = this.store.getOpenFolders(); const openFolders = this.store.getOpenFolders();
if (openFolders.length) { if (openFolders.length) {
openFolders.forEach(folder => this.fetchChildEnvironments(folder)); openFolders.forEach(folder => this.fetchChildEnvironments(folder));
} }
},
}, },
}; },
};
</script> </script>
<template> <template>
<div :class="cssContainerClass"> <div :class="cssContainerClass">
......
...@@ -34,14 +34,14 @@ export default class EnvironmentsStore { ...@@ -34,14 +34,14 @@ export default class EnvironmentsStore {
* @returns {Array} * @returns {Array}
*/ */
storeEnvironments(environments = []) { storeEnvironments(environments = []) {
const filteredEnvironments = environments.map((env) => { const filteredEnvironments = environments.map(env => {
const oldEnvironmentState = this.state.environments const oldEnvironmentState =
.find((element) => { this.state.environments.find(element => {
if (env.latest) { if (env.latest) {
return element.id === env.latest.id; return element.id === env.latest.id;
} }
return element.id === env.id; return element.id === env.id;
}) || {}; }) || {};
let filtered = {}; let filtered = {};
...@@ -101,11 +101,11 @@ export default class EnvironmentsStore { ...@@ -101,11 +101,11 @@ export default class EnvironmentsStore {
} }
/** /**
* Toggles folder open property for the given folder. * Toggles folder open property for the given folder.
* *
* @param {Object} folder * @param {Object} folder
* @return {Array} * @return {Array}
*/ */
toggleFolder(folder) { toggleFolder(folder) {
return this.updateEnvironmentProp(folder, 'isOpen', !folder.isOpen); return this.updateEnvironmentProp(folder, 'isOpen', !folder.isOpen);
} }
...@@ -119,7 +119,7 @@ export default class EnvironmentsStore { ...@@ -119,7 +119,7 @@ export default class EnvironmentsStore {
* @return {Object} * @return {Object}
*/ */
setfolderContent(folder, environments) { setfolderContent(folder, environments) {
const updatedEnvironments = environments.map((env) => { const updatedEnvironments = environments.map(env => {
let updated = env; let updated = env;
if (env.latest) { if (env.latest) {
...@@ -148,7 +148,7 @@ export default class EnvironmentsStore { ...@@ -148,7 +148,7 @@ export default class EnvironmentsStore {
updateEnvironmentProp(environment, prop, newValue) { updateEnvironmentProp(environment, prop, newValue) {
const { environments } = this.state; const { environments } = this.state;
const updatedEnvironments = environments.map((env) => { const updatedEnvironments = environments.map(env => {
const updateEnv = Object.assign({}, env); const updateEnv = Object.assign({}, env);
if (env.id === environment.id) { if (env.id === environment.id) {
updateEnv[prop] = newValue; updateEnv[prop] = newValue;
......
...@@ -39,8 +39,9 @@ export default class DropdownUser extends FilteredSearchDropdown { ...@@ -39,8 +39,9 @@ export default class DropdownUser extends FilteredSearchDropdown {
} }
itemClicked(e) { itemClicked(e) {
super.itemClicked(e, super.itemClicked(e, selected =>
selected => selected.querySelector('.dropdown-light-content').innerText.trim()); selected.querySelector('.dropdown-light-content').innerText.trim(),
);
} }
renderContent(forceShowList = false) { renderContent(forceShowList = false) {
...@@ -68,7 +69,7 @@ export default class DropdownUser extends FilteredSearchDropdown { ...@@ -68,7 +69,7 @@ export default class DropdownUser extends FilteredSearchDropdown {
// Removes the first character if it is a quotation so that we can search // Removes the first character if it is a quotation so that we can search
// with multiple words // with multiple words
if (value[0] === '"' || value[0] === '\'') { if (value[0] === '"' || value[0] === "'") {
value = value.slice(1); value = value.slice(1);
} }
......
...@@ -85,7 +85,7 @@ export default class FilteredSearchDropdown { ...@@ -85,7 +85,7 @@ export default class FilteredSearchDropdown {
} }
dispatchInputEvent() { dispatchInputEvent() {
// Propogate input change to FilteredSearchDropdownManager // Propagate input change to FilteredSearchDropdownManager
// so that it can determine which dropdowns to open // so that it can determine which dropdowns to open
this.input.dispatchEvent( this.input.dispatchEvent(
new CustomEvent('input', { new CustomEvent('input', {
......
...@@ -108,7 +108,7 @@ export default class FilteredSearchDropdownManager { ...@@ -108,7 +108,7 @@ export default class FilteredSearchDropdownManager {
}, },
}; };
supportedTokens.forEach((type) => { supportedTokens.forEach(type => {
if (availableMappings[type]) { if (availableMappings[type]) {
allowedMappings[type] = availableMappings[type]; allowedMappings[type] = availableMappings[type];
} }
...@@ -142,10 +142,7 @@ export default class FilteredSearchDropdownManager { ...@@ -142,10 +142,7 @@ export default class FilteredSearchDropdownManager {
} }
static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) { static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) {
const { const { uppercaseTokenName = false, capitalizeTokenValue = false } = options;
uppercaseTokenName = false,
capitalizeTokenValue = false,
} = options;
const input = FilteredSearchContainer.container.querySelector('.filtered-search'); const input = FilteredSearchContainer.container.querySelector('.filtered-search');
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue, { FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue, {
uppercaseTokenName, uppercaseTokenName,
...@@ -164,13 +161,16 @@ export default class FilteredSearchDropdownManager { ...@@ -164,13 +161,16 @@ export default class FilteredSearchDropdownManager {
updateDropdownOffset(key) { updateDropdownOffset(key) {
// Always align dropdown with the input field // Always align dropdown with the input field
let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left; let offset =
this.filteredSearchInput.getBoundingClientRect().left -
this.container.querySelector('.scroll-container').getBoundingClientRect().left;
const maxInputWidth = 240; const maxInputWidth = 240;
const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth; const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
// Make sure offset never exceeds the input container // Make sure offset never exceeds the input container
const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth; const offsetMaxWidth =
this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
if (offsetMaxWidth < offset) { if (offsetMaxWidth < offset) {
offset = offsetMaxWidth; offset = offsetMaxWidth;
} }
...@@ -196,8 +196,7 @@ export default class FilteredSearchDropdownManager { ...@@ -196,8 +196,7 @@ export default class FilteredSearchDropdownManager {
const glArguments = Object.assign({}, defaultArguments, extraArguments); const glArguments = Object.assign({}, defaultArguments, extraArguments);
// Passing glArguments to `new glClass(<arguments>)` // Passing glArguments to `new glClass(<arguments>)`
mappingKey.reference = mappingKey.reference = new (Function.prototype.bind.apply(glClass, [null, glArguments]))();
new (Function.prototype.bind.apply(glClass, [null, glArguments]))();
} }
if (firstLoad) { if (firstLoad) {
...@@ -224,8 +223,8 @@ export default class FilteredSearchDropdownManager { ...@@ -224,8 +223,8 @@ export default class FilteredSearchDropdownManager {
} }
const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key const shouldOpenFilterDropdown =
&& this.mapping[match.key]; match && this.currentDropdown !== match.key && this.mapping[match.key];
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
...@@ -236,8 +235,10 @@ export default class FilteredSearchDropdownManager { ...@@ -236,8 +235,10 @@ export default class FilteredSearchDropdownManager {
setDropdown() { setDropdown() {
const query = DropdownUtils.getSearchQuery(true); const query = DropdownUtils.getSearchQuery(true);
const { lastToken, searchToken } = const { lastToken, searchToken } = this.tokenizer.processTokens(
this.tokenizer.processTokens(query, this.filteredSearchTokenKeys.getKeys()); query,
this.filteredSearchTokenKeys.getKeys(),
);
if (this.currentDropdown) { if (this.currentDropdown) {
this.updateCurrentDropdownOffset(); this.updateCurrentDropdownOffset();
......
...@@ -68,12 +68,12 @@ export const conditions = [ ...@@ -68,12 +68,12 @@ export const conditions = [
value: 'any', value: 'any',
}, },
{ {
url: 'milestone_title=No+Milestone', url: 'milestone_title=None',
tokenKey: 'milestone', tokenKey: 'milestone',
value: 'none', value: 'none',
}, },
{ {
url: 'milestone_title=Any+Milestone', url: 'milestone_title=Any',
tokenKey: 'milestone', tokenKey: 'milestone',
value: 'any', value: 'any',
}, },
...@@ -92,6 +92,16 @@ export const conditions = [ ...@@ -92,6 +92,16 @@ export const conditions = [
tokenKey: 'label', tokenKey: 'label',
value: 'none', value: 'none',
}, },
{
url: 'my_reaction_emoji=None',
tokenKey: 'my-reaction',
value: 'none',
},
{
url: 'my_reaction_emoji=Any',
tokenKey: 'my-reaction',
value: 'any',
},
]; ];
const IssuableFilteredSearchTokenKeys = new FilteredSearchTokenKeys( const IssuableFilteredSearchTokenKeys = new FilteredSearchTokenKeys(
......
...@@ -40,7 +40,9 @@ const createFlashEl = (message, type, isFixedLayout = false) => ` ...@@ -40,7 +40,9 @@ const createFlashEl = (message, type, isFixedLayout = false) => `
class="flash-${type}" class="flash-${type}"
> >
<div <div
class="flash-text ${isFixedLayout ? 'container-fluid container-limited limit-container-width' : ''}" class="flash-text ${
isFixedLayout ? 'container-fluid container-limited limit-container-width' : ''
}"
> >
${_.escape(message)} ${_.escape(message)}
</div> </div>
...@@ -78,7 +80,9 @@ const createFlash = function createFlash( ...@@ -78,7 +80,9 @@ const createFlash = function createFlash(
if (!flashContainer) return null; if (!flashContainer) return null;
const isFixedLayout = navigation ? navigation.parentNode.classList.contains('container-limited') : true; const isFixedLayout = navigation
? navigation.parentNode.classList.contains('container-limited')
: true;
flashContainer.innerHTML = createFlashEl(message, type, isFixedLayout); flashContainer.innerHTML = createFlashEl(message, type, isFixedLayout);
......
...@@ -94,7 +94,7 @@ class GfmAutoComplete { ...@@ -94,7 +94,7 @@ class GfmAutoComplete {
...this.getDefaultCallbacks(), ...this.getDefaultCallbacks(),
beforeSave(commands) { beforeSave(commands) {
if (GfmAutoComplete.isLoading(commands)) return commands; if (GfmAutoComplete.isLoading(commands)) return commands;
return $.map(commands, (c) => { return $.map(commands, c => {
let search = c.name; let search = c.name;
if (c.aliases.length > 0) { if (c.aliases.length > 0) {
search = `${search} ${c.aliases.join(' ')}`; search = `${search} ${c.aliases.join(' ')}`;
...@@ -167,7 +167,7 @@ class GfmAutoComplete { ...@@ -167,7 +167,7 @@ class GfmAutoComplete {
callbacks: { callbacks: {
...this.getDefaultCallbacks(), ...this.getDefaultCallbacks(),
beforeSave(members) { beforeSave(members) {
return $.map(members, (m) => { return $.map(members, m => {
let title = ''; let title = '';
if (m.username == null) { if (m.username == null) {
return m; return m;
...@@ -178,7 +178,9 @@ class GfmAutoComplete { ...@@ -178,7 +178,9 @@ class GfmAutoComplete {
} }
const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`; const imgAvatar = `<img src="${m.avatar_url}" alt="${
m.username
}" class="avatar avatar-inline center s26"/>`;
const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`; const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`;
return { return {
...@@ -211,7 +213,7 @@ class GfmAutoComplete { ...@@ -211,7 +213,7 @@ class GfmAutoComplete {
callbacks: { callbacks: {
...this.getDefaultCallbacks(), ...this.getDefaultCallbacks(),
beforeSave(issues) { beforeSave(issues) {
return $.map(issues, (i) => { return $.map(issues, i => {
if (i.title == null) { if (i.title == null) {
return i; return i;
} }
...@@ -244,7 +246,7 @@ class GfmAutoComplete { ...@@ -244,7 +246,7 @@ class GfmAutoComplete {
callbacks: { callbacks: {
...this.getDefaultCallbacks(), ...this.getDefaultCallbacks(),
beforeSave(milestones) { beforeSave(milestones) {
return $.map(milestones, (m) => { return $.map(milestones, m => {
if (m.title == null) { if (m.title == null) {
return m; return m;
} }
...@@ -277,7 +279,7 @@ class GfmAutoComplete { ...@@ -277,7 +279,7 @@ class GfmAutoComplete {
callbacks: { callbacks: {
...this.getDefaultCallbacks(), ...this.getDefaultCallbacks(),
beforeSave(merges) { beforeSave(merges) {
return $.map(merges, (m) => { return $.map(merges, m => {
if (m.title == null) { if (m.title == null) {
return m; return m;
} }
...@@ -324,13 +326,20 @@ class GfmAutoComplete { ...@@ -324,13 +326,20 @@ class GfmAutoComplete {
}, },
matcher(flag, subtext) { matcher(flag, subtext) {
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers); const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext); const subtextNodes = subtext
.split(/\n+/g)
.pop()
.split(GfmAutoComplete.regexSubtext);
// Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands. // Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands.
command = subtextNodes.find((node) => { command = subtextNodes.find(node => {
if (node === LABEL_COMMAND.LABEL || if (
node === LABEL_COMMAND.RELABEL || node === LABEL_COMMAND.LABEL ||
node === LABEL_COMMAND.UNLABEL) { return node; } node === LABEL_COMMAND.RELABEL ||
node === LABEL_COMMAND.UNLABEL
) {
return node;
}
return null; return null;
}); });
...@@ -380,7 +389,7 @@ class GfmAutoComplete { ...@@ -380,7 +389,7 @@ class GfmAutoComplete {
callbacks: { callbacks: {
...this.getDefaultCallbacks(), ...this.getDefaultCallbacks(),
beforeSave(snippets) { beforeSave(snippets) {
return $.map(snippets, (m) => { return $.map(snippets, m => {
if (m.title == null) { if (m.title == null) {
return m; return m;
} }
...@@ -458,13 +467,17 @@ class GfmAutoComplete { ...@@ -458,13 +467,17 @@ class GfmAutoComplete {
this.loadData($input, at, validEmojiNames); this.loadData($input, at, validEmojiNames);
GfmAutoComplete.glEmojiTag = glEmojiTag; GfmAutoComplete.glEmojiTag = glEmojiTag;
}) })
.catch(() => { this.isLoadingData[at] = false; }); .catch(() => {
this.isLoadingData[at] = false;
});
} else if (dataSource) { } else if (dataSource) {
AjaxCache.retrieve(dataSource, true) AjaxCache.retrieve(dataSource, true)
.then((data) => { .then(data => {
this.loadData($input, at, data); this.loadData($input, at, data);
}) })
.catch(() => { this.isLoadingData[at] = false; }); .catch(() => {
this.isLoadingData[at] = false;
});
} else { } else {
this.isLoadingData[at] = false; this.isLoadingData[at] = false;
} }
...@@ -497,15 +510,16 @@ class GfmAutoComplete { ...@@ -497,15 +510,16 @@ class GfmAutoComplete {
} }
const loadingState = GfmAutoComplete.defaultLoadingData[0]; const loadingState = GfmAutoComplete.defaultLoadingData[0];
return dataToInspect && return dataToInspect && (dataToInspect === loadingState || dataToInspect.name === loadingState);
(dataToInspect === loadingState || dataToInspect.name === loadingState);
} }
static defaultMatcher(flag, subtext, controllers) { static defaultMatcher(flag, subtext, controllers) {
// The below is taken from At.js source // The below is taken from At.js source
// Tweaked to commands to start without a space only if char before is a non-word character // Tweaked to commands to start without a space only if char before is a non-word character
// https://github.com/ichord/At.js // https://github.com/ichord/At.js
const atSymbolsWithBar = Object.keys(controllers).join('|').replace(/[$]/, '\\$&'); const atSymbolsWithBar = Object.keys(controllers)
.join('|')
.replace(/[$]/, '\\$&');
const atSymbolsWithoutBar = Object.keys(controllers).join(''); const atSymbolsWithoutBar = Object.keys(controllers).join('');
const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop(); const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop();
const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
...@@ -513,7 +527,10 @@ class GfmAutoComplete { ...@@ -513,7 +527,10 @@ class GfmAutoComplete {
const accentAChar = decodeURI('%C3%80'); const accentAChar = decodeURI('%C3%80');
const accentYChar = decodeURI('%C3%BF'); const accentYChar = decodeURI('%C3%BF');
const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi'); const regexp = new RegExp(
`^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`,
'gi',
);
return regexp.exec(targetSubtext); return regexp.exec(targetSubtext);
} }
...@@ -552,8 +569,9 @@ GfmAutoComplete.Members = { ...@@ -552,8 +569,9 @@ GfmAutoComplete.Members = {
template: '<li>${avatarTag} ${username} <small>${title}</small></li>', template: '<li>${avatarTag} ${username} <small>${title}</small></li>',
}; };
GfmAutoComplete.Labels = { GfmAutoComplete.Labels = {
// eslint-disable-next-line no-template-curly-in-string template:
template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>', // eslint-disable-next-line no-template-curly-in-string
'<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>',
}; };
// Issues, MergeRequests and Snippets // Issues, MergeRequests and Snippets
GfmAutoComplete.Issues = { GfmAutoComplete.Issues = {
...@@ -567,7 +585,8 @@ GfmAutoComplete.Milestones = { ...@@ -567,7 +585,8 @@ GfmAutoComplete.Milestones = {
template: '<li>${title}</li>', template: '<li>${title}</li>',
}; };
GfmAutoComplete.Loading = { GfmAutoComplete.Loading = {
template: '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>', template:
'<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>',
}; };
export default GfmAutoComplete; export default GfmAutoComplete;
...@@ -28,7 +28,7 @@ export default class GlFieldErrors { ...@@ -28,7 +28,7 @@ export default class GlFieldErrors {
this.form.on('submit', GlFieldErrors.catchInvalidFormSubmit); this.form.on('submit', GlFieldErrors.catchInvalidFormSubmit);
} }
/* Neccessary to prevent intercept and override invalid form submit /* Necessary to prevent intercept and override invalid form submit
* because Safari & iOS quietly allow form submission when form is invalid * because Safari & iOS quietly allow form submission when form is invalid
* and prevents disabling of invalid submit button by application.js */ * and prevents disabling of invalid submit button by application.js */
......
import $ from 'jquery'; import $ from 'jquery';
import { slugifyWithHyphens } from './lib/utils/text_utility';
export default class Group { export default class Group {
constructor() { constructor() {
...@@ -7,17 +8,18 @@ export default class Group { ...@@ -7,17 +8,18 @@ export default class Group {
this.updateHandler = this.update.bind(this); this.updateHandler = this.update.bind(this);
this.resetHandler = this.reset.bind(this); this.resetHandler = this.reset.bind(this);
if (this.groupName.val() === '') { if (this.groupName.val() === '') {
this.groupPath.on('keyup', this.updateHandler); this.groupName.on('keyup', this.updateHandler);
this.groupName.on('keydown', this.resetHandler); this.groupPath.on('keydown', this.resetHandler);
} }
} }
update() { update() {
this.groupName.val(this.groupPath.val()); const slug = slugifyWithHyphens(this.groupName.val());
this.groupPath.val(slug);
} }
reset() { reset() {
this.groupPath.off('keyup', this.updateHandler); this.groupName.off('keyup', this.updateHandler);
this.groupName.off('keydown', this.resetHandler); this.groupPath.off('keydown', this.resetHandler);
} }
} }
...@@ -43,7 +43,7 @@ export default { ...@@ -43,7 +43,7 @@ export default {
'currentProjectId', 'currentProjectId',
'errorMessage', 'errorMessage',
]), ]),
...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges', 'isCommitModeActive']), ...mapGetters(['activeFile', 'hasChanges', 'someUncommittedChanges', 'isCommitModeActive']),
}, },
mounted() { mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e); window.onbeforeunload = e => this.onBeforeUnload(e);
...@@ -63,7 +63,7 @@ export default { ...@@ -63,7 +63,7 @@ export default {
onBeforeUnload(e = {}) { onBeforeUnload(e = {}) {
const returnValue = __('Are you sure you want to lose unsaved changes?'); const returnValue = __('Are you sure you want to lose unsaved changes?');
if (!this.someUncommitedChanges) return undefined; if (!this.someUncommittedChanges) return undefined;
Object.assign(e, { Object.assign(e, {
returnValue, returnValue,
......
...@@ -25,11 +25,11 @@ export default { ...@@ -25,11 +25,11 @@ export default {
}, },
computed: { computed: {
...mapState(['loading', 'currentActivityView', 'changedFiles', 'stagedFiles', 'lastCommitMsg']), ...mapState(['loading', 'currentActivityView', 'changedFiles', 'stagedFiles', 'lastCommitMsg']),
...mapGetters(['currentProject', 'someUncommitedChanges']), ...mapGetters(['currentProject', 'someUncommittedChanges']),
showSuccessMessage() { showSuccessMessage() {
return ( return (
this.currentActivityView === activityBarViews.edit && this.currentActivityView === activityBarViews.edit &&
(this.lastCommitMsg && !this.someUncommitedChanges) (this.lastCommitMsg && !this.someUncommittedChanges)
); );
}, },
}, },
......
...@@ -27,10 +27,10 @@ export default { ...@@ -27,10 +27,10 @@ export default {
'unusedSeal', 'unusedSeal',
]), ]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']), ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']), ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommittedChanges', 'activeFile']),
...mapGetters('commit', ['discardDraftButtonDisabled']), ...mapGetters('commit', ['discardDraftButtonDisabled']),
showStageUnstageArea() { showStageUnstageArea() {
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal); return !!(this.someUncommittedChanges || this.lastCommitMsg || !this.unusedSeal);
}, },
activeFileKey() { activeFileKey() {
return this.activeFile ? this.activeFile.key : null; return this.activeFile ? this.activeFile.key : null;
......
...@@ -63,7 +63,7 @@ export const isEditModeActive = state => state.currentActivityView === activityB ...@@ -63,7 +63,7 @@ export const isEditModeActive = state => state.currentActivityView === activityB
export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit; export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit;
export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review; export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review;
export const someUncommitedChanges = state => export const someUncommittedChanges = state =>
!!(state.changedFiles.length || state.stagedFiles.length); !!(state.changedFiles.length || state.stagedFiles.length);
export const getChangesInFolder = state => path => { export const getChangesInFolder = state => path => {
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { mapGetters, mapState, mapActions } from 'vuex'; import { mapGetters, mapState, mapActions } from 'vuex';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import bp from '~/breakpoints'; import bp from '~/breakpoints';
import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import Callout from '~/vue_shared/components/callout.vue'; import Callout from '~/vue_shared/components/callout.vue';
import createStore from '../store'; import createStore from '../store';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import EnvironmentsBlock from './environments_block.vue'; import EnvironmentsBlock from './environments_block.vue';
import ErasedBlock from './erased_block.vue'; import ErasedBlock from './erased_block.vue';
import Log from './job_log.vue'; import Log from './job_log.vue';
import LogTopBar from './job_log_controllers.vue'; import LogTopBar from './job_log_controllers.vue';
import StuckBlock from './stuck_block.vue'; import StuckBlock from './stuck_block.vue';
import Sidebar from './sidebar.vue'; import Sidebar from './sidebar.vue';
export default { export default {
name: 'JobPageApp', name: 'JobPageApp',
store: createStore(), store: createStore(),
components: { components: {
CiHeader, CiHeader,
Callout, Callout,
EmptyState, EmptyState,
EnvironmentsBlock, EnvironmentsBlock,
ErasedBlock, ErasedBlock,
Log, Log,
LogTopBar, LogTopBar,
StuckBlock, StuckBlock,
Sidebar, Sidebar,
},
props: {
runnerSettingsUrl: {
type: String,
required: false,
default: null,
}, },
props: { runnerHelpUrl: {
runnerSettingsUrl: { type: String,
type: String, required: false,
required: false, default: null,
default: null,
},
runnerHelpUrl: {
type: String,
required: false,
default: null,
},
endpoint: {
type: String,
required: true,
},
terminalPath: {
type: String,
required: false,
default: null,
},
pagePath: {
type: String,
required: true,
},
logState: {
type: String,
required: true,
},
}, },
computed: { endpoint: {
...mapState([ type: String,
'isLoading', required: true,
'job', },
'isSidebarOpen', terminalPath: {
'trace', type: String,
'isTraceComplete', required: false,
'traceSize', default: null,
'isTraceSizeVisible', },
'isScrollBottomDisabled', pagePath: {
'isScrollTopDisabled', type: String,
'isScrolledToBottomBeforeReceivingTrace', required: true,
'hasError', },
]), logState: {
...mapGetters([ type: String,
'headerActions', required: true,
'headerTime', },
'shouldRenderCalloutMessage', },
'shouldRenderTriggeredLabel', computed: {
'hasEnvironment', ...mapState([
'hasTrace', 'isLoading',
'emptyStateIllustration', 'job',
'isScrollingDown', 'isSidebarOpen',
'emptyStateAction', 'trace',
'hasRunnersForProject', 'isTraceComplete',
]), 'traceSize',
'isTraceSizeVisible',
'isScrollBottomDisabled',
'isScrollTopDisabled',
'isScrolledToBottomBeforeReceivingTrace',
'hasError',
]),
...mapGetters([
'headerActions',
'headerTime',
'shouldRenderCalloutMessage',
'shouldRenderTriggeredLabel',
'hasEnvironment',
'hasTrace',
'emptyStateIllustration',
'isScrollingDown',
'emptyStateAction',
'hasRunnersForProject',
]),
shouldRenderContent() { shouldRenderContent() {
return !this.isLoading && !this.hasError; return !this.isLoading && !this.hasError;
}
}, },
watch: { },
// Once the job log is loaded, watch: {
// fetch the stages for the dropdown on the sidebar // Once the job log is loaded,
job(newVal, oldVal) { // fetch the stages for the dropdown on the sidebar
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) { job(newVal, oldVal) {
this.fetchStages(); if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
} this.fetchStages();
}, }
}, },
created() { },
this.throttled = _.throttle(this.toggleScrollButtons, 100); created() {
this.throttled = _.throttle(this.toggleScrollButtons, 100);
this.setJobEndpoint(this.endpoint); this.setJobEndpoint(this.endpoint);
this.setTraceOptions({ this.setTraceOptions({
logState: this.logState, logState: this.logState,
pagePath: this.pagePath, pagePath: this.pagePath,
}); });
this.fetchJob(); this.fetchJob();
this.fetchTrace(); this.fetchTrace();
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
window.addEventListener('scroll', this.updateScroll); window.addEventListener('scroll', this.updateScroll);
}, },
mounted() { mounted() {
this.updateSidebar();
},
destroyed() {
window.removeEventListener('resize', this.onResize);
window.removeEventListener('scroll', this.updateScroll);
},
methods: {
...mapActions([
'setJobEndpoint',
'setTraceOptions',
'fetchJob',
'fetchStages',
'hideSidebar',
'showSidebar',
'toggleSidebar',
'fetchTrace',
'scrollBottom',
'scrollTop',
'toggleScrollButtons',
'toggleScrollAnimation',
]),
onResize() {
this.updateSidebar(); this.updateSidebar();
this.updateScroll();
}, },
updateSidebar() {
destroyed() { if (bp.getBreakpointSize() === 'xs') {
window.removeEventListener('resize', this.onResize); this.hideSidebar();
window.removeEventListener('scroll', this.updateScroll); } else if (!this.isSidebarOpen) {
this.showSidebar();
}
}, },
updateScroll() {
if (!isScrolledToBottom()) {
this.toggleScrollAnimation(false);
} else if (this.isScrollingDown) {
this.toggleScrollAnimation(true);
}
methods: { this.throttled();
...mapActions([
'setJobEndpoint',
'setTraceOptions',
'fetchJob',
'fetchStages',
'hideSidebar',
'showSidebar',
'toggleSidebar',
'fetchTrace',
'scrollBottom',
'scrollTop',
'toggleScrollButtons',
'toggleScrollAnimation',
]),
onResize() {
this.updateSidebar();
this.updateScroll();
},
updateSidebar() {
if (bp.getBreakpointSize() === 'xs') {
this.hideSidebar();
} else if (!this.isSidebarOpen) {
this.showSidebar();
}
},
updateScroll() {
if (!isScrolledToBottom()) {
this.toggleScrollAnimation(false);
} else if (this.isScrollingDown) {
this.toggleScrollAnimation(true);
}
this.throttled();
},
}, },
}; },
};
</script> </script>
<template> <template>
<div> <div>
......
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
export default { export default {
name: 'JobLog', name: 'JobLog',
props: { props: {
trace: { trace: {
type: String, type: String,
required: true, required: true,
},
isComplete: {
type: Boolean,
required: true,
},
}, },
computed: { isComplete: {
...mapState(['isScrolledToBottomBeforeReceivingTrace']), type: Boolean,
required: true,
}, },
updated() { },
this.$nextTick(() => this.handleScrollDown()); computed: {
...mapState(['isScrolledToBottomBeforeReceivingTrace']),
},
updated() {
this.$nextTick(() => this.handleScrollDown());
},
mounted() {
this.$nextTick(() => this.handleScrollDown());
},
methods: {
...mapActions(['scrollBottom']),
/**
* The job log is sent in HTML, which means we need to use `v-html` to render it
* Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
* in this case because it runs before `v-html` has finished running, since there's no
* Vue binding.
* In order to scroll the page down after `v-html` has finished, we need to use setTimeout
*/
handleScrollDown() {
if (this.isScrolledToBottomBeforeReceivingTrace) {
setTimeout(() => {
this.scrollBottom();
}, 0);
}
}, },
mounted() { },
this.$nextTick(() => this.handleScrollDown()); };
},
methods: {
...mapActions(['scrollBottom']),
/**
* The job log is sent in HTML, which means we need to use `v-html` to render it
* Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
* in this case because it runs before `v-html` has finished running, since there's no
* Vue binding.
* In order to scroll the page down after `v-html` has finished, we need to use setTimeout
*/
handleScrollDown() {
if (this.isScrolledToBottomBeforeReceivingTrace) {
setTimeout(() => {
this.scrollBottom();
}, 0);
}
},
},
};
</script> </script>
<template> <template>
<pre class="js-build-trace build-trace qa-build-trace"> <pre class="js-build-trace build-trace qa-build-trace">
......
...@@ -23,4 +23,3 @@ export default () => { ...@@ -23,4 +23,3 @@ export default () => {
}, },
}); });
}; };
...@@ -35,16 +35,19 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); ...@@ -35,16 +35,19 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
* Used to check if it should render the job log or the empty state * Used to check if it should render the job log or the empty state
* @returns {Boolean} * @returns {Boolean}
*/ */
export const hasTrace = state => state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running'); export const hasTrace = state =>
state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running');
export const emptyStateIllustration = state => export const emptyStateIllustration = state =>
(state.job && state.job.status && state.job.status.illustration) || {}; (state.job && state.job.status && state.job.status.illustration) || {};
export const emptyStateAction = state => (state.job && state.job.status && state.job.status.action) || {}; export const emptyStateAction = state =>
(state.job && state.job.status && state.job.status.action) || {};
export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete; export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete;
export const hasRunnersForProject = state => state.job.runners.available && !state.job.runners.online; export const hasRunnersForProject = state =>
state.job.runners.available && !state.job.runners.online;
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
This diff is collapsed.
/* global ace */ /* global ace */
export default function getModeByFileExtension(path) { export default function getModeByFileExtension(path) {
const modelist = ace.require("ace/ext/modelist"); const modelist = ace.require('ace/ext/modelist');
return modelist.getModeForPath(path).mode; return modelist.getModeForPath(path).mode;
}; }
...@@ -7,7 +7,7 @@ import { BYTES_IN_KIB } from './constants'; ...@@ -7,7 +7,7 @@ import { BYTES_IN_KIB } from './constants';
* * * Show 3 digits to the right * * * Show 3 digits to the right
* * For 2 digits to the left of the decimal point and X digits to the right of it * * For 2 digits to the left of the decimal point and X digits to the right of it
* * * Show 2 digits to the right * * * Show 2 digits to the right
*/ */
export function formatRelevantDigits(number) { export function formatRelevantDigits(number) {
let digitsLeft = ''; let digitsLeft = '';
let relevantDigits = 0; let relevantDigits = 0;
......
...@@ -7,8 +7,12 @@ export default class Members { ...@@ -7,8 +7,12 @@ export default class Members {
} }
addListeners() { addListeners() {
$('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this)); $('.js-member-update-control')
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this)); .off('change')
.on('change', this.formSubmit.bind(this));
$('.js-edit-member-form')
.off('ajax:success')
.on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
} }
...@@ -28,7 +32,7 @@ export default class Members { ...@@ -28,7 +32,7 @@ export default class Members {
toggleLabel(selected, $el) { toggleLabel(selected, $el) {
return $el.text(); return $el.text();
}, },
clicked: (options) => { clicked: options => {
this.formSubmit(null, options.$el); this.formSubmit(null, options.$el);
}, },
}); });
......
...@@ -9,7 +9,10 @@ import '~/gl_dropdown'; ...@@ -9,7 +9,10 @@ import '~/gl_dropdown';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility'; import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store'; import ModalStore from './boards/stores/modal_store';
import boardsStore, { boardStoreIssueSet, boardStoreIssueDelete } from './boards/stores/boards_store'; import boardsStore, {
boardStoreIssueSet,
boardStoreIssueDelete,
} from './boards/stores/boards_store';
export default class MilestoneSelect { export default class MilestoneSelect {
constructor(currentProject, els, options = {}) { constructor(currentProject, els, options = {}) {
......
...@@ -110,7 +110,7 @@ export default { ...@@ -110,7 +110,7 @@ export default {
// Get the remaining list to use in `and x more` text. // Get the remaining list to use in `and x more` text.
const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length); const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length);
// Add myself to the begining of the list so title will start with You. // Add myself to the beginning of the list so title will start with You.
if (hasReactionByCurrentUser) { if (hasReactionByCurrentUser) {
namesToShow.unshift('You'); namesToShow.unshift('You');
} }
......
...@@ -54,7 +54,13 @@ export default { ...@@ -54,7 +54,13 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount', 'isLoading']), ...mapGetters([
'isNotesFetched',
'discussions',
'getNotesDataByProp',
'discussionCount',
'isLoading',
]),
noteableType() { noteableType() {
return this.noteableData.noteableType; return this.noteableData.noteableType;
}, },
......
import Vue from 'vue'; import Vue from 'vue';
import DiscussionFilter from './components/discussion_filter.vue'; import DiscussionFilter from './components/discussion_filter.vue';
export default (store) => { export default store => {
const discussionFilterEl = document.getElementById('js-vue-discussion-filter'); const discussionFilterEl = document.getElementById('js-vue-discussion-filter');
if (discussionFilterEl) { if (discussionFilterEl) {
const { defaultFilter, notesFilters } = discussionFilterEl.dataset; const { defaultFilter, notesFilters } = discussionFilterEl.dataset;
const defaultValue = defaultFilter ? parseInt(defaultFilter, 10) : null; const defaultValue = defaultFilter ? parseInt(defaultFilter, 10) : null;
const filterValues = notesFilters ? JSON.parse(notesFilters) : {}; const filterValues = notesFilters ? JSON.parse(notesFilters) : {};
const filters = Object.keys(filterValues).map(entry => const filters = Object.keys(filterValues).map(entry => ({
({ title: entry, value: filterValues[entry] })); title: entry,
value: filterValues[entry],
}));
return new Vue({ return new Vue({
el: discussionFilterEl, el: discussionFilterEl,
......
...@@ -70,7 +70,7 @@ export const collapseSystemNotes = notes => { ...@@ -70,7 +70,7 @@ export const collapseSystemNotes = notes => {
} else if (lastDescriptionSystemNote) { } else if (lastDescriptionSystemNote) {
const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note); const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note);
// are they less than 10 minutes appart? // are they less than 10 minutes apart?
if (timeDifferenceMinutes > 10) { if (timeDifferenceMinutes > 10) {
// reset counter // reset counter
descriptionChangedTimes = 1; descriptionChangedTimes = 1;
......
<script> <script>
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import GlModal from '~/vue_shared/components/gl_modal.vue'; import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default { export default {
components: { components: {
GlModal, GlModal,
},
props: {
milestoneTitle: {
type: String,
required: true,
}, },
props: { url: {
milestoneTitle: { type: String,
type: String, required: true,
required: true,
},
url: {
type: String,
required: true,
},
groupName: {
type: String,
required: true,
},
}, },
computed: { groupName: {
title() { type: String,
return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle }); required: true,
}, },
text() { },
return sprintf(s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}. computed: {
title() {
return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), {
milestoneTitle: this.milestoneTitle,
});
},
text() {
return sprintf(
s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}.
Existing project milestones with the same title will be merged. Existing project milestones with the same title will be merged.
This action cannot be reversed.`), { milestoneTitle: this.milestoneTitle, groupName: this.groupName }); This action cannot be reversed.`),
}, { milestoneTitle: this.milestoneTitle, groupName: this.groupName },
);
}, },
methods: { },
onSubmit() { methods: {
eventHub.$emit('promoteMilestoneModal.requestStarted', this.url); onSubmit() {
return axios.post(this.url, { params: { format: 'json' } }) eventHub.$emit('promoteMilestoneModal.requestStarted', this.url);
.then((response) => { return axios
eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: true }); .post(this.url, { params: { format: 'json' } })
visitUrl(response.data.url); .then(response => {
}) eventHub.$emit('promoteMilestoneModal.requestFinished', {
.catch((error) => { milestoneUrl: this.url,
eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: false }); successful: true,
createFlash(error);
}); });
}, visitUrl(response.data.url);
})
.catch(error => {
eventHub.$emit('promoteMilestoneModal.requestFinished', {
milestoneUrl: this.url,
successful: false,
});
createFlash(error);
});
}, },
}; },
};
</script> </script>
<template> <template>
<gl-modal <gl-modal
...@@ -65,4 +77,3 @@ ...@@ -65,4 +77,3 @@
{{ text }} {{ text }}
</gl-modal> </gl-modal>
</template> </template>
import Vue from 'vue';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
document.addEventListener('DOMContentLoaded', () => {
const remainingTimeElements = document.querySelectorAll('.js-remaining-time');
remainingTimeElements.forEach(
el =>
new Vue({
...GlCountdown,
el,
propsData: {
endDateString: el.dateTime,
},
}),
);
});
...@@ -64,7 +64,9 @@ export default class Project { ...@@ -64,7 +64,9 @@ export default class Project {
const projectId = $(this).data('project-id'); const projectId = $(this).data('project-id');
const cookieKey = `hide_auto_devops_implicitly_enabled_banner_${projectId}`; const cookieKey = `hide_auto_devops_implicitly_enabled_banner_${projectId}`;
Cookies.set(cookieKey, 'false'); Cookies.set(cookieKey, 'false');
$(this).parents('.auto-devops-implicitly-enabled-banner').remove(); $(this)
.parents('.auto-devops-implicitly-enabled-banner')
.remove();
return e.preventDefault(); return e.preventDefault();
}); });
Project.projectSelectDropdown(); Project.projectSelectDropdown();
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { GlModal, GlModalDirective } from '@gitlab-org/gitlab-ui';
export default { export default {
components: {
GlModal,
},
directives: {
'gl-modal': GlModalDirective,
},
props: { props: {
deleteWikiUrl: { deleteWikiUrl: {
type: String, type: String,
...@@ -54,7 +61,7 @@ export default { ...@@ -54,7 +61,7 @@ export default {
> >
{{ __('Delete') }} {{ __('Delete') }}
</button> </button>
<gl-ui-modal <gl-modal
:title="title" :title="title"
:ok-title="s__('WikiPageConfirmDelete|Delete page')" :ok-title="s__('WikiPageConfirmDelete|Delete page')"
:modal-id="modalId" :modal-id="modalId"
...@@ -81,6 +88,6 @@ export default { ...@@ -81,6 +88,6 @@ export default {
name="authenticity_token" name="authenticity_token"
/> />
</form> </form>
</gl-ui-modal> </gl-modal>
</div> </div>
</template> </template>
<script> <script>
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
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 GlCountdown from '~/vue_shared/components/gl_countdown.vue'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
...@@ -10,7 +10,7 @@ export default { ...@@ -10,7 +10,7 @@ export default {
tooltip, tooltip,
}, },
components: { components: {
icon, Icon,
GlCountdown, GlCountdown,
}, },
props: { props: {
......
<script> <script>
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';
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
components: { components: {
icon, Icon,
}, },
props: { props: {
artifacts: { artifacts: {
......
...@@ -88,25 +88,25 @@ export default { ...@@ -88,25 +88,25 @@ export default {
class="table-section section-10 js-pipeline-status pipeline-status" class="table-section section-10 js-pipeline-status pipeline-status"
role="rowheader" role="rowheader"
> >
Status {{ s__('Pipeline|Status') }}
</div> </div>
<div <div
class="table-section section-15 js-pipeline-info pipeline-info" class="table-section section-15 js-pipeline-info pipeline-info"
role="rowheader" role="rowheader"
> >
Pipeline {{ s__('Pipeline|Pipeline') }}
</div> </div>
<div <div
class="table-section section-20 js-pipeline-commit pipeline-commit" class="table-section section-20 js-pipeline-commit pipeline-commit"
role="rowheader" role="rowheader"
> >
Commit {{ s__('Pipeline|Commit') }}
</div> </div>
<div <div
class="table-section section-20 js-pipeline-stages pipeline-stages" class="table-section section-20 js-pipeline-stages pipeline-stages"
role="rowheader" role="rowheader"
> >
Stages {{ s__('Pipeline|Stages') }}
</div> </div>
</div> </div>
<pipelines-table-row-component <pipelines-table-row-component
......
...@@ -261,7 +261,7 @@ export default { ...@@ -261,7 +261,7 @@ export default {
class="table-mobile-header" class="table-mobile-header"
role="rowheader" role="rowheader"
> >
Status {{ s__('Pipeline|Status') }}
</div> </div>
<div class="table-mobile-content"> <div class="table-mobile-content">
<ci-badge <ci-badge
...@@ -279,8 +279,9 @@ export default { ...@@ -279,8 +279,9 @@ export default {
<div class="table-section section-20"> <div class="table-section section-20">
<div <div
class="table-mobile-header" class="table-mobile-header"
role="rowheader"> role="rowheader"
Commit >
{{ s__('Pipeline|Commit') }}
</div> </div>
<div class="table-mobile-content"> <div class="table-mobile-content">
<commit-component <commit-component
...@@ -298,8 +299,9 @@ export default { ...@@ -298,8 +299,9 @@ export default {
<div class="table-section section-wrap section-20 stage-cell"> <div class="table-section section-wrap section-20 stage-cell">
<div <div
class="table-mobile-header" class="table-mobile-header"
role="rowheader"> role="rowheader"
Stages >
{{ s__('Pipeline|Stages') }}
</div> </div>
<div class="table-mobile-content"> <div class="table-mobile-content">
<template v-if="pipeline.details.stages.length > 0"> <template v-if="pipeline.details.stages.length > 0">
......
...@@ -60,7 +60,7 @@ export default { ...@@ -60,7 +60,7 @@ export default {
class="table-mobile-header" class="table-mobile-header"
role="rowheader" role="rowheader"
> >
Duration {{ s__('Pipeline|Duration') }}
</div> </div>
<div class="table-mobile-content"> <div class="table-mobile-content">
<p <p
...@@ -87,7 +87,8 @@ export default { ...@@ -87,7 +87,8 @@ export default {
v-tooltip v-tooltip
:title="tooltipTitle(finishedTime)" :title="tooltipTitle(finishedTime)"
data-placement="top" data-placement="top"
data-container="body"> data-container="body"
>
{{ timeFormated(finishedTime) }} {{ timeFormated(finishedTime) }}
</time> </time>
</p> </p>
......
...@@ -4,8 +4,10 @@ import { slugifyWithHyphens } from '../lib/utils/text_utility'; ...@@ -4,8 +4,10 @@ import { slugifyWithHyphens } from '../lib/utils/text_utility';
let hasUserDefinedProjectPath = false; let hasUserDefinedProjectPath = false;
const deriveProjectPathFromUrl = ($projectImportUrl) => { const deriveProjectPathFromUrl = $projectImportUrl => {
const $currentProjectPath = $projectImportUrl.parents('.toggle-import-form').find('#project_path'); const $currentProjectPath = $projectImportUrl
.parents('.toggle-import-form')
.find('#project_path');
if (hasUserDefinedProjectPath) { if (hasUserDefinedProjectPath) {
return; return;
} }
...@@ -52,9 +54,11 @@ const bindEvents = () => { ...@@ -52,9 +54,11 @@ const bindEvents = () => {
return; return;
} }
$('.how_to_import_link').on('click', (e) => { $('.how_to_import_link').on('click', e => {
e.preventDefault(); e.preventDefault();
$(e.currentTarget).next('.modal').show(); $(e.currentTarget)
.next('.modal')
.show();
}); });
$('.modal-header .close').on('click', () => { $('.modal-header .close').on('click', () => {
...@@ -63,15 +67,21 @@ const bindEvents = () => { ...@@ -63,15 +67,21 @@ const bindEvents = () => {
$('.btn_import_gitlab_project').on('click', () => { $('.btn_import_gitlab_project').on('click', () => {
const importHref = $('a.btn_import_gitlab_project').attr('href'); const importHref = $('a.btn_import_gitlab_project').attr('href');
$('.btn_import_gitlab_project') $('.btn_import_gitlab_project').attr(
.attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&name=${$projectName.val()}&path=${$projectPath.val()}`); 'href',
`${importHref}?namespace_id=${$(
'#project_namespace_id',
).val()}&name=${$projectName.val()}&path=${$projectPath.val()}`,
);
}); });
if ($pushNewProjectTipTrigger) { if ($pushNewProjectTipTrigger) {
$pushNewProjectTipTrigger $pushNewProjectTipTrigger
.removeAttr('rel') .removeAttr('rel')
.removeAttr('target') .removeAttr('target')
.on('click', (e) => { e.preventDefault(); }) .on('click', e => {
e.preventDefault();
})
.popover({ .popover({
title: $pushNewProjectTipTrigger.data('title'), title: $pushNewProjectTipTrigger.data('title'),
placement: 'bottom', placement: 'bottom',
...@@ -79,13 +89,15 @@ const bindEvents = () => { ...@@ -79,13 +89,15 @@ const bindEvents = () => {
content: $('.push-new-project-tip-template').html(), content: $('.push-new-project-tip-template').html(),
}) })
.on('shown.bs.popover', () => { .on('shown.bs.popover', () => {
$(document).on('click.popover touchstart.popover', (event) => { $(document).on('click.popover touchstart.popover', event => {
if ($(event.target).closest('.popover').length === 0) { if ($(event.target).closest('.popover').length === 0) {
$pushNewProjectTipTrigger.trigger('click'); $pushNewProjectTipTrigger.trigger('click');
} }
}); });
const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find('.js-select-on-focus'); const target = $(`#${$pushNewProjectTipTrigger.attr('aria-describedby')}`).find(
'.js-select-on-focus',
);
addSelectOnFocusBehaviour(target); addSelectOnFocusBehaviour(target);
target.focus(); target.focus();
...@@ -117,16 +129,18 @@ const bindEvents = () => { ...@@ -117,16 +129,18 @@ const bindEvents = () => {
const selectedTemplate = templates[value]; const selectedTemplate = templates[value];
$selectedTemplateText.text(selectedTemplate.text); $selectedTemplateText.text(selectedTemplate.text);
$(selectedTemplate.icon).clone().addClass('d-block').appendTo($selectedIcon); $(selectedTemplate.icon)
.clone()
.addClass('d-block')
.appendTo($selectedIcon);
const $activeTabProjectName = $('.tab-pane.active #project_name'); const $activeTabProjectName = $('.tab-pane.active #project_name');
const $activeTabProjectPath = $('.tab-pane.active #project_path'); const $activeTabProjectPath = $('.tab-pane.active #project_path');
$activeTabProjectName.focus(); $activeTabProjectName.focus();
$activeTabProjectName $activeTabProjectName.keyup(() => {
.keyup(() => { onProjectNameChange($activeTabProjectName, $activeTabProjectPath);
onProjectNameChange($activeTabProjectName, $activeTabProjectPath); hasUserDefinedProjectPath = $activeTabProjectPath.val().trim().length > 0;
hasUserDefinedProjectPath = $activeTabProjectPath.val().trim().length > 0; });
});
} }
$useTemplateBtn.on('change', chooseTemplate); $useTemplateBtn.on('change', chooseTemplate);
......
...@@ -21,7 +21,7 @@ Sidebar.initialize = function(currentUser) { ...@@ -21,7 +21,7 @@ Sidebar.initialize = function(currentUser) {
} }
}; };
Sidebar.prototype.removeListeners = function () { Sidebar.prototype.removeListeners = function() {
this.sidebar.off('click', '.sidebar-collapsed-icon'); this.sidebar.off('click', '.sidebar-collapsed-icon');
this.sidebar.off('hidden.gl.dropdown'); this.sidebar.off('hidden.gl.dropdown');
$('.dropdown').off('loading.gl.dropdown'); $('.dropdown').off('loading.gl.dropdown');
...@@ -38,10 +38,12 @@ Sidebar.prototype.addEventListeners = function() { ...@@ -38,10 +38,12 @@ Sidebar.prototype.addEventListeners = function() {
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked); $document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked);
return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo); return $(document)
.off('click', '.js-issuable-todo')
.on('click', '.js-issuable-todo', this.toggleTodo);
}; };
Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { Sidebar.prototype.sidebarToggleClicked = function(e, triggered) {
var $allGutterToggleIcons, $this, isExpanded, tooltipLabel; var $allGutterToggleIcons, $this, isExpanded, tooltipLabel;
e.preventDefault(); e.preventDefault();
$this = $(this); $this = $(this);
...@@ -51,18 +53,26 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { ...@@ -51,18 +53,26 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
if (isExpanded) { if (isExpanded) {
$allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left'); $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
$('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); $('aside.right-sidebar')
$('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); .removeClass('right-sidebar-expanded')
.addClass('right-sidebar-collapsed');
$('.layout-page')
.removeClass('right-sidebar-expanded')
.addClass('right-sidebar-collapsed');
} else { } else {
$allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right'); $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
$('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); $('aside.right-sidebar')
$('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); .removeClass('right-sidebar-collapsed')
.addClass('right-sidebar-expanded');
$('.layout-page')
.removeClass('right-sidebar-collapsed')
.addClass('right-sidebar-expanded');
} }
$this.attr('data-original-title', tooltipLabel); $this.attr('data-original-title', tooltipLabel);
if (!triggered) { if (!triggered) {
Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed')); Cookies.set('collapsed_gutter', $('.right-sidebar').hasClass('right-sidebar-collapsed'));
} }
}; };
...@@ -71,21 +81,27 @@ Sidebar.prototype.toggleTodo = function(e) { ...@@ -71,21 +81,27 @@ Sidebar.prototype.toggleTodo = function(e) {
$this = $(e.currentTarget); $this = $(e.currentTarget);
ajaxType = $this.attr('data-delete-path') ? 'delete' : 'post'; ajaxType = $this.attr('data-delete-path') ? 'delete' : 'post';
if ($this.attr('data-delete-path')) { if ($this.attr('data-delete-path')) {
url = "" + ($this.attr('data-delete-path')); url = '' + $this.attr('data-delete-path');
} else { } else {
url = "" + ($this.data('url')); url = '' + $this.data('url');
} }
$this.tooltip('hide'); $this.tooltip('hide');
$('.js-issuable-todo').disable().addClass('is-loading'); $('.js-issuable-todo')
.disable()
.addClass('is-loading');
axios[ajaxType](url, { axios[ajaxType](url, {
issuable_id: $this.data('issuableId'), issuable_id: $this.data('issuableId'),
issuable_type: $this.data('issuableType'), issuable_type: $this.data('issuableType'),
}).then(({ data }) => { })
this.todoUpdateDone(data); .then(({ data }) => {
}).catch(() => flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`)); this.todoUpdateDone(data);
})
.catch(() =>
flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`),
);
}; };
Sidebar.prototype.todoUpdateDone = function(data) { Sidebar.prototype.todoUpdateDone = function(data) {
...@@ -99,7 +115,8 @@ Sidebar.prototype.todoUpdateDone = function(data) { ...@@ -99,7 +115,8 @@ Sidebar.prototype.todoUpdateDone = function(data) {
const $el = $(el); const $el = $(el);
const $elText = $el.find('.js-issuable-todo-inner'); const $elText = $el.find('.js-issuable-todo-inner');
$el.removeClass('is-loading') $el
.removeClass('is-loading')
.enable() .enable()
.attr('aria-label', $el.data(`${attrPrefix}Text`)) .attr('aria-label', $el.data(`${attrPrefix}Text`))
.attr('data-delete-path', deletePath) .attr('data-delete-path', deletePath)
...@@ -119,7 +136,9 @@ Sidebar.prototype.todoUpdateDone = function(data) { ...@@ -119,7 +136,9 @@ Sidebar.prototype.todoUpdateDone = function(data) {
Sidebar.prototype.sidebarDropdownLoading = function(e) { Sidebar.prototype.sidebarDropdownLoading = function(e) {
var $loading, $sidebarCollapsedIcon, i, img; var $loading, $sidebarCollapsedIcon, i, img;
$sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon'); $sidebarCollapsedIcon = $(this)
.closest('.block')
.find('.sidebar-collapsed-icon');
img = $sidebarCollapsedIcon.find('img'); img = $sidebarCollapsedIcon.find('img');
i = $sidebarCollapsedIcon.find('i'); i = $sidebarCollapsedIcon.find('i');
$loading = $('<i class="fa fa-spinner fa-spin"></i>'); $loading = $('<i class="fa fa-spinner fa-spin"></i>');
...@@ -134,7 +153,9 @@ Sidebar.prototype.sidebarDropdownLoading = function(e) { ...@@ -134,7 +153,9 @@ Sidebar.prototype.sidebarDropdownLoading = function(e) {
Sidebar.prototype.sidebarDropdownLoaded = function(e) { Sidebar.prototype.sidebarDropdownLoaded = function(e) {
var $sidebarCollapsedIcon, i, img; var $sidebarCollapsedIcon, i, img;
$sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon'); $sidebarCollapsedIcon = $(this)
.closest('.block')
.find('.sidebar-collapsed-icon');
img = $sidebarCollapsedIcon.find('img'); img = $sidebarCollapsedIcon.find('img');
$sidebarCollapsedIcon.find('i.fa-spin').remove(); $sidebarCollapsedIcon.find('i.fa-spin').remove();
i = $sidebarCollapsedIcon.find('i'); i = $sidebarCollapsedIcon.find('i');
...@@ -220,7 +241,7 @@ Sidebar.prototype.isOpen = function() { ...@@ -220,7 +241,7 @@ Sidebar.prototype.isOpen = function() {
}; };
Sidebar.prototype.getBlock = function(name) { Sidebar.prototype.getBlock = function(name) {
return this.sidebar.find(".block." + name); return this.sidebar.find('.block.' + name);
}; };
export default Sidebar; export default Sidebar;
...@@ -226,7 +226,7 @@ export class SearchAutocomplete { ...@@ -226,7 +226,7 @@ export class SearchAutocomplete {
icon, icon,
text: term, text: term,
template: s__('SearchAutocomplete|in all GitLab'), template: s__('SearchAutocomplete|in all GitLab'),
url: `/search?search=${term}`, url: `${gon.relative_url_root}/search?search=${term}`,
}); });
if (template) { if (template) {
...@@ -234,7 +234,9 @@ export class SearchAutocomplete { ...@@ -234,7 +234,9 @@ export class SearchAutocomplete {
icon, icon,
text: term, text: term,
template, template,
url: `/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`, url: `${
gon.relative_url_root
}/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
}); });
} }
} }
......
...@@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue'; ...@@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import GfmAutoComplete from '~/gfm_auto_complete'; import GfmAutoComplete from '~/gfm_auto_complete';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import Api from '~/api'; import Api from '~/api';
import { GlModal } from '@gitlab-org/gitlab-ui';
import eventHub from './event_hub'; import eventHub from './event_hub';
import EmojiMenuInModal from './emoji_menu_in_modal'; import EmojiMenuInModal from './emoji_menu_in_modal';
...@@ -13,6 +14,7 @@ const emojiMenuClass = 'js-modal-status-emoji-menu'; ...@@ -13,6 +14,7 @@ const emojiMenuClass = 'js-modal-status-emoji-menu';
export default { export default {
components: { components: {
Icon, Icon,
GlModal,
}, },
props: { props: {
currentEmoji: { currentEmoji: {
...@@ -152,7 +154,7 @@ export default { ...@@ -152,7 +154,7 @@ export default {
</script> </script>
<template> <template>
<gl-ui-modal <gl-modal
:title="s__('SetStatusModal|Set a status')" :title="s__('SetStatusModal|Set a status')"
:modal-id="modalId" :modal-id="modalId"
:ok-title="s__('SetStatusModal|Set status')" :ok-title="s__('SetStatusModal|Set status')"
...@@ -237,5 +239,5 @@ export default { ...@@ -237,5 +239,5 @@ export default {
</div> </div>
</div> </div>
</div> </div>
</gl-ui-modal> </gl-modal>
</template> </template>
...@@ -74,8 +74,8 @@ export default { ...@@ -74,8 +74,8 @@ export default {
} }
if (!this.users.length) { if (!this.users.length) {
const emptyTooltipLabel = this.issuableType === 'issue' ? const emptyTooltipLabel =
__('Assignee(s)') : __('Assignee'); this.issuableType === 'issue' ? __('Assignee(s)') : __('Assignee');
names.push(emptyTooltipLabel); names.push(emptyTooltipLabel);
} }
...@@ -248,4 +248,3 @@ export default { ...@@ -248,4 +248,3 @@ export default {
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { __ } from '~/locale'; import { __ } from '~/locale';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import toggleButton from '~/vue_shared/components/toggle_button.vue'; import toggleButton from '~/vue_shared/components/toggle_button.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
const ICON_ON = 'notifications'; const ICON_ON = 'notifications';
const ICON_OFF = 'notifications-off'; const ICON_OFF = 'notifications-off';
const LABEL_ON = __('Notifications on'); const LABEL_ON = __('Notifications on');
const LABEL_OFF = __('Notifications off'); const LABEL_OFF = __('Notifications off');
export default { export default {
directives: { directives: {
tooltip, tooltip,
},
components: {
icon,
toggleButton,
},
props: {
loading: {
type: Boolean,
required: false,
default: false,
}, },
components: { subscribed: {
icon, type: Boolean,
toggleButton, required: false,
default: null,
}, },
props: { id: {
loading: { type: Number,
type: Boolean, required: false,
required: false, default: null,
default: false,
},
subscribed: {
type: Boolean,
required: false,
default: null,
},
id: {
type: Number,
required: false,
default: null,
},
}, },
computed: { },
showLoadingState() { computed: {
return this.subscribed === null; showLoadingState() {
}, return this.subscribed === null;
notificationIcon() {
return this.subscribed ? ICON_ON : ICON_OFF;
},
notificationTooltip() {
return this.subscribed ? LABEL_ON : LABEL_OFF;
},
}, },
methods: { notificationIcon() {
/** return this.subscribed ? ICON_ON : ICON_OFF;
* We need to emit this event on both component & eventHub },
* for 2 dependencies; notificationTooltip() {
* return this.subscribed ? LABEL_ON : LABEL_OFF;
* 1. eventHub: This component is used in Issue Boards sidebar },
* where component template is part of HAML },
* and event listeners are tied to app's eventHub. methods: {
* 2. Component: This compone is also used in Epics in EE /**
* where listeners are tied to component event. * We need to emit this event on both component & eventHub
*/ * for 2 dependencies;
toggleSubscription() { *
// App's eventHub event emission. * 1. eventHub: This component is used in Issue Boards sidebar
eventHub.$emit('toggleSubscription', this.id); * where component template is part of HAML
* and event listeners are tied to app's eventHub.
* 2. Component: This compone is also used in Epics in EE
* where listeners are tied to component event.
*/
toggleSubscription() {
// App's eventHub event emission.
eventHub.$emit('toggleSubscription', this.id);
// Component event emission. // Component event emission.
this.$emit('toggleSubscription', this.id); this.$emit('toggleSubscription', this.id);
}, },
onClickCollapsedIcon() { onClickCollapsedIcon() {
this.$emit('toggleSidebar'); this.$emit('toggleSidebar');
},
}, },
}; },
};
</script> </script>
<template> <template>
......
...@@ -39,9 +39,10 @@ export default class SidebarMediator { ...@@ -39,9 +39,10 @@ export default class SidebarMediator {
} }
fetch() { fetch() {
return this.service.get() return this.service
.get()
.then(response => response.json()) .then(response => response.json())
.then((data) => { .then(data => {
this.processFetchedData(data); this.processFetchedData(data);
}) })
.catch(() => new Flash('Error occurred when fetching sidebar data')); .catch(() => new Flash('Error occurred when fetching sidebar data'));
...@@ -56,30 +57,33 @@ export default class SidebarMediator { ...@@ -56,30 +57,33 @@ export default class SidebarMediator {
toggleSubscription() { toggleSubscription() {
this.store.setFetchingState('subscriptions', true); this.store.setFetchingState('subscriptions', true);
return this.service.toggleSubscription() return this.service
.toggleSubscription()
.then(() => { .then(() => {
this.store.setSubscribedState(!this.store.subscribed); this.store.setSubscribedState(!this.store.subscribed);
this.store.setFetchingState('subscriptions', false); this.store.setFetchingState('subscriptions', false);
}) })
.catch((err) => { .catch(err => {
this.store.setFetchingState('subscriptions', false); this.store.setFetchingState('subscriptions', false);
throw err; throw err;
}); });
} }
fetchAutocompleteProjects(searchTerm) { fetchAutocompleteProjects(searchTerm) {
return this.service.getProjectsAutocomplete(searchTerm) return this.service
.getProjectsAutocomplete(searchTerm)
.then(response => response.json()) .then(response => response.json())
.then((data) => { .then(data => {
this.store.setAutocompleteProjects(data); this.store.setAutocompleteProjects(data);
return this.store.autocompleteProjects; return this.store.autocompleteProjects;
}); });
} }
moveIssue() { moveIssue() {
return this.service.moveIssue(this.store.moveToProjectId) return this.service
.moveIssue(this.store.moveToProjectId)
.then(response => response.json()) .then(response => response.json())
.then((data) => { .then(data => {
if (window.location.pathname !== data.web_url) { if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url); visitUrl(data.web_url);
} }
......
<script> <script>
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
import { sprintf, __ } from '~/locale';
import PipelineStage from '~/pipelines/components/stage.vue'; import PipelineStage from '~/pipelines/components/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -36,6 +37,10 @@ export default { ...@@ -36,6 +37,10 @@ export default {
type: String, type: String,
required: false, required: false,
}, },
troubleshootingDocsPath: {
type: String,
required: true,
},
}, },
computed: { computed: {
hasPipeline() { hasPipeline() {
...@@ -57,6 +62,17 @@ export default { ...@@ -57,6 +62,17 @@ export default {
hasCommitInfo() { hasCommitInfo() {
return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0; return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
}, },
errorText() {
return sprintf(
__(
'Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}',
),
{
linkStart: `<a href="${this.troubleshootingDocsPath}">`,
linkEnd: '</a>',
},
);
},
}, },
}; };
</script> </script>
...@@ -77,8 +93,10 @@ export default { ...@@ -77,8 +93,10 @@ export default {
name="status_failed_borderless" name="status_failed_borderless"
/> />
</div> </div>
<div class="media-body"> <div
Could not connect to the CI server. Please check your settings and try again class="media-body"
v-html="errorText"
>
</div> </div>
</template> </template>
<template v-else-if="hasPipeline"> <template v-else-if="hasPipeline">
......
...@@ -71,7 +71,12 @@ export default { ...@@ -71,7 +71,12 @@ export default {
return defaultClass; return defaultClass;
}, },
iconClass() { iconClass() {
if (this.status === 'failed' || !this.commitMessage.length || !this.mr.isMergeAllowed || this.mr.preventMerge) { if (
this.status === 'failed' ||
!this.commitMessage.length ||
!this.mr.isMergeAllowed ||
this.mr.preventMerge
) {
return 'warning'; return 'warning';
} }
return 'success'; return 'success';
...@@ -90,10 +95,12 @@ export default { ...@@ -90,10 +95,12 @@ export default {
}, },
isMergeButtonDisabled() { isMergeButtonDisabled() {
const { commitMessage } = this; const { commitMessage } = this;
return Boolean(!commitMessage.length return Boolean(
|| !this.shouldShowMergeControls() !commitMessage.length ||
|| this.isMakingRequest !this.shouldShowMergeControls() ||
|| this.mr.preventMerge); this.isMakingRequest ||
this.mr.preventMerge,
);
}, },
isRemoveSourceBranchButtonDisabled() { isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled; return this.isMergeButtonDisabled;
...@@ -140,9 +147,10 @@ export default { ...@@ -140,9 +147,10 @@ export default {
}; };
this.isMakingRequest = true; this.isMakingRequest = true;
this.service.merge(options) this.service
.merge(options)
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => {
const hasError = data.status === 'failed' || data.status === 'hook_validation_error'; const hasError = data.status === 'failed' || data.status === 'hook_validation_error';
if (data.status === 'merge_when_pipeline_succeeds') { if (data.status === 'merge_when_pipeline_succeeds') {
...@@ -167,9 +175,10 @@ export default { ...@@ -167,9 +175,10 @@ export default {
}); });
}, },
handleMergePolling(continuePolling, stopPolling) { handleMergePolling(continuePolling, stopPolling) {
this.service.poll() this.service
.poll()
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => {
if (data.state === 'merged') { if (data.state === 'merged') {
// If state is merged we should update the widget and stop the polling // If state is merged we should update the widget and stop the polling
eventHub.$emit('MRWidgetUpdateRequested'); eventHub.$emit('MRWidgetUpdateRequested');
...@@ -205,9 +214,10 @@ export default { ...@@ -205,9 +214,10 @@ export default {
}); });
}, },
handleRemoveBranchPolling(continuePolling, stopPolling) { handleRemoveBranchPolling(continuePolling, stopPolling) {
this.service.poll() this.service
.poll()
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => {
// If source branch exists then we should continue polling // If source branch exists then we should continue polling
// because removing a source branch is a background task and takes time // because removing a source branch is a background task and takes time
if (data.source_branch_exists) { if (data.source_branch_exists) {
......
...@@ -24,8 +24,8 @@ export default class MRWidgetService { ...@@ -24,8 +24,8 @@ export default class MRWidgetService {
fetchDeployments(targetParam) { fetchDeployments(targetParam) {
return axios.get(this.endpoints.ciEnvironmentsStatusPath, { return axios.get(this.endpoints.ciEnvironmentsStatusPath, {
params: { params: {
environment_target: targetParam environment_target: targetParam,
} },
}); });
} }
......
...@@ -18,6 +18,7 @@ export default class MergeRequestStore { ...@@ -18,6 +18,7 @@ export default class MergeRequestStore {
this.squash = data.squash; this.squash = data.squash;
this.squashBeforeMergeHelpPath = this.squashBeforeMergeHelpPath =
this.squashBeforeMergeHelpPath || data.squash_before_merge_help_path; this.squashBeforeMergeHelpPath || data.squash_before_merge_help_path;
this.troubleshootingDocsPath = this.troubleshootingDocsPath || data.troubleshooting_docs_path;
this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true; this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true;
this.iid = data.iid; this.iid = data.iid;
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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