summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorPaul Slaughter <pslaughter@gitlab.com>2018-08-07 15:15:56 +0000
committerPhil Hughes <me@iamphill.com>2018-08-07 15:15:56 +0000
commit0d6e50d54270a973647f828047828b80fdf8d013 (patch)
tree9bf41acf27d039f673f45520187daff9d47cb04f /app
parent0e90f27ff79d1743d8ec5e49e003d4c68a689f78 (diff)
downloadgitlab-ce-0d6e50d54270a973647f828047828b80fdf8d013.tar.gz
Create Web IDE MR and branch picker
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/api.js12
-rw-r--r--app/assets/javascripts/ide/components/branches/item.vue60
-rw-r--r--app/assets/javascripts/ide/components/branches/search_list.vue111
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue5
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/dropdown.vue63
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/item.vue18
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue172
-rw-r--r--app/assets/javascripts/ide/components/nav_dropdown.vue59
-rw-r--r--app/assets/javascripts/ide/components/nav_dropdown_button.vue54
-rw-r--r--app/assets/javascripts/ide/components/nav_form.vue40
-rw-r--r--app/assets/javascripts/ide/components/shared/tokened_input.vue121
-rw-r--r--app/assets/javascripts/ide/stores/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/branches/actions.js39
-rw-r--r--app/assets/javascripts/ide/stores/modules/branches/index.js10
-rw-r--r--app/assets/javascripts/ide/stores/modules/branches/mutation_types.js5
-rw-r--r--app/assets/javascripts/ide/stores/modules/branches/mutations.js21
-rw-r--r--app/assets/javascripts/ide/stores/modules/branches/state.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/actions.js41
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/getters.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js18
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/state.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss3
-rw-r--r--app/assets/stylesheets/framework/images.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss60
28 files changed, 736 insertions, 218 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 422becb7db8..25fe2ae553e 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -244,6 +244,18 @@ const Api = {
});
},
+ branches(id, query = '', options = {}) {
+ const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id));
+
+ return axios.get(url, {
+ params: {
+ search: query,
+ per_page: 20,
+ ...options,
+ },
+ });
+ },
+
createBranch(id, { ref, branch }) {
const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id));
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>
diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index f8ce8a67ec0..a601dc8f5a0 100644
--- a/app/assets/javascripts/ide/stores/index.js
+++ b/app/assets/javascripts/ide/stores/index.js
@@ -7,6 +7,7 @@ import mutations from './mutations';
import commitModule from './modules/commit';
import pipelines from './modules/pipelines';
import mergeRequests from './modules/merge_requests';
+import branches from './modules/branches';
Vue.use(Vuex);
@@ -20,6 +21,7 @@ export const createStore = () =>
commit: commitModule,
pipelines,
mergeRequests,
+ branches,
},
});
diff --git a/app/assets/javascripts/ide/stores/modules/branches/actions.js b/app/assets/javascripts/ide/stores/modules/branches/actions.js
new file mode 100644
index 00000000000..74aa98ef9f9
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/branches/actions.js
@@ -0,0 +1,39 @@
+import { __ } from '~/locale';
+import Api from '~/api';
+import * as types from './mutation_types';
+
+export const requestBranches = ({ commit }) => commit(types.REQUEST_BRANCHES);
+export const receiveBranchesError = ({ commit, dispatch }, { search }) => {
+ dispatch(
+ 'setErrorMessage',
+ {
+ text: __('Error loading branches.'),
+ action: payload =>
+ dispatch('fetchBranches', payload).then(() =>
+ dispatch('setErrorMessage', null, { root: true }),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: { search },
+ },
+ { root: true },
+ );
+ commit(types.RECEIVE_BRANCHES_ERROR);
+};
+export const receiveBranchesSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_BRANCHES_SUCCESS, data);
+
+export const fetchBranches = ({ dispatch, rootGetters }, { search = '' }) => {
+ dispatch('requestBranches');
+ dispatch('resetBranches');
+
+ return Api.branches(rootGetters.currentProject.id, search, { sort: 'updated_desc' })
+ .then(({ data }) => dispatch('receiveBranchesSuccess', data))
+ .catch(() => dispatch('receiveBranchesError', { search }));
+};
+
+export const resetBranches = ({ commit }) => commit(types.RESET_BRANCHES);
+
+export const openBranch = ({ rootState, dispatch }, id) =>
+ dispatch('goToRoute', `/project/${rootState.currentProjectId}/edit/${id}`, { root: true });
+
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/branches/index.js b/app/assets/javascripts/ide/stores/modules/branches/index.js
new file mode 100644
index 00000000000..04e7e0f08f1
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/branches/index.js
@@ -0,0 +1,10 @@
+import state from './state';
+import * as actions from './actions';
+import mutations from './mutations';
+
+export default {
+ namespaced: true,
+ state: state(),
+ actions,
+ mutations,
+};
diff --git a/app/assets/javascripts/ide/stores/modules/branches/mutation_types.js b/app/assets/javascripts/ide/stores/modules/branches/mutation_types.js
new file mode 100644
index 00000000000..2272f7b9531
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/branches/mutation_types.js
@@ -0,0 +1,5 @@
+export const REQUEST_BRANCHES = 'REQUEST_BRANCHES';
+export const RECEIVE_BRANCHES_ERROR = 'RECEIVE_BRANCHES_ERROR';
+export const RECEIVE_BRANCHES_SUCCESS = 'RECEIVE_BRANCHES_SUCCESS';
+
+export const RESET_BRANCHES = 'RESET_BRANCHES';
diff --git a/app/assets/javascripts/ide/stores/modules/branches/mutations.js b/app/assets/javascripts/ide/stores/modules/branches/mutations.js
new file mode 100644
index 00000000000..081ec2d4c28
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/branches/mutations.js
@@ -0,0 +1,21 @@
+/* eslint-disable no-param-reassign */
+import * as types from './mutation_types';
+
+export default {
+ [types.REQUEST_BRANCHES](state) {
+ state.isLoading = true;
+ },
+ [types.RECEIVE_BRANCHES_ERROR](state) {
+ state.isLoading = false;
+ },
+ [types.RECEIVE_BRANCHES_SUCCESS](state, data) {
+ state.isLoading = false;
+ state.branches = data.map(branch => ({
+ name: branch.name,
+ committedDate: branch.commit.committed_date,
+ }));
+ },
+ [types.RESET_BRANCHES](state) {
+ state.branches = [];
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/modules/branches/state.js b/app/assets/javascripts/ide/stores/modules/branches/state.js
new file mode 100644
index 00000000000..89bf220c45f
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/branches/state.js
@@ -0,0 +1,4 @@
+export default () => ({
+ isLoading: false,
+ branches: [],
+});
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
index 6ef938b0ae2..baa2497ec5b 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
@@ -1,12 +1,10 @@
import { __ } from '../../../../locale';
import Api from '../../../../api';
-import router from '../../../ide_router';
import { scopes } from './constants';
import * as types from './mutation_types';
-import * as rootTypes from '../../mutation_types';
-export const requestMergeRequests = ({ commit }, type) =>
- commit(types.REQUEST_MERGE_REQUESTS, type);
+export const requestMergeRequests = ({ commit }) =>
+ commit(types.REQUEST_MERGE_REQUESTS);
export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }) => {
dispatch(
'setErrorMessage',
@@ -21,39 +19,22 @@ export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }
},
{ root: true },
);
- commit(types.RECEIVE_MERGE_REQUESTS_ERROR, type);
+ commit(types.RECEIVE_MERGE_REQUESTS_ERROR);
};
-export const receiveMergeRequestsSuccess = ({ commit }, { type, data }) =>
- commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, { type, data });
+export const receiveMergeRequestsSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, data);
export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, search = '' }) => {
- const scope = scopes[type];
- dispatch('requestMergeRequests', type);
- dispatch('resetMergeRequests', type);
+ dispatch('requestMergeRequests');
+ dispatch('resetMergeRequests');
+
+ const scope = type ? scopes[type] : 'all';
return Api.mergeRequests({ scope, state, search })
- .then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data }))
+ .then(({ data }) => dispatch('receiveMergeRequestsSuccess', data))
.catch(() => dispatch('receiveMergeRequestsError', { type, search }));
};
-export const resetMergeRequests = ({ commit }, type) => commit(types.RESET_MERGE_REQUESTS, type);
-
-export const openMergeRequest = ({ commit, dispatch }, { projectPath, id }) => {
- commit(rootTypes.CLEAR_PROJECTS, null, { root: true });
- commit(rootTypes.SET_CURRENT_MERGE_REQUEST, `${id}`, { root: true });
- commit(rootTypes.RESET_OPEN_FILES, null, { root: true });
- dispatch('setCurrentBranchId', '', { root: true });
- dispatch('pipelines/stopPipelinePolling', null, { root: true })
- .then(() => {
- dispatch('pipelines/resetLatestPipeline', null, { root: true });
- dispatch('pipelines/clearEtagPoll', null, { root: true });
- })
- .catch(e => {
- throw e;
- });
- dispatch('setRightPane', null, { root: true });
-
- router.push(`/project/${projectPath}/merge_requests/${id}`);
-};
+export const resetMergeRequests = ({ commit }) => commit(types.RESET_MERGE_REQUESTS);
export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js b/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js
deleted file mode 100644
index 8e2b234be8d..00000000000
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export const getData = state => type => state[type];
-
-export const assignedData = state => state.assigned;
-export const createdData = state => state.created;
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js
index 2e6dfb420f4..04e7e0f08f1 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js
@@ -1,6 +1,5 @@
import state from './state';
import * as actions from './actions';
-import * as getters from './getters';
import mutations from './mutations';
export default {
@@ -8,5 +7,4 @@ export default {
state: state(),
actions,
mutations,
- getters,
};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
index 971da0806bd..98102a68e08 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js
@@ -2,15 +2,15 @@
import * as types from './mutation_types';
export default {
- [types.REQUEST_MERGE_REQUESTS](state, type) {
- state[type].isLoading = true;
+ [types.REQUEST_MERGE_REQUESTS](state) {
+ state.isLoading = true;
},
- [types.RECEIVE_MERGE_REQUESTS_ERROR](state, type) {
- state[type].isLoading = false;
+ [types.RECEIVE_MERGE_REQUESTS_ERROR](state) {
+ state.isLoading = false;
},
- [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, { type, data }) {
- state[type].isLoading = false;
- state[type].mergeRequests = data.map(mergeRequest => ({
+ [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, data) {
+ state.isLoading = false;
+ state.mergeRequests = data.map(mergeRequest => ({
id: mergeRequest.id,
iid: mergeRequest.iid,
title: mergeRequest.title,
@@ -20,7 +20,7 @@ export default {
.replace(`/merge_requests/${mergeRequest.iid}`, ''),
}));
},
- [types.RESET_MERGE_REQUESTS](state, type) {
- state[type].mergeRequests = [];
+ [types.RESET_MERGE_REQUESTS](state) {
+ state.mergeRequests = [];
},
};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js
index 57eb6b04283..4748ccfa2e6 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js
@@ -1,13 +1,7 @@
import { states } from './constants';
export default () => ({
- created: {
- isLoading: false,
- mergeRequests: [],
- },
- assigned: {
- isLoading: false,
- mergeRequests: [],
- },
+ isLoading: false,
+ mergeRequests: [],
state: states.opened,
});
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
index 3cba0c5e633..af5ebcdc40a 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -38,9 +38,17 @@ export default {
v-show="isLoading"
:inline="true"
/>
- <span class="dropdown-toggle-text">
- {{ toggleText }}
- </span>
+ <template>
+ <slot
+ v-if="$slots.default"
+ ></slot>
+ <span
+ v-else
+ class="dropdown-toggle-text"
+ >
+ {{ toggleText }}
+ </span>
+ </template>
<span
v-show="!isLoading"
class="dropdown-toggle-icon"
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index 3cf90b45a97..5e0e7315e99 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -1,7 +1,7 @@
<script>
// only allow classes in images.scss e.g. s12
-const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
+const validSizes = [8, 10, 12, 16, 18, 24, 32, 48, 72];
let iconValidator = () => true;
/*
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index ec4a0f378d0..eebce8b9011 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -571,7 +571,8 @@
margin-bottom: 10px;
padding: 0 10px;
- .fa {
+ .fa,
+ .input-icon {
position: absolute;
top: 10px;
right: 20px;
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index ab3cceceae9..f878ec1ca91 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -39,7 +39,7 @@
svg {
fill: currentColor;
- $svg-sizes: 8 12 16 18 24 32 48 72;
+ $svg-sizes: 8 10 12 16 18 24 32 48 72;
@each $svg-size in $svg-sizes {
&.s#{$svg-size} {
@include svg-size(#{$svg-size}px);
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 37ad6a717d9..c3381e06c30 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -1,6 +1,7 @@
@import 'framework/variables';
@import 'framework/mixins';
+$search-list-icon-width: 18px;
$ide-activity-bar-width: 60px;
$ide-context-header-padding: 10px;
$ide-project-avatar-end: $ide-context-header-padding + 48px;
@@ -49,7 +50,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
display: flex;
flex-direction: column;
flex: 1;
- overflow: hidden;
+ min-height: 0;
.file {
height: 32px;
@@ -541,11 +542,11 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
display: flex;
flex: 1;
flex-direction: column;
- overflow: hidden;
background-color: $white-light;
border-left: 1px solid $white-dark;
border-top: 1px solid $white-dark;
border-top-left-radius: $border-radius-small;
+ min-height: 0;
}
}
@@ -1057,6 +1058,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
flex: 0 0 auto;
display: flex;
align-items: center;
+ flex-wrap: wrap;
padding: 12px 0;
margin-left: $ide-tree-padding;
margin-right: $ide-tree-padding;
@@ -1066,6 +1068,32 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
margin-left: auto;
}
+ .ide-nav-dropdown {
+ width: 100%;
+ margin-bottom: 12px;
+
+ .dropdown-menu {
+ width: 385px;
+ max-height: initial;
+ }
+
+ .dropdown-menu-toggle {
+ svg {
+ vertical-align: middle;
+ }
+
+ &:hover {
+ background-color: $white-normal;
+ }
+ }
+
+ &.show {
+ .dropdown-menu-toggle {
+ background-color: $white-dark;
+ }
+ }
+ }
+
button {
color: $gl-text-color;
}
@@ -1181,7 +1209,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
}
.ide-context-body {
- overflow: hidden;
+ min-height: 0;
}
.ide-sidebar-project-title {
@@ -1331,7 +1359,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
min-height: 60px;
}
-.ide-merge-requests-dropdown {
+.ide-nav-form {
.nav-links li {
width: 50%;
padding-left: 0;
@@ -1350,22 +1378,36 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
padding-left: $gl-padding;
padding-right: $gl-padding;
- .fa {
- right: 26px;
+ .input-icon {
+ right: auto;
+ left: 10px;
+ top: 50%;
+ transform: translateY(-50%);
}
}
+ .dropdown-input-field {
+ padding-left: $search-list-icon-width + $gl-padding;
+ padding-top: 2px;
+ padding-bottom: 2px;
+ }
+
+ .tokens-container {
+ padding-left: $search-list-icon-width + $gl-padding;
+ overflow-x: hidden;
+ }
+
.btn-link {
padding-top: $gl-padding;
padding-bottom: $gl-padding;
}
}
-.ide-merge-request-current-icon {
- min-width: 18px;
+.ide-search-list-current-icon {
+ min-width: $search-list-icon-width;
}
-.ide-merge-requests-empty {
+.ide-search-list-empty {
height: 230px;
}