summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/boards/components
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-21 07:08:36 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-21 07:08:36 +0000
commit48aff82709769b098321c738f3444b9bdaa694c6 (patch)
treee00c7c43e2d9b603a5a6af576b1685e400410dee /app/assets/javascripts/boards/components
parent879f5329ee916a948223f8f43d77fba4da6cd028 (diff)
downloadgitlab-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')
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.vue104
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue18
-rw-r--r--app/assets/javascripts/boards/components/board_configuration_options.vue65
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js30
-rw-r--r--app/assets/javascripts/boards/components/board_extra_actions.vue57
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue25
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue14
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue49
-rw-r--r--app/assets/javascripts/boards/components/board_list_new.vue166
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue33
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue9
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.vue34
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js32
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue6
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_editable_item.vue8
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue120
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>