summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/boards/models/list.js2
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js33
-rw-r--r--app/assets/javascripts/due_date_select.js9
-rw-r--r--app/assets/javascripts/ide/components/changed_file_icon.vue81
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue58
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue93
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue153
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue121
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue60
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/message_field.vue130
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue88
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue59
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue45
-rw-r--r--app/assets/javascripts/ide/components/file_finder/index.vue245
-rw-r--r--app/assets/javascripts/ide/components/file_finder/item.vue113
-rw-r--r--app/assets/javascripts/ide/components/ide.vue114
-rw-r--r--app/assets/javascripts/ide/components/ide_context_bar.vue42
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue7
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue90
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue11
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue123
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue9
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue8
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue14
-rw-r--r--app/assets/javascripts/ide/constants.js8
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js37
-rw-r--r--app/assets/javascripts/ide/lib/common/model_manager.js4
-rw-r--r--app/assets/javascripts/ide/lib/decorations/controller.js9
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js25
-rw-r--r--app/assets/javascripts/ide/lib/editor.js37
-rw-r--r--app/assets/javascripts/ide/lib/keymap.json11
-rw-r--r--app/assets/javascripts/ide/stores/actions.js42
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js40
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js100
-rw-r--r--app/assets/javascripts/ide/stores/getters.js28
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js37
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js14
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js8
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js37
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js71
-rw-r--r--app/assets/javascripts/ide/stores/state.js2
-rw-r--r--app/assets/javascripts/ide/stores/utils.js11
-rw-r--r--app/assets/javascripts/ide/stores/workers/files_decorator_worker.js8
-rw-r--r--app/assets/javascripts/issuable_context.js4
-rw-r--r--app/assets/javascripts/jobs/components/header.vue150
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue185
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js5
-rw-r--r--app/assets/javascripts/labels_select.js5
-rw-r--r--app/assets/javascripts/lib/utils/keycodes.js4
-rw-r--r--app/assets/javascripts/milestone_select.js83
-rw-r--r--app/assets/javascripts/notes.js2
-rw-r--r--app/assets/javascripts/notes/stores/actions.js3
-rw-r--r--app/assets/javascripts/notes/stores/getters.js3
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue5
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js205
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue5
-rw-r--r--app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue20
-rw-r--r--app/assets/javascripts/performance_bar/index.js2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue25
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue53
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue128
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue28
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue7
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js17
-rw-r--r--app/assets/javascripts/registry/stores/actions.js3
-rw-r--r--app/assets/javascripts/registry/stores/getters.js3
-rw-r--r--app/assets/javascripts/right_sidebar.js12
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue20
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue12
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue19
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue21
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue18
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue32
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js3
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js1
-rw-r--r--app/assets/javascripts/users_select.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue102
-rw-r--r--app/assets/javascripts/vue_shared/components/callout.vue27
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue88
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue75
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue76
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue215
-rw-r--r--app/assets/javascripts/vue_shared/components/expand_button.vue54
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue101
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue132
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue118
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue39
-rw-r--r--app/assets/javascripts/vue_shared/models/label.js2
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss30
-rw-r--r--app/assets/stylesheets/framework/mobile.scss4
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss4
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss9
-rw-r--r--app/assets/stylesheets/framework/variables.scss3
-rw-r--r--app/assets/stylesheets/framework/wells.scss1
-rw-r--r--app/assets/stylesheets/pages/builds.scss41
-rw-r--r--app/assets/stylesheets/pages/commits.scss6
-rw-r--r--app/assets/stylesheets/pages/issuable.scss28
-rw-r--r--app/assets/stylesheets/pages/labels.scss4
-rw-r--r--app/assets/stylesheets/pages/milestone.scss14
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss8
-rw-r--r--app/assets/stylesheets/pages/repo.scss210
-rw-r--r--app/assets/stylesheets/performance_bar.scss1
106 files changed, 3183 insertions, 1564 deletions
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index e210d69895e..7144f4190e7 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -113,6 +113,8 @@ class List {
issue.id = data.id;
issue.iid = data.iid;
issue.project = data.project;
+ issue.path = data.real_path;
+ issue.referencePath = data.reference_path;
if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id;
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index fb1fc9cd32e..a88b6971f90 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown {
if (data.can_create_branch) {
this.available();
this.enable();
+ this.updateBranchName(data.suggested_branch_name);
if (!this.droplabInitialized) {
this.droplabInitialized = true;
this.initDroplab();
this.bindEvents();
}
- } else if (data.has_related_branch) {
+ } else {
this.hide();
}
})
.catch(() => {
this.unavailable();
this.disable();
- Flash('Failed to check if a new branch can be created.');
+ Flash(__('Failed to check related branches.'));
});
}
@@ -409,13 +410,16 @@ export default class CreateMergeRequestDropdown {
this.unavailableButton.classList.remove('hide');
}
+ updateBranchName(suggestedBranchName) {
+ this.branchInput.value = suggestedBranchName;
+ this.updateCreatePaths('branch', suggestedBranchName);
+ }
+
updateInputState(target, ref, result) {
// target - 'branch' or 'ref' - which the input field we are searching a ref for.
// ref - string - what a user typed.
// result - string - what has been found on backend.
- const pathReplacement = `$1${ref}`;
-
// If a found branch equals exact the same text a user typed,
// that means a new branch cannot be created as it already exists.
if (ref === result) {
@@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown {
this.refIsValid = true;
this.refInput.dataset.value = ref;
this.showAvailableMessage('ref');
- this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath,
- pathReplacement);
- this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
- pathReplacement);
+ this.updateCreatePaths(target, ref);
}
} else if (target === 'branch') {
this.branchIsValid = true;
this.showAvailableMessage('branch');
- this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath,
- pathReplacement);
- this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
- pathReplacement);
+ this.updateCreatePaths(target, ref);
} else {
this.refIsValid = false;
this.refInput.dataset.value = ref;
@@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown {
this.disableCreateAction();
}
}
+
+ // target - 'branch' or 'ref'
+ // ref - string - the new value to use as branch or ref
+ updateCreatePaths(target, ref) {
+ const pathReplacement = `$1${ref}`;
+
+ this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath,
+ pathReplacement);
+ this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath,
+ pathReplacement);
+ }
}
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 842a4255f08..4164149dd06 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -2,7 +2,9 @@
import $ from 'jquery';
import Pikaday from 'pikaday';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
+import { timeFor } from './lib/utils/datetime_utility';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
class DueDateSelect {
@@ -14,6 +16,7 @@ class DueDateSelect {
this.$dropdownParent = $dropdownParent;
this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
this.$block = $block;
+ this.$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
this.$selectbox = $dropdown.closest('.selectbox');
this.$value = $block.find('.value');
this.$valueContent = $block.find('.value-content');
@@ -128,7 +131,8 @@ class DueDateSelect {
submitSelectedDate(isDropdown) {
const selectedDateValue = this.datePayload[this.abilityName].due_date;
- const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
+ const hasDueDate = this.displayedDate !== 'No due date';
+ const displayedDateStyle = hasDueDate ? 'bold' : 'no-value';
this.$loading.removeClass('hidden').fadeIn();
@@ -145,10 +149,13 @@ class DueDateSelect {
return axios.put(this.issueUpdateURL, this.datePayload)
.then(() => {
+ const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date');
if (isDropdown) {
this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.dropdown('toggle');
}
+ this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
+
return this.$loading.fadeOut();
});
}
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index 037e3efb4ce..fdbc14a4b8f 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -1,31 +1,94 @@
<script>
-import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+import { pluralize } from '~/lib/utils/text_utility';
+import { __, sprintf } from '~/locale';
export default {
components: {
- icon,
+ Icon,
+ },
+ directives: {
+ tooltip,
},
props: {
file: {
type: Object,
required: true,
},
+ showTooltip: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showStagedIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ forceModifiedIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
changedIcon() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
+ const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : '';
+ return this.file.tempFile && !this.forceModifiedIcon
+ ? `file-addition${suffix}`
+ : `file-modified${suffix}`;
+ },
+ stagedIcon() {
+ return `${this.changedIcon}-solid`;
},
changedIconClass() {
- return `multi-${this.changedIcon}`;
+ return `multi-${this.changedIcon} prepend-left-5 pull-left`;
+ },
+ tooltipTitle() {
+ if (!this.showTooltip) return undefined;
+
+ const type = this.file.tempFile ? 'addition' : 'modification';
+
+ if (this.file.changed && !this.file.staged) {
+ return sprintf(__('Unstaged %{type}'), {
+ type,
+ });
+ } else if (!this.file.changed && this.file.staged) {
+ return sprintf(__('Staged %{type}'), {
+ type,
+ });
+ } else if (this.file.changed && this.file.staged) {
+ return sprintf(__('Unstaged and staged %{type}'), {
+ type: pluralize(type),
+ });
+ }
+
+ return undefined;
},
},
};
</script>
<template>
- <icon
- :name="changedIcon"
- :size="12"
- :css-classes="`ide-file-changed-icon ${changedIconClass}`"
- />
+ <span
+ v-tooltip
+ :title="tooltipTitle"
+ data-container="body"
+ data-placement="right"
+ class="ide-file-changed-icon"
+ >
+ <icon
+ v-if="file.staged && showStagedIcon"
+ :name="stagedIcon"
+ :size="12"
+ :css-classes="changedIconClass"
+ />
+ <icon
+ v-if="file.changed || file.tempFile || (file.staged && !showStagedIcon)"
+ :name="changedIcon"
+ :size="12"
+ :css-classes="changedIconClass"
+ />
+ </span>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 2cbd982af19..45321df191c 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,41 +1,27 @@
<script>
- import { mapState } from 'vuex';
- import { sprintf, __ } from '~/locale';
- import * as consts from '../../stores/modules/commit/constants';
- import RadioGroup from './radio_group.vue';
+import { mapState } from 'vuex';
+import { sprintf, __ } from '~/locale';
+import * as consts from '../../stores/modules/commit/constants';
+import RadioGroup from './radio_group.vue';
- export default {
- components: {
- RadioGroup,
+export default {
+ components: {
+ RadioGroup,
+ },
+ computed: {
+ ...mapState(['currentBranchId']),
+ commitToCurrentBranchText() {
+ return sprintf(
+ __('Commit to %{branchName} branch'),
+ { branchName: `<strong class="monospace">${this.currentBranchId}</strong>` },
+ false,
+ );
},
- computed: {
- ...mapState([
- 'currentBranchId',
- ]),
- newMergeRequestHelpText() {
- return sprintf(
- __('Creates a new branch from %{branchName} and re-directs to create a new merge request'),
- { branchName: this.currentBranchId },
- );
- },
- commitToCurrentBranchText() {
- return sprintf(
- __('Commit to %{branchName} branch'),
- { branchName: `<strong>${this.currentBranchId}</strong>` },
- false,
- );
- },
- commitToNewBranchText() {
- return sprintf(
- __('Creates a new branch from %{branchName}'),
- { branchName: this.currentBranchId },
- );
- },
- },
- commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
- commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
- commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
- };
+ },
+ commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
+ commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
+ commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
+};
</script>
<template>
@@ -53,13 +39,11 @@
:value="$options.commitToNewBranch"
:label="__('Create a new branch')"
:show-input="true"
- :help-text="commitToNewBranchText"
/>
<radio-group
:value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')"
:show-input="true"
- :help-text="newMergeRequestHelpText"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
new file mode 100644
index 00000000000..6424b93ce54
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
@@ -0,0 +1,93 @@
+<script>
+import { mapActions, mapState, mapGetters } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ noChangesStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ committedStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['lastCommitMsg', 'rightPanelCollapsed']),
+ ...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
+ statusSvg() {
+ return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
+ },
+ },
+ methods: {
+ ...mapActions(['toggleRightPanelCollapsed']),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
+ >
+ <header
+ class="multi-file-commit-panel-header"
+ :class="{
+ 'is-collapsed': rightPanelCollapsed,
+ }"
+ >
+ <button
+ v-tooltip
+ :title="collapseButtonTooltip"
+ data-container="body"
+ data-placement="left"
+ type="button"
+ class="btn btn-transparent multi-file-commit-panel-collapse-btn"
+ :aria-label="__('Toggle sidebar')"
+ @click.stop="toggleRightPanelCollapsed"
+ >
+ <icon
+ :name="collapseButtonIcon"
+ :size="18"
+ />
+ </button>
+ </header>
+ <div
+ class="ide-commit-empty-state-container"
+ v-if="!rightPanelCollapsed"
+ >
+ <div class="svg-content svg-80">
+ <img :src="statusSvg" />
+ </div>
+ <div class="append-right-default prepend-left-default">
+ <div
+ class="text-content text-center"
+ v-if="!lastCommitMsg"
+ >
+ <h4>
+ {{ __('No changes') }}
+ </h4>
+ <p>
+ {{ __('Edit files in the editor and commit changes here') }}
+ </p>
+ </div>
+ <div
+ class="text-content text-center"
+ v-else
+ >
+ <h4>
+ {{ __('All changes are committed') }}
+ </h4>
+ <p v-html="lastCommitMsg"></p>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index 453208f3f19..ff05ee8682a 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -1,56 +1,132 @@
<script>
- import { mapState } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
- import listItem from './list_item.vue';
- import listCollapsed from './list_collapsed.vue';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import { __, sprintf } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import ListItem from './list_item.vue';
+import ListCollapsed from './list_collapsed.vue';
- export default {
- components: {
- icon,
- listItem,
- listCollapsed,
+export default {
+ components: {
+ Icon,
+ ListItem,
+ ListCollapsed,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
},
- props: {
- title: {
- type: String,
- required: true,
- },
- fileList: {
- type: Array,
- required: true,
- },
+ fileList: {
+ type: Array,
+ required: true,
},
- computed: {
- ...mapState([
- 'currentProjectId',
- 'currentBranchId',
- 'rightPanelCollapsed',
- ]),
- isCommitInfoShown() {
- return this.rightPanelCollapsed || this.fileList.length;
- },
+ showToggle: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- methods: {
- toggleCollapsed() {
- this.$emit('toggleCollapsed');
- },
+ iconName: {
+ type: String,
+ required: true,
},
- };
+ action: {
+ type: String,
+ required: true,
+ },
+ actionBtnText: {
+ type: String,
+ required: true,
+ },
+ itemActionComponent: {
+ type: String,
+ required: true,
+ },
+ stagedList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapState(['rightPanelCollapsed']),
+ ...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
+ titleText() {
+ return sprintf(__('%{title} changes'), {
+ title: this.title,
+ });
+ },
+ },
+ methods: {
+ ...mapActions(['toggleRightPanelCollapsed', 'stageAllChanges', 'unstageAllChanges']),
+ actionBtnClicked() {
+ this[this.action]();
+ },
+ },
+};
</script>
<template>
<div
+ class="ide-commit-list-container"
:class="{
- 'multi-file-commit-list': isCommitInfoShown
+ 'is-collapsed': rightPanelCollapsed,
}"
>
+ <header
+ class="multi-file-commit-panel-header"
+ >
+ <div
+ v-if="!rightPanelCollapsed"
+ class="multi-file-commit-panel-header-title"
+ :class="{
+ 'append-right-10': showToggle,
+ }"
+ >
+ <icon
+ v-once
+ :name="iconName"
+ :size="18"
+ />
+ {{ titleText }}
+ <button
+ type="button"
+ class="btn btn-blank btn-link ide-staged-action-btn"
+ @click="actionBtnClicked"
+ >
+ {{ actionBtnText }}
+ </button>
+ </div>
+ <button
+ v-if="showToggle"
+ v-tooltip
+ :title="collapseButtonTooltip"
+ data-container="body"
+ data-placement="left"
+ type="button"
+ class="btn btn-transparent multi-file-commit-panel-collapse-btn"
+ :aria-label="__('Toggle sidebar')"
+ @click.stop="toggleRightPanelCollapsed"
+ >
+ <icon
+ :name="collapseButtonIcon"
+ :size="18"
+ />
+ </button>
+ </header>
<list-collapsed
v-if="rightPanelCollapsed"
+ :files="fileList"
+ :icon-name="iconName"
+ :title="title"
/>
<template v-else>
<ul
v-if="fileList.length"
- class="list-unstyled append-bottom-0"
+ class="multi-file-commit-list list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
@@ -58,9 +134,18 @@
>
<list-item
:file="file"
+ :action-component="itemActionComponent"
+ :key-prefix="title"
+ :staged-list="stagedList"
/>
</li>
</ul>
+ <p
+ v-else
+ class="multi-file-commit-list help-block"
+ >
+ {{ __('No changes') }}
+ </p>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
index 15918ac9631..2254271c679 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
@@ -1,35 +1,110 @@
<script>
- import { mapGetters } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { sprintf, n__, __ } from '~/locale';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ files: {
+ type: Array,
+ required: true,
},
- computed: {
- ...mapGetters([
- 'addedFiles',
- 'modifiedFiles',
- ]),
+ iconName: {
+ type: String,
+ required: true,
},
- };
+ title: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ addedFilesLength() {
+ return this.files.filter(f => f.tempFile).length;
+ },
+ modifiedFilesLength() {
+ return this.files.filter(f => !f.tempFile).length;
+ },
+ addedFilesIconClass() {
+ return this.addedFilesLength ? 'multi-file-addition' : '';
+ },
+ modifiedFilesClass() {
+ return this.modifiedFilesLength ? 'multi-file-modified' : '';
+ },
+ additionsTooltip() {
+ return sprintf(n__('1 %{type} addition', '%d %{type} additions', this.addedFilesLength), {
+ type: this.title.toLowerCase(),
+ });
+ },
+ modifiedTooltip() {
+ return sprintf(
+ n__('1 %{type} modification', '%d %{type} modifications', this.modifiedFilesLength),
+ { type: this.title.toLowerCase() },
+ );
+ },
+ titleTooltip() {
+ return sprintf(__('%{title} changes'), { title: this.title });
+ },
+ additionIconName() {
+ return this.title.toLowerCase() === 'staged' ? 'file-addition-solid' : 'file-addition';
+ },
+ modifiedIconName() {
+ return this.title.toLowerCase() === 'staged' ? 'file-modified-solid' : 'file-modified';
+ },
+ },
+};
</script>
<template>
<div
class="multi-file-commit-list-collapsed text-center"
>
- <icon
- name="file-addition"
- :size="18"
- css-classes="multi-file-addition append-bottom-10"
- />
- {{ addedFiles.length }}
- <icon
- name="file-modified"
- :size="18"
- css-classes="multi-file-modified prepend-top-10 append-bottom-10"
- />
- {{ modifiedFiles.length }}
+ <div
+ v-tooltip
+ :title="titleTooltip"
+ data-container="body"
+ data-placement="left"
+ class="append-bottom-15"
+ >
+ <icon
+ v-once
+ :name="iconName"
+ :size="18"
+ />
+ </div>
+ <div
+ v-tooltip
+ :title="additionsTooltip"
+ data-container="body"
+ data-placement="left"
+ class="append-bottom-10"
+ >
+ <icon
+ :name="additionIconName"
+ :size="18"
+ :css-classes="addedFilesIconClass"
+ />
+ </div>
+ {{ addedFilesLength }}
+ <div
+ v-tooltip
+ :title="modifiedTooltip"
+ data-container="body"
+ data-placement="left"
+ class="prepend-top-10 append-bottom-10"
+ >
+ <icon
+ :name="modifiedIconName"
+ :size="18"
+ :css-classes="modifiedFilesClass"
+ />
+ </div>
+ {{ modifiedFilesLength }}
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 560cdd941cd..ad4713c40d5 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -1,34 +1,69 @@
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
+import StageButton from './stage_button.vue';
+import UnstageButton from './unstage_button.vue';
export default {
components: {
Icon,
+ StageButton,
+ UnstageButton,
},
props: {
file: {
type: Object,
required: true,
},
+ actionComponent: {
+ type: String,
+ required: true,
+ },
+ keyPrefix: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ stagedList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
iconName() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
+ const prefix = this.stagedList ? '-solid' : '';
+ return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
},
iconClass() {
- return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
+ return `multi-file-${this.file.tempFile ? 'additions' : 'modified'} append-right-8`;
},
},
methods: {
- ...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
- openFileInEditor(file) {
- return this.openPendingTab(file).then(changeViewer => {
+ ...mapActions([
+ 'discardFileChanges',
+ 'updateViewer',
+ 'openPendingTab',
+ 'unstageChange',
+ 'stageChange',
+ ]),
+ openFileInEditor() {
+ return this.openPendingTab({
+ file: this.file,
+ keyPrefix: this.keyPrefix.toLowerCase(),
+ }).then(changeViewer => {
if (changeViewer) {
this.updateViewer('diff');
}
});
},
+ fileAction() {
+ if (this.file.staged) {
+ this.unstageChange(this.file.path);
+ } else {
+ this.stageChange(this.file.path);
+ }
+ },
},
};
</script>
@@ -38,7 +73,9 @@ export default {
<button
type="button"
class="multi-file-commit-list-path"
- @click="openFileInEditor(file)">
+ @dblclick="fileAction"
+ @click="openFileInEditor"
+ >
<span class="multi-file-commit-list-file-path">
<icon
:name="iconName"
@@ -47,12 +84,9 @@ export default {
/>{{ file.path }}
</span>
</button>
- <button
- type="button"
- class="btn btn-blank multi-file-discard-btn"
- @click="discardFileChanges(file.path)"
- >
- Discard
- </button>
+ <component
+ :is="actionComponent"
+ :path="file.path"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
new file mode 100644
index 00000000000..dcd934f76b7
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
@@ -0,0 +1,130 @@
+<script>
+import { __, sprintf } from '../../../locale';
+import Icon from '../../../vue_shared/components/icon.vue';
+import popover from '../../../vue_shared/directives/popover';
+import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
+
+export default {
+ directives: {
+ popover,
+ },
+ components: {
+ Icon,
+ },
+ props: {
+ text: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ scrollTop: 0,
+ isFocused: false,
+ };
+ },
+ computed: {
+ allLines() {
+ return this.text.split('\n').map((line, i) => ({
+ text: line.substr(0, this.getLineLength(i)) || ' ',
+ highlightedText: line.substr(this.getLineLength(i)),
+ }));
+ },
+ },
+ methods: {
+ handleScroll() {
+ if (this.$refs.textarea) {
+ this.$nextTick(() => {
+ this.scrollTop = this.$refs.textarea.scrollTop;
+ });
+ }
+ },
+ getLineLength(i) {
+ return i === 0 ? MAX_TITLE_LENGTH : MAX_BODY_LENGTH;
+ },
+ onInput(e) {
+ this.$emit('input', e.target.value);
+ },
+ updateIsFocused(isFocused) {
+ this.isFocused = isFocused;
+ },
+ },
+ popoverOptions: {
+ trigger: 'hover',
+ placement: 'top',
+ content: sprintf(
+ __(`
+ The character highligher helps you keep the subject line to %{titleLength} characters
+ and wrap the body at %{bodyLength} so they are readable in git.
+ `),
+ { titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
+ ),
+ },
+};
+</script>
+
+<template>
+ <fieldset class="common-note-form ide-commit-message-field">
+ <div
+ class="md-area"
+ :class="{
+ 'is-focused': isFocused
+ }"
+ >
+ <div
+ v-once
+ class="md-header"
+ >
+ <ul class="nav-links">
+ <li>
+ {{ __('Commit Message') }}
+ <span
+ v-popover="$options.popoverOptions"
+ class="help-block prepend-left-10"
+ >
+ <icon
+ name="question"
+ />
+ </span>
+ </li>
+ </ul>
+ </div>
+ <div class="ide-commit-message-textarea-container">
+ <div class="ide-commit-message-highlights-container">
+ <div
+ class="note-textarea highlights monospace"
+ :style="{
+ transform: `translate3d(0, ${-scrollTop}px, 0)`
+ }"
+ >
+ <div
+ v-for="(line, index) in allLines"
+ :key="index"
+ >
+ <span
+ v-text="line.text"
+ >
+ </span><mark
+ v-show="line.highlightedText"
+ v-text="line.highlightedText"
+ >
+ </mark>
+ </div>
+ </div>
+ </div>
+ <textarea
+ class="note-textarea ide-commit-message-textarea"
+ name="commit-message"
+ :placeholder="__('Write a commit message...')"
+ :value="text"
+ @scroll="handleScroll"
+ @input="onInput"
+ @focus="updateIsFocused(true)"
+ @blur="updateIsFocused(false)"
+ ref="textarea"
+ >
+ </textarea>
+ </div>
+ </div>
+ </fieldset>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
index 4310d762c78..b660a2961cb 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -1,52 +1,40 @@
<script>
- import { mapActions, mapState, mapGetters } from 'vuex';
- import tooltip from '~/vue_shared/directives/tooltip';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import tooltip from '~/vue_shared/directives/tooltip';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ props: {
+ value: {
+ type: String,
+ required: true,
},
- props: {
- value: {
- type: String,
- required: true,
- },
- label: {
- type: String,
- required: false,
- default: null,
- },
- checked: {
- type: Boolean,
- required: false,
- default: false,
- },
- showInput: {
- type: Boolean,
- required: false,
- default: false,
- },
- helpText: {
- type: String,
- required: false,
- default: null,
- },
+ label: {
+ type: String,
+ required: false,
+ default: null,
},
- computed: {
- ...mapState('commit', [
- 'commitAction',
- ]),
- ...mapGetters('commit', [
- 'newBranchName',
- ]),
+ checked: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- methods: {
- ...mapActions('commit', [
- 'updateCommitAction',
- 'updateBranchName',
- ]),
+ showInput: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- };
+ },
+ computed: {
+ ...mapState('commit', ['commitAction']),
+ ...mapGetters('commit', ['newBranchName']),
+ },
+ methods: {
+ ...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
+ },
+};
</script>
<template>
@@ -65,18 +53,6 @@
{{ label }}
</template>
<slot v-else></slot>
- <span
- v-if="helpText"
- v-tooltip
- class="help-block inline"
- :title="helpText"
- >
- <i
- class="fa fa-question-circle"
- aria-hidden="true"
- >
- </i>
- </span>
</span>
</label>
<div
@@ -85,7 +61,7 @@
>
<input
type="text"
- class="form-control"
+ class="form-control monospace"
:placeholder="newBranchName"
@input="updateBranchName($event.target.value)"
/>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
new file mode 100644
index 00000000000..52dce8412ab
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
@@ -0,0 +1,59 @@
+<script>
+import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions(['stageChange', 'discardFileChanges']),
+ },
+};
+</script>
+
+<template>
+ <div
+ v-once
+ class="multi-file-discard-btn"
+ >
+ <button
+ v-tooltip
+ type="button"
+ class="btn btn-blank append-right-5"
+ :aria-label="__('Stage changes')"
+ :title="__('Stage changes')"
+ data-container="body"
+ @click.stop="stageChange(path)"
+ >
+ <icon
+ name="mobile-issue-close"
+ :size="12"
+ />
+ </button>
+ <button
+ v-tooltip
+ type="button"
+ class="btn btn-blank"
+ :aria-label="__('Discard changes')"
+ :title="__('Discard changes')"
+ data-container="body"
+ @click.stop="discardFileChanges(path)"
+ >
+ <icon
+ name="remove"
+ :size="12"
+ />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
new file mode 100644
index 00000000000..123d60da47e
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
@@ -0,0 +1,45 @@
+<script>
+import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions(['unstageChange']),
+ },
+};
+</script>
+
+<template>
+ <div
+ v-once
+ class="multi-file-discard-btn"
+ >
+ <button
+ v-tooltip
+ type="button"
+ class="btn btn-blank"
+ :aria-label="__('Unstage changes')"
+ :title="__('Unstage changes')"
+ data-container="body"
+ @click="unstageChange(path)"
+ >
+ <icon
+ name="history"
+ :size="12"
+ />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue
new file mode 100644
index 00000000000..ea2b13a8b21
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_finder/index.vue
@@ -0,0 +1,245 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import VirtualList from 'vue-virtual-scroll-list';
+import Item from './item.vue';
+import router from '../../ide_router';
+import {
+ MAX_FILE_FINDER_RESULTS,
+ FILE_FINDER_ROW_HEIGHT,
+ FILE_FINDER_EMPTY_ROW_HEIGHT,
+} from '../../constants';
+import {
+ UP_KEY_CODE,
+ DOWN_KEY_CODE,
+ ENTER_KEY_CODE,
+ ESC_KEY_CODE,
+} from '../../../lib/utils/keycodes';
+
+export default {
+ components: {
+ Item,
+ VirtualList,
+ },
+ data() {
+ return {
+ focusedIndex: 0,
+ searchText: '',
+ mouseOver: false,
+ cancelMouseOver: false,
+ };
+ },
+ computed: {
+ ...mapGetters(['allBlobs']),
+ ...mapState(['fileFindVisible', 'loading']),
+ filteredBlobs() {
+ const searchText = this.searchText.trim();
+
+ if (searchText === '') {
+ return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS);
+ }
+
+ return fuzzaldrinPlus
+ .filter(this.allBlobs, searchText, {
+ key: 'path',
+ maxResults: MAX_FILE_FINDER_RESULTS,
+ })
+ .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
+ },
+ filteredBlobsLength() {
+ return this.filteredBlobs.length;
+ },
+ listShowCount() {
+ return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1;
+ },
+ listHeight() {
+ return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT;
+ },
+ showClearInputButton() {
+ return this.searchText.trim() !== '';
+ },
+ },
+ watch: {
+ fileFindVisible() {
+ this.$nextTick(() => {
+ if (!this.fileFindVisible) {
+ this.searchText = '';
+ } else {
+ this.focusedIndex = 0;
+
+ if (this.$refs.searchInput) {
+ this.$refs.searchInput.focus();
+ }
+ }
+ });
+ },
+ searchText() {
+ this.focusedIndex = 0;
+ },
+ focusedIndex() {
+ if (!this.mouseOver) {
+ this.$nextTick(() => {
+ const el = this.$refs.virtualScrollList.$el;
+ const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT;
+ const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT;
+
+ if (this.focusedIndex === 0) {
+ // if index is the first index, scroll straight to start
+ el.scrollTop = 0;
+ } else if (this.focusedIndex === this.filteredBlobsLength - 1) {
+ // if index is the last index, scroll to the end
+ el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT;
+ } else if (scrollTop >= bottom + el.scrollTop) {
+ // if element is off the bottom of the scroll list, scroll down one item
+ el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT;
+ } else if (scrollTop < el.scrollTop) {
+ // if element is off the top of the scroll list, scroll up one item
+ el.scrollTop = scrollTop;
+ }
+ });
+ }
+ },
+ },
+ methods: {
+ ...mapActions(['toggleFileFinder']),
+ clearSearchInput() {
+ this.searchText = '';
+
+ this.$nextTick(() => {
+ this.$refs.searchInput.focus();
+ });
+ },
+ onKeydown(e) {
+ switch (e.keyCode) {
+ case UP_KEY_CODE:
+ e.preventDefault();
+ this.mouseOver = false;
+ this.cancelMouseOver = true;
+ if (this.focusedIndex > 0) {
+ this.focusedIndex -= 1;
+ } else {
+ this.focusedIndex = this.filteredBlobsLength - 1;
+ }
+ break;
+ case DOWN_KEY_CODE:
+ e.preventDefault();
+ this.mouseOver = false;
+ this.cancelMouseOver = true;
+ if (this.focusedIndex < this.filteredBlobsLength - 1) {
+ this.focusedIndex += 1;
+ } else {
+ this.focusedIndex = 0;
+ }
+ break;
+ default:
+ break;
+ }
+ },
+ onKeyup(e) {
+ switch (e.keyCode) {
+ case ENTER_KEY_CODE:
+ this.openFile(this.filteredBlobs[this.focusedIndex]);
+ break;
+ case ESC_KEY_CODE:
+ this.toggleFileFinder(false);
+ break;
+ default:
+ break;
+ }
+ },
+ openFile(file) {
+ this.toggleFileFinder(false);
+ router.push(`/project${file.url}`);
+ },
+ onMouseOver(index) {
+ if (!this.cancelMouseOver) {
+ this.mouseOver = true;
+ this.focusedIndex = index;
+ }
+ },
+ onMouseMove(index) {
+ this.cancelMouseOver = false;
+ this.onMouseOver(index);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-file-finder-overlay"
+ @mousedown.self="toggleFileFinder(false)"
+ >
+ <div
+ class="dropdown-menu diff-file-changes ide-file-finder show"
+ >
+ <div class="dropdown-input">
+ <input
+ type="search"
+ class="dropdown-input-field"
+ :placeholder="__('Search files')"
+ autocomplete="off"
+ v-model="searchText"
+ ref="searchInput"
+ @keydown="onKeydown($event)"
+ @keyup="onKeyup($event)"
+ />
+ <i
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search"
+ :class="{
+ hidden: showClearInputButton
+ }"
+ ></i>
+ <i
+ role="button"
+ :aria-label="__('Clear search input')"
+ class="fa fa-times dropdown-input-clear"
+ :class="{
+ show: showClearInputButton
+ }"
+ @click="clearSearchInput"
+ ></i>
+ </div>
+ <div>
+ <virtual-list
+ :size="listHeight"
+ :remain="listShowCount"
+ wtag="ul"
+ ref="virtualScrollList"
+ >
+ <template v-if="filteredBlobsLength">
+ <li
+ v-for="(file, index) in filteredBlobs"
+ :key="file.key"
+ >
+ <item
+ class="disable-hover"
+ :file="file"
+ :search-text="searchText"
+ :focused="index === focusedIndex"
+ :index="index"
+ @click="openFile"
+ @mouseover="onMouseOver"
+ @mousemove="onMouseMove"
+ />
+ </li>
+ </template>
+ <li
+ v-else
+ class="dropdown-menu-empty-item"
+ >
+ <div class="append-right-default prepend-left-default prepend-top-8 append-bottom-8">
+ <template v-if="loading">
+ {{ __('Loading...') }}
+ </template>
+ <template v-else>
+ {{ __('No files found.') }}
+ </template>
+ </div>
+ </li>
+ </virtual-list>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue
new file mode 100644
index 00000000000..d4427420207
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_finder/item.vue
@@ -0,0 +1,113 @@
+<script>
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import FileIcon from '../../../vue_shared/components/file_icon.vue';
+import ChangedFileIcon from '../changed_file_icon.vue';
+
+const MAX_PATH_LENGTH = 60;
+
+export default {
+ components: {
+ ChangedFileIcon,
+ FileIcon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ focused: {
+ type: Boolean,
+ required: true,
+ },
+ searchText: {
+ type: String,
+ required: true,
+ },
+ index: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ pathWithEllipsis() {
+ const path = this.file.path;
+
+ return path.length < MAX_PATH_LENGTH
+ ? path
+ : `...${path.substr(path.length - MAX_PATH_LENGTH)}`;
+ },
+ nameSearchTextOccurences() {
+ return fuzzaldrinPlus.match(this.file.name, this.searchText);
+ },
+ pathSearchTextOccurences() {
+ return fuzzaldrinPlus.match(this.pathWithEllipsis, this.searchText);
+ },
+ },
+ methods: {
+ clickRow() {
+ this.$emit('click', this.file);
+ },
+ mouseOverRow() {
+ this.$emit('mouseover', this.index);
+ },
+ mouseMove() {
+ this.$emit('mousemove', this.index);
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ type="button"
+ class="diff-changed-file"
+ :class="{
+ 'is-focused': focused,
+ }"
+ @click.prevent="clickRow"
+ @mouseover="mouseOverRow"
+ @mousemove="mouseMove"
+ >
+ <file-icon
+ :file-name="file.name"
+ :size="16"
+ css-classes="diff-file-changed-icon append-right-8"
+ />
+ <span class="diff-changed-file-content append-right-8">
+ <strong
+ class="diff-changed-file-name"
+ >
+ <span
+ v-for="(char, index) in file.name.split('')"
+ :key="index + char"
+ :class="{
+ highlighted: nameSearchTextOccurences.indexOf(index) >= 0,
+ }"
+ v-text="char"
+ >
+ </span>
+ </strong>
+ <span
+ class="diff-changed-file-path prepend-top-5"
+ >
+ <span
+ v-for="(char, index) in pathWithEllipsis.split('')"
+ :key="index + char"
+ :class="{
+ highlighted: pathSearchTextOccurences.indexOf(index) >= 0,
+ }"
+ v-text="char"
+ >
+ </span>
+ </span>
+ </span>
+ <span
+ v-if="file.changed || file.tempFile"
+ class="diff-changed-stats"
+ >
+ <changed-file-icon
+ :file="file"
+ />
+ </span>
+ </button>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 1c237c0ec97..0274fc7d299 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,55 +1,91 @@
<script>
-import { mapState, mapGetters } from 'vuex';
-import ideSidebar from './ide_side_bar.vue';
-import ideContextbar from './ide_context_bar.vue';
-import repoTabs from './repo_tabs.vue';
-import ideStatusBar from './ide_status_bar.vue';
-import repoEditor from './repo_editor.vue';
+ import { mapActions, mapState, mapGetters } from 'vuex';
+ import Mousetrap from 'mousetrap';
+ import ideSidebar from './ide_side_bar.vue';
+ import ideContextbar from './ide_context_bar.vue';
+ import repoTabs from './repo_tabs.vue';
+ import ideStatusBar from './ide_status_bar.vue';
+ import repoEditor from './repo_editor.vue';
+ import FindFile from './file_finder/index.vue';
-export default {
- components: {
- ideSidebar,
- ideContextbar,
- repoTabs,
- ideStatusBar,
- repoEditor,
- },
- props: {
- emptyStateSvgPath: {
- type: String,
- required: true,
+ const originalStopCallback = Mousetrap.stopCallback;
+
+ export default {
+ components: {
+ ideSidebar,
+ ideContextbar,
+ repoTabs,
+ ideStatusBar,
+ repoEditor,
+ FindFile,
},
- noChangesStateSvgPath: {
- type: String,
- required: true,
+ props: {
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ noChangesStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ committedStateSvgPath: {
+ type: String,
+ required: true,
+ },
},
- committedStateSvgPath: {
- type: String,
- required: true,
+ computed: {
+ ...mapState([
+ 'changedFiles',
+ 'openFiles',
+ 'viewer',
+ 'currentMergeRequestId',
+ 'fileFindVisible',
+ ]),
+ ...mapGetters(['activeFile', 'hasChanges']),
},
- },
- computed: {
- ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
- ...mapGetters(['activeFile', 'hasChanges']),
- },
- mounted() {
- const returnValue = 'Are you sure you want to lose unsaved changes?';
- window.onbeforeunload = e => {
- if (!this.changedFiles.length) return undefined;
+ mounted() {
+ const returnValue = 'Are you sure you want to lose unsaved changes?';
+ window.onbeforeunload = e => {
+ if (!this.changedFiles.length) return undefined;
+
+ Object.assign(e, {
+ returnValue,
+ });
+ return returnValue;
+ };
- Object.assign(e, {
- returnValue,
+ Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
+
+ this.toggleFileFinder(!this.fileFindVisible);
});
- return returnValue;
- };
- },
-};
+
+ Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
+ },
+ methods: {
+ ...mapActions(['toggleFileFinder']),
+ mousetrapStopCallback(e, el, combo) {
+ if (combo === 't' && el.classList.contains('dropdown-input-field')) {
+ return true;
+ } else if (combo === 'command+p' || combo === 'ctrl+p') {
+ return false;
+ }
+
+ return originalStopCallback(e, el, combo);
+ },
+ },
+ };
</script>
<template>
<div
class="ide-view"
>
+ <find-file
+ v-show="fileFindVisible"
+ />
<ide-sidebar />
<div
class="multi-file-edit-pane"
diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue
index 79a83b47994..627fbeb9adf 100644
--- a/app/assets/javascripts/ide/components/ide_context_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_context_bar.vue
@@ -1,5 +1,4 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue';
@@ -22,13 +21,6 @@ export default {
required: true,
},
},
- computed: {
- ...mapState(['changedFiles', 'rightPanelCollapsed']),
- ...mapGetters(['currentIcon']),
- },
- methods: {
- ...mapActions(['setPanelCollapsedStatus']),
- },
};
</script>
@@ -41,40 +33,6 @@ export default {
<div
class="multi-file-commit-panel-section"
>
- <header
- class="multi-file-commit-panel-header"
- :class="{
- 'is-collapsed': rightPanelCollapsed,
- }"
- >
- <div
- class="multi-file-commit-panel-header-title"
- v-if="!rightPanelCollapsed"
- >
- <div
- v-if="changedFiles.length"
- >
- <icon
- name="list-bulleted"
- :size="18"
- />
- Staged
- </div>
- </div>
- <button
- type="button"
- class="btn btn-transparent multi-file-commit-panel-collapse-btn"
- @click.stop="setPanelCollapsedStatus({
- side: 'right',
- collapsed: !rightPanelCollapsed,
- })"
- >
- <icon
- :name="currentIcon"
- :size="18"
- />
- </button>
- </header>
<repo-commit-section
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 152a5f632ad..c13eeeace3f 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -22,13 +22,6 @@ export default {
<template>
<div class="ide-status-bar">
- <div class="ref-name">
- <icon
- name="branch"
- :size="12"
- />
- {{ file.branchId }}
- </div>
<div>
<div v-if="file.lastCommit && file.lastCommit.id">
Last commit:
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index 769e9b79cad..b1b5c0d4a28 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -1,49 +1,54 @@
<script>
- import { mapActions } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
- import newModal from './modal.vue';
- import upload from './upload.vue';
+import { mapActions } from 'vuex';
+import icon from '~/vue_shared/components/icon.vue';
+import newModal from './modal.vue';
+import upload from './upload.vue';
- export default {
- components: {
- icon,
- newModal,
- upload,
+export default {
+ components: {
+ icon,
+ newModal,
+ upload,
+ },
+ props: {
+ branch: {
+ type: String,
+ required: true,
},
- props: {
- branch: {
- type: String,
- required: true,
- },
- path: {
- type: String,
- required: true,
- },
+ path: {
+ type: String,
+ required: true,
},
- data() {
- return {
- openModal: false,
- modalType: '',
- dropdownOpen: false,
- };
+ },
+ data() {
+ return {
+ openModal: false,
+ modalType: '',
+ dropdownOpen: false,
+ };
+ },
+ watch: {
+ dropdownOpen() {
+ this.$nextTick(() => {
+ this.$refs.dropdownMenu.scrollIntoView();
+ });
},
- methods: {
- ...mapActions([
- 'createTempEntry',
- ]),
- createNewItem(type) {
- this.modalType = type;
- this.openModal = true;
- this.dropdownOpen = false;
- },
- hideModal() {
- this.openModal = false;
- },
- openDropdown() {
- this.dropdownOpen = !this.dropdownOpen;
- },
+ },
+ methods: {
+ ...mapActions(['createTempEntry']),
+ createNewItem(type) {
+ this.modalType = type;
+ this.openModal = true;
+ this.dropdownOpen = false;
},
- };
+ hideModal() {
+ this.openModal = false;
+ },
+ openDropdown() {
+ this.dropdownOpen = !this.dropdownOpen;
+ },
+ },
+};
</script>
<template>
@@ -71,7 +76,10 @@
css-classes="pull-left"
/>
</button>
- <ul class="dropdown-menu dropdown-menu-right">
+ <ul
+ class="dropdown-menu dropdown-menu-right"
+ ref="dropdownMenu"
+ >
<li>
<a
href="#"
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 4b5a50785b6..a95a0225950 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -40,13 +40,6 @@ export default {
return __('Create file');
},
- formLabelName() {
- if (this.type === 'tree') {
- return __('Directory name');
- }
-
- return __('File name');
- },
},
mounted() {
this.$refs.fieldName.focus();
@@ -82,8 +75,8 @@ export default {
@submit.prevent="createEntryInStore"
>
<fieldset class="form-group append-bottom-0">
- <label class="label-light col-sm-3">
- {{ formLabelName }}
+ <label class="label-light col-sm-3 ide-new-modal-label">
+ {{ __('Name') }}
</label>
<div class="col-sm-9">
<input
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index d885ed5e301..877d1b5e026 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -1,20 +1,24 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
-import icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
-import commitFilesList from './commit_sidebar/list.vue';
+import CommitFilesList from './commit_sidebar/list.vue';
+import EmptyState from './commit_sidebar/empty_state.vue';
+import CommitMessageField from './commit_sidebar/message_field.vue';
import * as consts from '../stores/modules/commit/constants';
import Actions from './commit_sidebar/actions.vue';
export default {
components: {
DeprecatedModal,
- icon,
- commitFilesList,
+ Icon,
+ CommitFilesList,
+ EmptyState,
Actions,
LoadingButton,
+ CommitMessageField,
},
directives: {
tooltip,
@@ -30,43 +34,19 @@ export default {
},
},
computed: {
- ...mapState([
- 'currentProjectId',
- 'currentBranchId',
- 'rightPanelCollapsed',
- 'lastCommitMsg',
- 'changedFiles',
- ]),
+ ...mapState(['changedFiles', 'stagedFiles', 'rightPanelCollapsed']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
- ...mapGetters('commit', [
- 'commitButtonDisabled',
- 'discardDraftButtonDisabled',
- 'branchName',
- ]),
- statusSvg() {
- return this.lastCommitMsg
- ? this.committedStateSvgPath
- : this.noChangesStateSvgPath;
- },
+ ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled', 'branchName']),
},
methods: {
- ...mapActions(['setPanelCollapsedStatus']),
...mapActions('commit', [
'updateCommitMessage',
'discardDraft',
'commitChanges',
'updateCommitAction',
]),
- toggleCollapsed() {
- this.setPanelCollapsedStatus({
- side: 'right',
- collapsed: !this.rightPanelCollapsed,
- });
- },
forceCreateNewBranch() {
- return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() =>
- this.commitChanges(),
- );
+ return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
},
};
@@ -75,9 +55,6 @@ export default {
<template>
<div
class="multi-file-commit-panel-section"
- :class="{
- 'multi-file-commit-empty-state-container': !changedFiles.length
- }"
>
<deprecated-modal
id="ide-create-branch-modal"
@@ -91,30 +68,36 @@ export default {
Would you like to create a new branch?`) }}
</template>
</deprecated-modal>
- <commit-files-list
- title="Staged"
- :file-list="changedFiles"
- :collapsed="rightPanelCollapsed"
- @toggleCollapsed="toggleCollapsed"
- />
<template
- v-if="changedFiles.length"
+ v-if="changedFiles.length || stagedFiles.length"
>
+ <commit-files-list
+ icon-name="unstaged"
+ :title="__('Unstaged')"
+ :file-list="changedFiles"
+ action="stageAllChanges"
+ :action-btn-text="__('Stage all')"
+ item-action-component="stage-button"
+ />
+ <commit-files-list
+ icon-name="staged"
+ :title="__('Staged')"
+ :file-list="stagedFiles"
+ action="unstageAllChanges"
+ :action-btn-text="__('Unstage all')"
+ item-action-component="unstage-button"
+ :show-toggle="false"
+ :staged-list="true"
+ />
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent.stop="commitChanges"
v-if="!rightPanelCollapsed"
>
- <div class="multi-file-commit-fieldset">
- <textarea
- class="form-control multi-file-commit-message"
- name="commit-message"
- :value="commitMessage"
- :placeholder="__('Write a commit message...')"
- @input="updateCommitMessage($event.target.value)"
- >
- </textarea>
- </div>
+ <commit-message-field
+ :text="commitMessage"
+ @input="updateCommitMessage"
+ />
<div class="clearfix prepend-top-15">
<actions />
<loading-button
@@ -135,38 +118,10 @@ export default {
</div>
</form>
</template>
- <div
- v-else-if="!rightPanelCollapsed"
- class="row js-empty-state"
- >
- <div class="col-xs-10 col-xs-offset-1">
- <div class="svg-content svg-80">
- <img :src="statusSvg" />
- </div>
- </div>
- <div class="col-xs-10 col-xs-offset-1">
- <div
- class="text-content text-center"
- v-if="!lastCommitMsg"
- >
- <h4>
- {{ __('No changes') }}
- </h4>
- <p>
- {{ __('Edit files in the editor and commit changes here') }}
- </p>
- </div>
- <div
- class="text-content text-center"
- v-else
- >
- <h4>
- {{ __('All changes are committed') }}
- </h4>
- <p v-html="lastCommitMsg">
- </p>
- </div>
- </div>
- </div>
+ <empty-state
+ v-else
+ :no-changes-state-svg-path="noChangesStateSvgPath"
+ :committed-state-svg-path="committedStateSvgPath"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 711bafa17a9..3a04cdd8e46 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -20,7 +20,7 @@ export default {
},
computed: {
...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
- ...mapGetters(['currentMergeRequest']),
+ ...mapGetters(['currentMergeRequest', 'getStagedFile']),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.content;
},
@@ -120,7 +120,12 @@ export default {
setupEditor() {
if (!this.file || !this.editor.instance) return;
- this.model = this.editor.createModel(this.file);
+ const head = this.getStagedFile(this.file.path);
+
+ this.model = this.editor.createModel(
+ this.file,
+ this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
+ );
if (this.viewer === 'mrdiff') {
this.editor.attachMergeRequestModel(this.model);
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 3b5068d4910..e86db2da4a6 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -97,13 +97,17 @@ export default {
:file="file"
/>
</span>
- <span class="pull-right">
+ <span class="pull-right ide-file-icon-holder">
<mr-file-icon
v-if="file.mrChange"
/>
<changed-file-icon
+ v-if="file.changed || file.tempFile || file.staged"
:file="file"
- v-if="file.changed || file.tempFile"
+ :show-tooltip="true"
+ :show-staged-icon="true"
+ :force-modified-icon="true"
+ class="pull-right"
/>
</span>
<new-dropdown
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index 304a73ed1ad..a3ee3184c19 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -26,13 +26,16 @@ export default {
},
computed: {
closeLabel() {
- if (this.tab.changed || this.tab.tempFile) {
+ if (this.fileHasChanged) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
showChangedIcon() {
- return this.tab.changed ? !this.tabMouseOver : false;
+ return this.fileHasChanged ? !this.tabMouseOver : false;
+ },
+ fileHasChanged() {
+ return this.tab.changed || this.tab.tempFile || this.tab.staged;
},
},
@@ -42,18 +45,18 @@ export default {
this.updateDelayViewerUpdated(true);
if (tab.pending) {
- this.openPendingTab(tab);
+ this.openPendingTab({ file: tab, keyPrefix: tab.staged ? 'staged' : 'unstaged' });
} else {
this.$router.push(`/project${tab.url}`);
}
},
mouseOverTab() {
- if (this.tab.changed) {
+ if (this.fileHasChanged) {
this.tabMouseOver = true;
}
},
mouseOutTab() {
- if (this.tab.changed) {
+ if (this.fileHasChanged) {
this.tabMouseOver = false;
}
},
@@ -81,6 +84,7 @@ export default {
<changed-file-icon
v-else
:file="tab"
+ :force-modified-icon="true"
/>
</button>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
new file mode 100644
index 00000000000..b06da9f95d1
--- /dev/null
+++ b/app/assets/javascripts/ide/constants.js
@@ -0,0 +1,8 @@
+// Fuzzy file finder
+export const MAX_FILE_FINDER_RESULTS = 40;
+export const FILE_FINDER_ROW_HEIGHT = 55;
+export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
+
+// Commit message textarea
+export const MAX_TITLE_LENGTH = 50;
+export const MAX_BODY_LENGTH = 72;
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index e47adae99ed..016dcda1fa1 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -3,15 +3,16 @@ import Disposable from './disposable';
import eventHub from '../../eventhub';
export default class Model {
- constructor(monaco, file) {
+ constructor(monaco, file, head = null) {
this.monaco = monaco;
this.disposable = new Disposable();
this.file = file;
+ this.head = head;
this.content = file.content !== '' ? file.content : file.raw;
this.disposable.add(
(this.originalModel = this.monaco.editor.createModel(
- this.file.raw,
+ head ? head.content : this.file.raw,
undefined,
new this.monaco.Uri(null, null, `original/${this.file.key}`),
)),
@@ -31,13 +32,15 @@ export default class Model {
);
}
- this.events = new Map();
+ this.events = new Set();
this.updateContent = this.updateContent.bind(this);
+ this.updateNewContent = this.updateNewContent.bind(this);
this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose);
- eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
+ eventHub.$on(`editor.update.model.content.${this.file.key}`, this.updateContent);
+ eventHub.$on(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
}
get url() {
@@ -73,22 +76,36 @@ export default class Model {
}
onChange(cb) {
- this.events.set(
- this.path,
- this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))),
- );
+ this.events.add(this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))));
+ }
+
+ onDispose(cb) {
+ this.events.add(cb);
}
- updateContent(content) {
+ updateContent({ content, changed }) {
this.getOriginalModel().setValue(content);
+
+ if (!changed) {
+ this.getModel().setValue(content);
+ }
+ }
+
+ updateNewContent(content) {
this.getModel().setValue(content);
}
dispose() {
this.disposable.dispose();
+
+ this.events.forEach(cb => {
+ if (typeof cb === 'function') cb();
+ });
+
this.events.clear();
eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
- eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
+ eventHub.$off(`editor.update.model.content.${this.file.key}`, this.updateContent);
+ eventHub.$off(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
}
}
diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
index 0e7b563b5d6..7f643969480 100644
--- a/app/assets/javascripts/ide/lib/common/model_manager.js
+++ b/app/assets/javascripts/ide/lib/common/model_manager.js
@@ -17,12 +17,12 @@ export default class ModelManager {
return this.models.get(key);
}
- addModel(file) {
+ addModel(file, head = null) {
if (this.hasCachedModel(file.key)) {
return this.getModel(file.key);
}
- const model = new Model(this.monaco, file);
+ const model = new Model(this.monaco, file, head);
this.models.set(model.path, model);
this.disposable.add(model);
diff --git a/app/assets/javascripts/ide/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js
index 42904774747..13d477bb2cf 100644
--- a/app/assets/javascripts/ide/lib/decorations/controller.js
+++ b/app/assets/javascripts/ide/lib/decorations/controller.js
@@ -38,6 +38,15 @@ export default class DecorationsController {
);
}
+ hasDecorations(model) {
+ return this.decorations.has(model.url);
+ }
+
+ removeDecorations(model) {
+ this.decorations.delete(model.url);
+ this.editorDecorations.delete(model.url);
+ }
+
dispose() {
this.decorations.clear();
this.editorDecorations.clear();
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index b136545ad11..f579424cf33 100644
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
@@ -3,7 +3,7 @@ import { throttle } from 'underscore';
import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable';
-export const getDiffChangeType = (change) => {
+export const getDiffChangeType = change => {
if (change.modified) {
return 'modified';
} else if (change.added) {
@@ -16,12 +16,7 @@ export const getDiffChangeType = (change) => {
};
export const getDecorator = change => ({
- range: new monaco.Range(
- change.lineNumber,
- 1,
- change.endLineNumber,
- 1,
- ),
+ range: new monaco.Range(change.lineNumber, 1, change.endLineNumber, 1),
options: {
isWholeLine: true,
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
@@ -31,6 +26,7 @@ export const getDecorator = change => ({
export default class DirtyDiffController {
constructor(modelManager, decorationsController) {
this.disposable = new Disposable();
+ this.models = new Map();
this.editorSimpleWorker = null;
this.modelManager = modelManager;
this.decorationsController = decorationsController;
@@ -42,7 +38,15 @@ export default class DirtyDiffController {
}
attachModel(model) {
+ if (this.models.has(model.url)) return;
+
model.onChange(() => this.throttledComputeDiff(model));
+ model.onDispose(() => {
+ this.decorationsController.removeDecorations(model);
+ this.models.delete(model.url);
+ });
+
+ this.models.set(model.url, model);
}
computeDiff(model) {
@@ -54,7 +58,11 @@ export default class DirtyDiffController {
}
reDecorate(model) {
- this.decorationsController.decorate(model);
+ if (this.decorationsController.hasDecorations(model)) {
+ this.decorationsController.decorate(model);
+ } else {
+ this.computeDiff(model);
+ }
}
decorate({ data }) {
@@ -65,6 +73,7 @@ export default class DirtyDiffController {
dispose() {
this.disposable.dispose();
+ this.models.clear();
this.dirtyDiffWorker.removeEventListener('message', this.decorate);
this.dirtyDiffWorker.terminate();
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 001737d6ee8..b65d9c68a0b 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -1,10 +1,12 @@
import _ from 'underscore';
+import store from '../stores';
import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable';
import ModelManager from './common/model_manager';
import editorOptions, { defaultEditorOptions } from './editor_options';
import gitlabTheme from './themes/gl_theme';
+import keymap from './keymap.json';
export const clearDomElement = el => {
if (!el || !el.firstChild) return;
@@ -53,6 +55,8 @@ export default class Editor {
)),
);
+ this.addCommands();
+
window.addEventListener('resize', this.debouncedUpdate, false);
}
}
@@ -73,12 +77,14 @@ export default class Editor {
})),
);
+ this.addCommands();
+
window.addEventListener('resize', this.debouncedUpdate, false);
}
}
- createModel(file) {
- return this.modelManager.addModel(file);
+ createModel(file, head = null) {
+ return this.modelManager.addModel(file, head);
}
attachModel(model) {
@@ -189,4 +195,31 @@ export default class Editor {
static renderSideBySide(domElement) {
return domElement.offsetWidth >= 700;
}
+
+ addCommands() {
+ const getKeyCode = key => {
+ const monacoKeyMod = key.indexOf('KEY_') === 0;
+
+ return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key];
+ };
+
+ keymap.forEach(command => {
+ const keybindings = command.bindings.map(binding => {
+ const keys = binding.split('+');
+
+ // eslint-disable-next-line no-bitwise
+ return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]);
+ });
+
+ this.instance.addAction({
+ id: command.id,
+ label: command.label,
+ keybindings,
+ run() {
+ store.dispatch(command.action.name, command.action.params);
+ return null;
+ },
+ });
+ });
+ }
}
diff --git a/app/assets/javascripts/ide/lib/keymap.json b/app/assets/javascripts/ide/lib/keymap.json
new file mode 100644
index 00000000000..131abfebbed
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/keymap.json
@@ -0,0 +1,11 @@
+[
+ {
+ "id": "file-finder",
+ "label": "File finder",
+ "bindings": ["CtrlCmd+KEY_P"],
+ "action": {
+ "name": "toggleFileFinder",
+ "params": true
+ }
+ }
+]
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index c6ba679d99c..4c8c997e376 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -1,3 +1,4 @@
+import $ from 'jquery';
import Vue from 'vue';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
@@ -32,6 +33,19 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
}
};
+export const toggleRightPanelCollapsed = ({ dispatch, state }, e = undefined) => {
+ if (e) {
+ $(e.currentTarget)
+ .tooltip('hide')
+ .blur();
+ }
+
+ dispatch('setPanelCollapsedStatus', {
+ side: 'right',
+ collapsed: !state.rightPanelCollapsed,
+ });
+};
+
export const setResizingStatus = ({ commit }, resizing) => {
commit(types.SET_RESIZING_STATUS, resizing);
};
@@ -60,7 +74,7 @@ export const createTempEntry = (
}
worker.addEventListener('message', ({ data }) => {
- const { file } = data;
+ const { file, parentPath } = data;
worker.terminate();
@@ -76,6 +90,10 @@ export const createTempEntry = (
dispatch('setFileActive', file.path);
}
+ if (parentPath && !state.entries[parentPath].opened) {
+ commit(types.TOGGLE_TREE_OPEN, parentPath);
+ }
+
resolve(file);
});
@@ -104,6 +122,14 @@ export const scrollToTab = () => {
});
};
+export const stageAllChanges = ({ state, commit }) => {
+ state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
+};
+
+export const unstageAllChanges = ({ state, commit }) => {
+ state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path));
+};
+
export const updateViewer = ({ commit }, viewer) => {
commit(types.UPDATE_VIEWER, viewer);
};
@@ -112,7 +138,21 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
};
+export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => {
+ commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
+
+ if (file.parentPath) {
+ dispatch('updateTempFlagForEntry', { file: state.entries[file.parentPath], tempFile });
+ }
+};
+
+export const toggleFileFinder = ({ commit }, fileFindVisible) =>
+ commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
+
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
export * from './actions/merge_request';
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 1a17320a1ea..fcdb3b753b2 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -24,7 +24,10 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
if (nextFileToOpen.pending) {
dispatch('updateViewer', 'diff');
- dispatch('openPendingTab', nextFileToOpen);
+ dispatch('openPendingTab', {
+ file: nextFileToOpen,
+ keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
+ });
} else {
dispatch('updateDelayViewerUpdated', true);
router.push(`/project${nextFileToOpen.url}`);
@@ -60,7 +63,7 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
const file = state.entries[path];
commit(types.TOGGLE_LOADING, { entry: file });
return service
- .getFileData(file.url)
+ .getFileData(`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url}`)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
@@ -153,7 +156,7 @@ export const setFileViewMode = ({ state, commit }, { file, viewMode }) => {
commit(types.SET_FILE_VIEWMODE, { file, viewMode });
};
-export const discardFileChanges = ({ state, commit }, path) => {
+export const discardFileChanges = ({ dispatch, state, commit, getters }, path) => {
const file = state.entries[path];
commit(types.DISCARD_FILE_CHANGES, path);
@@ -161,17 +164,40 @@ export const discardFileChanges = ({ state, commit }, path) => {
if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, path);
+ } else if (getters.activeFile && file.path === getters.activeFile.path) {
+ dispatch('updateDelayViewerUpdated', true)
+ .then(() => {
+ router.push(`/project${file.url}`);
+ })
+ .catch(e => {
+ throw e;
+ });
+ }
+
+ eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.content);
+ eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content);
+};
+
+export const stageChange = ({ commit, state }, path) => {
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
+ commit(types.STAGE_CHANGE, path);
+
+ if (stagedFile) {
+ eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
}
+};
- eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
+export const unstageChange = ({ commit }, path) => {
+ commit(types.UNSTAGE_CHANGE, path);
};
-export const openPendingTab = ({ commit, getters, dispatch, state }, file) => {
- if (getters.activeFile && getters.activeFile.path === file.path && state.viewer === 'diff') {
+export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
+ if (getters.activeFile && getters.activeFile === file && state.viewer === 'diff') {
return false;
}
- commit(types.ADD_PENDING_TAB, { file });
+ commit(types.ADD_PENDING_TAB, { file, keyPrefix });
dispatch('scrollToTab');
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index b3882cb8d21..4eb23b2ee0f 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -5,45 +5,71 @@ import * as types from '../mutation_types';
export const getProjectData = (
{ commit, state, dispatch },
{ namespace, projectId, force = false } = {},
-) => new Promise((resolve, reject) => {
- if (!state.projects[`${namespace}/${projectId}`] || force) {
- commit(types.TOGGLE_LOADING, { entry: state });
- service.getProjectData(namespace, projectId)
- .then(res => res.data)
- .then((data) => {
+) =>
+ new Promise((resolve, reject) => {
+ if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, { entry: state });
- commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
- if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
- resolve(data);
- })
- .catch(() => {
- flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
- reject(new Error(`Project not loaded ${namespace}/${projectId}`));
- });
- } else {
- resolve(state.projects[`${namespace}/${projectId}`]);
- }
-});
+ service
+ .getProjectData(namespace, projectId)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.TOGGLE_LOADING, { entry: state });
+ commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
+ if (!state.currentProjectId)
+ commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
+ resolve(data);
+ })
+ .catch(() => {
+ flash(
+ 'Error loading project data. Please try again.',
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ reject(new Error(`Project not loaded ${namespace}/${projectId}`));
+ });
+ } else {
+ resolve(state.projects[`${namespace}/${projectId}`]);
+ }
+ });
export const getBranchData = (
{ commit, state, dispatch },
{ projectId, branchId, force = false } = {},
-) => new Promise((resolve, reject) => {
- if ((typeof state.projects[`${projectId}`] === 'undefined' ||
- !state.projects[`${projectId}`].branches[branchId])
- || force) {
- service.getBranchData(`${projectId}`, branchId)
- .then(({ data }) => {
- const { id } = data.commit;
- commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
- commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
- resolve(data);
- })
- .catch(() => {
- flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
- reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
- });
- } else {
- resolve(state.projects[`${projectId}`].branches[branchId]);
- }
-});
+) =>
+ new Promise((resolve, reject) => {
+ if (
+ typeof state.projects[`${projectId}`] === 'undefined' ||
+ !state.projects[`${projectId}`].branches[branchId] ||
+ force
+ ) {
+ service
+ .getBranchData(`${projectId}`, branchId)
+ .then(({ data }) => {
+ const { id } = data.commit;
+ commit(types.SET_BRANCH, {
+ projectPath: `${projectId}`,
+ branchName: branchId,
+ branch: data,
+ });
+ commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
+ commit(types.SET_CURRENT_BRANCH, branchId);
+ resolve(data);
+ })
+ .catch(() => {
+ flash(
+ 'Error loading branch data. Please try again.',
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
+ });
+ } else {
+ resolve(state.projects[`${projectId}`].branches[branchId]);
+ }
+ });
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index a77cdbc13c8..ec1ea155aee 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
@@ -29,9 +31,31 @@ export const currentMergeRequest = state => {
};
// eslint-disable-next-line no-confusing-arrow
-export const currentIcon = state =>
+export const collapseButtonIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
-export const hasChanges = state => !!state.changedFiles.length;
+export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
+
+// eslint-disable-next-line no-confusing-arrow
+export const collapseButtonTooltip = state =>
+ state.rightPanelCollapsed ? __('Expand sidebar') : __('Collapse sidebar');
export const hasMergeRequest = state => !!state.currentMergeRequestId;
+
+export const allBlobs = state =>
+ Object.keys(state.entries)
+ .reduce((acc, key) => {
+ const entry = state.entries[key];
+
+ if (entry.type === 'blob') {
+ acc.push(entry);
+ }
+
+ return acc;
+ }, [])
+ .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
+
+export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 367c45f7e2d..349ff68f1e3 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -98,23 +98,14 @@ export const updateFilesAfterCommit = (
{ root: true },
);
- rootState.changedFiles.forEach(entry => {
- commit(
- rootTypes.SET_LAST_COMMIT_DATA,
- {
- entry,
- lastCommit,
- },
- { root: true },
- );
-
- eventHub.$emit(`editor.update.model.content.${entry.path}`, entry.content);
+ rootState.stagedFiles.forEach(file => {
+ const changedFile = rootState.changedFiles.find(f => f.path === file.path);
commit(
- rootTypes.SET_FILE_RAW_DATA,
+ rootTypes.UPDATE_FILE_AFTER_COMMIT,
{
- file: entry,
- raw: entry.content,
+ file,
+ lastCommit,
},
{ root: true },
);
@@ -122,16 +113,21 @@ export const updateFilesAfterCommit = (
commit(
rootTypes.TOGGLE_FILE_CHANGED,
{
- file: entry,
+ file,
changed: false,
},
{ root: true },
);
- });
- commit(rootTypes.REMOVE_ALL_CHANGES_FILES, null, { root: true });
+ dispatch('updateTempFlagForEntry', { file, tempFile: false }, { root: true });
- if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
+ eventHub.$emit(`editor.update.model.content.${file.key}`, {
+ content: file.content,
+ changed: !!changedFile,
+ });
+ });
+
+ if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH && rootGetters.activeFile) {
router.push(
`/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`,
);
@@ -184,6 +180,8 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
{ root: true },
);
}
+
+ commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
})
.then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH));
})
@@ -198,3 +196,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
commit(types.UPDATE_LOADING, false);
});
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index f7cdd6adb0c..d01060201f2 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -1,12 +1,17 @@
import * as consts from './constants';
-export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading;
+const BRANCH_SUFFIX_COUNT = 5;
+
+export const discardDraftButtonDisabled = state =>
+ state.commitMessage === '' || state.submitCommitLoading;
export const commitButtonDisabled = (state, getters, rootState) =>
- getters.discardDraftButtonDisabled || !rootState.changedFiles.length;
+ getters.discardDraftButtonDisabled || !rootState.stagedFiles.length;
export const newBranchName = (state, _, rootState) =>
- `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(-5)}`;
+ `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(
+ -BRANCH_SUFFIX_COUNT,
+ )}`;
export const branchName = (state, getters, rootState) => {
if (
@@ -22,3 +27,6 @@ export const branchName = (state, getters, rootState) => {
return rootState.currentBranchId;
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index e3f504e5ab0..f5c12db6db0 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -51,5 +51,13 @@ export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
+export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES';
+export const STAGE_CHANGE = 'STAGE_CHANGE';
+export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
+
+export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
+
+export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
+export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 5e5eb831662..0c1d720df09 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -4,6 +4,7 @@ import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
+import { sortTree } from './utils';
export default {
[types.SET_INITIAL_DATA](state, data) {
@@ -49,6 +50,11 @@ export default {
lastCommitMsg,
});
},
+ [types.CLEAR_STAGED_CHANGES](state) {
+ Object.assign(state, {
+ stagedFiles: [],
+ });
+ },
[types.SET_ENTRIES](state, entries) {
Object.assign(state, {
entries,
@@ -68,7 +74,7 @@ export default {
f => foundEntry.tree.find(e => e.path === f.path) === undefined,
);
Object.assign(foundEntry, {
- tree: foundEntry.tree.concat(tree),
+ tree: sortTree(foundEntry.tree.concat(tree)),
});
}
@@ -81,10 +87,16 @@ export default {
if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], {
- tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
+ tree: sortTree(state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList)),
});
}
},
+ [types.UPDATE_TEMP_FLAG](state, { path, tempFile }) {
+ Object.assign(state.entries[path], {
+ tempFile,
+ changed: tempFile,
+ });
+ },
[types.UPDATE_VIEWER](state, viewer) {
Object.assign(state, {
viewer,
@@ -95,6 +107,27 @@ export default {
delayViewerUpdated,
});
},
+ [types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
+ Object.assign(state, {
+ fileFindVisible,
+ });
+ },
+ [types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
+ const changedFile = state.changedFiles.find(f => f.path === file.path);
+
+ Object.assign(state.entries[file.path], {
+ raw: file.content,
+ changed: !!changedFile,
+ staged: false,
+ lastCommit: Object.assign(state.entries[file.path].lastCommit, {
+ id: lastCommit.commit.id,
+ url: lastCommit.commit_path,
+ message: lastCommit.commit.message,
+ author: lastCommit.commit.author_name,
+ updatedAt: lastCommit.commit.authored_date,
+ }),
+ });
+ },
...projectMutations,
...mergeRequestMutation,
...fileMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index eeb14b5490c..c3041c77199 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -4,6 +4,7 @@ export default {
[types.SET_FILE_ACTIVE](state, { path, active }) {
Object.assign(state.entries[path], {
active,
+ lastOpenedAt: new Date().getTime(),
});
if (active && !state.entries[path].pending) {
@@ -57,7 +58,9 @@ export default {
});
},
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
- const changed = content !== state.entries[path].raw;
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+ const rawContent = stagedFile ? stagedFile.content : state.entries[path].raw;
+ const changed = content !== rawContent;
Object.assign(state.entries[path], {
content,
@@ -91,8 +94,10 @@ export default {
});
},
[types.DISCARD_FILE_CHANGES](state, path) {
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
Object.assign(state.entries[path], {
- content: state.entries[path].raw,
+ content: stagedFile ? stagedFile.content : state.entries[path].raw,
changed: false,
});
},
@@ -106,16 +111,67 @@ export default {
changedFiles: state.changedFiles.filter(f => f.path !== path),
});
},
+ [types.STAGE_CHANGE](state, path) {
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
+ Object.assign(state, {
+ changedFiles: state.changedFiles.filter(f => f.path !== path),
+ entries: Object.assign(state.entries, {
+ [path]: Object.assign(state.entries[path], {
+ staged: true,
+ changed: false,
+ }),
+ }),
+ });
+
+ if (stagedFile) {
+ Object.assign(stagedFile, {
+ ...state.entries[path],
+ });
+ } else {
+ Object.assign(state, {
+ stagedFiles: state.stagedFiles.concat({
+ ...state.entries[path],
+ }),
+ });
+ }
+ },
+ [types.UNSTAGE_CHANGE](state, path) {
+ const changedFile = state.changedFiles.find(f => f.path === path);
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
+ if (!changedFile && stagedFile) {
+ Object.assign(state.entries[path], {
+ ...stagedFile,
+ key: state.entries[path].key,
+ active: state.entries[path].active,
+ opened: state.entries[path].opened,
+ changed: true,
+ });
+
+ Object.assign(state, {
+ changedFiles: state.changedFiles.concat(state.entries[path]),
+ });
+ }
+
+ Object.assign(state, {
+ stagedFiles: state.stagedFiles.filter(f => f.path !== path),
+ entries: Object.assign(state.entries, {
+ [path]: Object.assign(state.entries[path], {
+ staged: false,
+ }),
+ }),
+ });
+ },
[types.TOGGLE_FILE_CHANGED](state, { file, changed }) {
Object.assign(state.entries[file.path], {
changed,
});
},
[types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
- const pendingTab = state.openFiles.find(f => f.path === file.path && f.pending);
- let openFiles = state.openFiles.map(f =>
- Object.assign(f, { active: f.path === file.path, opened: false }),
- );
+ const key = `${keyPrefix}-${file.key}`;
+ const pendingTab = state.openFiles.find(f => f.key === key && f.pending);
+ let openFiles = state.openFiles.map(f => Object.assign(f, { active: false, opened: false }));
if (!pendingTab) {
const openFile = openFiles.find(f => f.path === file.path);
@@ -126,10 +182,11 @@ export default {
if (f.path === file.path) {
return acc.concat({
...f,
+ content: file.content,
active: true,
pending: true,
opened: true,
- key: `${keyPrefix}-${f.key}`,
+ key,
});
}
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index e5cc8814000..3470bb9aec0 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -3,6 +3,7 @@ export default () => ({
currentBranchId: '',
currentMergeRequestId: '',
changedFiles: [],
+ stagedFiles: [],
endpoints: {},
lastCommitMsg: '',
lastCommitPath: '',
@@ -17,4 +18,5 @@ export default () => ({
entries: {},
viewer: 'editor',
delayViewerUpdated: false,
+ fileFindVisible: false,
});
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 05a019de54f..59185f8f0ad 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -15,6 +15,7 @@ export const dataStructure = () => ({
opened: false,
active: false,
changed: false,
+ staged: false,
lastCommitPath: '',
lastCommit: {
id: '',
@@ -32,6 +33,7 @@ export const dataStructure = () => ({
raw: '',
content: '',
parentTreeUrl: '',
+ parentPath: '',
renderError: false,
base64: false,
editorRow: 1,
@@ -41,6 +43,7 @@ export const dataStructure = () => ({
viewMode: 'edit',
previewMode: null,
size: 0,
+ lastOpenedAt: 0,
});
export const decorateData = entity => {
@@ -63,6 +66,7 @@ export const decorateData = entity => {
previewMode,
file_lock,
html,
+ parentPath = '',
} = entity;
return {
@@ -79,6 +83,7 @@ export const decorateData = entity => {
opened,
active,
parentTreeUrl,
+ parentPath,
changed,
renderError,
content,
@@ -101,7 +106,7 @@ export const setPageTitle = title => {
export const createCommitPayload = (branch, newBranch, state, rootState) => ({
branch,
commit_message: state.commitMessage,
- actions: rootState.changedFiles.map(f => ({
+ actions: rootState.stagedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.content,
@@ -119,8 +124,8 @@ const sortTreesByTypeAndName = (a, b) => {
} else if (a.type === 'blob' && b.type === 'tree') {
return 1;
}
- if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
- if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
+ if (a.name < b.name) return -1;
+ if (a.name > b.name) return 1;
return 0;
};
diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
index a1673276900..d249b05f47c 100644
--- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
+++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
@@ -6,6 +6,7 @@ self.addEventListener('message', e => {
const treeList = [];
let file;
+ let parentPath;
const entries = data.reduce((acc, path) => {
const pathSplit = path.split('/');
const blobName = pathSplit.pop().trim();
@@ -17,6 +18,8 @@ self.addEventListener('message', e => {
const foundEntry = acc[folderPath];
if (!foundEntry) {
+ parentPath = parentFolder ? parentFolder.path : null;
+
const tree = decorateData({
projectId,
branchId,
@@ -29,6 +32,7 @@ self.addEventListener('message', e => {
tempFile,
changed: tempFile,
opened: tempFile,
+ parentPath,
});
Object.assign(acc, {
@@ -52,6 +56,8 @@ self.addEventListener('message', e => {
if (blobName !== '') {
const fileFolder = acc[pathSplit.join('/')];
+ parentPath = fileFolder ? fileFolder.path : null;
+
file = decorateData({
projectId,
branchId,
@@ -66,6 +72,7 @@ self.addEventListener('message', e => {
content,
base64,
previewMode: viewerInformationForPath(blobName),
+ parentPath,
});
Object.assign(acc, {
@@ -86,5 +93,6 @@ self.addEventListener('message', e => {
entries,
treeList: sortTree(treeList),
file,
+ parentPath,
});
});
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 7470d634b99..f3d722409b0 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -30,10 +30,10 @@ export default class IssuableContext {
const $selectbox = $block.find('.selectbox');
if ($selectbox.is(':visible')) {
$selectbox.hide();
- $block.find('.value').show();
+ $block.find('.value:not(.dont-hide)').show();
} else {
$selectbox.show();
- $block.find('.value').hide();
+ $block.find('.value:not(.dont-hide)').hide();
}
if ($selectbox.is(':visible')) {
diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue
index 357bc9aab17..21b545d6cab 100644
--- a/app/assets/javascripts/jobs/components/header.vue
+++ b/app/assets/javascripts/jobs/components/header.vue
@@ -1,82 +1,94 @@
<script>
- import ciHeader from '../../vue_shared/components/header_ci_component.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import ciHeader from '../../vue_shared/components/header_ci_component.vue';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import callout from '../../vue_shared/components/callout.vue';
- export default {
- name: 'JobHeaderSection',
- components: {
- ciHeader,
- loadingIcon,
+export default {
+ name: 'JobHeaderSection',
+ components: {
+ ciHeader,
+ loadingIcon,
+ callout,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
},
- props: {
- job: {
- type: Object,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
+ isLoading: {
+ type: Boolean,
+ required: true,
},
- data() {
- return {
- actions: this.getActions(),
- };
+ },
+ data() {
+ return {
+ actions: this.getActions(),
+ };
+ },
+ computed: {
+ status() {
+ return this.job && this.job.status;
},
- computed: {
- status() {
- return this.job && this.job.status;
- },
- shouldRenderContent() {
- return !this.isLoading && Object.keys(this.job).length;
- },
- /**
- * When job has not started the key will be `false`
- * When job started the key will be a string with a date.
- */
- jobStarted() {
- return !this.job.started === false;
- },
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.job).length;
},
- watch: {
- job() {
- this.actions = this.getActions();
- },
+ shouldRenderReason() {
+ return !!(this.job.status && this.job.callout_message);
},
- methods: {
- getActions() {
- const actions = [];
+ /**
+ * When job has not started the key will be `false`
+ * When job started the key will be a string with a date.
+ */
+ jobStarted() {
+ return !this.job.started === false;
+ },
+ },
+ watch: {
+ job() {
+ this.actions = this.getActions();
+ },
+ },
+ methods: {
+ getActions() {
+ const actions = [];
- if (this.job.new_issue_path) {
- actions.push({
- label: 'New issue',
- path: this.job.new_issue_path,
- cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
- type: 'link',
- });
- }
- return actions;
- },
+ if (this.job.new_issue_path) {
+ actions.push({
+ label: 'New issue',
+ path: this.job.new_issue_path,
+ cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
+ type: 'link',
+ });
+ }
+ return actions;
},
- };
+ },
+};
</script>
<template>
- <div class="js-build-header build-header top-area">
- <ci-header
- v-if="shouldRenderContent"
- :status="status"
- item-name="Job"
- :item-id="job.id"
- :time="job.created_at"
- :user="job.user"
- :actions="actions"
- :has-sidebar-button="true"
- :should-render-triggered-label="jobStarted"
- />
- <loading-icon
- v-if="isLoading"
- size="2"
- class="prepend-top-default append-bottom-default"
+ <header>
+ <div class="js-build-header build-header top-area">
+ <ci-header
+ v-if="shouldRenderContent"
+ :status="status"
+ item-name="Job"
+ :item-id="job.id"
+ :time="job.created_at"
+ :user="job.user"
+ :actions="actions"
+ :has-sidebar-button="true"
+ :should-render-triggered-label="jobStarted"
+ />
+ <loading-icon
+ v-if="isLoading"
+ size="2"
+ class="prepend-top-default append-bottom-default"
+ />
+ </div>
+
+ <callout
+ v-if="shouldRenderReason"
+ :message="job.callout_message"
/>
- </div>
+ </header>
</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index af47056d98f..db19dc9b238 100644
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -1,80 +1,119 @@
<script>
- import detailRow from './sidebar_detail_row.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import timeagoMixin from '../../vue_shared/mixins/timeago';
- import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
+import detailRow from './sidebar_detail_row.vue';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import timeagoMixin from '../../vue_shared/mixins/timeago';
+import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
- export default {
- name: 'SidebarDetailsBlock',
- components: {
- detailRow,
- loadingIcon,
+export default {
+ name: 'SidebarDetailsBlock',
+ components: {
+ detailRow,
+ loadingIcon,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ job: {
+ type: Object,
+ required: true,
},
- mixins: [
- timeagoMixin,
- ],
- props: {
- job: {
- type: Object,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
- runnerHelpUrl: {
- type: String,
- required: false,
- default: '',
- },
+ isLoading: {
+ type: Boolean,
+ required: true,
},
- computed: {
- shouldRenderContent() {
- return !this.isLoading && Object.keys(this.job).length > 0;
- },
- coverage() {
- return `${this.job.coverage}%`;
- },
- duration() {
- return timeIntervalInWords(this.job.duration);
- },
- queued() {
- return timeIntervalInWords(this.job.queued);
- },
- runnerId() {
- return `#${this.job.runner.id}`;
- },
- hasTimeout() {
- return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
- },
- timeout() {
- if (this.job.metadata == null) {
- return '';
- }
+ canUserRetry: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ runnerHelpUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.job).length > 0;
+ },
+ coverage() {
+ return `${this.job.coverage}%`;
+ },
+ duration() {
+ return timeIntervalInWords(this.job.duration);
+ },
+ queued() {
+ return timeIntervalInWords(this.job.queued);
+ },
+ runnerId() {
+ return `${this.job.runner.description} (#${this.job.runner.id})`;
+ },
+ retryButtonClass() {
+ let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block';
+ className +=
+ this.job.status && this.job.recoverable
+ ? ' btn-primary'
+ : ' btn-inverted-secondary';
+ return className;
+ },
+ hasTimeout() {
+ return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
+ },
+ timeout() {
+ if (this.job.metadata == null) {
+ return '';
+ }
- let t = this.job.metadata.timeout_human_readable;
- if (this.job.metadata.timeout_source !== '') {
- t += ` (from ${this.job.metadata.timeout_source})`;
- }
+ let t = this.job.metadata.timeout_human_readable;
+ if (this.job.metadata.timeout_source !== '') {
+ t += ` (from ${this.job.metadata.timeout_source})`;
+ }
- return t;
- },
- renderBlock() {
- return this.job.merge_request ||
- this.job.duration ||
- this.job.finished_data ||
- this.job.erased_at ||
- this.job.queued ||
- this.job.runner ||
- this.job.coverage ||
- this.job.tags.length ||
- this.job.cancel_path;
- },
+ return t;
},
- };
+ renderBlock() {
+ return (
+ this.job.merge_request ||
+ this.job.duration ||
+ this.job.finished_data ||
+ this.job.erased_at ||
+ this.job.queued ||
+ this.job.runner ||
+ this.job.coverage ||
+ this.job.tags.length ||
+ this.job.cancel_path
+ );
+ },
+ },
+};
</script>
<template>
<div>
+ <div class="block">
+ <strong class="inline prepend-top-8">
+ {{ job.name }}
+ </strong>
+ <a
+ v-if="canUserRetry"
+ :class="retryButtonClass"
+ :href="job.retry_path"
+ data-method="post"
+ rel="nofollow"
+ >
+ {{ __('Retry') }}
+ </a>
+ <button
+ type="button"
+ :aria-label="__('Toggle Sidebar')"
+ class="btn btn-blank gutter-toggle pull-right
+ visible-xs-block visible-sm-block js-sidebar-build-toggle"
+ >
+ <i
+ aria-hidden="true"
+ data-hidden="true"
+ class="fa fa-angle-double-right"
+ ></i>
+ </button>
+ </div>
<template v-if="shouldRenderContent">
<div
class="block retry-link"
@@ -85,16 +124,16 @@
class="js-new-issue btn btn-new btn-inverted"
:href="job.new_issue_path"
>
- New issue
+ {{ __('New issue') }}
</a>
<a
- v-if="job.retry_path"
+ v-if="canUserRetry"
class="js-retry-job btn btn-inverted-secondary"
:href="job.retry_path"
data-method="post"
rel="nofollow"
>
- Retry
+ {{ __('Retry') }}
</a>
</div>
<div :class="{block : renderBlock }">
@@ -103,7 +142,7 @@
v-if="job.merge_request"
>
<span class="build-light-text">
- Merge Request:
+ {{ __('Merge Request:') }}
</span>
<a :href="job.merge_request.path">
!{{ job.merge_request.iid }}
@@ -158,7 +197,7 @@
v-if="job.tags.length"
>
<span class="build-light-text">
- Tags:
+ {{ __('Tags:') }}
</span>
<span
v-for="(tag, i) in job.tags"
@@ -178,7 +217,7 @@
data-method="post"
rel="nofollow"
>
- Cancel
+ {{ __('Cancel') }}
</a>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
index 656676ead91..f2939ad4dbe 100644
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -35,9 +35,11 @@ export default () => {
});
// Sidebar information block
+ const detailsBlockElement = document.getElementById('js-details-block-vue');
+ const detailsBlockDataset = detailsBlockElement.dataset;
// eslint-disable-next-line
new Vue({
- el: '#js-details-block-vue',
+ el: detailsBlockElement,
components: {
detailsBlock,
},
@@ -50,6 +52,7 @@ export default () => {
return createElement('details-block', {
props: {
isLoading: this.mediator.state.isLoading,
+ canUserRetry: !!('canUserRetry' in detailsBlockDataset),
job: this.mediator.store.state.job,
runnerHelpUrl: dataset.runnerHelpUrl,
},
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index d0050abb8e9..9b62cfb8206 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -83,7 +83,7 @@ export default class LabelsSelect {
$dropdown.trigger('loading.gl.dropdown');
axios.put(issueUpdateURL, data)
.then(({ data }) => {
- var labelCount, template, labelTooltipTitle, labelTitles;
+ var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
@@ -115,8 +115,7 @@ export default class LabelsSelect {
labelTooltipTitle = labelTitles.join(', ');
}
else {
- labelTooltipTitle = '';
- $sidebarLabelTooltip.tooltip('destroy');
+ labelTooltipTitle = __('Labels');
}
$sidebarLabelTooltip
diff --git a/app/assets/javascripts/lib/utils/keycodes.js b/app/assets/javascripts/lib/utils/keycodes.js
new file mode 100644
index 00000000000..5e0f9b612a2
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/keycodes.js
@@ -0,0 +1,4 @@
+export const UP_KEY_CODE = 38;
+export const DOWN_KEY_CODE = 40;
+export const ENTER_KEY_CODE = 13;
+export const ESC_KEY_CODE = 27;
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index d0a2b27b0e6..f8b3d3061f0 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -4,6 +4,7 @@
import $ from 'jquery';
import _ from 'underscore';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store';
@@ -11,7 +12,8 @@ import ModalStore from './boards/stores/modal_store';
export default class MilestoneSelect {
constructor(currentProject, els, options = {}) {
if (currentProject !== null) {
- this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
+ this.currentProject =
+ typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
}
this.init(els, options);
@@ -25,7 +27,10 @@ export default class MilestoneSelect {
}
$els.each((i, dropdown) => {
- let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
+ let milestoneLinkNoneTemplate,
+ milestoneLinkTemplate,
+ selectedMilestone,
+ selectedMilestoneDefault;
const $dropdown = $(dropdown);
const projectId = $dropdown.data('projectId');
const milestonesUrl = $dropdown.data('milestones');
@@ -45,46 +50,47 @@ export default class MilestoneSelect {
const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
const $value = $block.find('.value');
const $loading = $block.find('.block-loading').fadeOut();
- selectedMilestoneDefault = (showAny ? '' : null);
- selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault);
+ selectedMilestoneDefault = showAny ? '' : null;
+ selectedMilestoneDefault = showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault;
selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
if (issueUpdateURL) {
- milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
+ milestoneLinkTemplate = _.template(
+ '<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
+ );
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
- collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
- data: (term, callback) => axios.get(milestonesUrl)
- .then(({ data }) => {
+ data: (term, callback) =>
+ axios.get(milestonesUrl).then(({ data }) => {
const extraOptions = [];
if (showAny) {
extraOptions.push({
- id: 0,
- name: '',
- title: 'Any Milestone'
+ id: null,
+ name: null,
+ title: 'Any Milestone',
});
}
if (showNo) {
extraOptions.push({
id: -1,
name: 'No Milestone',
- title: 'No Milestone'
+ title: 'No Milestone',
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
- title: 'Upcoming'
+ title: 'Upcoming',
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
- title: 'Started'
+ title: 'Started',
});
}
if (extraOptions.length) {
@@ -106,7 +112,7 @@ export default class MilestoneSelect {
`,
filterable: true,
search: {
- fields: ['title']
+ fields: ['title'],
},
selectable: true,
toggleLabel: (selected, el, e) => {
@@ -119,7 +125,7 @@ export default class MilestoneSelect {
defaultLabel: defaultLabel,
fieldName: $dropdown.data('fieldName'),
text: milestone => _.escape(milestone.title),
- id: (milestone) => {
+ id: milestone => {
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name;
} else {
@@ -131,7 +137,7 @@ export default class MilestoneSelect {
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
- opened: (e) => {
+ opened: e => {
const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
@@ -140,7 +146,7 @@ export default class MilestoneSelect {
$(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: (clickEvent) => {
+ clicked: clickEvent => {
const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj;
@@ -155,11 +161,14 @@ export default class MilestoneSelect {
const page = $('body').attr('data-page');
const isIssueIndex = page === 'projects:issues:index';
- const isMRIndex = (page === page && page === 'projects:merge_requests:index');
- const isSelecting = (selected.name !== selectedMilestone);
+ const isMRIndex = page === page && page === 'projects:merge_requests:index';
+ const isSelecting = selected.name !== selectedMilestone;
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
- if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+ if (
+ $dropdown.hasClass('js-filter-bulk-update') ||
+ $dropdown.hasClass('js-issuable-form-dropdown')
+ ) {
e.preventDefault();
return;
}
@@ -177,10 +186,13 @@ export default class MilestoneSelect {
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (selected.id !== -1 && isSelecting) {
- gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
- id: selected.id,
- title: selected.name
- }));
+ gl.issueBoards.boardStoreIssueSet(
+ 'milestone',
+ new ListMilestone({
+ id: selected.id,
+ title: selected.name,
+ }),
+ );
} else {
gl.issueBoards.boardStoreIssueDelete('milestone');
}
@@ -188,7 +200,8 @@ export default class MilestoneSelect {
$dropdown.trigger('loading.gl.dropdown');
$loading.removeClass('hidden').fadeIn();
- gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ gl.issueBoards.BoardsStore.detail.issue
+ .update($dropdown.attr('data-issue-update'))
.then(() => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
@@ -203,7 +216,8 @@ export default class MilestoneSelect {
data[abilityName].milestone_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
- return axios.put(issueUpdateURL, data)
+ return axios
+ .put(issueUpdateURL, data)
.then(({ data }) => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
@@ -214,17 +228,26 @@ export default class MilestoneSelect {
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
- return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
+ return $sidebarCollapsedValue
+ .attr(
+ 'data-original-title',
+ `${data.milestone.name}<br />${data.milestone.remaining}`,
+ )
+ .find('span')
+ .text(data.milestone.title);
} else {
$value.html(milestoneLinkNoneTemplate);
- return $sidebarCollapsedValue.find('span').text('No');
+ return $sidebarCollapsedValue
+ .attr('data-original-title', __('Milestone'))
+ .find('span')
+ .text(__('None'));
}
})
.catch(() => {
$loading.fadeOut();
});
}
- }
+ },
});
});
}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 2121907dff0..96f2b3eac98 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1427,7 +1427,7 @@ export default class Notes {
const { discussion_html } = data;
const lines = $(discussion_html).find('.line_holder');
lines.addClass('fade-in');
- $container.find('tbody').prepend(lines);
+ $container.find('.diff-content > table > tbody').prepend(lines);
const fileHolder = $container.find('.file-holder');
$container.find('.line-holder-placeholder').remove();
syntaxHighlight(fileHolder);
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 244a6980b5a..98ce070288e 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -315,3 +315,6 @@ export const scrollToNoteIfNeeded = (context, el) => {
scrollToElement(el);
}
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index f89591a54d6..787be6f4c99 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -68,3 +68,6 @@ export const resolvedDiscussionCount = (state, getters) => {
return Object.keys(resolvedMap).length;
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index 0e3ac636661..9ce176744ba 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -52,16 +52,15 @@
text() {
const keepContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}.
- This will delete all of the issues, merge requests, and groups linked to them.
+ Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user".
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
const deleteContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}.
- Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user".
+ This will delete all of the issues, merge requests, and groups linked to them.
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
-
return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText,
{
username: `<strong>${_.escape(this.username)}</strong>`,
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 8ce938c958b..50d042fef29 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -19,7 +19,7 @@ function getSystemDate(systemUtcOffsetSeconds) {
const date = new Date();
const localUtcOffsetMinutes = 0 - date.getTimezoneOffset();
const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60;
- date.setMinutes((date.getMinutes() - localUtcOffsetMinutes) + systemUtcOffsetMinutes);
+ date.setMinutes(date.getMinutes() - localUtcOffsetMinutes + systemUtcOffsetMinutes);
return date;
}
@@ -35,18 +35,36 @@ function formatTooltipText({ date, count }) {
return `${contribText}<br />${dateDayName} ${dateText}`;
}
-const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]);
+const initColorKey = () =>
+ d3
+ .scaleLinear()
+ .range(['#acd5f2', '#254e77'])
+ .domain([0, 3]);
export default class ActivityCalendar {
- constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) {
+ constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0, firstDayOfWeek = 0) {
this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = '';
this.daySpace = 1;
this.daySize = 15;
- this.daySizeWithSpace = this.daySize + (this.daySpace * 2);
- this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+ this.daySizeWithSpace = this.daySize + this.daySpace * 2;
+ this.monthNames = [
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Oct',
+ 'Nov',
+ 'Dec',
+ ];
this.months = [];
+ this.firstDayOfWeek = firstDayOfWeek;
// Loop through the timestamps to create a group of objects
// The group of objects will be grouped based on the day of the week they are
@@ -70,7 +88,7 @@ export default class ActivityCalendar {
// Create a new group array if this is the first day of the week
// or if is first object
- if ((day === 0 && i !== 0) || i === 0) {
+ if ((day === this.firstDayOfWeek && i !== 0) || i === 0) {
this.timestampsTmp.push([]);
group += 1;
}
@@ -109,21 +127,30 @@ export default class ActivityCalendar {
}
renderSvg(container, group) {
- const width = ((group + 1) * this.daySizeWithSpace) + this.getExtraWidthPadding(group);
- return d3.select(container)
+ const width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
+ return d3
+ .select(container)
.append('svg')
- .attr('width', width)
- .attr('height', 167)
- .attr('class', 'contrib-calendar');
+ .attr('width', width)
+ .attr('height', 167)
+ .attr('class', 'contrib-calendar');
+ }
+
+ dayYPos(day) {
+ return this.daySizeWithSpace * ((day + 7 - this.firstDayOfWeek) % 7);
}
renderDays() {
- this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g')
+ this.svg
+ .selectAll('g')
+ .data(this.timestampsTmp)
+ .enter()
+ .append('g')
.attr('transform', (group, i) => {
_.each(group, (stamp, a) => {
if (a === 0 && stamp.day === 0) {
const month = stamp.date.getMonth();
- const x = (this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace;
+ const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace;
const lastMonth = _.last(this.months);
if (
lastMonth == null ||
@@ -133,86 +160,113 @@ export default class ActivityCalendar {
}
}
});
- return `translate(${(this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace}, 18)`;
+ return `translate(${this.daySizeWithSpace * i + 1 + this.daySizeWithSpace}, 18)`;
})
.selectAll('rect')
- .data(stamp => stamp)
- .enter()
- .append('rect')
- .attr('x', '0')
- .attr('y', stamp => this.daySizeWithSpace * stamp.day)
- .attr('width', this.daySize)
- .attr('height', this.daySize)
- .attr('fill', stamp => (
- stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'
- ))
- .attr('title', stamp => formatTooltipText(stamp))
- .attr('class', 'user-contrib-cell js-tooltip')
- .attr('data-container', 'body')
- .on('click', this.clickDay);
+ .data(stamp => stamp)
+ .enter()
+ .append('rect')
+ .attr('x', '0')
+ .attr('y', stamp => this.dayYPos(stamp.day))
+ .attr('width', this.daySize)
+ .attr('height', this.daySize)
+ .attr(
+ 'fill',
+ stamp => (stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'),
+ )
+ .attr('title', stamp => formatTooltipText(stamp))
+ .attr('class', 'user-contrib-cell js-tooltip')
+ .attr('data-container', 'body')
+ .on('click', this.clickDay);
}
renderDayTitles() {
const days = [
{
text: 'M',
- y: 29 + (this.daySizeWithSpace * 1),
- }, {
+ y: 29 + this.dayYPos(1),
+ },
+ {
text: 'W',
- y: 29 + (this.daySizeWithSpace * 3),
- }, {
+ y: 29 + this.dayYPos(3),
+ },
+ {
text: 'F',
- y: 29 + (this.daySizeWithSpace * 5),
+ y: 29 + this.dayYPos(5),
},
];
- this.svg.append('g')
+ this.svg
+ .append('g')
.selectAll('text')
- .data(days)
- .enter()
- .append('text')
- .attr('text-anchor', 'middle')
- .attr('x', 8)
- .attr('y', day => day.y)
- .text(day => day.text)
- .attr('class', 'user-contrib-text');
+ .data(days)
+ .enter()
+ .append('text')
+ .attr('text-anchor', 'middle')
+ .attr('x', 8)
+ .attr('y', day => day.y)
+ .text(day => day.text)
+ .attr('class', 'user-contrib-text');
}
renderMonths() {
- this.svg.append('g')
+ this.svg
+ .append('g')
.attr('direction', 'ltr')
.selectAll('text')
- .data(this.months)
- .enter()
- .append('text')
- .attr('x', date => date.x)
- .attr('y', 10)
- .attr('class', 'user-contrib-text')
- .text(date => this.monthNames[date.month]);
+ .data(this.months)
+ .enter()
+ .append('text')
+ .attr('x', date => date.x)
+ .attr('y', 10)
+ .attr('class', 'user-contrib-text')
+ .text(date => this.monthNames[date.month]);
}
renderKey() {
- const keyValues = ['no contributions', '1-9 contributions', '10-19 contributions', '20-29 contributions', '30+ contributions'];
- const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
+ const keyValues = [
+ 'no contributions',
+ '1-9 contributions',
+ '10-19 contributions',
+ '20-29 contributions',
+ '30+ contributions',
+ ];
+ const keyColors = [
+ '#ededed',
+ this.colorKey(0),
+ this.colorKey(1),
+ this.colorKey(2),
+ this.colorKey(3),
+ ];
- this.svg.append('g')
- .attr('transform', `translate(18, ${(this.daySizeWithSpace * 8) + 16})`)
+ this.svg
+ .append('g')
+ .attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
.selectAll('rect')
- .data(keyColors)
- .enter()
- .append('rect')
- .attr('width', this.daySize)
- .attr('height', this.daySize)
- .attr('x', (color, i) => this.daySizeWithSpace * i)
- .attr('y', 0)
- .attr('fill', color => color)
- .attr('class', 'js-tooltip')
- .attr('title', (color, i) => keyValues[i])
- .attr('data-container', 'body');
+ .data(keyColors)
+ .enter()
+ .append('rect')
+ .attr('width', this.daySize)
+ .attr('height', this.daySize)
+ .attr('x', (color, i) => this.daySizeWithSpace * i)
+ .attr('y', 0)
+ .attr('fill', color => color)
+ .attr('class', 'js-tooltip')
+ .attr('title', (color, i) => keyValues[i])
+ .attr('data-container', 'body');
}
initColor() {
- const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
- return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange);
+ const colorRange = [
+ '#ededed',
+ this.colorKey(0),
+ this.colorKey(1),
+ this.colorKey(2),
+ this.colorKey(3),
+ ];
+ return d3
+ .scaleThreshold()
+ .domain([0, 10, 20, 30])
+ .range(colorRange);
}
clickDay(stamp) {
@@ -227,14 +281,15 @@ export default class ActivityCalendar {
$('.user-calendar-activities').html(LOADING_HTML);
- axios.get(this.calendarActivitiesPath, {
- params: {
- date,
- },
- responseType: 'text',
- })
- .then(({ data }) => $('.user-calendar-activities').html(data))
- .catch(() => flash(__('An error occurred while retrieving calendar activity')));
+ axios
+ .get(this.calendarActivitiesPath, {
+ params: {
+ date,
+ },
+ responseType: 'text',
+ })
+ .then(({ data }) => $('.user-calendar-activities').html(data))
+ .catch(() => flash(__('An error occurred while retrieving calendar activity')));
} else {
this.currentSelectedDate = '';
$('.user-calendar-activities').html('');
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 2fd1715ee79..8ffaa52d9e8 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -5,7 +5,6 @@ import PerformanceBarService from '../services/performance_bar_service';
import detailedMetric from './detailed_metric.vue';
import requestSelector from './request_selector.vue';
import simpleMetric from './simple_metric.vue';
-import upstreamPerformanceBar from './upstream_performance_bar.vue';
import Flash from '../../flash';
@@ -14,7 +13,6 @@ export default {
detailedMetric,
requestSelector,
simpleMetric,
- upstreamPerformanceBar,
},
props: {
store: {
@@ -128,9 +126,6 @@ export default {
{{ currentRequest.details.host.hostname }}
</span>
</div>
- <upstream-performance-bar
- v-if="initialRequest && currentRequest.details"
- />
<detailed-metric
v-for="metric in $options.detailedMetrics"
:key="metric.metric"
diff --git a/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue b/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue
deleted file mode 100644
index 2b5915f381f..00000000000
--- a/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-<script>
-export default {
- mounted() {
- const upstreamPerformanceBar = document
- .getElementById('peek-view-performance-bar')
- .cloneNode(true);
-
- upstreamPerformanceBar.classList.remove('hidden');
-
- this.$refs.wrapper.appendChild(upstreamPerformanceBar);
- },
-};
-</script>
-<template>
- <div
- id="peek-view-performance-bar-vue"
- class="view"
- ref="wrapper"
- ></div>
-</template>
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index a0ddf36a672..4a98aed7679 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -1,5 +1,3 @@
-import 'vendor/peek.performance_bar';
-
import Vue from 'vue';
import performanceBarApp from './components/performance_bar_app.vue';
import PerformanceBarStore from './stores/performance_bar_store';
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index e99d949801f..29ee73a2a6f 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -32,26 +32,38 @@ export default {
required: true,
},
- buttonDisabled: {
+ requestFinishedFor: {
type: String,
required: false,
- default: null,
+ default: '',
},
},
+ data() {
+ return {
+ isDisabled: false,
+ linkRequested: '',
+ };
+ },
+
computed: {
cssClass() {
const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`;
},
- isDisabled() {
- return this.buttonDisabled === this.link;
+ },
+ watch: {
+ requestFinishedFor() {
+ if (this.requestFinishedFor === this.linkRequested) {
+ this.isDisabled = false;
+ }
},
},
-
methods: {
onClickAction() {
$(this.$el).tooltip('hide');
eventHub.$emit('graphAction', this.link);
+ this.linkRequested = this.link;
+ this.isDisabled = true;
},
},
};
@@ -62,7 +74,8 @@ export default {
@click="onClickAction"
v-tooltip
:title="tooltipText"
- class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper"
+ class="js-ci-action btn btn-blank
+btn-transparent ci-action-icon-container ci-action-icon-wrapper"
:class="cssClass"
data-container="body"
:disabled="isDisabled"
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
deleted file mode 100644
index 7c4fd65e36f..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<script>
- import icon from '../../../vue_shared/components/icon.vue';
- import tooltip from '../../../vue_shared/directives/tooltip';
-
- /**
- * Renders either a cancel, retry or play icon pointing to the given path.
- * TODO: Remove UJS from here and use an async request instead.
- */
- export default {
- components: {
- icon,
- },
-
- directives: {
- tooltip,
- },
- props: {
- tooltipText: {
- type: String,
- required: true,
- },
-
- link: {
- type: String,
- required: true,
- },
-
- actionMethod: {
- type: String,
- required: true,
- },
-
- actionIcon: {
- type: String,
- required: true,
- },
- },
- };
-</script>
-<template>
- <a
- v-tooltip
- :data-method="actionMethod"
- :title="tooltipText"
- :href="link"
- rel="nofollow"
- class="ci-action-icon-wrapper js-ci-status-icon"
- data-container="body"
- aria-label="Job's action"
- >
- <icon :name="actionIcon" />
- </a>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
index be213c2ee78..43121dd38f3 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -1,77 +1,83 @@
<script>
- import $ from 'jquery';
- import jobNameComponent from './job_name_component.vue';
- import jobComponent from './job_component.vue';
- import tooltip from '../../../vue_shared/directives/tooltip';
+import $ from 'jquery';
+import JobNameComponent from './job_name_component.vue';
+import JobComponent from './job_component.vue';
+import tooltip from '../../../vue_shared/directives/tooltip';
- /**
- * Renders the dropdown for the pipeline graph.
- *
- * The following object should be provided as `job`:
- *
- * {
- * "id": 4256,
- * "name": "test",
- * "status": {
- * "icon": "icon_status_success",
- * "text": "passed",
- * "label": "passed",
- * "group": "success",
- * "details_path": "/root/ci-mock/builds/4256",
- * "action": {
- * "icon": "retry",
- * "title": "Retry",
- * "path": "/root/ci-mock/builds/4256/retry",
- * "method": "post"
- * }
- * }
- * }
- */
- export default {
- directives: {
- tooltip,
- },
+/**
+ * Renders the dropdown for the pipeline graph.
+ *
+ * The following object should be provided as `job`:
+ *
+ * {
+ * "id": 4256,
+ * "name": "test",
+ * "status": {
+ * "icon": "icon_status_success",
+ * "text": "passed",
+ * "label": "passed",
+ * "group": "success",
+ * "details_path": "/root/ci-mock/builds/4256",
+ * "action": {
+ * "icon": "retry",
+ * "title": "Retry",
+ * "path": "/root/ci-mock/builds/4256/retry",
+ * "method": "post"
+ * }
+ * }
+ * }
+ */
+export default {
+ directives: {
+ tooltip,
+ },
- components: {
- jobComponent,
- jobNameComponent,
- },
+ components: {
+ JobComponent,
+ JobNameComponent,
+ },
- props: {
- job: {
- type: Object,
- required: true,
- },
+ props: {
+ job: {
+ type: Object,
+ required: true,
},
-
- computed: {
- tooltipText() {
- return `${this.job.name} - ${this.job.status.label}`;
- },
+ requestFinishedFor: {
+ type: String,
+ required: false,
+ default: '',
},
+ },
- mounted() {
- this.stopDropdownClickPropagation();
+ computed: {
+ tooltipText() {
+ return `${this.job.name} - ${this.job.status.label}`;
},
+ },
+
+ mounted() {
+ this.stopDropdownClickPropagation();
+ },
- methods: {
- /**
- * When the user right clicks or cmd/ctrl + click in the job name
- * the dropdown should not be closed and the link should open in another tab,
- * so we stop propagation of the click event inside the dropdown.
+ methods: {
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name or the action icon
+ * the dropdown should not be closed so we stop propagation
+ * of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
- stopDropdownClickPropagation() {
- $(this.$el
- .querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
- .on('click', (e) => {
- e.stopPropagation();
- });
- },
+ stopDropdownClickPropagation() {
+ $(
+ '.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item',
+ this.$el,
+ ).on('click', e => {
+ e.stopPropagation();
+ });
},
- };
+ },
+};
</script>
<template>
<div class="ci-job-dropdown-container">
@@ -101,8 +107,8 @@
:key="i">
<job-component
:job="item"
- :is-dropdown="true"
css-class-job-name="mini-pipeline-graph-dropdown-item"
+ :request-finished-for="requestFinishedFor"
/>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index ac9ce7e47d6..7b8a5edcbff 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -7,7 +7,6 @@ export default {
StageColumnComponent,
LoadingIcon,
},
-
props: {
isLoading: {
type: Boolean,
@@ -17,10 +16,10 @@ export default {
type: Object,
required: true,
},
- actionDisabled: {
+ requestFinishedFor: {
type: String,
required: false,
- default: null,
+ default: '',
},
},
@@ -75,7 +74,7 @@ export default {
:key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
- :action-disabled="actionDisabled"
+ :request-finished-for="requestFinishedFor"
/>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index c6e5ae6df41..4fcd4b79f4a 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -1,6 +1,5 @@
<script>
import ActionComponent from './action_component.vue';
-import DropdownActionComponent from './dropdown_action_component.vue';
import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
@@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip';
export default {
components: {
ActionComponent,
- DropdownActionComponent,
JobNameComponent,
},
-
directives: {
tooltip,
},
@@ -44,26 +41,17 @@ export default {
type: Object,
required: true,
},
-
cssClassJobName: {
type: String,
required: false,
default: '',
},
-
- isDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- actionDisabled: {
+ requestFinishedFor: {
type: String,
required: false,
- default: null,
+ default: '',
},
},
-
computed: {
status() {
return this.job && this.job.status ? this.job.status : {};
@@ -134,19 +122,11 @@ export default {
</div>
<action-component
- v-if="hasAction && !isDropdown"
- :tooltip-text="status.action.title"
- :link="status.action.path"
- :action-icon="status.action.icon"
- :button-disabled="actionDisabled"
- />
-
- <dropdown-action-component
- v-if="hasAction && isDropdown"
+ v-if="hasAction"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
- :action-method="status.action.method"
+ :request-finished-for="requestFinishedFor"
/>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index f6e6569e15b..5461fdbbadd 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -29,10 +29,11 @@ export default {
required: false,
default: '',
},
- actionDisabled: {
+
+ requestFinishedFor: {
type: String,
required: false,
- default: null,
+ default: '',
},
},
@@ -74,12 +75,12 @@ export default {
v-if="job.size === 1"
:job="job"
css-class-job-name="build-content"
- :action-disabled="actionDisabled"
/>
<dropdown-job-component
v-if="job.size > 1"
:job="job"
+ :request-finished-for="requestFinishedFor"
/>
</li>
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 900eb7855f4..6584f96130b 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -25,7 +25,7 @@ export default () => {
data() {
return {
mediator,
- actionDisabled: null,
+ requestFinishedFor: null,
};
},
created() {
@@ -36,15 +36,17 @@ export default () => {
},
methods: {
postAction(action) {
- this.actionDisabled = action;
+ // Click was made, reset this variable
+ this.requestFinishedFor = null;
- this.mediator.service.postAction(action)
+ this.mediator.service
+ .postAction(action)
.then(() => {
this.mediator.refreshPipeline();
- this.actionDisabled = null;
+ this.requestFinishedFor = action;
})
.catch(() => {
- this.actionDisabled = null;
+ this.requestFinishedFor = action;
Flash(__('An error occurred while making the request.'));
});
},
@@ -54,7 +56,7 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
- actionDisabled: this.actionDisabled,
+ requestFinishedFor: this.requestFinishedFor,
},
});
},
@@ -79,7 +81,8 @@ export default () => {
},
methods: {
postAction(action) {
- this.mediator.service.postAction(action.path)
+ this.mediator.service
+ .postAction(action.path)
.then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.')));
},
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
index 795b39bb3dc..593a43c7cc1 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -35,3 +35,6 @@ export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destr
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/stores/getters.js
index 588f479c492..f4923512578 100644
--- a/app/assets/javascripts/registry/stores/getters.js
+++ b/app/assets/javascripts/registry/stores/getters.js
@@ -1,2 +1,5 @@
export const isLoading = state => state.isLoading;
export const repos = state => state.repos;
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 2088a49590a..6eb0b62fa1c 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -5,6 +5,7 @@ import _ from 'underscore';
import Cookies from 'js-cookie';
import flash from './flash';
import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
function Sidebar(currentUser) {
this.toggleTodo = this.toggleTodo.bind(this);
@@ -41,12 +42,14 @@ Sidebar.prototype.addEventListeners = function() {
};
Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
- var $allGutterToggleIcons, $this, $thisIcon;
+ var $allGutterToggleIcons, $this, isExpanded, tooltipLabel;
e.preventDefault();
$this = $(this);
- $thisIcon = $this.find('i');
+ isExpanded = $this.find('i').hasClass('fa-angle-double-right');
+ tooltipLabel = isExpanded ? __('Expand sidebar') : __('Collapse sidebar');
$allGutterToggleIcons = $('.js-sidebar-toggle i');
- if ($thisIcon.hasClass('fa-angle-double-right')) {
+
+ if (isExpanded) {
$allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
$('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
$('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
@@ -57,6 +60,9 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
}
+
+ $this.attr('data-original-title', tooltipLabel);
+
if (!triggered) {
Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
}
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 1e7f46454bf..2d00e8ac7e0 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -1,6 +1,12 @@
<script>
+import { __ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+
export default {
name: 'Assignees',
+ directives: {
+ tooltip,
+ },
props: {
rootPath: {
type: String,
@@ -14,6 +20,11 @@ export default {
type: Boolean,
required: true,
},
+ issuableType: {
+ type: String,
+ require: true,
+ default: 'issue',
+ },
},
data() {
return {
@@ -62,6 +73,12 @@ export default {
names.push(`+ ${this.users.length - maxRender} more`);
}
+ if (!this.users.length) {
+ const emptyTooltipLabel = this.issuableType === 'issue' ?
+ __('Assignee(s)') : __('Assignee');
+ names.push(emptyTooltipLabel);
+ }
+
return names.join(', ');
},
sidebarAvatarCounter() {
@@ -109,7 +126,8 @@ export default {
<div>
<div
class="sidebar-collapsed-icon sidebar-collapsed-user"
- :class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
+ :class="{ 'multiple-users': hasMoreThanOneAssignee }"
+ v-tooltip
data-container="body"
data-placement="left"
:title="collapsedTooltipTitle"
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 3c6b9c27814..b04a2eff798 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -1,9 +1,9 @@
<script>
-import Flash from '../../../flash';
+import Flash from '~/flash';
+import eventHub from '~/sidebar/event_hub';
+import Store from '~/sidebar/stores/sidebar_store';
import AssigneeTitle from './assignee_title.vue';
import Assignees from './assignees.vue';
-import Store from '../../stores/sidebar_store';
-import eventHub from '../../event_hub';
export default {
name: 'SidebarAssignees',
@@ -25,6 +25,11 @@ export default {
required: false,
default: false,
},
+ issuableType: {
+ type: String,
+ require: true,
+ default: 'issue',
+ },
},
data() {
return {
@@ -90,6 +95,7 @@ export default {
:users="store.assignees"
:editable="store.editable"
@assign-self="assignSelf"
+ :issuable-type="issuableType"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index ceb02309959..7f0de722f61 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,15 +1,19 @@
<script>
-import Flash from '../../../flash';
+import { __ } from '~/locale';
+import Flash from '~/flash';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
-import Icon from '../../../vue_shared/components/icon.vue';
-import { __ } from '../../../locale';
-import eventHub from '../../event_hub';
export default {
components: {
editForm,
Icon,
},
+ directives: {
+ tooltip,
+ },
props: {
isConfidential: {
required: true,
@@ -33,6 +37,9 @@ export default {
confidentialityIcon() {
return this.isConfidential ? 'eye-slash' : 'eye';
},
+ tooltipLabel() {
+ return this.isConfidential ? __('Confidential') : __('Not confidential');
+ },
},
created() {
eventHub.$on('closeConfidentialityForm', this.toggleForm);
@@ -65,6 +72,10 @@ export default {
<div
class="sidebar-collapsed-icon"
@click="toggleForm"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="tooltipLabel"
>
<icon
:name="confidentialityIcon"
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index e4893451af3..1a5e7b67eca 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -1,15 +1,22 @@
<script>
+import { __ } from '~/locale';
import Flash from '~/flash';
+import tooltip from '~/vue_shared/directives/tooltip';
+import issuableMixin from '~/vue_shared/mixins/issuable';
+import Icon from '~/vue_shared/components/icon.vue';
+import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
-import issuableMixin from '../../../vue_shared/mixins/issuable';
-import Icon from '../../../vue_shared/components/icon.vue';
-import eventHub from '../../event_hub';
export default {
components: {
editForm,
Icon,
},
+
+ directives: {
+ tooltip,
+ },
+
mixins: [issuableMixin],
props: {
@@ -44,6 +51,10 @@ export default {
isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen;
},
+
+ tooltipLabel() {
+ return this.isLocked ? __('Locked') : __('Unlocked');
+ },
},
created() {
@@ -85,6 +96,10 @@ export default {
<div
class="sidebar-collapsed-icon"
@click="toggleForm"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="tooltipLabel"
>
<icon
:name="lockIcon"
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 006a6d2905d..6d95153af28 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -1,9 +1,13 @@
<script>
- import { __, n__, sprintf } from '../../../locale';
- import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
- import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue';
+ import { __, n__, sprintf } from '~/locale';
+ import tooltip from '~/vue_shared/directives/tooltip';
+ import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+ import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
+ directives: {
+ tooltip,
+ },
components: {
loadingIcon,
userAvatarImage,
@@ -72,7 +76,13 @@
<template>
<div>
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="participantLabel"
+ >
<i
class="fa fa-users"
aria-hidden="true"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index 3b86f1145d1..9d9ee9dea4d 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -1,12 +1,17 @@
<script>
- import icon from '../../../vue_shared/components/icon.vue';
- import { abbreviateTime } from '../../../lib/utils/pretty_time';
+ import { __, sprintf } from '~/locale';
+ import { abbreviateTime } from '~/lib/utils/pretty_time';
+ import icon from '~/vue_shared/components/icon.vue';
+ import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'TimeTrackingCollapsedState',
components: {
icon,
},
+ directives: {
+ tooltip,
+ },
props: {
showComparisonState: {
type: Boolean,
@@ -79,6 +84,21 @@
return '';
},
+ timeTrackedTooltipText() {
+ let title;
+ if (this.showComparisonState) {
+ title = __('Time remaining');
+ } else if (this.showEstimateOnlyState) {
+ title = __('Estimated');
+ } else if (this.showSpentOnlyState) {
+ title = __('Time spent');
+ }
+
+ return sprintf('%{title}: %{text}', ({ title, text: this.text }));
+ },
+ tooltipText() {
+ return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
+ },
},
methods: {
abbreviateTime(timeStr) {
@@ -89,7 +109,13 @@
</script>
<template>
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="tooltipText"
+ >
<icon name="timer" />
<div class="time-tracking-collapsed-summary">
<div :class="divClass">
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index 1eadebc7004..b267422cd97 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import _ from 'underscore';
function isValidProjectId(id) {
return id > 0;
@@ -43,7 +44,7 @@ class SidebarMoveIssue {
renderRow: project => `
<li>
<a href="#" class="js-move-issue-dropdown-item">
- ${project.name_with_namespace}
+ ${_.escape(project.name_with_namespace)}
</a>
</li>
`,
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 9f5d852260e..26eb4cffba3 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -27,6 +27,7 @@ function mountAssigneesComponent(mediator) {
mediator,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
+ issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
},
}),
});
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 520a0b3f424..8486019897d 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -5,6 +5,7 @@
import $ from 'jquery';
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
import ModalStore from './boards/stores/modal_store';
// TODO: remove eventHub hack after code splitting refactor
@@ -182,7 +183,7 @@ function UsersSelect(currentUser, els, options = {}) {
return axios.put(issueURL, data)
.then(({ data }) => {
- var user;
+ var user, tooltipTitle;
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
if (data.assignee) {
@@ -191,15 +192,17 @@ function UsersSelect(currentUser, els, options = {}) {
username: data.assignee.username,
avatar: data.assignee.avatar_url
};
+ tooltipTitle = _.escape(user.name);
} else {
user = {
name: 'Unassigned',
username: '',
avatar: ''
};
+ tooltipTitle = __('Assignee');
}
$value.html(assigneeTemplate(user));
- $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle');
+ $collapsedSidebar.attr('title', tooltipTitle).tooltip('fixTitle');
return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
});
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
index 602b68ea572..7d366c495f0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
@@ -1,66 +1,70 @@
<script>
- import { n__ } from '~/locale';
- import statusIcon from '../mr_widget_status_icon.vue';
- import eventHub from '../../event_hub';
+import { n__ } from '~/locale';
+import statusIcon from '../mr_widget_status_icon.vue';
+import eventHub from '../../event_hub';
- export default {
- name: 'MRWidgetFailedToMerge',
+export default {
+ name: 'MRWidgetFailedToMerge',
- components: {
- statusIcon,
- },
+ components: {
+ statusIcon,
+ },
- props: {
- mr: {
- type: Object,
- required: true,
- default: () => ({}),
- },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ default: () => ({}),
},
+ },
- data() {
- return {
- timer: 10,
- isRefreshing: false,
- };
- },
+ data() {
+ return {
+ timer: 10,
+ isRefreshing: false,
+ intervalId: null,
+ };
+ },
- computed: {
- timerText() {
- return n__(
- 'Refreshing in a second to show the updated status...',
- 'Refreshing in %d seconds to show the updated status...',
- this.timer,
- );
- },
+ computed: {
+ timerText() {
+ return n__(
+ 'Refreshing in a second to show the updated status...',
+ 'Refreshing in %d seconds to show the updated status...',
+ this.timer,
+ );
},
+ },
- mounted() {
- setInterval(() => {
- this.updateTimer();
- }, 1000);
- },
+ mounted() {
+ this.intervalId = setInterval(this.updateTimer, 1000);
+ },
- created() {
- eventHub.$emit('DisablePolling');
- },
+ created() {
+ eventHub.$emit('DisablePolling');
+ },
- methods: {
- refresh() {
- this.isRefreshing = true;
- eventHub.$emit('MRWidgetUpdateRequested');
- eventHub.$emit('EnablePolling');
- },
- updateTimer() {
- this.timer = this.timer - 1;
+ beforeDestroy() {
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ }
+ },
- if (this.timer === 0) {
- this.refresh();
- }
- },
+ methods: {
+ refresh() {
+ this.isRefreshing = true;
+ eventHub.$emit('MRWidgetUpdateRequested');
+ eventHub.$emit('EnablePolling');
},
+ updateTimer() {
+ this.timer = this.timer - 1;
- };
+ if (this.timer === 0) {
+ this.refresh();
+ }
+ },
+ },
+};
</script>
<template>
<div class="mr-widget-body media">
diff --git a/app/assets/javascripts/vue_shared/components/callout.vue b/app/assets/javascripts/vue_shared/components/callout.vue
new file mode 100644
index 00000000000..ccf802c456c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/callout.vue
@@ -0,0 +1,27 @@
+<script>
+const calloutVariants = ['danger', 'success', 'info', 'warning'];
+
+export default {
+ props: {
+ category: {
+ type: String,
+ required: false,
+ default: calloutVariants[0],
+ validator: value => calloutVariants.includes(value),
+ },
+ message: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div
+ :class="`bs-callout bs-callout-${category}`"
+ role="alert"
+ aria-live="assertive"
+ >
+ {{ message }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index 5324d5dc797..0d64efcbf68 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -1,52 +1,52 @@
<script>
- import ciIcon from './ci_icon.vue';
- import tooltip from '../directives/tooltip';
- /**
- * Renders CI Badge link with CI icon and status text based on
- * API response shared between all places where it is used.
- *
- * Receives status object containing:
- * status: {
- * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
- * group:"running" // used for CSS class
- * icon: "icon_status_running" // used to render the icon
- * label:"running" // used for potential tooltip
- * text:"running" // text rendered
- * }
- *
- * Used in:
- * - Pipelines table - first column
- * - Jobs table - first column
- * - Pipeline show view - header
- * - Job show view - header
- * - MR widget
- */
+import CiIcon from './ci_icon.vue';
+import tooltip from '../directives/tooltip';
+/**
+ * Renders CI Badge link with CI icon and status text based on
+ * API response shared between all places where it is used.
+ *
+ * Receives status object containing:
+ * status: {
+ * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
+ * group:"running" // used for CSS class
+ * icon: "icon_status_running" // used to render the icon
+ * label:"running" // used for potential tooltip
+ * text:"running" // text rendered
+ * }
+ *
+ * Used in:
+ * - Pipelines table - first column
+ * - Jobs table - first column
+ * - Pipeline show view - header
+ * - Job show view - header
+ * - MR widget
+ */
- export default {
- components: {
- ciIcon,
+export default {
+ components: {
+ CiIcon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ status: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ showText: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- props: {
- status: {
- type: Object,
- required: true,
- },
- showText: {
- type: Boolean,
- required: false,
- default: true,
- },
+ },
+ computed: {
+ cssClass() {
+ const className = this.status.group;
+ return className ? `ci-status ci-${className}` : 'ci-status';
},
- computed: {
- cssClass() {
- const className = this.status.group;
- return className ? `ci-status ci-${className}` : 'ci-status';
- },
- },
- };
+ },
+};
</script>
<template>
<a
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index 8fea746f4de..fcab8f571dd 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -1,45 +1,44 @@
<script>
- import icon from '../../vue_shared/components/icon.vue';
+import Icon from '../../vue_shared/components/icon.vue';
- /**
- * Renders CI icon based on API response shared between all places where it is used.
- *
- * Receives status object containing:
- * status: {
- * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
- * group:"running" // used for CSS class
- * icon: "icon_status_running" // used to render the icon
- * label:"running" // used for potential tooltip
- * text:"running" // text rendered
- * }
- *
- * Used in:
- * - Pipelines table Badge
- * - Pipelines table mini graph
- * - Pipeline graph
- * - Pipeline show view badge
- * - Jobs table
- * - Jobs show view header
- * - Jobs show view sidebar
- */
- export default {
- components: {
- icon,
+/**
+ * Renders CI icon based on API response shared between all places where it is used.
+ *
+ * Receives status object containing:
+ * status: {
+ * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
+ * group:"running" // used for CSS class
+ * icon: "icon_status_running" // used to render the icon
+ * label:"running" // used for potential tooltip
+ * text:"running" // text rendered
+ * }
+ *
+ * Used in:
+ * - Pipelines table Badge
+ * - Pipelines table mini graph
+ * - Pipeline graph
+ * - Pipeline show view badge
+ * - Jobs table
+ * - Jobs show view header
+ * - Jobs show view sidebar
+ */
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ status: {
+ type: Object,
+ required: true,
},
- props: {
- status: {
- type: Object,
- required: true,
- },
+ },
+ computed: {
+ cssClass() {
+ const status = this.status.group;
+ return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
-
- computed: {
- cssClass() {
- const status = this.status.group;
- return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
- },
- },
- };
+ },
+};
</script>
<template>
<span :class="cssClass">
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
index cab126a7eca..cb2cc3901ad 100644
--- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -1,40 +1,50 @@
<script>
- /**
- * Falls back to the code used in `copy_to_clipboard.js`
- */
- import tooltip from '../directives/tooltip';
+/**
+ * Falls back to the code used in `copy_to_clipboard.js`
+ *
+ * Renders a button with a clipboard icon that copies the content of `data-clipboard-text`
+ * when clicked.
+ *
+ * @example
+ * <clipboard-button
+ * title="Copy to clipbard"
+ * text="Content to be copied"
+ * css-class="btn-transparent"
+ * />
+ */
+import tooltip from '../directives/tooltip';
- export default {
- name: 'ClipboardButton',
- directives: {
- tooltip,
+export default {
+ name: 'ClipboardButton',
+ directives: {
+ tooltip,
+ },
+ props: {
+ text: {
+ type: String,
+ required: true,
},
- props: {
- text: {
- type: String,
- required: true,
- },
- title: {
- type: String,
- required: true,
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'top',
- },
- tooltipContainer: {
- type: [String, Boolean],
- required: false,
- default: false,
- },
- cssClass: {
- type: String,
- required: false,
- default: 'btn-default',
- },
+ title: {
+ type: String,
+ required: true,
},
- };
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+ tooltipContainer: {
+ type: [String, Boolean],
+ required: false,
+ default: false,
+ },
+ cssClass: {
+ type: String,
+ required: false,
+ default: 'btn-default',
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index b8875d04488..8f250a6c989 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -1,119 +1,111 @@
<script>
- import commitIconSvg from 'icons/_icon_commit.svg';
- import userAvatarLink from './user_avatar/user_avatar_link.vue';
- import tooltip from '../directives/tooltip';
- import icon from '../../vue_shared/components/icon.vue';
+import UserAvatarLink from './user_avatar/user_avatar_link.vue';
+import tooltip from '../directives/tooltip';
+import Icon from '../../vue_shared/components/icon.vue';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ UserAvatarLink,
+ Icon,
+ },
+ props: {
+ /**
+ * Indicates the existance of a tag.
+ * Used to render the correct icon, if true will render `fa-tag` icon,
+ * if false will render a svg sprite fork icon
+ */
+ tag: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- components: {
- userAvatarLink,
- icon,
+ /**
+ * If provided is used to render the branch name and url.
+ * Should contain the following properties:
+ * name
+ * ref_url
+ */
+ commitRef: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ /**
+ * Used to link to the commit sha.
+ */
+ commitUrl: {
+ type: String,
+ required: false,
+ default: '',
},
- props: {
- /**
- * Indicates the existance of a tag.
- * Used to render the correct icon, if true will render `fa-tag` icon,
- * if false will render a svg sprite fork icon
- */
- tag: {
- type: Boolean,
- required: false,
- default: false,
- },
- /**
- * If provided is used to render the branch name and url.
- * Should contain the following properties:
- * name
- * ref_url
- */
- commitRef: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- /**
- * Used to link to the commit sha.
- */
- commitUrl: {
- type: String,
- required: false,
- default: '',
- },
- /**
- * Used to show the commit short sha that links to the commit url.
- */
- shortSha: {
- type: String,
- required: false,
- default: '',
- },
- /**
- * If provided shows the commit tile.
- */
- title: {
- type: String,
- required: false,
- default: '',
- },
- /**
- * If provided renders information about the author of the commit.
- * When provided should include:
- * `avatar_url` to render the avatar icon
- * `web_url` to link to user profile
- * `username` to render alt and title tags
- */
- author: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- showBranch: {
- type: Boolean,
- required: false,
- default: true,
- },
+ /**
+ * Used to show the commit short sha that links to the commit url.
+ */
+ shortSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ /**
+ * If provided shows the commit tile.
+ */
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ /**
+ * If provided renders information about the author of the commit.
+ * When provided should include:
+ * `avatar_url` to render the avatar icon
+ * `web_url` to link to user profile
+ * `username` to render alt and title tags
+ */
+ author: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ showBranch: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- computed: {
- /**
- * Used to verify if all the properties needed to render the commit
- * ref section were provided.
- *
- * @returns {Boolean}
- */
- hasCommitRef() {
- return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
- },
- /**
- * Used to verify if all the properties needed to render the commit
- * author section were provided.
- *
- * @returns {Boolean}
- */
- hasAuthor() {
- return this.author &&
- this.author.avatar_url &&
- this.author.path &&
- this.author.username;
- },
- /**
- * If information about the author is provided will return a string
- * to be rendered as the alt attribute of the img tag.
- *
- * @returns {String}
- */
- userImageAltDescription() {
- return this.author &&
- this.author.username ? `${this.author.username}'s avatar` : null;
- },
+ },
+ computed: {
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * ref section were provided.
+ *
+ * @returns {Boolean}
+ */
+ hasCommitRef() {
+ return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
- created() {
- this.commitIconSvg = commitIconSvg;
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * author section were provided.
+ *
+ * @returns {Boolean}
+ */
+ hasAuthor() {
+ return this.author && this.author.avatar_url && this.author.path && this.author.username;
},
- };
+ /**
+ * If information about the author is provided will return a string
+ * to be rendered as the alt attribute of the img tag.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ return this.author && this.author.username ? `${this.author.username}'s avatar` : null;
+ },
+ },
+};
</script>
<template>
<div class="branch-commit">
@@ -141,11 +133,10 @@
{{ commitRef.name }}
</a>
</template>
- <div
- v-html="commitIconSvg"
+ <icon
+ name="commit"
class="commit-icon js-commit-icon"
- >
- </div>
+ />
<a
class="commit-sha"
diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue
index c943c8d98a4..9295be3e2b2 100644
--- a/app/assets/javascripts/vue_shared/components/expand_button.vue
+++ b/app/assets/javascripts/vue_shared/components/expand_button.vue
@@ -1,33 +1,33 @@
<script>
- import { __ } from '~/locale';
- /**
- * Port of detail_behavior expand button.
- *
- * @example
- * <expand-button>
- * <template slot="expanded">
- * Text goes here.
- * </template>
- * </expand-button>
- */
- export default {
- name: 'ExpandButton',
- data() {
- return {
- isCollapsed: true,
- };
+import { __ } from '~/locale';
+/**
+ * Port of detail_behavior expand button.
+ *
+ * @example
+ * <expand-button>
+ * <template slot="expanded">
+ * Text goes here.
+ * </template>
+ * </expand-button>
+ */
+export default {
+ name: 'ExpandButton',
+ data() {
+ return {
+ isCollapsed: true,
+ };
+ },
+ computed: {
+ ariaLabel() {
+ return __('Click to expand text');
},
- computed: {
- ariaLabel() {
- return __('Click to expand text');
- },
+ },
+ methods: {
+ onClick() {
+ this.isCollapsed = !this.isCollapsed;
},
- methods: {
- onClick() {
- this.isCollapsed = !this.isCollapsed;
- },
- },
- };
+ },
+};
</script>
<template>
<span>
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index ee1c3498748..be2755452e2 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -1,9 +1,9 @@
<script>
- import getIconForFile from './file_icon/file_icon_map';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import icon from '../../vue_shared/components/icon.vue';
+import getIconForFile from './file_icon/file_icon_map';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import icon from '../../vue_shared/components/icon.vue';
- /* This is a re-usable vue component for rendering a svg sprite
+/* This is a re-usable vue component for rendering a svg sprite
icon
Sample configuration:
@@ -15,60 +15,60 @@
/>
*/
- export default {
- components: {
- loadingIcon,
- icon,
+export default {
+ components: {
+ loadingIcon,
+ icon,
+ },
+ props: {
+ fileName: {
+ type: String,
+ required: true,
},
- props: {
- fileName: {
- type: String,
- required: true,
- },
- folder: {
- type: Boolean,
- required: false,
- default: false,
- },
+ folder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
- opened: {
- type: Boolean,
- required: false,
- default: false,
- },
+ opened: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
- loading: {
- type: Boolean,
- required: false,
- default: false,
- },
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
- size: {
- type: Number,
- required: false,
- default: 16,
- },
+ size: {
+ type: Number,
+ required: false,
+ default: 16,
+ },
- cssClasses: {
- type: String,
- required: false,
- default: '',
- },
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ spriteHref() {
+ const iconName = getIconForFile(this.fileName) || 'file';
+ return `${gon.sprite_file_icons}#${iconName}`;
+ },
+ folderIconName() {
+ return this.opened ? 'folder-open' : 'folder';
},
- computed: {
- spriteHref() {
- const iconName = getIconForFile(this.fileName) || 'file';
- return `${gon.sprite_file_icons}#${iconName}`;
- },
- folderIconName() {
- return this.opened ? 'folder-open' : 'folder';
- },
- iconSizeClass() {
- return this.size ? `s${this.size}` : '';
- },
+ iconSizeClass() {
+ return this.size ? `s${this.size}` : '';
},
- };
+ },
+};
</script>
<template>
<span>
@@ -82,6 +82,7 @@
v-if="!loading && folder"
:name="folderIconName"
:size="size"
+ css-classes="folder-icon"
/>
<loading-icon
v-if="loading"
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index a0cd0cbd200..088187ed348 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,78 +1,78 @@
<script>
- import ciIconBadge from './ci_badge_link.vue';
- import loadingIcon from './loading_icon.vue';
- import timeagoTooltip from './time_ago_tooltip.vue';
- import tooltip from '../directives/tooltip';
- import userAvatarImage from './user_avatar/user_avatar_image.vue';
+import CiIconBadge from './ci_badge_link.vue';
+import LoadingIcon from './loading_icon.vue';
+import TimeagoTooltip from './time_ago_tooltip.vue';
+import tooltip from '../directives/tooltip';
+import UserAvatarImage from './user_avatar/user_avatar_image.vue';
- /**
- * Renders header component for job and pipeline page based on UI mockups
- *
- * Used in:
- * - job show page
- * - pipeline show page
- */
- export default {
- components: {
- ciIconBadge,
- loadingIcon,
- timeagoTooltip,
- userAvatarImage,
+/**
+ * Renders header component for job and pipeline page based on UI mockups
+ *
+ * Used in:
+ * - job show page
+ * - pipeline show page
+ */
+export default {
+ components: {
+ CiIconBadge,
+ LoadingIcon,
+ TimeagoTooltip,
+ UserAvatarImage,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ status: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ itemName: {
+ type: String,
+ required: true,
},
- props: {
- status: {
- type: Object,
- required: true,
- },
- itemName: {
- type: String,
- required: true,
- },
- itemId: {
- type: Number,
- required: true,
- },
- time: {
- type: String,
- required: true,
- },
- user: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- actions: {
- type: Array,
- required: false,
- default: () => [],
- },
- hasSidebarButton: {
- type: Boolean,
- required: false,
- default: false,
- },
- shouldRenderTriggeredLabel: {
- type: Boolean,
- required: false,
- default: true,
- },
+ itemId: {
+ type: Number,
+ required: true,
},
+ time: {
+ type: String,
+ required: true,
+ },
+ user: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ actions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ hasSidebarButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ shouldRenderTriggeredLabel: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
- computed: {
- userAvatarAltText() {
- return `${this.user.name}'s avatar`;
- },
+ computed: {
+ userAvatarAltText() {
+ return `${this.user.name}'s avatar`;
},
+ },
- methods: {
- onClickAction(action) {
- this.$emit('actionClicked', action);
- },
+ methods: {
+ onClickAction(action) {
+ this.$emit('actionClicked', action);
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index 6a2e05000e1..1a0df49bc29 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -1,76 +1,75 @@
<script>
+/* This is a re-usable vue component for rendering a svg sprite
+ icon
- /* This is a re-usable vue component for rendering a svg sprite
- icon
+ Sample configuration:
- Sample configuration:
+ <icon
+ name="retry"
+ :size="32"
+ css-classes="top"
+ />
- <icon
- name="retry"
- :size="32"
- css-classes="top"
- />
+*/
+// only allow classes in images.scss e.g. s12
+const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
- */
- // only allow classes in images.scss e.g. s12
- const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
-
- export default {
- props: {
- name: {
- type: String,
- required: true,
- },
+export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
- size: {
- type: Number,
- required: false,
- default: 16,
- validator(value) {
- return validSizes.includes(value);
- },
+ size: {
+ type: Number,
+ required: false,
+ default: 16,
+ validator(value) {
+ return validSizes.includes(value);
},
+ },
- cssClasses: {
- type: String,
- required: false,
- default: '',
- },
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
- width: {
- type: Number,
- required: false,
- default: null,
- },
+ width: {
+ type: Number,
+ required: false,
+ default: null,
+ },
- height: {
- type: Number,
- required: false,
- default: null,
- },
+ height: {
+ type: Number,
+ required: false,
+ default: null,
+ },
- y: {
- type: Number,
- required: false,
- default: null,
- },
+ y: {
+ type: Number,
+ required: false,
+ default: null,
+ },
- x: {
- type: Number,
- required: false,
- default: null,
- },
+ x: {
+ type: Number,
+ required: false,
+ default: null,
},
+ },
- computed: {
- spriteHref() {
- return `${gon.sprite_icons}#${this.name}`;
- },
- iconSizeClass() {
- return this.size ? `s${this.size}` : '';
- },
+ computed: {
+ spriteHref() {
+ return `${gon.sprite_icons}#${this.name}`;
+ },
+ iconSizeClass() {
+ return this.size ? `s${this.size}` : '';
},
- };
+ },
+};
</script>
<template>
@@ -79,7 +78,8 @@
:width="width"
:height="height"
:x="x"
- :y="y">
+ :y="y"
+ >
<use v-bind="{ 'xlink:href':spriteHref }" />
</svg>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index 5ede53d8d01..70b46a9c2bb 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -1,4 +1,5 @@
<script>
+import $ from 'jquery';
import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
import LoadingIcon from '../../loading_icon.vue';
@@ -98,11 +99,18 @@ export default {
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
handleClick: this.handleClick,
});
+ $(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden);
},
methods: {
handleClick(label) {
this.$emit('onLabelClick', label);
},
+ handleCollapsedValueClick() {
+ this.$emit('toggleCollapse');
+ },
+ handleDropdownHidden() {
+ this.$emit('onDropdownClose');
+ },
},
};
</script>
@@ -112,6 +120,7 @@ export default {
<dropdown-value-collapsed
v-if="showCreate"
:labels="context.labels"
+ @onValueClick="handleCollapsedValueClick"
/>
<dropdown-title
:can-edit="canEdit"
@@ -133,7 +142,10 @@ export default {
:name="hiddenInputName"
:label="label"
/>
- <div class="dropdown">
+ <div
+ class="dropdown"
+ ref="dropdown"
+ >
<dropdown-button
:ability-name="abilityName"
:field-name="hiddenInputName"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
index 5cf728fe050..68fa2ab8d01 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
@@ -26,6 +26,11 @@ export default {
return labelsString;
},
},
+ methods: {
+ handleClick() {
+ this.$emit('onValueClick');
+ },
+ },
};
</script>
@@ -36,6 +41,7 @@ export default {
data-placement="left"
data-container="body"
:title="labelsList"
+ @click="handleClick"
>
<i
aria-hidden="true"
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 8211d425b1f..de6f8c32e74 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
@@ -1,18 +1,29 @@
<script>
- export default {
- name: 'ToggleSidebar',
- props: {
- collapsed: {
- type: Boolean,
- required: true,
- },
+import { __ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ name: 'ToggleSidebar',
+ directives: {
+ tooltip,
+ },
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ tooltipLabel() {
+ return this.collapsed ? __('Expand sidebar') : __('Collapse sidebar');
},
- methods: {
- toggle() {
- this.$emit('toggle');
- },
+ },
+ methods: {
+ toggle() {
+ this.$emit('toggle');
},
- };
+ },
+};
</script>
<template>
@@ -20,6 +31,10 @@
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
@click="toggle"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="tooltipLabel"
>
<i
aria-label="toggle collapse"
diff --git a/app/assets/javascripts/vue_shared/models/label.js b/app/assets/javascripts/vue_shared/models/label.js
index 70b9efe0c68..d29c7fe973a 100644
--- a/app/assets/javascripts/vue_shared/models/label.js
+++ b/app/assets/javascripts/vue_shared/models/label.js
@@ -1,4 +1,4 @@
-class ListLabel {
+export default class ListLabel {
constructor(obj) {
this.id = obj.id;
this.title = obj.title;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index d0dda50a835..e058a0b35b7 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -472,6 +472,7 @@ img.emoji {
.append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; }
.append-bottom-5 { margin-bottom: 5px; }
+.append-bottom-8 { margin-bottom: $grid-size; }
.append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; }
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index cc5fac6816d..664aade7375 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -43,7 +43,7 @@
border-color: $gray-darkest;
}
- [data-toggle="dropdown"] {
+ [data-toggle='dropdown'] {
outline: 0;
}
}
@@ -172,7 +172,11 @@
color: $brand-danger;
}
- &:hover,
+ &.disable-hover {
+ text-decoration: none;
+ }
+
+ &:not(.disable-hover):hover,
&:active,
&:focus,
&.is-focused {
@@ -508,17 +512,16 @@
}
&.is-indeterminate::before {
- content: "\f068";
+ content: '\f068';
}
&.is-active::before {
- content: "\f00c";
+ content: '\f00c';
}
}
}
}
-
.dropdown-title {
position: relative;
padding: 2px 25px 10px;
@@ -724,7 +727,6 @@
}
}
-
.dropdown-menu-due-date {
.dropdown-content {
max-height: 230px;
@@ -854,9 +856,13 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
.projects-list-frequent-container,
- .projects-list-search-container, {
+ .projects-list-search-container {
padding: 8px 0;
overflow-y: auto;
+
+ li.section-empty.section-failure {
+ color: $callout-danger-color;
+ }
}
.section-header,
@@ -867,13 +873,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
font-size: $gl-font-size;
}
- .projects-list-frequent-container,
- .projects-list-search-container {
- li.section-empty.section-failure {
- color: $callout-danger-color;
- }
- }
-
.search-input-container {
position: relative;
padding: 4px $gl-padding;
@@ -905,8 +904,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
.projects-list-item-container {
- .project-item-avatar-container
- .project-item-metadata-container {
+ .project-item-avatar-container .project-item-metadata-container {
float: left;
}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 8604e753c18..9e03bb98b8e 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -40,10 +40,6 @@
.project-home-panel {
padding-left: 0 !important;
- .project-avatar {
- display: block;
- }
-
.project-repo-buttons,
.git-clone-holder {
display: none;
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 17c31d6b184..66dbe403385 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -241,8 +241,6 @@
}
.scrolling-tabs-container {
- position: relative;
-
.merge-request-tabs-container & {
overflow: hidden;
}
@@ -272,8 +270,6 @@
}
.inner-page-scroll-tabs {
- position: relative;
-
.fade-right {
@include fade(left, $white-light);
right: 0;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 798f248dad4..64fff7463d2 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -16,7 +16,7 @@
.nav-header-btn {
padding: 10px $gl-sidebar-padding;
color: inherit;
- transition-duration: .3s;
+ transition-duration: 0.3s;
position: absolute;
top: 0;
cursor: pointer;
@@ -137,6 +137,12 @@
}
}
+.issuable-sidebar .labels {
+ .value.dont-hide ~ .selectbox {
+ padding-top: $gl-padding-8;
+ }
+}
+
.pikaday-container {
.pika-single {
margin-top: 2px;
@@ -151,4 +157,3 @@
.sidebar-collapsed-icon .sidebar-collapsed-value {
font-size: 12px;
}
-
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 37223175199..8c44ebc85ef 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -247,6 +247,7 @@ $btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
$issue-status-expired: $orange-500;
$issuable-sidebar-color: $gl-text-color-secondary;
+$sidebar-block-hover-color: #ebebeb;
$group-path-color: #999;
$namespace-kind-color: #aaa;
$panel-heading-link-color: #777;
@@ -373,6 +374,8 @@ $dropdown-hover-color: $blue-400;
$link-active-background: rgba(0, 0, 0, 0.04);
$link-hover-background: rgba(0, 0, 0, 0.06);
$inactive-badge-background: rgba(0, 0, 0, 0.08);
+$sidebar-toggle-height: 60px;
+$sidebar-milestone-toggle-bottom-margin: 10px;
/*
* Buttons
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index 2f3a80daa90..3fa7a260017 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -19,6 +19,7 @@
.fork-svg {
margin-right: 4px;
+ vertical-align: bottom;
}
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 7a6352e45f1..50f32660445 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -1,39 +1,56 @@
@keyframes fade-out-status {
- 0%, 50% { opacity: 1; }
- 100% { opacity: 0; }
+ 0%,
+ 50% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ }
}
@keyframes blinking-dots {
0% {
background-color: rgba($white-light, 1);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ 24px 0 0 0 rgba($white-light, 0.2);
}
25% {
background-color: rgba($white-light, 0.4);
box-shadow: 12px 0 0 0 rgba($white-light, 2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ 24px 0 0 0 rgba($white-light, 0.2);
}
75% {
background-color: rgba($white-light, 0.4);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 1);
+ 24px 0 0 0 rgba($white-light, 1);
}
100% {
background-color: rgba($white-light, 1);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ 24px 0 0 0 rgba($white-light, 0.2);
}
}
@keyframes blinking-scroll-button {
- 0% { opacity: 0.2; }
- 25% { opacity: 0.5; }
- 50% { opacity: 0.7; }
- 100% { opacity: 1; }
+ 0% {
+ opacity: 0.2;
+ }
+
+ 25% {
+ opacity: 0.5;
+ }
+
+ 50% {
+ opacity: 0.7;
+ }
+
+ 100% {
+ opacity: 1;
+ }
}
.build-page {
@@ -125,12 +142,12 @@
.btn-scroll.animate {
.first-triangle {
animation: blinking-scroll-button 1s ease infinite;
- animation-delay: .3s;
+ animation-delay: 0.3s;
}
.second-triangle {
animation: blinking-scroll-button 1s ease infinite;
- animation-delay: .2s;
+ animation-delay: 0.2s;
}
.third-triangle {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 86cdda0359e..1aca3c5cf1a 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -70,7 +70,7 @@
}
.branch-info .commit-icon {
- margin-right: 3px;
+ margin-right: 8px;
svg {
top: 3px;
@@ -180,10 +180,6 @@
justify-content: space-between;
align-items: center;
flex-grow: 1;
-
- .merge-request-branches & {
- flex-direction: column;
- }
}
.commit-content {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 2c0ed976301..b2dad4a358a 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -187,7 +187,12 @@
padding-left: 10px;
&:hover {
- color: $gray-darkest;
+ color: $gl-text-color;
+ }
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
}
}
@@ -368,6 +373,14 @@
padding: 15px 0 0;
border-bottom: 0;
overflow: hidden;
+
+ &:hover {
+ background-color: $sidebar-block-hover-color;
+ }
+
+ &.issuable-sidebar-header {
+ padding-top: 0;
+ }
}
.participants {
@@ -380,8 +393,17 @@
.gutter-toggle {
width: 100%;
+ height: $sidebar-toggle-height;
margin-left: 0;
- padding-left: 25px;
+ padding-left: 0;
+ border-bottom: 1px solid $border-gray-dark;
+ }
+
+ a.gutter-toggle {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ text-align: center;
}
.sidebar-collapsed-icon {
@@ -428,10 +450,10 @@
.btn-clipboard {
border: 0;
+ background: transparent;
color: $issuable-sidebar-color;
&:hover {
- background: transparent;
color: $gl-text-color;
}
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index b0852adb459..d81236c5883 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -314,6 +314,10 @@
display: inline-flex;
vertical-align: top;
+ &:hover .color-label {
+ text-decoration: underline;
+ }
+
.label {
vertical-align: inherit;
font-size: $label-font-size;
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 3af8d80daab..bac3b70c734 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -53,10 +53,6 @@
}
.milestone-sidebar {
- .gutter-toggle {
- margin-bottom: 10px;
- }
-
.milestone-progress {
.title {
padding-top: 5px;
@@ -102,7 +98,17 @@
margin-right: 0;
}
+ .right-sidebar-expanded & {
+ .gutter-toggle {
+ margin-bottom: $sidebar-milestone-toggle-bottom-margin;
+ }
+ }
+
.right-sidebar-collapsed & {
+ .milestone-progress {
+ padding-top: 0;
+ }
+
.reference {
border-top: 1px solid $border-gray-normal;
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 855ebf7d86d..3a8ec779c14 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -468,6 +468,14 @@
margin-bottom: 10px;
white-space: normal;
+ .ci-job-dropdown-container {
+ // override dropdown.scss
+ .dropdown-menu li button {
+ padding: 0;
+ text-align: center;
+ }
+ }
+
// ensure .build-content has hover style when action-icon is hovered
.ci-job-dropdown-container:hover .build-content {
@extend .build-content:hover;
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 5f46e69a56d..6342042374f 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -17,6 +17,7 @@
}
.ide-view {
+ position: relative;
display: flex;
height: calc(100vh - #{$header-height});
margin-top: 0;
@@ -54,6 +55,7 @@
white-space: nowrap;
text-overflow: ellipsis;
max-width: inherit;
+ line-height: 22px;
svg {
vertical-align: middle;
@@ -66,13 +68,21 @@
}
}
+ .ide-file-icon-holder {
+ display: flex;
+ align-items: center;
+ }
+
.ide-file-changed-icon {
margin-left: auto;
+
+ > svg {
+ display: block;
+ }
}
.ide-new-btn {
display: none;
- margin-bottom: -4px;
margin-right: -8px;
}
@@ -85,10 +95,8 @@
}
}
- &.folder {
- svg {
- fill: $gl-text-color-secondary;
- }
+ .folder-icon {
+ fill: $gl-text-color-secondary;
}
}
@@ -106,6 +114,7 @@
.file-col-commit-message {
display: flex;
overflow: visible;
+ align-items: center;
padding: 6px 12px;
}
@@ -378,7 +387,11 @@
padding: $gl-bar-padding $gl-padding;
background: $white-light;
display: flex;
- justify-content: space-between;
+ justify-content: flex-end;
+
+ > div + div {
+ padding-left: $gl-padding;
+ }
svg {
vertical-align: middle;
@@ -429,7 +442,7 @@
.projects-sidebar {
display: flex;
flex-direction: column;
- height: 100%;
+ flex: 1;
.context-header {
width: auto;
@@ -521,9 +534,13 @@
overflow: auto;
}
-.multi-file-commit-empty-state-container {
- align-items: center;
- justify-content: center;
+.ide-commit-empty-state {
+ padding: 0 $gl-padding;
+}
+
+.ide-commit-empty-state-container {
+ margin-top: auto;
+ margin-bottom: auto;
}
.multi-file-commit-panel-header {
@@ -532,35 +549,22 @@
margin-bottom: 0;
border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding 0;
-
- &.is-collapsed {
- border-bottom: 1px solid $white-dark;
-
- svg {
- margin-left: auto;
- margin-right: auto;
- }
-
- .multi-file-commit-panel-collapse-btn {
- margin-right: auto;
- margin-left: auto;
- border-left: 0;
- }
- }
}
.multi-file-commit-panel-header-title {
display: flex;
flex: 1;
- padding: 0 $gl-btn-padding;
+ padding-left: $grid-size;
svg {
margin-right: $gl-btn-padding;
+ color: $theme-gray-700;
}
}
.multi-file-commit-panel-collapse-btn {
border-left: 1px solid $white-dark;
+ margin-left: auto;
}
.multi-file-commit-list {
@@ -574,12 +578,14 @@
display: flex;
padding: 0;
align-items: center;
+ border-radius: $border-radius-default;
.multi-file-discard-btn {
display: none;
+ margin-top: -2px;
margin-left: auto;
+ margin-right: $grid-size;
color: $gl-link-color;
- padding: 0 2px;
&:focus,
&:hover {
@@ -591,26 +597,31 @@
background: $white-normal;
.multi-file-discard-btn {
- display: block;
+ display: flex;
}
}
}
-.multi-file-addition {
+.multi-file-additions,
+.multi-file-additions-solid {
fill: $green-500;
}
-.multi-file-modified {
+.multi-file-modified,
+.multi-file-modified-solid {
fill: $orange-500;
}
.multi-file-commit-list-collapsed {
display: flex;
flex-direction: column;
+ padding: $gl-padding 0;
- > svg {
+ svg {
+ display: block;
margin-left: auto;
margin-right: auto;
+ color: $theme-gray-700;
}
.file-status-icon {
@@ -622,7 +633,7 @@
.multi-file-commit-list-path {
padding: $grid-size / 2;
- padding-left: $gl-padding;
+ padding-left: $grid-size;
background: none;
border: 0;
text-align: left;
@@ -662,11 +673,6 @@
}
}
-.multi-file-commit-message.form-control {
- height: 160px;
- resize: none;
-}
-
.dirty-diff {
// !important need to override monaco inline style
width: 4px !important;
@@ -812,6 +818,41 @@
}
}
+.ide-commit-list-container {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ padding: 0 16px;
+
+ &:not(.is-collapsed) {
+ flex: 1;
+ min-height: 140px;
+ }
+
+ &.is-collapsed {
+ .multi-file-commit-panel-header {
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+
+ svg {
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .multi-file-commit-panel-collapse-btn {
+ margin-right: auto;
+ margin-left: auto;
+ border-left: 0;
+ }
+ }
+ }
+}
+
+.ide-staged-action-btn {
+ margin-left: auto;
+ color: $gl-link-color;
+}
+
.ide-commit-radios {
label {
font-weight: normal;
@@ -839,3 +880,98 @@
align-items: center;
font-weight: $gl-font-weight-bold;
}
+
+.ide-file-finder-overlay {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 100;
+}
+
+.ide-file-finder {
+ top: 10px;
+ left: 50%;
+ transform: translateX(-50%);
+
+ .highlighted {
+ color: $blue-500;
+ font-weight: $gl-font-weight-bold;
+ }
+}
+
+.ide-commit-message-field {
+ height: 200px;
+ background-color: $white-light;
+
+ .md-area {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+
+ .nav-links {
+ height: 30px;
+ }
+
+ .help-block {
+ margin-top: 2px;
+ color: $blue-500;
+ cursor: pointer;
+ }
+}
+
+.ide-commit-message-textarea-container {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+
+ .note-textarea {
+ font-family: $monospace_font;
+ }
+}
+
+.ide-commit-message-highlights-container {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: -100px;
+ bottom: 0;
+ padding-right: 100px;
+ pointer-events: none;
+ z-index: 1;
+
+ .highlights {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ color: transparent;
+ }
+
+ mark {
+ margin-left: -1px;
+ padding: 0 2px;
+ border-radius: $border-radius-small;
+ background-color: $orange-200;
+ color: transparent;
+ opacity: 0.6;
+ }
+}
+
+.ide-commit-message-textarea {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 2;
+ background: transparent;
+ resize: none;
+}
+
+.ide-new-modal-label {
+ line-height: 34px;
+}
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index 45ae94abaff..06ef58531d7 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -1,5 +1,4 @@
@import 'framework/variables';
-@import 'peek/views/performance_bar';
@import 'peek/views/rblineprof';
#js-peek {