diff options
author | Phil Hughes <me@iamphill.com> | 2018-07-10 14:56:50 +0100 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2018-07-11 16:47:13 +0100 |
commit | be74e3936d1103e0f7180a562fa43d8cdeaf020b (patch) | |
tree | 1c92ef12033f416ef33bcb0dfaab5088f7466568 /app | |
parent | e68a547bc790d44a1df3c9ae8b07b004ab8dd47e (diff) | |
download | gitlab-ce-be74e3936d1103e0f7180a562fa43d8cdeaf020b.tar.gz |
Improvements to new entry dropdowns in Web IDE
Closes #44845
Diffstat (limited to 'app')
13 files changed, 276 insertions, 170 deletions
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 9f016e0338f..257a7432c20 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,6 +1,7 @@ <script> import Mousetrap from 'mousetrap'; import { mapActions, mapState, mapGetters } from 'vuex'; +import NewModal from './new_dropdown/modal.vue'; import IdeSidebar from './ide_side_bar.vue'; import RepoTabs from './repo_tabs.vue'; import IdeStatusBar from './ide_status_bar.vue'; @@ -13,6 +14,7 @@ const originalStopCallback = Mousetrap.stopCallback; export default { components: { + NewModal, IdeSidebar, RepoTabs, IdeStatusBar, @@ -137,5 +139,6 @@ export default { /> </div> <ide-status-bar :file="activeFile"/> + <new-modal /> </article> </template> diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue index 8fc4ebe6ca6..0a95c0bb30d 100644 --- a/app/assets/javascripts/ide/components/ide_tree.vue +++ b/app/assets/javascripts/ide/components/ide_tree.vue @@ -1,12 +1,16 @@ <script> import { mapState, mapGetters, mapActions } from 'vuex'; -import NewDropdown from './new_dropdown/index.vue'; +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: { - NewDropdown, + Icon, + Upload, IdeTreeList, + NewEntryButton, }, computed: { ...mapState(['currentBranchId']), @@ -20,23 +24,42 @@ export default { } }, methods: { - ...mapActions(['updateViewer']), + ...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry']), }, }; </script> <template> <ide-tree-list + header-class="d-flex w-100" viewer-type="editor" > <template slot="header" > {{ __('Edit') }} - <new-dropdown - :project-id="currentProject.name_with_namespace" - :branch="currentBranchId" - /> + <div class="ml-auto d-flex"> + <new-entry-button + :label="__('New file')" + :show-label="false" + class="d-flex border-0 p-0 mr-3" + icon="doc-new" + @click="openNewEntryModal({ type: 'blob' })" + /> + <upload + :show-label="false" + class="d-flex mr-3" + button-css-classes="border-0 p-0" + @create="createTempEntry" + /> + <new-entry-button + :label="__('New directory')" + :show-label="false" + class="d-flex border-0 p-0" + icon="folder-new" + @click="openNewEntryModal({ type: 'tree' })" + /> + </div> </template> </ide-tree-list> </template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/button.vue b/app/assets/javascripts/ide/components/new_dropdown/button.vue new file mode 100644 index 00000000000..7682b34ce4d --- /dev/null +++ b/app/assets/javascripts/ide/components/new_dropdown/button.vue @@ -0,0 +1,51 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + }, + props: { + label: { + type: String, + required: false, + default: null, + }, + icon: { + type: String, + required: true, + }, + iconClasses: { + type: String, + required: false, + default: null, + }, + showLabel: { + type: Boolean, + required: false, + default: true, + }, + }, + methods: { + clicked() { + this.$emit('click'); + }, + }, +}; +</script> + +<template> + <button + :aria-label="label" + type="button" + @click.stop.prevent="clicked" + > + <icon + :name="icon" + :css-classes="iconClasses" + /> + <template v-if="showLabel"> + {{ label }} + </template> + </button> +</template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index 1e398d7e1aa..c29e49ba766 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -3,12 +3,14 @@ import { mapActions } from 'vuex'; import icon from '~/vue_shared/components/icon.vue'; import newModal from './modal.vue'; import upload from './upload.vue'; +import ItemButton from './button.vue'; export default { components: { icon, newModal, upload, + ItemButton, }, props: { branch: { @@ -20,11 +22,13 @@ export default { required: false, default: '', }, + mouseOver: { + type: Boolean, + required: true, + }, }, data() { return { - openModal: false, - modalType: '', dropdownOpen: false, }; }, @@ -34,17 +38,18 @@ export default { this.$refs.dropdownMenu.scrollIntoView(); }); }, + mouseOver() { + if (!this.mouseOver) { + this.dropdownOpen = false; + } + }, }, methods: { - ...mapActions(['createTempEntry']), + ...mapActions(['createTempEntry', 'openNewEntryModal']), createNewItem(type) { - this.modalType = type; - this.openModal = true; + this.openNewEntryModal({ type, path: this.path }); this.dropdownOpen = false; }, - hideModal() { - this.openModal = false; - }, openDropdown() { this.dropdownOpen = !this.dropdownOpen; }, @@ -58,23 +63,19 @@ export default { :class="{ show: dropdownOpen, }" - class="dropdown" + class="dropdown d-flex" > <button + :aria-label="__('Create new file or directory')" type="button" - class="btn btn-sm btn-default dropdown-toggle add-to-tree" - aria-label="Create new file or directory" + class="rounded border-0 d-flex ide-entry-dropdown-toggle" @click.stop="openDropdown()" > <icon - :size="12" - name="plus" - css-classes="float-left" + name="hamburger" /> <icon - :size="12" name="arrow-down" - css-classes="float-left" /> </button> <ul @@ -82,39 +83,30 @@ export default { class="dropdown-menu dropdown-menu-right" > <li> - <a - href="#" - role="button" - @click.stop.prevent="createNewItem('blob')" - > - {{ __('New file') }} - </a> + <item-button + :label="__('New file')" + class="d-flex" + icon="doc-new" + icon-classes="mr-2" + @click="createNewItem('blob')" + /> </li> <li> <upload - :branch-id="branch" :path="path" @create="createTempEntry" /> </li> <li> - <a - href="#" - role="button" - @click.stop.prevent="createNewItem('tree')" - > - {{ __('New directory') }} - </a> + <item-button + :label="__('New directory')" + class="d-flex" + icon="folder-new" + icon-classes="mr-2" + @click="createNewItem('tree')" + /> </li> </ul> </div> - <new-modal - v-if="openModal" - :type="modalType" - :branch-id="branch" - :path="path" - @hide="hideModal" - @create="createTempEntry" - /> </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 1e9668d5154..1867b7980d2 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -1,78 +1,70 @@ <script> import { __ } from '~/locale'; -import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { mapActions, mapState } from 'vuex'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; export default { components: { - DeprecatedModal, - }, - props: { - branchId: { - type: String, - required: true, - }, - type: { - type: String, - required: true, - }, - path: { - type: String, - required: true, - }, + GlModal, }, data() { return { - entryName: this.path !== '' ? `${this.path}/` : '', + name: '', }; }, computed: { + ...mapState(['newEntryModal']), + entryName: { + get() { + return this.name || (this.newEntryModal.path !== '' ? `${this.newEntryModal.path}/` : ''); + }, + set(val) { + this.name = val; + }, + }, modalTitle() { - if (this.type === 'tree') { + if (this.newEntryModal.type === 'tree') { return __('Create new directory'); } return __('Create new file'); }, buttonLabel() { - if (this.type === 'tree') { + if (this.newEntryModal.type === 'tree') { return __('Create directory'); } return __('Create file'); }, }, - mounted() { - this.$refs.fieldName.focus(); - }, methods: { + ...mapActions(['createTempEntry']), createEntryInStore() { - this.$emit('create', { - branchId: this.branchId, - name: this.entryName, - type: this.type, + this.createTempEntry({ + name: this.name, + type: this.newEntryModal.type, }); - - this.hideModal(); }, - hideModal() { - this.$emit('hide'); + focusInput() { + setTimeout(() => { + this.$refs.fieldName.focus(); + }); }, }, }; </script> <template> - <deprecated-modal - :title="modalTitle" - :primary-button-label="buttonLabel" - kind="success" - @cancel="hideModal" + <gl-modal + id="ide-new-entry" + :header-title-text="modalTitle" + :footer-primary-button-text="buttonLabel" + footer-primary-button-variant="success" @submit="createEntryInStore" + @open="focusInput" > - <form - slot="body" + <div class="form-group row" - @submit.prevent="createEntryInStore" > <label class="label-light col-form-label col-sm-3"> {{ __('Name') }} @@ -85,6 +77,6 @@ export default { class="form-control" /> </div> - </form> - </deprecated-modal> + </div> + </gl-modal> </template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue index 677b282bd61..5b1743bb30e 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -1,71 +1,85 @@ <script> - export default { - props: { - branchId: { - type: String, - required: true, - }, - path: { - type: String, - required: false, - default: '', - }, +import Icon from '~/vue_shared/components/icon.vue'; +import ItemButton from './button.vue'; + +export default { + components: { + Icon, + ItemButton, + }, + props: { + path: { + type: String, + required: false, + default: '', }, - mounted() { - this.$refs.fileUpload.addEventListener('change', this.openFile); + showLabel: { + type: Boolean, + required: false, + default: true, }, - beforeDestroy() { - this.$refs.fileUpload.removeEventListener('change', this.openFile); + buttonCssClasses: { + type: String, + required: false, + default: null, }, - methods: { - createFile(target, file, isText) { - const { name } = file; - let { result } = target; + }, + mounted() { + this.$refs.fileUpload.addEventListener('change', this.openFile); + }, + beforeDestroy() { + this.$refs.fileUpload.removeEventListener('change', this.openFile); + }, + methods: { + createFile(target, file, isText) { + const { name } = file; + let { result } = target; - if (!isText) { - // eslint-disable-next-line prefer-destructuring - result = result.split('base64,')[1]; - } + if (!isText) { + // eslint-disable-next-line prefer-destructuring + result = result.split('base64,')[1]; + } - this.$emit('create', { - name: `${(this.path ? `${this.path}/` : '')}${name}`, - branchId: this.branchId, - type: 'blob', - content: result, - base64: !isText, - }); - }, - readFile(file) { - const reader = new FileReader(); - const isText = file.type.match(/text.*/) !== null; + this.$emit('create', { + name: `${this.path ? `${this.path}/` : ''}${name}`, + type: 'blob', + content: result, + base64: !isText, + }); + }, + readFile(file) { + const reader = new FileReader(); + const isText = file.type.match(/text.*/) !== null; - reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true }); + reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true }); - if (isText) { - reader.readAsText(file); - } else { - reader.readAsDataURL(file); - } - }, - openFile() { - Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file)); - }, - startFileUpload() { - this.$refs.fileUpload.click(); - }, + if (isText) { + reader.readAsText(file); + } else { + reader.readAsDataURL(file); + } + }, + openFile() { + Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file)); }, - }; + startFileUpload() { + this.$refs.fileUpload.click(); + }, + }, +}; </script> <template> <div> - <a - href="#" - role="button" - @click.stop.prevent="startFileUpload" - > - {{ __('Upload file') }} - </a> + <item-button + :class="buttonCssClasses" + :show-label="showLabel" + :icon-classes="showLabel ? 'mr-2' : ''" + :label="__('Upload file')" + class="d-flex" + icon="upload" + @click="startFileUpload" + /> <input id="file-upload" ref="fileUpload" diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index f490a3a2a39..3b4dd5ae9aa 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -40,6 +40,11 @@ export default { default: false, }, }, + data() { + return { + mouseOver: false, + }; + }, computed: { ...mapGetters([ 'getChangesInFolder', @@ -142,6 +147,9 @@ export default { hasUrlAtCurrentRoute() { return this.$router.currentRoute.path === `/project${this.file.url}`; }, + toggleHover(over) { + this.mouseOver = over; + }, }, }; </script> @@ -153,6 +161,8 @@ export default { class="file" role="button" @click="clickFile" + @mouseover="toggleHover(true)" + @mouseout="toggleHover(false)" > <div class="file-name" @@ -206,6 +216,7 @@ export default { :project-id="file.projectId" :branch="file.branchId" :path="file.path" + :mouse-over="mouseOver" class="float-right prepend-left-8" /> </div> diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index 5e91fa915ff..b5bd6f5a6bb 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -52,7 +52,7 @@ export const setResizingStatus = ({ commit }, resizing) => { export const createTempEntry = ( { state, commit, dispatch }, - { branchId, name, type, content = '', base64 = false }, + { name, type, content = '', base64 = false }, ) => new Promise(resolve => { const worker = new FilesDecoratorWorker(); @@ -81,7 +81,7 @@ export const createTempEntry = ( commit(types.CREATE_TMP_ENTRY, { data, projectId: state.currentProjectId, - branchId, + branchId: state.currentBranchId, }); if (type === 'blob') { @@ -100,7 +100,7 @@ export const createTempEntry = ( worker.postMessage({ data: [fullName], projectId: state.currentProjectId, - branchId, + branchId: state.currentBranchId, type, tempFile: true, base64, @@ -178,6 +178,13 @@ export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links); export const setErrorMessage = ({ commit }, errorMessage) => commit(types.SET_ERROR_MESSAGE, errorMessage); +export const openNewEntryModal = ({ commit }, { type, path = '' }) => { + commit(types.OPEN_NEW_ENTRY_MODAL, { type, path }); + + // open the modal manually so we don't mess around with dropdown/rows + $('#ide-new-entry').modal('show'); +}; + export * from './actions/tree'; export * from './actions/file'; export * from './actions/project'; diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 555802e1811..8d6f9ccaf34 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -74,3 +74,5 @@ export const CLEAR_PROJECTS = 'CLEAR_PROJECTS'; export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; + +export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 702be2140e2..f8091f5b5e0 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -166,6 +166,11 @@ export default { [types.SET_ERROR_MESSAGE](state, errorMessage) { Object.assign(state, { errorMessage }); }, + [types.OPEN_NEW_ENTRY_MODAL](state, { type, path }) { + Object.assign(state, { + newEntryModal: { type, path }, + }); + }, ...projectMutations, ...mergeRequestMutation, ...fileMutations, diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index be229b2c723..0f32a267469 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -26,4 +26,8 @@ export default () => ({ rightPane: null, links: {}, errorMessage: null, + newEntryModal: { + type: '', + path: '', + }, }); diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue index b298b989203..416eda796a7 100644 --- a/app/assets/javascripts/vue_shared/components/gl_modal.vue +++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue @@ -45,6 +45,11 @@ export default { emitSubmit(event) { this.$emit('submit', event); }, + opened({ propertyName }) { + if (propertyName === 'opacity') { + this.$emit('open'); + } + }, }, }; </script> @@ -55,6 +60,7 @@ export default { class="modal fade" tabindex="-1" role="dialog" + @transitionend="opened" > <div :class="modalSizeClass" diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 6e2b285285a..8b1227b9131 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -44,6 +44,7 @@ padding-bottom: $grid-size; .file { + height: 32px; cursor: pointer; &.file-active { @@ -716,32 +717,6 @@ justify-content: center; } -.ide-new-btn { - .btn { - padding-top: 3px; - padding-bottom: 3px; - } - - .dropdown { - display: flex; - } - - .dropdown-toggle svg { - top: 0; - } - - .dropdown-menu { - left: auto; - right: 0; - - label { - font-weight: $gl-font-weight-normal; - padding: 5px 8px; - margin-bottom: 0; - } - } -} - .ide { overflow: hidden; @@ -1340,3 +1315,24 @@ overflow: auto; } } + +.ide-entry-dropdown-toggle { + padding: $gl-padding-4; + background-color: $theme-gray-100; + + &:hover { + background-color: $theme-gray-200; + } + + &:active, + &:focus { + color: $white-normal; + background-color: $blue-500; + outline: 0; + } +} + +.ide-new-btn .dropdown.show .ide-entry-dropdown-toggle { + color: $white-normal; + background-color: $blue-500; +} |