diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-21 07:08:36 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-21 07:08:36 +0000 |
commit | 48aff82709769b098321c738f3444b9bdaa694c6 (patch) | |
tree | e00c7c43e2d9b603a5a6af576b1685e400410dee /app/assets/javascripts/boards/components | |
parent | 879f5329ee916a948223f8f43d77fba4da6cd028 (diff) | |
download | gitlab-ce-48aff82709769b098321c738f3444b9bdaa694c6.tar.gz |
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc42
Diffstat (limited to 'app/assets/javascripts/boards/components')
18 files changed, 544 insertions, 239 deletions
diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue deleted file mode 100644 index 55e3e4a6329..00000000000 --- a/app/assets/javascripts/boards/components/board_blank_state.vue +++ /dev/null @@ -1,104 +0,0 @@ -<script> -import { GlButton } from '@gitlab/ui'; -import Cookies from 'js-cookie'; -import { __ } from '~/locale'; -import ListLabel from '~/boards/models/label'; -import boardsStore from '../stores/boards_store'; - -export default { - components: { - GlButton, - }, - data() { - return { - predefinedLabels: [ - new ListLabel({ title: __('To Do'), color: '#F0AD4E' }), - new ListLabel({ title: __('Doing'), color: '#5CB85C' }), - ], - }; - }, - methods: { - addDefaultLists() { - this.clearBlankState(); - - this.predefinedLabels.forEach((label, i) => { - boardsStore.addList({ - title: label.title, - position: i, - list_type: 'label', - label: { - title: label.title, - color: label.color, - }, - }); - }); - - const loadListIssues = listObj => { - const list = boardsStore.findList('title', listObj.title); - - if (!list) { - return null; - } - - list.id = listObj.id; - list.label.id = listObj.label.id; - return list.getIssues().catch(() => { - // TODO: handle request error - }); - }; - - // Save the labels - boardsStore - .generateDefaultLists() - .then(res => res.data) - .then(data => Promise.all(data.map(loadListIssues))) - .catch(() => { - boardsStore.removeList(undefined, 'label'); - Cookies.remove('issue_board_welcome_hidden', { - path: '', - }); - boardsStore.addBlankState(); - }); - }, - clearBlankState: boardsStore.removeBlankState.bind(boardsStore), - }, -}; -</script> - -<template> - <div class="board-blank-state p-3"> - <p> - {{ - s__('BoardBlankState|Add the following default lists to your Issue Board with one click:') - }} - </p> - <ul class="list-unstyled board-blank-state-list"> - <li v-for="(label, index) in predefinedLabels" :key="index"> - <span - :style="{ backgroundColor: label.color }" - class="label-color position-relative d-inline-block rounded" - ></span> - {{ label.title }} - </li> - </ul> - <p> - {{ - s__( - 'BoardBlankState|Starting out with the default set of lists will get you right on the way to making the most of your board.', - ) - }} - </p> - <gl-button - category="secondary" - variant="success" - block="block" - class="gl-mb-0" - @click.stop="addDefaultLists" - > - {{ s__('BoardBlankState|Add default lists') }} - </gl-button> - <gl-button category="secondary" variant="default" block="block" @click.stop="clearBlankState"> - {{ s__("BoardBlankState|Nevermind, I'll use my own") }} - </gl-button> - </div> -</template> diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue index 6d216911798..9295065b7b7 100644 --- a/app/assets/javascripts/boards/components/board_column.vue +++ b/app/assets/javascripts/boards/components/board_column.vue @@ -1,13 +1,12 @@ <script> import { mapGetters, mapActions } from 'vuex'; import Sortable from 'sortablejs'; -import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits'; import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue'; import Tooltip from '~/vue_shared/directives/tooltip'; import EmptyComponent from '~/vue_shared/components/empty_component'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import BoardBlankState from './board_blank_state.vue'; import BoardList from './board_list.vue'; +import BoardListNew from './board_list_new.vue'; import boardsStore from '../stores/boards_store'; import eventHub from '../eventhub'; import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options'; @@ -16,14 +15,13 @@ import { ListType } from '../constants'; export default { components: { BoardPromotionState: EmptyComponent, - BoardBlankState, BoardListHeader, - BoardList, + BoardList: gon.features?.graphqlBoardLists ? BoardListNew : BoardList, }, directives: { Tooltip, }, - mixins: [isWipLimitsOn, glFeatureFlagMixin()], + mixins: [glFeatureFlagMixin()], props: { list: { type: Object, @@ -42,7 +40,7 @@ export default { }, inject: { boardId: { - type: String, + default: '', }, }, data() { @@ -54,7 +52,7 @@ export default { computed: { ...mapGetters(['getIssues']), showBoardListAndBoardInfo() { - return this.list.type !== ListType.blank && this.list.type !== ListType.promotion; + return this.list.type !== ListType.promotion; }, uniqueKey() { // eslint-disable-next-line @gitlab/require-i18n-strings @@ -74,7 +72,7 @@ export default { filter: { handler() { if (this.shouldFetchIssues) { - this.fetchIssuesForList(this.list.id); + this.fetchIssuesForList({ listId: this.list.id }); } else { this.list.page = 1; this.list.getIssues(true).catch(() => { @@ -87,7 +85,7 @@ export default { }, mounted() { if (this.shouldFetchIssues) { - this.fetchIssuesForList(this.list.id); + this.fetchIssuesForList({ listId: this.list.id }); } const instance = this; @@ -146,9 +144,7 @@ export default { :disabled="disabled" :issues="listIssues" :list="list" - :loading="list.loading" /> - <board-blank-state v-if="canAdminList && list.id === 'blank'" /> <!-- Will be only available in EE --> <board-promotion-state v-if="list.id === 'promotion'" /> diff --git a/app/assets/javascripts/boards/components/board_configuration_options.vue b/app/assets/javascripts/boards/components/board_configuration_options.vue new file mode 100644 index 00000000000..ad3d653b905 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_configuration_options.vue @@ -0,0 +1,65 @@ +<script> +import { GlFormCheckbox } from '@gitlab/ui'; + +export default { + components: { + GlFormCheckbox, + }, + props: { + currentBoard: { + type: Object, + required: true, + }, + board: { + type: Object, + required: true, + }, + isNewForm: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + const { hide_backlog_list: hideBacklogList, hide_closed_list: hideClosedList } = this.isNewForm + ? this.board + : this.currentBoard; + + return { + hideClosedList, + hideBacklogList, + }; + }, + methods: { + changeClosedList(checked) { + this.board.hideClosedList = !checked; + }, + changeBacklogList(checked) { + this.board.hideBacklogList = !checked; + }, + }, +}; +</script> + +<template> + <div class="append-bottom-20"> + <label class="form-section-title label-bold" for="board-new-name"> + {{ __('List options') }} + </label> + <p class="text-secondary gl-mb-3"> + {{ __('Configure which lists are shown for anyone who visits this board') }} + </p> + <gl-form-checkbox + :checked="!hideBacklogList" + data-testid="backlog-list-checkbox" + @change="changeBacklogList" + >{{ __('Show the Open list') }} + </gl-form-checkbox> + <gl-form-checkbox + :checked="!hideClosedList" + data-testid="closed-list-checkbox" + @change="changeClosedList" + >{{ __('Show the Closed list') }} + </gl-form-checkbox> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue index c7b3da0e672..2515f471379 100644 --- a/app/assets/javascripts/boards/components/board_content.vue +++ b/app/assets/javascripts/boards/components/board_content.vue @@ -1,5 +1,6 @@ <script> import { mapState, mapGetters, mapActions } from 'vuex'; +import { sortBy } from 'lodash'; import BoardColumn from 'ee_else_ce/boards/components/board_column.vue'; import { GlAlert } from '@gitlab/ui'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -30,7 +31,9 @@ export default { ...mapState(['boardLists', 'error']), ...mapGetters(['isSwimlanesOn']), boardListsToUse() { - return this.glFeatures.graphqlBoardLists ? this.boardLists : this.lists; + const lists = + this.glFeatures.graphqlBoardLists || this.isSwimlanesOn ? this.boardLists : this.lists; + return sortBy([...Object.values(lists)], 'position'); }, }, mounted() { @@ -68,7 +71,7 @@ export default { <template v-else> <epics-swimlanes ref="swimlanes" - :lists="boardLists" + :lists="boardListsToUse" :can-admin-list="canAdminList" :disabled="disabled" /> diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js deleted file mode 100644 index b74234a2e3c..00000000000 --- a/app/assets/javascripts/boards/components/board_delete.js +++ /dev/null @@ -1,30 +0,0 @@ -import $ from 'jquery'; -import Vue from 'vue'; -import { GlButton, GlTooltipDirective } from '@gitlab/ui'; -import { __ } from '~/locale'; - -export default Vue.extend({ - components: { - GlButton, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - props: { - list: { - type: Object, - default: () => ({}), - required: false, - }, - }, - methods: { - deleteBoard() { - $(this.$el).tooltip('hide'); - - // eslint-disable-next-line no-alert - if (window.confirm(__('Are you sure you want to delete this list?'))) { - this.list.destroy(); - } - }, - }, -}); diff --git a/app/assets/javascripts/boards/components/board_extra_actions.vue b/app/assets/javascripts/boards/components/board_extra_actions.vue new file mode 100644 index 00000000000..b802ccc7882 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_extra_actions.vue @@ -0,0 +1,57 @@ +<script> +import { GlTooltip, GlButton } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + name: 'BoardExtraActions', + components: { + GlTooltip, + GlButton, + }, + props: { + canAdminList: { + type: Boolean, + required: true, + }, + disabled: { + type: Boolean, + required: true, + }, + openModal: { + type: Function, + required: true, + }, + }, + computed: { + tooltipTitle() { + if (this.disabled) { + return __('Please add a list to your board first'); + } + + return ''; + }, + }, +}; +</script> + +<template> + <div class="board-extra-actions"> + <span ref="addIssuesButtonTooltip" class="gl-ml-3"> + <gl-button + v-if="canAdminList" + type="button" + data-placement="bottom" + data-track-event="click_button" + data-track-label="board_add_issues" + :disabled="disabled" + :aria-disabled="disabled" + @click="openModal" + > + {{ __('Add issues') }} + </gl-button> + </span> + <gl-tooltip v-if="disabled" :target="() => $refs.addIssuesButtonTooltip" placement="bottom"> + {{ tooltipTitle }} + </gl-tooltip> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index 385dd5fdc71..793c594cf16 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -5,6 +5,8 @@ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; import { visitUrl } from '~/lib/utils/url_utility'; import boardsStore from '~/boards/stores/boards_store'; +import BoardConfigurationOptions from './board_configuration_options.vue'; + const boardDefaults = { id: false, name: '', @@ -13,12 +15,15 @@ const boardDefaults = { assignee: {}, assignee_id: undefined, weight: null, + hide_backlog_list: false, + hide_closed_list: false, }; export default { components: { BoardScope: () => import('ee_component/boards/components/board_scope.vue'), DeprecatedModal, + BoardConfigurationOptions, }, props: { canAdminBoard: { @@ -140,7 +145,17 @@ export default { } else { boardsStore .createBoard(this.board) - .then(resp => resp.data) + .then(resp => { + // This handles 2 use cases + // - In create call we only get one parameter, the new board + // - In update call, due to Promise.all, we get REST response in + // array index 0 + + if (Array.isArray(resp)) { + return resp[0].data; + } + return resp.data ? resp.data : resp; + }) .then(data => { visitUrl(data.board_path); }) @@ -182,7 +197,7 @@ export default { <form v-else class="js-board-config-modal" @submit.prevent> <div v-if="!readonly" class="append-bottom-20"> <label class="form-section-title label-bold" for="board-new-name">{{ - __('Board name') + __('Title') }}</label> <input id="board-new-name" @@ -196,6 +211,12 @@ export default { /> </div> + <board-configuration-options + :is-new-form="isNewForm" + :board="board" + :current-board="currentBoard" + /> + <board-scope v-if="scopedIssueBoardFeatureEnabled" :collapse-scope="isNewForm" diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 25f8ffca633..d01df44e7e4 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -14,6 +14,8 @@ import { sortableEnd, } from '../mixins/sortable_default_options'; +// This component is being replaced in favor of './board_list_new.vue' for GraphQL boards + if (gon.features && gon.features.multiSelectBoard) { Sortable.mount(new MultiDrag()); } @@ -39,10 +41,6 @@ export default { type: Array, required: true, }, - loading: { - type: Boolean, - required: true, - }, }, data() { return { @@ -62,6 +60,9 @@ export default { issuesSizeExceedsMax() { return this.list.maxIssueCount > 0 && this.list.issuesSize > this.list.maxIssueCount; }, + loading() { + return this.list.loading; + }, }, watch: { filters: { @@ -72,7 +73,6 @@ export default { deep: true, }, issues() { - if (this.glFeatures.graphqlBoardLists) return; this.$nextTick(() => { if ( this.scrollHeight() <= this.listHeight() && @@ -98,6 +98,8 @@ export default { eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop); }, mounted() { + // TODO: Use Draggable in ./board_list_new.vue to drag & drop issue + // https://gitlab.com/gitlab-org/gitlab/-/issues/218164 const multiSelectOpts = {}; if (gon.features && gon.features.multiSelectBoard) { multiSelectOpts.multiDrag = true; @@ -403,8 +405,6 @@ export default { this.showIssueForm = !this.showIssueForm; }, onScroll() { - if (this.glFeatures.graphqlBoardLists) return; - if (!this.list.loadingMore && this.scrollTop() > this.scrollHeight() - this.scrollOffset) { this.loadNextPage(); } diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index 361fe252afb..bb9a1b79d91 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -1,5 +1,5 @@ <script> -import { mapActions } from 'vuex'; +import { mapActions, mapState } from 'vuex'; import { GlButton, GlButtonGroup, @@ -9,20 +9,18 @@ import { GlSprintf, GlTooltipDirective, } from '@gitlab/ui'; -import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits'; import { n__, s__ } from '~/locale'; import AccessorUtilities from '../../lib/utils/accessor'; -import BoardDelete from './board_delete'; import IssueCount from './issue_count.vue'; import boardsStore from '../stores/boards_store'; import eventHub from '../eventhub'; -import { ListType } from '../constants'; +import sidebarEventHub from '~/sidebar/event_hub'; +import { inactiveId, LIST, ListType } from '../constants'; import { isScopedLabel } from '~/lib/utils/common_utils'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { - BoardDelete, GlButtonGroup, GlButton, GlLabel, @@ -34,7 +32,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - mixins: [isWipLimitsOn, glFeatureFlagMixin()], + mixins: [glFeatureFlagMixin()], props: { list: { type: Object, @@ -45,11 +43,6 @@ export default { type: Boolean, required: true, }, - canAdminList: { - type: Boolean, - required: false, - default: false, - }, isSwimlanesHeader: { type: Boolean, required: false, @@ -58,7 +51,7 @@ export default { }, inject: { boardId: { - type: String, + default: '', }, }, data() { @@ -67,6 +60,7 @@ export default { }; }, computed: { + ...mapState(['activeId']), isLoggedIn() { return Boolean(gon.current_user_id); }, @@ -114,10 +108,7 @@ export default { }, isSettingsShown() { return ( - this.listType !== ListType.backlog && - this.showListHeaderButton && - this.list.isExpanded && - this.isWipLimitsOn + this.listType !== ListType.backlog && this.showListHeaderButton && this.list.isExpanded ); }, showBoardListAndBoardInfo() { @@ -135,7 +126,14 @@ export default { }, }, methods: { - ...mapActions(['updateList']), + ...mapActions(['updateList', 'setActiveId']), + openSidebarSettings() { + if (this.activeId === inactiveId) { + sidebarEventHub.$emit('sidebar.closeAll'); + } + + this.setActiveId({ id: this.list.id, sidebarType: LIST }); + }, showScopedLabels(label) { return boardsStore.scopedLabels.enabled && isScopedLabel(label); }, @@ -176,7 +174,6 @@ export default { <header :class="{ 'has-border': list.label && list.label.color, - 'gl-relative': list.isExpanded, 'gl-h-full': !list.isExpanded, 'board-inner gl-rounded-top-left-base gl-rounded-top-right-base': isSwimlanesHeader, }" @@ -279,22 +276,6 @@ export default { </div> </gl-tooltip> - <board-delete - v-if="canAdminList && !list.preset && list.id" - :list="list" - inline-template="true" - > - <gl-button - v-gl-tooltip.hover.bottom - :class="{ 'gl-display-none': !list.isExpanded }" - :aria-label="__('Delete list')" - class="board-delete no-drag gl-pr-0 gl-shadow-none! gl-mr-3" - :title="__('Delete list')" - icon="remove" - size="small" - @click.stop="deleteBoard" - /> - </board-delete> <div v-if="showBoardListAndBoardInfo" class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag text-secondary" diff --git a/app/assets/javascripts/boards/components/board_list_new.vue b/app/assets/javascripts/boards/components/board_list_new.vue new file mode 100644 index 00000000000..0a495d05122 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_list_new.vue @@ -0,0 +1,166 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import { GlLoadingIcon } from '@gitlab/ui'; +import BoardNewIssue from './board_new_issue.vue'; +import BoardCard from './board_card.vue'; +import eventHub from '../eventhub'; +import boardsStore from '../stores/boards_store'; +import { sprintf, __ } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; + +export default { + name: 'BoardList', + components: { + BoardCard, + BoardNewIssue, + GlLoadingIcon, + }, + mixins: [glFeatureFlagMixin()], + props: { + disabled: { + type: Boolean, + required: true, + }, + list: { + type: Object, + required: true, + }, + issues: { + type: Array, + required: true, + }, + }, + data() { + return { + scrollOffset: 250, + filters: boardsStore.state.filters, + showCount: false, + showIssueForm: false, + }; + }, + computed: { + ...mapState(['pageInfoByListId', 'listsFlags']), + paginatedIssueText() { + return sprintf(__('Showing %{pageSize} of %{total} issues'), { + pageSize: this.issues.length, + total: this.list.issuesSize, + }); + }, + issuesSizeExceedsMax() { + return this.list.maxIssueCount > 0 && this.list.issuesSize > this.list.maxIssueCount; + }, + hasNextPage() { + return this.pageInfoByListId[this.list.id].hasNextPage; + }, + loading() { + return this.listsFlags[this.list.id]?.isLoading; + }, + }, + watch: { + filters: { + handler() { + this.list.loadingMore = false; + this.$refs.list.scrollTop = 0; + }, + deep: true, + }, + issues() { + this.$nextTick(() => { + this.showCount = this.scrollHeight() > Math.ceil(this.listHeight()); + }); + }, + }, + created() { + eventHub.$on(`toggle-issue-form-${this.list.id}`, this.toggleForm); + eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop); + }, + mounted() { + // Scroll event on list to load more + this.$refs.list.addEventListener('scroll', this.onScroll); + }, + beforeDestroy() { + eventHub.$off(`toggle-issue-form-${this.list.id}`, this.toggleForm); + eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop); + this.$refs.list.removeEventListener('scroll', this.onScroll); + }, + methods: { + ...mapActions(['fetchIssuesForList']), + listHeight() { + return this.$refs.list.getBoundingClientRect().height; + }, + scrollHeight() { + return this.$refs.list.scrollHeight; + }, + scrollTop() { + return this.$refs.list.scrollTop + this.listHeight(); + }, + scrollToTop() { + this.$refs.list.scrollTop = 0; + }, + loadNextPage() { + const loadingDone = () => { + this.list.loadingMore = false; + }; + this.list.loadingMore = true; + this.fetchIssuesForList({ listId: this.list.id, fetchNext: true }) + .then(loadingDone) + .catch(loadingDone); + }, + toggleForm() { + this.showIssueForm = !this.showIssueForm; + }, + onScroll() { + window.requestAnimationFrame(() => { + if ( + !this.list.loadingMore && + this.scrollTop() > this.scrollHeight() - this.scrollOffset && + this.hasNextPage + ) { + this.loadNextPage(); + } + }); + }, + }, +}; +</script> + +<template> + <div + v-show="list.isExpanded" + class="board-list-component gl-relative gl-h-full gl-display-flex gl-flex-direction-column" + data-qa-selector="board_list_cards_area" + > + <div + v-if="loading" + class="gl-mt-4 gl-text-center" + :aria-label="__('Loading issues')" + data-testid="board_list_loading" + > + <gl-loading-icon /> + </div> + <board-new-issue v-if="list.type !== 'closed' && showIssueForm" :list="list" /> + <ul + v-show="!loading" + ref="list" + :data-board="list.id" + :data-board-type="list.type" + :class="{ 'bg-danger-100': issuesSizeExceedsMax }" + class="board-list gl-w-full gl-h-full gl-list-style-none gl-mb-0 gl-p-2 js-board-list" + > + <board-card + v-for="(issue, index) in issues" + ref="issue" + :key="issue.id" + :index="index" + :list="list" + :issue="issue" + :disabled="disabled" + /> + <li v-if="showCount" class="board-list-count gl-text-center" data-issue-id="-1"> + <gl-loading-icon v-show="list.loadingMore" label="Loading more issues" /> + <span v-if="issues.length === list.issuesSize">{{ __('Showing all issues') }}</span> + <span v-else>{{ paginatedIssueText }}</span> + </li> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index 348d485ff37..0a665b82880 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -22,11 +22,7 @@ export default { required: true, }, }, - inject: { - groupId: { - type: Number, - }, - }, + inject: ['groupId'], data() { return { title: '', diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue index e2600883e89..392e056dcbf 100644 --- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue @@ -1,5 +1,5 @@ <script> -import { GlDrawer, GlLabel } from '@gitlab/ui'; +import { GlButton, GlDrawer, GlLabel } from '@gitlab/ui'; import { mapActions, mapState, mapGetters } from 'vuex'; import { __ } from '~/locale'; import boardsStore from '~/boards/stores/boards_store'; @@ -17,6 +17,7 @@ export default { label: 'label', labelListText: __('Label'), components: { + GlButton, GlDrawer, GlLabel, BoardSettingsSidebarWipLimit: () => @@ -25,16 +26,23 @@ export default { import('ee_component/boards/components/board_settings_list_types.vue'), }, mixins: [glFeatureFlagMixin()], + props: { + canAdminList: { + type: Boolean, + required: false, + default: false, + }, + }, computed: { - ...mapGetters(['isSidebarOpen']), + ...mapGetters(['isSidebarOpen', 'shouldUseGraphQL']), ...mapState(['activeId', 'sidebarType', 'boardLists']), activeList() { /* Warning: Though a computed property it is not reactive because we are referencing a List Model class. Reactivity only applies to plain JS objects */ - if (this.glFeatures.graphqlBoardLists) { - return this.boardLists.find(({ id }) => id === this.activeId); + if (this.shouldUseGraphQL) { + return this.boardLists[this.activeId]; } return boardsStore.state.lists.find(({ id }) => id === this.activeId); }, @@ -62,6 +70,13 @@ export default { showScopedLabels(label) { return boardsStore.scopedLabels.enabled && isScopedLabel(label); }, + deleteBoard() { + // eslint-disable-next-line no-alert + if (window.confirm(__('Are you sure you want to delete this list?'))) { + this.activeList.destroy(); + this.unsetActiveId(); + } + }, }, }; </script> @@ -91,6 +106,16 @@ export default { :board-list-type="boardListType" /> <board-settings-sidebar-wip-limit :max-issue-count="activeList.maxIssueCount" /> + <div v-if="canAdminList && !activeList.preset && activeList.id" class="gl-m-4"> + <gl-button + variant="danger" + category="secondary" + icon="remove" + data-testid="remove-list" + @click.stop="deleteBoard" + >{{ __('Remove list') }} + </gl-button> + </div> </template> </gl-drawer> </template> diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index 8658f51e5cf..a181ea51c4a 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -41,14 +41,7 @@ export default { default: false, }, }, - inject: { - groupId: { - type: Number, - }, - rootPath: { - type: String, - }, - }, + inject: ['groupId', 'rootPath'], data() { return { limitBeforeCounter: 2, diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue index a71fda9d7c5..b066fb25360 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.vue +++ b/app/assets/javascripts/boards/components/modal/tabs.vue @@ -1,9 +1,15 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ +import { GlTabs, GlTab, GlBadge } from '@gitlab/ui'; import ModalStore from '../../stores/modal_store'; import modalMixin from '../../mixins/modal_mixins'; export default { + components: { + GlTabs, + GlTab, + GlBadge, + }, mixins: [modalMixin], data() { return ModalStore.store; @@ -19,18 +25,18 @@ export default { }; </script> <template> - <div class="top-area gl-mt-3 gl-mb-3"> - <ul class="nav-links issues-state-filters"> - <li :class="{ active: activeTab == 'all' }"> - <a href="#" role="button" @click.prevent="changeTab('all')"> - Open issues <span class="badge badge-pill"> {{ issuesCount }} </span> - </a> - </li> - <li :class="{ active: activeTab == 'selected' }"> - <a href="#" role="button" @click.prevent="changeTab('selected')"> - Selected issues <span class="badge badge-pill"> {{ selectedCount }} </span> - </a> - </li> - </ul> - </div> + <gl-tabs class="gl-mt-3"> + <gl-tab @click.prevent="changeTab('all')"> + <template slot="title"> + <span>Open issues</span> + <gl-badge size="sm" class="gl-tab-counter-badge">{{ issuesCount }}</gl-badge> + </template> + </gl-tab> + <gl-tab @click.prevent="changeTab('selected')"> + <template slot="title"> + <span>Selected issues</span> + <gl-badge size="sm" class="gl-tab-counter-badge">{{ selectedCount }}</gl-badge> + </template> + </gl-tab> + </gl-tabs> </template> diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 2e356f1353a..c8926c5ef2a 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -6,8 +6,14 @@ import axios from '~/lib/utils/axios_utils'; import { deprecatedCreateFlash as flash } from '~/flash'; import CreateLabelDropdown from '../../create_label'; import boardsStore from '../stores/boards_store'; +import { fullLabelId } from '../boards_util'; +import store from '~/boards/stores'; import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; +function shouldCreateListGraphQL(label) { + return store.getters.shouldUseGraphQL && !store.getters.getListByLabelId(fullLabelId(label)); +} + $(document) .off('created.label') .on('created.label', (e, label, addNewList) => { @@ -15,16 +21,20 @@ $(document) return; } - boardsStore.new({ - title: label.title, - position: boardsStore.state.lists.length - 2, - list_type: 'label', - label: { - id: label.id, + if (shouldCreateListGraphQL(label)) { + store.dispatch('createList', { labelId: fullLabelId(label) }); + } else { + boardsStore.new({ title: label.title, - color: label.color, - }, - }); + position: boardsStore.state.lists.length - 2, + list_type: 'label', + label: { + id: label.id, + title: label.title, + color: label.color, + }, + }); + } }); export default function initNewListDropdown() { @@ -74,7 +84,9 @@ export default function initNewListDropdown() { const label = options.selectedObj; e.preventDefault(); - if (!boardsStore.findListByLabelId(label.id)) { + if (shouldCreateListGraphQL(label)) { + store.dispatch('createList', { labelId: fullLabelId(label) }); + } else if (!boardsStore.findListByLabelId(label.id)) { boardsStore.new({ title: label.title, position: boardsStore.state.lists.length - 2, diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index 59e7620962a..566c0081b9d 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -20,11 +20,7 @@ export default { required: true, }, }, - inject: { - groupId: { - type: Number, - }, - }, + inject: ['groupId'], data() { return { loading: true, diff --git a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue index 8df03ea581f..5fb7a9b210c 100644 --- a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue +++ b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue @@ -36,16 +36,18 @@ export default { } this.edit = true; - this.$emit('changed', this.edit); + this.$emit('open'); window.addEventListener('click', this.collapseWhenOffClick); }, - collapse() { + collapse({ emitEvent = true } = {}) { if (!this.edit) { return; } this.edit = false; - this.$emit('changed', this.edit); + if (emitEvent) { + this.$emit('close'); + } window.removeEventListener('click', this.collapseWhenOffClick); }, }, diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue new file mode 100644 index 00000000000..0f063c7582e --- /dev/null +++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue @@ -0,0 +1,120 @@ +<script> +import { mapGetters, mapActions } from 'vuex'; +import { GlLabel } from '@gitlab/ui'; +import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; +import { isScopedLabel } from '~/lib/utils/common_utils'; +import createFlash from '~/flash'; +import { __ } from '~/locale'; + +export default { + components: { + BoardEditableItem, + LabelsSelect, + GlLabel, + }, + data() { + return { + loading: false, + }; + }, + inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'], + computed: { + ...mapGetters({ issue: 'getActiveIssue' }), + selectedLabels() { + const { labels = [] } = this.issue; + + return labels.map(label => ({ + ...label, + id: getIdFromGraphQLId(label.id), + })); + }, + issueLabels() { + const { labels = [] } = this.issue; + + return labels.map(label => ({ + ...label, + scoped: isScopedLabel(label), + })); + }, + projectPath() { + const { referencePath = '' } = this.issue; + return referencePath.slice(0, referencePath.indexOf('#')); + }, + }, + methods: { + ...mapActions(['setActiveIssueLabels']), + async setLabels(payload) { + this.loading = true; + this.$refs.sidebarItem.collapse(); + + try { + const addLabelIds = payload.filter(label => label.set).map(label => label.id); + const removeLabelIds = this.selectedLabels + .filter(label => !payload.find(selected => selected.id === label.id)) + .map(label => label.id); + + const input = { addLabelIds, removeLabelIds, projectPath: this.projectPath }; + await this.setActiveIssueLabels(input); + } catch (e) { + createFlash({ message: __('An error occurred while updating labels.') }); + } finally { + this.loading = false; + } + }, + async removeLabel(id) { + this.loading = true; + + try { + const removeLabelIds = [getIdFromGraphQLId(id)]; + const input = { removeLabelIds, projectPath: this.projectPath }; + await this.setActiveIssueLabels(input); + } catch (e) { + createFlash({ message: __('An error occurred when removing the label.') }); + } finally { + this.loading = false; + } + }, + }, +}; +</script> + +<template> + <board-editable-item ref="sidebarItem" :title="__('Labels')" :loading="loading"> + <template #collapsed> + <gl-label + v-for="label in issueLabels" + :key="label.id" + :background-color="label.color" + :title="label.title" + :description="label.description" + :scoped="label.scoped" + :show-close-button="true" + :disabled="loading" + class="gl-mr-2 gl-mb-2" + @close="removeLabel(label.id)" + /> + </template> + <template> + <labels-select + ref="labelsSelect" + :allow-label-edit="false" + :allow-label-create="false" + :allow-multiselect="true" + :allow-scoped-labels="true" + :selected-labels="selectedLabels" + :labels-fetch-path="labelsFetchPath" + :labels-manage-path="labelsManagePath" + :labels-filter-base-path="labelsFilterBasePath" + :labels-list-title="__('Select label')" + :dropdown-button-text="__('Choose labels')" + variant="embedded" + class="gl-display-block labels gl-w-full" + @updateSelectedLabels="setLabels" + > + {{ __('None') }} + </labels-select> + </template> + </board-editable-item> +</template> |