summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/api.js12
-rw-r--r--app/assets/javascripts/autosave.js4
-rw-r--r--app/assets/javascripts/awards_handler.js55
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue15
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js10
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js15
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js6
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue45
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue6
-rw-r--r--app/assets/javascripts/clusters/constants.js15
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/commons/polyfills/request_idle_callback.js17
-rw-r--r--app/assets/javascripts/create_item_dropdown.js3
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue1
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue29
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue31
-rw-r--r--app/assets/javascripts/diffs/components/diff_table_cell.vue7
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_comment_row.vue11
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue8
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue22
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue51
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_table_row.vue14
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue39
-rw-r--r--app/assets/javascripts/diffs/store/getters.js41
-rw-r--r--app/assets/javascripts/diffs/store/utils.js21
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue3
-rw-r--r--app/assets/javascripts/gl_dropdown.js594
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue75
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue21
-rw-r--r--app/assets/javascripts/ide/components/branches/item.vue60
-rw-r--r--app/assets/javascripts/ide/components/branches/search_list.vue111
-rw-r--r--app/assets/javascripts/ide/components/ide_project_header.vue37
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue163
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue3
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue21
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/dropdown.vue63
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/item.vue18
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue172
-rw-r--r--app/assets/javascripts/ide/components/nav_dropdown.vue59
-rw-r--r--app/assets/javascripts/ide/components/nav_dropdown_button.vue54
-rw-r--r--app/assets/javascripts/ide/components/nav_form.vue40
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue33
-rw-r--r--app/assets/javascripts/ide/components/preview/clientside.vue171
-rw-r--r--app/assets/javascripts/ide/components/preview/navigator.vue147
-rw-r--r--app/assets/javascripts/ide/components/shared/tokened_input.vue121
-rw-r--r--app/assets/javascripts/ide/constants.js3
-rw-r--r--app/assets/javascripts/ide/index.js8
-rw-r--r--app/assets/javascripts/ide/stores/getters.js4
-rw-r--r--app/assets/javascripts/ide/stores/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/branches/actions.js39
-rw-r--r--app/assets/javascripts/ide/stores/modules/branches/index.js10
-rw-r--r--app/assets/javascripts/ide/stores/modules/branches/mutation_types.js5
-rw-r--r--app/assets/javascripts/ide/stores/modules/branches/mutations.js21
-rw-r--r--app/assets/javascripts/ide/stores/modules/branches/state.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/actions.js41
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/getters.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js18
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/state.js10
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js9
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js2
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/ide/utils.js7
-rw-r--r--app/assets/javascripts/importer_status.js17
-rw-r--r--app/assets/javascripts/labels_select.js16
-rw-r--r--app/assets/javascripts/lazy_loader.js36
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue1
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue28
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue73
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js17
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js29
-rw-r--r--app/assets/javascripts/notes/stores/getters.js85
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js12
-rw-r--r--app/assets/javascripts/pages/profiles/show/emoji_menu.js18
-rw-r--r--app/assets/javascripts/pages/profiles/show/index.js49
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js15
-rw-r--r--app/assets/javascripts/reports/components/grouped_test_reports_app.vue2
-rw-r--r--app/assets/javascripts/reports/store/mutations.js2
-rw-r--r--app/assets/javascripts/search_autocomplete.js263
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue98
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/project_avatar/default.vue47
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue5
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss10
-rw-r--r--app/assets/stylesheets/framework/avatar.scss1
-rw-r--r--app/assets/stylesheets/framework/common.scss3
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss5
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss3
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss32
-rw-r--r--app/assets/stylesheets/framework/images.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss3
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss167
-rw-r--r--app/assets/stylesheets/pages/groups.scss52
-rw-r--r--app/assets/stylesheets/pages/issuable.scss1
-rw-r--r--app/assets/stylesheets/pages/labels.scss78
-rw-r--r--app/assets/stylesheets/pages/notes.scss1
-rw-r--r--app/assets/stylesheets/pages/profile.scss20
-rw-r--r--app/assets/stylesheets/pages/search.scss72
-rw-r--r--app/assets/stylesheets/pages/settings_ci_cd.scss5
-rw-r--r--app/assets/stylesheets/pages/todos.scss16
104 files changed, 2820 insertions, 1134 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 422becb7db8..25fe2ae553e 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -244,6 +244,18 @@ const Api = {
});
},
+ branches(id, query = '', options = {}) {
+ const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id));
+
+ return axios.get(url, {
+ params: {
+ search: query,
+ per_page: 20,
+ ...options,
+ },
+ });
+ },
+
createBranch(id, { ref, branch }) {
const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id));
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index fa00a3cf386..e8c59fab609 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -53,4 +53,8 @@ export default class Autosave {
return window.localStorage.removeItem(this.key);
}
+
+ dispose() {
+ this.field.off('input');
+ }
}
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 70f20c5c7cf..e34db893989 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -33,19 +33,24 @@ const categoryLabelMap = {
const IS_VISIBLE = 'is-visible';
const IS_RENDERED = 'is-rendered';
-class AwardsHandler {
+export class AwardsHandler {
constructor(emoji) {
this.emoji = emoji;
this.eventListeners = [];
+ this.toggleButtonSelector = '.js-add-award';
+ this.menuClass = 'js-award-emoji-menu';
+ }
+
+ bindEvents() {
// If the user shows intent let's pre-build the menu
this.registerEventListener(
'one',
$(document),
'mouseenter focus',
- '.js-add-award',
+ this.toggleButtonSelector,
'mouseenter focus',
() => {
- const $menu = $('.emoji-menu');
+ const $menu = $(`.${this.menuClass}`);
if ($menu.length === 0) {
requestAnimationFrame(() => {
this.createEmojiMenu();
@@ -53,7 +58,7 @@ class AwardsHandler {
}
},
);
- this.registerEventListener('on', $(document), 'click', '.js-add-award', e => {
+ this.registerEventListener('on', $(document), 'click', this.toggleButtonSelector, e => {
e.stopPropagation();
e.preventDefault();
this.showEmojiMenu($(e.currentTarget));
@@ -61,15 +66,17 @@ class AwardsHandler {
this.registerEventListener('on', $('html'), 'click', e => {
const $target = $(e.target);
- if (!$target.closest('.emoji-menu').length) {
+ if (!$target.closest(`.${this.menuClass}`).length) {
$('.js-awards-block.current').removeClass('current');
- if ($('.emoji-menu').is(':visible')) {
- $('.js-add-award.is-active').removeClass('is-active');
- this.hideMenuElement($('.emoji-menu'));
+ if ($(`.${this.menuClass}`).is(':visible')) {
+ $(`${this.toggleButtonSelector}.is-active`).removeClass('is-active');
+ this.hideMenuElement($(`.${this.menuClass}`));
}
}
});
- this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', e => {
+
+ const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`;
+ this.registerEventListener('on', $(document), 'click', emojiButtonSelector, e => {
e.preventDefault();
const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji');
@@ -101,7 +108,7 @@ class AwardsHandler {
$addBtn.closest('.js-awards-block').addClass('current');
}
- const $menu = $('.emoji-menu');
+ const $menu = $(`.${this.menuClass}`);
const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent();
const $userAuthored = this.isUserAuthored($addBtn);
if ($menu.length) {
@@ -118,7 +125,7 @@ class AwardsHandler {
} else {
$addBtn.addClass('is-loading is-active');
this.createEmojiMenu(() => {
- const $createdMenu = $('.emoji-menu');
+ const $createdMenu = $(`.${this.menuClass}`);
$addBtn.removeClass('is-loading');
this.positionMenu($createdMenu, $addBtn);
return setTimeout(() => {
@@ -156,7 +163,7 @@ class AwardsHandler {
}
const emojiMenuMarkup = `
- <div class="emoji-menu">
+ <div class="emoji-menu ${this.menuClass}">
<input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" />
<div class="emoji-menu-content">
@@ -185,7 +192,7 @@ class AwardsHandler {
// Avoid the jank and render the remaining categories separately
// This will take more time, but makes UI more responsive
- const menu = document.querySelector('.emoji-menu');
+ const menu = document.querySelector(`.${this.menuClass}`);
const emojiContentElement = menu.querySelector('.emoji-menu-content');
const remainingCategories = Object.keys(categoryMap).slice(1);
const allCategoriesAddedPromise = remainingCategories.reduce(
@@ -270,9 +277,9 @@ class AwardsHandler {
if (isInVueNoteablePage() && !isMainAwardsBlock) {
const id = votesBlock.attr('id').replace('note_', '');
- this.hideMenuElement($('.emoji-menu'));
+ this.hideMenuElement($(`.${this.menuClass}`));
- $('.js-add-award.is-active').removeClass('is-active');
+ $(`${this.toggleButtonSelector}.is-active`).removeClass('is-active');
const toggleAwardEvent = new CustomEvent('toggleAward', {
detail: {
awardName: emoji,
@@ -291,9 +298,9 @@ class AwardsHandler {
return typeof callback === 'function' ? callback() : undefined;
});
- this.hideMenuElement($('.emoji-menu'));
+ this.hideMenuElement($(`.${this.menuClass}`));
- return $('.js-add-award.is-active').removeClass('is-active');
+ return $(`${this.toggleButtonSelector}.is-active`).removeClass('is-active');
}
addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) {
@@ -321,7 +328,7 @@ class AwardsHandler {
getVotesBlock() {
if (isInVueNoteablePage()) {
- const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
+ const $el = $(`${this.toggleButtonSelector}.is-active`).closest('.note.timeline-entry');
if ($el.length) {
return $el;
@@ -458,7 +465,7 @@ class AwardsHandler {
}
createEmoji(votesBlock, emoji) {
- if ($('.emoji-menu').length) {
+ if ($(`.${this.menuClass}`).length) {
this.createAwardButtonForVotesBlock(votesBlock, emoji);
}
this.createEmojiMenu(() => {
@@ -538,7 +545,7 @@ class AwardsHandler {
this.searchEmojis(term);
});
- const $menu = $('.emoji-menu');
+ const $menu = $(`.${this.menuClass}`);
this.registerEventListener('on', $menu, transitionEndEventString, e => {
if (e.target === e.currentTarget) {
// Clear the search
@@ -608,7 +615,7 @@ class AwardsHandler {
this.eventListeners.forEach(entry => {
entry.element.off.call(entry.element, ...entry.args);
});
- $('.emoji-menu').remove();
+ $(`.${this.menuClass}`).remove();
}
}
@@ -616,7 +623,11 @@ let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) {
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(
- Emoji => new AwardsHandler(Emoji),
+ Emoji => {
+ const awardsHandler = new AwardsHandler(Emoji);
+ awardsHandler.bindEvents();
+ return awardsHandler;
+ },
);
}
return awardsHandlerPromise;
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 5c7565234d8..3e610a4088c 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -112,12 +112,20 @@ export default {
if (e.target) {
const containerEl = e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list');
const toBoardType = containerEl.dataset.boardType;
+ const cloneActions = {
+ label: ['milestone', 'assignee'],
+ assignee: ['milestone', 'label'],
+ milestone: ['label', 'assignee'],
+ };
if (toBoardType) {
const fromBoardType = this.list.type;
+ // For each list we check if the destination list is
+ // a the list were we should clone the issue
+ const shouldClone = Object.entries(cloneActions).some(entry => (
+ fromBoardType === entry[0] && entry[1].includes(toBoardType)));
- if ((fromBoardType === 'assignee' && toBoardType === 'label') ||
- (fromBoardType === 'label' && toBoardType === 'assignee')) {
+ if (shouldClone) {
return 'clone';
}
}
@@ -145,7 +153,8 @@ export default {
});
},
onUpdate: (e) => {
- const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
+ const sortedArray = this.sortable.toArray()
+ .filter(id => id !== '-1');
gl.issueBoards.BoardsStore
.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
},
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 371be109229..a9102743bf9 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -51,6 +51,16 @@ gl.issueBoards.BoardSidebar = Vue.extend({
canRemove() {
return !this.list.preset;
},
+ hasLabels() {
+ return this.issue.labels && this.issue.labels.length;
+ },
+ labelDropdownTitle() {
+ return this.hasLabels ?
+ `${this.issue.labels[0].title} ${this.issue.labels.length - 1}+ more` : 'Label';
+ },
+ selectedLabels() {
+ return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : '';
+ }
},
watch: {
detail: {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 76467564608..957114cf420 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -108,6 +108,16 @@ gl.issueBoards.BoardsStore = {
issue.findAssignee(listTo.assignee)) {
const targetIssue = listTo.findIssue(issue.id);
targetIssue.removeAssignee(listFrom.assignee);
+ } else if (listTo.type === 'milestone') {
+ const currentMilestone = issue.milestone;
+ const currentLists = this.state.lists
+ .filter(list => (list.type === 'milestone' && list.id !== listTo.id))
+ .filter(list => list.issues.some(listIssue => issue.id === listIssue.id));
+
+ issue.removeMilestone(currentMilestone);
+ issue.addMilestone(listTo.milestone);
+ currentLists.forEach(currentList => currentList.removeIssue(issue));
+ listTo.addIssue(issue, listFrom, newIndex);
} else {
// Add to new lists issues if it doesn't already exist
listTo.addIssue(issue, listFrom, newIndex);
@@ -125,6 +135,9 @@ gl.issueBoards.BoardsStore = {
} else if (listTo.type === 'backlog' && listFrom.type === 'assignee') {
issue.removeAssignee(listFrom.assignee);
listFrom.removeIssue(issue);
+ } else if (listTo.type === 'backlog' && listFrom.type === 'milestone') {
+ issue.removeMilestone(listFrom.milestone);
+ listFrom.removeIssue(issue);
} else if (this.shouldRemoveIssue(listFrom, listTo)) {
listFrom.removeIssue(issue);
}
@@ -144,7 +157,7 @@ gl.issueBoards.BoardsStore = {
},
findList (key, val, type = 'label') {
const filteredList = this.state.lists.filter((list) => {
- const byType = type ? (list.type === type) || (list.type === 'assignee') : true;
+ const byType = type ? (list.type === type) || (list.type === 'assignee') || (list.type === 'milestone') : true;
return list[key] === val && byType;
});
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index e565af800d0..0fdf0c7a389 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -6,7 +6,7 @@ import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
import {
- APPLICATION_INSTALLED,
+ APPLICATION_STATUS,
REQUEST_LOADING,
REQUEST_SUCCESS,
REQUEST_FAILURE,
@@ -177,8 +177,8 @@ export default class Clusters {
checkForNewInstalls(prevApplicationMap, newApplicationMap) {
const appTitles = Object.keys(newApplicationMap)
- .filter(appId => newApplicationMap[appId].status === APPLICATION_INSTALLED &&
- prevApplicationMap[appId].status !== APPLICATION_INSTALLED &&
+ .filter(appId => newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
+ prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
prevApplicationMap[appId].status !== null)
.map(appId => newApplicationMap[appId].title);
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index ec52fdfdf32..651f3b50236 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -4,12 +4,7 @@
import eventHub from '../event_hub';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import {
- APPLICATION_NOT_INSTALLABLE,
- APPLICATION_SCHEDULED,
- APPLICATION_INSTALLABLE,
- APPLICATION_INSTALLING,
- APPLICATION_INSTALLED,
- APPLICATION_ERROR,
+ APPLICATION_STATUS,
REQUEST_LOADING,
REQUEST_SUCCESS,
REQUEST_FAILURE,
@@ -59,49 +54,57 @@
},
},
computed: {
+ isUnknownStatus() {
+ return !this.isKnownStatus && this.status !== null;
+ },
+ isKnownStatus() {
+ return Object.values(APPLICATION_STATUS).includes(this.status);
+ },
rowJsClass() {
return `js-cluster-application-row-${this.id}`;
},
installButtonLoading() {
return !this.status ||
- this.status === APPLICATION_SCHEDULED ||
- this.status === APPLICATION_INSTALLING ||
+ this.status === APPLICATION_STATUS.SCHEDULED ||
+ this.status === APPLICATION_STATUS.INSTALLING ||
this.requestStatus === REQUEST_LOADING;
},
installButtonDisabled() {
- // Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but
+ // Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
// we already made a request to install and are just waiting for the real-time
// to sync up.
- return (this.status !== APPLICATION_INSTALLABLE
- && this.status !== APPLICATION_ERROR) ||
+ return ((this.status !== APPLICATION_STATUS.INSTALLABLE
+ && this.status !== APPLICATION_STATUS.ERROR) ||
this.requestStatus === REQUEST_LOADING ||
- this.requestStatus === REQUEST_SUCCESS;
+ this.requestStatus === REQUEST_SUCCESS) && this.isKnownStatus;
},
installButtonLabel() {
let label;
if (
- this.status === APPLICATION_NOT_INSTALLABLE ||
- this.status === APPLICATION_INSTALLABLE ||
- this.status === APPLICATION_ERROR
+ this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
+ this.status === APPLICATION_STATUS.INSTALLABLE ||
+ this.status === APPLICATION_STATUS.ERROR ||
+ this.isUnknownStatus
) {
label = s__('ClusterIntegration|Install');
- } else if (this.status === APPLICATION_SCHEDULED ||
- this.status === APPLICATION_INSTALLING) {
+ } else if (this.status === APPLICATION_STATUS.SCHEDULED ||
+ this.status === APPLICATION_STATUS.INSTALLING) {
label = s__('ClusterIntegration|Installing');
- } else if (this.status === APPLICATION_INSTALLED) {
+ } else if (this.status === APPLICATION_STATUS.INSTALLED ||
+ this.status === APPLICATION_STATUS.UPDATED) {
label = s__('ClusterIntegration|Installed');
}
return label;
},
showManageButton() {
- return this.manageLink && this.status === APPLICATION_INSTALLED;
+ return this.manageLink && this.status === APPLICATION_STATUS.INSTALLED;
},
manageButtonLabel() {
return s__('ClusterIntegration|Manage');
},
hasError() {
- return this.status === APPLICATION_ERROR ||
+ return this.status === APPLICATION_STATUS.ERROR ||
this.requestStatus === REQUEST_FAILURE;
},
generalErrorDescription() {
@@ -182,7 +185,7 @@
</div>
</div>
<div
- v-if="hasError"
+ v-if="hasError || isUnknownStatus"
class="gl-responsive-table-row-layout"
role="row"
>
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 8ee7279e544..d708a9e595a 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -3,7 +3,7 @@ import _ from 'underscore';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
-import { APPLICATION_INSTALLED, INGRESS } from '../constants';
+import { APPLICATION_STATUS, INGRESS } from '../constants';
export default {
components: {
@@ -58,7 +58,7 @@ export default {
return INGRESS;
},
ingressInstalled() {
- return this.applications.ingress.status === APPLICATION_INSTALLED;
+ return this.applications.ingress.status === APPLICATION_STATUS.INSTALLED;
},
ingressExternalIp() {
return this.applications.ingress.externalIp;
@@ -122,7 +122,7 @@ export default {
);
},
jupyterInstalled() {
- return this.applications.jupyter.status === APPLICATION_INSTALLED;
+ return this.applications.jupyter.status === APPLICATION_STATUS.INSTALLED;
},
jupyterHostname() {
return this.applications.jupyter.hostname;
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index 371f71fde44..72fc9355d82 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -1,10 +1,13 @@
// These need to match what is returned from the server
-export const APPLICATION_NOT_INSTALLABLE = 'not_installable';
-export const APPLICATION_INSTALLABLE = 'installable';
-export const APPLICATION_SCHEDULED = 'scheduled';
-export const APPLICATION_INSTALLING = 'installing';
-export const APPLICATION_INSTALLED = 'installed';
-export const APPLICATION_ERROR = 'errored';
+export const APPLICATION_STATUS = {
+ NOT_INSTALLABLE: 'not_installable',
+ INSTALLABLE: 'installable',
+ SCHEDULED: 'scheduled',
+ INSTALLING: 'installing',
+ INSTALLED: 'installed',
+ UPDATED: 'updated',
+ ERROR: 'errored',
+};
// These are only used client-side
export const REQUEST_LOADING = 'request-loading';
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index f595f3c3187..589eeee9695 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -19,3 +19,4 @@ import './polyfills/custom_event';
import './polyfills/element';
import './polyfills/event';
import './polyfills/nodelist';
+import './polyfills/request_idle_callback';
diff --git a/app/assets/javascripts/commons/polyfills/request_idle_callback.js b/app/assets/javascripts/commons/polyfills/request_idle_callback.js
new file mode 100644
index 00000000000..2356569d06e
--- /dev/null
+++ b/app/assets/javascripts/commons/polyfills/request_idle_callback.js
@@ -0,0 +1,17 @@
+window.requestIdleCallback =
+ window.requestIdleCallback ||
+ function requestShim(cb) {
+ const start = Date.now();
+ return setTimeout(() => {
+ cb({
+ didTimeout: false,
+ timeRemaining: () => Math.max(0, 50 - (Date.now() - start)),
+ });
+ }, 1);
+ };
+
+window.cancelIdleCallback =
+ window.cancelIdleCallback ||
+ function cancelShim(id) {
+ clearTimeout(id);
+ };
diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js
index 42e9e568170..8ef9aa7f529 100644
--- a/app/assets/javascripts/create_item_dropdown.js
+++ b/app/assets/javascripts/create_item_dropdown.js
@@ -12,6 +12,7 @@ export default class CreateItemDropdown {
this.fieldName = options.fieldName;
this.onSelect = options.onSelect || (() => {});
this.getDataOption = options.getData;
+ this.getDataRemote = !!options.filterRemote;
this.createNewItemFromValueOption = options.createNewItemFromValue;
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
@@ -29,7 +30,7 @@ export default class CreateItemDropdown {
this.$dropdown.glDropdown({
data: this.getData.bind(this),
filterable: true,
- remote: false,
+ filterRemote: this.getDataRemote,
search: {
fields: ['text'],
},
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index 20483161033..e64d5511d78 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -30,6 +30,7 @@ export default {
:render-header="false"
:render-diff-file="false"
:always-expanded="true"
+ :discussions-by-diff-order="true"
/>
</ul>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
index ad838a32518..8ad1ea34245 100644
--- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -71,13 +71,23 @@ export default {
required: false,
default: false,
},
+ isHover: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ discussions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
computed: {
...mapState({
diffViewType: state => state.diffs.diffViewType,
diffFiles: state => state.diffs.diffFiles,
}),
- ...mapGetters(['isLoggedIn', 'discussionsByLineCode']),
+ ...mapGetters(['isLoggedIn']),
lineHref() {
return this.lineCode ? `#${this.lineCode}` : '#';
},
@@ -85,26 +95,22 @@ export default {
return (
this.isLoggedIn &&
this.showCommentButton &&
+ this.isHover &&
!this.isMatchLine &&
!this.isContextLine &&
- !this.hasDiscussions &&
- !this.isMetaLine
+ !this.isMetaLine &&
+ !this.hasDiscussions
);
},
- discussions() {
- return this.discussionsByLineCode[this.lineCode] || [];
- },
hasDiscussions() {
return this.discussions.length > 0;
},
shouldShowAvatarsOnGutter() {
- let render = this.hasDiscussions && this.showCommentButton;
-
if (!this.lineType && this.linePosition === LINE_POSITION_RIGHT) {
- render = false;
+ return false;
}
- return render;
+ return this.showCommentButton && this.hasDiscussions;
},
},
methods: {
@@ -176,7 +182,7 @@ export default {
v-else
>
<button
- v-show="shouldShowCommentButton"
+ v-if="shouldShowCommentButton"
type="button"
class="add-diff-note js-add-diff-note-button"
title="Add a comment to this line"
@@ -189,7 +195,6 @@ export default {
</button>
<a
v-if="lineNumber"
- v-once
:data-linenumber="lineNumber"
:href="lineHref"
>
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index 32f9516d332..cbe4551d06b 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -1,17 +1,17 @@
<script>
-import $ from 'jquery';
import { mapState, mapGetters, mapActions } from 'vuex';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import noteForm from '../../notes/components/note_form.vue';
import { getNoteFormData } from '../store/utils';
-import Autosave from '../../autosave';
-import { DIFF_NOTE_TYPE, NOTE_TYPE } from '../constants';
+import autosave from '../../notes/mixins/autosave';
+import { DIFF_NOTE_TYPE } from '../constants';
export default {
components: {
noteForm,
},
+ mixins: [autosave],
props: {
diffFileHash: {
type: String,
@@ -41,28 +41,35 @@ export default {
},
mounted() {
if (this.isLoggedIn) {
- const noteableData = this.getNoteableData;
const keys = [
- NOTE_TYPE,
- this.noteableType,
- noteableData.id,
- noteableData.diff_head_sha,
+ this.noteableData.diff_head_sha,
DIFF_NOTE_TYPE,
- noteableData.source_project_id,
+ this.noteableData.source_project_id,
this.line.lineCode,
];
- this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), keys);
+ this.initAutoSave(this.noteableData, keys);
}
},
methods: {
...mapActions('diffs', ['cancelCommentForm']),
...mapActions(['saveNote', 'refetchDiscussionById']),
- handleCancelCommentForm() {
- this.autosave.reset();
+ handleCancelCommentForm(shouldConfirm, isDirty) {
+ if (shouldConfirm && isDirty) {
+ const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
+
+ // eslint-disable-next-line no-alert
+ if (!window.confirm(msg)) {
+ return;
+ }
+ }
+
this.cancelCommentForm({
lineCode: this.line.lineCode,
});
+ this.$nextTick(() => {
+ this.resetAutoSave();
+ });
},
handleSaveNote(note) {
const selectedDiffFile = this.getDiffFileByHash(this.diffFileHash);
diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue
index 5962f30d9bb..33bc8d9971e 100644
--- a/app/assets/javascripts/diffs/components/diff_table_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue
@@ -67,6 +67,11 @@ export default {
required: false,
default: false,
},
+ discussions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
computed: {
...mapGetters(['isLoggedIn']),
@@ -132,10 +137,12 @@ export default {
:line-number="lineNumber"
:meta-data="normalizedLine.metaData"
:show-comment-button="showCommentButton"
+ :is-hover="isHover"
:is-bottom="isBottom"
:is-match-line="isMatchLine"
:is-context-line="isContentLine"
:is-meta-line="isMetaLine"
+ :discussions="discussions"
/>
</td>
</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
index ca265dd892c..caf84dc9573 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
@@ -1,5 +1,5 @@
<script>
-import { mapState, mapGetters } from 'vuex';
+import { mapState } from 'vuex';
import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue';
@@ -21,15 +21,16 @@ export default {
type: Number,
required: true,
},
+ discussions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
computed: {
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
- ...mapGetters(['discussionsByLineCode']),
- discussions() {
- return this.discussionsByLineCode[this.line.lineCode] || [];
- },
className() {
return this.discussions.length ? '' : 'js-temp-notes-holder';
},
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
index 0197a510ef1..32d65ff994f 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -33,6 +33,11 @@ export default {
required: false,
default: false,
},
+ discussions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
@@ -89,6 +94,7 @@ export default {
:is-bottom="isBottom"
:is-hover="isHover"
:show-comment-button="true"
+ :discussions="discussions"
class="diff-line-num old_line"
/>
<diff-table-cell
@@ -98,10 +104,10 @@ export default {
:line-type="newLineType"
:is-bottom="isBottom"
:is-hover="isHover"
+ :discussions="discussions"
class="diff-line-num new_line"
/>
<td
- v-once
:class="line.type"
class="line_content"
v-html="line.richText"
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
index 9fd19b74cd7..e7d789734c3 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -20,8 +20,11 @@ export default {
},
},
computed: {
- ...mapGetters('diffs', ['commitId']),
- ...mapGetters(['discussionsByLineCode']),
+ ...mapGetters('diffs', [
+ 'commitId',
+ 'shouldRenderInlineCommentRow',
+ 'singleDiscussionByLineCode',
+ ]),
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
@@ -36,15 +39,8 @@ export default {
},
},
methods: {
- shouldRenderCommentRow(line) {
- if (this.diffLineCommentForms[line.lineCode]) return true;
-
- const lineDiscussions = this.discussionsByLineCode[line.lineCode];
- if (lineDiscussions === undefined) {
- return false;
- }
-
- return lineDiscussions.every(discussion => discussion.expanded);
+ discussionsList(line) {
+ return line.lineCode !== undefined ? this.singleDiscussionByLineCode(line.lineCode) : [];
},
},
};
@@ -65,13 +61,15 @@ export default {
:line="line"
:is-bottom="index + 1 === diffLinesLength"
:key="line.lineCode"
+ :discussions="discussionsList(line)"
/>
<inline-diff-comment-row
- v-if="shouldRenderCommentRow(line)"
+ v-if="shouldRenderInlineCommentRow(line)"
:diff-file-hash="diffFile.fileHash"
:line="line"
:line-index="index"
:key="index"
+ :discussions="discussionsList(line)"
/>
</template>
</tbody>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
index cc5248c25d9..48b8feeb0b4 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
@@ -1,5 +1,5 @@
<script>
-import { mapState, mapGetters } from 'vuex';
+import { mapState } from 'vuex';
import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue';
@@ -21,30 +21,34 @@ export default {
type: Number,
required: true,
},
+ leftDiscussions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ rightDiscussions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
computed: {
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
- ...mapGetters(['discussionsByLineCode']),
leftLineCode() {
return this.line.left.lineCode;
},
rightLineCode() {
return this.line.right.lineCode;
},
- hasDiscussion() {
- const discussions = this.discussionsByLineCode;
-
- return discussions[this.leftLineCode] || discussions[this.rightLineCode];
- },
hasExpandedDiscussionOnLeft() {
- const discussions = this.discussionsByLineCode[this.leftLineCode];
+ const discussions = this.leftDiscussions;
return discussions ? discussions.every(discussion => discussion.expanded) : false;
},
hasExpandedDiscussionOnRight() {
- const discussions = this.discussionsByLineCode[this.rightLineCode];
+ const discussions = this.rightDiscussions;
return discussions ? discussions.every(discussion => discussion.expanded) : false;
},
@@ -52,17 +56,18 @@ export default {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
},
shouldRenderDiscussionsOnLeft() {
- return this.discussionsByLineCode[this.leftLineCode] && this.hasExpandedDiscussionOnLeft;
+ return this.leftDiscussions && this.hasExpandedDiscussionOnLeft;
},
shouldRenderDiscussionsOnRight() {
- return (
- this.discussionsByLineCode[this.rightLineCode] &&
- this.hasExpandedDiscussionOnRight &&
- this.line.right.type
- );
+ return this.rightDiscussions && this.hasExpandedDiscussionOnRight && this.line.right.type;
+ },
+ showRightSideCommentForm() {
+ return this.line.right.type && this.diffLineCommentForms[this.rightLineCode];
},
className() {
- return this.hasDiscussion ? '' : 'js-temp-notes-holder';
+ return this.leftDiscussions.length > 0 || this.rightDiscussions.length > 0
+ ? ''
+ : 'js-temp-notes-holder';
},
},
};
@@ -80,13 +85,12 @@ export default {
class="content"
>
<diff-discussions
- v-if="discussionsByLineCode[leftLineCode].length"
- :discussions="discussionsByLineCode[leftLineCode]"
+ v-if="leftDiscussions.length"
+ :discussions="leftDiscussions"
/>
</div>
<diff-line-note-form
- v-if="diffLineCommentForms[leftLineCode] &&
- diffLineCommentForms[leftLineCode]"
+ v-if="diffLineCommentForms[leftLineCode]"
:diff-file-hash="diffFileHash"
:line="line.left"
:note-target-line="line.left"
@@ -100,13 +104,12 @@ export default {
class="content"
>
<diff-discussions
- v-if="discussionsByLineCode[rightLineCode].length"
- :discussions="discussionsByLineCode[rightLineCode]"
+ v-if="rightDiscussions.length"
+ :discussions="rightDiscussions"
/>
</div>
<diff-line-note-form
- v-if="diffLineCommentForms[rightLineCode] &&
- diffLineCommentForms[rightLineCode] && line.right.type"
+ v-if="showRightSideCommentForm"
:diff-file-hash="diffFileHash"
:line="line.right"
:note-target-line="line.right"
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
index ee5bb4d8d05..d4e54c2bd00 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
@@ -36,6 +36,16 @@ export default {
required: false,
default: false,
},
+ leftDiscussions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ rightDiscussions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
@@ -116,10 +126,10 @@ export default {
:is-hover="isLeftHover"
:show-comment-button="true"
:diff-view-type="parallelDiffViewType"
+ :discussions="leftDiscussions"
class="diff-line-num old_line"
/>
<td
- v-once
:id="line.left.lineCode"
:class="parallelViewLeftLineType"
class="line_content parallel left-side"
@@ -137,10 +147,10 @@ export default {
:is-hover="isRightHover"
:show-comment-button="true"
:diff-view-type="parallelDiffViewType"
+ :discussions="rightDiscussions"
class="diff-line-num new_line"
/>
<td
- v-once
:id="line.right.lineCode"
:class="line.right.type"
class="line_content parallel right-side"
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
index 32528c9e7ab..24ceb52a04a 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -21,8 +21,11 @@ export default {
},
},
computed: {
- ...mapGetters('diffs', ['commitId']),
- ...mapGetters(['discussionsByLineCode']),
+ ...mapGetters('diffs', [
+ 'commitId',
+ 'singleDiscussionByLineCode',
+ 'shouldRenderParallelCommentRow',
+ ]),
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
@@ -53,29 +56,9 @@ export default {
},
},
methods: {
- shouldRenderCommentRow(line) {
- const leftLineCode = line.left.lineCode;
- const rightLineCode = line.right.lineCode;
- const discussions = this.discussionsByLineCode;
- const leftDiscussions = discussions[leftLineCode];
- const rightDiscussions = discussions[rightLineCode];
- const hasDiscussion = leftDiscussions || rightDiscussions;
-
- const hasExpandedDiscussionOnLeft = leftDiscussions
- ? leftDiscussions.every(discussion => discussion.expanded)
- : false;
- const hasExpandedDiscussionOnRight = rightDiscussions
- ? rightDiscussions.every(discussion => discussion.expanded)
- : false;
-
- if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
- return true;
- }
-
- const hasCommentFormOnLeft = this.diffLineCommentForms[leftLineCode];
- const hasCommentFormOnRight = this.diffLineCommentForms[rightLineCode];
-
- return hasCommentFormOnLeft || hasCommentFormOnRight;
+ discussionsByLine(line, leftOrRight) {
+ return line[leftOrRight] && line[leftOrRight].lineCode !== undefined ?
+ this.singleDiscussionByLineCode(line[leftOrRight].lineCode) : [];
},
},
};
@@ -98,13 +81,17 @@ export default {
:line="line"
:is-bottom="index + 1 === diffLinesLength"
:key="index"
+ :left-discussions="discussionsByLine(line, 'left')"
+ :right-discussions="discussionsByLine(line, 'right')"
/>
<parallel-diff-comment-row
- v-if="shouldRenderCommentRow(line)"
+ v-if="shouldRenderParallelCommentRow(line)"
:key="`dcr-${index}`"
:line="line"
:diff-file-hash="diffFile.fileHash"
:line-index="index"
+ :left-discussions="discussionsByLine(line, 'left')"
+ :right-discussions="discussionsByLine(line, 'right')"
/>
</template>
</tbody>
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index 9aec117c236..4a47646d7fa 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -64,6 +64,47 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) =
discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash),
) || [];
+export const singleDiscussionByLineCode = (state, getters, rootState, rootGetters) => lineCode => {
+ if (!lineCode || lineCode === undefined) return [];
+ const discussions = rootGetters.discussionsByLineCode;
+ return discussions[lineCode] || [];
+};
+
+export const shouldRenderParallelCommentRow = (state, getters) => line => {
+ const leftLineCode = line.left.lineCode;
+ const rightLineCode = line.right.lineCode;
+ const leftDiscussions = getters.singleDiscussionByLineCode(leftLineCode);
+ const rightDiscussions = getters.singleDiscussionByLineCode(rightLineCode);
+ const hasDiscussion = leftDiscussions.length || rightDiscussions.length;
+
+ const hasExpandedDiscussionOnLeft = leftDiscussions.length
+ ? leftDiscussions.every(discussion => discussion.expanded)
+ : false;
+ const hasExpandedDiscussionOnRight = rightDiscussions.length
+ ? rightDiscussions.every(discussion => discussion.expanded)
+ : false;
+
+ if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
+ return true;
+ }
+
+ const hasCommentFormOnLeft = state.diffLineCommentForms[leftLineCode];
+ const hasCommentFormOnRight = state.diffLineCommentForms[rightLineCode];
+
+ return hasCommentFormOnLeft || hasCommentFormOnRight;
+};
+
+export const shouldRenderInlineCommentRow = (state, getters) => line => {
+ if (state.diffLineCommentForms[line.lineCode]) return true;
+
+ const lineDiscussions = getters.singleDiscussionByLineCode(line.lineCode);
+ if (lineDiscussions.length === 0) {
+ return false;
+ }
+
+ return lineDiscussions.every(discussion => discussion.expanded);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests
export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.fileHash === fileHash);
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index d9589baa76e..82082ac508a 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -173,3 +173,24 @@ export function trimFirstCharOfLineContent(line = {}) {
return parsedLine;
}
+
+export function getDiffRefsByLineCode(diffFiles) {
+ return diffFiles.reduce((acc, diffFile) => {
+ const { baseSha, headSha, startSha } = diffFile.diffRefs;
+ const { newPath, oldPath } = diffFile;
+
+ // We can only use highlightedDiffLines to create the map of diff lines because
+ // highlightedDiffLines will also include every parallel diff line in it.
+ if (diffFile.highlightedDiffLines) {
+ diffFile.highlightedDiffLines.forEach(line => {
+ const { lineCode, oldLine, newLine } = line;
+
+ if (lineCode) {
+ acc[lineCode] = { baseSha, headSha, startSha, newPath, oldPath, oldLine, newLine };
+ }
+ });
+ }
+
+ return acc;
+ }, {});
+}
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 21cf92d1bc5..11e3b781e5a 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -116,7 +116,8 @@ export default {
this.model &&
this.hasLastDeploymentKey &&
this.model.last_deployment &&
- this.model.last_deployment.deployable
+ this.model.last_deployment.deployable &&
+ this.model.last_deployment.deployable.retry_path
);
},
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 8d231e6c405..c3959ef3e9e 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-underscore-dangle, no-var, one-var, one-var-declaration-per-line, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func */
+/* eslint-disable func-names, no-underscore-dangle, no-var, one-var, one-var-declaration-per-line, max-len, vars-on-top, wrap-iife, no-unused-vars, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func */
/* global fuzzaldrinPlus */
import $ from 'jquery';
@@ -19,32 +19,42 @@ GitLabDropdownInput = (function() {
this.fieldName = this.options.fieldName || 'field-name';
$inputContainer = this.input.parent();
$clearButton = $inputContainer.find('.js-dropdown-input-clear');
- $clearButton.on('click', (function(_this) {
- // Clear click
- return function(e) {
- e.preventDefault();
- e.stopPropagation();
- return _this.input.val('').trigger('input').focus();
- };
- })(this));
+ $clearButton.on(
+ 'click',
+ (function(_this) {
+ // Clear click
+ return function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ return _this.input
+ .val('')
+ .trigger('input')
+ .focus();
+ };
+ })(this),
+ );
this.input
- .on('keydown', function (e) {
- var keyCode = e.which;
- if (keyCode === 13 && !options.elIsInput) {
- e.preventDefault();
- }
- })
- .on('input', function(e) {
- var val = e.currentTarget.value || _this.options.inputFieldName;
- val = val.split(' ').join('-') // replaces space with dash
- .replace(/[^a-zA-Z0-9 -]/g, '').toLowerCase() // replace non alphanumeric
- .replace(/(-)\1+/g, '-'); // replace repeated dashes
- _this.cb(_this.options.fieldName, val, {}, true);
- _this.input.closest('.dropdown')
- .find('.dropdown-toggle-text')
- .text(val);
- });
+ .on('keydown', function(e) {
+ var keyCode = e.which;
+ if (keyCode === 13 && !options.elIsInput) {
+ e.preventDefault();
+ }
+ })
+ .on('input', function(e) {
+ var val = e.currentTarget.value || _this.options.inputFieldName;
+ val = val
+ .split(' ')
+ .join('-') // replaces space with dash
+ .replace(/[^a-zA-Z0-9 -]/g, '')
+ .toLowerCase() // replace non alphanumeric
+ .replace(/(-)\1+/g, '-'); // replace repeated dashes
+ _this.cb(_this.options.fieldName, val, {}, true);
+ _this.input
+ .closest('.dropdown')
+ .find('.dropdown-toggle-text')
+ .text(val);
+ });
}
GitLabDropdownInput.prototype.onInput = function(cb) {
@@ -61,7 +71,7 @@ GitLabDropdownFilter = (function() {
ARROW_KEY_CODES = [38, 40];
- HAS_VALUE_CLASS = "has-value";
+ HAS_VALUE_CLASS = 'has-value';
function GitLabDropdownFilter(input, options) {
var $clearButton, $inputContainer, ref, timeout;
@@ -70,44 +80,59 @@ GitLabDropdownFilter = (function() {
this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
$inputContainer = this.input.parent();
$clearButton = $inputContainer.find('.js-dropdown-input-clear');
- $clearButton.on('click', (function(_this) {
- // Clear click
- return function(e) {
- e.preventDefault();
- e.stopPropagation();
- return _this.input.val('').trigger('input').focus();
- };
- })(this));
+ $clearButton.on(
+ 'click',
+ (function(_this) {
+ // Clear click
+ return function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ return _this.input
+ .val('')
+ .trigger('input')
+ .focus();
+ };
+ })(this),
+ );
// Key events
- timeout = "";
+ timeout = '';
this.input
- .on('keydown', function (e) {
+ .on('keydown', function(e) {
var keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) {
e.preventDefault();
}
})
- .on('input', function() {
- if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
- $inputContainer.addClass(HAS_VALUE_CLASS);
- } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
- $inputContainer.removeClass(HAS_VALUE_CLASS);
- }
- // Only filter asynchronously only if option remote is set
- if (this.options.remote) {
- clearTimeout(timeout);
- return timeout = setTimeout(function() {
- $inputContainer.parent().addClass('is-loading');
-
- return this.options.query(this.input.val(), function(data) {
- $inputContainer.parent().removeClass('is-loading');
- return this.options.callback(data);
- }.bind(this));
- }.bind(this), 250);
- } else {
- return this.filter(this.input.val());
- }
- }.bind(this));
+ .on(
+ 'input',
+ function() {
+ if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
+ $inputContainer.addClass(HAS_VALUE_CLASS);
+ } else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
+ $inputContainer.removeClass(HAS_VALUE_CLASS);
+ }
+ // Only filter asynchronously only if option remote is set
+ if (this.options.remote) {
+ clearTimeout(timeout);
+ return (timeout = setTimeout(
+ function() {
+ $inputContainer.parent().addClass('is-loading');
+
+ return this.options.query(
+ this.input.val(),
+ function(data) {
+ $inputContainer.parent().removeClass('is-loading');
+ return this.options.callback(data);
+ }.bind(this),
+ );
+ }.bind(this),
+ 250,
+ ));
+ } else {
+ return this.filter(this.input.val());
+ }
+ }.bind(this),
+ );
}
GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
@@ -120,7 +145,7 @@ GitLabDropdownFilter = (function() {
this.options.onFilter(search_text);
}
data = this.options.data();
- if ((data != null) && !this.options.filterByText) {
+ if (data != null && !this.options.filterByText) {
results = data;
if (search_text !== '') {
// When data is an array of objects therefore [object Array] e.g.
@@ -130,7 +155,7 @@ GitLabDropdownFilter = (function() {
// ]
if (_.isArray(data)) {
results = fuzzaldrinPlus.filter(data, search_text, {
- key: this.options.keys
+ key: this.options.keys,
});
} else {
// If data is grouped therefore an [object Object]. e.g.
@@ -149,7 +174,7 @@ GitLabDropdownFilter = (function() {
for (key in data) {
group = data[key];
tmp = fuzzaldrinPlus.filter(group, search_text, {
- key: this.options.keys
+ key: this.options.keys,
});
if (tmp.length) {
results[key] = tmp.map(function(item) {
@@ -180,7 +205,10 @@ GitLabDropdownFilter = (function() {
elements.show().removeClass('option-hidden');
}
- elements.parent().find('.dropdown-menu-empty-item').toggleClass('hidden', elements.is(':visible'));
+ elements
+ .parent()
+ .find('.dropdown-menu-empty-item')
+ .toggleClass('hidden', elements.is(':visible'));
}
};
@@ -194,23 +222,26 @@ GitLabDropdownRemote = (function() {
}
GitLabDropdownRemote.prototype.execute = function() {
- if (typeof this.dataEndpoint === "string") {
+ if (typeof this.dataEndpoint === 'string') {
return this.fetchData();
- } else if (typeof this.dataEndpoint === "function") {
+ } else if (typeof this.dataEndpoint === 'function') {
if (this.options.beforeSend) {
this.options.beforeSend();
}
- return this.dataEndpoint("", (function(_this) {
- // Fetch the data by calling the data funcfion
- return function(data) {
- if (_this.options.success) {
- _this.options.success(data);
- }
- if (_this.options.beforeSend) {
- return _this.options.beforeSend();
- }
- };
- })(this));
+ return this.dataEndpoint(
+ '',
+ (function(_this) {
+ // Fetch the data by calling the data funcfion
+ return function(data) {
+ if (_this.options.success) {
+ _this.options.success(data);
+ }
+ if (_this.options.beforeSend) {
+ return _this.options.beforeSend();
+ }
+ };
+ })(this),
+ );
}
};
@@ -220,33 +251,41 @@ GitLabDropdownRemote = (function() {
}
// Fetch the data through ajax if the data is a string
- return axios.get(this.dataEndpoint)
- .then(({ data }) => {
- if (this.options.success) {
- return this.options.success(data);
- }
- });
+ return axios.get(this.dataEndpoint).then(({ data }) => {
+ if (this.options.success) {
+ return this.options.success(data);
+ }
+ });
};
return GitLabDropdownRemote;
})();
GitLabDropdown = (function() {
- var ACTIVE_CLASS, FILTER_INPUT, NO_FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
+ var ACTIVE_CLASS,
+ FILTER_INPUT,
+ NO_FILTER_INPUT,
+ INDETERMINATE_CLASS,
+ LOADING_CLASS,
+ PAGE_TWO_CLASS,
+ NON_SELECTABLE_CLASSES,
+ SELECTABLE_CLASSES,
+ CURSOR_SELECT_SCROLL_PADDING,
+ currentIndex;
- LOADING_CLASS = "is-loading";
+ LOADING_CLASS = 'is-loading';
- PAGE_TWO_CLASS = "is-page-two";
+ PAGE_TWO_CLASS = 'is-page-two';
- ACTIVE_CLASS = "is-active";
+ ACTIVE_CLASS = 'is-active';
- INDETERMINATE_CLASS = "is-indeterminate";
+ INDETERMINATE_CLASS = 'is-indeterminate';
currentIndex = -1;
NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item';
- SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)";
+ SELECTABLE_CLASSES = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ', .option-hidden)';
CURSOR_SELECT_SCROLL_PADDING = 5;
@@ -263,15 +302,15 @@ GitLabDropdown = (function() {
this.opened = this.opened.bind(this);
this.shouldPropagate = this.shouldPropagate.bind(this);
self = this;
- selector = $(this.el).data("target");
+ selector = $(this.el).data('target');
this.dropdown = selector != null ? $(selector) : $(this.el).parent();
// Set Defaults
this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT);
this.highlight = !!this.options.highlight;
- this.filterInputBlur = this.options.filterInputBlur != null
- ? this.options.filterInputBlur
- : true;
+ this.icon = !!this.options.icon;
+ this.filterInputBlur =
+ this.options.filterInputBlur != null ? this.options.filterInputBlur : true;
// If no input is passed create a default one
self = this;
// If selector was passed
@@ -296,11 +335,17 @@ GitLabDropdown = (function() {
_this.fullData = data;
_this.parseData(_this.fullData);
_this.focusTextInput();
- if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
+ if (
+ _this.options.filterable &&
+ _this.filter &&
+ _this.filter.input &&
+ _this.filter.input.val() &&
+ _this.filter.input.val().trim() !== ''
+ ) {
return _this.filter.input.trigger('input');
}
};
- // Remote data
+ // Remote data
})(this),
instance: this,
});
@@ -325,7 +370,7 @@ GitLabDropdown = (function() {
return function() {
selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
if (_this.dropdown.find('.dropdown-toggle-page').length) {
- selector = ".dropdown-page-one " + selector;
+ selector = '.dropdown-page-one ' + selector;
}
return $(selector, this.instance.dropdown);
};
@@ -341,80 +386,97 @@ GitLabDropdown = (function() {
if (_this.filterInput.val() !== '') {
selector = SELECTABLE_CLASSES;
if (_this.dropdown.find('.dropdown-toggle-page').length) {
- selector = ".dropdown-page-one " + selector;
+ selector = '.dropdown-page-one ' + selector;
}
if ($(_this.el).is('input')) {
currentIndex = -1;
} else {
- $(selector, _this.dropdown).first().find('a').addClass('is-focused');
+ $(selector, _this.dropdown)
+ .first()
+ .find('a')
+ .addClass('is-focused');
currentIndex = 0;
}
}
};
- })(this)
+ })(this),
});
}
// Event listeners
- this.dropdown.on("shown.bs.dropdown", this.opened);
- this.dropdown.on("hidden.bs.dropdown", this.hidden);
- $(this.el).on("update.label", this.updateLabel);
- this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate);
- this.dropdown.on('keyup', (function(_this) {
- return function(e) {
- // Escape key
- if (e.which === 27) {
- return $('.dropdown-menu-close', _this.dropdown).trigger('click');
- }
- };
- })(this));
- this.dropdown.on('blur', 'a', (function(_this) {
- return function(e) {
- var $dropdownMenu, $relatedTarget;
- if (e.relatedTarget != null) {
- $relatedTarget = $(e.relatedTarget);
- $dropdownMenu = $relatedTarget.closest('.dropdown-menu');
- if ($dropdownMenu.length === 0) {
- return _this.dropdown.removeClass('show');
+ this.dropdown.on('shown.bs.dropdown', this.opened);
+ this.dropdown.on('hidden.bs.dropdown', this.hidden);
+ $(this.el).on('update.label', this.updateLabel);
+ this.dropdown.on('click', '.dropdown-menu, .dropdown-menu-close', this.shouldPropagate);
+ this.dropdown.on(
+ 'keyup',
+ (function(_this) {
+ return function(e) {
+ // Escape key
+ if (e.which === 27) {
+ return $('.dropdown-menu-close', _this.dropdown).trigger('click');
}
- }
- };
- })(this));
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) {
+ };
+ })(this),
+ );
+ this.dropdown.on(
+ 'blur',
+ 'a',
+ (function(_this) {
return function(e) {
- e.preventDefault();
- e.stopPropagation();
- return _this.togglePage();
+ var $dropdownMenu, $relatedTarget;
+ if (e.relatedTarget != null) {
+ $relatedTarget = $(e.relatedTarget);
+ $dropdownMenu = $relatedTarget.closest('.dropdown-menu');
+ if ($dropdownMenu.length === 0) {
+ return _this.dropdown.removeClass('show');
+ }
+ }
};
- })(this));
+ })(this),
+ );
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ this.dropdown.find('.dropdown-toggle-page, .dropdown-menu-back').on(
+ 'click',
+ (function(_this) {
+ return function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ return _this.togglePage();
+ };
+ })(this),
+ );
}
if (this.options.selectable) {
- selector = ".dropdown-content a";
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one .dropdown-content a";
+ selector = '.dropdown-content a';
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = '.dropdown-page-one .dropdown-content a';
}
- this.dropdown.on("click", selector, function(e) {
- var $el, selected, selectedObj, isMarking;
- $el = $(e.currentTarget);
- selected = self.rowClicked($el);
- selectedObj = selected ? selected[0] : null;
- isMarking = selected ? selected[1] : null;
- if (this.options.clicked) {
- this.options.clicked.call(this, {
- selectedObj,
- $el,
- e,
- isMarking,
- });
- }
+ this.dropdown.on(
+ 'click',
+ selector,
+ function(e) {
+ var $el, selected, selectedObj, isMarking;
+ $el = $(e.currentTarget);
+ selected = self.rowClicked($el);
+ selectedObj = selected ? selected[0] : null;
+ isMarking = selected ? selected[1] : null;
+ if (this.options.clicked) {
+ this.options.clicked.call(this, {
+ selectedObj,
+ $el,
+ e,
+ isMarking,
+ });
+ }
- // Update label right after all modifications in dropdown has been done
- if (this.options.toggleLabel) {
- this.updateLabel(selectedObj, $el, this);
- }
+ // Update label right after all modifications in dropdown has been done
+ if (this.options.toggleLabel) {
+ this.updateLabel(selectedObj, $el, this);
+ }
- $el.trigger('blur');
- }.bind(this));
+ $el.trigger('blur');
+ }.bind(this),
+ );
}
}
@@ -452,10 +514,15 @@ GitLabDropdown = (function() {
html = [];
for (name in data) {
groupData = data[name];
- html.push(this.renderItem({
- header: name
- // Add header for each group
- }, name));
+ html.push(
+ this.renderItem(
+ {
+ header: name,
+ // Add header for each group
+ },
+ name,
+ ),
+ );
this.renderData(groupData, name).map(function(item) {
return html.push(item);
});
@@ -474,20 +541,25 @@ GitLabDropdown = (function() {
if (group == null) {
group = false;
}
- return data.map((function(_this) {
- return function(obj, index) {
- return _this.renderItem(obj, group, index);
- };
- })(this));
+ return data.map(
+ (function(_this) {
+ return function(obj, index) {
+ return _this.renderItem(obj, group, index);
+ };
+ })(this),
+ );
};
GitLabDropdown.prototype.shouldPropagate = function(e) {
var $target;
if (this.options.multiSelect || this.options.shouldPropagate === false) {
$target = $(e.target);
- if ($target && !$target.hasClass('dropdown-menu-close') &&
- !$target.hasClass('dropdown-menu-close-icon') &&
- !$target.data('isLink')) {
+ if (
+ $target &&
+ !$target.hasClass('dropdown-menu-close') &&
+ !$target.hasClass('dropdown-menu-close-icon') &&
+ !$target.data('isLink')
+ ) {
e.stopPropagation();
return false;
} else {
@@ -497,9 +569,11 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.filteredFullData = function() {
- return this.fullData.filter(r => typeof r === 'object'
- && !Object.prototype.hasOwnProperty.call(r, 'beforeDivider')
- && !Object.prototype.hasOwnProperty.call(r, 'header')
+ return this.fullData.filter(
+ r =>
+ typeof r === 'object' &&
+ !Object.prototype.hasOwnProperty.call(r, 'beforeDivider') &&
+ !Object.prototype.hasOwnProperty.call(r, 'header'),
);
};
@@ -522,11 +596,16 @@ GitLabDropdown = (function() {
// matches the correct layout
const inputValue = this.filterInput.val();
if (this.fullData && hasMultiSelect && this.options.processData && inputValue.length === 0) {
- this.options.processData.call(this.options, inputValue, this.filteredFullData(), this.parseData.bind(this));
+ this.options.processData.call(
+ this.options,
+ inputValue,
+ this.filteredFullData(),
+ this.parseData.bind(this),
+ );
}
contentHtml = $('.dropdown-content', this.dropdown).html();
- if (this.remote && contentHtml === "") {
+ if (this.remote && contentHtml === '') {
this.remote.execute();
} else {
this.focusTextInput();
@@ -537,7 +616,11 @@ GitLabDropdown = (function() {
}
if (this.options.opened) {
- this.options.opened.call(this, e);
+ if (this.options.preserveContext) {
+ this.options.opened(e);
+ } else {
+ this.options.opened.call(this, e);
+ }
}
return this.dropdown.trigger('shown.gl.dropdown');
@@ -555,11 +638,11 @@ GitLabDropdown = (function() {
var $input;
this.resetRows();
this.removeArrayKeyEvent();
- $input = this.dropdown.find(".dropdown-input-field");
+ $input = this.dropdown.find('.dropdown-input-field');
if (this.options.filterable) {
$input.blur();
}
- if (this.dropdown.find(".dropdown-toggle-page").length) {
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
$('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
}
if (this.options.hidden) {
@@ -601,7 +684,7 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.clearMenu = function() {
var selector;
selector = '.dropdown-content';
- if (this.dropdown.find(".dropdown-toggle-page").length) {
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
if (this.options.containerSelector) {
selector = this.options.containerSelector;
} else {
@@ -619,7 +702,7 @@ GitLabDropdown = (function() {
value = this.options.id ? this.options.id(data) : data.id;
if (value) {
- value = value.toString().replace(/'/g, '\\\'');
+ value = value.toString().replace(/'/g, "\\'");
}
}
@@ -676,21 +759,27 @@ GitLabDropdown = (function() {
text = data.text != null ? data.text : '';
}
if (this.highlight) {
- text = this.highlightTextMatches(text, this.filterInput.val());
+ text = data.template
+ ? this.highlightTemplate(text, data.template)
+ : this.highlightTextMatches(text, this.filterInput.val());
}
// Create the list item & the link
var link = document.createElement('a');
link.href = url;
- if (this.highlight) {
+ if (this.icon) {
+ text = `<span>${text}</span>`;
+ link.classList.add('d-flex', 'align-items-center');
+ link.innerHTML = data.icon ? data.icon + text : text;
+ } else if (this.highlight) {
link.innerHTML = text;
} else {
link.textContent = text;
}
if (selected) {
- link.className = 'is-active';
+ link.classList.add('is-active');
}
if (group) {
@@ -703,17 +792,24 @@ GitLabDropdown = (function() {
return html;
};
+ GitLabDropdown.prototype.highlightTemplate = function(text, template) {
+ return `"<b>${_.escape(text)}</b>" ${template}`;
+ };
+
GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
const occurrences = fuzzaldrinPlus.match(text, term);
const { indexOf } = [];
- return text.split('').map(function(character, i) {
- if (indexOf.call(occurrences, i) !== -1) {
- return "<b>" + character + "</b>";
- } else {
- return character;
- }
- }).join('');
+ return text
+ .split('')
+ .map(function(character, i) {
+ if (indexOf.call(occurrences, i) !== -1) {
+ return '<b>' + character + '</b>';
+ } else {
+ return character;
+ }
+ })
+ .join('');
};
GitLabDropdown.prototype.noResults = function() {
@@ -748,13 +844,15 @@ GitLabDropdown = (function() {
}
field = [];
- value = this.options.id
- ? this.options.id(selectedObject, el)
- : selectedObject.id;
+ value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
if (isInput) {
field = $(this.el);
} else if (value != null) {
- field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
+ field = this.dropdown
+ .parent()
+ .find(
+ "input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, "\\'") + "']",
+ );
}
if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
@@ -780,9 +878,12 @@ GitLabDropdown = (function() {
} else {
isMarking = true;
if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
- this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
+ this.dropdown.find('.' + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
if (!isInput) {
- this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
+ this.dropdown
+ .parent()
+ .find("input[name='" + fieldName + "']")
+ .remove();
}
}
if (field && field.length && value == null) {
@@ -823,13 +924,16 @@ GitLabDropdown = (function() {
$('input[name="' + fieldName + '"]').remove();
}
- $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
+ $input = $('<input>')
+ .attr('type', 'hidden')
+ .attr('name', fieldName)
+ .val(value);
if (this.options.inputId != null) {
$input.attr('id', this.options.inputId);
}
if (this.options.multiSelect) {
- Object.keys(selectedObject).forEach((attribute) => {
+ Object.keys(selectedObject).forEach(attribute => {
$input.attr(`data-${attribute}`, selectedObject[attribute]);
});
}
@@ -844,13 +948,13 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.selectRowAtIndex = function(index) {
var $el, selector;
// If we pass an option index
- if (typeof index !== "undefined") {
- selector = SELECTABLE_CLASSES + ":eq(" + index + ") a";
+ if (typeof index !== 'undefined') {
+ selector = SELECTABLE_CLASSES + ':eq(' + index + ') a';
} else {
- selector = ".dropdown-content .is-focused";
+ selector = '.dropdown-content .is-focused';
}
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one " + selector;
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = '.dropdown-page-one ' + selector;
}
// simulate a click on the first link
$el = $(selector, this.dropdown);
@@ -867,44 +971,47 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.addArrowKeyEvent = function() {
var $input, ARROW_KEY_CODES, selector;
ARROW_KEY_CODES = [38, 40];
- $input = this.dropdown.find(".dropdown-input-field");
+ $input = this.dropdown.find('.dropdown-input-field');
selector = SELECTABLE_CLASSES;
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one " + selector;
- }
- return $('body').on('keydown', (function(_this) {
- return function(e) {
- var $listItems, PREV_INDEX, currentKeyCode;
- currentKeyCode = e.which;
- if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
- e.preventDefault();
- e.stopImmediatePropagation();
- PREV_INDEX = currentIndex;
- $listItems = $(selector, _this.dropdown);
- // if @options.filterable
- // $input.blur()
- if (currentKeyCode === 40) {
- // Move down
- if (currentIndex < ($listItems.length - 1)) {
- currentIndex += 1;
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = '.dropdown-page-one ' + selector;
+ }
+ return $('body').on(
+ 'keydown',
+ (function(_this) {
+ return function(e) {
+ var $listItems, PREV_INDEX, currentKeyCode;
+ currentKeyCode = e.which;
+ if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ PREV_INDEX = currentIndex;
+ $listItems = $(selector, _this.dropdown);
+ // if @options.filterable
+ // $input.blur()
+ if (currentKeyCode === 40) {
+ // Move down
+ if (currentIndex < $listItems.length - 1) {
+ currentIndex += 1;
+ }
+ } else if (currentKeyCode === 38) {
+ // Move up
+ if (currentIndex > 0) {
+ currentIndex -= 1;
+ }
}
- } else if (currentKeyCode === 38) {
- // Move up
- if (currentIndex > 0) {
- currentIndex -= 1;
+ if (currentIndex !== PREV_INDEX) {
+ _this.highlightRowAtIndex($listItems, currentIndex);
}
+ return false;
}
- if (currentIndex !== PREV_INDEX) {
- _this.highlightRowAtIndex($listItems, currentIndex);
+ if (currentKeyCode === 13 && currentIndex !== -1) {
+ e.preventDefault();
+ _this.selectRowAtIndex();
}
- return false;
- }
- if (currentKeyCode === 13 && currentIndex !== -1) {
- e.preventDefault();
- _this.selectRowAtIndex();
- }
- };
- })(this));
+ };
+ })(this),
+ );
};
GitLabDropdown.prototype.removeArrayKeyEvent = function() {
@@ -917,12 +1024,25 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
- var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
+ var $dropdownContent,
+ $listItem,
+ dropdownContentBottom,
+ dropdownContentHeight,
+ dropdownContentTop,
+ dropdownScrollTop,
+ listItemBottom,
+ listItemHeight,
+ listItemTop;
+
+ if (!$listItems) {
+ $listItems = $(SELECTABLE_CLASSES, this.dropdown);
+ }
+
// Remove the class for the previously focused row
$('.is-focused', this.dropdown).removeClass('is-focused');
// Update the class for the row at the specific index
$listItem = $listItems.eq(index);
- $listItem.find('a:first-child').addClass("is-focused");
+ $listItem.find('a:first-child').addClass('is-focused');
// Dropdown content scroll area
$dropdownContent = $listItem.closest('.dropdown-content');
dropdownScrollTop = $dropdownContent.scrollTop();
@@ -936,15 +1056,19 @@ GitLabDropdown = (function() {
if (!index) {
// Scroll the dropdown content to the top
$dropdownContent.scrollTop(0);
- } else if (index === ($listItems.length - 1)) {
+ } else if (index === $listItems.length - 1) {
// Scroll the dropdown content to the bottom
$dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
- } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
+ } else if (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
// Scroll the dropdown content down
- $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
- } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
+ $dropdownContent.scrollTop(
+ listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING,
+ );
+ } else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
// Scroll the dropdown content up
- return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
+ return $dropdownContent.scrollTop(
+ listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING,
+ );
}
};
@@ -965,7 +1089,9 @@ GitLabDropdown = (function() {
toggleText = this.options.updateLabel;
}
- return $(this.el).find(".dropdown-toggle-text").text(toggleText);
+ return $(this.el)
+ .find('.dropdown-toggle-text')
+ .text(toggleText);
};
GitLabDropdown.prototype.clearField = function(field, isInput) {
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index efbf2e3a295..2b9e2a929fc 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -78,17 +78,10 @@ export default {
>
<div
:class="{ 'project-row-contents': !isGroup }"
- class="group-row-contents">
- <item-actions
- v-if="isGroup"
- :group="group"
- :parent-group="parentGroup"
- />
- <item-stats
- :item="group"
- />
+ class="group-row-contents d-flex justify-content-end align-items-center"
+ >
<div
- class="folder-toggle-wrap"
+ class="folder-toggle-wrap append-right-4 d-flex align-items-center"
>
<item-caret
:is-group-open="group.isOpen"
@@ -100,7 +93,7 @@ export default {
</div>
<div
:class="{ 'content-loading': group.isChildrenLoading }"
- class="avatar-container prepend-top-8 prepend-left-5 s24 d-none d-sm-block"
+ class="avatar-container s24 d-none d-sm-block"
>
<a
:href="group.relativePath"
@@ -120,32 +113,46 @@ export default {
</a>
</div>
<div
- class="title namespace-title"
+ class="group-text flex-grow"
>
- <a
- v-tooltip
- :href="group.relativePath"
- :title="group.fullName"
- class="no-expand"
- data-placement="bottom"
- >{{
- // ending bracket must be by closing tag to prevent
- // link hover text-decoration from over-extending
- group.name
- }}</a>
- <span
- v-if="group.permission"
- class="user-access-role"
+ <div
+ class="title namespace-title append-right-8"
>
- {{ group.permission }}
- </span>
- </div>
- <div
- v-if="group.description"
- class="description">
- <span v-html="group.description">
- </span>
+ <a
+ v-tooltip
+ :href="group.relativePath"
+ :title="group.fullName"
+ class="no-expand"
+ data-placement="bottom"
+ >{{
+ // ending bracket must be by closing tag to prevent
+ // link hover text-decoration from over-extending
+ group.name
+ }}</a>
+ <span
+ v-if="group.permission"
+ class="user-access-role"
+ >
+ {{ group.permission }}
+ </span>
+ </div>
+ <div
+ v-if="group.description"
+ class="description"
+ >
+ <span v-html="group.description">
+ </span>
+ </div>
</div>
+ <item-stats
+ :item="group"
+ class="group-stats prepend-top-2"
+ />
+ <item-actions
+ v-if="isGroup"
+ :group="group"
+ :parent-group="parentGroup"
+ />
</div>
<group-folder
v-if="group.isOpen && hasChildren"
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index 62697e0ecc3..2cebacc1c4c 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -13,11 +13,8 @@ export default {
tooltip,
},
computed: {
- ...mapGetters(['currentProject', 'hasChanges']),
+ ...mapGetters(['hasChanges']),
...mapState(['currentActivityView']),
- goBackUrl() {
- return document.referrer || this.currentProject.web_url;
- },
},
methods: {
...mapActions(['updateActivityBarView']),
@@ -36,22 +33,6 @@ export default {
<template>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
- <li v-once>
- <a
- v-tooltip
- :href="goBackUrl"
- :title="s__('IDE|Go back')"
- :aria-label="s__('IDE|Go back')"
- data-container="body"
- data-placement="right"
- class="ide-sidebar-link"
- >
- <icon
- :size="16"
- name="go-back"
- />
- </a>
- </li>
<li>
<button
v-tooltip
diff --git a/app/assets/javascripts/ide/components/branches/item.vue b/app/assets/javascripts/ide/components/branches/item.vue
new file mode 100644
index 00000000000..cc3e84e3f77
--- /dev/null
+++ b/app/assets/javascripts/ide/components/branches/item.vue
@@ -0,0 +1,60 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
+import router from '../../ide_router';
+
+export default {
+ components: {
+ Icon,
+ Timeago,
+ },
+ props: {
+ item: {
+ type: Object,
+ required: true,
+ },
+ projectId: {
+ type: String,
+ required: true,
+ },
+ isActive: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ branchHref() {
+ return router.resolve(`/project/${this.projectId}/edit/${this.item.name}`).href;
+ },
+ },
+};
+</script>
+
+<template>
+ <a
+ :href="branchHref"
+ class="btn-link d-flex align-items-center"
+ >
+ <span class="d-flex append-right-default ide-search-list-current-icon">
+ <icon
+ v-if="isActive"
+ :size="18"
+ name="mobile-issue-close"
+ />
+ </span>
+ <span>
+ <strong>
+ {{ item.name }}
+ </strong>
+ <span
+ class="ide-merge-request-project-path d-block mt-1"
+ >
+ Updated
+ <timeago
+ :time="item.committedDate || ''"
+ />
+ </span>
+ </span>
+ </a>
+</template>
diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue
new file mode 100644
index 00000000000..6db7b9d6b0e
--- /dev/null
+++ b/app/assets/javascripts/ide/components/branches/search_list.vue
@@ -0,0 +1,111 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import _ from 'underscore';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import Item from './item.vue';
+
+export default {
+ components: {
+ LoadingIcon,
+ Item,
+ Icon,
+ },
+ data() {
+ return {
+ search: '',
+ };
+ },
+ computed: {
+ ...mapState('branches', ['branches', 'isLoading']),
+ ...mapState(['currentBranchId', 'currentProjectId']),
+ hasBranches() {
+ return this.branches.length !== 0;
+ },
+ hasNoSearchResults() {
+ return this.search !== '' && !this.hasBranches;
+ },
+ },
+ watch: {
+ isLoading: {
+ handler: 'focusSearch',
+ },
+ },
+ mounted() {
+ this.loadBranches();
+ },
+ methods: {
+ ...mapActions('branches', ['fetchBranches']),
+ loadBranches() {
+ this.fetchBranches({ search: this.search });
+ },
+ searchBranches: _.debounce(function debounceSearch() {
+ this.loadBranches();
+ }, 250),
+ focusSearch() {
+ if (!this.isLoading) {
+ this.$nextTick(() => {
+ this.$refs.searchInput.focus();
+ });
+ }
+ },
+ isActiveBranch(item) {
+ return item.name === this.currentBranchId;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="dropdown-input mt-3 pb-3 mb-0 border-bottom">
+ <div class="position-relative">
+ <input
+ ref="searchInput"
+ :placeholder="__('Search branches')"
+ v-model="search"
+ type="search"
+ class="form-control dropdown-input-field"
+ @input="searchBranches"
+ />
+ <icon
+ :size="18"
+ name="search"
+ class="input-icon"
+ />
+ </div>
+ </div>
+ <div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
+ <loading-icon
+ v-if="isLoading"
+ class="mt-3 mb-3 align-self-center ml-auto mr-auto"
+ size="2"
+ />
+ <ul
+ v-else
+ class="mb-3 w-100"
+ >
+ <template v-if="hasBranches">
+ <li
+ v-for="item in branches"
+ :key="item.name"
+ >
+ <item
+ :item="item"
+ :project-id="currentProjectId"
+ :is-active="isActiveBranch(item)"
+ />
+ </li>
+ </template>
+ <li
+ v-else
+ class="ide-search-list-empty d-flex align-items-center justify-content-center"
+ >
+ <template v-if="hasNoSearchResults">
+ {{ __('No branches found') }}
+ </template>
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_header.vue b/app/assets/javascripts/ide/components/ide_project_header.vue
new file mode 100644
index 00000000000..6cf190288e8
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_project_header.vue
@@ -0,0 +1,37 @@
+<script>
+import ProjectAvatarDefault from '~/vue_shared/components/project_avatar/default.vue';
+
+export default {
+ components: {
+ ProjectAvatarDefault,
+ },
+ props: {
+ project: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="context-header ide-context-header">
+ <a
+ :href="project.web_url"
+ :title="s__('IDE|Go to project')"
+ >
+ <project-avatar-default
+ :project="project"
+ :size="48"
+ />
+ <span class="ide-sidebar-project-title">
+ <span class="sidebar-context-title">
+ {{ project.name }}
+ </span>
+ <span class="sidebar-context-title text-secondary">
+ {{ project.path_with_namespace }}
+ </span>
+ </span>
+ </a>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index 21906674c4b..4771c58a11d 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -1,12 +1,6 @@
<script>
-import $ from 'jquery';
import { mapState, mapGetters } from 'vuex';
-import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
-import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
-import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-import Identicon from '../../vue_shared/components/identicon.vue';
import IdeTree from './ide_tree.vue';
import ResizablePanel from './resizable_panel.vue';
import ActivityBar from './activity_bar.vue';
@@ -14,43 +8,28 @@ import CommitSection from './repo_commit_section.vue';
import CommitForm from './commit_sidebar/form.vue';
import IdeReview from './ide_review.vue';
import SuccessMessage from './commit_sidebar/success_message.vue';
-import MergeRequestDropdown from './merge_requests/dropdown.vue';
+import IdeProjectHeader from './ide_project_header.vue';
import { activityBarViews } from '../constants';
export default {
- directives: {
- tooltip,
- },
components: {
- Icon,
- PanelResizer,
SkeletonLoadingContainer,
ResizablePanel,
ActivityBar,
- ProjectAvatarImage,
- Identicon,
CommitSection,
IdeTree,
CommitForm,
IdeReview,
SuccessMessage,
- MergeRequestDropdown,
- },
- data() {
- return {
- showTooltip: false,
- showMergeRequestsDropdown: false,
- };
+ IdeProjectHeader,
},
computed: {
...mapState([
'loading',
- 'currentBranchId',
'currentActivityView',
'changedFiles',
'stagedFiles',
'lastCommitMsg',
- 'currentMergeRequestId',
]),
...mapGetters(['currentProject', 'someUncommitedChanges']),
showSuccessMessage() {
@@ -59,46 +38,6 @@ export default {
(this.lastCommitMsg && !this.someUncommitedChanges)
);
},
- branchTooltipTitle() {
- return this.showTooltip ? this.currentBranchId : undefined;
- },
- },
- watch: {
- currentBranchId() {
- this.$nextTick(() => {
- if (!this.$refs.branchId) return;
-
- this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
- });
- },
- loading() {
- this.$nextTick(() => {
- this.addDropdownListeners();
- });
- },
- },
- mounted() {
- this.addDropdownListeners();
- },
- beforeDestroy() {
- $(this.$refs.mergeRequestDropdown)
- .off('show.bs.dropdown')
- .off('hide.bs.dropdown');
- },
- methods: {
- addDropdownListeners() {
- if (!this.$refs.mergeRequestDropdown) return;
-
- $(this.$refs.mergeRequestDropdown)
- .on('show.bs.dropdown', () => {
- this.toggleMergeRequestDropdown();
- }).on('hide.bs.dropdown', () => {
- this.toggleMergeRequestDropdown();
- });
- },
- toggleMergeRequestDropdown() {
- this.showMergeRequestsDropdown = !this.showMergeRequestsDropdown;
- },
},
};
</script>
@@ -108,12 +47,10 @@ export default {
:collapsible="false"
:initial-width="340"
side="left"
+ class="flex-column"
>
- <activity-bar
- v-if="!loading"
- />
- <div class="multi-file-commit-panel-inner">
- <template v-if="loading">
+ <template v-if="loading">
+ <div class="multi-file-commit-panel-inner">
<div
v-for="n in 3"
:key="n"
@@ -121,81 +58,23 @@ export default {
>
<skeleton-loading-container />
</div>
- </template>
- <template v-else>
- <div
- ref="mergeRequestDropdown"
- class="context-header ide-context-header dropdown"
- >
- <button
- type="button"
- data-toggle="dropdown"
- >
- <div
- v-if="currentProject.avatar_url"
- class="avatar-container s40 project-avatar"
- >
- <project-avatar-image
- :link-href="currentProject.path"
- :img-src="currentProject.avatar_url"
- :img-alt="currentProject.name"
- :img-size="40"
- class="avatar-container project-avatar"
- />
- </div>
- <identicon
- v-else
- :entity-id="currentProject.id"
- :entity-name="currentProject.name"
- size-class="s40"
+ </div>
+ </template>
+ <template v-else>
+ <ide-project-header
+ :project="currentProject"
+ />
+ <div class="ide-context-body d-flex flex-fill">
+ <activity-bar />
+ <div class="multi-file-commit-panel-inner">
+ <div class="multi-file-commit-panel-inner-content">
+ <component
+ :is="currentActivityView"
/>
- <div class="ide-sidebar-project-title">
- <div class="sidebar-context-title">
- {{ currentProject.name }}
- </div>
- <div class="d-flex">
- <div
- v-tooltip
- v-if="currentBranchId"
- ref="branchId"
- :title="branchTooltipTitle"
- class="sidebar-context-title ide-sidebar-branch-title"
- >
- <icon
- name="branch"
- css-classes="append-right-5"
- />{{ currentBranchId }}
- </div>
- <div
- v-if="currentMergeRequestId"
- :class="{
- 'prepend-left-8': currentBranchId
- }"
- class="sidebar-context-title ide-sidebar-branch-title"
- >
- <icon
- name="git-merge"
- css-classes="append-right-5"
- />!{{ currentMergeRequestId }}
- </div>
- </div>
- </div>
- <icon
- class="ml-auto"
- name="chevron-down"
- />
- </button>
- <merge-request-dropdown
- :show="showMergeRequestsDropdown"
- />
- </div>
- <div class="multi-file-commit-panel-inner-scroll">
- <component
- :is="currentActivityView"
- />
+ </div>
+ <commit-form />
</div>
- <commit-form />
- </template>
- </div>
+ </div>
+ </template>
</resizable-panel>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
index e996dd9059e..39d46a91731 100644
--- a/app/assets/javascripts/ide/components/ide_tree.vue
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -35,14 +35,13 @@ export default {
<template>
<ide-tree-list
- header-class="d-flex w-100"
viewer-type="editor"
>
<template
slot="header"
>
{{ __('Edit') }}
- <div class="ml-auto d-flex">
+ <div class="ide-tree-actions ml-auto d-flex">
<new-entry-button
:label="__('New file')"
:show-label="false"
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index 2e7226b727c..5611b37be7c 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -3,14 +3,14 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import RepoFile from './repo_file.vue';
-import NewDropdown from './new_dropdown/index.vue';
+import NavDropdown from './nav_dropdown.vue';
export default {
components: {
Icon,
RepoFile,
SkeletonLoadingContainer,
- NewDropdown,
+ NavDropdown,
},
props: {
viewerType: {
@@ -57,14 +57,19 @@ export default {
:class="headerClass"
class="ide-tree-header"
>
+ <nav-dropdown />
<slot name="header"></slot>
</header>
- <repo-file
- v-for="file in currentTree.tree"
- :key="file.key"
- :file="file"
- :level="0"
- />
+ <div
+ class="ide-tree-body"
+ >
+ <repo-file
+ v-for="file in currentTree.tree"
+ :key="file.key"
+ :file="file"
+ :level="0"
+ />
+ </div>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/merge_requests/dropdown.vue b/app/assets/javascripts/ide/components/merge_requests/dropdown.vue
deleted file mode 100644
index 4b9824bf04b..00000000000
--- a/app/assets/javascripts/ide/components/merge_requests/dropdown.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<script>
-import { mapGetters } from 'vuex';
-import Tabs from '../../../vue_shared/components/tabs/tabs';
-import Tab from '../../../vue_shared/components/tabs/tab.vue';
-import List from './list.vue';
-
-export default {
- components: {
- Tabs,
- Tab,
- List,
- },
- props: {
- show: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- ...mapGetters('mergeRequests', ['assignedData', 'createdData']),
- createdMergeRequestLength() {
- return this.createdData.mergeRequests.length;
- },
- assignedMergeRequestLength() {
- return this.assignedData.mergeRequests.length;
- },
- },
-};
-</script>
-
-<template>
- <div class="dropdown-menu ide-merge-requests-dropdown p-0">
- <tabs
- v-if="show"
- stop-propagation
- >
- <tab active>
- <template slot="title">
- {{ __('Created by me') }}
- <span class="badge badge-pill">
- {{ createdMergeRequestLength }}
- </span>
- </template>
- <list
- :empty-text="__('You have not created any merge requests')"
- type="created"
- />
- </tab>
- <tab>
- <template slot="title">
- {{ __('Assigned to me') }}
- <span class="badge badge-pill">
- {{ assignedMergeRequestLength }}
- </span>
- </template>
- <list
- :empty-text="__('You do not have any assigned merge requests')"
- type="assigned"
- />
- </tab>
- </tabs>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/merge_requests/item.vue b/app/assets/javascripts/ide/components/merge_requests/item.vue
index 4e18376bd48..0c4ea80ba08 100644
--- a/app/assets/javascripts/ide/components/merge_requests/item.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/item.vue
@@ -1,5 +1,6 @@
<script>
import Icon from '../../../vue_shared/components/icon.vue';
+import router from '../../ide_router';
export default {
components: {
@@ -29,22 +30,21 @@ export default {
pathWithID() {
return `${this.item.projectPathWithNamespace}!${this.item.iid}`;
},
- },
- methods: {
- clickItem() {
- this.$emit('click', this.item);
+ mergeRequestHref() {
+ const path = `/project/${this.item.projectPathWithNamespace}/merge_requests/${this.item.iid}`;
+
+ return router.resolve(path).href;
},
},
};
</script>
<template>
- <button
- type="button"
+ <a
+ :href="mergeRequestHref"
class="btn-link d-flex align-items-center"
- @click="clickItem"
>
- <span class="d-flex append-right-default ide-merge-request-current-icon">
+ <span class="d-flex append-right-default ide-search-list-current-icon">
<icon
v-if="isActive"
:size="18"
@@ -59,5 +59,5 @@ export default {
{{ pathWithID }}
</span>
</span>
- </button>
+ </a>
</template>
diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue
index 19d3e48ee10..fc612956688 100644
--- a/app/assets/javascripts/ide/components/merge_requests/list.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/list.vue
@@ -1,96 +1,101 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
-import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import { __ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Item from './item.vue';
+import TokenedInput from '../shared/tokened_input.vue';
+
+const SEARCH_TYPES = [
+ { type: 'created', label: __('Created by me') },
+ { type: 'assigned', label: __('Assigned to me') },
+];
export default {
components: {
LoadingIcon,
+ TokenedInput,
Item,
- },
- props: {
- type: {
- type: String,
- required: true,
- },
- emptyText: {
- type: String,
- required: true,
- },
+ Icon,
},
data() {
return {
search: '',
+ currentSearchType: null,
+ hasSearchFocus: false,
};
},
computed: {
- ...mapGetters('mergeRequests', ['getData']),
+ ...mapState('mergeRequests', ['mergeRequests', 'isLoading']),
...mapState(['currentMergeRequestId', 'currentProjectId']),
- data() {
- return this.getData(this.type);
- },
- isLoading() {
- return this.data.isLoading;
- },
- mergeRequests() {
- return this.data.mergeRequests;
- },
hasMergeRequests() {
return this.mergeRequests.length !== 0;
},
hasNoSearchResults() {
return this.search !== '' && !this.hasMergeRequests;
},
+ showSearchTypes() {
+ return this.hasSearchFocus && !this.search && !this.currentSearchType;
+ },
+ type() {
+ return this.currentSearchType
+ ? this.currentSearchType.type
+ : '';
+ },
+ searchTokens() {
+ return this.currentSearchType
+ ? [this.currentSearchType]
+ : [];
+ },
},
watch: {
- isLoading: {
- handler: 'focusSearch',
+ search() {
+ // When the search is updated, let's turn off this flag to hide the search types
+ this.hasSearchFocus = false;
},
},
mounted() {
this.loadMergeRequests();
},
methods: {
- ...mapActions('mergeRequests', ['fetchMergeRequests', 'openMergeRequest']),
+ ...mapActions('mergeRequests', ['fetchMergeRequests']),
loadMergeRequests() {
this.fetchMergeRequests({ type: this.type, search: this.search });
},
- viewMergeRequest(item) {
- this.openMergeRequest({
- projectPath: item.projectPathWithNamespace,
- id: item.iid,
- });
- },
searchMergeRequests: _.debounce(function debounceSearch() {
this.loadMergeRequests();
}, 250),
- focusSearch() {
- if (!this.isLoading) {
- this.$nextTick(() => {
- this.$refs.searchInput.focus();
- });
- }
+ onSearchFocus() {
+ this.hasSearchFocus = true;
+ },
+ setSearchType(searchType) {
+ this.currentSearchType = searchType;
+ this.loadMergeRequests();
},
},
+ searchTypes: SEARCH_TYPES,
};
</script>
<template>
<div>
<div class="dropdown-input mt-3 pb-3 mb-0 border-bottom">
- <input
- ref="searchInput"
- :placeholder="__('Search merge requests')"
- v-model="search"
- type="search"
- class="dropdown-input-field"
- @input="searchMergeRequests"
- />
- <i
- aria-hidden="true"
- class="fa fa-search dropdown-input-search"
- ></i>
+ <div class="position-relative">
+ <tokened-input
+ v-model="search"
+ :tokens="searchTokens"
+ :placeholder="__('Search merge requests')"
+ @focus="onSearchFocus"
+ @input="searchMergeRequests"
+ @removeToken="setSearchType(null)"
+ />
+ <icon
+ :size="18"
+ name="search"
+ class="input-icon"
+ />
+ </div>
</div>
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<loading-icon
@@ -98,35 +103,52 @@ export default {
class="mt-3 mb-3 align-self-center ml-auto mr-auto"
size="2"
/>
- <ul
- v-else
- class="mb-3 w-100"
- >
- <template v-if="hasMergeRequests">
- <li
- v-for="item in mergeRequests"
- :key="item.id"
- >
- <item
- :item="item"
- :current-id="currentMergeRequestId"
- :current-project-id="currentProjectId"
- @click="viewMergeRequest"
- />
- </li>
- </template>
- <li
- v-else
- class="ide-merge-requests-empty d-flex align-items-center justify-content-center"
+ <template v-else>
+ <ul
+ class="mb-3 w-100"
>
- <template v-if="hasNoSearchResults">
- {{ __('No merge requests found') }}
+ <template v-if="showSearchTypes">
+ <li
+ v-for="searchType in $options.searchTypes"
+ :key="searchType.type"
+ >
+ <button
+ type="button"
+ class="btn-link d-flex align-items-center"
+ @click.stop="setSearchType(searchType)"
+ >
+ <span class="d-flex append-right-default ide-search-list-current-icon">
+ <icon
+ :size="18"
+ name="search"
+ />
+ </span>
+ <span>
+ {{ searchType.label }}
+ </span>
+ </button>
+ </li>
</template>
- <template v-else>
- {{ emptyText }}
+ <template v-else-if="hasMergeRequests">
+ <li
+ v-for="item in mergeRequests"
+ :key="item.id"
+ >
+ <item
+ :item="item"
+ :current-id="currentMergeRequestId"
+ :current-project-id="currentProjectId"
+ />
+ </li>
</template>
- </li>
- </ul>
+ <li
+ v-else
+ class="ide-search-list-empty d-flex align-items-center justify-content-center"
+ >
+ {{ __('No merge requests found') }}
+ </li>
+ </ul>
+ </template>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/nav_dropdown.vue b/app/assets/javascripts/ide/components/nav_dropdown.vue
new file mode 100644
index 00000000000..db36779c395
--- /dev/null
+++ b/app/assets/javascripts/ide/components/nav_dropdown.vue
@@ -0,0 +1,59 @@
+<script>
+import $ from 'jquery';
+import Icon from '~/vue_shared/components/icon.vue';
+import NavForm from './nav_form.vue';
+import NavDropdownButton from './nav_dropdown_button.vue';
+
+export default {
+ components: {
+ Icon,
+ NavDropdownButton,
+ NavForm,
+ },
+ data() {
+ return {
+ isVisibleDropdown: false,
+ };
+ },
+ mounted() {
+ this.addDropdownListeners();
+ },
+ beforeDestroy() {
+ this.removeDropdownListeners();
+ },
+ methods: {
+ addDropdownListeners() {
+ $(this.$refs.dropdown)
+ .on('show.bs.dropdown', () => this.showDropdown())
+ .on('hide.bs.dropdown', () => this.hideDropdown());
+ },
+ removeDropdownListeners() {
+ $(this.$refs.dropdown)
+ .off('show.bs.dropdown')
+ .off('hide.bs.dropdown');
+ },
+ showDropdown() {
+ this.isVisibleDropdown = true;
+ },
+ hideDropdown() {
+ this.isVisibleDropdown = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ ref="dropdown"
+ class="btn-group ide-nav-dropdown dropdown"
+ >
+ <nav-dropdown-button />
+ <div
+ class="dropdown-menu dropdown-menu-left p-0"
+ >
+ <nav-form
+ v-if="isVisibleDropdown"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/nav_dropdown_button.vue b/app/assets/javascripts/ide/components/nav_dropdown_button.vue
new file mode 100644
index 00000000000..7f98769d484
--- /dev/null
+++ b/app/assets/javascripts/ide/components/nav_dropdown_button.vue
@@ -0,0 +1,54 @@
+<script>
+import { mapState } from 'vuex';
+import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+
+const EMPTY_LABEL = '-';
+
+export default {
+ components: {
+ Icon,
+ DropdownButton,
+ },
+ computed: {
+ ...mapState(['currentBranchId', 'currentMergeRequestId']),
+ mergeRequestLabel() {
+ return this.currentMergeRequestId
+ ? `!${this.currentMergeRequestId}`
+ : EMPTY_LABEL;
+ },
+ branchLabel() {
+ return this.currentBranchId || EMPTY_LABEL;
+ },
+ },
+};
+</script>
+
+<template>
+ <dropdown-button>
+ <span
+ class="row"
+ >
+ <span
+ class="col-7 text-truncate"
+ >
+ <icon
+ :size="16"
+ :aria-label="__('Current Branch')"
+ name="branch"
+ />
+ {{ branchLabel }}
+ </span>
+ <span
+ class="col-5 pl-0 text-truncate"
+ >
+ <icon
+ :size="16"
+ :aria-label="__('Merge Request')"
+ name="merge-request"
+ />
+ {{ mergeRequestLabel }}
+ </span>
+ </span>
+ </dropdown-button>
+</template>
diff --git a/app/assets/javascripts/ide/components/nav_form.vue b/app/assets/javascripts/ide/components/nav_form.vue
new file mode 100644
index 00000000000..718b836e11c
--- /dev/null
+++ b/app/assets/javascripts/ide/components/nav_form.vue
@@ -0,0 +1,40 @@
+<script>
+import Tabs from '~/vue_shared/components/tabs/tabs';
+import Tab from '~/vue_shared/components/tabs/tab.vue';
+import BranchesSearchList from './branches/search_list.vue';
+import MergeRequestSearchList from './merge_requests/list.vue';
+
+export default {
+ components: {
+ Tabs,
+ Tab,
+ BranchesSearchList,
+ MergeRequestSearchList,
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-nav-form p-0"
+ >
+ <tabs
+ stop-propagation
+ >
+ <tab
+ active
+ >
+ <template slot="title">
+ {{ __('Merge Requests') }}
+ </template>
+ <merge-request-search-list />
+ </tab>
+ <tab>
+ <template slot="title">
+ {{ __('Branches') }}
+ </template>
+ <branches-search-list />
+ </tab>
+ </tabs>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index e4a5fcc67c4..79df225c432 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -1,5 +1,5 @@
<script>
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
import { rightSidebarViews } from '../../constants';
@@ -7,6 +7,7 @@ import PipelinesList from '../pipelines/list.vue';
import JobsDetail from '../jobs/detail.vue';
import MergeRequestInfo from '../merge_requests/info.vue';
import ResizablePanel from '../resizable_panel.vue';
+import Clientside from '../preview/clientside.vue';
export default {
directives: {
@@ -18,15 +19,20 @@ export default {
JobsDetail,
ResizablePanel,
MergeRequestInfo,
+ Clientside,
},
computed: {
- ...mapState(['rightPane', 'currentMergeRequestId']),
+ ...mapState(['rightPane', 'currentMergeRequestId', 'clientsidePreviewEnabled']),
+ ...mapGetters(['packageJson']),
pipelinesActive() {
return (
this.rightPane === rightSidebarViews.pipelines ||
this.rightPane === rightSidebarViews.jobsDetail
);
},
+ showLivePreview() {
+ return this.packageJson && this.clientsidePreviewEnabled;
+ },
},
methods: {
...mapActions(['setRightPane']),
@@ -49,8 +55,9 @@ export default {
:collapsible="false"
:initial-width="350"
:min-size="350"
- class="multi-file-commit-panel-inner"
+ :class="`ide-right-sidebar-${rightPane}`"
side="right"
+ class="multi-file-commit-panel-inner"
>
<component :is="rightPane" />
</resizable-panel>
@@ -98,6 +105,26 @@ export default {
/>
</button>
</li>
+ <li v-if="showLivePreview">
+ <button
+ v-tooltip
+ :title="__('Live preview')"
+ :aria-label="__('Live preview')"
+ :class="{
+ active: rightPane === $options.rightSidebarViews.clientSidePreview
+ }"
+ data-container="body"
+ data-placement="left"
+ class="ide-sidebar-link is-right"
+ type="button"
+ @click="clickTab($event, $options.rightSidebarViews.clientSidePreview)"
+ >
+ <icon
+ :size="16"
+ name="live-preview"
+ />
+ </button>
+ </li>
</ul>
</nav>
</div>
diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue
new file mode 100644
index 00000000000..fef36eae7b1
--- /dev/null
+++ b/app/assets/javascripts/ide/components/preview/clientside.vue
@@ -0,0 +1,171 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import _ from 'underscore';
+import { Manager } from 'smooshpack';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import Navigator from './navigator.vue';
+import { packageJsonPath } from '../../constants';
+import { createPathWithExt } from '../../utils';
+
+export default {
+ components: {
+ LoadingIcon,
+ Navigator,
+ },
+ data() {
+ return {
+ manager: {},
+ loading: false,
+ };
+ },
+ computed: {
+ ...mapState(['entries', 'promotionSvgPath', 'links']),
+ ...mapGetters(['packageJson', 'currentProject']),
+ normalizedEntries() {
+ return Object.keys(this.entries).reduce((acc, path) => {
+ const file = this.entries[path];
+
+ if (file.type === 'tree' || !(file.raw || file.content)) return acc;
+
+ return {
+ ...acc,
+ [`/${path}`]: {
+ code: file.content || file.raw,
+ },
+ };
+ }, {});
+ },
+ mainEntry() {
+ if (!this.packageJson.raw) return false;
+
+ const parsedPackage = JSON.parse(this.packageJson.raw);
+
+ return parsedPackage.main;
+ },
+ showPreview() {
+ return this.mainEntry && !this.loading;
+ },
+ showEmptyState() {
+ return !this.mainEntry && !this.loading;
+ },
+ showOpenInCodeSandbox() {
+ return this.currentProject && this.currentProject.visibility === 'public';
+ },
+ sandboxOpts() {
+ return {
+ files: { ...this.normalizedEntries },
+ entry: `/${this.mainEntry}`,
+ showOpenInCodeSandbox: this.showOpenInCodeSandbox,
+ };
+ },
+ },
+ watch: {
+ entries: {
+ deep: true,
+ handler: 'update',
+ },
+ },
+ mounted() {
+ this.loading = true;
+
+ return this.loadFileContent(packageJsonPath)
+ .then(() => {
+ this.loading = false;
+ })
+ .then(() => this.$nextTick())
+ .then(() => this.initPreview());
+ },
+ beforeDestroy() {
+ if (!_.isEmpty(this.manager)) {
+ this.manager.listener();
+ }
+ this.manager = {};
+
+ clearTimeout(this.timeout);
+ this.timeout = null;
+ },
+ methods: {
+ ...mapActions(['getFileData', 'getRawFileData']),
+ loadFileContent(path) {
+ return this.getFileData({ path, makeFileActive: false }).then(() =>
+ this.getRawFileData({ path }),
+ );
+ },
+ initPreview() {
+ if (!this.mainEntry) return null;
+
+ return this.loadFileContent(this.mainEntry)
+ .then(() => this.$nextTick())
+ .then(() =>
+ this.initManager('#ide-preview', this.sandboxOpts, {
+ fileResolver: {
+ isFile: p => Promise.resolve(!!this.entries[createPathWithExt(p)]),
+ readFile: p => this.loadFileContent(createPathWithExt(p)).then(content => content),
+ },
+ }),
+ );
+ },
+ update() {
+ if (this.timeout) return;
+
+ this.timeout = setTimeout(() => {
+ if (_.isEmpty(this.manager)) {
+ this.initPreview();
+
+ return;
+ }
+
+ this.manager.updatePreview(this.sandboxOpts);
+
+ clearTimeout(this.timeout);
+ this.timeout = null;
+ }, 500);
+ },
+ initManager(el, opts, resolver) {
+ this.manager = new Manager(el, opts, resolver);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="preview h-100 w-100 d-flex flex-column">
+ <template v-if="showPreview">
+ <navigator
+ :manager="manager"
+ />
+ <div id="ide-preview"></div>
+ </template>
+ <div
+ v-else-if="showEmptyState"
+ v-once
+ class="d-flex h-100 flex-column align-items-center justify-content-center svg-content"
+ >
+ <img
+ :src="promotionSvgPath"
+ :alt="s__('IDE|Live Preview')"
+ width="130"
+ height="100"
+ />
+ <h3>
+ {{ s__('IDE|Live Preview') }}
+ </h3>
+ <p class="text-center">
+ {{ s__('IDE|Preview your web application using Web IDE client-side evaluation.') }}
+ </p>
+ <a
+ :href="links.webIDEHelpPagePath"
+ class="btn btn-primary"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {{ s__('IDE|Get started with Live Preview') }}
+ </a>
+ </div>
+ <loading-icon
+ v-else
+ size="2"
+ class="align-self-center mt-auto mb-auto"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/preview/navigator.vue b/app/assets/javascripts/ide/components/preview/navigator.vue
new file mode 100644
index 00000000000..4bf346946b6
--- /dev/null
+++ b/app/assets/javascripts/ide/components/preview/navigator.vue
@@ -0,0 +1,147 @@
+<script>
+import { listen } from 'codesandbox-api';
+import Icon from '~/vue_shared/components/icon.vue';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+
+export default {
+ components: {
+ Icon,
+ LoadingIcon,
+ },
+ props: {
+ manager: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ currentBrowsingIndex: null,
+ navigationStack: [],
+ forwardNavigationStack: [],
+ path: '',
+ loading: true,
+ };
+ },
+ computed: {
+ backButtonDisabled() {
+ return this.navigationStack.length <= 1;
+ },
+ forwardButtonDisabled() {
+ return !this.forwardNavigationStack.length;
+ },
+ },
+ mounted() {
+ this.listener = listen(e => {
+ switch (e.type) {
+ case 'urlchange':
+ this.onUrlChange(e);
+ break;
+ case 'done':
+ this.loading = false;
+ break;
+ default:
+ break;
+ }
+ });
+ },
+ beforeDestroy() {
+ this.listener();
+ },
+ methods: {
+ onUrlChange(e) {
+ const lastPath = this.path;
+
+ this.path = e.url.replace(this.manager.bundlerURL, '') || '/';
+
+ if (lastPath !== this.path) {
+ this.currentBrowsingIndex =
+ this.currentBrowsingIndex === null ? 0 : this.currentBrowsingIndex + 1;
+ this.navigationStack.push(this.path);
+ }
+ },
+ back() {
+ const lastPath = this.path;
+
+ this.visitPath(this.navigationStack[this.currentBrowsingIndex - 1]);
+
+ this.forwardNavigationStack.push(lastPath);
+
+ if (this.currentBrowsingIndex === 1) {
+ this.currentBrowsingIndex = null;
+ this.navigationStack = [];
+ }
+ },
+ forward() {
+ this.visitPath(this.forwardNavigationStack.splice(0, 1)[0]);
+ },
+ refresh() {
+ this.visitPath(this.path);
+ },
+ visitPath(path) {
+ this.manager.iframe.src = `${this.manager.bundlerURL}${path}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <header class="ide-preview-header d-flex align-items-center">
+ <button
+ :aria-label="s__('IDE|Back')"
+ :disabled="backButtonDisabled"
+ :class="{
+ 'disabled-content': backButtonDisabled
+ }"
+ type="button"
+ class="ide-navigator-btn d-flex align-items-center d-transparent border-0 bg-transparent"
+ @click="back"
+ >
+ <icon
+ :size="24"
+ name="chevron-left"
+ class="m-auto"
+ />
+ </button>
+ <button
+ :aria-label="s__('IDE|Back')"
+ :disabled="forwardButtonDisabled"
+ :class="{
+ 'disabled-content': forwardButtonDisabled
+ }"
+ type="button"
+ class="ide-navigator-btn d-flex align-items-center d-transparent border-0 bg-transparent"
+ @click="forward"
+ >
+ <icon
+ :size="24"
+ name="chevron-right"
+ class="m-auto"
+ />
+ </button>
+ <button
+ :aria-label="s__('IDE|Refresh preview')"
+ type="button"
+ class="ide-navigator-btn d-flex align-items-center d-transparent border-0 bg-transparent"
+ @click="refresh"
+ >
+ <icon
+ :size="18"
+ name="retry"
+ class="m-auto"
+ />
+ </button>
+ <div class="position-relative w-100 prepend-left-4">
+ <input
+ :value="path || '/'"
+ type="text"
+ class="ide-navigator-location form-control bg-white"
+ readonly
+ />
+ <loading-icon
+ v-if="loading"
+ class="position-absolute ide-preview-loading-icon"
+ />
+ </div>
+ </header>
+</template>
diff --git a/app/assets/javascripts/ide/components/shared/tokened_input.vue b/app/assets/javascripts/ide/components/shared/tokened_input.vue
new file mode 100644
index 00000000000..a7a12f6785d
--- /dev/null
+++ b/app/assets/javascripts/ide/components/shared/tokened_input.vue
@@ -0,0 +1,121 @@
+<script>
+import { __ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ placeholder: {
+ type: String,
+ required: false,
+ default: __('Search'),
+ },
+ tokens: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ backspaceCount: 0,
+ };
+ },
+ computed: {
+ placeholderText() {
+ return this.tokens.length
+ ? ''
+ : this.placeholder;
+ },
+ },
+ watch: {
+ tokens() {
+ this.$refs.input.focus();
+ },
+ },
+ methods: {
+ onFocus() {
+ this.$emit('focus');
+ },
+ onBlur() {
+ this.$emit('blur');
+ },
+ onInput(evt) {
+ this.$emit('input', evt.target.value);
+ },
+ onBackspace() {
+ if (!this.value && this.tokens.length) {
+ this.backspaceCount += 1;
+ } else {
+ this.backspaceCount = 0;
+ return;
+ }
+
+ if (this.backspaceCount > 1) {
+ this.removeToken(this.tokens[this.tokens.length - 1]);
+ this.backspaceCount = 0;
+ }
+ },
+ removeToken(token) {
+ this.$emit('removeToken', token);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="filtered-search-wrapper">
+ <div class="filtered-search-box">
+ <div class="tokens-container list-unstyled">
+ <div
+ v-for="token in tokens"
+ :key="token.label"
+ class="filtered-search-token"
+ >
+ <button
+ class="selectable btn-blank"
+ type="button"
+ @click.stop="removeToken(token)"
+ @keyup.delete="removeToken(token)"
+ >
+ <div
+ class="value-container rounded"
+ >
+ <div
+ class="value"
+ >{{ token.label }}</div>
+ <div
+ class="remove-token inverted"
+ >
+ <icon
+ :size="10"
+ name="close"
+ />
+ </div>
+ </div>
+ </button>
+ </div>
+ <div class="input-token">
+ <input
+ ref="input"
+ :placeholder="placeholderText"
+ :value="value"
+ type="search"
+ class="form-control filtered-search"
+ @input="onInput"
+ @focus="onFocus"
+ @blur="onBlur"
+ @keyup.delete="onBackspace"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index d3ac57471c9..8caa5b86a9b 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -32,6 +32,7 @@ export const rightSidebarViews = {
pipelines: 'pipelines-list',
jobsDetail: 'jobs-detail',
mergeRequestInfo: 'merge-request-info',
+ clientSidePreview: 'clientside',
};
export const stageKeys = {
@@ -58,3 +59,5 @@ export const modalTypes = {
rename: 'rename',
tree: 'tree',
};
+
+export const packageJsonPath = 'package.json';
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index 2d74192e6b3..79e38ae911e 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -4,6 +4,7 @@ import Translate from '~/vue_shared/translate';
import ide from './components/ide.vue';
import store from './stores';
import router from './ide_router';
+import { convertPermissionToBoolean } from '../lib/utils/common_utils';
Vue.use(Translate);
@@ -23,13 +24,18 @@ export function initIde(el) {
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
committedStateSvgPath: el.dataset.committedStateSvgPath,
pipelinesEmptyStateSvgPath: el.dataset.pipelinesEmptyStateSvgPath,
+ promotionSvgPath: el.dataset.promotionSvgPath,
});
this.setLinks({
ciHelpPagePath: el.dataset.ciHelpPagePath,
+ webIDEHelpPagePath: el.dataset.webIdeHelpPagePath,
+ });
+ this.setInitialData({
+ clientsidePreviewEnabled: convertPermissionToBoolean(el.dataset.clientsidePreviewEnabled),
});
},
methods: {
- ...mapActions(['setEmptyStateSvgs', 'setLinks']),
+ ...mapActions(['setEmptyStateSvgs', 'setLinks', 'setInitialData']),
},
render(createElement) {
return createElement('ide');
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 79cdb494e5a..709748fb530 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,5 +1,5 @@
import { getChangesCountForFiles, filePathMatches } from './utils';
-import { activityBarViews } from '../constants';
+import { activityBarViews, packageJsonPath } from '../constants';
export const activeFile = state => state.openFiles.find(file => file.active) || null;
@@ -90,5 +90,7 @@ export const lastCommit = (state, getters) => {
export const currentBranch = (state, getters) =>
getters.currentProject && getters.currentProject.branches[state.currentBranchId];
+export const packageJson = state => state.entries[packageJsonPath];
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index f8ce8a67ec0..a601dc8f5a0 100644
--- a/app/assets/javascripts/ide/stores/index.js
+++ b/app/assets/javascripts/ide/stores/index.js
@@ -7,6 +7,7 @@ import mutations from './mutations';
import commitModule from './modules/commit';
import pipelines from './modules/pipelines';
import mergeRequests from './modules/merge_requests';
+import branches from './modules/branches';
Vue.use(Vuex);
@@ -20,6 +21,7 @@ export const createStore = () =>
commit: commitModule,
pipelines,
mergeRequests,
+ branches,
},
});
diff --git a/app/assets/javascripts/ide/stores/modules/branches/actions.js b/app/assets/javascripts/ide/stores/modules/branches/actions.js
new file mode 100644
index 00000000000..74aa98ef9f9
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/branches/actions.js
@@ -0,0 +1,39 @@
+import { __ } from '~/locale';
+import Api from '~/api';
+import * as types from './mutation_types';
+
+export const requestBranches = ({ commit }) => commit(types.REQUEST_BRANCHES);
+export const receiveBranchesError = ({ commit, dispatch }, { search }) => {
+ dispatch(
+ 'setErrorMessage',
+ {
+ text: __('Error loading branches.'),
+ action: payload =>
+ dispatch('fetchBranches', payload).then(() =>
+ dispatch('setErrorMessage', null, { root: true }),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: { search },
+ },
+ { root: true },
+ );
+ commit(types.RECEIVE_BRANCHES_ERROR);
+};
+export const receiveBranchesSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_BRANCHES_SUCCESS, data);
+
+export const fetchBranches = ({ dispatch, rootGetters }, { search = '' }) => {
+ dispatch('requestBranches');
+ dispatch('resetBranches');
+
+ return Api.branches(rootGetters.currentProject.id, search, { sort: 'updated_desc' })
+ .then(({ data }) => dispatch('receiveBranchesSuccess', data))
+ .catch(() => dispatch('receiveBranchesError', { search }));
+};
+
+export const resetBranches = ({ commit }) => commit(types.RESET_BRANCHES);
+
+export const openBranch = ({ rootState, dispatch }, id) =>
+ dispatch('goToRoute', `/project/${rootState.currentProjectId}/edit/${id}`, { root: true });
+
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/branches/index.js b/app/assets/javascripts/ide/stores/modules/branches/index.js
new file mode 100644
index 00000000000..04e7e0f08f1
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/branches/index.js
@@ -0,0 +1,10 @@
+import state from './state';
+import * as actions from './actions';
+import mutations from './mutations';
+
+export default {
+ namespaced: true,
+ state: state(),
+ actions,
+ mutations,
+};
diff --git a/app/assets/javascripts/ide/stores/modules/branches/mutation_types.js b/app/assets/javascripts/ide/stores/modules/branches/mutation_types.js
new file mode 100644
index 00000000000..2272f7b9531
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/branches/mutation_types.js
@@ -0,0 +1,5 @@
+export const REQUEST_BRANCHES = 'REQUEST_BRANCHES';
+export const RECEIVE_BRANCHES_ERROR = 'RECEIVE_BRANCHES_ERROR';
+export const RECEIVE_BRANCHES_SUCCESS = 'RECEIVE_BRANCHES_SUCCESS';
+
+export const RESET_BRANCHES = 'RESET_BRANCHES';
diff --git a/app/assets/javascripts/ide/stores/modules/branches/mutations.js b/app/assets/javascripts/ide/stores/modules/branches/mutations.js
new file mode 100644
index 00000000000..081ec2d4c28
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/branches/mutations.js
@@ -0,0 +1,21 @@
+/* eslint-disable no-param-reassign */
+import * as types from './mutation_types';
+
+export default {
+ [types.REQUEST_BRANCHES](state) {
+ state.isLoading = true;
+ },
+ [types.RECEIVE_BRANCHES_ERROR](state) {
+ state.isLoading = false;
+ },
+ [types.RECEIVE_BRANCHES_SUCCESS](state, data) {
+ state.isLoading = false;
+ state.branches = data.map(branch => ({
+ name: branch.name,
+ committedDate: branch.commit.committed_date,
+ }));
+ },
+ [types.RESET_BRANCHES](state) {
+ state.branches = [];
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/modules/branches/state.js b/app/assets/javascripts/ide/stores/modules/branches/state.js
new file mode 100644
index 00000000000..89bf220c45f
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/branches/state.js
@@ -0,0 +1,4 @@
+export default () => ({
+ isLoading: false,
+ branches: [],
+});
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
index 6ef938b0ae2..baa2497ec5b 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
@@ -1,12 +1,10 @@
import { __ } from '../../../../locale';
import Api from '../../../../api';
-import router from '../../../ide_router';
import { scopes } from './constants';
import * as types from './mutation_types';
-import * as rootTypes from '../../mutation_types';
-export const requestMergeRequests = ({ commit }, type) =>
- commit(types.REQUEST_MERGE_REQUESTS, type);
+export const requestMergeRequests = ({ commit }) =>
+ commit(types.REQUEST_MERGE_REQUESTS);
export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }) => {
dispatch(
'setErrorMessage',
@@ -21,39 +19,22 @@ export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }
},
{ root: true },
);
- commit(types.RECEIVE_MERGE_REQUESTS_ERROR, type);
+ commit(types.RECEIVE_MERGE_REQUESTS_ERROR);
};
-export const receiveMergeRequestsSuccess = ({ commit }, { type, data }) =>
- commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, { type, data });
+export const receiveMergeRequestsSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, data);
export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, search = '' }) => {
- const scope = scopes[type];
- dispatch('requestMergeRequests', type);
- dispatch('resetMergeRequests', type);
+ dispatch('requestMergeRequests');
+ dispatch('resetMergeRequests');
+
+ const scope = type ? scopes[type] : 'all';
return Api.mergeRequests({ scope, state, search })
- .then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data }))
+ .then(({ data }) => dispatch('receiveMergeRequestsSuccess', data))
.catch(() => dispatch('receiveMergeRequestsError', { type, search }));
};
-export const resetMergeRequests = ({ commit }, type) => commit(types.RESET_MERGE_REQUESTS, type);
-
-export const openMergeRequest = ({ commit, dispatch }, { projectPath, id }) => {
- commit(rootTypes.CLEAR_PROJECTS, null, { root: true });
- commit(rootTypes.SET_CURRENT_MERGE_REQUEST, `${id}`, { root: true });
- commit(rootTypes.RESET_OPEN_FILES, null, { root: true });
- dispatch('setCurrentBranchId', '', { root: true });
- dispatch('pipelines/stopPipelinePolling', null, { root: true })
- .then(() => {
- dispatch('pipelines/resetLatestPipeline', null, { root: true });
- dispatch('pipelines/clearEtagPoll', null, { root: true });
- })
- .catch(e => {
- throw e;
- });
- dispatch('setRightPane', null, { root: true });
-
- router.push(`/project/${projectPath}/merge_requests/${id}`);
-};
+export const resetMergeRequests = ({ commit }) => commit(types.RESET_MERGE_REQUESTS);
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js b/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js
deleted file mode 100644
index 8e2b234be8d..00000000000
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export const getData = state => type => state[type];
-
-export const assignedData = state => state.assigned;
-export const createdData = state => state.created;
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js
index 2e6dfb420f4..04e7e0f08f1 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js
@@ -1,6 +1,5 @@
import state from './state';
import * as actions from './actions';
-import * as getters from './getters';
import mutations from './mutations';
export default {
@@ -8,5 +7,4 @@ export default {
state: state(),
actions,
mutations,
- getters,
};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
index 971da0806bd..98102a68e08 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
@@ -2,15 +2,15 @@
import * as types from './mutation_types';
export default {
- [types.REQUEST_MERGE_REQUESTS](state, type) {
- state[type].isLoading = true;
+ [types.REQUEST_MERGE_REQUESTS](state) {
+ state.isLoading = true;
},
- [types.RECEIVE_MERGE_REQUESTS_ERROR](state, type) {
- state[type].isLoading = false;
+ [types.RECEIVE_MERGE_REQUESTS_ERROR](state) {
+ state.isLoading = false;
},
- [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, { type, data }) {
- state[type].isLoading = false;
- state[type].mergeRequests = data.map(mergeRequest => ({
+ [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, data) {
+ state.isLoading = false;
+ state.mergeRequests = data.map(mergeRequest => ({
id: mergeRequest.id,
iid: mergeRequest.iid,
title: mergeRequest.title,
@@ -20,7 +20,7 @@ export default {
.replace(`/merge_requests/${mergeRequest.iid}`, ''),
}));
},
- [types.RESET_MERGE_REQUESTS](state, type) {
- state[type].mergeRequests = [];
+ [types.RESET_MERGE_REQUESTS](state) {
+ state.mergeRequests = [];
},
};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js
index 57eb6b04283..4748ccfa2e6 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js
@@ -1,13 +1,7 @@
import { states } from './constants';
export default () => ({
- created: {
- isLoading: false,
- mergeRequests: [],
- },
- assigned: {
- isLoading: false,
- mergeRequests: [],
- },
+ isLoading: false,
+ mergeRequests: [],
state: states.opened,
});
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index d0bf847dbde..1eda5768709 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -115,13 +115,20 @@ export default {
},
[types.SET_EMPTY_STATE_SVGS](
state,
- { emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath, pipelinesEmptyStateSvgPath },
+ {
+ emptyStateSvgPath,
+ noChangesStateSvgPath,
+ committedStateSvgPath,
+ pipelinesEmptyStateSvgPath,
+ promotionSvgPath,
+ },
) {
Object.assign(state, {
emptyStateSvgPath,
noChangesStateSvgPath,
committedStateSvgPath,
pipelinesEmptyStateSvgPath,
+ promotionSvgPath,
});
},
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index c75add39bcd..a937fb157f8 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -44,7 +44,7 @@ export default {
rawPath: data.raw_path,
binary: data.binary,
renderError: data.render_error,
- raw: null,
+ raw: (state.entries[file.path] && state.entries[file.path].raw) || null,
baseRaw: null,
html: data.html,
size: data.size,
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 2371b201f8c..46b52fa00fc 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -31,4 +31,5 @@ export default () => ({
path: '',
entry: {},
},
+ clientsidePreviewEnabled: false,
});
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index 92b15cf232d..d895eca7af0 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -1,6 +1,5 @@
import { commitItemIconMap } from './constants';
-// eslint-disable-next-line import/prefer-default-export
export const getCommitIconMap = file => {
if (file.deleted) {
return commitItemIconMap.deleted;
@@ -10,3 +9,9 @@ export const getCommitIconMap = file => {
return commitItemIconMap.modified;
};
+
+export const createPathWithExt = p => {
+ const ext = p.lastIndexOf('.') >= 0 ? p.substring(p.lastIndexOf('.') + 1) : '';
+
+ return `${p.substring(1, p.lastIndexOf('.') + 1 || p.length)}${ext || '.js'}`;
+};
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index f9ff0722c01..0035d809062 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -36,6 +36,8 @@ class ImporterStatus {
const $targetField = $tr.find('.import-target');
const $namespaceInput = $targetField.find('.js-select-namespace option:selected');
const id = $tr.attr('id').replace('repo_', '');
+ const repoData = $tr.data();
+
let targetNamespace;
let newName;
if ($namespaceInput.length > 0) {
@@ -45,12 +47,20 @@ class ImporterStatus {
}
$btn.disable().addClass('is-loading');
- return axios.post(this.importUrl, {
+ this.id = id;
+
+ let attributes = {
repo_id: id,
target_namespace: targetNamespace,
new_name: newName,
ci_cd_only: this.ciCdOnly,
- })
+ };
+
+ if (repoData) {
+ attributes = Object.assign(repoData, attributes);
+ }
+
+ return axios.post(this.importUrl, attributes)
.then(({ data }) => {
const job = $(`tr#repo_${id}`);
job.attr('id', `project_${data.id}`);
@@ -70,6 +80,9 @@ class ImporterStatus {
.catch((error) => {
let details = error;
+ const $statusField = $(`#repo_${this.id} .job-status`);
+ $statusField.text(__('Failed'));
+
if (error.response && error.response.data && error.response.data.errors) {
details = error.response.data.errors;
}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 37a45d1d1a2..cb851ff6745 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -39,7 +39,7 @@ export default class LabelsSelect {
showNo = $dropdown.data('showNo');
showAny = $dropdown.data('showAny');
showMenuAbove = $dropdown.data('showMenuAbove');
- defaultLabel = $dropdown.data('defaultLabel');
+ defaultLabel = $dropdown.data('defaultLabel') || 'Label';
abilityName = $dropdown.data('abilityName');
$selectbox = $dropdown.closest('.selectbox');
$block = $selectbox.closest('.block');
@@ -244,21 +244,21 @@ export default class LabelsSelect {
var $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
var isSelected = el !== null ? el.hasClass('is-active') : false;
- var { title } = selected;
+ var title = selected ? selected.title : null;
var selectedLabels = this.selected;
if ($dropdownInputField.length && $dropdownInputField.val().length) {
$dropdownParent.find('.dropdown-input-clear').trigger('click');
}
- if (selected.id === 0) {
+ if (selected && selected.id === 0) {
this.selected = [];
return 'No Label';
}
else if (isSelected) {
this.selected.push(title);
}
- else {
+ else if (!isSelected && title) {
var index = this.selected.indexOf(title);
this.selected.splice(index, 1);
}
@@ -409,6 +409,14 @@ export default class LabelsSelect {
}
}
},
+ opened: function(e) {
+ if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ const previousSelection = $dropdown.attr('data-selected');
+ this.selected = previousSelection ? previousSelection.split(',') : [];
+ $dropdown.data('glDropdown').updateLabel();
+ }
+ },
+ preserveContext: true,
});
// Set dropdown data
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index 9482d131344..bd2212edec7 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -1,6 +1,7 @@
import _ from 'underscore';
-export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
+export const placeholderImage =
+ 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
const SCROLL_THRESHOLD = 300;
export default class LazyLoader {
@@ -18,11 +19,17 @@ export default class LazyLoader {
scrollContainer.addEventListener('load', () => this.loadCheck());
}
searchLazyImages() {
- this.lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
+ const that = this;
+ requestIdleCallback(
+ () => {
+ that.lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
- if (this.lazyImages.length) {
- this.checkElementsInView();
- }
+ if (that.lazyImages.length) {
+ that.checkElementsInView();
+ }
+ },
+ { timeout: 500 },
+ );
}
startContentObserver() {
const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
@@ -48,14 +55,16 @@ export default class LazyLoader {
const visHeight = scrollTop + window.innerHeight + SCROLL_THRESHOLD;
// Loading Images which are in the current viewport or close to them
- this.lazyImages = this.lazyImages.filter((selectedImage) => {
+ this.lazyImages = this.lazyImages.filter(selectedImage => {
if (selectedImage.getAttribute('data-src')) {
const imgBoundRect = selectedImage.getBoundingClientRect();
const imgTop = scrollTop + imgBoundRect.top;
const imgBound = imgTop + imgBoundRect.height;
if (scrollTop < imgBound && visHeight > imgTop) {
- LazyLoader.loadImage(selectedImage);
+ requestAnimationFrame(() => {
+ LazyLoader.loadImage(selectedImage);
+ });
return false;
}
@@ -66,7 +75,18 @@ export default class LazyLoader {
}
static loadImage(img) {
if (img.getAttribute('data-src')) {
- img.setAttribute('src', img.getAttribute('data-src'));
+ let imgUrl = img.getAttribute('data-src');
+ // Only adding width + height for avatars for now
+ if (imgUrl.indexOf('/avatar/') > -1 && imgUrl.indexOf('?') === -1) {
+ let targetWidth = null;
+ if (img.getAttribute('width')) {
+ targetWidth = img.getAttribute('width');
+ } else {
+ targetWidth = img.width;
+ }
+ if (targetWidth) imgUrl += `?width=${targetWidth}`;
+ }
+ img.setAttribute('src', imgUrl);
img.removeAttribute('data-src');
img.classList.remove('lazy');
img.classList.add('js-lazy-loaded');
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 17a6d5bcd2a..6afaefc56f8 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -147,6 +147,7 @@ export default {
}
this.showEmptyState = false;
})
+ .then(this.resize)
.catch(() => {
this.state = 'unableToConnect';
});
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 6385b75e557..ad6e7cf501d 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -5,19 +5,20 @@ import resolvedSvg from 'icons/_icon_status_success_solid.svg';
import mrIssueSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionSvg from 'icons/_next_discussion.svg';
import { pluralize } from '../../lib/utils/text_utility';
-import { scrollToElement } from '../../lib/utils/common_utils';
+import discussionNavigation from '../mixins/discussion_navigation';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
},
+ mixins: [discussionNavigation],
computed: {
...mapGetters([
'getUserData',
'getNoteableData',
'discussionCount',
- 'unresolvedDiscussions',
+ 'firstUnresolvedDiscussionId',
'resolvedDiscussionCount',
]),
isLoggedIn() {
@@ -35,11 +36,6 @@ export default {
resolveAllDiscussionsIssuePath() {
return this.getNoteableData.create_issue_to_resolve_discussions_path;
},
- firstUnresolvedDiscussionId() {
- const item = this.unresolvedDiscussions[0] || {};
-
- return item.id;
- },
},
created() {
this.resolveSvg = resolveSvg;
@@ -50,22 +46,10 @@ export default {
methods: {
...mapActions(['expandDiscussion']),
jumpToFirstUnresolvedDiscussion() {
- const discussionId = this.firstUnresolvedDiscussionId;
- if (!discussionId) {
- return;
- }
-
- const el = document.querySelector(`[data-discussion-id="${discussionId}"]`);
- const activeTab = window.mrTabs.currentAction;
-
- if (activeTab === 'commits' || activeTab === 'pipelines') {
- window.mrTabs.activateTab('show');
- }
+ const diffTab = window.mrTabs.currentAction === 'diffs';
+ const discussionId = this.firstUnresolvedDiscussionId(diffTab);
- if (el) {
- this.expandDiscussion({ discussionId });
- scrollToElement(el);
- }
+ this.jumpToDiscussion(discussionId);
},
},
};
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 26482a02e00..abcd4422d7c 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -7,7 +7,7 @@ import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable';
export default {
- name: 'IssueNoteForm',
+ name: 'NoteForm',
components: {
issueWarning,
markdownField,
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index bee635398b3..0fe1c16854a 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,11 +1,11 @@
<script>
-import _ from 'underscore';
import { mapActions, mapGetters } from 'vuex';
import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionsSvg from 'icons/_next_discussion.svg';
-import { convertObjectPropsToCamelCase, scrollToElement } from '~/lib/utils/common_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
import systemNote from '~/vue_shared/components/notes/system_note.vue';
+import { s__ } from '~/locale';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -20,6 +20,7 @@ import placeholderSystemNote from '../../vue_shared/components/notes/placeholder
import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
+import discussionNavigation from '../mixins/discussion_navigation';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
@@ -39,7 +40,7 @@ export default {
directives: {
tooltip,
},
- mixins: [autosave, noteable, resolvable],
+ mixins: [autosave, noteable, resolvable, discussionNavigation],
props: {
discussion: {
type: Object,
@@ -60,6 +61,11 @@ export default {
required: false,
default: false,
},
+ discussionsByDiffOrder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -74,7 +80,12 @@ export default {
'discussionCount',
'resolvedDiscussionCount',
'allDiscussions',
+ 'unresolvedDiscussionsIdsByDiff',
+ 'unresolvedDiscussionsIdsByDate',
'unresolvedDiscussions',
+ 'unresolvedDiscussionsIdsOrdered',
+ 'nextUnresolvedDiscussionId',
+ 'isLastUnresolvedDiscussion',
]),
transformedDiscussion() {
return {
@@ -125,6 +136,10 @@ export default {
hasMultipleUnresolvedDiscussions() {
return this.unresolvedDiscussions.length > 1;
},
+ showJumpToNextDiscussion() {
+ return this.hasMultipleUnresolvedDiscussions &&
+ !this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder);
+ },
shouldRenderDiffs() {
const { diffDiscussion, diffFile } = this.transformedDiscussion;
@@ -144,19 +159,17 @@ export default {
return this.isDiffDiscussion ? '' : 'card discussion-wrapper';
},
},
- mounted() {
- if (this.isReplying) {
- this.initAutoSave(this.transformedDiscussion);
- }
- },
- updated() {
- if (this.isReplying) {
- if (!this.autosave) {
- this.initAutoSave(this.transformedDiscussion);
+ watch: {
+ isReplying() {
+ if (this.isReplying) {
+ this.$nextTick(() => {
+ // Pass an extra key to separate reply and note edit forms
+ this.initAutoSave(this.transformedDiscussion, ['Reply']);
+ });
} else {
- this.setAutoSave();
+ this.disposeAutoSave();
}
- }
+ },
},
created() {
this.resolveDiscussionsSvg = resolveDiscussionsSvg;
@@ -194,16 +207,18 @@ export default {
showReplyForm() {
this.isReplying = true;
},
- cancelReplyForm(shouldConfirm) {
- if (shouldConfirm && this.$refs.noteForm.isDirty) {
+ cancelReplyForm(shouldConfirm, isDirty) {
+ if (shouldConfirm && isDirty) {
+ const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
+
// eslint-disable-next-line no-alert
- if (!window.confirm('Are you sure you want to cancel creating this comment?')) {
+ if (!window.confirm(msg)) {
return;
}
}
- this.resetAutoSave();
this.isReplying = false;
+ this.resetAutoSave();
},
saveReply(noteText, form, callback) {
const postData = {
@@ -241,21 +256,10 @@ Please check your network connection and try again.`;
});
},
jumpToNextDiscussion() {
- const discussionIds = this.allDiscussions.map(d => d.id);
- const unresolvedIds = this.unresolvedDiscussions.map(d => d.id);
- const currentIndex = discussionIds.indexOf(this.discussion.id);
- const remainingAfterCurrent = discussionIds.slice(currentIndex + 1);
- const nextIndex = _.findIndex(remainingAfterCurrent, id => unresolvedIds.indexOf(id) > -1);
-
- if (nextIndex > -1) {
- const nextId = remainingAfterCurrent[nextIndex];
- const el = document.querySelector(`[data-discussion-id="${nextId}"]`);
+ const nextId =
+ this.nextUnresolvedDiscussionId(this.discussion.id, this.discussionsByDiffOrder);
- if (el) {
- this.expandDiscussion({ discussionId: nextId });
- scrollToElement(el);
- }
- }
+ this.jumpToDiscussion(nextId);
},
},
};
@@ -397,7 +401,7 @@ Please check your network connection and try again.`;
</a>
</div>
<div
- v-if="hasMultipleUnresolvedDiscussions"
+ v-if="showJumpToNextDiscussion"
class="btn-group"
role="group">
<button
@@ -420,7 +424,8 @@ Please check your network connection and try again.`;
:is-editing="false"
save-button-title="Comment"
@handleFormUpdate="saveReply"
- @cancelForm="cancelReplyForm" />
+ @cancelForm="cancelReplyForm"
+ />
<note-signed-out-widget v-if="!canReply" />
</div>
</div>
diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
index 36cc8d5d056..4f45f912479 100644
--- a/app/assets/javascripts/notes/mixins/autosave.js
+++ b/app/assets/javascripts/notes/mixins/autosave.js
@@ -4,12 +4,18 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export default {
methods: {
- initAutoSave(noteable) {
- this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), [
+ initAutoSave(noteable, extraKeys = []) {
+ let keys = [
'Note',
- capitalizeFirstCharacter(noteable.noteable_type),
+ capitalizeFirstCharacter(noteable.noteable_type || noteable.noteableType),
noteable.id,
- ]);
+ ];
+
+ if (extraKeys) {
+ keys = keys.concat(extraKeys);
+ }
+
+ this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), keys);
},
resetAutoSave() {
this.autosave.reset();
@@ -17,5 +23,8 @@ export default {
setAutoSave() {
this.autosave.save();
},
+ disposeAutoSave() {
+ this.autosave.dispose();
+ },
},
};
diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js
new file mode 100644
index 00000000000..f7c4deee1f8
--- /dev/null
+++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js
@@ -0,0 +1,29 @@
+import { scrollToElement } from '~/lib/utils/common_utils';
+
+export default {
+ methods: {
+ jumpToDiscussion(id) {
+ if (id) {
+ const activeTab = window.mrTabs.currentAction;
+ const selector =
+ activeTab === 'diffs'
+ ? `ul.notes[data-discussion-id="${id}"]`
+ : `div.discussion[data-discussion-id="${id}"]`;
+ const el = document.querySelector(selector);
+
+ if (activeTab === 'commits' || activeTab === 'pipelines') {
+ window.mrTabs.activateTab('show');
+ }
+
+ if (el) {
+ this.expandDiscussion({ discussionId: id });
+
+ scrollToElement(el);
+ return true;
+ }
+ }
+
+ return false;
+ },
+ },
+};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 5c65e1c3bb5..5b3b9f8776f 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -82,6 +82,9 @@ export const allDiscussions = (state, getters) => {
return Object.values(resolved).concat(unresolved);
};
+export const allResolvableDiscussions = (state, getters) =>
+ getters.allDiscussions.filter(d => !d.individual_note && d.resolvable);
+
export const resolvedDiscussionsById = state => {
const map = {};
@@ -98,6 +101,51 @@ export const resolvedDiscussionsById = state => {
return map;
};
+// Gets Discussions IDs ordered by the date of their initial note
+export const unresolvedDiscussionsIdsByDate = (state, getters) =>
+ getters.allResolvableDiscussions
+ .filter(d => !d.resolved)
+ .sort((a, b) => {
+ const aDate = new Date(a.notes[0].created_at);
+ const bDate = new Date(b.notes[0].created_at);
+
+ if (aDate < bDate) {
+ return -1;
+ }
+
+ return aDate === bDate ? 0 : 1;
+ })
+ .map(d => d.id);
+
+// Gets Discussions IDs ordered by their position in the diff
+//
+// Sorts the array of resolvable yet unresolved discussions by
+// comparing file names first. If file names are the same, compares
+// line numbers.
+export const unresolvedDiscussionsIdsByDiff = (state, getters) =>
+ getters.allResolvableDiscussions
+ .filter(d => !d.resolved)
+ .sort((a, b) => {
+ if (!a.diff_file || !b.diff_file) {
+ return 0;
+ }
+
+ // Get file names comparison result
+ const filenameComparison = a.diff_file.file_path.localeCompare(b.diff_file.file_path);
+
+ // Get the line numbers, to compare within the same file
+ const aLines = [a.position.formatter.new_line, a.position.formatter.old_line];
+ const bLines = [b.position.formatter.new_line, b.position.formatter.old_line];
+
+ return filenameComparison < 0 ||
+ (filenameComparison === 0 &&
+ // .max() because one of them might be zero (if removed/added)
+ Math.max(aLines[0], aLines[1]) < Math.max(bLines[0], bLines[1]))
+ ? -1
+ : 1;
+ })
+ .map(d => d.id);
+
export const resolvedDiscussionCount = (state, getters) => {
const resolvedMap = getters.resolvedDiscussionsById;
@@ -114,5 +162,42 @@ export const discussionTabCounter = state => {
return all.length;
};
+// Returns the list of discussion IDs ordered according to given parameter
+// @param {Boolean} diffOrder - is ordered by diff?
+export const unresolvedDiscussionsIdsOrdered = (state, getters) => diffOrder => {
+ if (diffOrder) {
+ return getters.unresolvedDiscussionsIdsByDiff;
+ }
+ return getters.unresolvedDiscussionsIdsByDate;
+};
+
+// Checks if a given discussion is the last in the current order (diff or date)
+// @param {Boolean} discussionId - id of the discussion
+// @param {Boolean} diffOrder - is ordered by diff?
+export const isLastUnresolvedDiscussion = (state, getters) => (discussionId, diffOrder) => {
+ const idsOrdered = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
+ const lastDiscussionId = idsOrdered[idsOrdered.length - 1];
+
+ return lastDiscussionId === discussionId;
+};
+
+// Gets the ID of the discussion following the one provided, respecting order (diff or date)
+// @param {Boolean} discussionId - id of the current discussion
+// @param {Boolean} diffOrder - is ordered by diff?
+export const nextUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) => {
+ const idsOrdered = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
+ const currentIndex = idsOrdered.indexOf(discussionId);
+
+ return idsOrdered.slice(currentIndex + 1, currentIndex + 2)[0];
+};
+
+// @param {Boolean} diffOrder - is ordered by diff?
+export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => {
+ if (diffOrder) {
+ return getters.unresolvedDiscussionsIdsByDiff[0];
+ }
+ return getters.unresolvedDiscussionsIdsByDate[0];
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index ff19b9a9c30..9aa83ce6269 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -39,6 +39,7 @@ export default class Todos {
}
initFilters() {
+ this.initFilterDropdown($('.js-group-search'), 'group_id', ['text']);
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
this.initFilterDropdown($('.js-type-search'), 'type');
this.initFilterDropdown($('.js-action-search'), 'action_id');
@@ -53,7 +54,16 @@ export default class Todos {
filterable: searchFields ? true : false,
search: { fields: searchFields },
data: $dropdown.data('data'),
- clicked: () => $dropdown.closest('form.filter-form').submit(),
+ clicked: () => {
+ const $formEl = $dropdown.closest('form.filter-form');
+ const mutexDropdowns = {
+ group_id: 'project_id',
+ project_id: 'group_id',
+ };
+
+ $formEl.find(`input[name="${mutexDropdowns[fieldName]}"]`).remove();
+ $formEl.submit();
+ },
});
}
diff --git a/app/assets/javascripts/pages/profiles/show/emoji_menu.js b/app/assets/javascripts/pages/profiles/show/emoji_menu.js
new file mode 100644
index 00000000000..094837b40e0
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/show/emoji_menu.js
@@ -0,0 +1,18 @@
+import { AwardsHandler } from '~/awards_handler';
+
+class EmojiMenu extends AwardsHandler {
+ constructor(emoji, toggleButtonSelector, menuClass, selectEmojiCallback) {
+ super(emoji);
+
+ this.selectEmojiCallback = selectEmojiCallback;
+ this.toggleButtonSelector = toggleButtonSelector;
+ this.menuClass = menuClass;
+ }
+
+ postEmoji($emojiButton, awardUrl, selectedEmoji, callback) {
+ this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji));
+ callback();
+ }
+}
+
+export default EmojiMenu;
diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js
new file mode 100644
index 00000000000..949219a0837
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/show/index.js
@@ -0,0 +1,49 @@
+import $ from 'jquery';
+import createFlash from '~/flash';
+import GfmAutoComplete from '~/gfm_auto_complete';
+import EmojiMenu from './emoji_menu';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const toggleEmojiMenuButtonSelector = '.js-toggle-emoji-menu';
+ const toggleEmojiMenuButton = document.querySelector(toggleEmojiMenuButtonSelector);
+ const statusEmojiField = document.getElementById('js-status-emoji-field');
+ const statusMessageField = document.getElementById('js-status-message-field');
+ const findNoEmojiPlaceholder = () => document.getElementById('js-no-emoji-placeholder');
+
+ const removeStatusEmoji = () => {
+ const statusEmoji = toggleEmojiMenuButton.querySelector('gl-emoji');
+ if (statusEmoji) {
+ statusEmoji.remove();
+ }
+ };
+
+ const selectEmojiCallback = (emoji, emojiTag) => {
+ statusEmojiField.value = emoji;
+ findNoEmojiPlaceholder().classList.add('hidden');
+ removeStatusEmoji();
+ toggleEmojiMenuButton.innerHTML += emojiTag;
+ };
+
+ const clearEmojiButton = document.getElementById('js-clear-user-status-button');
+ clearEmojiButton.addEventListener('click', () => {
+ statusEmojiField.value = '';
+ statusMessageField.value = '';
+ removeStatusEmoji();
+ findNoEmojiPlaceholder().classList.remove('hidden');
+ });
+
+ const emojiAutocomplete = new GfmAutoComplete();
+ emojiAutocomplete.setup($(statusMessageField), { emojis: true });
+
+ import(/* webpackChunkName: 'emoji' */ '~/emoji')
+ .then(Emoji => {
+ const emojiMenu = new EmojiMenu(
+ Emoji,
+ toggleEmojiMenuButtonSelector,
+ 'js-status-emoji-menu',
+ selectEmojiCallback,
+ );
+ emojiMenu.bindEvents();
+ })
+ .catch(() => createFlash('Failed to load emoji list!'));
+});
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 1faa59fb45b..8f5ac3d8082 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -23,17 +23,12 @@ document.addEventListener('DOMContentLoaded', () => {
saveEndpoint: variableListEl.dataset.saveEndpoint,
});
- // hide extra auto devops settings based on data-attributes
- const autoDevOpsSettings = document.querySelector('.js-auto-devops-settings');
+ // hide extra auto devops settings based checkbox state
const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings');
-
- autoDevOpsSettings.addEventListener('click', event => {
+ const instanceDefaultBadge = document.querySelector('.js-instance-default-badge');
+ document.querySelector('.js-toggle-extra-settings').addEventListener('click', event => {
const { target } = event;
- if (target.classList.contains('js-toggle-extra-settings')) {
- autoDevOpsExtraSettings.classList.toggle(
- 'hidden',
- !!(target.dataset && target.dataset.hideExtraSettings),
- );
- }
+ if (instanceDefaultBadge) instanceDefaultBadge.style.display = 'none';
+ autoDevOpsExtraSettings.classList.toggle('hidden', !target.checked);
});
});
diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
index 5bc3c2c4d21..140475b4dfa 100644
--- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
+++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
@@ -69,7 +69,7 @@
return (
report.existing_failures.length > 0 ||
report.new_failures.length > 0 ||
- report.resolved_failures > 0
+ report.resolved_failures.length > 0
);
},
},
diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js
index e806d120b51..1983a8c9e56 100644
--- a/app/assets/javascripts/reports/store/mutations.js
+++ b/app/assets/javascripts/reports/store/mutations.js
@@ -9,6 +9,8 @@ export default {
state.isLoading = true;
},
[types.RECEIVE_REPORTS_SUCCESS](state, response) {
+ // Make sure to clean previous state in case it was an error
+ state.hasError = false;
state.isLoading = false;
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 72a2c7ca101..aec09b8bc0a 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -1,9 +1,18 @@
-/* eslint-disable no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, consistent-return, object-shorthand, prefer-template, quotes, class-methods-use-this, no-lonely-if, no-else-return, vars-on-top, max-len */
+/* eslint-disable no-return-assign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, object-shorthand, prefer-template, class-methods-use-this, no-lonely-if, vars-on-top, max-len */
import $ from 'jquery';
+import { escape, throttle } from 'underscore';
+import { s__, sprintf } from '~/locale';
+import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
import axios from './lib/utils/axios_utils';
import DropdownUtils from './filtered_search/dropdown_utils';
-import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils';
+import {
+ isInGroupsPage,
+ isInProjectPage,
+ getGroupSlug,
+ getProjectSlug,
+ spriteIcon,
+} from './lib/utils/common_utils';
/**
* Search input in top navigation bar.
@@ -52,6 +61,7 @@ function setSearchOptions() {
if ($dashboardOptionsDataEl.length) {
gl.dashboardOptions = {
+ name: s__('SearchAutocomplete|All GitLab'),
issuesPath: $dashboardOptionsDataEl.data('issuesPath'),
mrPath: $dashboardOptionsDataEl.data('mrPath'),
};
@@ -69,8 +79,8 @@ export default class SearchAutocomplete {
this.projectRef = projectRef || (this.optsEl.data('autocompleteProjectRef') || '');
this.dropdown = this.wrap.find('.dropdown');
this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle');
+ this.dropdownMenu = this.dropdown.find('.dropdown-menu');
this.dropdownContent = this.dropdown.find('.dropdown-content');
- this.locationBadgeEl = this.getElement('.location-badge');
this.scopeInputEl = this.getElement('#scope');
this.searchInput = this.getElement('.search-input');
this.projectInputEl = this.getElement('#search_project_id');
@@ -78,6 +88,7 @@ export default class SearchAutocomplete {
this.searchCodeInputEl = this.getElement('#search_code');
this.repositoryInputEl = this.getElement('#repository_ref');
this.clearInput = this.getElement('.js-clear-input');
+ this.scrollFadeInitialized = false;
this.saveOriginalState();
// Only when user is logged in
@@ -98,17 +109,18 @@ export default class SearchAutocomplete {
this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
+ this.setScrollFade = this.setScrollFade.bind(this);
}
getElement(selector) {
return this.wrap.find(selector);
}
saveOriginalState() {
- return this.originalState = this.serializeState();
+ return (this.originalState = this.serializeState());
}
saveTextLength() {
- return this.lastTextLength = this.searchInput.val().length;
+ return (this.lastTextLength = this.searchInput.val().length);
}
createAutocomplete() {
@@ -117,6 +129,7 @@ export default class SearchAutocomplete {
filterable: true,
filterRemote: true,
highlight: true,
+ icon: true,
enterCallback: false,
filterInput: 'input#search',
search: {
@@ -154,60 +167,87 @@ export default class SearchAutocomplete {
this.loadingSuggestions = true;
- return axios.get(this.autocompletePath, {
- params: {
- project_id: this.projectId,
- project_ref: this.projectRef,
- term: term,
- },
- }).then((response) => {
- // Hide dropdown menu if no suggestions returns
- if (!response.data.length) {
- this.disableAutocomplete();
- return;
- }
+ return axios
+ .get(this.autocompletePath, {
+ params: {
+ project_id: this.projectId,
+ project_ref: this.projectRef,
+ term: term,
+ },
+ })
+ .then(response => {
+ // Hide dropdown menu if no suggestions returns
+ if (!response.data.length) {
+ this.disableAutocomplete();
+ return;
+ }
- const data = [];
- // List results
- let firstCategory = true;
- let lastCategory;
- for (let i = 0, len = response.data.length; i < len; i += 1) {
- const suggestion = response.data[i];
- // Add group header before list each group
- if (lastCategory !== suggestion.category) {
- if (!firstCategory) {
- data.push('separator');
- }
- if (firstCategory) {
- firstCategory = false;
+ const data = [];
+ // List results
+ let firstCategory = true;
+ let lastCategory;
+ for (let i = 0, len = response.data.length; i < len; i += 1) {
+ const suggestion = response.data[i];
+ // Add group header before list each group
+ if (lastCategory !== suggestion.category) {
+ if (!firstCategory) {
+ data.push('separator');
+ }
+ if (firstCategory) {
+ firstCategory = false;
+ }
+ data.push({
+ header: suggestion.category,
+ });
+ lastCategory = suggestion.category;
}
data.push({
- header: suggestion.category,
+ id: `${suggestion.category.toLowerCase()}-${suggestion.id}`,
+ icon: this.getAvatar(suggestion),
+ category: suggestion.category,
+ text: suggestion.label,
+ url: suggestion.url,
});
- lastCategory = suggestion.category;
}
- data.push({
- id: `${suggestion.category.toLowerCase()}-${suggestion.id}`,
- category: suggestion.category,
- text: suggestion.label,
- url: suggestion.url,
- });
- }
- // Add option to proceed with the search
- if (data.length) {
- data.push('separator');
- data.push({
- text: `Result name contains "${term}"`,
- url: `/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
- });
- }
+ // Add option to proceed with the search
+ if (data.length) {
+ const icon = spriteIcon('search', 's16 inline-search-icon');
+ let template;
- callback(data);
+ if (this.projectInputEl.val()) {
+ template = s__('SearchAutocomplete|in this project');
+ }
+ if (this.groupInputEl.val()) {
+ template = s__('SearchAutocomplete|in this group');
+ }
- this.loadingSuggestions = false;
- }).catch(() => {
- this.loadingSuggestions = false;
- });
+ data.unshift('separator');
+ data.unshift({
+ icon,
+ text: term,
+ template: s__('SearchAutocomplete|in all GitLab'),
+ url: `/search?search=${term}`,
+ });
+
+ if (template) {
+ data.unshift({
+ icon,
+ text: term,
+ template,
+ url: `/search?search=${term}&project_id=${this.projectInputEl.val()}&group_id=${this.groupInputEl.val()}`,
+ });
+ }
+ }
+
+ callback(data);
+
+ this.loadingSuggestions = false;
+ this.highlightFirstRow();
+ this.setScrollFade();
+ })
+ .catch(() => {
+ this.loadingSuggestions = false;
+ });
}
getCategoryContents() {
@@ -236,21 +276,21 @@ export default class SearchAutocomplete {
const issueItems = [
{
- text: 'Issues assigned to me',
+ text: s__('SearchAutocomplete|Issues assigned to me'),
url: `${issuesPath}/?assignee_id=${userId}`,
},
{
- text: "Issues I've created",
+ text: s__("SearchAutocomplete|Issues I've created"),
url: `${issuesPath}/?author_id=${userId}`,
},
];
const mergeRequestItems = [
{
- text: 'Merge requests assigned to me',
+ text: s__('SearchAutocomplete|Merge requests assigned to me'),
url: `${mrPath}/?assignee_id=${userId}`,
},
{
- text: "Merge requests I've created",
+ text: s__("SearchAutocomplete|Merge requests I've created"),
url: `${mrPath}/?author_id=${userId}`,
},
];
@@ -259,7 +299,7 @@ export default class SearchAutocomplete {
if (issuesDisabled) {
items = baseItems.concat(mergeRequestItems);
} else {
- items = baseItems.concat(...issueItems, 'separator', ...mergeRequestItems);
+ items = baseItems.concat(...issueItems, ...mergeRequestItems);
}
return items;
}
@@ -272,8 +312,6 @@ export default class SearchAutocomplete {
search_code: this.searchCodeInputEl.val(),
repository_ref: this.repositoryInputEl.val(),
scope: this.scopeInputEl.val(),
- // Location badge
- _location: this.locationBadgeEl.text(),
};
}
@@ -283,10 +321,12 @@ export default class SearchAutocomplete {
this.searchInput.on('focus', this.onSearchInputFocus);
this.searchInput.on('blur', this.onSearchInputBlur);
this.clearInput.on('click', this.onClearInputClick);
- this.locationBadgeEl.on('click', () => this.searchInput.focus());
+ this.dropdownContent.on('scroll', throttle(this.setScrollFade, 250));
}
enableAutocomplete() {
+ this.setScrollFade();
+
// No need to enable anything if user is not logged in
if (!gon.current_user_id) {
return;
@@ -308,10 +348,6 @@ export default class SearchAutocomplete {
onSearchInputKeyUp(e) {
switch (e.keyCode) {
case KEYCODE.BACKSPACE:
- // when trying to remove the location badge
- if (this.lastTextLength === 0 && this.badgePresent()) {
- this.removeLocationBadge();
- }
// When removing the last character and no badge is present
if (this.lastTextLength === 1) {
this.disableAutocomplete();
@@ -372,37 +408,13 @@ export default class SearchAutocomplete {
}
}
- addLocationBadge(item) {
- var badgeText, category, value;
- category = item.category != null ? item.category + ": " : '';
- value = item.value != null ? item.value : '';
- badgeText = "" + category + value;
- this.locationBadgeEl.text(badgeText).show();
- return this.wrap.addClass('has-location-badge');
- }
-
- hasLocationBadge() {
- return this.wrap.is('.has-location-badge');
- }
-
restoreOriginalState() {
var i, input, inputs, len;
inputs = Object.keys(this.originalState);
for (i = 0, len = inputs.length; i < len; i += 1) {
input = inputs[i];
- this.getElement("#" + input).val(this.originalState[input]);
+ this.getElement('#' + input).val(this.originalState[input]);
}
- if (this.originalState._location === '') {
- return this.locationBadgeEl.hide();
- } else {
- return this.addLocationBadge({
- value: this.originalState._location,
- });
- }
- }
-
- badgePresent() {
- return this.locationBadgeEl.length;
}
resetSearchState() {
@@ -411,22 +423,11 @@ export default class SearchAutocomplete {
results = [];
for (i = 0, len = inputs.length; i < len; i += 1) {
input = inputs[i];
- // _location isnt a input
- if (input === '_location') {
- break;
- }
- results.push(this.getElement("#" + input).val(''));
+ results.push(this.getElement('#' + input).val(''));
}
return results;
}
- removeLocationBadge() {
- this.locationBadgeEl.hide();
- this.resetSearchState();
- this.wrap.removeClass('has-location-badge');
- return this.disableAutocomplete();
- }
-
disableAutocomplete() {
if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('show')) {
this.searchInput.addClass('disabled');
@@ -444,23 +445,57 @@ export default class SearchAutocomplete {
onClick(item, $el, e) {
if (window.location.pathname.indexOf(item.url) !== -1) {
if (!e.metaKey) e.preventDefault();
- if (!this.badgePresent) {
- if (item.category === 'Projects') {
- this.projectInputEl.val(item.id);
- this.addLocationBadge({
- value: 'This project',
- });
- }
- if (item.category === 'Groups') {
- this.groupInputEl.val(item.id);
- this.addLocationBadge({
- value: 'This group',
- });
- }
+ if (item.category === 'Projects') {
+ this.projectInputEl.val(item.id);
+ }
+ if (item.category === 'Groups') {
+ this.groupInputEl.val(item.id);
}
$el.removeClass('is-active');
this.disableAutocomplete();
return this.searchInput.val('').focus();
}
}
+
+ highlightFirstRow() {
+ this.searchInput.data('glDropdown').highlightRowAtIndex(null, 0);
+ }
+
+ getAvatar(item) {
+ if (!Object.hasOwnProperty.call(item, 'avatar_url')) {
+ return false;
+ }
+
+ const { label, id } = item;
+ const avatarUrl = item.avatar_url;
+ const avatar = avatarUrl
+ ? `<img class="search-item-avatar" src="${avatarUrl}" />`
+ : `<div class="s16 avatar identicon ${getIdenticonBackgroundClass(id)}">${getIdenticonTitle(
+ escape(label),
+ )}</div>`;
+
+ return avatar;
+ }
+
+ isScrolledUp() {
+ const el = this.dropdownContent[0];
+ const currentPosition = this.contentClientHeight + el.scrollTop;
+
+ return currentPosition < this.maxPosition;
+ }
+
+ initScrollFade() {
+ const el = this.dropdownContent[0];
+ this.scrollFadeInitialized = true;
+
+ this.contentClientHeight = el.clientHeight;
+ this.maxPosition = el.scrollHeight;
+ this.dropdownMenu.addClass('dropdown-content-faded-mask');
+ }
+
+ setScrollFade() {
+ this.initScrollFade();
+
+ this.dropdownMenu.toggleClass('fade-out', !this.isScrolledUp());
+ }
}
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
new file mode 100644
index 00000000000..ffaed9c7193
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -0,0 +1,98 @@
+<script>
+import { __ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+import Icon from '~/vue_shared/components/icon.vue';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+
+const MARK_TEXT = __('Mark todo as done');
+const TODO_TEXT = __('Add todo');
+
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ LoadingIcon,
+ },
+ props: {
+ issuableId: {
+ type: Number,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: true,
+ },
+ isTodo: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ isActionActive: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ collapsed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ buttonClasses() {
+ return this.collapsed ?
+ 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state' :
+ 'btn btn-default btn-todo issuable-header-btn float-right';
+ },
+ buttonLabel() {
+ return this.isTodo ? MARK_TEXT : TODO_TEXT;
+ },
+ collapsedButtonIconClasses() {
+ return this.isTodo ? 'todo-undone' : '';
+ },
+ collapsedButtonIcon() {
+ return this.isTodo ? 'todo-done' : 'todo-add';
+ },
+ },
+ methods: {
+ handleButtonClick() {
+ this.$emit('toggleTodo');
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ v-tooltip
+ :class="buttonClasses"
+ :title="buttonLabel"
+ :aria-label="buttonLabel"
+ :data-issuable-id="issuableId"
+ :data-issuable-type="issuableType"
+ type="button"
+ data-container="body"
+ data-placement="left"
+ data-boundary="viewport"
+ @click="handleButtonClick"
+ >
+ <icon
+ v-show="collapsed"
+ :css-classes="collapsedButtonIconClasses"
+ :name="collapsedButtonIcon"
+ />
+ <span
+ v-show="!collapsed"
+ class="issuable-todo-inner"
+ >
+ {{ buttonLabel }}
+ </span>
+ <loading-icon
+ v-show="isActionActive"
+ :inline="true"
+ />
+ </button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
index 133bdbb54f7..8163947cd0c 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
@@ -42,6 +42,9 @@ export default {
},
methods: {
onImgLoad() {
+ requestIdleCallback(this.calculateImgSize, { timeout: 1000 });
+ },
+ calculateImgSize() {
const { contentImg } = this.$refs;
if (contentImg) {
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
index 3cba0c5e633..af5ebcdc40a 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -38,9 +38,17 @@ export default {
v-show="isLoading"
:inline="true"
/>
- <span class="dropdown-toggle-text">
- {{ toggleText }}
- </span>
+ <template>
+ <slot
+ v-if="$slots.default"
+ ></slot>
+ <span
+ v-else
+ class="dropdown-toggle-text"
+ >
+ {{ toggleText }}
+ </span>
+ </template>
<span
v-show="!isLoading"
class="dropdown-toggle-icon"
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index e7ff76c8218..5e0e7315e99 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -1,7 +1,7 @@
<script>
// only allow classes in images.scss e.g. s12
-const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
+const validSizes = [8, 10, 12, 16, 18, 24, 32, 48, 72];
let iconValidator = () => true;
/*
@@ -75,6 +75,12 @@ export default {
required: false,
default: null,
},
+
+ tabIndex: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
@@ -98,6 +104,7 @@ export default {
:height="height"
:x="x"
:y="y"
+ :tabindex="tabIndex"
>
<use v-bind="{ 'xlink:href':spriteHref }"/>
</svg>
diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/default.vue b/app/assets/javascripts/vue_shared/components/project_avatar/default.vue
new file mode 100644
index 00000000000..17927fabbcc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/project_avatar/default.vue
@@ -0,0 +1,47 @@
+<script>
+import Identicon from '../identicon.vue';
+import ProjectAvatarImage from './image.vue';
+
+export default {
+ components: {
+ Identicon,
+ ProjectAvatarImage,
+ },
+ props: {
+ project: {
+ type: Object,
+ required: true,
+ },
+ size: {
+ type: Number,
+ default: 40,
+ },
+ },
+ computed: {
+ sizeClass() {
+ return `s${this.size}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <span
+ :class="sizeClass"
+ class="avatar-container project-avatar"
+ >
+ <project-avatar-image
+ v-if="project.avatar_url"
+ :link-href="project.path"
+ :img-src="project.avatar_url"
+ :img-alt="project.name"
+ :img-size="size"
+ />
+ <identicon
+ v-else
+ :entity-id="project.id"
+ :entity-name="project.name"
+ :size-class="sizeClass"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
index ac2e99abe77..80dc7d3557c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
@@ -12,6 +12,11 @@ export default {
type: Boolean,
required: true,
},
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
tooltipLabel() {
@@ -30,10 +35,12 @@ export default {
<button
v-tooltip
:title="tooltipLabel"
+ :class="cssClasses"
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
data-container="body"
data-placement="left"
+ data-boundary="viewport"
@click="toggle"
>
<i
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index 3a413c74410..7737b9f2697 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -1,5 +1,4 @@
<script>
-
/* This is a re-usable vue component for rendering a user avatar that
does not need to link to the user's profile. The image and an optional
tooltip can be configured by props passed to this component.
@@ -67,7 +66,9 @@ export default {
// we provide an empty string when we use it inside user avatar link.
// In both cases we should render the defaultAvatarUrl
sanitizedSource() {
- return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
+ let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
+ if (baseSrc.indexOf('?') === -1) baseSrc += `?width=${this.size}`;
+ return baseSrc;
},
resultantSrcAttribute() {
return this.lazy ? placeholderImage : this.sanitizedSource;
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index d28ad407734..c20738a20c3 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -339,3 +339,13 @@ input[type=color].form-control {
vertical-align: unset;
}
}
+
+// Bootstrap 3 compatibility because bootstrap_form Gem is not updated yet
+.input-group-btn:first-child {
+ @extend .input-group-prepend;
+}
+
+// Bootstrap 3 compatibility because bootstrap_form Gem is not updated yet
+.input-group-btn:last-child {
+ @extend .input-group-append;
+}
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index dddd07c798c..369556dc24e 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -78,6 +78,7 @@
&.s26 { font-size: 20px; line-height: 1.33; }
&.s32 { font-size: 20px; line-height: 30px; }
&.s40 { font-size: 16px; line-height: 38px; }
+ &.s48 { font-size: 20px; line-height: 46px; }
&.s60 { font-size: 32px; line-height: 58px; }
&.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 88px; }
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index c9865610b78..af17210f341 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -454,6 +454,7 @@ img.emoji {
.prepend-left-10 { margin-left: 10px; }
.prepend-left-default { margin-left: $gl-padding; }
.prepend-left-20 { margin-left: 20px; }
+.append-right-4 { margin-right: 4px; }
.append-right-5 { margin-right: 5px; }
.append-right-8 { margin-right: 8px; }
.append-right-10 { margin-right: 10px; }
@@ -470,3 +471,5 @@ img.emoji {
.center { text-align: center; }
.vertical-align-middle { vertical-align: middle; }
.flex-align-self-center { align-self: center; }
+.flex-grow { flex-grow: 1; }
+.flex-no-shrink { flex-shrink: 0; }
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index ea4cb9a0b75..e2bbcc67a67 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -55,6 +55,11 @@
.sidebar-context-title {
overflow: hidden;
text-overflow: ellipsis;
+
+ &.text-secondary {
+ font-weight: normal;
+ font-size: 0.8em;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index ec4a0f378d0..eebce8b9011 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -571,7 +571,8 @@
margin-bottom: 10px;
padding: 0 10px;
- .fa {
+ .fa,
+ .input-icon {
position: absolute;
top: 10px;
right: 20px;
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index dff6bce370f..50ebc6d0dd1 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -3,7 +3,6 @@
*/
@mixin gitlab-theme(
- $location-badge-color,
$search-and-nav-links,
$active-tab-border,
$border-and-box-shadow,
@@ -119,12 +118,6 @@
}
}
- .location-badge {
- color: $location-badge-color;
- background-color: rgba($search-and-nav-links, 0.1);
- border-right: 1px solid $sidebar-text;
- }
-
.search-input::placeholder {
color: rgba($search-and-nav-links, 0.8);
}
@@ -141,10 +134,6 @@
background-color: $white-light;
}
- .location-badge {
- color: $gl-text-color;
- }
-
.search-input-wrap {
.search-icon {
fill: rgba($search-and-nav-links, 0.8);
@@ -200,7 +189,6 @@
body {
&.ui-indigo {
@include gitlab-theme(
- $indigo-100,
$indigo-200,
$indigo-500,
$indigo-700,
@@ -212,7 +200,6 @@ body {
&.ui-light-indigo {
@include gitlab-theme(
- $indigo-100,
$indigo-200,
$indigo-500,
$indigo-500,
@@ -224,7 +211,6 @@ body {
&.ui-blue {
@include gitlab-theme(
- $theme-blue-100,
$theme-blue-200,
$theme-blue-500,
$theme-blue-700,
@@ -236,7 +222,6 @@ body {
&.ui-light-blue {
@include gitlab-theme(
- $theme-light-blue-100,
$theme-light-blue-200,
$theme-light-blue-500,
$theme-light-blue-500,
@@ -248,7 +233,6 @@ body {
&.ui-green {
@include gitlab-theme(
- $theme-green-100,
$theme-green-200,
$theme-green-500,
$theme-green-700,
@@ -260,7 +244,6 @@ body {
&.ui-light-green {
@include gitlab-theme(
- $theme-green-100,
$theme-green-200,
$theme-green-500,
$theme-green-500,
@@ -272,7 +255,6 @@ body {
&.ui-red {
@include gitlab-theme(
- $theme-red-100,
$theme-red-200,
$theme-red-500,
$theme-red-700,
@@ -284,7 +266,6 @@ body {
&.ui-light-red {
@include gitlab-theme(
- $theme-light-red-100,
$theme-light-red-200,
$theme-light-red-500,
$theme-light-red-500,
@@ -296,7 +277,6 @@ body {
&.ui-dark {
@include gitlab-theme(
- $theme-gray-100,
$theme-gray-200,
$theme-gray-500,
$theme-gray-700,
@@ -308,7 +288,6 @@ body {
&.ui-light {
@include gitlab-theme(
- $theme-gray-900,
$theme-gray-700,
$theme-gray-800,
$theme-gray-700,
@@ -357,10 +336,6 @@ body {
&:hover {
background-color: $white-light;
box-shadow: inset 0 0 0 1px $blue-200;
-
- .location-badge {
- box-shadow: inset 0 0 0 1px $blue-200;
- }
}
}
@@ -373,13 +348,6 @@ body {
color: $gl-text-color;
}
}
-
- .location-badge {
- color: $theme-gray-700;
- box-shadow: inset 0 0 0 1px $border-color;
- background-color: $nav-badge-bg;
- border-right: 0;
- }
}
.nav-sidebar li.active {
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index ab3cceceae9..f878ec1ca91 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -39,7 +39,7 @@
svg {
fill: currentColor;
- $svg-sizes: 8 12 16 18 24 32 48 72;
+ $svg-sizes: 8 10 12 16 18 24 32 48 72;
@each $svg-size in $svg-sizes {
&.s#{$svg-size} {
@include svg-size(#{$svg-size}px);
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 56940a7564a..4db9efff6ee 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -467,7 +467,8 @@ $award-emoji-positive-add-lines: #bb9c13;
*/
$search-input-border-color: rgba($blue-400, 0.8);
$search-input-focus-shadow-color: $dropdown-input-focus-shadow;
-$search-input-width: 220px;
+$search-input-width: 240px;
+$search-input-active-width: 320px;
$location-badge-active-bg: $blue-500;
$location-icon-color: #e7e9ed;
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 58ed5bf6455..2b8163b8c68 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -1,6 +1,13 @@
@import 'framework/variables';
@import 'framework/mixins';
+$search-list-icon-width: 18px;
+$ide-activity-bar-width: 60px;
+$ide-context-header-padding: 10px;
+$ide-project-avatar-end: $ide-context-header-padding + 48px;
+$ide-tree-padding: $gl-padding;
+$ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
+
.project-refs-form,
.project-refs-target-form {
display: inline-block;
@@ -24,7 +31,6 @@
display: flex;
height: calc(100vh - #{$header-height});
margin-top: 0;
- border-top: 1px solid $white-dark;
padding-bottom: $ide-statusbar-height;
color: $gl-text-color;
@@ -41,10 +47,10 @@
}
.ide-file-list {
+ display: flex;
+ flex-direction: column;
flex: 1;
- padding-left: $gl-padding;
- padding-right: $gl-padding;
- padding-bottom: $grid-size;
+ min-height: 0;
.file {
height: 32px;
@@ -517,35 +523,30 @@
> a,
> button {
- height: 60px;
+ text-decoration: none;
+ padding-top: $gl-padding-8;
+ padding-bottom: $gl-padding-8;
}
}
- .projects-sidebar {
- min-height: 0;
- display: flex;
- flex-direction: column;
- flex: 1;
- }
-
.multi-file-commit-panel-inner {
position: relative;
display: flex;
flex-direction: column;
- height: 100%;
+ min-height: 100%;
min-width: 0;
width: 100%;
}
- .multi-file-commit-panel-inner-scroll {
+ .multi-file-commit-panel-inner-content {
display: flex;
flex: 1;
flex-direction: column;
- overflow: auto;
background-color: $white-light;
border-left: 1px solid $white-dark;
border-top: 1px solid $white-dark;
border-top-left-radius: $border-radius-small;
+ min-height: 0;
}
}
@@ -803,12 +804,6 @@
height: calc(100vh - #{$header-height + $flash-height});
}
}
-
- .projects-sidebar {
- .multi-file-commit-panel-inner-scroll {
- flex: 1;
- }
- }
}
}
@@ -964,7 +959,7 @@
.ide-activity-bar {
position: relative;
- flex: 0 0 60px;
+ flex: 0 0 $ide-activity-bar-width;
z-index: 1;
}
@@ -1060,21 +1055,56 @@
}
.ide-tree-header {
+ flex: 0 0 auto;
display: flex;
align-items: center;
- margin-bottom: 8px;
+ flex-wrap: wrap;
padding: 12px 0;
+ margin-left: $ide-tree-padding;
+ margin-right: $ide-tree-padding;
border-bottom: 1px solid $white-dark;
.ide-new-btn {
margin-left: auto;
}
+ .ide-nav-dropdown {
+ width: 100%;
+ margin-bottom: 12px;
+
+ .dropdown-menu {
+ width: 385px;
+ max-height: initial;
+ }
+
+ .dropdown-menu-toggle {
+ svg {
+ vertical-align: middle;
+ }
+
+ &:hover {
+ background-color: $white-normal;
+ }
+ }
+
+ &.show {
+ .dropdown-menu-toggle {
+ background-color: $white-dark;
+ }
+ }
+ }
+
button {
color: $gl-text-color;
}
}
+.ide-tree-body {
+ overflow: auto;
+ padding-left: $ide-tree-padding;
+ padding-right: $ide-tree-padding;
+}
+
.ide-sidebar-branch-title {
font-weight: $gl-font-weight-normal;
@@ -1163,14 +1193,23 @@
}
.ide-context-header {
- .avatar {
- flex: 0 0 38px;
- }
-
.ide-merge-requests-dropdown.dropdown-menu {
width: 385px;
max-height: initial;
}
+
+ .avatar-container {
+ flex: initial;
+ margin-right: 0;
+ }
+
+ .ide-sidebar-project-title {
+ margin-left: $ide-tree-text-start - $ide-project-avatar-end;
+ }
+}
+
+.ide-context-body {
+ min-height: 0;
}
.ide-sidebar-project-title {
@@ -1178,10 +1217,11 @@
.sidebar-context-title {
white-space: nowrap;
- }
+ display: block;
- .ide-sidebar-branch-title {
- min-width: 50px;
+ &.text-secondary {
+ font-weight: normal;
+ }
}
}
@@ -1217,6 +1257,10 @@
background-color: $white-light;
border-left: 1px solid $white-dark;
}
+
+ .ide-right-sidebar-clientside {
+ padding: 0;
+ }
}
.ide-pipeline {
@@ -1315,7 +1359,7 @@
min-height: 60px;
}
-.ide-merge-requests-dropdown {
+.ide-nav-form {
.nav-links li {
width: 50%;
padding-left: 0;
@@ -1334,22 +1378,36 @@
padding-left: $gl-padding;
padding-right: $gl-padding;
- .fa {
- right: 26px;
+ .input-icon {
+ right: auto;
+ left: 10px;
+ top: 50%;
+ transform: translateY(-50%);
}
}
+ .dropdown-input-field {
+ padding-left: $search-list-icon-width + $gl-padding;
+ padding-top: 2px;
+ padding-bottom: 2px;
+ }
+
+ .tokens-container {
+ padding-left: $search-list-icon-width + $gl-padding;
+ overflow-x: hidden;
+ }
+
.btn-link {
padding-top: $gl-padding;
padding-bottom: $gl-padding;
}
}
-.ide-merge-request-current-icon {
- min-width: 18px;
+.ide-search-list-current-icon {
+ min-width: $search-list-icon-width;
}
-.ide-merge-requests-empty {
+.ide-search-list-empty {
height: 230px;
}
@@ -1400,3 +1458,40 @@
color: $white-normal;
background-color: $blue-500;
}
+
+.ide-preview-header {
+ padding: 0 $grid-size;
+ border-bottom: 1px solid $white-dark;
+ background-color: $gray-light;
+ min-height: 44px;
+}
+
+.ide-navigator-btn {
+ height: 24px;
+ min-width: 24px;
+ max-width: 24px;
+ padding: 0;
+ margin: 0 ($grid-size / 2);
+ color: $gl-gray-light;
+
+ &:first-child {
+ margin-left: 0;
+ }
+}
+
+.ide-navigator-location {
+ padding-top: ($grid-size / 2);
+ padding-bottom: ($grid-size / 2);
+
+ &:focus {
+ outline: 0;
+ box-shadow: none;
+ border-color: $theme-gray-200;
+ }
+}
+
+.ide-preview-loading-icon {
+ right: $grid-size;
+ top: 50%;
+ transform: translateY(-50%);
+}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 05bf5596fb3..1587aebfe1d 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -290,9 +290,8 @@
}
.folder-toggle-wrap {
- float: left;
- line-height: $list-text-height;
font-size: 0;
+ flex-shrink: 0;
span {
font-size: $gl-font-size;
@@ -308,7 +307,7 @@
width: 15px;
svg {
- margin-bottom: 2px;
+ margin-bottom: 1px;
}
}
@@ -391,9 +390,17 @@
cursor: pointer;
}
- .avatar-container > a {
- width: 100%;
- text-decoration: none;
+ .group-text {
+ min-width: 0; // allows for truncated text within flex children
+ }
+
+ .avatar-container {
+ flex-shrink: 0;
+
+ > a {
+ width: 100%;
+ text-decoration: none;
+ }
}
&.has-more-items {
@@ -401,9 +408,18 @@
padding: 20px 10px;
}
+ .description {
+ p {
+ @include str-truncated;
+
+ max-width: none;
+ }
+ }
+
.stats {
position: relative;
- line-height: 46px;
+ line-height: normal;
+ flex-shrink: 0;
> span {
display: inline-flex;
@@ -422,14 +438,20 @@
}
.controls {
- margin-left: 5px;
+ flex-shrink: 0;
> .btn {
- margin-right: $btn-margin-5;
+ margin: 0 0 0 $btn-margin-5;
}
}
}
+ @include media-breakpoint-down(xs) {
+ .group-stats {
+ display: none;
+ }
+ }
+
.project-row-contents .stats {
line-height: inherit;
@@ -451,18 +473,6 @@
}
}
-ul.group-list-tree {
- li.group-row {
- > .group-row-contents .title {
- line-height: $list-text-height;
- }
-
- &.has-description > .group-row-contents .title {
- line-height: inherit;
- }
- }
-}
-
.js-groups-list-holder {
.groups-list-loading {
font-size: 34px;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index d5ae2b673d9..8e78d9f65eb 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -449,6 +449,7 @@
.todo-undone {
color: $gl-link-color;
+ fill: $gl-link-color;
}
.author {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 391dfea0703..2b40404971c 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -72,6 +72,9 @@
}
.manage-labels-list {
+ padding: 0;
+ margin-bottom: 0;
+
> li:not(.empty-message):not(.is-not-draggable) {
background-color: $white-light;
margin-bottom: 5px;
@@ -81,6 +84,10 @@
border-radius: $border-radius-default;
border: 1px solid $theme-gray-100;
+ &:last-child {
+ margin-bottom: 0;
+ }
+
&.sortable-ghost {
opacity: 0.3;
}
@@ -243,7 +250,10 @@
.label-actions-list {
list-style: none;
flex-shrink: 0;
+ text-align: right;
padding: 0;
+ position: relative;
+ top: -3px;
}
.label-badge {
@@ -272,6 +282,16 @@
padding: 0;
}
+.label-description {
+ .description-text {
+ margin-bottom: 10px;
+
+ .admin-labels & {
+ margin-bottom: 0;
+ }
+ }
+}
+
.label-list-item {
.content-list &::before,
.content-list &::after {
@@ -319,6 +339,64 @@
fill: $blue-600;
}
}
+
+ &.remove-row {
+ &:hover {
+ color: $gl-text-red;
+
+ svg {
+ fill: $gl-text-red;
+ }
+ }
+ }
+ }
+}
+
+@media (max-width: map-get($grid-breakpoints, md)-1) {
+ .manage-labels-list {
+ > li:not(.empty-message):not(.is-not-draggable) {
+ flex-wrap: wrap;
+ }
+
+ .label-name {
+ order: 1;
+ flex-grow: 1;
+ width: auto;
+ max-width: 100%;
+ }
+
+ .label-actions-list {
+ order: 2;
+ flex-shrink: 1;
+ text-align: left;
+ }
+
+ .label-links {
+ white-space: normal;
+ }
+
+ .label-description {
+ order: 3;
+ width: 100%;
+
+ > .append-right-default.prepend-left-default {
+ margin-left: 0;
+ margin-right: 0;
+ }
+ }
+ }
+}
+
+@media (max-width: 910px) {
+ .priority-badge {
+ display: block;
+ width: 100%;
+ margin-left: 0;
+ margin-top: $gl-padding;
+
+ .label-badge {
+ display: inline-block;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 7fc2936c5e6..c369d89d63c 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -546,6 +546,7 @@ ul.notes {
svg {
@include btn-svg;
+ margin: 0;
}
.award-control-icon-positive,
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 5d0d59e12f2..b45e305897c 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -418,3 +418,23 @@ table.u2f-registrations {
}
}
}
+
+.edit-user {
+ .clear-user-status {
+ svg {
+ fill: $gl-text-color-secondary;
+ }
+ }
+
+ .emoji-menu-toggle-button {
+ @extend .note-action-button;
+
+ .no-emoji-placeholder {
+ position: relative;
+
+ svg {
+ fill: $gl-text-color-secondary;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 2d66f336076..60b280fd12e 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -1,3 +1,6 @@
+$search-dropdown-max-height: 400px;
+$search-avatar-size: 16px;
+
.search-results {
.search-result-row {
border-bottom: 1px solid $border-color;
@@ -24,8 +27,9 @@
box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
}
-input[type="checkbox"]:hover {
- box-shadow: 0 0 2px 2px lighten($search-input-focus-shadow-color, 20%), 0 0 0 1px lighten($search-input-focus-shadow-color, 20%);
+input[type='checkbox']:hover {
+ box-shadow: 0 0 2px 2px lighten($search-input-focus-shadow-color, 20%),
+ 0 0 0 1px lighten($search-input-focus-shadow-color, 20%);
}
.search {
@@ -40,24 +44,15 @@ input[type="checkbox"]:hover {
height: 32px;
border: 0;
border-radius: $border-radius-default;
- transition: border-color ease-in-out $default-transition-duration, background-color ease-in-out $default-transition-duration;
+ transition: border-color ease-in-out $default-transition-duration,
+ background-color ease-in-out $default-transition-duration,
+ width ease-in-out $default-transition-duration;
&:hover {
box-shadow: none;
}
}
- .location-badge {
- white-space: nowrap;
- height: 32px;
- font-size: 12px;
- margin: -4px 4px -4px -4px;
- line-height: 25px;
- padding: 4px 8px;
- border-radius: $border-radius-default 0 0 $border-radius-default;
- transition: border-color ease-in-out $default-transition-duration;
- }
-
.search-input {
border: 0;
font-size: 14px;
@@ -104,17 +99,28 @@ input[type="checkbox"]:hover {
}
.dropdown-header {
- text-transform: uppercase;
- font-size: 11px;
+ // Necessary because glDropdown doesn't support a second style of headers
+ font-weight: $gl-font-weight-bold;
+ // .dropdown-menu li has 1px side padding
+ padding: $gl-padding-8 17px;
+ color: $gl-text-color;
+ font-size: $gl-font-size;
+ line-height: 16px;
}
// Custom dropdown positioning
.dropdown-menu {
left: -5px;
+ max-height: $search-dropdown-max-height;
+ overflow: auto;
+
+ @include media-breakpoint-up(xl) {
+ width: $search-input-active-width;
+ }
}
.dropdown-content {
- max-height: none;
+ max-height: $search-dropdown-max-height - 18px;
}
}
@@ -124,6 +130,10 @@ input[type="checkbox"]:hover {
border-color: $dropdown-input-focus-border;
box-shadow: none;
+ @include media-breakpoint-up(xl) {
+ width: $search-input-active-width;
+ }
+
.search-input-wrap {
.search-icon,
.clear-icon {
@@ -141,12 +151,6 @@ input[type="checkbox"]:hover {
color: $gl-text-color-tertiary;
}
}
-
- .location-badge {
- transition: all $default-transition-duration;
- background-color: $nav-badge-bg;
- border-color: $border-color;
- }
}
&.has-value {
@@ -160,10 +164,24 @@ input[type="checkbox"]:hover {
}
}
- &.has-location-badge {
- .search-input-wrap {
- width: 68%;
- }
+ .inline-search-icon {
+ position: relative;
+ margin-right: 4px;
+ color: $gl-text-color-secondary;
+ }
+
+ .identicon,
+ .search-item-avatar {
+ flex-basis: $search-avatar-size;
+ flex-shrink: 0;
+ margin-right: 4px;
+ }
+
+ .search-item-avatar {
+ width: $search-avatar-size;
+ height: $search-avatar-size;
+ border-radius: 50%;
+ border: 1px solid $avatar-border;
}
}
diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss
index 777fdb3581e..239123fc3ab 100644
--- a/app/assets/stylesheets/pages/settings_ci_cd.scss
+++ b/app/assets/stylesheets/pages/settings_ci_cd.scss
@@ -19,9 +19,4 @@
.auto-devops-card {
margin-bottom: $gl-vert-padding;
-
- > .card-body {
- border-radius: $card-border-radius;
- padding: $gl-padding $gl-padding-24;
- }
}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index e5d7dd13915..010a2c05a1c 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -174,6 +174,18 @@
}
}
+@include media-breakpoint-down(lg) {
+ .todos-filters {
+ .filter-categories {
+ width: 75%;
+
+ .filter-item {
+ margin-bottom: 10px;
+ }
+ }
+ }
+}
+
@include media-breakpoint-down(xs) {
.todo {
.avatar {
@@ -199,6 +211,10 @@
}
.todos-filters {
+ .filter-categories {
+ width: auto;
+ }
+
.dropdown-menu-toggle {
width: 100%;
}