summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/ide
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/ide')
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue32
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue44
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue5
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue8
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue78
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue41
-rw-r--r--app/assets/javascripts/ide/components/file_row_extra.vue15
-rw-r--r--app/assets/javascripts/ide/components/ide.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue2
-rw-r--r--app/assets/javascripts/ide/components/nav_dropdown.vue2
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue25
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue2
-rw-r--r--app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue151
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue103
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue4
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue15
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue2
-rw-r--r--app/assets/javascripts/ide/index.js1
-rw-r--r--app/assets/javascripts/ide/lib/editor.js14
-rw-r--r--app/assets/javascripts/ide/stores/actions.js181
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js76
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js15
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js11
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js11
-rw-r--r--app/assets/javascripts/ide/stores/getters.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/pane/actions.js8
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js64
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
30 files changed, 466 insertions, 451 deletions
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
index 6b2ef34c960..3398cd091ba 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
@@ -1,12 +1,13 @@
<script>
-import $ from 'jquery';
import { mapActions } from 'vuex';
-import { __ } from '~/locale';
+import { sprintf, __ } from '~/locale';
+import { GlModal } from '@gitlab/ui';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
export default {
components: {
+ GlModal,
FileIcon,
ChangedFileIcon,
},
@@ -17,7 +18,13 @@ export default {
},
},
computed: {
- activeButtonText() {
+ discardModalId() {
+ return `discard-file-${this.activeFile.path}`;
+ },
+ discardModalTitle() {
+ return sprintf(__('Discard changes to %{path}?'), { path: this.activeFile.path });
+ },
+ actionButtonText() {
return this.activeFile.staged ? __('Unstage') : __('Stage');
},
isStaged() {
@@ -25,7 +32,7 @@ export default {
},
},
methods: {
- ...mapActions(['stageChange', 'unstageChange']),
+ ...mapActions(['stageChange', 'unstageChange', 'discardFileChanges']),
actionButtonClicked() {
if (this.activeFile.staged) {
this.unstageChange(this.activeFile.path);
@@ -34,7 +41,7 @@ export default {
}
},
showDiscardModal() {
- $(document.getElementById(`discard-file-${this.activeFile.path}`)).modal('show');
+ this.$refs.discardModal.show();
},
},
};
@@ -53,6 +60,7 @@ export default {
<div class="ml-auto">
<button
v-if="!isStaged"
+ ref="discardButton"
type="button"
class="btn btn-remove btn-inverted append-right-8"
@click="showDiscardModal"
@@ -60,6 +68,7 @@ export default {
{{ __('Discard') }}
</button>
<button
+ ref="actionButton"
:class="{
'btn-success': !isStaged,
'btn-warning': isStaged,
@@ -68,8 +77,19 @@ export default {
class="btn btn-inverted"
@click="actionButtonClicked"
>
- {{ activeButtonText }}
+ {{ actionButtonText }}
</button>
</div>
+ <gl-modal
+ ref="discardModal"
+ ok-variant="danger"
+ cancel-variant="light"
+ :ok-title="__('Discard changes')"
+ :modal-id="discardModalId"
+ :title="discardModalTitle"
+ @ok="discardFileChanges(activeFile.path)"
+ >
+ {{ __("You will lose all changes you've made to this file. This action cannot be undone.") }}
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index f7ed7006874..9d5473a1201 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -6,6 +6,7 @@ import CommitMessageField from './message_field.vue';
import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@@ -14,6 +15,7 @@ export default {
CommitMessageField,
SuccessMessage,
},
+ mixins: [glFeatureFlagsMixin()],
data() {
return {
isCompact: true,
@@ -27,9 +29,13 @@ export default {
...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']),
overviewText() {
return sprintf(
- __(
- '<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
- ),
+ this.glFeatures.stageAllByDefault
+ ? __(
+ '<strong>%{stagedFilesLength} staged</strong> and <strong>%{changedFilesLength} unstaged</strong> changes',
+ )
+ : __(
+ '<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
+ ),
{
stagedFilesLength: this.stagedFiles.length,
changedFilesLength: this.changedFiles.length,
@@ -39,6 +45,10 @@ export default {
commitButtonText() {
return this.stagedFiles.length ? __('Commit') : __('Stage & Commit');
},
+
+ currentViewIsCommitView() {
+ return this.currentActivityView === activityBarViews.commit;
+ },
},
watch: {
currentActivityView() {
@@ -46,11 +56,11 @@ export default {
this.isCompact = false;
} else {
this.isCompact = !(
- this.currentActivityView === activityBarViews.commit &&
- window.innerHeight >= MAX_WINDOW_HEIGHT_COMPACT
+ this.currentViewIsCommitView && window.innerHeight >= MAX_WINDOW_HEIGHT_COMPACT
);
}
},
+
lastCommitMsg() {
this.isCompact =
this.currentActivityView !== activityBarViews.commit && this.lastCommitMsg === '';
@@ -59,14 +69,18 @@ export default {
methods: {
...mapActions(['updateActivityBarView']),
...mapActions('commit', ['updateCommitMessage', 'discardDraft', 'commitChanges']),
- toggleIsSmall() {
- this.updateActivityBarView(activityBarViews.commit)
- .then(() => {
- this.isCompact = !this.isCompact;
- })
- .catch(e => {
- throw e;
- });
+ toggleIsCompact() {
+ if (this.currentViewIsCommitView) {
+ this.isCompact = !this.isCompact;
+ } else {
+ this.updateActivityBarView(activityBarViews.commit)
+ .then(() => {
+ this.isCompact = false;
+ })
+ .catch(e => {
+ throw e;
+ });
+ }
},
beforeEnterTransition() {
const elHeight = this.isCompact
@@ -114,7 +128,7 @@ export default {
:disabled="!hasChanges"
type="button"
class="btn btn-primary btn-sm btn-block qa-begin-commit-button"
- @click="toggleIsSmall"
+ @click="toggleIsCompact"
>
{{ __('Commit…') }}
</button>
@@ -148,7 +162,7 @@ export default {
v-else
type="button"
class="btn btn-default btn-sm float-right"
- @click="toggleIsSmall"
+ @click="toggleIsCompact"
>
{{ __('Collapse') }}
</button>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index e16918ae025..d9a385a9d31 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -41,10 +41,6 @@ export default {
type: String,
required: true,
},
- itemActionComponent: {
- type: String,
- required: true,
- },
stagedList: {
type: Boolean,
required: false,
@@ -142,7 +138,6 @@ export default {
<li v-for="file in fileList" :key="file.key">
<list-item
:file="file"
- :action-component="itemActionComponent"
:key-prefix="keyPrefix"
:staged-list="stagedList"
:active-file-key="activeFileKey"
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 230dfaf047b..726e2b7e1fc 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -3,16 +3,12 @@ import { mapActions } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
-import StageButton from './stage_button.vue';
-import UnstageButton from './unstage_button.vue';
import { viewerTypes } from '../../constants';
import { getCommitIconMap } from '../../utils';
export default {
components: {
Icon,
- StageButton,
- UnstageButton,
FileIcon,
},
directives: {
@@ -23,10 +19,6 @@ export default {
type: Object,
required: true,
},
- actionComponent: {
- type: String,
- required: true,
- },
keyPrefix: {
type: String,
required: false,
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
deleted file mode 100644
index c14b8a47841..00000000000
--- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
+++ /dev/null
@@ -1,78 +0,0 @@
-<script>
-import $ from 'jquery';
-import { mapActions } from 'vuex';
-import { sprintf, __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
-import tooltip from '~/vue_shared/directives/tooltip';
-import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
-
-export default {
- components: {
- Icon,
- GlModal: DeprecatedModal2,
- },
- directives: {
- tooltip,
- },
- props: {
- path: {
- type: String,
- required: true,
- },
- },
- computed: {
- modalId() {
- return `discard-file-${this.path}`;
- },
- modalTitle() {
- return sprintf(__('Discard changes to %{path}?'), { path: this.path });
- },
- },
- methods: {
- ...mapActions(['stageChange', 'discardFileChanges']),
- showDiscardModal() {
- $(document.getElementById(this.modalId)).modal('show');
- },
- },
-};
-</script>
-
-<template>
- <div v-once class="multi-file-discard-btn d-flex">
- <button
- v-tooltip
- :aria-label="__('Stage changes')"
- :title="__('Stage changes')"
- type="button"
- class="btn btn-blank align-items-center"
- data-container="body"
- data-boundary="viewport"
- data-placement="bottom"
- @click.stop.prevent="stageChange(path)"
- >
- <icon :size="16" name="mobile-issue-close" class="ml-auto mr-auto" />
- </button>
- <button
- v-tooltip
- :aria-label="__('Discard changes')"
- :title="__('Discard changes')"
- type="button"
- class="btn btn-blank align-items-center"
- data-container="body"
- data-boundary="viewport"
- data-placement="bottom"
- @click.stop.prevent="showDiscardModal"
- >
- <icon :size="16" name="remove" class="ml-auto mr-auto" />
- </button>
- <gl-modal
- :id="modalId"
- :header-title-text="modalTitle"
- :footer-primary-button-text="__('Discard changes')"
- footer-primary-button-variant="danger"
- @submit="discardFileChanges(path)"
- >
- {{ __("You will lose all changes you've made to this file. This action cannot be undone.") }}
- </gl-modal>
- </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
deleted file mode 100644
index 0567ef54ff3..00000000000
--- a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<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 d-flex">
- <button
- v-tooltip
- :aria-label="__('Unstage changes')"
- :title="__('Unstage changes')"
- type="button"
- class="btn btn-blank align-items-center"
- data-container="body"
- data-boundary="viewport"
- data-placement="bottom"
- @click.stop.prevent="unstageChange(path)"
- >
- <icon :size="16" name="redo" class="ml-auto mr-auto" />
- </button>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue
index f0bedcfbd6b..33098eb1af0 100644
--- a/app/assets/javascripts/ide/components/file_row_extra.vue
+++ b/app/assets/javascripts/ide/components/file_row_extra.vue
@@ -6,6 +6,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
import NewDropdown from './new_dropdown/index.vue';
import MrFileIcon from './mr_file_icon.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'FileRowExtra',
@@ -18,6 +19,7 @@ export default {
ChangedFileIcon,
MrFileIcon,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
file: {
type: Object,
@@ -55,10 +57,15 @@ export default {
return n__('%d staged change', '%d staged changes', this.folderStagedCount);
}
- return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
- unstaged: this.folderUnstagedCount,
- staged: this.folderStagedCount,
- });
+ return sprintf(
+ this.glFeatures.stageAllByDefault
+ ? __('%{staged} staged and %{unstaged} unstaged changes')
+ : __('%{unstaged} unstaged and %{staged} staged changes'),
+ {
+ unstaged: this.folderUnstagedCount,
+ staged: this.folderStagedCount,
+ },
+ );
},
showTreeChangesCount() {
return this.isTree && this.changesCount > 0 && !this.file.opened;
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 363a8f43033..6ed863c9c2e 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,6 +1,6 @@
<script>
import Vue from 'vue';
-import { mapActions, mapState, mapGetters } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import FindFile from '~/vue_shared/components/file_finder/index.vue';
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
index f93496132a4..598f3a1dac6 100644
--- a/app/assets/javascripts/ide/components/ide_tree.vue
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -1,13 +1,11 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
import IdeTreeList from './ide_tree_list.vue';
import Upload from './new_dropdown/upload.vue';
import NewEntryButton from './new_dropdown/button.vue';
export default {
components: {
- Icon,
Upload,
IdeTreeList,
NewEntryButton,
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index 3a0dd60f0e0..bacdfc7c05e 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -1,14 +1,12 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlSkeletonLoading } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import NavDropdown from './nav_dropdown.vue';
import FileRowExtra from './file_row_extra.vue';
export default {
components: {
- Icon,
GlSkeletonLoading,
NavDropdown,
FileRow,
diff --git a/app/assets/javascripts/ide/components/nav_dropdown.vue b/app/assets/javascripts/ide/components/nav_dropdown.vue
index e45d2a62dae..2e290de0943 100644
--- a/app/assets/javascripts/ide/components/nav_dropdown.vue
+++ b/app/assets/javascripts/ide/components/nav_dropdown.vue
@@ -1,12 +1,10 @@
<script>
import $ from 'jquery';
-import Icon from '~/vue_shared/components/icon.vue';
import NavForm from './nav_form.vue';
import NavDropdownButton from './nav_dropdown_button.vue';
export default {
components: {
- Icon,
NavDropdownButton,
NavForm,
},
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index ecafb4e81c4..bf3d736ddf3 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -67,8 +67,8 @@ export default {
if (this.entryModal.type === modalTypes.rename) {
if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) {
flash(
- sprintf(s__('The name %{entryName} is already taken in this directory.'), {
- entryName: this.entryName,
+ sprintf(s__('The name "%{name}" is already taken in this directory.'), {
+ name: this.entryName,
}),
'alert',
document,
@@ -81,22 +81,11 @@ export default {
const entryName = parentPath.pop();
parentPath = parentPath.join('/');
- const createPromise =
- parentPath && !this.entries[parentPath]
- ? this.createTempEntry({ name: parentPath, type: 'tree' })
- : Promise.resolve();
-
- createPromise
- .then(() =>
- this.renameEntry({
- path: this.entryModal.entry.path,
- name: entryName,
- parentPath,
- }),
- )
- .catch(() =>
- flash(__('Error creating a new path'), 'alert', document, null, false, true),
- );
+ this.renameEntry({
+ path: this.entryModal.entry.path,
+ name: entryName,
+ parentPath,
+ });
}
} else {
this.createTempEntry({
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index 188518dd419..e52613086a4 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -1,10 +1,8 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
import ItemButton from './button.vue';
export default {
components: {
- Icon,
ItemButton,
},
props: {
diff --git a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
new file mode 100644
index 00000000000..d5a123edb80
--- /dev/null
+++ b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
@@ -0,0 +1,151 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import _ from 'underscore';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+import ResizablePanel from '../resizable_panel.vue';
+import { GlSkeletonLoading } from '@gitlab/ui';
+
+export default {
+ name: 'CollapsibleSidebar',
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ ResizablePanel,
+ GlSkeletonLoading,
+ },
+ props: {
+ extensionTabs: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ side: {
+ type: String,
+ required: true,
+ },
+ width: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['loading']),
+ ...mapState({
+ isOpen(state) {
+ return state[this.namespace].isOpen;
+ },
+ currentView(state) {
+ return state[this.namespace].currentView;
+ },
+ isActiveView(state, getters) {
+ return getters[`${this.namespace}/isActiveView`];
+ },
+ isAliveView(_state, getters) {
+ return getters[`${this.namespace}/isAliveView`];
+ },
+ }),
+ namespace() {
+ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+ return `${this.side}Pane`;
+ },
+ tabs() {
+ return this.extensionTabs.filter(tab => tab.show);
+ },
+ tabViews() {
+ return _.flatten(this.tabs.map(tab => tab.views));
+ },
+ aliveTabViews() {
+ return this.tabViews.filter(view => this.isAliveView(view.name));
+ },
+ otherSide() {
+ return this.side === 'right' ? 'left' : 'right';
+ },
+ },
+ methods: {
+ ...mapActions({
+ toggleOpen(dispatch) {
+ return dispatch(`${this.namespace}/toggleOpen`);
+ },
+ open(dispatch, view) {
+ return dispatch(`${this.namespace}/open`, view);
+ },
+ }),
+ clickTab(e, tab) {
+ e.target.blur();
+
+ if (this.isActiveTab(tab)) {
+ this.toggleOpen();
+ } else {
+ this.open(tab.views[0]);
+ }
+ },
+ isActiveTab(tab) {
+ return tab.views.some(view => this.isActiveView(view.name));
+ },
+ buttonClasses(tab) {
+ return [
+ this.side === 'right' ? 'is-right' : '',
+ this.isActiveTab(tab) && this.isOpen ? 'active' : '',
+ ...(tab.buttonClasses || []),
+ ];
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ :class="`ide-${side}-sidebar`"
+ :data-qa-selector="`ide_${side}_sidebar`"
+ class="multi-file-commit-panel ide-sidebar"
+ >
+ <resizable-panel
+ v-show="isOpen"
+ :collapsible="false"
+ :initial-width="width"
+ :min-size="width"
+ :class="`ide-${side}-sidebar-${currentView}`"
+ :side="side"
+ class="multi-file-commit-panel-inner"
+ >
+ <div class="h-100 d-flex flex-column align-items-stretch">
+ <slot v-if="isOpen" name="header"></slot>
+ <div
+ v-for="tabView in aliveTabViews"
+ v-show="isActiveView(tabView.name)"
+ :key="tabView.name"
+ class="flex-fill js-tab-view"
+ >
+ <component :is="tabView.component" />
+ </div>
+ <slot name="footer"></slot>
+ </div>
+ </resizable-panel>
+ <nav class="ide-activity-bar">
+ <ul class="list-unstyled">
+ <li>
+ <slot name="header-icon"></slot>
+ </li>
+ <li v-for="tab of tabs" :key="tab.title">
+ <button
+ v-tooltip
+ :title="tab.title"
+ :aria-label="tab.title"
+ :class="buttonClasses(tab)"
+ data-container="body"
+ :data-placement="otherSide"
+ :data-qa-selector="`${tab.title.toLowerCase()}_tab_button`"
+ class="ide-sidebar-link"
+ type="button"
+ @click="clickTab($event, tab)"
+ >
+ <icon :size="16" :name="tab.icon" />
+ </button>
+ </li>
+ </ul>
+ </nav>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index 200391282e7..40ed7d9c422 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -1,27 +1,17 @@
<script>
-import { mapActions, mapState, mapGetters } from 'vuex';
-import _ from 'underscore';
+import { mapGetters, mapState } from 'vuex';
import { __ } from '~/locale';
-import tooltip from '../../../vue_shared/directives/tooltip';
-import Icon from '../../../vue_shared/components/icon.vue';
+import CollapsibleSidebar from './collapsible_sidebar.vue';
import { rightSidebarViews } from '../../constants';
+import MergeRequestInfo from '../merge_requests/info.vue';
import PipelinesList from '../pipelines/list.vue';
import JobsDetail from '../jobs/detail.vue';
-import MergeRequestInfo from '../merge_requests/info.vue';
-import ResizablePanel from '../resizable_panel.vue';
import Clientside from '../preview/clientside.vue';
export default {
- directives: {
- tooltip,
- },
+ name: 'RightPane',
components: {
- Icon,
- PipelinesList,
- JobsDetail,
- ResizablePanel,
- MergeRequestInfo,
- Clientside,
+ CollapsibleSidebar,
},
props: {
extensionTabs: {
@@ -32,103 +22,40 @@ export default {
},
computed: {
...mapState(['currentMergeRequestId', 'clientsidePreviewEnabled']),
- ...mapState('rightPane', ['isOpen', 'currentView']),
...mapGetters(['packageJson']),
- ...mapGetters('rightPane', ['isActiveView', 'isAliveView']),
showLivePreview() {
return this.packageJson && this.clientsidePreviewEnabled;
},
- defaultTabs() {
+ rightExtensionTabs() {
return [
{
- show: this.currentMergeRequestId,
+ show: Boolean(this.currentMergeRequestId),
title: __('Merge Request'),
- views: [rightSidebarViews.mergeRequestInfo],
+ views: [{ component: MergeRequestInfo, ...rightSidebarViews.mergeRequestInfo }],
icon: 'text-description',
},
{
show: true,
title: __('Pipelines'),
- views: [rightSidebarViews.pipelines, rightSidebarViews.jobsDetail],
+ views: [
+ { component: PipelinesList, ...rightSidebarViews.pipelines },
+ { component: JobsDetail, ...rightSidebarViews.jobsDetail },
+ ],
icon: 'rocket',
},
{
show: this.showLivePreview,
title: __('Live preview'),
- views: [rightSidebarViews.clientSidePreview],
+ views: [{ component: Clientside, ...rightSidebarViews.clientSidePreview }],
icon: 'live-preview',
},
+ ...this.extensionTabs,
];
},
- tabs() {
- return this.defaultTabs.concat(this.extensionTabs).filter(tab => tab.show);
- },
- tabViews() {
- return _.flatten(this.tabs.map(tab => tab.views));
- },
- aliveTabViews() {
- return this.tabViews.filter(view => this.isAliveView(view.name));
- },
- },
- methods: {
- ...mapActions('rightPane', ['toggleOpen', 'open']),
- clickTab(e, tab) {
- e.target.blur();
-
- if (this.isActiveTab(tab)) {
- this.toggleOpen();
- } else {
- this.open(tab.views[0]);
- }
- },
- isActiveTab(tab) {
- return tab.views.some(view => this.isActiveView(view.name));
- },
},
};
</script>
<template>
- <div class="multi-file-commit-panel ide-right-sidebar" data-qa-selector="ide_right_sidebar">
- <resizable-panel
- v-show="isOpen"
- :collapsible="false"
- :initial-width="350"
- :min-size="350"
- :class="`ide-right-sidebar-${currentView}`"
- side="right"
- class="multi-file-commit-panel-inner"
- >
- <div
- v-for="tabView in aliveTabViews"
- v-show="isActiveView(tabView.name)"
- :key="tabView.name"
- class="h-100"
- >
- <component :is="tabView.component || tabView.name" />
- </div>
- </resizable-panel>
- <nav class="ide-activity-bar">
- <ul class="list-unstyled">
- <li v-for="tab of tabs" :key="tab.title">
- <button
- v-tooltip
- :title="tab.title"
- :aria-label="tab.title"
- :class="{
- active: isActiveTab(tab) && isOpen,
- }"
- data-container="body"
- data-placement="left"
- :data-qa-selector="`${tab.title.toLowerCase()}_tab_button`"
- class="ide-sidebar-link is-right"
- type="button"
- @click="clickTab($event, tab)"
- >
- <icon :size="16" :name="tab.icon" />
- </button>
- </li>
- </ul>
- </nav>
- </div>
+ <collapsible-sidebar :extension-tabs="rightExtensionTabs" side="right" :width="350" />
</template>
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 5201c33b1b4..b3a7597e7bb 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -1,7 +1,6 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
-import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
@@ -11,7 +10,6 @@ import { activityBarViews, stageKeys } from '../constants';
export default {
components: {
DeprecatedModal,
- Icon,
CommitFilesList,
EmptyState,
},
@@ -96,7 +94,6 @@ export default {
:empty-state-text="__('There are no unstaged changes')"
action="stageAllChanges"
action-btn-icon="stage-all"
- item-action-component="stage-button"
class="is-first"
icon-name="unstaged"
/>
@@ -110,7 +107,6 @@ export default {
:empty-state-text="__('There are no staged changes')"
action="unstageAllChanges"
action-btn-icon="unstage-all"
- item-action-component="unstage-button"
icon-name="staged"
/>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 08b3e8a34d6..7e2ab96d1de 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -32,7 +32,13 @@ export default {
...mapState('rightPane', {
rightPaneIsOpen: 'isOpen',
}),
- ...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']),
+ ...mapState([
+ 'rightPanelCollapsed',
+ 'viewer',
+ 'panelResizing',
+ 'currentActivityView',
+ 'renderWhitespaceInCode',
+ ]),
...mapGetters([
'currentMergeRequest',
'getStagedFile',
@@ -76,6 +82,11 @@ export default {
showEditor() {
return !this.shouldHideEditor && this.isEditorViewMode;
},
+ editorOptions() {
+ return {
+ renderWhitespace: this.renderWhitespaceInCode ? 'all' : 'none',
+ };
+ },
},
watch: {
file(newVal, oldVal) {
@@ -131,7 +142,7 @@ export default {
},
mounted() {
if (!this.editor) {
- this.editor = Editor.create();
+ this.editor = Editor.create(this.editorOptions);
}
this.initEditor();
},
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index 4dbc4383894..1b7f149097b 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -1,13 +1,11 @@
<script>
import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue';
-import EditorMode from './editor_mode_dropdown.vue';
import router from '../ide_router';
export default {
components: {
RepoTab,
- EditorMode,
},
props: {
activeFile: {
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index cdfebd19fa4..4c4166e11f5 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -50,6 +50,7 @@ export function initIde(el, options = {}) {
});
this.setInitialData({
clientsidePreviewEnabled: parseBoolean(el.dataset.clientsidePreviewEnabled),
+ renderWhitespaceInCode: parseBoolean(el.dataset.renderWhitespaceInCode),
});
},
methods: {
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 02038fcb534..d1056ea6b98 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -23,20 +23,24 @@ export const clearDomElement = el => {
};
export default class Editor {
- static create() {
+ static create(options = {}) {
if (!this.editorInstance) {
- this.editorInstance = new Editor();
+ this.editorInstance = new Editor(options);
}
return this.editorInstance;
}
- constructor() {
+ constructor(options = {}) {
this.currentModel = null;
this.instance = null;
this.dirtyDiffController = null;
this.disposable = new Disposable();
this.modelManager = new ModelManager();
this.decorationsController = new DecorationsController(this);
+ this.options = {
+ ...defaultEditorOptions,
+ ...options,
+ };
setupMonacoTheme();
@@ -51,7 +55,7 @@ export default class Editor {
this.disposable.add(
(this.instance = monacoEditor.create(domElement, {
- ...defaultEditorOptions,
+ ...this.options,
})),
(this.dirtyDiffController = new DirtyDiffController(
this.modelManager,
@@ -71,7 +75,7 @@ export default class Editor {
this.disposable.add(
(this.instance = monacoEditor.createDiffEditor(domElement, {
- ...defaultEditorOptions,
+ ...this.options,
quickSuggestions: false,
occurrencesHighlight: false,
renderSideBySide: Editor.renderSideBySide(domElement),
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index dd69e2d6f1f..34e7cc304dd 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -16,21 +16,7 @@ export const redirectToUrl = (self, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const discardAllChanges = ({ state, commit, dispatch }) => {
- state.changedFiles.forEach(file => {
- if (file.tempFile || file.prevPath) dispatch('closeFile', file);
-
- if (file.tempFile) {
- dispatch('deleteEntry', file.path);
- } else if (file.prevPath) {
- dispatch('renameEntry', {
- path: file.path,
- name: file.prevName,
- parentPath: file.prevParentPath,
- });
- } else {
- commit(types.DISCARD_FILE_CHANGES, file.path);
- }
- });
+ state.changedFiles.forEach(file => dispatch('restoreOriginalFile', file.path));
commit(types.REMOVE_ALL_CHANGES_FILES);
};
@@ -47,79 +33,66 @@ 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);
};
export const createTempEntry = (
- { state, commit, dispatch },
+ { state, commit, dispatch, getters },
{ name, type, content = '', base64 = false, binary = false, rawPath = '' },
-) =>
- new Promise(resolve => {
- const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
-
- if (state.entries[name] && !state.entries[name].deleted) {
- flash(
- `The name "${name.split('/').pop()}" is already taken in this directory.`,
- 'alert',
- document,
- null,
- false,
- true,
- );
-
- resolve();
-
- return null;
- }
+) => {
+ const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
+
+ if (state.entries[name] && !state.entries[name].deleted) {
+ flash(
+ sprintf(__('The name "%{name}" is already taken in this directory.'), {
+ name: name.split('/').pop(),
+ }),
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
- const data = decorateFiles({
- data: [fullName],
- projectId: state.currentProjectId,
- branchId: state.currentBranchId,
- type,
- tempFile: true,
- content,
- base64,
- binary,
- rawPath,
- });
- const { file, parentPath } = data;
+ return;
+ }
- commit(types.CREATE_TMP_ENTRY, {
- data,
- projectId: state.currentProjectId,
- branchId: state.currentBranchId,
- });
+ const data = decorateFiles({
+ data: [fullName],
+ projectId: state.currentProjectId,
+ branchId: state.currentBranchId,
+ type,
+ tempFile: true,
+ content,
+ base64,
+ binary,
+ rawPath,
+ });
+ const { file, parentPath } = data;
- if (type === 'blob') {
- commit(types.TOGGLE_FILE_OPEN, file.path);
- commit(types.ADD_FILE_TO_CHANGED, file.path);
- dispatch('setFileActive', file.path);
- dispatch('triggerFilesChange');
- }
+ commit(types.CREATE_TMP_ENTRY, {
+ data,
+ projectId: state.currentProjectId,
+ branchId: state.currentBranchId,
+ });
- if (parentPath && !state.entries[parentPath].opened) {
- commit(types.TOGGLE_TREE_OPEN, parentPath);
- }
+ if (type === 'blob') {
+ commit(types.TOGGLE_FILE_OPEN, file.path);
- resolve(file);
+ if (gon.features?.stageAllByDefault)
+ commit(types.STAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) });
+ else commit(types.ADD_FILE_TO_CHANGED, file.path);
- return null;
- });
+ dispatch('setFileActive', file.path);
+ dispatch('triggerFilesChange');
+ dispatch('burstUnusedSeal');
+ }
+
+ if (parentPath && !state.entries[parentPath].opened) {
+ commit(types.TOGGLE_TREE_OPEN, parentPath);
+ }
+};
export const scrollToTab = () => {
Vue.nextTick(() => {
@@ -133,28 +106,40 @@ export const scrollToTab = () => {
});
};
-export const stageAllChanges = ({ state, commit, dispatch }) => {
+export const stageAllChanges = ({ state, commit, dispatch, getters }) => {
const openFile = state.openFiles[0];
commit(types.SET_LAST_COMMIT_MSG, '');
- state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
+ state.changedFiles.forEach(file =>
+ commit(types.STAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) }),
+ );
- dispatch('openPendingTab', {
- file: state.stagedFiles.find(f => f.path === openFile.path),
- keyPrefix: stageKeys.staged,
- });
+ const file = getters.getStagedFile(openFile.path);
+
+ if (file) {
+ dispatch('openPendingTab', {
+ file,
+ keyPrefix: stageKeys.staged,
+ });
+ }
};
-export const unstageAllChanges = ({ state, commit, dispatch }) => {
+export const unstageAllChanges = ({ state, commit, dispatch, getters }) => {
const openFile = state.openFiles[0];
- state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path));
+ state.stagedFiles.forEach(file =>
+ commit(types.UNSTAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) }),
+ );
- dispatch('openPendingTab', {
- file: state.changedFiles.find(f => f.path === openFile.path),
- keyPrefix: stageKeys.unstaged,
- });
+ const file = getters.getChangedFile(openFile.path);
+
+ if (file) {
+ dispatch('openPendingTab', {
+ file,
+ keyPrefix: stageKeys.unstaged,
+ });
+ }
};
export const updateViewer = ({ commit }, viewer) => {
@@ -212,8 +197,9 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => {
const entry = state.entries[path];
const { prevPath, prevName, prevParentPath } = entry;
const isTree = entry.type === 'tree';
+ const prevEntry = prevPath && state.entries[prevPath];
- if (prevPath) {
+ if (prevPath && (!prevEntry || prevEntry.deleted)) {
dispatch('renameEntry', {
path,
name: prevName,
@@ -222,7 +208,9 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => {
dispatch('deleteEntry', prevPath);
return;
}
- if (state.unusedSeal) dispatch('burstUnusedSeal');
+
+ dispatch('burstUnusedSeal');
+
if (entry.opened) dispatch('closeFile', entry);
if (isTree) {
@@ -241,9 +229,14 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => {
export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
-export const renameEntry = ({ dispatch, commit, state }, { path, name, parentPath }) => {
+export const renameEntry = ({ dispatch, commit, state, getters }, { path, name, parentPath }) => {
const entry = state.entries[path];
const newPath = parentPath ? `${parentPath}/${name}` : name;
+ const existingParent = parentPath && state.entries[parentPath];
+
+ if (parentPath && (!existingParent || existingParent.deleted)) {
+ dispatch('createTempEntry', { name: parentPath, type: 'tree' });
+ }
commit(types.RENAME_ENTRY, { path, name, parentPath });
@@ -266,7 +259,11 @@ export const renameEntry = ({ dispatch, commit, state }, { path, name, parentPat
if (isReset) {
commit(types.REMOVE_FILE_FROM_STAGED_AND_CHANGED, newEntry);
} else if (!isInChanges) {
- commit(types.ADD_FILE_TO_CHANGED, newPath);
+ if (gon.features?.stageAllByDefault)
+ commit(types.STAGE_CHANGE, { path: newPath, diffInfo: getters.getDiffInfo(newPath) });
+ else commit(types.ADD_FILE_TO_CHANGED, newPath);
+
+ dispatch('burstUnusedSeal');
}
if (!newEntry.tempFile) {
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 8864224c19e..70a966afa66 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -61,8 +61,10 @@ export const getFileData = (
{ path, makeFileActive = true, openFile = makeFileActive },
) => {
const file = state.entries[path];
+ const fileDeletedAndReadded = getters.isFileDeletedAndReadded(path);
- if (file.raw || (file.tempFile && !file.prevPath)) return Promise.resolve();
+ if (file.raw || (file.tempFile && !file.prevPath && !fileDeletedAndReadded))
+ return Promise.resolve();
commit(types.TOGGLE_LOADING, { entry: file });
@@ -102,11 +104,16 @@ export const setFileMrChange = ({ commit }, { file, mrChange }) => {
export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) => {
const file = state.entries[path];
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
return new Promise((resolve, reject) => {
+ const fileDeletedAndReadded = getters.isFileDeletedAndReadded(path);
service
- .getRawFileData(file)
+ .getRawFileData(fileDeletedAndReadded ? stagedFile : file)
.then(raw => {
- if (!(file.tempFile && !file.prevPath)) commit(types.SET_FILE_RAW_DATA, { file, raw });
+ if (!(file.tempFile && !file.prevPath && !fileDeletedAndReadded))
+ commit(types.SET_FILE_RAW_DATA, { file, raw, fileDeletedAndReadded });
+
if (file.mrChange && file.mrChange.new_file === false) {
const baseSha =
(getters.currentMergeRequest && getters.currentMergeRequest.baseCommitSha) || '';
@@ -140,7 +147,7 @@ export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) =
});
};
-export const changeFileContent = ({ commit, dispatch, state }, { path, content }) => {
+export const changeFileContent = ({ commit, dispatch, state, getters }, { path, content }) => {
const file = state.entries[path];
commit(types.UPDATE_FILE_CONTENT, {
path,
@@ -150,8 +157,10 @@ export const changeFileContent = ({ commit, dispatch, state }, { path, content }
const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path);
if (file.changed && indexOfChangedFile === -1) {
- commit(types.ADD_FILE_TO_CHANGED, path);
- } else if (!file.changed && indexOfChangedFile !== -1) {
+ if (gon.features?.stageAllByDefault)
+ commit(types.STAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) });
+ else commit(types.ADD_FILE_TO_CHANGED, path);
+ } else if (!file.changed && !file.tempFile && indexOfChangedFile !== -1) {
commit(types.REMOVE_FILE_FROM_CHANGED, path);
}
@@ -184,23 +193,40 @@ export const setFileViewMode = ({ commit }, { file, viewMode }) => {
commit(types.SET_FILE_VIEWMODE, { file, viewMode });
};
-export const discardFileChanges = ({ dispatch, state, commit, getters }, path) => {
+export const restoreOriginalFile = ({ dispatch, state, commit }, path) => {
const file = state.entries[path];
+ const isDestructiveDiscard = file.tempFile || file.prevPath;
if (file.deleted && file.parentPath) {
dispatch('restoreTree', file.parentPath);
}
- commit(types.DISCARD_FILE_CHANGES, path);
- commit(types.REMOVE_FILE_FROM_CHANGED, path);
+ if (isDestructiveDiscard) {
+ dispatch('closeFile', file);
+ }
+
+ if (file.tempFile) {
+ dispatch('deleteEntry', file.path);
+ } else {
+ commit(types.DISCARD_FILE_CHANGES, file.path);
+ }
if (file.prevPath) {
- dispatch('discardFileChanges', file.prevPath);
+ dispatch('renameEntry', {
+ path: file.path,
+ name: file.prevName,
+ parentPath: file.prevParentPath,
+ });
}
+};
- if (file.tempFile && file.opened) {
- commit(types.TOGGLE_FILE_OPEN, path);
- } else if (getters.activeFile && file.path === getters.activeFile.path) {
+export const discardFileChanges = ({ dispatch, state, commit, getters }, path) => {
+ const file = state.entries[path];
+ const isDestructiveDiscard = file.tempFile || file.prevPath;
+
+ dispatch('restoreOriginalFile', path);
+
+ if (!isDestructiveDiscard && file.path === getters.activeFile?.path) {
dispatch('updateDelayViewerUpdated', true)
.then(() => {
router.push(`/project${file.url}`);
@@ -210,24 +236,26 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) =
});
}
+ commit(types.REMOVE_FILE_FROM_CHANGED, path);
+
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, dispatch }, path) => {
- const stagedFile = state.stagedFiles.find(f => f.path === path);
- const openFile = state.openFiles.find(f => f.path === path);
+export const stageChange = ({ commit, dispatch, getters }, path) => {
+ const stagedFile = getters.getStagedFile(path);
+ const openFile = getters.getOpenFile(path);
- commit(types.STAGE_CHANGE, path);
+ commit(types.STAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) });
commit(types.SET_LAST_COMMIT_MSG, '');
if (stagedFile) {
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
}
- if (openFile && openFile.active) {
- const file = state.stagedFiles.find(f => f.path === path);
+ const file = getters.getStagedFile(path);
+ if (openFile && openFile.active && file) {
dispatch('openPendingTab', {
file,
keyPrefix: stageKeys.staged,
@@ -235,14 +263,14 @@ export const stageChange = ({ commit, state, dispatch }, path) => {
}
};
-export const unstageChange = ({ commit, dispatch, state }, path) => {
- const openFile = state.openFiles.find(f => f.path === path);
+export const unstageChange = ({ commit, dispatch, getters }, path) => {
+ const openFile = getters.getOpenFile(path);
- commit(types.UNSTAGE_CHANGE, path);
+ commit(types.UNSTAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) });
- if (openFile && openFile.active) {
- const file = state.changedFiles.find(f => f.path === path);
+ const file = getters.getChangedFile(path);
+ if (openFile && openFile.active && file) {
dispatch('openPendingTab', {
file,
keyPrefix: stageKeys.unstaged,
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index 6790c0fbdaa..806ec38430c 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -141,7 +141,7 @@ export const getMergeRequestVersions = (
});
export const openMergeRequest = (
- { dispatch, state },
+ { dispatch, state, getters },
{ projectId, targetProjectId, mergeRequestId } = {},
) =>
dispatch('getMergeRequestData', {
@@ -152,17 +152,18 @@ export const openMergeRequest = (
.then(mr => {
dispatch('setCurrentBranchId', mr.source_branch);
- // getFiles needs to be called after getting the branch data
- // since files are fetched using the last commit sha of the branch
return dispatch('getBranchData', {
projectId,
branchId: mr.source_branch,
- }).then(() =>
- dispatch('getFiles', {
+ }).then(() => {
+ const branch = getters.findBranch(projectId, mr.source_branch);
+
+ return dispatch('getFiles', {
projectId,
branchId: mr.source_branch,
- }),
- );
+ ref: branch.commit.id,
+ });
+ });
})
.then(() =>
dispatch('getMergeRequestVersions', {
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 20887e7d0ac..e206f9bee9e 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -83,8 +83,11 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
});
};
-export const showEmptyState = ({ commit, state }, { projectId, branchId }) => {
+export const showEmptyState = ({ commit, state, dispatch }, { projectId, branchId }) => {
const treePath = `${projectId}/${branchId}`;
+
+ dispatch('setCurrentBranchId', branchId);
+
commit(types.CREATE_TREE, { treePath });
commit(types.TOGGLE_LOADING, {
entry: state.trees[treePath],
@@ -111,7 +114,7 @@ export const loadFile = ({ dispatch, state }, { basePath }) => {
}
};
-export const loadBranch = ({ dispatch }, { projectId, branchId }) =>
+export const loadBranch = ({ dispatch, getters }, { projectId, branchId }) =>
dispatch('getBranchData', {
projectId,
branchId,
@@ -121,9 +124,13 @@ export const loadBranch = ({ dispatch }, { projectId, branchId }) =>
projectId,
branchId,
});
+
+ const branch = getters.findBranch(projectId, branchId);
+
return dispatch('getFiles', {
projectId,
branchId,
+ ref: branch.commit.id,
});
})
.catch(() => {
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 72cd099c5a5..ba85194b910 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -46,19 +46,20 @@ export const setDirectoryData = ({ state, commit }, { projectId, branchId, treeL
});
};
-export const getFiles = ({ state, commit, dispatch, getters }, { projectId, branchId } = {}) =>
+export const getFiles = ({ state, commit, dispatch }, payload = {}) =>
new Promise((resolve, reject) => {
+ const { projectId, branchId, ref = branchId } = payload;
+
if (
!state.trees[`${projectId}/${branchId}`] ||
(state.trees[`${projectId}/${branchId}`].tree &&
state.trees[`${projectId}/${branchId}`].tree.length === 0)
) {
const selectedProject = state.projects[projectId];
- const selectedBranch = getters.findBranch(projectId, branchId);
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
service
- .getFiles(selectedProject.web_url, selectedBranch.commit.id)
+ .getFiles(selectedProject.web_url, ref)
.then(({ data }) => {
const { entries, treeList } = decorateFiles({
data,
@@ -77,8 +78,8 @@ export const getFiles = ({ state, commit, dispatch, getters }, { projectId, bran
.catch(e => {
dispatch('setErrorMessage', {
text: __('An error occurred whilst loading all the files.'),
- action: payload =>
- dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)),
+ action: actionPayload =>
+ dispatch('getFiles', actionPayload).then(() => dispatch('setErrorMessage', null)),
actionText: __('Please try again'),
actionPayload: { projectId, branchId },
});
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index bb8374b4e78..2fc574cd343 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -64,6 +64,7 @@ export const allBlobs = state =>
export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path);
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
+export const getOpenFile = state => path => state.openFiles.find(f => f.path === path);
export const lastOpenedFile = state =>
[...state.changedFiles, ...state.stagedFiles].sort((a, b) => b.lastOpenedAt - a.lastOpenedAt)[0];
diff --git a/app/assets/javascripts/ide/stores/modules/pane/actions.js b/app/assets/javascripts/ide/stores/modules/pane/actions.js
index 7f5d167a14f..a8fcdf539ec 100644
--- a/app/assets/javascripts/ide/stores/modules/pane/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/pane/actions.js
@@ -1,17 +1,17 @@
import * as types from './mutation_types';
-export const toggleOpen = ({ dispatch, state }, view) => {
+export const toggleOpen = ({ dispatch, state }) => {
if (state.isOpen) {
dispatch('close');
} else {
- dispatch('open', view);
+ dispatch('open');
}
};
-export const open = ({ commit }, view) => {
+export const open = ({ state, commit }, view) => {
commit(types.SET_OPEN, true);
- if (view) {
+ if (view && view.name !== state.currentView) {
const { name, keepAlive } = view;
commit(types.SET_CURRENT_VIEW, name);
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index f0b4718d025..4dde53a9fdf 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -11,7 +11,6 @@ export const SET_LINKS = 'SET_LINKS';
// Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
-export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
// Merge Request Mutation Types
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 8caeb2d73b2..313fa1fe029 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -54,27 +54,29 @@ export default {
}
});
},
- [types.SET_FILE_RAW_DATA](state, { file, raw }) {
+ [types.SET_FILE_RAW_DATA](state, { file, raw, fileDeletedAndReadded = false }) {
const openPendingFile = state.openFiles.find(
- f => f.path === file.path && f.pending && !(f.tempFile && !f.prevPath),
+ f =>
+ f.path === file.path && f.pending && !(f.tempFile && !f.prevPath && !fileDeletedAndReadded),
);
+ const stagedFile = state.stagedFiles.find(f => f.path === file.path);
- if (file.tempFile && file.content === '') {
- Object.assign(state.entries[file.path], {
- content: raw,
- });
+ if (file.tempFile && file.content === '' && !fileDeletedAndReadded) {
+ Object.assign(state.entries[file.path], { content: raw });
+ } else if (fileDeletedAndReadded) {
+ Object.assign(stagedFile, { raw });
} else {
- Object.assign(state.entries[file.path], {
- raw,
- });
+ Object.assign(state.entries[file.path], { raw });
}
if (!openPendingFile) return;
if (!openPendingFile.tempFile) {
openPendingFile.raw = raw;
- } else if (openPendingFile.tempFile) {
+ } else if (openPendingFile.tempFile && !fileDeletedAndReadded) {
openPendingFile.content = raw;
+ } else if (fileDeletedAndReadded) {
+ Object.assign(stagedFile, { raw });
}
},
[types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
@@ -132,7 +134,7 @@ export default {
[types.DISCARD_FILE_CHANGES](state, path) {
const stagedFile = state.stagedFiles.find(f => f.path === path);
const entry = state.entries[path];
- const { deleted, prevPath } = entry;
+ const { deleted } = entry;
Object.assign(state.entries[path], {
content: stagedFile ? stagedFile.content : state.entries[path].raw,
@@ -146,12 +148,6 @@ export default {
: state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
parent.tree = sortTree(parent.tree.concat(entry));
- } else if (prevPath) {
- const parent = entry.parentPath
- ? state.entries[entry.parentPath]
- : state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
-
- parent.tree = parent.tree.filter(f => f.path !== path);
}
},
[types.ADD_FILE_TO_CHANGED](state, path) {
@@ -164,31 +160,32 @@ export default {
changedFiles: state.changedFiles.filter(f => f.path !== path),
});
},
- [types.STAGE_CHANGE](state, path) {
+ [types.STAGE_CHANGE](state, { path, diffInfo }) {
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,
+ staged: diffInfo.exists,
+ changed: diffInfo.changed,
+ tempFile: diffInfo.tempFile,
+ deleted: diffInfo.deleted,
}),
}),
});
if (stagedFile) {
- Object.assign(stagedFile, {
- ...state.entries[path],
- });
+ Object.assign(stagedFile, { ...state.entries[path] });
} else {
- Object.assign(state, {
- stagedFiles: state.stagedFiles.concat({
- ...state.entries[path],
- }),
- });
+ state.stagedFiles = [...state.stagedFiles, { ...state.entries[path] }];
+ }
+
+ if (!diffInfo.exists) {
+ state.stagedFiles = state.stagedFiles.filter(f => f.path !== path);
}
},
- [types.UNSTAGE_CHANGE](state, path) {
+ [types.UNSTAGE_CHANGE](state, { path, diffInfo }) {
const changedFile = state.changedFiles.find(f => f.path === path);
const stagedFile = state.stagedFiles.find(f => f.path === path);
@@ -201,9 +198,11 @@ export default {
changed: true,
});
- Object.assign(state, {
- changedFiles: state.changedFiles.concat(state.entries[path]),
- });
+ state.changedFiles = state.changedFiles.concat(state.entries[path]);
+ }
+
+ if (!diffInfo.exists) {
+ state.changedFiles = state.changedFiles.filter(f => f.path !== path);
}
Object.assign(state, {
@@ -211,6 +210,9 @@ export default {
entries: Object.assign(state.entries, {
[path]: Object.assign(state.entries[path], {
staged: false,
+ changed: diffInfo.changed,
+ tempFile: diffInfo.tempFile,
+ deleted: diffInfo.deleted,
}),
}),
});
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index d400b9831a9..6488389977c 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -31,4 +31,5 @@ export default () => ({
entry: {},
},
clientsidePreviewEnabled: false,
+ renderWhitespaceInCode: false,
});