Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-19 11:27:35 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-19 11:27:35 +0300
commit7e9c479f7de77702622631cff2628a9c8dcbc627 (patch)
treec8f718a08e110ad7e1894510980d2155a6549197 /app/assets/javascripts/boards/components
parente852b0ae16db4052c1c567d9efa4facc81146e88 (diff)
Add latest changes from gitlab-org/gitlab@13-6-stable-eev13.6.0-rc42
Diffstat (limited to 'app/assets/javascripts/boards/components')
-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
23 files changed, 1065 insertions, 151 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>