diff options
Diffstat (limited to 'app/assets/javascripts/ide')
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, }); |