summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/boards
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-19 08:27:35 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-19 08:27:35 +0000
commit7e9c479f7de77702622631cff2628a9c8dcbc627 (patch)
treec8f718a08e110ad7e1894510980d2155a6549197 /app/assets/javascripts/boards
parente852b0ae16db4052c1c567d9efa4facc81146e88 (diff)
downloadgitlab-ce-7e9c479f7de77702622631cff2628a9c8dcbc627.tar.gz
Add latest changes from gitlab-org/gitlab@13-6-stable-eev13.6.0-rc42
Diffstat (limited to 'app/assets/javascripts/boards')
-rw-r--r--app/assets/javascripts/boards/components/board_assignee_dropdown.vue178
-rw-r--r--app/assets/javascripts/boards/components/board_card_layout.vue5
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue49
-rw-r--r--app/assets/javascripts/boards/components/board_column_new.vue94
-rw-r--r--app/assets/javascripts/boards/components/board_configuration_options.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue8
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue15
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue50
-rw-r--r--app/assets/javascripts/boards/components/board_list_header_new.vue358
-rw-r--r--app/assets/javascripts/boards/components/board_list_new.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue34
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue_new.vue129
-rw-r--r--app/assets/javascripts/boards/components/board_promotion_state.js1
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue18
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue2
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue35
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.vue21
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js3
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue12
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue111
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue14
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue71
-rw-r--r--app/assets/javascripts/boards/constants.js4
-rw-r--r--app/assets/javascripts/boards/graphql/mutations/issue_set_subscription.mutation.graphql8
-rw-r--r--app/assets/javascripts/boards/index.js12
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js5
-rw-r--r--app/assets/javascripts/boards/queries/board_labels.query.graphql23
-rw-r--r--app/assets/javascripts/boards/queries/board_list_destroy.mutation.graphql5
-rw-r--r--app/assets/javascripts/boards/queries/issue_create.mutation.graphql10
-rw-r--r--app/assets/javascripts/boards/queries/issue_set_due_date.mutation.graphql8
-rw-r--r--app/assets/javascripts/boards/queries/users_search.query.graphql11
-rw-r--r--app/assets/javascripts/boards/stores/actions.js193
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js7
-rw-r--r--app/assets/javascripts/boards/stores/getters.js11
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js9
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js41
-rw-r--r--app/assets/javascripts/boards/toggle_focus.js5
38 files changed, 1362 insertions, 206 deletions
diff --git a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
new file mode 100644
index 00000000000..c81f171af2b
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
@@ -0,0 +1,178 @@
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import {
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlAvatarLabeled,
+ GlAvatarLink,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import { __, n__ } from '~/locale';
+import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
+import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
+import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
+import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql';
+import searchUsers from '~/boards/queries/users_search.query.graphql';
+
+export default {
+ noSearchDelay: 0,
+ searchDelay: 250,
+ i18n: {
+ unassigned: __('Unassigned'),
+ assignee: __('Assignee'),
+ assignees: __('Assignees'),
+ assignTo: __('Assign to'),
+ },
+ components: {
+ BoardEditableItem,
+ IssuableAssignees,
+ MultiSelectDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlAvatarLabeled,
+ GlAvatarLink,
+ GlSearchBoxByType,
+ },
+ data() {
+ return {
+ search: '',
+ participants: [],
+ selected: this.$store.getters.activeIssue.assignees,
+ };
+ },
+ apollo: {
+ participants: {
+ query() {
+ return this.isSearchEmpty ? getIssueParticipants : searchUsers;
+ },
+ variables() {
+ if (this.isSearchEmpty) {
+ return {
+ id: `gid://gitlab/Issue/${this.activeIssue.iid}`,
+ };
+ }
+
+ return {
+ search: this.search,
+ };
+ },
+ update(data) {
+ if (this.isSearchEmpty) {
+ return data.issue?.participants?.nodes || [];
+ }
+
+ return data.users?.nodes || [];
+ },
+ debounce() {
+ const { noSearchDelay, searchDelay } = this.$options;
+
+ return this.isSearchEmpty ? noSearchDelay : searchDelay;
+ },
+ },
+ },
+ computed: {
+ ...mapGetters(['activeIssue']),
+ assigneeText() {
+ return n__('Assignee', '%d Assignees', this.selected.length);
+ },
+ unSelectedFiltered() {
+ return this.participants.filter(({ username }) => {
+ return !this.selectedUserNames.includes(username);
+ });
+ },
+ selectedIsEmpty() {
+ return this.selected.length === 0;
+ },
+ selectedUserNames() {
+ return this.selected.map(({ username }) => username);
+ },
+ isSearchEmpty() {
+ return this.search === '';
+ },
+ },
+ methods: {
+ ...mapActions(['setAssignees']),
+ clearSelected() {
+ this.selected = [];
+ },
+ selectAssignee(name) {
+ if (name === undefined) {
+ this.clearSelected();
+ return;
+ }
+
+ this.selected = this.selected.concat(name);
+ },
+ unselect(name) {
+ this.selected = this.selected.filter(user => user.username !== name);
+ },
+ saveAssignees() {
+ this.setAssignees(this.selectedUserNames);
+ },
+ isChecked(id) {
+ return this.selectedUserNames.includes(id);
+ },
+ },
+};
+</script>
+
+<template>
+ <board-editable-item :title="assigneeText" @close="saveAssignees">
+ <template #collapsed>
+ <issuable-assignees :users="activeIssue.assignees" />
+ </template>
+
+ <template #default>
+ <multi-select-dropdown
+ class="w-100"
+ :text="$options.i18n.assignees"
+ :header-text="$options.i18n.assignTo"
+ >
+ <template #search>
+ <gl-search-box-by-type v-model.trim="search" />
+ </template>
+ <template #items>
+ <gl-dropdown-item
+ :is-checked="selectedIsEmpty"
+ data-testid="unassign"
+ class="mt-2"
+ @click="selectAssignee()"
+ >{{ $options.i18n.unassigned }}</gl-dropdown-item
+ >
+ <gl-dropdown-divider data-testid="unassign-divider" />
+ <gl-dropdown-item
+ v-for="item in selected"
+ :key="item.id"
+ :is-checked="isChecked(item.username)"
+ @click="unselect(item.username)"
+ >
+ <gl-avatar-link>
+ <gl-avatar-labeled
+ :size="32"
+ :label="item.name"
+ :sub-label="item.username"
+ :src="item.avatarUrl || item.avatar"
+ />
+ </gl-avatar-link>
+ </gl-dropdown-item>
+ <gl-dropdown-divider v-if="!selectedIsEmpty" data-testid="selected-user-divider" />
+ <gl-dropdown-item
+ v-for="unselectedUser in unSelectedFiltered"
+ :key="unselectedUser.id"
+ :data-testid="`item_${unselectedUser.name}`"
+ @click="selectAssignee(unselectedUser)"
+ >
+ <gl-avatar-link>
+ <gl-avatar-labeled
+ :size="32"
+ :label="unselectedUser.name"
+ :sub-label="unselectedUser.username"
+ :src="unselectedUser.avatarUrl || unselectedUser.avatar"
+ />
+ </gl-avatar-link>
+ </gl-dropdown-item>
+ </template>
+ </multi-select-dropdown>
+ </template>
+ </board-editable-item>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_card_layout.vue b/app/assets/javascripts/boards/components/board_card_layout.vue
index 072dd87861a..f796acd2303 100644
--- a/app/assets/javascripts/boards/components/board_card_layout.vue
+++ b/app/assets/javascripts/boards/components/board_card_layout.vue
@@ -44,9 +44,6 @@ export default {
multiSelectVisible() {
return this.multiSelect.list.findIndex(issue => issue.id === this.issue.id) > -1;
},
- canMultiSelect() {
- return gon.features && gon.features.multiSelectBoard;
- },
},
methods: {
mouseDown() {
@@ -59,7 +56,7 @@ export default {
// Don't do anything if this happened on a no trigger element
if (e.target.classList.contains('js-no-trigger')) return;
- const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey);
+ const isMultiSelect = e.ctrlKey || e.metaKey;
if (this.showDetail || isMultiSelect) {
this.showDetail = false;
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index 9295065b7b7..cb93340bcf8 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -1,14 +1,10 @@
<script>
-import { mapGetters, mapActions } from 'vuex';
+// This component is being replaced in favor of './board_column_new.vue' for GraphQL boards
import Sortable from 'sortablejs';
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 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';
import { ListType } from '../constants';
@@ -16,12 +12,8 @@ export default {
components: {
BoardPromotionState: EmptyComponent,
BoardListHeader,
- BoardList: gon.features?.graphqlBoardLists ? BoardListNew : BoardList,
+ BoardList,
},
- directives: {
- Tooltip,
- },
- mixins: [glFeatureFlagMixin()],
props: {
list: {
type: Object,
@@ -50,44 +42,25 @@ export default {
};
},
computed: {
- ...mapGetters(['getIssues']),
showBoardListAndBoardInfo() {
return this.list.type !== ListType.promotion;
},
- uniqueKey() {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- return `boards.${this.boardId}.${this.list.type}.${this.list.id}`;
- },
listIssues() {
- if (!this.glFeatures.graphqlBoardLists) {
- return this.list.issues;
- }
- return this.getIssues(this.list.id);
- },
- shouldFetchIssues() {
- return this.glFeatures.graphqlBoardLists && this.list.type !== ListType.blank;
+ return this.list.issues;
},
},
watch: {
filter: {
handler() {
- if (this.shouldFetchIssues) {
- this.fetchIssuesForList({ listId: this.list.id });
- } else {
- this.list.page = 1;
- this.list.getIssues(true).catch(() => {
- // TODO: handle request error
- });
- }
+ this.list.page = 1;
+ this.list.getIssues(true).catch(() => {
+ // TODO: handle request error
+ });
},
deep: true,
},
},
mounted() {
- if (this.shouldFetchIssues) {
- this.fetchIssuesForList({ listId: this.list.id });
- }
-
const instance = this;
const sortableOptions = getBoardSortableDefaultOptions({
@@ -113,12 +86,6 @@ export default {
Sortable.create(this.$el.parentNode, sortableOptions);
},
- methods: {
- ...mapActions(['fetchIssuesForList']),
- showListNewIssueForm(listId) {
- eventHub.$emit('showForm', listId);
- },
- },
};
</script>
@@ -131,7 +98,7 @@ export default {
'board-type-assignee': list.type === 'assignee',
}"
:data-id="list.id"
- class="board gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal"
+ class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal"
data-qa-selector="board_list"
>
<div
diff --git a/app/assets/javascripts/boards/components/board_column_new.vue b/app/assets/javascripts/boards/components/board_column_new.vue
new file mode 100644
index 00000000000..8a59355eb83
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_column_new.vue
@@ -0,0 +1,94 @@
+<script>
+import { mapGetters, mapActions, mapState } from 'vuex';
+import BoardListHeader from 'ee_else_ce/boards/components/board_list_header_new.vue';
+import BoardPromotionState from 'ee_else_ce/boards/components/board_promotion_state';
+import BoardList from './board_list_new.vue';
+import { ListType } from '../constants';
+
+export default {
+ components: {
+ BoardPromotionState,
+ BoardListHeader,
+ BoardList,
+ },
+ props: {
+ list: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
+ disabled: {
+ type: Boolean,
+ required: true,
+ },
+ canAdminList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ inject: {
+ boardId: {
+ default: '',
+ },
+ },
+ computed: {
+ ...mapState(['filterParams']),
+ ...mapGetters(['getIssuesByList']),
+ showBoardListAndBoardInfo() {
+ return this.list.type !== ListType.promotion;
+ },
+ listIssues() {
+ return this.getIssuesByList(this.list.id);
+ },
+ shouldFetchIssues() {
+ return this.list.type !== ListType.blank;
+ },
+ },
+ watch: {
+ filterParams: {
+ handler() {
+ if (this.shouldFetchIssues) {
+ this.fetchIssuesForList({ listId: this.list.id });
+ }
+ },
+ deep: true,
+ immediate: true,
+ },
+ },
+ methods: {
+ ...mapActions(['fetchIssuesForList']),
+ // TODO: Reordering of lists https://gitlab.com/gitlab-org/gitlab/-/issues/280515
+ },
+};
+</script>
+
+<template>
+ <div
+ :class="{
+ 'is-draggable': !list.preset,
+ 'is-expandable': list.isExpandable,
+ 'is-collapsed': !list.isExpanded,
+ 'board-type-assignee': list.type === 'assignee',
+ }"
+ :data-id="list.id"
+ class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal"
+ data-qa-selector="board_list"
+ >
+ <div
+ class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
+ >
+ <board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" />
+ <board-list
+ v-if="showBoardListAndBoardInfo"
+ ref="board-list"
+ :disabled="disabled"
+ :issues="listIssues"
+ :list="list"
+ />
+
+ <!-- Will be only available in EE -->
+ <board-promotion-state v-if="list.id === 'promotion'" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_configuration_options.vue b/app/assets/javascripts/boards/components/board_configuration_options.vue
index ad3d653b905..754b00b54e0 100644
--- a/app/assets/javascripts/boards/components/board_configuration_options.vue
+++ b/app/assets/javascripts/boards/components/board_configuration_options.vue
@@ -43,7 +43,7 @@ export default {
<template>
<div class="append-bottom-20">
- <label class="form-section-title label-bold" for="board-new-name">
+ <label class="label-bold gl-font-lg" for="board-new-name">
{{ __('List options') }}
</label>
<p class="text-secondary gl-mb-3">
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 2515f471379..92976574efb 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -1,13 +1,14 @@
<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 BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
+import BoardColumnNew from './board_column_new.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
- BoardColumn,
+ BoardColumn: gon.features?.graphqlBoardLists ? BoardColumnNew : BoardColumn,
BoardContentSidebar: () => import('ee_component/boards/components/board_content_sidebar.vue'),
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
GlAlert,
@@ -38,12 +39,11 @@ export default {
},
mounted() {
if (this.glFeatures.graphqlBoardLists) {
- this.fetchLists();
this.showPromotionList();
}
},
methods: {
- ...mapActions(['fetchLists', 'showPromotionList']),
+ ...mapActions(['showPromotionList']),
},
};
</script>
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 793c594cf16..e4ef3600ff9 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -196,9 +196,7 @@ export default {
<p v-if="isDeleteForm">{{ __('Are you sure you want to delete this board?') }}</p>
<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">{{
- __('Title')
- }}</label>
+ <label class="label-bold gl-font-lg" for="board-new-name">{{ __('Title') }}</label>
<input
id="board-new-name"
ref="name"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index d01df44e7e4..53989e2d9de 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -16,9 +16,7 @@ import {
// 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());
-}
+Sortable.mount(new MultiDrag());
export default {
name: 'BoardList',
@@ -100,12 +98,11 @@ export default {
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;
- multiSelectOpts.selectedClass = 'js-multi-select';
- multiSelectOpts.animation = 500;
- }
+ const multiSelectOpts = {
+ multiDrag: true,
+ selectedClass: 'js-multi-select',
+ animation: 500,
+ };
const options = getBoardSortableDefaultOptions({
scroll: true,
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index bb9a1b79d91..d85ba2038a7 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -17,7 +17,6 @@ import eventHub from '../eventhub';
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: {
@@ -32,7 +31,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagMixin()],
props: {
list: {
type: Object,
@@ -121,12 +119,9 @@ export default {
collapsedTooltipTitle() {
return this.listTitle || this.listAssignee;
},
- shouldDisplaySwimlanes() {
- return this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn;
- },
},
methods: {
- ...mapActions(['updateList', 'setActiveId']),
+ ...mapActions(['setActiveId']),
openSidebarSettings() {
if (this.activeId === inactiveId) {
sidebarEventHub.$emit('sidebar.closeAll');
@@ -160,11 +155,7 @@ export default {
}
},
updateListFunction() {
- if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) {
- this.updateList({ listId: this.list.id, collapsed: !this.list.isExpanded });
- } else {
- this.list.update();
- }
+ this.list.update();
},
},
};
@@ -188,8 +179,9 @@ export default {
'gl-py-3 gl-h-full': !list.isExpanded && !isSwimlanesHeader,
'gl-border-b-0': !list.isExpanded || isSwimlanesHeader,
'gl-py-2': !list.isExpanded && isSwimlanesHeader,
+ 'gl-flex-direction-column': !list.isExpanded,
}"
- class="board-title gl-m-0 gl-display-flex js-board-handle"
+ class="board-title gl-m-0 gl-display-flex gl-align-items-center gl-font-base gl-px-3 js-board-handle"
>
<gl-button
v-if="list.isExpandable"
@@ -202,7 +194,15 @@ export default {
@click="toggleExpanded"
/>
<!-- The following is only true in EE and if it is a milestone -->
- <span v-if="showMilestoneListDetails" aria-hidden="true" class="gl-mr-2 milestone-icon">
+ <span
+ v-if="showMilestoneListDetails"
+ aria-hidden="true"
+ class="milestone-icon"
+ :class="{
+ 'gl-mt-3 gl-rotate-90': !list.isExpanded,
+ 'gl-mr-2': list.isExpanded,
+ }"
+ >
<gl-icon name="timer" />
</span>
@@ -210,6 +210,9 @@ export default {
v-if="showAssigneeListDetails"
:href="list.assignee.path"
class="user-avatar-link js-no-trigger"
+ :class="{
+ 'gl-mt-3 gl-rotate-90': !list.isExpanded,
+ }"
>
<img
v-gl-tooltip.hover.bottom
@@ -223,20 +226,28 @@ export default {
</a>
<div
class="board-title-text"
- :class="{ 'gl-display-none': !list.isExpanded && isSwimlanesHeader }"
+ :class="{
+ 'gl-display-none': !list.isExpanded && isSwimlanesHeader,
+ 'gl-flex-grow-0 gl-my-3 gl-mx-0': !list.isExpanded,
+ 'gl-flex-grow-1': list.isExpanded,
+ }"
>
<span
v-if="list.type !== 'label'"
v-gl-tooltip.hover
:class="{
- 'gl-display-inline-block': list.type === 'milestone',
+ 'gl-display-block': !list.isExpanded || list.type === 'milestone',
}"
:title="listTitle"
- class="board-title-main-text block-truncated"
+ class="board-title-main-text gl-text-truncate"
>
{{ list.title }}
</span>
- <span v-if="list.type === 'assignee'" class="board-title-sub-text gl-ml-2">
+ <span
+ v-if="list.type === 'assignee'"
+ class="gl-ml-2 gl-font-weight-normal gl-text-gray-500"
+ :class="{ 'gl-display-none': !list.isExpanded }"
+ >
@{{ listAssignee }}
</span>
<gl-label
@@ -279,7 +290,10 @@ export default {
<div
v-if="showBoardListAndBoardInfo"
class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag text-secondary"
- :class="{ 'gl-display-none!': !list.isExpanded && isSwimlanesHeader }"
+ :class="{
+ 'gl-display-none!': !list.isExpanded && isSwimlanesHeader,
+ 'gl-p-0': !list.isExpanded,
+ }"
>
<span class="gl-display-inline-flex">
<gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltipLabel" />
diff --git a/app/assets/javascripts/boards/components/board_list_header_new.vue b/app/assets/javascripts/boards/components/board_list_header_new.vue
new file mode 100644
index 00000000000..99347a4cd4d
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_list_header_new.vue
@@ -0,0 +1,358 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import {
+ GlButton,
+ GlButtonGroup,
+ GlLabel,
+ GlTooltip,
+ GlIcon,
+ GlSprintf,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { n__, s__ } from '~/locale';
+import AccessorUtilities from '../../lib/utils/accessor';
+import IssueCount from './issue_count.vue';
+import eventHub from '../eventhub';
+import sidebarEventHub from '~/sidebar/event_hub';
+import { inactiveId, LIST, ListType } from '../constants';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+
+export default {
+ components: {
+ GlButtonGroup,
+ GlButton,
+ GlLabel,
+ GlTooltip,
+ GlIcon,
+ GlSprintf,
+ IssueCount,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ list: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
+ disabled: {
+ type: Boolean,
+ required: true,
+ },
+ isSwimlanesHeader: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ inject: {
+ boardId: {
+ default: '',
+ },
+ weightFeatureAvailable: {
+ default: false,
+ },
+ scopedLabelsAvailable: {
+ default: false,
+ },
+ currentUserId: {
+ default: null,
+ },
+ },
+ computed: {
+ ...mapState(['activeId']),
+ isLoggedIn() {
+ return Boolean(this.currentUserId);
+ },
+ listType() {
+ return this.list.type;
+ },
+ listAssignee() {
+ return this.list?.assignee?.username || '';
+ },
+ listTitle() {
+ return this.list?.label?.description || this.list.title || '';
+ },
+ showListHeaderButton() {
+ return (
+ !this.disabled &&
+ this.listType !== ListType.closed &&
+ this.listType !== ListType.blank &&
+ this.listType !== ListType.promotion
+ );
+ },
+ showMilestoneListDetails() {
+ return (
+ this.list.type === ListType.milestone &&
+ this.list.milestone &&
+ (this.list.isExpanded || !this.isSwimlanesHeader)
+ );
+ },
+ showAssigneeListDetails() {
+ return (
+ this.list.type === ListType.assignee && (this.list.isExpanded || !this.isSwimlanesHeader)
+ );
+ },
+ issuesCount() {
+ return this.list.issuesSize;
+ },
+ issuesTooltipLabel() {
+ return n__(`%d issue`, `%d issues`, this.issuesCount);
+ },
+ chevronTooltip() {
+ return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand');
+ },
+ chevronIcon() {
+ return this.list.isExpanded ? 'chevron-right' : 'chevron-down';
+ },
+ isNewIssueShown() {
+ return this.listType === ListType.backlog || this.showListHeaderButton;
+ },
+ isSettingsShown() {
+ return (
+ this.listType !== ListType.backlog && this.showListHeaderButton && this.list.isExpanded
+ );
+ },
+ showBoardListAndBoardInfo() {
+ return this.listType !== ListType.blank && this.listType !== ListType.promotion;
+ },
+ uniqueKey() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `boards.${this.boardId}.${this.listType}.${this.list.id}`;
+ },
+ collapsedTooltipTitle() {
+ return this.listTitle || this.listAssignee;
+ },
+ headerStyle() {
+ return { borderTopColor: this.list?.label?.color };
+ },
+ },
+ methods: {
+ ...mapActions(['updateList', 'setActiveId']),
+ openSidebarSettings() {
+ if (this.activeId === inactiveId) {
+ sidebarEventHub.$emit('sidebar.closeAll');
+ }
+
+ this.setActiveId({ id: this.list.id, sidebarType: LIST });
+ },
+ showScopedLabels(label) {
+ return this.scopedLabelsAvailable && isScopedLabel(label);
+ },
+
+ showNewIssueForm() {
+ eventHub.$emit(`toggle-issue-form-${this.list.id}`);
+ },
+ toggleExpanded() {
+ this.list.isExpanded = !this.list.isExpanded;
+
+ if (!this.isLoggedIn) {
+ this.addToLocalStorage();
+ } else {
+ this.updateListFunction();
+ }
+
+ // When expanding/collapsing, the tooltip on the caret button sometimes stays open.
+ // Close all tooltips manually to prevent dangling tooltips.
+ this.$root.$emit('bv::hide::tooltip');
+ },
+ addToLocalStorage() {
+ if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded);
+ }
+ },
+ updateListFunction() {
+ this.updateList({ listId: this.list.id, collapsed: !this.list.isExpanded });
+ },
+ },
+};
+</script>
+
+<template>
+ <header
+ :class="{
+ 'has-border': list.label && list.label.color,
+ 'gl-h-full': !list.isExpanded,
+ 'board-inner gl-rounded-top-left-base gl-rounded-top-right-base': isSwimlanesHeader,
+ }"
+ :style="headerStyle"
+ class="board-header gl-relative"
+ data-qa-selector="board_list_header"
+ data-testid="board-list-header"
+ >
+ <h3
+ :class="{
+ 'user-can-drag': !disabled && !list.preset,
+ 'gl-py-3 gl-h-full': !list.isExpanded && !isSwimlanesHeader,
+ 'gl-border-b-0': !list.isExpanded || isSwimlanesHeader,
+ 'gl-py-2': !list.isExpanded && isSwimlanesHeader,
+ 'gl-flex-direction-column': !list.isExpanded,
+ }"
+ class="board-title gl-m-0 gl-display-flex gl-align-items-center gl-font-base gl-px-3 js-board-handle"
+ >
+ <gl-button
+ v-if="list.isExpandable"
+ v-gl-tooltip.hover
+ :aria-label="chevronTooltip"
+ :title="chevronTooltip"
+ :icon="chevronIcon"
+ class="board-title-caret no-drag gl-cursor-pointer"
+ variant="link"
+ @click="toggleExpanded"
+ />
+ <!-- EE start -->
+ <span
+ v-if="showMilestoneListDetails"
+ aria-hidden="true"
+ class="milestone-icon"
+ :class="{
+ 'gl-mt-3 gl-rotate-90': !list.isExpanded,
+ 'gl-mr-2': list.isExpanded,
+ }"
+ >
+ <gl-icon name="timer" />
+ </span>
+
+ <a
+ v-if="showAssigneeListDetails"
+ :href="list.assignee.path"
+ class="user-avatar-link js-no-trigger"
+ :class="{
+ 'gl-mt-3 gl-rotate-90': !list.isExpanded,
+ }"
+ >
+ <img
+ v-gl-tooltip.hover.bottom
+ :title="listAssignee"
+ :alt="list.assignee.name"
+ :src="list.assignee.avatar"
+ class="avatar s20"
+ height="20"
+ width="20"
+ />
+ </a>
+ <!-- EE end -->
+ <div
+ class="board-title-text"
+ :class="{
+ 'gl-display-none': !list.isExpanded && isSwimlanesHeader,
+ 'gl-flex-grow-0 gl-my-3 gl-mx-0': !list.isExpanded,
+ 'gl-flex-grow-1': list.isExpanded,
+ }"
+ >
+ <!-- EE start -->
+ <span
+ v-if="listType !== 'label'"
+ v-gl-tooltip.hover
+ :class="{
+ 'gl-display-block': !list.isExpanded || listType === 'milestone',
+ }"
+ :title="listTitle"
+ class="board-title-main-text gl-text-truncate"
+ >
+ {{ list.title }}
+ </span>
+ <span
+ v-if="listType === 'assignee'"
+ v-show="list.isExpanded"
+ class="gl-ml-2 gl-font-weight-normal gl-text-gray-500"
+ >
+ @{{ listAssignee }}
+ </span>
+ <!-- EE end -->
+ <gl-label
+ v-if="listType === 'label'"
+ v-gl-tooltip.hover.bottom
+ :background-color="list.label.color"
+ :description="list.label.description"
+ :scoped="showScopedLabels(list.label)"
+ :size="!list.isExpanded ? 'sm' : ''"
+ :title="list.label.title"
+ />
+ </div>
+
+ <!-- EE start -->
+ <span
+ v-if="isSwimlanesHeader && !list.isExpanded"
+ ref="collapsedInfo"
+ aria-hidden="true"
+ class="board-header-collapsed-info-icon gl-mt-2 gl-cursor-pointer gl-text-gray-500"
+ >
+ <gl-icon name="information" />
+ </span>
+ <gl-tooltip v-if="isSwimlanesHeader && !list.isExpanded" :target="() => $refs.collapsedInfo">
+ <div class="gl-font-weight-bold gl-pb-2">{{ collapsedTooltipTitle }}</div>
+ <div v-if="list.maxIssueCount !== 0">
+ •
+ <gl-sprintf :message="__('%{issuesSize} with a limit of %{maxIssueCount}')">
+ <template #issuesSize>{{ issuesTooltipLabel }}</template>
+ <template #maxIssueCount>{{ list.maxIssueCount }}</template>
+ </gl-sprintf>
+ </div>
+ <div v-else>• {{ issuesTooltipLabel }}</div>
+ <div v-if="weightFeatureAvailable">
+ •
+ <gl-sprintf :message="__('%{totalWeight} total weight')">
+ <template #totalWeight>{{ list.totalWeight }}</template>
+ </gl-sprintf>
+ </div>
+ </gl-tooltip>
+ <!-- EE end -->
+
+ <div
+ v-if="showBoardListAndBoardInfo"
+ class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag gl-text-gray-500"
+ :class="{
+ 'gl-display-none!': !list.isExpanded && isSwimlanesHeader,
+ 'gl-p-0': !list.isExpanded,
+ }"
+ >
+ <span class="gl-display-inline-flex">
+ <gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltipLabel" />
+ <span ref="issueCount" class="issue-count-badge-count">
+ <gl-icon class="gl-mr-2" name="issues" />
+ <issue-count :issues-size="issuesCount" :max-issue-count="list.maxIssueCount" />
+ </span>
+ <!-- EE start -->
+ <template v-if="weightFeatureAvailable">
+ <gl-tooltip :target="() => $refs.weightTooltip" :title="weightCountToolTip" />
+ <span ref="weightTooltip" class="gl-display-inline-flex gl-ml-3">
+ <gl-icon class="gl-mr-2" name="weight" />
+ {{ list.totalWeight }}
+ </span>
+ </template>
+ <!-- EE end -->
+ </span>
+ </div>
+ <gl-button-group
+ v-if="isNewIssueShown || isSettingsShown"
+ class="board-list-button-group pl-2"
+ >
+ <gl-button
+ v-if="isNewIssueShown"
+ v-show="list.isExpanded"
+ ref="newIssueBtn"
+ v-gl-tooltip.hover
+ :aria-label="__('New issue')"
+ :title="__('New issue')"
+ class="issue-count-badge-add-button no-drag"
+ icon="plus"
+ @click="showNewIssueForm"
+ />
+
+ <gl-button
+ v-if="isSettingsShown"
+ ref="settingsBtn"
+ v-gl-tooltip.hover
+ :aria-label="__('List settings')"
+ class="no-drag js-board-settings-button"
+ :title="__('List settings')"
+ icon="settings"
+ @click="openSidebarSettings"
+ />
+ <gl-tooltip :target="() => $refs.settingsBtn">{{ __('List settings') }}</gl-tooltip>
+ </gl-button-group>
+ </h3>
+ </header>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_list_new.vue b/app/assets/javascripts/boards/components/board_list_new.vue
index 0a495d05122..396aedcc557 100644
--- a/app/assets/javascripts/boards/components/board_list_new.vue
+++ b/app/assets/javascripts/boards/components/board_list_new.vue
@@ -1,7 +1,7 @@
<script>
import { mapActions, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
-import BoardNewIssue from './board_new_issue.vue';
+import BoardNewIssue from './board_new_issue_new.vue';
import BoardCard from './board_card.vue';
import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 0a665b82880..a9e6d768656 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,6 +1,4 @@
<script>
-import $ from 'jquery';
-import { mapActions, mapGetters } from 'vuex';
import { GlButton } from '@gitlab/ui';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
import ListIssue from 'ee_else_ce/boards/models/issue';
@@ -9,6 +7,8 @@ import ProjectSelect from './project_select.vue';
import boardsStore from '../stores/boards_store';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+// This component is being replaced in favor of './board_new_issue_new.vue' for GraphQL boards
+
export default {
name: 'BoardNewIssue',
components: {
@@ -31,23 +31,18 @@ export default {
};
},
computed: {
- ...mapGetters(['isSwimlanesOn']),
disabled() {
if (this.groupId) {
return this.title === '' || !this.selectedProject.name;
}
return this.title === '';
},
- shouldDisplaySwimlanes() {
- return this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn;
- },
},
mounted() {
this.$refs.input.focus();
eventHub.$on('setSelectedProject', this.setSelectedProject);
},
methods: {
- ...mapActions(['addListIssue', 'addListIssueFailure']),
submit(e) {
e.preventDefault();
if (this.title.trim() === '') return Promise.resolve();
@@ -74,31 +69,14 @@ export default {
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
- if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) {
- this.addListIssue({ list: this.list, issue, position: 0 });
- }
-
return this.list
.newIssue(issue)
.then(() => {
- // Need this because our jQuery very kindly disables buttons on ALL form submissions
- $(this.$refs.submitButton).enable();
-
- if (!this.shouldDisplaySwimlanes && !this.glFeatures.graphqlBoardLists) {
- boardsStore.setIssueDetail(issue);
- boardsStore.setListDetail(this.list);
- }
+ boardsStore.setIssueDetail(issue);
+ boardsStore.setListDetail(this.list);
})
.catch(() => {
- // Need this because our jQuery very kindly disables buttons on ALL form submissions
- $(this.$refs.submitButton).enable();
-
- // Remove the issue
- if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) {
- this.addListIssueFailure({ list: this.list, issue });
- } else {
- this.list.removeIssue(issue);
- }
+ this.list.removeIssue(issue);
// Show error message
this.error = true;
@@ -137,7 +115,7 @@ export default {
<gl-button
ref="submitButton"
:disabled="disabled"
- class="float-left"
+ class="float-left js-no-auto-disable"
variant="success"
category="primary"
type="submit"
diff --git a/app/assets/javascripts/boards/components/board_new_issue_new.vue b/app/assets/javascripts/boards/components/board_new_issue_new.vue
new file mode 100644
index 00000000000..969c84ddb59
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_new_issue_new.vue
@@ -0,0 +1,129 @@
+<script>
+import { mapActions } from 'vuex';
+import { GlButton } from '@gitlab/ui';
+import { getMilestone } from 'ee_else_ce/boards/boards_util';
+import eventHub from '../eventhub';
+import ProjectSelect from './project_select.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { __ } from '~/locale';
+
+export default {
+ name: 'BoardNewIssue',
+ i18n: {
+ submit: __('Submit issue'),
+ cancel: __('Cancel'),
+ },
+ components: {
+ ProjectSelect,
+ GlButton,
+ },
+ mixins: [glFeatureFlagMixin()],
+ props: {
+ list: {
+ type: Object,
+ required: true,
+ },
+ },
+ inject: ['groupId', 'weightFeatureAvailable', 'boardWeight'],
+ data() {
+ return {
+ title: '',
+ selectedProject: {},
+ };
+ },
+ computed: {
+ disabled() {
+ if (this.groupId) {
+ return this.title === '' || !this.selectedProject.name;
+ }
+ return this.title === '';
+ },
+ inputFieldId() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `${this.list.id}-title`;
+ },
+ },
+ mounted() {
+ this.$refs.input.focus();
+ eventHub.$on('setSelectedProject', this.setSelectedProject);
+ },
+ methods: {
+ ...mapActions(['addListNewIssue']),
+ submit(e) {
+ e.preventDefault();
+
+ const labels = this.list.label ? [this.list.label] : [];
+ const assignees = this.list.assignee ? [this.list.assignee] : [];
+ const milestone = getMilestone(this.list);
+
+ const weight = this.weightFeatureAvailable ? this.boardWeight : undefined;
+
+ const { title } = this;
+
+ eventHub.$emit(`scroll-board-list-${this.list.id}`);
+
+ return this.addListNewIssue({
+ issueInput: {
+ title,
+ labelIds: labels?.map(l => l.id),
+ assigneeIds: assignees?.map(a => a?.id),
+ milestoneId: milestone?.id,
+ projectPath: this.selectedProject.path,
+ weight: weight >= 0 ? weight : null,
+ },
+ list: this.list,
+ }).then(() => {
+ this.reset();
+ });
+ },
+ reset() {
+ this.title = '';
+ eventHub.$emit(`toggle-issue-form-${this.list.id}`);
+ },
+ setSelectedProject(selectedProject) {
+ this.selectedProject = selectedProject;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="board-new-issue-form">
+ <div class="board-card position-relative p-3 rounded">
+ <form ref="submitForm" @submit="submit">
+ <label :for="inputFieldId" class="label-bold">{{ __('Title') }}</label>
+ <input
+ :id="inputFieldId"
+ ref="input"
+ v-model="title"
+ class="form-control"
+ type="text"
+ name="issue_title"
+ autocomplete="off"
+ />
+ <project-select v-if="groupId" :group-id="groupId" :list="list" />
+ <div class="clearfix gl-mt-3">
+ <gl-button
+ ref="submitButton"
+ :disabled="disabled"
+ class="float-left js-no-auto-disable"
+ variant="success"
+ category="primary"
+ type="submit"
+ >
+ {{ $options.i18n.submit }}
+ </gl-button>
+ <gl-button
+ ref="cancelButton"
+ class="float-right"
+ type="button"
+ variant="default"
+ @click="reset"
+ >
+ {{ $options.i18n.cancel }}
+ </gl-button>
+ </div>
+ </form>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_promotion_state.js b/app/assets/javascripts/boards/components/board_promotion_state.js
new file mode 100644
index 00000000000..ff8b4c56321
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_promotion_state.js
@@ -0,0 +1 @@
+export default {};
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 392e056dcbf..80070b25bd0 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -36,6 +36,9 @@ export default {
computed: {
...mapGetters(['isSidebarOpen', 'shouldUseGraphQL']),
...mapState(['activeId', 'sidebarType', 'boardLists']),
+ isWipLimitsOn() {
+ return this.glFeatures.wipLimits;
+ },
activeList() {
/*
Warning: Though a computed property it is not reactive because we are
@@ -66,14 +69,18 @@ export default {
eventHub.$off('sidebar.closeAll', this.unsetActiveId);
},
methods: {
- ...mapActions(['unsetActiveId']),
+ ...mapActions(['unsetActiveId', 'removeList']),
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();
+ if (window.confirm(__('Are you sure you want to remove this list?'))) {
+ if (this.shouldUseGraphQL) {
+ this.removeList(this.activeId);
+ } else {
+ this.activeList.destroy();
+ }
this.unsetActiveId();
}
},
@@ -105,7 +112,10 @@ export default {
:active-list="activeList"
:board-list-type="boardListType"
/>
- <board-settings-sidebar-wip-limit :max-issue-count="activeList.maxIssueCount" />
+ <board-settings-sidebar-wip-limit
+ v-if="isWipLimitsOn"
+ :max-issue-count="activeList.maxIssueCount"
+ />
<div v-if="canAdminList && !activeList.preset && activeList.id" class="gl-m-4">
<gl-button
variant="danger"
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 271e1fc4b5f..0b079c78209 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -261,7 +261,7 @@ export default {
>
<gl-deprecated-dropdown-item
v-show="filteredBoards.length === 0"
- class="no-pointer-events text-secondary"
+ class="gl-pointer-events-none text-secondary"
>
{{ s__('IssueBoards|No matching boards found') }}
</gl-deprecated-dropdown-item>
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index a181ea51c4a..45ce1e51489 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -3,7 +3,7 @@ import { sortBy } from 'lodash';
import { mapState } from 'vuex';
import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
-import { sprintf, __ } from '~/locale';
+import { sprintf, __, n__ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import IssueDueDate from './issue_due_date.vue';
@@ -89,6 +89,12 @@ export default {
orderedLabels() {
return sortBy(this.issue.labels.filter(this.isNonListLabel), 'title');
},
+ blockedLabel() {
+ if (this.issue.blockedByCount) {
+ return n__(`Blocked by %d issue`, `Blocked by %d issues`, this.issue.blockedByCount);
+ }
+ return __('Blocked issue');
+ },
},
methods: {
isIndexLessThanlimit(index) {
@@ -133,15 +139,16 @@ export default {
</script>
<template>
<div>
- <div class="d-flex board-card-header" dir="auto">
+ <div class="gl-display-flex" dir="auto">
<h4 class="board-card-title gl-mb-0 gl-mt-0">
<gl-icon
v-if="issue.blocked"
v-gl-tooltip
name="issue-block"
- :title="__('Blocked issue')"
+ :title="blockedLabel"
class="issue-blocked-icon gl-mr-2"
- :aria-label="__('Blocked issue')"
+ :aria-label="blockedLabel"
+ data-testid="issue-blocked-icon"
/>
<gl-icon
v-if="issue.confidential"
@@ -156,7 +163,7 @@ export default {
}}</a>
</h4>
</div>
- <div v-if="showLabelFooter" class="board-card-labels gl-mt-2 d-flex flex-wrap">
+ <div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap">
<template v-for="label in orderedLabels">
<gl-label
:key="label.id"
@@ -169,24 +176,26 @@ export default {
/>
</template>
</div>
- <div class="board-card-footer d-flex justify-content-between align-items-end">
+ <div
+ class="board-card-footer gl-display-flex gl-justify-content-space-between gl-align-items-flex-end"
+ >
<div
- class="d-flex align-items-start flex-wrap-reverse board-card-number-container overflow-hidden js-board-card-number-container"
+ class="gl-display-flex align-items-start flex-wrap-reverse board-card-number-container gl-overflow-hidden js-board-card-number-container"
>
<span
v-if="issue.referencePath"
- class="board-card-number overflow-hidden d-flex gl-mr-3 gl-mt-3"
+ class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3"
>
<tooltip-on-truncate
v-if="issueReferencePath"
:title="issueReferencePath"
placement="bottom"
- class="board-issue-path block-truncated bold"
+ class="board-issue-path gl-text-truncate gl-font-weight-bold"
>{{ issueReferencePath }}</tooltip-on-truncate
>
#{{ issue.iid }}
</span>
- <span class="board-info-items gl-mt-3 d-inline-block">
+ <span class="board-info-items gl-mt-3 gl-display-inline-block">
<issue-due-date v-if="issue.dueDate" :date="issue.dueDate" :closed="issue.closed" />
<issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" />
<issue-card-weight
@@ -196,20 +205,20 @@ export default {
/>
</span>
</div>
- <div class="board-card-assignee d-flex">
+ <div class="board-card-assignee gl-display-flex">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
v-if="shouldRenderAssignee(index)"
:key="assignee.id"
:link-href="assigneeUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)"
- :img-src="assignee.avatar || assignee.avatar_url"
+ :img-src="assignee.avatarUrl || assignee.avatar || assignee.avatar_url"
:img-size="24"
class="js-no-trigger"
tooltip-placement="bottom"
>
<span class="js-assignee-tooltip">
- <span class="bold d-block">{{ __('Assignee') }}</span>
+ <span class="gl-font-weight-bold gl-display-block">{{ __('Assignee') }}</span>
{{ assignee.name }}
<span class="text-white-50">@{{ assignee.username }}</span>
</span>
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue
index cd4512f320f..eb2db260717 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.vue
+++ b/app/assets/javascripts/boards/components/modal/empty_state.vue
@@ -1,13 +1,13 @@
<script>
-/* eslint-disable vue/no-v-html */
-import { GlButton } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
+import { GlButton, GlSprintf } from '@gitlab/ui';
+import { __ } from '~/locale';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
export default {
components: {
GlButton,
+ GlSprintf,
},
mixins: [modalMixin],
props: {
@@ -34,11 +34,8 @@ export default {
if (this.activeTab === 'selected') {
obj.title = __("You haven't selected any issues yet");
- obj.content = sprintf(
- __(
- 'Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board.',
- ),
- { startTag: '<strong>', endTag: '</strong>' },
+ obj.content = __(
+ 'Go back to %{tagStart}Open issues%{tagEnd} and select some issues to add to your board.',
);
}
@@ -57,7 +54,13 @@ export default {
<div class="col-12 col-md-6 order-md-first">
<div class="text-content">
<h4>{{ contents.title }}</h4>
- <p v-html="contents.content"></p>
+ <p>
+ <gl-sprintf :message="contents.content">
+ <template #tag="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
<gl-button
v-if="activeTab === 'all'"
:href="newIssuePath"
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index c8926c5ef2a..47eee5306da 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -7,6 +7,7 @@ import { deprecatedCreateFlash as flash } from '~/flash';
import CreateLabelDropdown from '../../create_label';
import boardsStore from '../stores/boards_store';
import { fullLabelId } from '../boards_util';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import store from '~/boards/stores';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
@@ -61,7 +62,7 @@ export default function initNewListDropdown() {
const active = boardsStore.findListByLabelId(label.id);
const $li = $('<li />');
const $a = $('<a />', {
- class: active ? `is-active js-board-list-${active.id}` : '',
+ class: active ? `is-active js-board-list-${getIdFromGraphQLId(active.id)}` : '',
text: label.title,
href: '#',
});
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 566c0081b9d..f90fe582566 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -44,6 +44,7 @@ export default {
this.selectedProject = {
id: $el.data('project-id'),
name: $el.data('project-name'),
+ path: $el.data('project-path'),
};
eventHub.$emit('setSelectedProject', this.selectedProject);
},
@@ -75,11 +76,12 @@ export default {
renderRow(project) {
return `
<li>
- <a href='#' class='dropdown-menu-link' data-project-id="${
- project.id
- }" data-project-name="${project.name}" data-project-name-with-namespace="${
- project.name_with_namespace
- }">
+ <a href='#' class='dropdown-menu-link'
+ data-project-id="${project.id}"
+ data-project-name="${project.name}"
+ data-project-name-with-namespace="${project.name_with_namespace}"
+ data-project-path="${project.path_with_namespace}"
+ >
${escape(project.name_with_namespace)}
</a>
</li>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
new file mode 100644
index 00000000000..6935ead2706
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
@@ -0,0 +1,111 @@
+<script>
+import { mapGetters, mapActions } from 'vuex';
+import { GlButton, GlDatepicker } from '@gitlab/ui';
+import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
+import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ BoardEditableItem,
+ GlButton,
+ GlDatepicker,
+ },
+ data() {
+ return {
+ loading: false,
+ };
+ },
+ computed: {
+ ...mapGetters({ issue: 'activeIssue', projectPathForActiveIssue: 'projectPathForActiveIssue' }),
+ hasDueDate() {
+ return this.issue.dueDate != null;
+ },
+ parsedDueDate() {
+ if (!this.hasDueDate) {
+ return null;
+ }
+
+ return parsePikadayDate(this.issue.dueDate);
+ },
+ formattedDueDate() {
+ if (!this.hasDueDate) {
+ return '';
+ }
+
+ return dateInWords(this.parsedDueDate, true);
+ },
+ },
+ methods: {
+ ...mapActions(['setActiveIssueDueDate']),
+ async openDatePicker() {
+ await this.$nextTick();
+ this.$refs.datePicker.calendar.show();
+ },
+ async setDueDate(date) {
+ this.loading = true;
+ this.$refs.sidebarItem.collapse();
+
+ try {
+ const dueDate = date ? formatDate(date, 'yyyy-mm-dd') : null;
+ await this.setActiveIssueDueDate({ dueDate, projectPath: this.projectPathForActiveIssue });
+ } catch (e) {
+ createFlash({ message: this.$options.i18n.updateDueDateError });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ i18n: {
+ dueDate: __('Due date'),
+ removeDueDate: __('remove due date'),
+ updateDueDateError: __('An error occurred when updating the issue due date'),
+ },
+};
+</script>
+
+<template>
+ <board-editable-item
+ ref="sidebarItem"
+ class="board-sidebar-due-date"
+ :title="$options.i18n.dueDate"
+ :loading="loading"
+ @open="openDatePicker"
+ >
+ <template v-if="hasDueDate" #collapsed>
+ <div class="gl-display-flex gl-align-items-center">
+ <strong class="gl-text-gray-900">{{ formattedDueDate }}</strong>
+ <span class="gl-mx-2">-</span>
+ <gl-button
+ variant="link"
+ class="gl-text-gray-400!"
+ data-testid="reset-button"
+ :disabled="loading"
+ @click="setDueDate(null)"
+ >
+ {{ $options.i18n.removeDueDate }}
+ </gl-button>
+ </div>
+ </template>
+ <template>
+ <gl-datepicker
+ ref="datePicker"
+ :value="parsedDueDate"
+ show-clear-button
+ @input="setDueDate"
+ @clear="setDueDate(null)"
+ />
+ </template>
+ </board-editable-item>
+</template>
+<style>
+/*
+ * This can be removed after closing:
+ * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1048
+ */
+.board-sidebar-due-date .gl-datepicker,
+.board-sidebar-due-date .gl-datepicker-input {
+ width: 100%;
+}
+</style>
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
index 0f063c7582e..9d537a4ef2c 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
@@ -21,9 +21,9 @@ export default {
},
inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'],
computed: {
- ...mapGetters({ issue: 'getActiveIssue' }),
+ ...mapGetters(['activeIssue', 'projectPathForActiveIssue']),
selectedLabels() {
- const { labels = [] } = this.issue;
+ const { labels = [] } = this.activeIssue;
return labels.map(label => ({
...label,
@@ -31,17 +31,13 @@ export default {
}));
},
issueLabels() {
- const { labels = [] } = this.issue;
+ const { labels = [] } = this.activeIssue;
return labels.map(label => ({
...label,
scoped: isScopedLabel(label),
}));
},
- projectPath() {
- const { referencePath = '' } = this.issue;
- return referencePath.slice(0, referencePath.indexOf('#'));
- },
},
methods: {
...mapActions(['setActiveIssueLabels']),
@@ -55,7 +51,7 @@ export default {
.filter(label => !payload.find(selected => selected.id === label.id))
.map(label => label.id);
- const input = { addLabelIds, removeLabelIds, projectPath: this.projectPath };
+ const input = { addLabelIds, removeLabelIds, projectPath: this.projectPathForActiveIssue };
await this.setActiveIssueLabels(input);
} catch (e) {
createFlash({ message: __('An error occurred while updating labels.') });
@@ -68,7 +64,7 @@ export default {
try {
const removeLabelIds = [getIdFromGraphQLId(id)];
- const input = { removeLabelIds, projectPath: this.projectPath };
+ const input = { removeLabelIds, projectPath: this.projectPathForActiveIssue };
await this.setActiveIssueLabels(input);
} catch (e) {
createFlash({ message: __('An error occurred when removing the label.') });
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
new file mode 100644
index 00000000000..ed069cea630
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
@@ -0,0 +1,71 @@
+<script>
+import { mapGetters, mapActions } from 'vuex';
+import { GlToggle } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { __, s__ } from '~/locale';
+
+export default {
+ i18n: {
+ header: {
+ title: __('Notifications'),
+ /* Any change to subscribeDisabledDescription
+ must be reflected in app/helpers/notifications_helper.rb */
+ subscribeDisabledDescription: __(
+ 'Notifications have been disabled by the project or group owner',
+ ),
+ },
+ updateSubscribedErrorMessage: s__(
+ 'IssueBoards|An error occurred while setting notifications status.',
+ ),
+ },
+ components: {
+ GlToggle,
+ },
+ data() {
+ return {
+ loading: false,
+ };
+ },
+ computed: {
+ ...mapGetters(['activeIssue', 'projectPathForActiveIssue']),
+ notificationText() {
+ return this.activeIssue.emailsDisabled
+ ? this.$options.i18n.header.subscribeDisabledDescription
+ : this.$options.i18n.header.title;
+ },
+ },
+ methods: {
+ ...mapActions(['setActiveIssueSubscribed']),
+ async handleToggleSubscription() {
+ this.loading = true;
+
+ try {
+ await this.setActiveIssueSubscribed({
+ subscribed: !this.activeIssue.subscribed,
+ projectPath: this.projectPathForActiveIssue,
+ });
+ } catch (error) {
+ createFlash({ message: this.$options.i18n.updateSubscribedErrorMessage });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="gl-display-flex gl-align-items-center gl-justify-content-space-between"
+ data-testid="sidebar-notifications"
+ >
+ <span data-testid="notification-header-text"> {{ notificationText }} </span>
+ <gl-toggle
+ v-if="!activeIssue.emailsDisabled"
+ :value="activeIssue.subscribed"
+ :is-loading="loading"
+ data-testid="notification-subscribe-toggle"
+ @change="handleToggleSubscription"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 2f64014a949..49cb560594c 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -18,7 +18,11 @@ export const inactiveId = 0;
export const ISSUABLE = 'issuable';
export const LIST = 'list';
+/* eslint-disable-next-line @gitlab/require-i18n-strings */
+export const DEFAULT_LABELS = ['to do', 'doing'];
+
export default {
BoardType,
ListType,
+ DEFAULT_LABELS,
};
diff --git a/app/assets/javascripts/boards/graphql/mutations/issue_set_subscription.mutation.graphql b/app/assets/javascripts/boards/graphql/mutations/issue_set_subscription.mutation.graphql
new file mode 100644
index 00000000000..1f383245ac2
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/mutations/issue_set_subscription.mutation.graphql
@@ -0,0 +1,8 @@
+mutation issueSetSubscription($input: IssueSetSubscriptionInput!) {
+ issueSetSubscription(input: $input) {
+ issue {
+ subscribed
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 887abe79059..d3e40299d8d 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapGetters, mapState } from 'vuex';
import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list';
@@ -86,10 +86,17 @@ export default () => {
boardId: $boardApp.dataset.boardId,
groupId: Number($boardApp.dataset.groupId),
rootPath: $boardApp.dataset.rootPath,
+ currentUserId: gon.current_user_id || null,
canUpdate: $boardApp.dataset.canUpdate,
labelsFetchPath: $boardApp.dataset.labelsFetchPath,
labelsManagePath: $boardApp.dataset.labelsManagePath,
labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath,
+ timeTrackingLimitToHours: parseBoolean($boardApp.dataset.timeTrackingLimitToHours),
+ weightFeatureAvailable: parseBoolean($boardApp.dataset.weightFeatureAvailable),
+ boardWeight: $boardApp.dataset.boardWeight
+ ? parseInt($boardApp.dataset.boardWeight, 10)
+ : null,
+ scopedLabelsAvailable: parseBoolean($boardApp.dataset.scopedLabels),
},
store,
apolloProvider,
@@ -108,6 +115,7 @@ export default () => {
},
computed: {
...mapState(['isShowingEpicsSwimlanes']),
+ ...mapGetters(['shouldUseGraphQL']),
detailIssueVisible() {
return Object.keys(this.detailIssue.issue).length;
},
@@ -153,7 +161,7 @@ export default () => {
boardsStore.disabled = this.disabled;
- if (!gon.features.graphqlBoardLists) {
+ if (!this.shouldUseGraphQL) {
this.initialBoardLoad();
}
},
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index fceb8c9d48e..f02c92e4230 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
@@ -1,17 +1,12 @@
/* global DocumentTouch */
-import $ from 'jquery';
import sortableConfig from 'ee_else_ce/sortable/sortable_config';
export function sortableStart() {
- $('.has-tooltip')
- .tooltip('hide')
- .tooltip('disable');
document.body.classList.add('is-dragging');
}
export function sortableEnd() {
- $('.has-tooltip').tooltip('enable');
document.body.classList.remove('is-dragging');
}
diff --git a/app/assets/javascripts/boards/queries/board_labels.query.graphql b/app/assets/javascripts/boards/queries/board_labels.query.graphql
new file mode 100644
index 00000000000..42a94419a97
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/board_labels.query.graphql
@@ -0,0 +1,23 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
+query BoardLabels(
+ $fullPath: ID!
+ $searchTerm: String
+ $isGroup: Boolean = false
+ $isProject: Boolean = false
+) {
+ group(fullPath: $fullPath) @include(if: $isGroup) {
+ labels(searchTerm: $searchTerm) {
+ nodes {
+ ...Label
+ }
+ }
+ }
+ project(fullPath: $fullPath) @include(if: $isProject) {
+ labels(searchTerm: $searchTerm) {
+ nodes {
+ ...Label
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/board_list_destroy.mutation.graphql b/app/assets/javascripts/boards/queries/board_list_destroy.mutation.graphql
new file mode 100644
index 00000000000..ef3fd36e980
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/board_list_destroy.mutation.graphql
@@ -0,0 +1,5 @@
+mutation DestroyBoardList($listId: ID!) {
+ destroyBoardList(input: { listId: $listId }) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/issue_create.mutation.graphql b/app/assets/javascripts/boards/queries/issue_create.mutation.graphql
new file mode 100644
index 00000000000..65be147be07
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/issue_create.mutation.graphql
@@ -0,0 +1,10 @@
+#import "ee_else_ce/boards/queries/issue.fragment.graphql"
+
+mutation CreateIssue($input: CreateIssueInput!) {
+ createIssue(input: $input) {
+ issue {
+ ...IssueNode
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/issue_set_due_date.mutation.graphql b/app/assets/javascripts/boards/queries/issue_set_due_date.mutation.graphql
new file mode 100644
index 00000000000..bbea248cf85
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/issue_set_due_date.mutation.graphql
@@ -0,0 +1,8 @@
+mutation issueSetDueDate($input: UpdateIssueInput!) {
+ updateIssue(input: $input) {
+ issue {
+ dueDate
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/users_search.query.graphql b/app/assets/javascripts/boards/queries/users_search.query.graphql
new file mode 100644
index 00000000000..ca016495d79
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/users_search.query.graphql
@@ -0,0 +1,11 @@
+query usersSearch($search: String!) {
+ users(search: $search) {
+ nodes {
+ username
+ name
+ webUrl
+ avatarUrl
+ id
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 1fed1228106..dd950a45076 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,26 +1,30 @@
-import Cookies from 'js-cookie';
import { pick } from 'lodash';
import boardListsQuery from 'ee_else_ce/boards/queries/board_lists.query.graphql';
-import { __ } from '~/locale';
-import { parseBoolean } from '~/lib/utils/common_utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { BoardType, ListType, inactiveId } from '~/boards/constants';
+import { BoardType, ListType, inactiveId, DEFAULT_LABELS } from '~/boards/constants';
import * as types from './mutation_types';
import {
formatBoardLists,
formatListIssues,
fullBoardId,
formatListsPageInfo,
+ formatIssue,
} from '../boards_util';
import boardStore from '~/boards/stores/boards_store';
+import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql';
import listsIssuesQuery from '../queries/lists_issues.query.graphql';
+import boardLabelsQuery from '../queries/board_labels.query.graphql';
import createBoardListMutation from '../queries/board_list_create.mutation.graphql';
import updateBoardListMutation from '../queries/board_list_update.mutation.graphql';
import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql';
+import destroyBoardListMutation from '../queries/board_list_destroy.mutation.graphql';
+import issueCreateMutation from '../queries/issue_create.mutation.graphql';
import issueSetLabels from '../queries/issue_set_labels.mutation.graphql';
+import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql';
+import issueSetSubscriptionMutation from '../graphql/mutations/issue_set_subscription.mutation.graphql';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
@@ -83,7 +87,7 @@ export default {
if (!lists.nodes.find(l => l.listType === ListType.backlog) && !hideBacklogList) {
dispatch('createList', { backlog: true });
}
- dispatch('showWelcomeList');
+ dispatch('generateDefaultLists');
})
.catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE));
},
@@ -121,7 +125,32 @@ export default {
);
},
- showWelcomeList: ({ state, dispatch }) => {
+ showPromotionList: () => {},
+
+ fetchLabels: ({ state, commit }, searchTerm) => {
+ const { endpoints, boardType } = state;
+ const { fullPath } = endpoints;
+
+ const variables = {
+ fullPath,
+ searchTerm,
+ isGroup: boardType === BoardType.group,
+ isProject: boardType === BoardType.project,
+ };
+
+ return gqlClient
+ .query({
+ query: boardLabelsQuery,
+ variables,
+ })
+ .then(({ data }) => {
+ const labels = data[boardType]?.labels;
+ return labels.nodes;
+ })
+ .catch(() => commit(types.RECEIVE_LABELS_FAILURE));
+ },
+
+ generateDefaultLists: async ({ state, commit, dispatch }) => {
if (state.disabled) {
return;
}
@@ -132,22 +161,18 @@ export default {
) {
return;
}
- if (parseBoolean(Cookies.get('issue_board_welcome_hidden'))) {
- return;
- }
- dispatch('addList', {
- id: 'blank',
- listType: ListType.blank,
- title: __('Welcome to your issue board!'),
- position: 0,
- });
- },
-
- showPromotionList: () => {},
+ const fetchLabelsAndCreateList = label => {
+ return dispatch('fetchLabels', label)
+ .then(res => {
+ if (res.length > 0) {
+ dispatch('createList', { labelId: res[0].id });
+ }
+ })
+ .catch(() => commit(types.GENERATE_DEFAULT_LISTS_FAILURE));
+ };
- generateDefaultLists: () => {
- notImplemented();
+ await Promise.all(DEFAULT_LABELS.map(fetchLabelsAndCreateList));
},
moveList: (
@@ -191,8 +216,26 @@ export default {
});
},
- deleteList: () => {
- notImplemented();
+ removeList: ({ state, commit }, listId) => {
+ const listsBackup = { ...state.boardLists };
+
+ commit(types.REMOVE_LIST, listId);
+
+ return gqlClient
+ .mutate({
+ mutation: destroyBoardListMutation,
+ variables: {
+ listId,
+ },
+ })
+ .then(({ data: { destroyBoardList: { errors } } }) => {
+ if (errors.length > 0) {
+ commit(types.REMOVE_LIST_FAILURE, listsBackup);
+ }
+ })
+ .catch(() => {
+ commit(types.REMOVE_LIST_FAILURE, listsBackup);
+ });
},
fetchIssuesForList: ({ state, commit }, { listId, fetchNext = false }) => {
@@ -271,20 +314,69 @@ export default {
);
},
- createNewIssue: () => {
- notImplemented();
+ setAssignees: ({ commit, getters }, assigneeUsernames) => {
+ return gqlClient
+ .mutate({
+ mutation: updateAssignees,
+ variables: {
+ iid: getters.activeIssue.iid,
+ projectPath: getters.activeIssue.referencePath.split('#')[0],
+ assigneeUsernames,
+ },
+ })
+ .then(({ data }) => {
+ commit('UPDATE_ISSUE_BY_ID', {
+ issueId: getters.activeIssue.id,
+ prop: 'assignees',
+ value: data.issueSetAssignees.issue.assignees.nodes,
+ });
+ });
+ },
+
+ createNewIssue: ({ commit, state }, issueInput) => {
+ const input = issueInput;
+ const { boardType, endpoints } = state;
+ if (boardType === BoardType.project) {
+ input.projectPath = endpoints.fullPath;
+ }
+
+ return gqlClient
+ .mutate({
+ mutation: issueCreateMutation,
+ variables: { input },
+ })
+ .then(({ data }) => {
+ if (data.createIssue.errors.length) {
+ commit(types.CREATE_ISSUE_FAILURE);
+ } else {
+ return data.createIssue?.issue;
+ }
+ return null;
+ })
+ .catch(() => commit(types.CREATE_ISSUE_FAILURE));
},
addListIssue: ({ commit }, { list, issue, position }) => {
commit(types.ADD_ISSUE_TO_LIST, { list, issue, position });
},
- addListIssueFailure: ({ commit }, { list, issue }) => {
- commit(types.ADD_ISSUE_TO_LIST_FAILURE, { list, issue });
+ addListNewIssue: ({ commit, dispatch }, { issueInput, list }) => {
+ const issue = formatIssue({ ...issueInput, id: 'tmp' });
+ commit(types.ADD_ISSUE_TO_LIST, { list, issue, position: 0 });
+
+ dispatch('createNewIssue', issueInput)
+ .then(res => {
+ commit(types.ADD_ISSUE_TO_LIST, {
+ list,
+ issue: formatIssue({ ...res, id: getIdFromGraphQLId(res.id) }),
+ });
+ commit(types.REMOVE_ISSUE_FROM_LIST, { list, issue });
+ })
+ .catch(() => commit(types.ADD_ISSUE_TO_LIST_FAILURE, { list, issueId: issueInput.id }));
},
setActiveIssueLabels: async ({ commit, getters }, input) => {
- const activeIssue = getters.getActiveIssue;
+ const { activeIssue } = getters;
const { data } = await gqlClient.mutate({
mutation: issueSetLabels,
variables: {
@@ -308,6 +400,53 @@ export default {
});
},
+ setActiveIssueDueDate: async ({ commit, getters }, input) => {
+ const { activeIssue } = getters;
+ const { data } = await gqlClient.mutate({
+ mutation: issueSetDueDate,
+ variables: {
+ input: {
+ iid: String(activeIssue.iid),
+ projectPath: input.projectPath,
+ dueDate: input.dueDate,
+ },
+ },
+ });
+
+ if (data.updateIssue?.errors?.length > 0) {
+ throw new Error(data.updateIssue.errors);
+ }
+
+ commit(types.UPDATE_ISSUE_BY_ID, {
+ issueId: activeIssue.id,
+ prop: 'dueDate',
+ value: data.updateIssue.issue.dueDate,
+ });
+ },
+
+ setActiveIssueSubscribed: async ({ commit, getters }, input) => {
+ const { data } = await gqlClient.mutate({
+ mutation: issueSetSubscriptionMutation,
+ variables: {
+ input: {
+ iid: String(getters.activeIssue.iid),
+ projectPath: input.projectPath,
+ subscribedState: input.subscribed,
+ },
+ },
+ });
+
+ if (data.issueSetSubscription?.errors?.length > 0) {
+ throw new Error(data.issueSetSubscription.errors);
+ }
+
+ commit(types.UPDATE_ISSUE_BY_ID, {
+ issueId: getters.activeIssue.id,
+ prop: 'subscribed',
+ value: data.issueSetSubscription.issue.subscribed,
+ });
+ },
+
fetchBacklog: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index d1a5db1bcc5..337b2897fe9 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -1,7 +1,6 @@
/* eslint-disable no-shadow, no-param-reassign,consistent-return */
/* global List */
/* global ListIssue */
-import $ from 'jquery';
import { sortBy, pick } from 'lodash';
import Vue from 'vue';
import Cookies from 'js-cookie';
@@ -119,8 +118,12 @@ const boardsStore = {
// https://gitlab.com/gitlab-org/gitlab-foss/issues/30821
});
},
+
updateNewListDropdown(listId) {
- $(`.js-board-list-${listId}`).removeClass('is-active');
+ // eslint-disable-next-line no-unused-expressions
+ document
+ .querySelector(`.js-board-list-${getIdFromGraphQLId(listId)}`)
+ ?.classList.remove('is-active');
},
shouldAddBlankState() {
// Decide whether to add the blank state
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index 89a3b14b262..cd28b4a0ff7 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -2,7 +2,7 @@ import { find } from 'lodash';
import { inactiveId } from '../constants';
export default {
- getLabelToggleState: state => (state.isShowingLabels ? 'on' : 'off'),
+ labelToggleState: state => (state.isShowingLabels ? 'on' : 'off'),
isSidebarOpen: state => state.activeId !== inactiveId,
isSwimlanesOn: state => {
if (!gon?.features?.boardsWithSwimlanes && !gon?.features?.swimlanes) {
@@ -15,15 +15,20 @@ export default {
return state.issues[id] || {};
},
- getIssues: (state, getters) => listId => {
+ getIssuesByList: (state, getters) => listId => {
const listIssueIds = state.issuesByListId[listId] || [];
return listIssueIds.map(id => getters.getIssueById(id));
},
- getActiveIssue: state => {
+ activeIssue: state => {
return state.issues[state.activeId] || {};
},
+ projectPathForActiveIssue: (_, getters) => {
+ const referencePath = getters.activeIssue.referencePath || '';
+ return referencePath.slice(0, referencePath.indexOf('#'));
+ },
+
getListByLabelId: state => labelId => {
return find(state.boardLists, l => l.label?.id === labelId);
},
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 09ab08062df..3a57cb9b5e1 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -2,6 +2,8 @@ export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';
export const SET_FILTERS = 'SET_FILTERS';
export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS';
export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE';
+export const RECEIVE_LABELS_FAILURE = 'RECEIVE_LABELS_FAILURE';
+export const GENERATE_DEFAULT_LISTS_FAILURE = 'GENERATE_DEFAULT_LISTS_FAILURE';
export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS';
export const RECEIVE_BOARD_LISTS_FAILURE = 'RECEIVE_BOARD_LISTS_FAILURE';
export const SHOW_PROMOTION_LIST = 'SHOW_PROMOTION_LIST';
@@ -10,12 +12,12 @@ export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS';
export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR';
export const MOVE_LIST = 'MOVE_LIST';
export const UPDATE_LIST_FAILURE = 'UPDATE_LIST_FAILURE';
-export const REQUEST_REMOVE_LIST = 'REQUEST_REMOVE_LIST';
-export const RECEIVE_REMOVE_LIST_SUCCESS = 'RECEIVE_REMOVE_LIST_SUCCESS';
-export const RECEIVE_REMOVE_LIST_ERROR = 'RECEIVE_REMOVE_LIST_ERROR';
+export const REMOVE_LIST = 'REMOVE_LIST';
+export const REMOVE_LIST_FAILURE = 'REMOVE_LIST_FAILURE';
export const REQUEST_ISSUES_FOR_LIST = 'REQUEST_ISSUES_FOR_LIST';
export const RECEIVE_ISSUES_FOR_LIST_FAILURE = 'RECEIVE_ISSUES_FOR_LIST_FAILURE';
export const RECEIVE_ISSUES_FOR_LIST_SUCCESS = 'RECEIVE_ISSUES_FOR_LIST_SUCCESS';
+export const CREATE_ISSUE_FAILURE = 'CREATE_ISSUE_FAILURE';
export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE';
export const RECEIVE_ADD_ISSUE_SUCCESS = 'RECEIVE_ADD_ISSUE_SUCCESS';
export const RECEIVE_ADD_ISSUE_ERROR = 'RECEIVE_ADD_ISSUE_ERROR';
@@ -27,6 +29,7 @@ export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS';
export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR';
export const ADD_ISSUE_TO_LIST = 'ADD_ISSUE_TO_LIST';
export const ADD_ISSUE_TO_LIST_FAILURE = 'ADD_ISSUE_TO_LIST_FAILURE';
+export const REMOVE_ISSUE_FROM_LIST = 'REMOVE_ISSUE_FROM_LIST';
export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 0c7dbc0d2ef..bb083158c8f 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -62,6 +62,14 @@ export default {
state.error = s__('Boards|An error occurred while creating the list. Please try again.');
},
+ [mutationTypes.RECEIVE_LABELS_FAILURE]: state => {
+ state.error = s__('Boards|An error occurred while fetching labels. Please reload the page.');
+ },
+
+ [mutationTypes.GENERATE_DEFAULT_LISTS_FAILURE]: state => {
+ state.error = s__('Boards|An error occurred while generating lists. Please reload the page.');
+ },
+
[mutationTypes.REQUEST_ADD_LIST]: () => {
notImplemented();
},
@@ -85,16 +93,13 @@ export default {
Vue.set(state, 'boardLists', backupList);
},
- [mutationTypes.REQUEST_REMOVE_LIST]: () => {
- notImplemented();
+ [mutationTypes.REMOVE_LIST]: (state, listId) => {
+ Vue.delete(state.boardLists, listId);
},
- [mutationTypes.RECEIVE_REMOVE_LIST_SUCCESS]: () => {
- notImplemented();
- },
-
- [mutationTypes.RECEIVE_REMOVE_LIST_ERROR]: () => {
- notImplemented();
+ [mutationTypes.REMOVE_LIST_FAILURE](state, listsBackup) {
+ state.error = s__('Boards|An error occurred while removing the list. Please try again.');
+ state.boardLists = listsBackup;
},
[mutationTypes.REQUEST_ISSUES_FOR_LIST]: (state, { listId, fetchNext }) => {
@@ -196,16 +201,28 @@ export default {
notImplemented();
},
+ [mutationTypes.CREATE_ISSUE_FAILURE]: state => {
+ state.error = s__('Boards|An error occurred while creating the issue. Please try again.');
+ },
+
[mutationTypes.ADD_ISSUE_TO_LIST]: (state, { list, issue, position }) => {
- const listIssues = state.issuesByListId[list.id];
- listIssues.splice(position, 0, issue.id);
- Vue.set(state.issuesByListId, list.id, listIssues);
+ addIssueToList({
+ state,
+ listId: list.id,
+ issueId: issue.id,
+ atIndex: position,
+ });
Vue.set(state.issues, issue.id, issue);
},
- [mutationTypes.ADD_ISSUE_TO_LIST_FAILURE]: (state, { list, issue }) => {
+ [mutationTypes.ADD_ISSUE_TO_LIST_FAILURE]: (state, { list, issueId }) => {
state.error = s__('Boards|An error occurred while creating the issue. Please try again.');
+ removeIssueFromList({ state, listId: list.id, issueId });
+ },
+
+ [mutationTypes.REMOVE_ISSUE_FROM_LIST]: (state, { list, issue }) => {
removeIssueFromList({ state, listId: list.id, issueId: issue.id });
+ Vue.delete(state.issues, issue.id);
},
[mutationTypes.SET_CURRENT_PAGE]: () => {
diff --git a/app/assets/javascripts/boards/toggle_focus.js b/app/assets/javascripts/boards/toggle_focus.js
index fa13d3a9e3c..347deb81846 100644
--- a/app/assets/javascripts/boards/toggle_focus.js
+++ b/app/assets/javascripts/boards/toggle_focus.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import Vue from 'vue';
import { GlIcon } from '@gitlab/ui';
+import { hide } from '~/tooltips';
export default (ModalStore, boardsStore) => {
const issueBoardsContent = document.querySelector('.content-wrapper > .js-focus-mode-board');
@@ -17,7 +18,9 @@ export default (ModalStore, boardsStore) => {
},
methods: {
toggleFocusMode() {
- $(this.$refs.toggleFocusModeButton).tooltip('hide');
+ const $el = $(this.$refs.toggleFocusModeButton);
+ hide($el);
+
issueBoardsContent.classList.toggle('is-focused');
this.isFullscreen = !this.isFullscreen;