diff options
Diffstat (limited to 'app/assets/javascripts/ide/components')
11 files changed, 555 insertions, 150 deletions
diff --git a/app/assets/javascripts/ide/components/branches/item.vue b/app/assets/javascripts/ide/components/branches/item.vue new file mode 100644 index 00000000000..cc3e84e3f77 --- /dev/null +++ b/app/assets/javascripts/ide/components/branches/item.vue @@ -0,0 +1,60 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; +import Timeago from '~/vue_shared/components/time_ago_tooltip.vue'; +import router from '../../ide_router'; + +export default { + components: { + Icon, + Timeago, + }, + props: { + item: { + type: Object, + required: true, + }, + projectId: { + type: String, + required: true, + }, + isActive: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + branchHref() { + return router.resolve(`/project/${this.projectId}/edit/${this.item.name}`).href; + }, + }, +}; +</script> + +<template> + <a + :href="branchHref" + class="btn-link d-flex align-items-center" + > + <span class="d-flex append-right-default ide-search-list-current-icon"> + <icon + v-if="isActive" + :size="18" + name="mobile-issue-close" + /> + </span> + <span> + <strong> + {{ item.name }} + </strong> + <span + class="ide-merge-request-project-path d-block mt-1" + > + Updated + <timeago + :time="item.committedDate || ''" + /> + </span> + </span> + </a> +</template> diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue new file mode 100644 index 00000000000..6db7b9d6b0e --- /dev/null +++ b/app/assets/javascripts/ide/components/branches/search_list.vue @@ -0,0 +1,111 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import _ from 'underscore'; +import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; +import Icon from '~/vue_shared/components/icon.vue'; +import Item from './item.vue'; + +export default { + components: { + LoadingIcon, + Item, + Icon, + }, + data() { + return { + search: '', + }; + }, + computed: { + ...mapState('branches', ['branches', 'isLoading']), + ...mapState(['currentBranchId', 'currentProjectId']), + hasBranches() { + return this.branches.length !== 0; + }, + hasNoSearchResults() { + return this.search !== '' && !this.hasBranches; + }, + }, + watch: { + isLoading: { + handler: 'focusSearch', + }, + }, + mounted() { + this.loadBranches(); + }, + methods: { + ...mapActions('branches', ['fetchBranches']), + loadBranches() { + this.fetchBranches({ search: this.search }); + }, + searchBranches: _.debounce(function debounceSearch() { + this.loadBranches(); + }, 250), + focusSearch() { + if (!this.isLoading) { + this.$nextTick(() => { + this.$refs.searchInput.focus(); + }); + } + }, + isActiveBranch(item) { + return item.name === this.currentBranchId; + }, + }, +}; +</script> + +<template> + <div> + <div class="dropdown-input mt-3 pb-3 mb-0 border-bottom"> + <div class="position-relative"> + <input + ref="searchInput" + :placeholder="__('Search branches')" + v-model="search" + type="search" + class="form-control dropdown-input-field" + @input="searchBranches" + /> + <icon + :size="18" + name="search" + class="input-icon" + /> + </div> + </div> + <div class="dropdown-content ide-merge-requests-dropdown-content d-flex"> + <loading-icon + v-if="isLoading" + class="mt-3 mb-3 align-self-center ml-auto mr-auto" + size="2" + /> + <ul + v-else + class="mb-3 w-100" + > + <template v-if="hasBranches"> + <li + v-for="item in branches" + :key="item.name" + > + <item + :item="item" + :project-id="currentProjectId" + :is-active="isActiveBranch(item)" + /> + </li> + </template> + <li + v-else + class="ide-search-list-empty d-flex align-items-center justify-content-center" + > + <template v-if="hasNoSearchResults"> + {{ __('No branches found') }} + </template> + </li> + </ul> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue index 33f1179a234..39d46a91731 100644 --- a/app/assets/javascripts/ide/components/ide_tree.vue +++ b/app/assets/javascripts/ide/components/ide_tree.vue @@ -41,7 +41,7 @@ export default { slot="header" > {{ __('Edit') }} - <div class="ml-auto d-flex"> + <div class="ide-tree-actions ml-auto d-flex"> <new-entry-button :label="__('New file')" :show-label="false" diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index e303ff6ea8f..5611b37be7c 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -3,14 +3,14 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import RepoFile from './repo_file.vue'; -import NewDropdown from './new_dropdown/index.vue'; +import NavDropdown from './nav_dropdown.vue'; export default { components: { Icon, RepoFile, SkeletonLoadingContainer, - NewDropdown, + NavDropdown, }, props: { viewerType: { @@ -57,6 +57,7 @@ export default { :class="headerClass" class="ide-tree-header" > + <nav-dropdown /> <slot name="header"></slot> </header> <div diff --git a/app/assets/javascripts/ide/components/merge_requests/dropdown.vue b/app/assets/javascripts/ide/components/merge_requests/dropdown.vue deleted file mode 100644 index 4b9824bf04b..00000000000 --- a/app/assets/javascripts/ide/components/merge_requests/dropdown.vue +++ /dev/null @@ -1,63 +0,0 @@ -<script> -import { mapGetters } from 'vuex'; -import Tabs from '../../../vue_shared/components/tabs/tabs'; -import Tab from '../../../vue_shared/components/tabs/tab.vue'; -import List from './list.vue'; - -export default { - components: { - Tabs, - Tab, - List, - }, - props: { - show: { - type: Boolean, - required: true, - }, - }, - computed: { - ...mapGetters('mergeRequests', ['assignedData', 'createdData']), - createdMergeRequestLength() { - return this.createdData.mergeRequests.length; - }, - assignedMergeRequestLength() { - return this.assignedData.mergeRequests.length; - }, - }, -}; -</script> - -<template> - <div class="dropdown-menu ide-merge-requests-dropdown p-0"> - <tabs - v-if="show" - stop-propagation - > - <tab active> - <template slot="title"> - {{ __('Created by me') }} - <span class="badge badge-pill"> - {{ createdMergeRequestLength }} - </span> - </template> - <list - :empty-text="__('You have not created any merge requests')" - type="created" - /> - </tab> - <tab> - <template slot="title"> - {{ __('Assigned to me') }} - <span class="badge badge-pill"> - {{ assignedMergeRequestLength }} - </span> - </template> - <list - :empty-text="__('You do not have any assigned merge requests')" - type="assigned" - /> - </tab> - </tabs> - </div> -</template> diff --git a/app/assets/javascripts/ide/components/merge_requests/item.vue b/app/assets/javascripts/ide/components/merge_requests/item.vue index 4e18376bd48..0c4ea80ba08 100644 --- a/app/assets/javascripts/ide/components/merge_requests/item.vue +++ b/app/assets/javascripts/ide/components/merge_requests/item.vue @@ -1,5 +1,6 @@ <script> import Icon from '../../../vue_shared/components/icon.vue'; +import router from '../../ide_router'; export default { components: { @@ -29,22 +30,21 @@ export default { pathWithID() { return `${this.item.projectPathWithNamespace}!${this.item.iid}`; }, - }, - methods: { - clickItem() { - this.$emit('click', this.item); + mergeRequestHref() { + const path = `/project/${this.item.projectPathWithNamespace}/merge_requests/${this.item.iid}`; + + return router.resolve(path).href; }, }, }; </script> <template> - <button - type="button" + <a + :href="mergeRequestHref" class="btn-link d-flex align-items-center" - @click="clickItem" > - <span class="d-flex append-right-default ide-merge-request-current-icon"> + <span class="d-flex append-right-default ide-search-list-current-icon"> <icon v-if="isActive" :size="18" @@ -59,5 +59,5 @@ export default { {{ pathWithID }} </span> </span> - </button> + </a> </template> diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue index 19d3e48ee10..fc612956688 100644 --- a/app/assets/javascripts/ide/components/merge_requests/list.vue +++ b/app/assets/javascripts/ide/components/merge_requests/list.vue @@ -1,96 +1,101 @@ <script> -import { mapActions, mapGetters, mapState } from 'vuex'; +import { mapActions, mapState } from 'vuex'; import _ from 'underscore'; -import LoadingIcon from '../../../vue_shared/components/loading_icon.vue'; +import { __ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; +import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import Item from './item.vue'; +import TokenedInput from '../shared/tokened_input.vue'; + +const SEARCH_TYPES = [ + { type: 'created', label: __('Created by me') }, + { type: 'assigned', label: __('Assigned to me') }, +]; export default { components: { LoadingIcon, + TokenedInput, Item, - }, - props: { - type: { - type: String, - required: true, - }, - emptyText: { - type: String, - required: true, - }, + Icon, }, data() { return { search: '', + currentSearchType: null, + hasSearchFocus: false, }; }, computed: { - ...mapGetters('mergeRequests', ['getData']), + ...mapState('mergeRequests', ['mergeRequests', 'isLoading']), ...mapState(['currentMergeRequestId', 'currentProjectId']), - data() { - return this.getData(this.type); - }, - isLoading() { - return this.data.isLoading; - }, - mergeRequests() { - return this.data.mergeRequests; - }, hasMergeRequests() { return this.mergeRequests.length !== 0; }, hasNoSearchResults() { return this.search !== '' && !this.hasMergeRequests; }, + showSearchTypes() { + return this.hasSearchFocus && !this.search && !this.currentSearchType; + }, + type() { + return this.currentSearchType + ? this.currentSearchType.type + : ''; + }, + searchTokens() { + return this.currentSearchType + ? [this.currentSearchType] + : []; + }, }, watch: { - isLoading: { - handler: 'focusSearch', + search() { + // When the search is updated, let's turn off this flag to hide the search types + this.hasSearchFocus = false; }, }, mounted() { this.loadMergeRequests(); }, methods: { - ...mapActions('mergeRequests', ['fetchMergeRequests', 'openMergeRequest']), + ...mapActions('mergeRequests', ['fetchMergeRequests']), loadMergeRequests() { this.fetchMergeRequests({ type: this.type, search: this.search }); }, - viewMergeRequest(item) { - this.openMergeRequest({ - projectPath: item.projectPathWithNamespace, - id: item.iid, - }); - }, searchMergeRequests: _.debounce(function debounceSearch() { this.loadMergeRequests(); }, 250), - focusSearch() { - if (!this.isLoading) { - this.$nextTick(() => { - this.$refs.searchInput.focus(); - }); - } + onSearchFocus() { + this.hasSearchFocus = true; + }, + setSearchType(searchType) { + this.currentSearchType = searchType; + this.loadMergeRequests(); }, }, + searchTypes: SEARCH_TYPES, }; </script> <template> <div> <div class="dropdown-input mt-3 pb-3 mb-0 border-bottom"> - <input - ref="searchInput" - :placeholder="__('Search merge requests')" - v-model="search" - type="search" - class="dropdown-input-field" - @input="searchMergeRequests" - /> - <i - aria-hidden="true" - class="fa fa-search dropdown-input-search" - ></i> + <div class="position-relative"> + <tokened-input + v-model="search" + :tokens="searchTokens" + :placeholder="__('Search merge requests')" + @focus="onSearchFocus" + @input="searchMergeRequests" + @removeToken="setSearchType(null)" + /> + <icon + :size="18" + name="search" + class="input-icon" + /> + </div> </div> <div class="dropdown-content ide-merge-requests-dropdown-content d-flex"> <loading-icon @@ -98,35 +103,52 @@ export default { class="mt-3 mb-3 align-self-center ml-auto mr-auto" size="2" /> - <ul - v-else - class="mb-3 w-100" - > - <template v-if="hasMergeRequests"> - <li - v-for="item in mergeRequests" - :key="item.id" - > - <item - :item="item" - :current-id="currentMergeRequestId" - :current-project-id="currentProjectId" - @click="viewMergeRequest" - /> - </li> - </template> - <li - v-else - class="ide-merge-requests-empty d-flex align-items-center justify-content-center" + <template v-else> + <ul + class="mb-3 w-100" > - <template v-if="hasNoSearchResults"> - {{ __('No merge requests found') }} + <template v-if="showSearchTypes"> + <li + v-for="searchType in $options.searchTypes" + :key="searchType.type" + > + <button + type="button" + class="btn-link d-flex align-items-center" + @click.stop="setSearchType(searchType)" + > + <span class="d-flex append-right-default ide-search-list-current-icon"> + <icon + :size="18" + name="search" + /> + </span> + <span> + {{ searchType.label }} + </span> + </button> + </li> </template> - <template v-else> - {{ emptyText }} + <template v-else-if="hasMergeRequests"> + <li + v-for="item in mergeRequests" + :key="item.id" + > + <item + :item="item" + :current-id="currentMergeRequestId" + :current-project-id="currentProjectId" + /> + </li> </template> - </li> - </ul> + <li + v-else + class="ide-search-list-empty d-flex align-items-center justify-content-center" + > + {{ __('No merge requests found') }} + </li> + </ul> + </template> </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/nav_dropdown.vue b/app/assets/javascripts/ide/components/nav_dropdown.vue new file mode 100644 index 00000000000..db36779c395 --- /dev/null +++ b/app/assets/javascripts/ide/components/nav_dropdown.vue @@ -0,0 +1,59 @@ +<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, + }, + data() { + return { + isVisibleDropdown: false, + }; + }, + mounted() { + this.addDropdownListeners(); + }, + beforeDestroy() { + this.removeDropdownListeners(); + }, + methods: { + addDropdownListeners() { + $(this.$refs.dropdown) + .on('show.bs.dropdown', () => this.showDropdown()) + .on('hide.bs.dropdown', () => this.hideDropdown()); + }, + removeDropdownListeners() { + $(this.$refs.dropdown) + .off('show.bs.dropdown') + .off('hide.bs.dropdown'); + }, + showDropdown() { + this.isVisibleDropdown = true; + }, + hideDropdown() { + this.isVisibleDropdown = false; + }, + }, +}; +</script> + +<template> + <div + ref="dropdown" + class="btn-group ide-nav-dropdown dropdown" + > + <nav-dropdown-button /> + <div + class="dropdown-menu dropdown-menu-left p-0" + > + <nav-form + v-if="isVisibleDropdown" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/nav_dropdown_button.vue b/app/assets/javascripts/ide/components/nav_dropdown_button.vue new file mode 100644 index 00000000000..7f98769d484 --- /dev/null +++ b/app/assets/javascripts/ide/components/nav_dropdown_button.vue @@ -0,0 +1,54 @@ +<script> +import { mapState } from 'vuex'; +import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue'; +import Icon from '~/vue_shared/components/icon.vue'; + +const EMPTY_LABEL = '-'; + +export default { + components: { + Icon, + DropdownButton, + }, + computed: { + ...mapState(['currentBranchId', 'currentMergeRequestId']), + mergeRequestLabel() { + return this.currentMergeRequestId + ? `!${this.currentMergeRequestId}` + : EMPTY_LABEL; + }, + branchLabel() { + return this.currentBranchId || EMPTY_LABEL; + }, + }, +}; +</script> + +<template> + <dropdown-button> + <span + class="row" + > + <span + class="col-7 text-truncate" + > + <icon + :size="16" + :aria-label="__('Current Branch')" + name="branch" + /> + {{ branchLabel }} + </span> + <span + class="col-5 pl-0 text-truncate" + > + <icon + :size="16" + :aria-label="__('Merge Request')" + name="merge-request" + /> + {{ mergeRequestLabel }} + </span> + </span> + </dropdown-button> +</template> diff --git a/app/assets/javascripts/ide/components/nav_form.vue b/app/assets/javascripts/ide/components/nav_form.vue new file mode 100644 index 00000000000..718b836e11c --- /dev/null +++ b/app/assets/javascripts/ide/components/nav_form.vue @@ -0,0 +1,40 @@ +<script> +import Tabs from '~/vue_shared/components/tabs/tabs'; +import Tab from '~/vue_shared/components/tabs/tab.vue'; +import BranchesSearchList from './branches/search_list.vue'; +import MergeRequestSearchList from './merge_requests/list.vue'; + +export default { + components: { + Tabs, + Tab, + BranchesSearchList, + MergeRequestSearchList, + }, +}; +</script> + +<template> + <div + class="ide-nav-form p-0" + > + <tabs + stop-propagation + > + <tab + active + > + <template slot="title"> + {{ __('Merge Requests') }} + </template> + <merge-request-search-list /> + </tab> + <tab> + <template slot="title"> + {{ __('Branches') }} + </template> + <branches-search-list /> + </tab> + </tabs> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/shared/tokened_input.vue b/app/assets/javascripts/ide/components/shared/tokened_input.vue new file mode 100644 index 00000000000..a7a12f6785d --- /dev/null +++ b/app/assets/javascripts/ide/components/shared/tokened_input.vue @@ -0,0 +1,121 @@ +<script> +import { __ } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + }, + props: { + placeholder: { + type: String, + required: false, + default: __('Search'), + }, + tokens: { + type: Array, + required: false, + default: () => [], + }, + value: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + backspaceCount: 0, + }; + }, + computed: { + placeholderText() { + return this.tokens.length + ? '' + : this.placeholder; + }, + }, + watch: { + tokens() { + this.$refs.input.focus(); + }, + }, + methods: { + onFocus() { + this.$emit('focus'); + }, + onBlur() { + this.$emit('blur'); + }, + onInput(evt) { + this.$emit('input', evt.target.value); + }, + onBackspace() { + if (!this.value && this.tokens.length) { + this.backspaceCount += 1; + } else { + this.backspaceCount = 0; + return; + } + + if (this.backspaceCount > 1) { + this.removeToken(this.tokens[this.tokens.length - 1]); + this.backspaceCount = 0; + } + }, + removeToken(token) { + this.$emit('removeToken', token); + }, + }, +}; +</script> + +<template> + <div class="filtered-search-wrapper"> + <div class="filtered-search-box"> + <div class="tokens-container list-unstyled"> + <div + v-for="token in tokens" + :key="token.label" + class="filtered-search-token" + > + <button + class="selectable btn-blank" + type="button" + @click.stop="removeToken(token)" + @keyup.delete="removeToken(token)" + > + <div + class="value-container rounded" + > + <div + class="value" + >{{ token.label }}</div> + <div + class="remove-token inverted" + > + <icon + :size="10" + name="close" + /> + </div> + </div> + </button> + </div> + <div class="input-token"> + <input + ref="input" + :placeholder="placeholderText" + :value="value" + type="search" + class="form-control filtered-search" + @input="onInput" + @focus="onFocus" + @blur="onBlur" + @keyup.delete="onBackspace" + /> + </div> + </div> + </div> + </div> +</template> |