diff options
Diffstat (limited to 'app/assets/javascripts/ide')
28 files changed, 611 insertions, 485 deletions
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue index 704dff981df..a8459b011df 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -32,7 +32,6 @@ this.$emit('toggleCollapsed'); }, }, - }; </script> diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 7f29a355eca..89981ab2c65 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,70 +1,96 @@ <script> -import { mapState, mapGetters } from 'vuex'; -import ideSidebar from './ide_side_bar.vue'; -import ideContextbar from './ide_context_bar.vue'; -import repoTabs from './repo_tabs.vue'; -import repoFileButtons from './repo_file_buttons.vue'; -import ideStatusBar from './ide_status_bar.vue'; -import repoPreview from './repo_preview.vue'; -import repoEditor from './repo_editor.vue'; + import { mapState, mapGetters } from 'vuex'; + import ideSidebar from './ide_side_bar.vue'; + import ideContextbar from './ide_context_bar.vue'; + import repoTabs from './repo_tabs.vue'; + import repoFileButtons from './repo_file_buttons.vue'; + import ideStatusBar from './ide_status_bar.vue'; + import repoPreview from './repo_preview.vue'; + import repoEditor from './repo_editor.vue'; -export default { - computed: { - ...mapState([ - 'currentBlobView', - 'selectedFile', - ]), - ...mapGetters([ - 'changedFiles', - 'activeFile', - ]), - }, - components: { - ideSidebar, - ideContextbar, - repoTabs, - repoFileButtons, - ideStatusBar, - repoEditor, - repoPreview, - }, - mounted() { - const returnValue = 'Are you sure you want to lose unsaved changes?'; - window.onbeforeunload = (e) => { - if (!this.changedFiles.length) return undefined; + export default { + components: { + ideSidebar, + ideContextbar, + repoTabs, + repoFileButtons, + ideStatusBar, + repoEditor, + repoPreview, + }, + props: { + emptyStateSvgPath: { + type: String, + required: true, + }, + }, + computed: { + ...mapState([ + 'currentBlobView', + 'selectedFile', + ]), + ...mapGetters([ + 'changedFiles', + 'activeFile', + ]), + }, + mounted() { + const returnValue = 'Are you sure you want to lose unsaved changes?'; + window.onbeforeunload = (e) => { + if (!this.changedFiles.length) return undefined; - Object.assign(e, { - returnValue, - }); - return returnValue; - }; - }, -}; + Object.assign(e, { + returnValue, + }); + return returnValue; + }; + }, + }; </script> <template> - <div + <div class="ide-view" > - <ide-sidebar/> + <ide-sidebar /> <div class="multi-file-edit-pane" > <template - v-if="activeFile"> + v-if="activeFile" + > <repo-tabs/> <component class="multi-file-edit-pane-content" :is="currentBlobView" /> - <repo-file-buttons/> + <repo-file-buttons /> <ide-status-bar - :file="selectedFile"/> + :file="selectedFile" + /> </template> <template - v-else> + v-else + > <div class="ide-empty-state"> - <h2 class="clgray">Welcome to the GitLab IDE</h2> + <div class="row js-empty-state"> + <div class="col-xs-12"> + <div class="svg-content svg-250"> + <img :src="emptyStateSvgPath" /> + </div> + </div> + <div class="col-xs-12"> + <div class="text-content text-center"> + <h4> + Welcome to the GitLab IDE + </h4> + <p> + You can select a file in the left sidebar to begin + editing and use the right sidebar to commit your changes. + </p> + </div> + </div> + </div> </div> </template> </div> diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue index 5a08718e386..dd947f66969 100644 --- a/app/assets/javascripts/ide/components/ide_context_bar.vue +++ b/app/assets/javascripts/ide/components/ide_context_bar.vue @@ -1,36 +1,59 @@ <script> -import { mapGetters, mapState, mapActions } from 'vuex'; -import repoCommitSection from './repo_commit_section.vue'; -import icon from '../../vue_shared/components/icon.vue'; + import { mapGetters, mapState, mapActions } from 'vuex'; + import repoCommitSection from './repo_commit_section.vue'; + import icon from '../../vue_shared/components/icon.vue'; + import panelResizer from '../../vue_shared/components/panel_resizer.vue'; -export default { - components: { - repoCommitSection, - icon, - }, - computed: { - ...mapState([ - 'rightPanelCollapsed', - ]), - ...mapGetters([ - 'changedFiles', - ]), - currentIcon() { - return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right'; + export default { + components: { + repoCommitSection, + icon, + panelResizer, }, - }, - methods: { - ...mapActions([ - 'setPanelCollapsedStatus', - ]), - toggleCollapsed() { - this.setPanelCollapsedStatus({ - side: 'right', - collapsed: !this.rightPanelCollapsed, - }); + data() { + return { + width: 290, + }; }, - }, -}; + computed: { + ...mapState([ + 'rightPanelCollapsed', + ]), + ...mapGetters([ + 'changedFiles', + ]), + currentIcon() { + return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right'; + }, + maxSize() { + return window.innerWidth / 2; + }, + panelStyle() { + if (!this.rightPanelCollapsed) { + return { width: `${this.width}px` }; + } + return {}; + }, + }, + methods: { + ...mapActions([ + 'setPanelCollapsedStatus', + 'setResizingStatus', + ]), + toggleCollapsed() { + this.setPanelCollapsedStatus({ + side: 'right', + collapsed: !this.rightPanelCollapsed, + }); + }, + resizingStarted() { + this.setResizingStatus(true); + }, + resizingEnded() { + this.setResizingStatus(false); + }, + }, + }; </script> <template> @@ -39,18 +62,19 @@ export default { :class="{ 'is-collapsed': rightPanelCollapsed, }" + :style="panelStyle" > - <div - class="multi-file-commit-panel-section"> + <div class="multi-file-commit-panel-section"> <header class="multi-file-commit-panel-header" :class="{ - 'is-collapsed': rightPanelCollapsed, - }" - > + 'is-collapsed': rightPanelCollapsed, + }" + > <div class="multi-file-commit-panel-header-title" - v-if="!rightPanelCollapsed"> + v-if="!rightPanelCollapsed" + > <icon name="list-bulleted" :size="18" @@ -68,8 +92,17 @@ export default { /> </button> </header> - <repo-commit-section - class=""/> + <repo-commit-section /> </div> + <panel-resizer + :size.sync="width" + :enabled="!rightPanelCollapsed" + :start-size="290" + :min-size="200" + :max-size="maxSize" + @resize-start="resizingStarted" + @resize-end="resizingEnded" + side="left" + /> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue index bd3a521ff43..af2f7341a91 100644 --- a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue +++ b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue @@ -28,20 +28,20 @@ export default { <div class="branch-header-title"> <icon name="branch" - :size="12"> - </icon> + :size="12" + /> {{ branch.name }} </div> <div class="branch-header-btns"> <new-dropdown :project-id="projectId" :branch="branch.name" - path=""/> + path="" + /> </div> </div> <div> - <repo-tree - :treeId="branch.treeId"/> + <repo-tree :tree-id="branch.treeId" /> </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue index 61daba6d176..ed49a0e72a2 100644 --- a/app/assets/javascripts/ide/components/ide_project_tree.vue +++ b/app/assets/javascripts/ide/components/ide_project_tree.vue @@ -19,9 +19,10 @@ export default { <template> <div class="projects-sidebar"> <div class="context-header"> - <a - :title="project.name" - :href="project.web_url"> + <a + :title="project.name" + :href="project.web_url" + > <div class="avatar-container s40 project-avatar"> <project-avatar-image class="avatar-container project-avatar" @@ -29,7 +30,7 @@ export default { :img-src="project.avatar_url" :img-alt="project.name" :img-size="40" - /> + /> </div> <div class="sidebar-context-title"> {{ project.name }} @@ -38,10 +39,11 @@ export default { </div> <div class="multi-file-commit-panel-inner-scroll"> <branches-tree - v-for="(branch, index) in project.branches" + v-for="branch in project.branches" :key="branch.name" :project-id="project.path_with_namespace" - :branch="branch"/> + :branch="branch" + /> </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue index b6b089e6b25..4651e345d75 100644 --- a/app/assets/javascripts/ide/components/ide_repo_tree.vue +++ b/app/assets/javascripts/ide/components/ide_repo_tree.vue @@ -1,15 +1,15 @@ <script> import { mapState } from 'vuex'; -import RepoPreviousDirectory from './repo_prev_directory.vue'; -import RepoFile from './repo_file.vue'; -import RepoLoadingFile from './repo_loading_file.vue'; +import repoPreviousDirectory from './repo_prev_directory.vue'; +import repoFile from './repo_file.vue'; +import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; import { treeList } from '../stores/utils'; export default { components: { - 'repo-previous-directory': RepoPreviousDirectory, - 'repo-file': RepoFile, - 'repo-loading-file': RepoLoadingFile, + repoPreviousDirectory, + repoFile, + skeletonLoadingContainer, }, props: { treeId: { @@ -19,7 +19,7 @@ export default { }, computed: { ...mapState([ - 'loading', + 'trees', 'isRoot', ]), ...mapState({ @@ -34,33 +34,41 @@ export default { return !this.isRoot && this.fetchedList.length; }, showLoading() { - return this.loading; + if (this.trees[this.treeId]) { + return this.trees[this.treeId].loading; + } + return true; }, }, }; </script> <template> -<div> - <div class="ide-file-list"> - <table class="table"> - <tbody - v-if="treeId"> - <repo-previous-directory - v-if="hasPreviousDirectory" - /> - <repo-loading-file - v-if="showLoading" - v-for="n in 5" - :key="n" - /> - <repo-file - v-for="file in fetchedList" - :key="file.key" - :file="file" - /> - </tbody> - </table> + <div> + <div class="ide-file-list"> + <table class="table"> + <tbody + v-if="treeId" + > + <repo-previous-directory + v-if="hasPreviousDirectory" + /> + <template v-if="showLoading"> + <div + class="multi-file-loading-container" + v-for="n in 3" + :key="n" + > + <skeleton-loading-container /> + </div> + </template> + <repo-file + v-for="file in fetchedList" + :key="file.key" + :file="file" + /> + </tbody> + </table> + </div> </div> -</div> </template> diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index 535398d98c2..a68f8ce0169 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,48 +1,88 @@ <script> -import { mapState, mapActions } from 'vuex'; -import projectTree from './ide_project_tree.vue'; -import icon from '../../vue_shared/components/icon.vue'; + import { mapState, mapActions } from 'vuex'; + import projectTree from './ide_project_tree.vue'; + import icon from '../../vue_shared/components/icon.vue'; + import panelResizer from '../../vue_shared/components/panel_resizer.vue'; + import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; -export default { - components: { - projectTree, - icon, - }, - computed: { - ...mapState([ - 'projects', - 'leftPanelCollapsed', - ]), - currentIcon() { - return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left'; + export default { + components: { + projectTree, + icon, + panelResizer, + skeletonLoadingContainer, }, - }, - methods: { - ...mapActions([ - 'setPanelCollapsedStatus', - ]), - toggleCollapsed() { - this.setPanelCollapsedStatus({ - side: 'left', - collapsed: !this.leftPanelCollapsed, - }); + data() { + return { + width: 290, + }; }, - }, -}; + computed: { + ...mapState([ + 'loading', + 'projects', + 'leftPanelCollapsed', + ]), + currentIcon() { + return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left'; + }, + maxSize() { + return window.innerWidth / 2; + }, + panelStyle() { + if (!this.leftPanelCollapsed) { + return { width: `${this.width}px` }; + } + return {}; + }, + showLoading() { + return this.loading; + }, + }, + methods: { + ...mapActions([ + 'setPanelCollapsedStatus', + 'setResizingStatus', + ]), + toggleCollapsed() { + this.setPanelCollapsedStatus({ + side: 'left', + collapsed: !this.leftPanelCollapsed, + }); + }, + resizingStarted() { + this.setResizingStatus(true); + }, + resizingEnded() { + this.setResizingStatus(false); + }, + }, + }; </script> <template> <div - class="multi-file-commit-panel" - :class="{ - 'is-collapsed': leftPanelCollapsed, - }" - > + class="multi-file-commit-panel" + :class="{ + 'is-collapsed': leftPanelCollapsed, + }" + :style="panelStyle" + > <div class="multi-file-commit-panel-inner"> + <template v-if="showLoading"> + <div + class="multi-file-loading-container" + v-for="n in 3" + :key="n" + > + <skeleton-loading-container /> + </div> + </template> <project-tree - v-for="(project, index) in projects" + v-for="project in projects" :key="project.id" - :project="project"/> + :project="project" + /> </div> <button type="button" @@ -56,7 +96,19 @@ export default { <span v-if="!leftPanelCollapsed" class="collapse-text" - >Collapse sidebar</span> + > + Collapse sidebar + </span> </button> + <panel-resizer + :size.sync="width" + :enabled="!leftPanelCollapsed" + :start-size="290" + :min-size="200" + :max-size="maxSize" + @resize-start="resizingStarted" + @resize-end="resizingEnded" + side="right" + /> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index a24abadd936..e48c446c4a4 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -1,70 +1,65 @@ <script> -import { mapState } from 'vuex'; -import icon from '../../vue_shared/components/icon.vue'; -import tooltip from '../../vue_shared/directives/tooltip'; -import timeAgoMixin from '../../vue_shared/mixins/timeago'; + import { mapState } from 'vuex'; + import icon from '../../vue_shared/components/icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; + import timeAgoMixin from '../../vue_shared/mixins/timeago'; -export default { - props: { - file: { - type: Object, - required: true, + export default { + components: { + icon, }, - }, - components: { - icon, - }, - directives: { - tooltip, - }, - mixins: [ - timeAgoMixin, - ], - computed: { - ...mapState([ - 'selectedFile', - ]), - }, -}; + directives: { + tooltip, + }, + mixins: [ + timeAgoMixin, + ], + props: { + file: { + type: Object, + required: true, + }, + }, + computed: { + ...mapState([ + 'selectedFile', + ]), + }, + }; </script> <template> - <div - class="ide-status-bar"> + <div class="ide-status-bar"> <div> <icon name="branch" - :size="12"> - </icon> + :size="12" + /> {{ selectedFile.branchId }} </div> <div> - <div - v-if="selectedFile.lastCommit && selectedFile.lastCommit.id"> + <div v-if="selectedFile.lastCommit && selectedFile.lastCommit.id"> Last commit: <a v-tooltip :title="selectedFile.lastCommit.message" - :href="selectedFile.lastCommit.url"> - {{ timeFormated(selectedFile.lastCommit.updatedAt) }} by + :href="selectedFile.lastCommit.url" + > + {{ timeFormated(selectedFile.lastCommit.updatedAt) }} by {{ selectedFile.lastCommit.author }} </a> - </div> + </div> </div> - <div - class="text-right"> + <div class="text-right"> {{ selectedFile.name }} </div> - <div - class="text-right"> + <div class="text-right"> {{ selectedFile.eol }} </div> - <div - class="text-right"> + <div class="text-right"> {{ file.editorRow }}:{{ file.editorColumn }} </div> - <div - class="text-right"> + <div class="text-right"> {{ selectedFile.fileLanguage }} </div> </div> diff --git a/app/assets/javascripts/ide/components/new_branch_form.vue b/app/assets/javascripts/ide/components/new_branch_form.vue index 2119d373d31..56e31256132 100644 --- a/app/assets/javascripts/ide/components/new_branch_form.vue +++ b/app/assets/javascripts/ide/components/new_branch_form.vue @@ -21,6 +21,13 @@ return this.loading || this.branchName === ''; }, }, + created() { + // Dropdown is outside of Vue instance & is controlled by Bootstrap + this.$dropdown = $('.git-revision-dropdown'); + + // text element is outside Vue app + this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text'); + }, methods: { ...mapActions([ 'createNewBranch', @@ -55,13 +62,6 @@ })); }, }, - created() { - // Dropdown is outside of Vue instance & is controlled by Bootstrap - this.$dropdown = $('.git-revision-dropdown'); - - // text element is outside Vue app - this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text'); - }, }; </script> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index 6e67e99a70f..ef653357f5f 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -4,6 +4,11 @@ import icon from '../../../vue_shared/components/icon.vue'; export default { + components: { + icon, + newModal, + upload, + }, props: { branch: { type: String, @@ -18,11 +23,6 @@ default: null, }, }, - components: { - icon, - newModal, - upload, - }, data() { return { openModal: false, @@ -32,10 +32,10 @@ methods: { createNewItem(type) { this.modalType = type; - this.toggleModalOpen(); + this.openModal = true; }, - toggleModalOpen() { - this.openModal = !this.openModal; + hideModal() { + this.openModal = false; }, }, }; @@ -95,7 +95,7 @@ :branch-id="branch" :path="path" :parent="parent" - @toggle="toggleModalOpen" + @hide="hideModal" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index a0650d37690..36cd825c6dd 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -4,6 +4,9 @@ import modal from '../../../vue_shared/components/modal.vue'; export default { + components: { + modal, + }, props: { branchId: { type: String, @@ -27,28 +30,6 @@ entryName: this.path !== '' ? `${this.path}/` : '', }; }, - components: { - modal, - }, - methods: { - ...mapActions([ - 'createTempEntry', - ]), - createEntryInStore() { - this.createTempEntry({ - projectId: this.currentProjectId, - branchId: this.branchId, - parent: this.parent, - name: this.entryName.replace(new RegExp(`^${this.path}/`), ''), - type: this.type, - }); - - this.toggleModalOpen(); - }, - toggleModalOpen() { - this.$emit('toggle'); - }, - }, computed: { ...mapState([ 'currentProjectId', @@ -78,6 +59,25 @@ mounted() { this.$refs.fieldName.focus(); }, + methods: { + ...mapActions([ + 'createTempEntry', + ]), + createEntryInStore() { + this.createTempEntry({ + projectId: this.currentProjectId, + branchId: this.branchId, + parent: this.parent, + name: this.entryName.replace(new RegExp(`^${this.path}/`), ''), + type: this.type, + }); + + this.hideModal(); + }, + hideModal() { + this.$emit('hide'); + }, + }, }; </script> @@ -86,7 +86,7 @@ :title="modalTitle" :primary-button-label="buttonLabel" kind="success" - @toggle="toggleModalOpen" + @cancel="hideModal" @submit="createEntryInStore" > <form diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue index 2a2f2a241fc..6244737fa43 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -18,6 +18,12 @@ 'currentProjectId', ]), }, + mounted() { + this.$refs.fileUpload.addEventListener('change', this.openFile); + }, + beforeDestroy() { + this.$refs.fileUpload.removeEventListener('change', this.openFile); + }, methods: { ...mapActions([ 'createTempEntry', @@ -59,12 +65,6 @@ this.$refs.fileUpload.click(); }, }, - mounted() { - this.$refs.fileUpload.addEventListener('change', this.openFile); - }, - beforeDestroy() { - this.$refs.fileUpload.removeEventListener('change', this.openFile); - }, }; </script> diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 470db2c9650..5279417a72a 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -49,7 +49,9 @@ export default { const createNewBranch = newBranch || this.startNewMR; const payload = { - branch: createNewBranch ? `${this.currentBranchId}-${new Date().getTime().toString()}` : this.currentBranchId, + branch: createNewBranch ? + `${this.currentBranchId}-${new Date().getTime().toString()}` : + this.currentBranchId, commit_message: this.commitMessage, actions: this.changedFiles.map(f => ({ action: f.tempFile ? 'create' : 'update', @@ -103,69 +105,70 @@ export default { </script> <template> -<div class="multi-file-commit-panel-section"> - <modal - v-if="showNewBranchModal" - :primary-button-label="__('Create new branch')" - kind="primary" - :title="__('Branch has changed')" - :text="__('This branch has changed since you started editing. Would you like to create a new branch?')" - @toggle="showNewBranchModal = false" - @submit="makeCommit(true)" - /> - <commit-files-list - title="Staged" - :file-list="changedFiles" - :collapsed="rightPanelCollapsed" - @toggleCollapsed="toggleCollapsed" - /> - <form - class="form-horizontal multi-file-commit-form" - @submit.prevent="tryCommit" - v-if="!rightPanelCollapsed" - > - <div class="multi-file-commit-fieldset"> - <textarea - class="form-control multi-file-commit-message" - name="commit-message" - v-model="commitMessage" - placeholder="Commit message" - > - </textarea> - </div> - <div class="multi-file-commit-fieldset"> - <label - v-tooltip - title="Create a new merge request with these changes" - data-container="body" - data-placement="top" - > - <input - type="checkbox" - v-model="startNewMR" - /> - Merge Request - </label> - <button - type="submit" - :disabled="commitButtonDisabled" - class="btn btn-default btn-sm append-right-10 prepend-left-10" - > - <i - v-if="submitCommitsLoading" - class="js-commit-loading-icon fa fa-spinner fa-spin" - aria-hidden="true" - aria-label="loading" + <div class="multi-file-commit-panel-section"> + <modal + v-if="showNewBranchModal" + :primary-button-label="__('Create new branch')" + kind="primary" + :title="__('Branch has changed')" + :text="__(`This branch has changed since +you started editing. Would you like to create a new branch?`)" + @cancel="showNewBranchModal = false" + @submit="makeCommit(true)" + /> + <commit-files-list + title="Staged" + :file-list="changedFiles" + :collapsed="rightPanelCollapsed" + @toggleCollapsed="toggleCollapsed" + /> + <form + class="form-horizontal multi-file-commit-form" + @submit.prevent="tryCommit" + v-if="!rightPanelCollapsed" + > + <div class="multi-file-commit-fieldset"> + <textarea + class="form-control multi-file-commit-message" + name="commit-message" + v-model="commitMessage" + placeholder="Commit message" > - </i> - Commit - </button> - <div - class="multi-file-commit-message-count" - > - {{ commitMessageCount }} + </textarea> </div> - </div> - </form> -</div> + <div class="multi-file-commit-fieldset"> + <label + v-tooltip + title="Create a new merge request with these changes" + data-container="body" + data-placement="top" + > + <input + type="checkbox" + v-model="startNewMR" + /> + Merge Request + </label> + <button + type="submit" + :disabled="commitButtonDisabled" + class="btn btn-default btn-sm append-right-10 prepend-left-10" + > + <i + v-if="submitCommitsLoading" + class="js-commit-loading-icon fa fa-spinner fa-spin" + aria-hidden="true" + aria-label="loading" + > + </i> + Commit + </button> + <div + class="multi-file-commit-message-count" + > + {{ commitMessageCount }} + </div> + </div> + </form> + </div> </template> diff --git a/app/assets/javascripts/ide/components/repo_edit_button.vue b/app/assets/javascripts/ide/components/repo_edit_button.vue index 37bd9003e96..c43e9163340 100644 --- a/app/assets/javascripts/ide/components/repo_edit_button.vue +++ b/app/assets/javascripts/ide/components/repo_edit_button.vue @@ -40,7 +40,7 @@ export default { aria-hidden="true"> </i> <span> - {{buttonLabel}} + {{ buttonLabel }} </span> </button> <modal @@ -50,7 +50,7 @@ export default { kind="warning" :title="__('Are you sure?')" :text="__('Are you sure you want to discard your changes?')" - @toggle="closeDiscardPopup" + @cancel="closeDiscardPopup" @submit="toggleEditMode(true)" /> </div> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 221be4b9074..83b82ae44c9 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -6,6 +6,38 @@ import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; export default { + computed: { + ...mapGetters([ + 'activeFile', + 'activeFileExtension', + ]), + ...mapState([ + 'leftPanelCollapsed', + 'rightPanelCollapsed', + 'panelResizing', + ]), + shouldHideEditor() { + return this.activeFile.binary && !this.activeFile.raw; + }, + }, + watch: { + activeFile(oldVal, newVal) { + if (newVal && !newVal.active) { + this.initMonaco(); + } + }, + leftPanelCollapsed() { + this.editor.updateDimensions(); + }, + rightPanelCollapsed() { + this.editor.updateDimensions(); + }, + panelResizing(isResizing) { + if (isResizing === false) { + this.editor.updateDimensions(); + } + }, + }, beforeDestroy() { this.editor.dispose(); }, @@ -78,32 +110,6 @@ export default { }); }, }, - watch: { - activeFile(oldVal, newVal) { - if (newVal && !newVal.active) { - this.initMonaco(); - } - }, - leftPanelCollapsed() { - this.editor.updateDimensions(); - }, - rightPanelCollapsed() { - this.editor.updateDimensions(); - }, - }, - computed: { - ...mapGetters([ - 'activeFile', - 'activeFileExtension', - ]), - ...mapState([ - 'leftPanelCollapsed', - 'rightPanelCollapsed', - ]), - shouldHideEditor() { - return this.activeFile.binary && !this.activeFile.raw; - }, - }, }; </script> diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index 09ca11531b1..f7f4db89bdf 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -3,15 +3,17 @@ import timeAgoMixin from '../../vue_shared/mixins/timeago'; import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue'; import newDropdown from './new_dropdown/index.vue'; + import fileIcon from '../../vue_shared/components/file_icon.vue'; export default { - mixins: [ - timeAgoMixin, - ], components: { skeletonLoadingContainer, newDropdown, + fileIcon, }, + mixins: [ + timeAgoMixin, + ], props: { file: { type: Object, @@ -26,13 +28,6 @@ ...mapState([ 'leftPanelCollapsed', ]), - fileIcon() { - return { - 'fa-spinner fa-spin': this.file.loading, - [this.file.icon]: !this.file.loading, - 'fa-folder-open': !this.file.loading && this.file.opened, - }; - }, isSubmodule() { return this.file.type === 'submodule'; }, @@ -65,6 +60,11 @@ }; }, }, + updated() { + if (this.file.type === 'blob' && this.file.active) { + this.$el.scrollIntoView(); + } + }, methods: { clickFile(row) { // Manual Action if a tree is selected/opened @@ -77,11 +77,6 @@ this.$router.push(`/project${row.url}`); }, }, - updated() { - if (this.file.type === 'blob' && this.file.active) { - this.$el.scrollIntoView(); - } - }, }; </script> @@ -94,16 +89,17 @@ class="multi-file-table-name" :colspan="submoduleColSpan" > - <i - class="fa fa-fw file-icon" - :class="fileIcon" - :style="levelIndentation" - aria-hidden="true" - > - </i> <a class="repo-file-name" > + <file-icon + :file-name="file.name" + :loading="file.loading" + :folder="file.type === 'tree'" + :opened="file.opened" + :style="levelIndentation" + :size="16" + /> {{ file.name }} </a> <new-dropdown @@ -111,7 +107,8 @@ :project-id="file.projectId" :branch="file.branchId" :path="file.path" - :parent="file"/> + :parent="file" + /> <i class="fa" v-if="changedClass" diff --git a/app/assets/javascripts/ide/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue index 34f0d51819a..aabc0d8eada 100644 --- a/app/assets/javascripts/ide/components/repo_file_buttons.vue +++ b/app/assets/javascripts/ide/components/repo_file_buttons.vue @@ -35,20 +35,24 @@ export default { <div class="btn-group" role="group" - aria-label="File actions"> + aria-label="File actions" + > <a :href="activeFile.blamePath" - class="btn btn-default btn-sm blame"> + class="btn btn-default btn-sm blame" + > Blame </a> <a :href="activeFile.commitsPath" - class="btn btn-default btn-sm history"> + class="btn btn-default btn-sm history" + > History </a> <a :href="activeFile.permalink" - class="btn btn-default btn-sm permalink"> + class="btn btn-default btn-sm permalink" + > Permalink </a> </div> diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue index 7eb840c7608..3aeb6f0b28f 100644 --- a/app/assets/javascripts/ide/components/repo_loading_file.vue +++ b/app/assets/javascripts/ide/components/repo_loading_file.vue @@ -25,15 +25,13 @@ /> </td> <template v-if="!leftPanelCollapsed"> - <td - class="hidden-sm hidden-xs"> + <td class="hidden-sm hidden-xs"> <skeleton-loading-container :small="true" /> </td> - <td - class="hidden-xs"> + <td class="hidden-xs"> <skeleton-loading-container class="animation-container-right" :small="true" diff --git a/app/assets/javascripts/ide/components/repo_preview.vue b/app/assets/javascripts/ide/components/repo_preview.vue index 3d1e0297bd5..e47270a9855 100644 --- a/app/assets/javascripts/ide/components/repo_preview.vue +++ b/app/assets/javascripts/ide/components/repo_preview.vue @@ -1,65 +1,71 @@ <script> -import { mapGetters } from 'vuex'; -import LineHighlighter from '../../line_highlighter'; -import syntaxHighlight from '../../syntax_highlight'; + import { mapGetters } from 'vuex'; + import LineHighlighter from '../../line_highlighter'; + import syntaxHighlight from '../../syntax_highlight'; -export default { - computed: { - ...mapGetters([ - 'activeFile', - ]), - renderErrorTooLarge() { - return this.activeFile.renderError === 'too_large'; + export default { + computed: { + ...mapGetters([ + 'activeFile', + ]), + renderErrorTooLarge() { + return this.activeFile.renderError === 'too_large'; + }, }, - }, - methods: { - highlightFile() { - syntaxHighlight($(this.$el).find('.file-content')); - }, - }, - mounted() { - this.highlightFile(); - this.lineHighlighter = new LineHighlighter({ - fileHolderSelector: '.blob-viewer-container', - scrollFileHolder: true, - }); - }, - updated() { - this.$nextTick(() => { + mounted() { this.highlightFile(); - }); - }, -}; + this.lineHighlighter = new LineHighlighter({ + fileHolderSelector: '.blob-viewer-container', + scrollFileHolder: true, + }); + }, + updated() { + this.$nextTick(() => { + this.highlightFile(); + }); + }, + methods: { + highlightFile() { + syntaxHighlight($(this.$el).find('.file-content')); + }, + }, + }; </script> <template> -<div> - <div - v-if="!activeFile.renderError" - v-html="activeFile.html" - class="multi-file-preview-holder" - > - </div> - <div - v-else-if="activeFile.tempFile" - class="vertical-center render-error"> - <p class="text-center"> - The source could not be displayed for this temporary file. - </p> - </div> - <div - v-else-if="renderErrorTooLarge" - class="vertical-center render-error"> - <p class="text-center"> - The source could not be displayed because it is too large. You can <a :href="activeFile.rawPath" download>download</a> it instead. - </p> - </div> - <div - v-else - class="vertical-center render-error"> - <p class="text-center"> - The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.rawPath" download>download</a> it instead. - </p> + <div> + <div + v-if="!activeFile.renderError" + v-html="activeFile.html" + class="multi-file-preview-holder" + > + </div> + <div + v-else-if="activeFile.tempFile" + class="vertical-center render-error"> + <p class="text-center"> + The source could not be displayed for this temporary file. + </p> + </div> + <div + v-else-if="renderErrorTooLarge" + class="vertical-center render-error"> + <p class="text-center"> + The source could not be displayed because it is too large. + You can <a + :href="activeFile.rawPath" + download>download</a> it instead. + </p> + </div> + <div + v-else + class="vertical-center render-error"> + <p class="text-center"> + The source could not be displayed because a rendering error occurred. + You can <a + :href="activeFile.rawPath" + download>download</a> it instead. + </p> + </div> </div> -</div> </template> diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index 5bd63ac9ec5..5ed7bddf6ae 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -1,45 +1,46 @@ <script> -import { mapActions } from 'vuex'; + import { mapActions } from 'vuex'; + import fileIcon from '../../vue_shared/components/file_icon.vue'; -export default { - props: { - tab: { - type: Object, - required: true, + export default { + components: { + fileIcon, }, - }, - - computed: { - closeLabel() { - if (this.tab.changed || this.tab.tempFile) { - return `${this.tab.name} changed`; - } - return `Close ${this.tab.name}`; + props: { + tab: { + type: Object, + required: true, + }, }, - changedClass() { - const tabChangedObj = { - 'fa-times close-icon': !this.tab.changed && !this.tab.tempFile, - 'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile, - }; - return tabChangedObj; + computed: { + closeLabel() { + if (this.tab.changed || this.tab.tempFile) { + return `${this.tab.name} changed`; + } + return `Close ${this.tab.name}`; + }, + changedClass() { + const tabChangedObj = { + 'fa-times close-icon': !this.tab.changed && !this.tab.tempFile, + 'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile, + }; + return tabChangedObj; + }, }, - }, - methods: { - ...mapActions([ - 'closeFile', - ]), - clickFile(tab) { - this.$router.push(`/project${tab.url}`); + methods: { + ...mapActions([ + 'closeFile', + ]), + clickFile(tab) { + this.$router.push(`/project${tab.url}`); + }, }, - }, -}; + }; </script> <template> - <li - @click="clickFile(tab)" - > + <li @click="clickFile(tab)"> <button type="button" class="multi-file-tab-close" @@ -63,6 +64,10 @@ export default { :class="{active : tab.active }" :title="tab.url" > + <file-icon + :file-name="tab.name" + :size="16" + /> {{ tab.name }} </div> </li> diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index a96bd339f51..e8a19f47cee 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -1,12 +1,8 @@ import Vue from 'vue'; -import { mapActions } from 'vuex'; -import { convertPermissionToBoolean } from '../lib/utils/common_utils'; import ide from './components/ide.vue'; - import store from './stores'; import router from './ide_router'; import Translate from '../vue_shared/translate'; -import ContextualSidebar from '../contextual_sidebar'; function initIde(el) { if (!el) return null; @@ -18,30 +14,13 @@ function initIde(el) { components: { ide, }, - methods: { - ...mapActions([ - 'setInitialData', - ]), - }, - created() { - const data = el.dataset; - - this.setInitialData({ - endpoints: { - rootEndpoint: data.url, - newMergeRequestUrl: data.newMergeRequestUrl, - rootUrl: data.rootUrl, + render(createElement) { + return createElement('ide', { + props: { + emptyStateSvgPath: el.dataset.emptyStateSvgPath, }, - canCommit: convertPermissionToBoolean(data.canCommit), - onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch), - path: data.currentPath, - isRoot: convertPermissionToBoolean(data.root), - isInitialRoot: convertPermissionToBoolean(data.root), }); }, - render(createElement) { - return createElement('ide'); - }, }); } @@ -50,6 +29,3 @@ const ideElement = document.getElementById('ide'); Vue.use(Translate); initIde(ideElement); - -const contextualSidebar = new ContextualSidebar(); -contextualSidebar.bindEvents(); diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 51e202b9348..668221c0296 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import DecorationsController from './decorations/controller'; import DirtyDiffController from './diff/controller'; import Disposable from './common/disposable'; diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index c01046c8c76..335882bb6d7 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -63,6 +63,10 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => { } }; +export const setResizingStatus = ({ commit }, resizing) => { + commit(types.SET_RESIZING_STATUS, resizing); +}; + export const checkCommitStatus = ({ state }) => service .getBranchData(state.currentProjectId, state.currentBranchId) diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 75e332090cb..02d4bd87ab0 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -8,9 +8,11 @@ export const getProjectData = ( { namespace, projectId, force = false } = {}, ) => new Promise((resolve, reject) => { if (!state.projects[`${namespace}/${projectId}`] || force) { + commit(types.TOGGLE_LOADING, state); service.getProjectData(namespace, projectId) .then(res => res.data) .then((data) => { + commit(types.TOGGLE_LOADING, state); commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); resolve(data); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 4e3c10972ba..69b218a5e7d 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -5,6 +5,7 @@ export const SET_ROOT = 'SET_ROOT'; export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA'; export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED'; export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED'; +export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS'; // Project Mutation Types export const SET_PROJECT = 'SET_PROJECT'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 2fed9019cb6..03d81be10a1 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -49,6 +49,11 @@ export default { rightPanelCollapsed: collapsed, }); }, + [types.SET_RESIZING_STATUS](state, resizing) { + Object.assign(state, { + panelResizing: resizing, + }); + }, [types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) { Object.assign(entry.lastCommit, { id: lastCommit.commit.id, diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index 539e382830f..61d12096946 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -19,4 +19,5 @@ export default () => ({ projects: {}, leftPanelCollapsed: false, rightPanelCollapsed: true, + panelResizing: false, }); diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 29e3ab5d040..d556404faa5 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -1,3 +1,5 @@ +import _ from 'underscore'; + export const dataStructure = () => ({ id: '', key: '', |