summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/boards
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/boards')
-rw-r--r--app/assets/javascripts/boards/boards_util.js33
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column.vue56
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_form.vue119
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_trigger.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_blocked_icon.vue192
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue28
-rw-r--r--app/assets/javascripts/boards/components/board_card_loading_skeleton.vue26
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue29
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue96
-rw-r--r--app/assets/javascripts/boards/components/board_extra_actions.vue57
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue33
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list_deprecated.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue15
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue_deprecated.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue8
-rw-r--r--app/assets/javascripts/boards/components/config_toggle.vue12
-rw-r--r--app/assets/javascripts/boards/components/filtered_search.vue54
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.vue84
-rw-r--r--app/assets/javascripts/boards/components/modal/filters.js27
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.vue80
-rw-r--r--app/assets/javascripts/boards/components/modal/header.vue80
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue151
-rw-r--r--app/assets/javascripts/boards/components/modal/list.vue141
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.vue49
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.vue42
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_editable_item.vue4
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue7
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue12
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue19
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue18
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue25
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue (renamed from app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue)48
-rw-r--r--app/assets/javascripts/boards/components/toggle_focus.vue4
-rw-r--r--app/assets/javascripts/boards/config_toggle.js5
-rw-r--r--app/assets/javascripts/boards/constants.js29
-rw-r--r--app/assets/javascripts/boards/ee_functions.js1
-rw-r--r--app/assets/javascripts/boards/filtered_search.js25
-rw-r--r--app/assets/javascripts/boards/graphql/board_blocking_issues.query.graphql16
-rw-r--r--app/assets/javascripts/boards/graphql/issue.fragment.graphql4
-rw-r--r--app/assets/javascripts/boards/graphql/issue_set_subscription.mutation.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql2
-rw-r--r--app/assets/javascripts/boards/index.js71
-rw-r--r--app/assets/javascripts/boards/mixins/board_new_issue.js6
-rw-r--r--app/assets/javascripts/boards/mixins/modal_footer.js1
-rw-r--r--app/assets/javascripts/boards/mixins/modal_mixins.js12
-rw-r--r--app/assets/javascripts/boards/stores/actions.js306
-rw-r--r--app/assets/javascripts/boards/stores/getters.js12
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js95
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js15
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js70
52 files changed, 925 insertions, 1312 deletions
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index 2cd25f58770..a8b870f9b8e 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,4 +1,4 @@
-import { sortBy } from 'lodash';
+import { sortBy, cloneDeep } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { ListType, NOT_FILTER } from './constants';
@@ -113,6 +113,37 @@ export function formatIssueInput(issueInput, boardConfig) {
};
}
+export function shouldCloneCard(fromListType, toListType) {
+ const involvesClosed = fromListType === ListType.closed || toListType === ListType.closed;
+ const involvesBacklog = fromListType === ListType.backlog || toListType === ListType.backlog;
+
+ if (involvesClosed || involvesBacklog) {
+ return false;
+ }
+
+ if (fromListType !== toListType) {
+ return true;
+ }
+
+ return false;
+}
+
+export function getMoveData(state, params) {
+ const { boardItems, boardItemsByListId, boardLists } = state;
+ const { itemId, fromListId, toListId } = params;
+ const fromListType = boardLists[fromListId].listType;
+ const toListType = boardLists[toListId].listType;
+
+ return {
+ reordering: fromListId === toListId,
+ shouldClone: shouldCloneCard(fromListType, toListType),
+ itemNotInToList: !boardItemsByListId[toListId].includes(itemId),
+ originalIssue: cloneDeep(boardItems[itemId]),
+ originalIndex: boardItemsByListId[fromListId].indexOf(itemId),
+ ...params,
+ };
+}
+
export function moveItemListHelper(item, fromList, toList) {
const updatedItem = item;
if (
diff --git a/app/assets/javascripts/boards/components/board_add_new_column.vue b/app/assets/javascripts/boards/components/board_add_new_column.vue
index 3c7c792b787..d4b559add6e 100644
--- a/app/assets/javascripts/boards/components/board_add_new_column.vue
+++ b/app/assets/javascripts/boards/components/board_add_new_column.vue
@@ -1,23 +1,16 @@
<script>
-import {
- GlFormRadio,
- GlFormRadioGroup,
- GlLabel,
- GlTooltipDirective as GlTooltip,
-} from '@gitlab/ui';
+import { GlFormRadio, GlFormRadioGroup, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
import { ListType } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { isScopedLabel } from '~/lib/utils/common_utils';
export default {
components: {
BoardAddNewColumnForm,
GlFormRadio,
GlFormRadioGroup,
- GlLabel,
},
directives: {
GlTooltip,
@@ -26,17 +19,12 @@ export default {
data() {
return {
selectedId: null,
+ selectedLabel: null,
};
},
computed: {
...mapState(['labels', 'labelsLoading']),
...mapGetters(['getListByLabelId', 'shouldUseGraphQL']),
- selectedLabel() {
- if (!this.selectedId) {
- return null;
- }
- return this.labels.find(({ id }) => id === this.selectedId);
- },
columnForSelected() {
return this.getListByLabelId(this.selectedId);
},
@@ -89,8 +77,13 @@ export default {
this.fetchLabels(searchTerm);
},
- showScopedLabels(label) {
- return this.scopedLabelsAvailable && isScopedLabel(label);
+ setSelectedItem(selectedId) {
+ const label = this.labels.find(({ id }) => id === selectedId);
+ if (!selectedId || !label) {
+ this.selectedLabel = null;
+ } else {
+ this.selectedLabel = { ...label };
+ }
},
},
};
@@ -99,38 +92,39 @@ export default {
<template>
<board-add-new-column-form
:loading="labelsLoading"
- :form-description="__('A label list displays issues with the selected label.')"
- :search-label="__('Select label')"
+ :none-selected="__('Select a label')"
:search-placeholder="__('Search labels')"
:selected-id="selectedId"
@filter-items="filterItems"
@add-list="addList"
>
- <template slot="selected">
- <gl-label
- v-if="selectedLabel"
- v-gl-tooltip
- :title="selectedLabel.title"
- :description="selectedLabel.description"
- :background-color="selectedLabel.color"
- :scoped="showScopedLabels(selectedLabel)"
- />
+ <template #selected>
+ <template v-if="selectedLabel">
+ <span
+ class="dropdown-label-box gl-top-0 gl-flex-shrink-0"
+ :style="{
+ backgroundColor: selectedLabel.color,
+ }"
+ ></span>
+ <div class="gl-text-truncate">{{ selectedLabel.title }}</div>
+ </template>
</template>
- <template slot="items">
+ <template #items>
<gl-form-radio-group
v-if="labels.length > 0"
v-model="selectedId"
class="gl-overflow-y-auto gl-px-5 gl-pt-3"
+ @change="setSelectedItem"
>
<label
v-for="label in labels"
:key="label.id"
- class="gl-display-flex gl-flex-align-items-center gl-mb-5 gl-font-weight-normal"
+ class="gl-display-flex gl-mb-5 gl-font-weight-normal gl-overflow-break-word"
>
- <gl-form-radio :value="label.id" class="gl-mb-0" />
+ <gl-form-radio :value="label.id" />
<span
- class="dropdown-label-box gl-top-0"
+ class="dropdown-label-box gl-top-0 gl-flex-shrink-0"
:style="{
backgroundColor: label.color,
}"
diff --git a/app/assets/javascripts/boards/components/board_add_new_column_form.vue b/app/assets/javascripts/boards/components/board_add_new_column_form.vue
index d85343a5390..70ba90bb1d4 100644
--- a/app/assets/javascripts/boards/components/board_add_new_column_form.vue
+++ b/app/assets/javascripts/boards/components/board_add_new_column_form.vue
@@ -1,5 +1,12 @@
<script>
-import { GlButton, GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
+import {
+ GlButton,
+ GlDropdown,
+ GlFormGroup,
+ GlIcon,
+ GlSearchBoxByType,
+ GlSkeletonLoader,
+} from '@gitlab/ui';
import { mapActions } from 'vuex';
import { __ } from '~/locale';
@@ -8,13 +15,16 @@ export default {
add: __('Add to board'),
cancel: __('Cancel'),
newList: __('New list'),
- noneSelected: __('None'),
noResults: __('No matching results'),
+ scope: __('Scope'),
+ scopeDescription: __('Issues must match this scope to appear in this list.'),
selected: __('Selected'),
},
components: {
GlButton,
+ GlDropdown,
GlFormGroup,
+ GlIcon,
GlSearchBoxByType,
GlSkeletonLoader,
},
@@ -23,11 +33,12 @@ export default {
type: Boolean,
required: true,
},
- formDescription: {
+ searchLabel: {
type: String,
- required: true,
+ required: false,
+ default: null,
},
- searchLabel: {
+ noneSelected: {
type: String,
required: true,
},
@@ -46,8 +57,23 @@ export default {
searchValue: '',
};
},
+ watch: {
+ selectedId(val) {
+ if (val) {
+ this.$refs.dropdown.hide(true);
+ }
+ },
+ },
methods: {
...mapActions(['setAddColumnFormVisibility']),
+ setFocus() {
+ this.$refs.searchBox.focusInput();
+ },
+ onHide() {
+ this.searchValue = '';
+ this.$emit('filter-items', '');
+ this.$emit('hide');
+ },
},
};
</script>
@@ -62,51 +88,64 @@ export default {
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-white"
>
<h3
- class="gl-font-base gl-px-5 gl-py-5 gl-m-0 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
+ class="gl-font-size-h2 gl-px-5 gl-py-4 gl-m-0 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
data-testid="board-add-column-form-title"
>
{{ $options.i18n.newList }}
</h3>
- <div class="gl-display-flex gl-flex-direction-column gl-h-full gl-overflow-hidden">
- <slot name="select-list-type">
- <div class="gl-mb-5"></div>
- </slot>
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-h-full gl-overflow-y-auto gl-align-items-flex-start"
+ >
+ <div class="gl-px-5">
+ <h3 class="gl-font-lg gl-mt-5 gl-mb-2">
+ {{ $options.i18n.scope }}
+ </h3>
+ <p class="gl-mb-3">{{ $options.i18n.scopeDescription }}</p>
+ </div>
- <p class="gl-px-5">{{ formDescription }}</p>
+ <slot name="select-list-type"></slot>
- <div class="gl-px-5 gl-pb-4">
- <label class="gl-mb-2">{{ $options.i18n.selected }}</label>
- <slot name="selected">
- <div class="gl-text-gray-500">{{ $options.i18n.noneSelected }}</div>
- </slot>
- </div>
+ <gl-form-group class="gl-px-5 lg-mb-3 gl-max-w-full" :label="searchLabel">
+ <gl-dropdown
+ ref="dropdown"
+ class="gl-mb-3 gl-max-w-full"
+ toggle-class="gl-max-w-full gl-display-flex gl-align-items-center gl-text-trunate"
+ boundary="viewport"
+ @shown="setFocus"
+ @hide="onHide"
+ >
+ <template #button-content>
+ <slot name="selected">
+ <div>{{ noneSelected }}</div>
+ </slot>
+ <gl-icon class="dropdown-chevron gl-flex-shrink-0" name="chevron-down" />
+ </template>
- <gl-form-group
- class="gl-mx-5 gl-mb-3"
- :label="searchLabel"
- label-for="board-available-column-entities"
- >
- <gl-search-box-by-type
- id="board-available-column-entities"
- v-model="searchValue"
- debounce="250"
- :placeholder="searchPlaceholder"
- @input="$emit('filter-items', $event)"
- />
- </gl-form-group>
+ <template #header>
+ <gl-search-box-by-type
+ ref="searchBox"
+ v-model="searchValue"
+ debounce="250"
+ class="gl-mt-0!"
+ :placeholder="searchPlaceholder"
+ @input="$emit('filter-items', $event)"
+ />
+ </template>
- <div v-if="loading" class="gl-px-5">
- <gl-skeleton-loader :width="500" :height="172">
- <rect width="480" height="20" x="10" y="15" rx="4" />
- <rect width="380" height="20" x="10" y="50" rx="4" />
- <rect width="430" height="20" x="10" y="85" rx="4" />
- </gl-skeleton-loader>
- </div>
+ <div v-if="loading" class="gl-px-5">
+ <gl-skeleton-loader :width="400" :height="172">
+ <rect width="380" height="20" x="10" y="15" rx="4" />
+ <rect width="280" height="20" x="10" y="50" rx="4" />
+ <rect width="330" height="20" x="10" y="85" rx="4" />
+ </gl-skeleton-loader>
+ </div>
- <slot v-else name="items">
- <p class="gl-mx-5">{{ $options.i18n.noResults }}</p>
- </slot>
+ <slot v-else name="items">
+ <p class="gl-mx-5">{{ $options.i18n.noResults }}</p>
+ </slot>
+ </gl-dropdown>
+ </gl-form-group>
</div>
<div
class="gl-display-flex gl-p-3 gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
diff --git a/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
index 7c08e33be7e..85f001d9d61 100644
--- a/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
+++ b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
@@ -13,9 +13,9 @@ export default {
</script>
<template>
- <span class="gl-ml-3 gl-display-flex gl-align-items-center" data-testid="boards-create-list">
+ <div class="gl-ml-3 gl-display-flex gl-align-items-center" data-testid="boards-create-list">
<gl-button variant="confirm" @click="setAddColumnFormVisibility(true)"
>{{ __('Create list') }}
</gl-button>
- </span>
+ </div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_blocked_icon.vue b/app/assets/javascripts/boards/components/board_blocked_icon.vue
new file mode 100644
index 00000000000..0f92e714752
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_blocked_icon.vue
@@ -0,0 +1,192 @@
+<script>
+import { GlIcon, GlLink, GlPopover, GlLoadingIcon } from '@gitlab/ui';
+import { blockingIssuablesQueries, issuableTypes } from '~/boards/constants';
+import { IssueType } from '~/graphql_shared/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { truncate } from '~/lib/utils/text_utility';
+import { __, n__, s__, sprintf } from '~/locale';
+
+export default {
+ i18n: {
+ issuableType: {
+ [issuableTypes.issue]: __('issue'),
+ },
+ },
+ graphQLIdType: {
+ [issuableTypes.issue]: IssueType,
+ },
+ referenceFormatter: {
+ [issuableTypes.issue]: (r) => r.split('/')[1],
+ },
+ defaultDisplayLimit: 3,
+ textTruncateWidth: 80,
+ components: {
+ GlIcon,
+ GlPopover,
+ GlLink,
+ GlLoadingIcon,
+ },
+ blockingIssuablesQueries,
+ props: {
+ item: {
+ type: Object,
+ required: true,
+ },
+ uniqueId: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: true,
+ validator(value) {
+ return [issuableTypes.issue].includes(value);
+ },
+ },
+ },
+ apollo: {
+ blockingIssuables: {
+ skip() {
+ return this.skip;
+ },
+ query() {
+ return blockingIssuablesQueries[this.issuableType].query;
+ },
+ variables() {
+ return {
+ id: convertToGraphQLId(this.$options.graphQLIdType[this.issuableType], this.item.id),
+ };
+ },
+ update(data) {
+ this.skip = true;
+
+ return data?.issuable?.blockingIssuables?.nodes || [];
+ },
+ error(error) {
+ const message = sprintf(s__('Boards|Failed to fetch blocking %{issuableType}s'), {
+ issuableType: this.issuableTypeText,
+ });
+ this.$emit('blocking-issuables-error', { error, message });
+ },
+ },
+ },
+ data() {
+ return {
+ skip: true,
+ blockingIssuables: [],
+ };
+ },
+ computed: {
+ displayedIssuables() {
+ const { defaultDisplayLimit, referenceFormatter } = this.$options;
+ return this.blockingIssuables.slice(0, defaultDisplayLimit).map((i) => {
+ return {
+ ...i,
+ title: truncate(i.title, this.$options.textTruncateWidth),
+ reference: referenceFormatter[this.issuableType](i.reference),
+ };
+ });
+ },
+ loading() {
+ return this.$apollo.queries.blockingIssuables.loading;
+ },
+ issuableTypeText() {
+ return this.$options.i18n.issuableType[this.issuableType];
+ },
+ blockedLabel() {
+ return sprintf(
+ n__(
+ 'Boards|Blocked by %{blockedByCount} %{issuableType}',
+ 'Boards|Blocked by %{blockedByCount} %{issuableType}s',
+ this.item.blockedByCount,
+ ),
+ {
+ blockedByCount: this.item.blockedByCount,
+ issuableType: this.issuableTypeText,
+ },
+ );
+ },
+ glIconId() {
+ return `blocked-icon-${this.uniqueId}`;
+ },
+ hasMoreIssuables() {
+ return this.item.blockedByCount > this.$options.defaultDisplayLimit;
+ },
+ displayedIssuablesCount() {
+ return this.hasMoreIssuables
+ ? this.item.blockedByCount - this.$options.defaultDisplayLimit
+ : this.item.blockedByCount;
+ },
+ moreIssuablesText() {
+ return sprintf(
+ n__(
+ 'Boards|+ %{displayedIssuablesCount} more %{issuableType}',
+ 'Boards|+ %{displayedIssuablesCount} more %{issuableType}s',
+ this.displayedIssuablesCount,
+ ),
+ {
+ displayedIssuablesCount: this.displayedIssuablesCount,
+ issuableType: this.issuableTypeText,
+ },
+ );
+ },
+ viewAllIssuablesText() {
+ return sprintf(s__('Boards|View all blocking %{issuableType}s'), {
+ issuableType: this.issuableTypeText,
+ });
+ },
+ loadingMessage() {
+ return sprintf(s__('Boards|Retrieving blocking %{issuableType}s'), {
+ issuableType: this.issuableTypeText,
+ });
+ },
+ },
+ methods: {
+ handleMouseEnter() {
+ this.skip = false;
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-display-inline">
+ <gl-icon
+ :id="glIconId"
+ ref="icon"
+ name="issue-block"
+ class="issue-blocked-icon gl-mr-2 gl-cursor-pointer"
+ data-testid="issue-blocked-icon"
+ @mouseenter="handleMouseEnter"
+ />
+ <gl-popover :target="glIconId" placement="top">
+ <template #title
+ ><span data-testid="popover-title">{{ blockedLabel }}</span></template
+ >
+ <template v-if="loading">
+ <gl-loading-icon />
+ <p class="gl-mt-4 gl-mb-0 gl-font-small">{{ loadingMessage }}</p>
+ </template>
+ <template v-else>
+ <ul class="gl-list-style-none gl-p-0">
+ <li v-for="issuable in displayedIssuables" :key="issuable.id">
+ <gl-link :href="issuable.webUrl" class="gl-text-blue-500! gl-font-sm">{{
+ issuable.reference
+ }}</gl-link>
+ <p class="gl-mb-3 gl-display-block!" data-testid="issuable-title">
+ {{ issuable.title }}
+ </p>
+ </li>
+ </ul>
+ <div v-if="hasMoreIssuables" class="gl-mt-4">
+ <p class="gl-mb-3" data-testid="hidden-blocking-count">{{ moreIssuablesText }}</p>
+ <gl-link
+ data-testid="view-all-issues"
+ :href="`${item.webUrl}#related-issues`"
+ class="gl-text-blue-500! gl-font-sm"
+ >{{ viewAllIssuablesText }}</gl-link
+ >
+ </div>
+ </template>
+ </gl-popover>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index d4d6b17a589..9ff2cdd76d0 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -10,6 +10,7 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import { ListType } from '../constants';
import eventHub from '../eventhub';
+import BoardBlockedIcon from './board_blocked_icon.vue';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
@@ -22,6 +23,7 @@ export default {
IssueDueDate,
IssueTimeEstimate,
IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
+ BoardBlockedIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -52,7 +54,7 @@ export default {
};
},
computed: {
- ...mapState(['isShowingLabels']),
+ ...mapState(['isShowingLabels', 'issuableType']),
...mapGetters(['isEpicBoard']),
cappedAssignees() {
// e.g. maxRender is 4,
@@ -114,7 +116,7 @@ export default {
},
},
methods: {
- ...mapActions(['performSearch']),
+ ...mapActions(['performSearch', 'setError']),
isIndexLessThanlimit(index) {
return index < this.limitBeforeCounter;
},
@@ -164,14 +166,12 @@ export default {
<div>
<div class="gl-display-flex" dir="auto">
<h4 class="board-card-title gl-mb-0 gl-mt-0">
- <gl-icon
+ <board-blocked-icon
v-if="item.blocked"
- v-gl-tooltip
- name="issue-block"
- :title="blockedLabel"
- class="issue-blocked-icon gl-mr-2"
- :aria-label="blockedLabel"
- data-testid="issue-blocked-icon"
+ :item="item"
+ :unique-id="`${item.id}${list.id}`"
+ :issuable-type="issuableType"
+ @blocking-issuables-error="setError"
/>
<gl-icon
v-if="item.confidential"
@@ -181,13 +181,9 @@ export default {
class="confidential-icon gl-mr-2"
:aria-label="__('Confidential')"
/>
- <a
- :href="item.path || item.webUrl || ''"
- :title="item.title"
- class="js-no-trigger"
- @mousemove.stop
- >{{ item.title }}</a
- >
+ <a :href="item.path || item.webUrl || ''" :title="item.title" @mousemove.stop>{{
+ item.title
+ }}</a>
</h4>
</div>
<div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap">
diff --git a/app/assets/javascripts/boards/components/board_card_loading_skeleton.vue b/app/assets/javascripts/boards/components/board_card_loading_skeleton.vue
new file mode 100644
index 00000000000..15bff1226a6
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card_loading_skeleton.vue
@@ -0,0 +1,26 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ name: 'BoardCardLoading',
+ components: {
+ GlSkeletonLoader,
+ },
+};
+</script>
+
+<template>
+ <div
+ class="board-card-skeleton gl-mb-3 gl-bg-white gl-rounded-base gl-p-5 gl-border-1 gl-border-solid gl-border-gray-50"
+ >
+ <div class="board-card-skeleton-inner">
+ <gl-skeleton-loader :width="340" :height="100">
+ <rect width="340" height="16" rx="4" />
+ <rect y="30" width="118" height="16" rx="8" />
+ <rect x="122" y="30" width="130" height="16" rx="8" />
+ <rect y="62" width="38" height="16" rx="4" />
+ <circle cx="320" cy="68" r="16" />
+ </gl-skeleton-loader>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index e9c4237d759..a4b1e6adacf 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -17,21 +17,20 @@ export default {
gon.features?.graphqlBoardLists || gon.features?.epicBoards
? BoardColumn
: BoardColumnDeprecated,
- BoardContentSidebar: () => import('ee_component/boards/components/board_content_sidebar.vue'),
+ BoardContentSidebar: () => import('~/boards/components/board_content_sidebar.vue'),
+ EpicBoardContentSidebar: () =>
+ import('ee_component/boards/components/epic_board_content_sidebar.vue'),
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
GlAlert,
},
mixins: [glFeatureFlagMixin()],
+ inject: ['canAdminList'],
props: {
lists: {
type: Array,
required: false,
default: () => [],
},
- canAdminList: {
- type: Boolean,
- required: true,
- },
disabled: {
type: Boolean,
required: true,
@@ -69,7 +68,7 @@ export default {
},
},
methods: {
- ...mapActions(['moveList']),
+ ...mapActions(['moveList', 'unsetError']),
afterFormEnters() {
const el = this.canDragColumns ? this.$refs.list.$el : this.$refs.list;
el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' });
@@ -99,8 +98,8 @@ export default {
</script>
<template>
- <div>
- <gl-alert v-if="error" variant="danger" :dismissible="false">
+ <div v-cloak data-qa-selector="boards_list">
+ <gl-alert v-if="error" variant="danger" :dismissible="true" @dismiss="unsetError">
{{ error }}
</gl-alert>
<component
@@ -127,13 +126,23 @@ export default {
</component>
<epics-swimlanes
- v-else
+ v-else-if="boardListsToUse.length"
ref="swimlanes"
:lists="boardListsToUse"
:can-admin-list="canAdminList"
:disabled="disabled"
/>
- <board-content-sidebar v-if="isSwimlanesOn || glFeatures.graphqlBoardLists" />
+ <board-content-sidebar
+ v-if="isSwimlanesOn || glFeatures.graphqlBoardLists"
+ class="boards-sidebar"
+ data-testid="issue-boards-sidebar"
+ />
+
+ <epic-board-content-sidebar
+ v-else-if="isEpicBoard"
+ class="boards-sidebar"
+ data-testid="epic-boards-sidebar"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
new file mode 100644
index 00000000000..46359cc2bca
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -0,0 +1,96 @@
+<script>
+import { GlDrawer } from '@gitlab/ui';
+import { mapState, mapActions, mapGetters } from 'vuex';
+import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
+import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
+import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
+import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
+import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
+import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
+import { ISSUABLE } from '~/boards/constants';
+import { contentTop } from '~/lib/utils/common_utils';
+import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
+export default {
+ headerHeight: `${contentTop()}px`,
+ components: {
+ GlDrawer,
+ BoardSidebarTitle,
+ SidebarAssigneesWidget,
+ BoardSidebarTimeTracker,
+ BoardSidebarLabelsSelect,
+ BoardSidebarDueDate,
+ BoardSidebarSubscription,
+ BoardSidebarMilestoneSelect,
+ BoardSidebarEpicSelect: () =>
+ import('ee_component/boards/components/sidebar/board_sidebar_epic_select.vue'),
+ BoardSidebarWeightInput: () =>
+ import('ee_component/boards/components/sidebar/board_sidebar_weight_input.vue'),
+ SidebarIterationWidget: () =>
+ import('ee_component/sidebar/components/sidebar_iteration_widget.vue'),
+ },
+ mixins: [glFeatureFlagsMixin()],
+ computed: {
+ ...mapGetters([
+ 'isSidebarOpen',
+ 'activeBoardItem',
+ 'groupPathForActiveIssue',
+ 'projectPathForActiveIssue',
+ ]),
+ ...mapState(['sidebarType', 'issuableType']),
+ isIssuableSidebar() {
+ return this.sidebarType === ISSUABLE;
+ },
+ showSidebar() {
+ return this.isIssuableSidebar && this.isSidebarOpen;
+ },
+ fullPath() {
+ return this.activeBoardItem?.referencePath?.split('#')[0] || '';
+ },
+ },
+ methods: {
+ ...mapActions(['toggleBoardItem', 'setAssignees']),
+ handleClose() {
+ this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-drawer
+ v-if="showSidebar"
+ :open="isSidebarOpen"
+ :header-height="$options.headerHeight"
+ @close="handleClose"
+ >
+ <template #header>{{ __('Issue details') }}</template>
+ <template #default>
+ <board-sidebar-title />
+ <sidebar-assignees-widget
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :initial-assignees="activeBoardItem.assignees"
+ class="assignee"
+ @assignees-updated="setAssignees"
+ />
+ <board-sidebar-epic-select class="epic" />
+ <div>
+ <board-sidebar-milestone-select />
+ <sidebar-iteration-widget
+ :iid="activeBoardItem.iid"
+ :workspace-path="projectPathForActiveIssue"
+ :iterations-workspace-path="groupPathForActiveIssue"
+ :issuable-type="issuableType"
+ class="gl-mt-5"
+ />
+ </div>
+ <board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" />
+ <board-sidebar-due-date />
+ <board-sidebar-labels-select class="labels" />
+ <board-sidebar-weight-input v-if="glFeatures.issueWeights" class="weight" />
+ <board-sidebar-subscription class="subscriptions" />
+ </template>
+ </gl-drawer>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_extra_actions.vue b/app/assets/javascripts/boards/components/board_extra_actions.vue
deleted file mode 100644
index b802ccc7882..00000000000
--- a/app/assets/javascripts/boards/components/board_extra_actions.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<script>
-import { GlTooltip, GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
-
-export default {
- name: 'BoardExtraActions',
- components: {
- GlTooltip,
- GlButton,
- },
- props: {
- canAdminList: {
- type: Boolean,
- required: true,
- },
- disabled: {
- type: Boolean,
- required: true,
- },
- openModal: {
- type: Function,
- required: true,
- },
- },
- computed: {
- tooltipTitle() {
- if (this.disabled) {
- return __('Please add a list to your board first');
- }
-
- return '';
- },
- },
-};
-</script>
-
-<template>
- <div class="board-extra-actions">
- <span ref="addIssuesButtonTooltip" class="gl-ml-3">
- <gl-button
- v-if="canAdminList"
- type="button"
- data-placement="bottom"
- data-track-event="click_button"
- data-track-label="board_add_issues"
- :disabled="disabled"
- :aria-disabled="disabled"
- @click="openModal"
- >
- {{ __('Add issues') }}
- </gl-button>
- </span>
- <gl-tooltip v-if="disabled" :target="() => $refs.addIssuesButtonTooltip" placement="bottom">
- {{ tooltipTitle }}
- </gl-tooltip>
- </div>
-</template>
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index d8504dcfb0f..78da4137d69 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -107,7 +107,7 @@ export default {
};
},
computed: {
- ...mapGetters(['isEpicBoard', 'isGroupBoard', 'isProjectBoard']),
+ ...mapGetters(['isIssueBoard', 'isGroupBoard', 'isProjectBoard']),
isNewForm() {
return this.currentPage === formType.new;
},
@@ -127,7 +127,7 @@ export default {
if (this.isDeleteForm) {
return 'danger';
}
- return 'info';
+ return 'confirm';
},
title() {
if (this.readonly) {
@@ -163,6 +163,9 @@ export default {
currentMutation() {
return this.board.id ? updateBoardMutation : createBoardMutation;
},
+ deleteMutation() {
+ return destroyBoardMutation;
+ },
baseMutationVariables() {
const { board } = this;
const variables = {
@@ -182,7 +185,7 @@ export default {
groupPath: this.isGroupBoard ? this.fullPath : undefined,
};
},
- boardScopeMutationVariables() {
+ issueBoardScopeMutationVariables() {
/* eslint-disable @gitlab/require-i18n-strings */
return {
weight: this.board.weight,
@@ -193,13 +196,18 @@ export default {
this.board.milestone?.id || this.board.milestone?.id === 0
? convertToGraphQLId('Milestone', this.board.milestone.id)
: null,
- labelIds: this.board.labels.map(fullLabelId),
iterationId: this.board.iteration_id
? convertToGraphQLId('Iteration', this.board.iteration_id)
: null,
};
/* eslint-enable @gitlab/require-i18n-strings */
},
+ boardScopeMutationVariables() {
+ return {
+ labelIds: this.board.labels.map(fullLabelId),
+ ...(this.isIssueBoard && this.issueBoardScopeMutationVariables),
+ };
+ },
mutationVariables() {
return {
...this.baseMutationVariables,
@@ -239,17 +247,20 @@ export default {
return this.boardUpdateResponse(response.data);
},
+ async deleteBoard() {
+ await this.$apollo.mutate({
+ mutation: this.deleteMutation,
+ variables: {
+ id: fullBoardId(this.board.id),
+ },
+ });
+ },
async submit() {
if (this.board.name.length === 0) return;
this.isLoading = true;
if (this.isDeleteForm) {
try {
- await this.$apollo.mutate({
- mutation: destroyBoardMutation,
- variables: {
- id: fullBoardId(this.board.id),
- },
- });
+ await this.deleteBoard();
visitUrl(this.rootPath);
} catch {
Flash(this.$options.i18n.deleteErrorMessage);
@@ -324,7 +335,7 @@ export default {
/>
<board-scope
- v-if="scopedIssueBoardFeatureEnabled && !isEpicBoard"
+ v-if="scopedIssueBoardFeatureEnabled"
:collapse-scope="isNewForm"
:board="board"
:can-admin-board="canAdminBoard"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index ae8434be312..94e29f3ad86 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -190,7 +190,7 @@ export default {
}
this.moveItem({
- itemId,
+ itemId: Number(itemId),
itemIid,
itemPath,
fromListId: from.dataset.listId,
diff --git a/app/assets/javascripts/boards/components/board_list_deprecated.vue b/app/assets/javascripts/boards/components/board_list_deprecated.vue
index d59fbcc1b31..0534e027c86 100644
--- a/app/assets/javascripts/boards/components/board_list_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_list_deprecated.vue
@@ -134,9 +134,10 @@ export default {
e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list');
const toBoardType = containerEl.dataset.boardType;
const cloneActions = {
- label: ['milestone', 'assignee'],
- assignee: ['milestone', 'label'],
- milestone: ['label', 'assignee'],
+ label: ['milestone', 'assignee', 'iteration'],
+ assignee: ['milestone', 'label', 'iteration'],
+ milestone: ['label', 'assignee', 'iteration'],
+ iteration: ['label', 'assignee', 'milestone'],
};
if (toBoardType) {
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 6ccaec4a633..ca66ad6934a 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -328,6 +328,7 @@ export default {
<div
class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag gl-text-gray-500"
+ data-testid="issue-count-badge"
:class="{
'gl-display-none!': list.collapsed && isSwimlanesHeader,
'gl-p-0': list.collapsed,
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index a81c28733cd..144cae15ab3 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -2,23 +2,23 @@
import { GlButton } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
+import BoardNewIssueMixin from 'ee_else_ce/boards/mixins/board_new_issue';
import { __ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
export default {
name: 'BoardNewIssue',
i18n: {
- submit: __('Submit issue'),
+ submit: __('Create issue'),
cancel: __('Cancel'),
},
components: {
ProjectSelect,
GlButton,
},
- mixins: [glFeatureFlagMixin()],
- inject: ['groupId', 'weightFeatureAvailable', 'boardWeight'],
+ mixins: [BoardNewIssueMixin],
+ inject: ['groupId'],
props: {
list: {
type: Object,
@@ -53,14 +53,11 @@ export default {
submit(e) {
e.preventDefault();
+ const { title } = this;
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({
@@ -70,7 +67,7 @@ export default {
assigneeIds: assignees?.map((a) => a?.id),
milestoneId: milestone?.id,
projectPath: this.selectedProject.fullPath,
- weight: weight >= 0 ? weight : null,
+ ...this.extraIssueInput(),
},
list: this.list,
}).then(() => {
diff --git a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
index 16f23dfff0e..1218941065f 100644
--- a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
@@ -121,7 +121,7 @@ export default {
variant="success"
category="primary"
type="submit"
- >{{ __('Submit issue') }}</gl-button
+ >{{ __('Create issue') }}</gl-button
>
<gl-button
ref="cancelButton"
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 7cfedad0aed..997655c346a 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -22,13 +22,7 @@ export default {
import('ee_component/boards/components/board_settings_list_types.vue'),
},
mixins: [glFeatureFlagMixin()],
- props: {
- canAdminList: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
+ inject: ['canAdminList'],
data() {
return {
ListType,
diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue
index 7ec99e51f5b..fdb60d0ae6a 100644
--- a/app/assets/javascripts/boards/components/config_toggle.vue
+++ b/app/assets/javascripts/boards/components/config_toggle.vue
@@ -15,7 +15,8 @@ export default {
props: {
boardsStore: {
type: Object,
- required: true,
+ required: false,
+ default: null,
},
canAdminList: {
type: Boolean,
@@ -26,11 +27,6 @@ export default {
required: true,
},
},
- data() {
- return {
- state: this.boardsStore.state,
- };
- },
computed: {
buttonText() {
return this.canAdminList ? s__('Boards|Edit board') : s__('Boards|View scope');
@@ -42,7 +38,9 @@ export default {
methods: {
showPage() {
eventHub.$emit('showBoardModal', formType.edit);
- return this.boardsStore.showPage(formType.edit);
+ if (this.boardsStore) {
+ this.boardsStore.showPage(formType.edit);
+ }
},
},
};
diff --git a/app/assets/javascripts/boards/components/filtered_search.vue b/app/assets/javascripts/boards/components/filtered_search.vue
deleted file mode 100644
index 8505ea39a6b..00000000000
--- a/app/assets/javascripts/boards/components/filtered_search.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<script>
-import { mapActions } from 'vuex';
-import { historyPushState } from '~/lib/utils/common_utils';
-import { setUrlParams } from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
-import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-
-export default {
- i18n: {
- search: __('Search'),
- },
- components: { FilteredSearch },
- props: {
- search: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- initialSearch() {
- return [{ type: 'filtered-search-term', value: { data: this.search } }];
- },
- },
- methods: {
- ...mapActions(['performSearch']),
- handleSearch(filters) {
- let itemValue = '';
- const [item] = filters;
-
- if (filters.length === 0) {
- itemValue = '';
- } else {
- itemValue = item?.value?.data;
- }
-
- historyPushState(setUrlParams({ search: itemValue }, window.location.href));
-
- this.performSearch();
- },
- },
-};
-</script>
-
-<template>
- <filtered-search
- class="gl-w-full"
- namespace=""
- :tokens="[]"
- :search-input-placeholder="$options.i18n.search"
- :initial-filter-value="initialSearch"
- @onFilter="handleSearch"
- />
-</template>
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue
deleted file mode 100644
index 486b012e3d2..00000000000
--- a/app/assets/javascripts/boards/components/modal/empty_state.vue
+++ /dev/null
@@ -1,84 +0,0 @@
-<script>
-import { GlButton, GlSprintf } from '@gitlab/ui';
-import { __ } from '~/locale';
-import modalMixin from '../../mixins/modal_mixins';
-import ModalStore from '../../stores/modal_store';
-
-export default {
- components: {
- GlButton,
- GlSprintf,
- },
- mixins: [modalMixin],
- props: {
- newIssuePath: {
- type: String,
- required: true,
- },
- emptyStateSvg: {
- type: String,
- required: true,
- },
- },
- data() {
- return ModalStore.store;
- },
- computed: {
- contents() {
- const obj = {
- title: __("You haven't added any issues to your project yet"),
- content: __(
- 'An issue can be a bug, a todo or a feature request that needs to be discussed in a project. Besides, issues are searchable and filterable.',
- ),
- };
-
- if (this.activeTab === 'selected') {
- obj.title = __("You haven't selected any issues yet");
- obj.content = __(
- 'Go back to %{tagStart}Open issues%{tagEnd} and select some issues to add to your board.',
- );
- }
-
- return obj;
- },
- },
-};
-</script>
-
-<template>
- <section class="empty-state d-flex mt-0 h-100">
- <div class="row w-100 my-auto mx-0">
- <div class="col-12 col-md-6 order-md-last">
- <aside class="svg-content d-none d-md-block"><img :src="emptyStateSvg" /></aside>
- </div>
- <div class="col-12 col-md-6 order-md-first">
- <div class="text-content">
- <h4>{{ contents.title }}</h4>
- <p>
- <gl-sprintf :message="contents.content">
- <template #tag="{ content }">
- <strong>{{ content }}</strong>
- </template>
- </gl-sprintf>
- </p>
- <gl-button
- v-if="activeTab === 'all'"
- :href="newIssuePath"
- category="secondary"
- variant="success"
- >
- {{ __('New issue') }}
- </gl-button>
- <gl-button
- v-if="activeTab === 'selected'"
- category="primary"
- variant="default"
- @click="changeTab('all')"
- >
- {{ __('Open issues') }}
- </gl-button>
- </div>
- </div>
- </div>
- </section>
-</template>
diff --git a/app/assets/javascripts/boards/components/modal/filters.js b/app/assets/javascripts/boards/components/modal/filters.js
deleted file mode 100644
index 2fb38a549f3..00000000000
--- a/app/assets/javascripts/boards/components/modal/filters.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import FilteredSearchContainer from '../../../filtered_search/container';
-import FilteredSearchBoards from '../../filtered_search_boards';
-
-export default {
- name: 'modal-filters',
- props: {
- store: {
- type: Object,
- required: true,
- },
- },
- mounted() {
- FilteredSearchContainer.container = this.$el;
-
- this.filteredSearch = new FilteredSearchBoards(this.store);
- this.filteredSearch.setup();
- this.filteredSearch.removeTokens();
- this.filteredSearch.handleInputPlaceholder();
- this.filteredSearch.toggleClearSearchButton();
- },
- destroyed() {
- this.filteredSearch.cleanup();
- FilteredSearchContainer.container = document;
- this.store.path = '';
- },
- template: '#js-board-modal-filter',
-};
diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue
deleted file mode 100644
index 05e1219bc70..00000000000
--- a/app/assets/javascripts/boards/components/modal/footer.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import footerEEMixin from 'ee_else_ce/boards/mixins/modal_footer';
-import { deprecatedCreateFlash as Flash } from '../../../flash';
-import { __, n__ } from '../../../locale';
-import modalMixin from '../../mixins/modal_mixins';
-import boardsStore from '../../stores/boards_store';
-import ModalStore from '../../stores/modal_store';
-import ListsDropdown from './lists_dropdown.vue';
-
-export default {
- components: {
- ListsDropdown,
- GlButton,
- },
- mixins: [modalMixin, footerEEMixin],
- data() {
- return {
- modal: ModalStore.store,
- state: boardsStore.state,
- };
- },
- computed: {
- submitDisabled() {
- return !ModalStore.selectedCount();
- },
- submitText() {
- const count = ModalStore.selectedCount();
- if (!count) return __('Add issues');
- return n__(`Add %d issue`, `Add %d issues`, count);
- },
- },
- methods: {
- buildUpdateRequest(list) {
- return {
- add_label_ids: [list.label.id],
- };
- },
- addIssues() {
- const firstListIndex = 1;
- const list = this.modal.selectedList || this.state.lists[firstListIndex];
- const selectedIssues = ModalStore.getSelectedIssues();
- const issueIds = selectedIssues.map((issue) => issue.id);
- const req = this.buildUpdateRequest(list);
-
- // Post the data to the backend
- boardsStore.bulkUpdate(issueIds, req).catch(() => {
- Flash(__('Failed to update issues, please try again.'));
-
- selectedIssues.forEach((issue) => {
- list.removeIssue(issue);
- list.issuesSize -= 1;
- });
- });
-
- // Add the issues on the frontend
- selectedIssues.forEach((issue) => {
- list.addIssue(issue);
- list.issuesSize += 1;
- });
-
- this.toggleModal(false);
- },
- },
-};
-</script>
-<template>
- <footer class="form-actions add-issues-footer">
- <div class="float-left">
- <gl-button :disabled="submitDisabled" category="primary" variant="success" @click="addIssues">
- {{ submitText }}
- </gl-button>
- <span class="inline add-issues-footer-to-list">{{ __('to list') }}</span>
- <lists-dropdown />
- </div>
- <gl-button class="float-right" @click="toggleModal(false)">
- {{ __('Cancel') }}
- </gl-button>
- </footer>
-</template>
diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue
deleted file mode 100644
index c3a71e7177a..00000000000
--- a/app/assets/javascripts/boards/components/modal/header.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-<script>
-/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
-import modalMixin from '../../mixins/modal_mixins';
-import ModalStore from '../../stores/modal_store';
-import ModalFilters from './filters';
-import ModalTabs from './tabs.vue';
-
-export default {
- components: {
- ModalTabs,
- ModalFilters,
- GlButton,
- },
- mixins: [modalMixin],
- props: {
- projectId: {
- type: Number,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return ModalStore.store;
- },
- computed: {
- selectAllText() {
- if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
- return __('Select all');
- }
-
- return __('Deselect all');
- },
- showSearch() {
- return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
- },
- },
- methods: {
- toggleAll() {
- this.$refs.selectAllBtn.$el.blur();
-
- ModalStore.toggleAll();
- },
- },
-};
-</script>
-<template>
- <div>
- <header class="add-issues-header border-top-0 form-actions">
- <h2 class="m-0">
- Add issues
- <gl-button
- category="tertiary"
- icon="close"
- class="close"
- data-dismiss="modal"
- :aria-label="__('Close')"
- @click="toggleModal(false)"
- />
- </h2>
- </header>
- <modal-tabs v-if="!loading && issuesCount > 0" />
- <div v-if="showSearch" class="d-flex gl-mb-3">
- <modal-filters :store="filter" />
- <gl-button
- ref="selectAllBtn"
- category="secondary"
- variant="success"
- class="gl-ml-3"
- @click="toggleAll"
- >
- {{ selectAllText }}
- </gl-button>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
deleted file mode 100644
index 5af90c1ee66..00000000000
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ /dev/null
@@ -1,151 +0,0 @@
-<script>
-/* global ListIssue */
-import { GlLoadingIcon } from '@gitlab/ui';
-import boardsStore from '~/boards/stores/boards_store';
-import { urlParamsToObject } from '~/lib/utils/common_utils';
-import ModalStore from '../../stores/modal_store';
-import EmptyState from './empty_state.vue';
-import ModalFooter from './footer.vue';
-import ModalHeader from './header.vue';
-import ModalList from './list.vue';
-
-export default {
- components: {
- EmptyState,
- ModalHeader,
- ModalList,
- ModalFooter,
- GlLoadingIcon,
- },
- props: {
- newIssuePath: {
- type: String,
- required: true,
- },
- emptyStateSvg: {
- type: String,
- required: true,
- },
- projectId: {
- type: Number,
- required: true,
- },
- labelPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return ModalStore.store;
- },
- computed: {
- showList() {
- if (this.activeTab === 'selected') {
- return this.selectedIssues.length > 0;
- }
-
- return this.issuesCount > 0;
- },
- showEmptyState() {
- if (!this.loading && this.issuesCount === 0) {
- return true;
- }
-
- return this.activeTab === 'selected' && this.selectedIssues.length === 0;
- },
- },
- watch: {
- page() {
- this.loadIssues();
- },
- showAddIssuesModal() {
- if (this.showAddIssuesModal && !this.issues.length) {
- this.loading = true;
- const loadingDone = () => {
- this.loading = false;
- };
-
- this.loadIssues().then(loadingDone).catch(loadingDone);
- } else if (!this.showAddIssuesModal) {
- this.issues = [];
- this.selectedIssues = [];
- this.issuesCount = false;
- }
- },
- filter: {
- handler() {
- if (this.$el.tagName) {
- this.page = 1;
- this.filterLoading = true;
- const loadingDone = () => {
- this.filterLoading = false;
- };
-
- this.loadIssues(true).then(loadingDone).catch(loadingDone);
- }
- },
- deep: true,
- },
- },
- created() {
- this.page = 1;
- },
- methods: {
- loadIssues(clearIssues = false) {
- if (!this.showAddIssuesModal) return false;
-
- return boardsStore
- .getBacklog({
- ...urlParamsToObject(this.filter.path),
- page: this.page,
- per: this.perPage,
- })
- .then((res) => res.data)
- .then((data) => {
- if (clearIssues) {
- this.issues = [];
- }
-
- data.issues.forEach((issueObj) => {
- const issue = new ListIssue(issueObj);
- const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
- issue.selected = Boolean(foundSelectedIssue);
-
- this.issues.push(issue);
- });
-
- this.loadingNewPage = false;
-
- if (!this.issuesCount) {
- this.issuesCount = data.size;
- }
- })
- .catch(() => {
- // TODO: handle request error
- });
- },
- },
-};
-</script>
-<template>
- <div
- v-if="showAddIssuesModal"
- class="add-issues-modal d-flex position-fixed position-top-0 position-bottom-0 position-left-0 position-right-0 h-100"
- >
- <div class="add-issues-container d-flex flex-column m-auto rounded">
- <modal-header :project-id="projectId" :label-path="labelPath" />
- <modal-list v-if="!loading && showList && !filterLoading" :empty-state-svg="emptyStateSvg" />
- <empty-state
- v-if="showEmptyState"
- :new-issue-path="newIssuePath"
- :empty-state-svg="emptyStateSvg"
- />
- <section v-if="loading || filterLoading" class="add-issues-list d-flex h-100 text-center">
- <div class="add-issues-list-loading w-100 align-self-center">
- <gl-loading-icon size="md" />
- </div>
- </section>
- <modal-footer />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue
deleted file mode 100644
index e66cae0ce18..00000000000
--- a/app/assets/javascripts/boards/components/modal/list.vue
+++ /dev/null
@@ -1,141 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import ModalStore from '../../stores/modal_store';
-import BoardCardInner from '../board_card_inner.vue';
-
-export default {
- components: {
- BoardCardInner,
- GlIcon,
- },
- props: {
- emptyStateSvg: {
- type: String,
- required: true,
- },
- },
- data() {
- return ModalStore.store;
- },
- computed: {
- loopIssues() {
- if (this.activeTab === 'all') {
- return this.issues;
- }
-
- return this.selectedIssues;
- },
- groupedIssues() {
- const groups = [];
- this.loopIssues.forEach((issue, i) => {
- const index = i % this.columns;
-
- if (!groups[index]) {
- groups.push([]);
- }
-
- groups[index].push(issue);
- });
-
- return groups;
- },
- },
- watch: {
- activeTab() {
- if (this.activeTab === 'all') {
- ModalStore.purgeUnselectedIssues();
- }
- },
- },
- mounted() {
- this.scrollHandlerWrapper = this.scrollHandler.bind(this);
- this.setColumnCountWrapper = this.setColumnCount.bind(this);
- this.setColumnCount();
-
- this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
- window.addEventListener('resize', this.setColumnCountWrapper);
- },
- beforeDestroy() {
- this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
- window.removeEventListener('resize', this.setColumnCountWrapper);
- },
- methods: {
- scrollHandler() {
- const currentPage = Math.floor(this.issues.length / this.perPage);
-
- if (
- this.scrollTop() > this.scrollHeight() - 100 &&
- !this.loadingNewPage &&
- currentPage === this.page
- ) {
- this.loadingNewPage = true;
- this.page += 1;
- }
- },
- toggleIssue(e, issue) {
- if (e.target.tagName !== 'A') {
- ModalStore.toggleIssue(issue);
- }
- },
- listHeight() {
- return this.$refs.list.getBoundingClientRect().height;
- },
- scrollHeight() {
- return this.$refs.list.scrollHeight;
- },
- scrollTop() {
- return this.$refs.list.scrollTop + this.listHeight();
- },
- showIssue(issue) {
- if (this.activeTab === 'all') return true;
-
- const index = ModalStore.selectedIssueIndex(issue);
-
- return index !== -1;
- },
- setColumnCount() {
- const breakpoint = bp.getBreakpointSize();
-
- if (breakpoint === 'xl' || breakpoint === 'lg') {
- this.columns = 3;
- } else if (breakpoint === 'md') {
- this.columns = 2;
- } else {
- this.columns = 1;
- }
- },
- },
-};
-</script>
-<template>
- <section ref="list" class="add-issues-list add-issues-list-columns d-flex h-100">
- <div
- v-if="issuesCount > 0 && issues.length === 0"
- class="empty-state add-issues-empty-state-filter text-center"
- >
- <div class="svg-content"><img :src="emptyStateSvg" /></div>
- <div class="text-content">
- <h4>{{ __('There are no issues to show.') }}</h4>
- </div>
- </div>
- <div v-for="(group, index) in groupedIssues" :key="index" class="add-issues-list-column">
- <div v-for="issue in group" v-if="showIssue(issue)" :key="issue.id" class="board-card-parent">
- <div
- :class="{ 'is-active': issue.selected }"
- class="board-card position-relative p-3 rounded"
- @click="toggleIssue($event, issue)"
- >
- <board-card-inner :item="issue" />
- <gl-icon
- v-if="issue.selected"
- :aria-label="'Issue #' + issue.id + ' selected'"
- name="mobile-issue-close"
- aria-checked="true"
- class="issue-card-selected text-center"
- />
- </div>
- </div>
- </div>
- </section>
-</template>
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
deleted file mode 100644
index 2065568d275..00000000000
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<script>
-import { GlLink, GlIcon } from '@gitlab/ui';
-import boardsStore from '../../stores/boards_store';
-import ModalStore from '../../stores/modal_store';
-
-export default {
- components: {
- GlLink,
- GlIcon,
- },
- data() {
- return {
- modal: ModalStore.store,
- state: boardsStore.state,
- };
- },
- computed: {
- selected() {
- return this.modal.selectedList || this.state.lists[1];
- },
- },
- destroyed() {
- this.modal.selectedList = null;
- },
-};
-</script>
-<template>
- <div class="dropdown inline">
- <button class="dropdown-menu-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
- <span :style="{ backgroundColor: selected.label.color }" class="dropdown-label-box"> </span>
- {{ selected.title }} <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon" />
- </button>
- <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
- <ul>
- <li v-for="(list, i) in state.lists" v-if="list.type == 'label'" :key="i">
- <gl-link
- :class="{ 'is-active': list.id == selected.id }"
- href="#"
- role="button"
- @click.prevent="modal.selectedList = list"
- >
- <span :style="{ backgroundColor: list.label.color }" class="dropdown-label-box"> </span>
- {{ list.title }}
- </gl-link>
- </li>
- </ul>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue
deleted file mode 100644
index 0b717f516db..00000000000
--- a/app/assets/javascripts/boards/components/modal/tabs.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<script>
-/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
-import modalMixin from '../../mixins/modal_mixins';
-import ModalStore from '../../stores/modal_store';
-
-export default {
- components: {
- GlTabs,
- GlTab,
- GlBadge,
- },
- mixins: [modalMixin],
- data() {
- return ModalStore.store;
- },
- computed: {
- selectedCount() {
- return ModalStore.selectedCount();
- },
- },
- destroyed() {
- this.activeTab = 'all';
- },
-};
-</script>
-<template>
- <gl-tabs class="gl-mt-3">
- <gl-tab @click.prevent="changeTab('all')">
- <template slot="title">
- <span>Open issues</span>
- <gl-badge size="sm" class="gl-tab-counter-badge">{{ issuesCount }}</gl-badge>
- </template>
- </gl-tab>
- <gl-tab @click.prevent="changeTab('selected')">
- <template slot="title">
- <span>Selected issues</span>
- <gl-badge size="sm" class="gl-tab-counter-badge">{{ selectedCount }}</gl-badge>
- </template>
- </gl-tab>
- </gl-tabs>
-</template>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
index 61863bbe2a9..352a25ef6d9 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
@@ -98,14 +98,14 @@ export default {
<gl-button
v-if="canUpdate"
variant="link"
- class="gl-text-gray-900! gl-ml-5 js-sidebar-dropdown-toggle"
+ class="gl-text-gray-900! gl-ml-5 js-sidebar-dropdown-toggle edit-link"
data-testid="edit-button"
@click="toggle"
>
{{ __('Edit') }}
</gl-button>
</header>
- <div v-show="!edit" class="gl-text-gray-500" data-testid="collapsed-content">
+ <div v-show="!edit" class="gl-text-gray-500 value" data-testid="collapsed-content">
<slot name="collapsed">{{ __('None') }}</slot>
</div>
<div v-show="edit" data-testid="expanded-content">
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
index 6d928337396..13e1e232676 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
@@ -18,16 +18,16 @@ export default {
};
},
computed: {
- ...mapGetters(['activeIssue', 'projectPathForActiveIssue']),
+ ...mapGetters(['activeBoardItem', 'projectPathForActiveIssue']),
hasDueDate() {
- return this.activeIssue.dueDate != null;
+ return this.activeBoardItem.dueDate != null;
},
parsedDueDate() {
if (!this.hasDueDate) {
return null;
}
- return parsePikadayDate(this.activeIssue.dueDate);
+ return parsePikadayDate(this.activeBoardItem.dueDate);
},
formattedDueDate() {
if (!this.hasDueDate) {
@@ -69,6 +69,7 @@ export default {
<board-editable-item
ref="sidebarItem"
class="board-sidebar-due-date"
+ data-testid="sidebar-due-date"
:title="$options.i18n.dueDate"
:loading="loading"
@open="openDatePicker"
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 55b1596ee18..f78be83cd82 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 {
};
},
computed: {
- ...mapGetters(['activeIssue', 'projectPathForActiveIssue']),
+ ...mapGetters(['activeBoardItem', 'projectPathForActiveIssue']),
selectedLabels() {
- const { labels = [] } = this.activeIssue;
+ const { labels = [] } = this.activeBoardItem;
return labels.map((label) => ({
...label,
@@ -31,7 +31,7 @@ export default {
}));
},
issueLabels() {
- const { labels = [] } = this.activeIssue;
+ const { labels = [] } = this.activeBoardItem;
return labels.map((label) => ({
...label,
@@ -40,7 +40,7 @@ export default {
},
},
methods: {
- ...mapActions(['setActiveIssueLabels']),
+ ...mapActions(['setActiveBoardItemLabels']),
async setLabels(payload) {
this.loading = true;
this.$refs.sidebarItem.collapse();
@@ -52,7 +52,7 @@ export default {
.map((label) => label.id);
const input = { addLabelIds, removeLabelIds, projectPath: this.projectPathForActiveIssue };
- await this.setActiveIssueLabels(input);
+ await this.setActiveBoardItemLabels(input);
} catch (e) {
createFlash({ message: __('An error occurred while updating labels.') });
} finally {
@@ -65,7 +65,7 @@ export default {
try {
const removeLabelIds = [getIdFromGraphQLId(id)];
const input = { removeLabelIds, projectPath: this.projectPathForActiveIssue };
- await this.setActiveIssueLabels(input);
+ await this.setActiveBoardItemLabels(input);
} catch (e) {
createFlash({ message: __('An error occurred when removing the label.') });
} finally {
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
index 829f1c72806..ad225c7bf5c 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
@@ -56,20 +56,20 @@ export default {
},
},
computed: {
- ...mapGetters(['activeIssue']),
+ ...mapGetters(['activeBoardItem']),
hasMilestone() {
- return this.activeIssue.milestone !== null;
+ return this.activeBoardItem.milestone !== null;
},
groupFullPath() {
- const { referencePath = '' } = this.activeIssue;
+ const { referencePath = '' } = this.activeBoardItem;
return referencePath.slice(0, referencePath.indexOf('/'));
},
projectPath() {
- const { referencePath = '' } = this.activeIssue;
+ const { referencePath = '' } = this.activeBoardItem;
return referencePath.slice(0, referencePath.indexOf('#'));
},
dropdownText() {
- return this.activeIssue.milestone?.title ?? this.$options.i18n.noMilestone;
+ return this.activeBoardItem.milestone?.title ?? this.$options.i18n.noMilestone;
},
},
methods: {
@@ -113,11 +113,12 @@ export default {
ref="sidebarItem"
:title="$options.i18n.milestone"
:loading="loading"
- @open="handleOpen()"
+ data-testid="sidebar-milestones"
+ @open="handleOpen"
@close="handleClose"
>
<template v-if="hasMilestone" #collapsed>
- <strong class="gl-text-gray-900">{{ activeIssue.milestone.title }}</strong>
+ <strong class="gl-text-gray-900">{{ activeBoardItem.milestone.title }}</strong>
</template>
<gl-dropdown
ref="dropdown"
@@ -130,7 +131,7 @@ export default {
<gl-dropdown-item
data-testid="no-milestone-item"
:is-check-item="true"
- :is-checked="!activeIssue.milestone"
+ :is-checked="!activeBoardItem.milestone"
@click="setMilestone(null)"
>
{{ $options.i18n.noMilestone }}
@@ -142,7 +143,7 @@ export default {
v-for="milestone in milestones"
:key="milestone.id"
:is-check-item="true"
- :is-checked="activeIssue.milestone && milestone.id === activeIssue.milestone.id"
+ :is-checked="activeBoardItem.milestone && milestone.id === activeBoardItem.milestone.id"
data-testid="milestone-item"
@click="setMilestone(milestone.id)"
>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
index f01c8e8fa20..376985f7cb6 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
@@ -21,27 +21,31 @@ export default {
components: {
GlToggle,
},
+ inject: ['emailsDisabled'],
data() {
return {
loading: false,
};
},
computed: {
- ...mapGetters(['activeIssue', 'projectPathForActiveIssue']),
+ ...mapGetters(['activeBoardItem', 'projectPathForActiveIssue', 'isEpicBoard']),
+ isEmailsDisabled() {
+ return this.isEpicBoard ? this.emailsDisabled : this.activeBoardItem.emailsDisabled;
+ },
notificationText() {
- return this.activeIssue.emailsDisabled
+ return this.isEmailsDisabled
? this.$options.i18n.header.subscribeDisabledDescription
: this.$options.i18n.header.title;
},
},
methods: {
- ...mapActions(['setActiveIssueSubscribed']),
+ ...mapActions(['setActiveItemSubscribed']),
async handleToggleSubscription() {
this.loading = true;
try {
- await this.setActiveIssueSubscribed({
- subscribed: !this.activeIssue.subscribed,
+ await this.setActiveItemSubscribed({
+ subscribed: !this.activeBoardItem.subscribed,
projectPath: this.projectPathForActiveIssue,
});
} catch (error) {
@@ -61,8 +65,8 @@ export default {
>
<span data-testid="notification-header-text"> {{ notificationText }} </span>
<gl-toggle
- v-if="!activeIssue.emailsDisabled"
- :value="activeIssue.subscribed"
+ v-if="!isEmailsDisabled"
+ :value="activeBoardItem.subscribed"
:is-loading="loading"
:label="$options.i18n.header.title"
label-position="hidden"
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue
new file mode 100644
index 00000000000..96d444980a8
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue
@@ -0,0 +1,25 @@
+<script>
+import { mapGetters } from 'vuex';
+import IssuableTimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
+
+export default {
+ components: {
+ IssuableTimeTracker,
+ },
+ inject: ['timeTrackingLimitToHours'],
+ computed: {
+ ...mapGetters(['activeBoardItem']),
+ },
+};
+</script>
+
+<template>
+ <issuable-time-tracker
+ :time-estimate="activeBoardItem.timeEstimate"
+ :time-spent="activeBoardItem.totalTimeSpent"
+ :human-time-estimate="activeBoardItem.humanTimeEstimate"
+ :human-time-spent="activeBoardItem.humanTotalTimeSpent"
+ :limit-to-hours="timeTrackingLimitToHours"
+ :show-collapsed="false"
+ />
+</template>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
index 95864bd62a7..b8d3107c377 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
@@ -27,12 +27,12 @@ export default {
};
},
computed: {
- ...mapGetters({ issue: 'activeIssue' }),
+ ...mapGetters({ item: 'activeBoardItem' }),
pendingChangesStorageKey() {
- return this.getPendingChangesKey(this.issue);
+ return this.getPendingChangesKey(this.item);
},
projectPath() {
- const referencePath = this.issue.referencePath || '';
+ const referencePath = this.item.referencePath || '';
return referencePath.slice(0, referencePath.indexOf('#'));
},
validationState() {
@@ -40,29 +40,29 @@ export default {
},
},
watch: {
- issue: {
- handler(updatedIssue, formerIssue) {
- if (formerIssue?.title !== this.title) {
- localStorage.setItem(this.getPendingChangesKey(formerIssue), this.title);
+ item: {
+ handler(updatedItem, formerItem) {
+ if (formerItem?.title !== this.title) {
+ localStorage.setItem(this.getPendingChangesKey(formerItem), this.title);
}
- this.title = updatedIssue.title;
+ this.title = updatedItem.title;
this.setPendingState();
},
immediate: true,
},
},
methods: {
- ...mapActions(['setActiveIssueTitle']),
- getPendingChangesKey(issue) {
- if (!issue) {
+ ...mapActions(['setActiveItemTitle']),
+ getPendingChangesKey(item) {
+ if (!item) {
return '';
}
return joinPaths(
window.location.pathname.slice(1),
- String(issue.id),
- 'issue-title-pending-changes',
+ String(item.id),
+ 'item-title-pending-changes',
);
},
async setPendingState() {
@@ -78,7 +78,7 @@ export default {
}
},
cancel() {
- this.title = this.issue.title;
+ this.title = this.item.title;
this.$refs.sidebarItem.collapse();
this.showChangesAlert = false;
localStorage.removeItem(this.pendingChangesStorageKey);
@@ -86,24 +86,24 @@ export default {
async setTitle() {
this.$refs.sidebarItem.collapse();
- if (!this.title || this.title === this.issue.title) {
+ if (!this.title || this.title === this.item.title) {
return;
}
try {
this.loading = true;
- await this.setActiveIssueTitle({ title: this.title, projectPath: this.projectPath });
+ await this.setActiveItemTitle({ title: this.title, projectPath: this.projectPath });
localStorage.removeItem(this.pendingChangesStorageKey);
this.showChangesAlert = false;
} catch (e) {
- this.title = this.issue.title;
+ this.title = this.item.title;
createFlash({ message: this.$options.i18n.updateTitleError });
} finally {
this.loading = false;
}
},
handleOffClick() {
- if (this.title !== this.issue.title) {
+ if (this.title !== this.item.title) {
this.showChangesAlert = true;
localStorage.setItem(this.pendingChangesStorageKey, this.title);
} else {
@@ -112,11 +112,11 @@ export default {
},
},
i18n: {
- issueTitlePlaceholder: __('Issue title'),
+ titlePlaceholder: __('Title'),
submitButton: __('Save changes'),
cancelButton: __('Cancel'),
- updateTitleError: __('An error occurred when updating the issue title'),
- invalidFeedback: __('An issue title is required'),
+ updateTitleError: __('An error occurred when updating the title'),
+ invalidFeedback: __('A title is required'),
reviewYourChanges: __('Changes to the title have not been saved'),
},
};
@@ -131,10 +131,10 @@ export default {
@off-click="handleOffClick"
>
<template #title>
- <span class="gl-font-weight-bold" data-testid="issue-title">{{ issue.title }}</span>
+ <span class="gl-font-weight-bold" data-testid="item-title">{{ item.title }}</span>
</template>
<template #collapsed>
- <span class="gl-text-gray-800">{{ issue.referencePath }}</span>
+ <span class="gl-text-gray-800">{{ item.referencePath }}</span>
</template>
<gl-alert v-if="showChangesAlert" variant="warning" class="gl-mb-5" :dismissible="false">
{{ $options.i18n.reviewYourChanges }}
@@ -144,7 +144,7 @@ export default {
<gl-form-input
v-model="title"
v-autofocusonshow
- :placeholder="$options.i18n.issueTitlePlaceholder"
+ :placeholder="$options.i18n.titlePlaceholder"
:state="validationState"
/>
</gl-form-group>
diff --git a/app/assets/javascripts/boards/components/toggle_focus.vue b/app/assets/javascripts/boards/components/toggle_focus.vue
index 74805f8a681..49f5e7d20a9 100644
--- a/app/assets/javascripts/boards/components/toggle_focus.vue
+++ b/app/assets/javascripts/boards/components/toggle_focus.vue
@@ -38,14 +38,16 @@ export default {
</script>
<template>
- <div class="board-extra-actions gl-ml-3 gl-display-flex gl-align-items-center">
+ <div class="gl-ml-3 gl-display-none gl-md-display-flex gl-align-items-center">
<gl-button
ref="toggleFocusModeButton"
v-gl-tooltip
+ category="tertiary"
:icon="isFullscreen ? 'minimize' : 'maximize'"
class="js-focus-mode-btn"
data-qa-selector="focus_mode_button"
:title="$options.i18n.toggleFocusMode"
+ :aria-label="$options.i18n.toggleFocusMode"
@click="toggleFocusMode"
/>
</div>
diff --git a/app/assets/javascripts/boards/config_toggle.js b/app/assets/javascripts/boards/config_toggle.js
index 7f327c5764d..41938d8e284 100644
--- a/app/assets/javascripts/boards/config_toggle.js
+++ b/app/assets/javascripts/boards/config_toggle.js
@@ -2,14 +2,15 @@ import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import ConfigToggle from './components/config_toggle.vue';
-export default (boardsStore) => {
+export default (boardsStore = undefined) => {
const el = document.querySelector('.js-board-config');
if (!el) {
return;
}
- gl.boardConfigToggle = new Vue({
+ // eslint-disable-next-line no-new
+ new Vue({
el,
render(h) {
return h(ConfigToggle, {
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 65ebfe7be6c..4ebd30fe67b 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -1,4 +1,9 @@
import { __ } from '~/locale';
+import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql';
+import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql';
+import boardBlockingIssuesQuery from './graphql/board_blocking_issues.query.graphql';
+import issueSetSubscriptionMutation from './graphql/issue_set_subscription.mutation.graphql';
+import issueSetTitleMutation from './graphql/issue_set_title.mutation.graphql';
export const issuableTypes = {
issue: 'issue',
@@ -45,3 +50,27 @@ export default {
BoardType,
ListType,
};
+
+export const blockingIssuablesQueries = {
+ [issuableTypes.issue]: {
+ query: boardBlockingIssuesQuery,
+ },
+};
+
+export const titleQueries = {
+ [issuableTypes.issue]: {
+ mutation: issueSetTitleMutation,
+ },
+ [issuableTypes.epic]: {
+ mutation: updateEpicTitleMutation,
+ },
+};
+
+export const subscriptionQueries = {
+ [issuableTypes.issue]: {
+ mutation: issueSetSubscriptionMutation,
+ },
+ [issuableTypes.epic]: {
+ mutation: updateEpicSubscriptionMutation,
+ },
+};
diff --git a/app/assets/javascripts/boards/ee_functions.js b/app/assets/javascripts/boards/ee_functions.js
index b6b34556663..62a0d930ec0 100644
--- a/app/assets/javascripts/boards/ee_functions.js
+++ b/app/assets/javascripts/boards/ee_functions.js
@@ -2,4 +2,3 @@ export const setWeightFetchingState = () => {};
export const setEpicFetchingState = () => {};
export const getMilestoneTitle = () => ({});
-export const getBoardsModalData = () => ({});
diff --git a/app/assets/javascripts/boards/filtered_search.js b/app/assets/javascripts/boards/filtered_search.js
deleted file mode 100644
index 182a2cf3724..00000000000
--- a/app/assets/javascripts/boards/filtered_search.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import Vue from 'vue';
-import store from '~/boards/stores';
-import { queryToObject } from '~/lib/utils/url_utility';
-import FilteredSearch from './components/filtered_search.vue';
-
-export default () => {
- const queryParams = queryToObject(window.location.search);
- const el = document.getElementById('js-board-filtered-search');
-
- /*
- When https://github.com/vuejs/vue-apollo/pull/1153 is merged and deployed
- we can remove apolloProvider option from here. Currently without it its causing
- an error
- */
-
- return new Vue({
- el,
- store,
- apolloProvider: {},
- render: (createElement) =>
- createElement(FilteredSearch, {
- props: { search: queryParams.search },
- }),
- });
-};
diff --git a/app/assets/javascripts/boards/graphql/board_blocking_issues.query.graphql b/app/assets/javascripts/boards/graphql/board_blocking_issues.query.graphql
new file mode 100644
index 00000000000..4dc245660a4
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/board_blocking_issues.query.graphql
@@ -0,0 +1,16 @@
+query BoardBlockingIssues($id: IssueID!) {
+ issuable: issue(id: $id) {
+ __typename
+ id
+ blockingIssuables: blockedByIssues {
+ __typename
+ nodes {
+ id
+ iid
+ title
+ reference(full: true)
+ webUrl
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/issue.fragment.graphql b/app/assets/javascripts/boards/graphql/issue.fragment.graphql
index 1395bef39ed..7ecf9261214 100644
--- a/app/assets/javascripts/boards/graphql/issue.fragment.graphql
+++ b/app/assets/javascripts/boards/graphql/issue.fragment.graphql
@@ -7,6 +7,10 @@ fragment IssueNode on Issue {
referencePath: reference(full: true)
dueDate
timeEstimate
+ totalTimeSpent
+ humanTimeEstimate
+ humanTotalTimeSpent
+ emailsDisabled
confidential
webUrl
subscribed
diff --git a/app/assets/javascripts/boards/graphql/issue_set_subscription.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_subscription.mutation.graphql
index 1f383245ac2..bfb87758e17 100644
--- a/app/assets/javascripts/boards/graphql/issue_set_subscription.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_set_subscription.mutation.graphql
@@ -1,5 +1,5 @@
mutation issueSetSubscription($input: IssueSetSubscriptionInput!) {
- issueSetSubscription(input: $input) {
+ updateIssuableSubscription: issueSetSubscription(input: $input) {
issue {
subscribed
}
diff --git a/app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql
index 62e6c1352a6..6ad12d982e0 100644
--- a/app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql
@@ -1,5 +1,5 @@
mutation issueSetTitle($input: UpdateIssueInput!) {
- updateIssue(input: $input) {
+ updateIssuableTitle: updateIssue(input: $input) {
issue {
title
}
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index ceca5b0a451..e3f9d2f24c2 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -10,26 +10,21 @@ import {
setWeightFetchingState,
setEpicFetchingState,
getMilestoneTitle,
- getBoardsModalData,
} from 'ee_else_ce/boards/ee_functions';
import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
import toggleLabels from 'ee_else_ce/boards/toggle_labels';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardContent from '~/boards/components/board_content.vue';
-import BoardExtraActions from '~/boards/components/board_extra_actions.vue';
import './models/label';
import './models/assignee';
import '~/boards/models/milestone';
import '~/boards/models/project';
import '~/boards/filters/due_date_filters';
-import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
-import modalMixin from '~/boards/mixins/modal_mixins';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
-import ModalStore from '~/boards/stores/modal_store';
import toggleFocusMode from '~/boards/toggle_focus';
import { deprecatedCreateFlash as Flash } from '~/flash';
import createDefaultClient from '~/lib/graphql';
@@ -72,21 +67,12 @@ export default () => {
boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours);
}
- if (gon?.features?.boardsFilteredSearch) {
- import('~/boards/filtered_search')
- .then(({ default: initFilteredSearch }) => {
- initFilteredSearch(apolloProvider);
- })
- .catch(() => {});
- }
-
// eslint-disable-next-line @gitlab/no-runtime-template-compiler
issueBoardsApp = new Vue({
el: $boardApp,
components: {
BoardContent,
BoardSidebar,
- BoardAddIssuesModal,
BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'),
},
provide: {
@@ -95,6 +81,7 @@ export default () => {
rootPath: $boardApp.dataset.rootPath,
currentUserId: gon.current_user_id || null,
canUpdate: parseBoolean($boardApp.dataset.canUpdate),
+ canAdminList: parseBoolean($boardApp.dataset.canAdminList),
labelsFetchPath: $boardApp.dataset.labelsFetchPath,
labelsManagePath: $boardApp.dataset.labelsManagePath,
labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath,
@@ -107,6 +94,8 @@ export default () => {
milestoneListsAvailable: parseBoolean($boardApp.dataset.milestoneListsAvailable),
assigneeListsAvailable: parseBoolean($boardApp.dataset.assigneeListsAvailable),
iterationListsAvailable: parseBoolean($boardApp.dataset.iterationListsAvailable),
+ issuableType: issuableTypes.issue,
+ emailsDisabled: parseBoolean($boardApp.dataset.emailsDisabled),
},
store,
apolloProvider,
@@ -174,15 +163,9 @@ export default () => {
eventHub.$off('initialBoardLoad', this.initialBoardLoad);
},
mounted() {
- if (!gon.features?.boardsFilteredSearch) {
- this.filterManager = new FilteredSearchBoards(
- boardsStore.filter,
- true,
- boardsStore.cantEdit,
- );
+ this.filterManager = new FilteredSearchBoards(boardsStore.filter, true, boardsStore.cantEdit);
- this.filterManager.setup();
- }
+ this.filterManager.setup();
this.performSearch();
@@ -323,49 +306,7 @@ export default () => {
boardConfigToggle(boardsStore);
- const issueBoardsModal = document.getElementById('js-add-issues-btn');
-
- if (issueBoardsModal && gon.features.addIssuesButton) {
- // eslint-disable-next-line no-new
- new Vue({
- el: issueBoardsModal,
- mixins: [modalMixin],
- data() {
- return {
- modal: ModalStore.store,
- store: boardsStore.state,
- ...getBoardsModalData(),
- canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
- };
- },
- computed: {
- disabled() {
- if (!this.store) {
- return true;
- }
- return !this.store.lists.filter((list) => !list.preset).length;
- },
- },
- methods: {
- openModal() {
- if (!this.disabled) {
- this.toggleModal(true);
- }
- },
- },
- render(createElement) {
- return createElement(BoardExtraActions, {
- props: {
- canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
- openModal: this.openModal,
- disabled: this.disabled,
- },
- });
- },
- });
- }
-
- toggleFocusMode(ModalStore, boardsStore);
+ toggleFocusMode();
toggleLabels();
if (gon.licensed_features?.swimlanes) {
diff --git a/app/assets/javascripts/boards/mixins/board_new_issue.js b/app/assets/javascripts/boards/mixins/board_new_issue.js
new file mode 100644
index 00000000000..d4b74544735
--- /dev/null
+++ b/app/assets/javascripts/boards/mixins/board_new_issue.js
@@ -0,0 +1,6 @@
+export default {
+ // EE-only
+ methods: {
+ extraIssueInput: () => {},
+ },
+};
diff --git a/app/assets/javascripts/boards/mixins/modal_footer.js b/app/assets/javascripts/boards/mixins/modal_footer.js
deleted file mode 100644
index ff8b4c56321..00000000000
--- a/app/assets/javascripts/boards/mixins/modal_footer.js
+++ /dev/null
@@ -1 +0,0 @@
-export default {};
diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js b/app/assets/javascripts/boards/mixins/modal_mixins.js
deleted file mode 100644
index 6c97e1629bf..00000000000
--- a/app/assets/javascripts/boards/mixins/modal_mixins.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import ModalStore from '../stores/modal_store';
-
-export default {
- methods: {
- toggleModal(toggle) {
- ModalStore.store.showAddIssuesModal = toggle;
- },
- changeTab(tab) {
- ModalStore.store.activeTab = tab;
- },
- },
-};
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 19b31ee7291..8005414962c 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,16 +1,21 @@
+import * as Sentry from '@sentry/browser';
import { pick } from 'lodash';
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
+import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import {
BoardType,
ListType,
inactiveId,
flashAnimationDuration,
ISSUABLE,
+ titleQueries,
+ subscriptionQueries,
} from '~/boards/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { convertObjectPropsToCamelCase, urlParamsToObject } from '~/lib/utils/common_utils';
+import { s__ } from '~/locale';
import {
formatBoardLists,
formatListIssues,
@@ -20,18 +25,17 @@ import {
formatIssueInput,
updateListPosition,
transformNotFilters,
+ moveItemListHelper,
+ getMoveData,
} from '../boards_util';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import destroyBoardListMutation from '../graphql/board_list_destroy.mutation.graphql';
import updateBoardListMutation from '../graphql/board_list_update.mutation.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
-import issueMoveListMutation from '../graphql/issue_move_list.mutation.graphql';
import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.graphql';
-import issueSetSubscriptionMutation from '../graphql/issue_set_subscription.mutation.graphql';
-import issueSetTitleMutation from '../graphql/issue_set_title.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import * as types from './mutation_types';
@@ -68,6 +72,7 @@ export default {
'milestoneTitle',
'releaseTag',
'search',
+ 'myReactionEmoji',
]);
filterParams.not = transformNotFilters(filters);
commit(types.SET_FILTERS, filterParams);
@@ -326,63 +331,155 @@ export default {
commit(types.RESET_ISSUES);
},
- moveItem: ({ dispatch }) => {
- dispatch('moveIssue');
+ moveItem: ({ dispatch }, payload) => {
+ dispatch('moveIssue', payload);
},
- moveIssue: (
- { state, commit },
- { itemId, itemIid, itemPath, fromListId, toListId, moveBeforeId, moveAfterId },
+ moveIssue: ({ dispatch, state }, params) => {
+ const moveData = getMoveData(state, params);
+
+ dispatch('moveIssueCard', moveData);
+ dispatch('updateMovedIssue', moveData);
+ dispatch('updateIssueOrder', { moveData });
+ },
+
+ moveIssueCard: ({ commit }, moveData) => {
+ const {
+ reordering,
+ shouldClone,
+ itemNotInToList,
+ originalIndex,
+ itemId,
+ fromListId,
+ toListId,
+ moveBeforeId,
+ moveAfterId,
+ } = moveData;
+
+ commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: fromListId });
+
+ if (reordering) {
+ commit(types.ADD_BOARD_ITEM_TO_LIST, {
+ itemId,
+ listId: toListId,
+ moveBeforeId,
+ moveAfterId,
+ });
+
+ return;
+ }
+
+ if (itemNotInToList) {
+ commit(types.ADD_BOARD_ITEM_TO_LIST, {
+ itemId,
+ listId: toListId,
+ moveBeforeId,
+ moveAfterId,
+ });
+ }
+
+ if (shouldClone) {
+ commit(types.ADD_BOARD_ITEM_TO_LIST, { itemId, listId: fromListId, atIndex: originalIndex });
+ }
+ },
+
+ updateMovedIssue: (
+ { commit, state: { boardItems, boardLists } },
+ { itemId, fromListId, toListId },
) => {
- const originalIssue = state.boardItems[itemId];
- const fromList = state.boardItemsByListId[fromListId];
- const originalIndex = fromList.indexOf(Number(itemId));
- commit(types.MOVE_ISSUE, { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId });
+ const updatedIssue = moveItemListHelper(
+ boardItems[itemId],
+ boardLists[fromListId],
+ boardLists[toListId],
+ );
- const { boardId } = state;
- const [fullProjectPath] = itemPath.split(/[#]/);
+ commit(types.UPDATE_BOARD_ITEM, updatedIssue);
+ },
- gqlClient
- .mutate({
+ undoMoveIssueCard: ({ commit }, moveData) => {
+ const {
+ reordering,
+ shouldClone,
+ itemNotInToList,
+ itemId,
+ fromListId,
+ toListId,
+ originalIssue,
+ originalIndex,
+ } = moveData;
+
+ commit(types.UPDATE_BOARD_ITEM, originalIssue);
+
+ if (reordering) {
+ commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: fromListId });
+ commit(types.ADD_BOARD_ITEM_TO_LIST, { itemId, listId: fromListId, atIndex: originalIndex });
+ return;
+ }
+
+ if (shouldClone) {
+ commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: fromListId });
+ }
+ if (itemNotInToList) {
+ commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: toListId });
+ }
+
+ commit(types.ADD_BOARD_ITEM_TO_LIST, { itemId, listId: fromListId, atIndex: originalIndex });
+ },
+
+ updateIssueOrder: async ({ commit, dispatch, state }, { moveData, mutationVariables = {} }) => {
+ try {
+ const { itemId, fromListId, toListId, moveBeforeId, moveAfterId } = moveData;
+ const {
+ boardId,
+ boardItems: {
+ [itemId]: { iid, referencePath },
+ },
+ } = state;
+
+ const { data } = await gqlClient.mutate({
mutation: issueMoveListMutation,
variables: {
- projectPath: fullProjectPath,
+ iid,
+ projectPath: referencePath.split(/[#]/)[0],
boardId: fullBoardId(boardId),
- iid: itemIid,
fromListId: getIdFromGraphQLId(fromListId),
toListId: getIdFromGraphQLId(toListId),
moveBeforeId,
moveAfterId,
+ // 'mutationVariables' allows EE code to pass in extra parameters.
+ ...mutationVariables,
},
- })
- .then(({ data }) => {
- if (data?.issueMoveList?.errors.length) {
- throw new Error();
- } else {
- const issue = data.issueMoveList?.issue;
- commit(types.MOVE_ISSUE_SUCCESS, { issue });
- }
- })
- .catch(() =>
- commit(types.MOVE_ISSUE_FAILURE, { originalIssue, fromListId, toListId, originalIndex }),
+ });
+
+ if (data?.issueMoveList?.errors.length || !data.issueMoveList) {
+ throw new Error('issueMoveList empty');
+ }
+
+ commit(types.MUTATE_ISSUE_SUCCESS, { issue: data.issueMoveList.issue });
+ } catch {
+ commit(
+ types.SET_ERROR,
+ s__('Boards|An error occurred while moving the issue. Please try again.'),
);
+ dispatch('undoMoveIssueCard', moveData);
+ }
},
setAssignees: ({ commit, getters }, assigneeUsernames) => {
- commit('UPDATE_ISSUE_BY_ID', {
- issueId: getters.activeIssue.id,
+ commit('UPDATE_BOARD_ITEM_BY_ID', {
+ itemId: getters.activeBoardItem.id,
prop: 'assignees',
value: assigneeUsernames,
});
},
setActiveIssueMilestone: async ({ commit, getters }, input) => {
- const { activeIssue } = getters;
+ const { activeBoardItem } = getters;
const { data } = await gqlClient.mutate({
mutation: issueSetMilestoneMutation,
variables: {
input: {
- iid: String(activeIssue.iid),
+ iid: String(activeBoardItem.iid),
milestoneId: getIdFromGraphQLId(input.milestoneId),
projectPath: input.projectPath,
},
@@ -393,65 +490,71 @@ export default {
throw new Error(data.updateIssue.errors);
}
- commit(types.UPDATE_ISSUE_BY_ID, {
- issueId: activeIssue.id,
+ commit(types.UPDATE_BOARD_ITEM_BY_ID, {
+ itemId: activeBoardItem.id,
prop: 'milestone',
value: data.updateIssue.issue.milestone,
});
},
- createNewIssue: ({ commit, state }, issueInput) => {
- const { boardConfig } = state;
+ addListItem: ({ commit }, { list, item, position }) => {
+ commit(types.ADD_BOARD_ITEM_TO_LIST, { listId: list.id, itemId: item.id, atIndex: position });
+ commit(types.UPDATE_BOARD_ITEM, item);
+ },
+
+ removeListItem: ({ commit }, { listId, itemId }) => {
+ commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { listId, itemId });
+ commit(types.REMOVE_BOARD_ITEM, itemId);
+ },
+ addListNewIssue: (
+ { state: { boardConfig, boardType, fullPath }, dispatch, commit },
+ { issueInput, list, placeholderId = `tmp-${new Date().getTime()}` },
+ ) => {
const input = formatIssueInput(issueInput, boardConfig);
- const { boardType, fullPath } = state;
if (boardType === BoardType.project) {
input.projectPath = fullPath;
}
- return gqlClient
+ const placeholderIssue = formatIssue({ ...issueInput, id: placeholderId });
+ dispatch('addListItem', { list, item: placeholderIssue, position: 0 });
+
+ gqlClient
.mutate({
mutation: issueCreateMutation,
variables: { input },
})
.then(({ data }) => {
if (data.createIssue.errors.length) {
- commit(types.CREATE_ISSUE_FAILURE);
- } else {
- return data.createIssue?.issue;
+ throw new Error();
}
- return null;
- })
- .catch(() => commit(types.CREATE_ISSUE_FAILURE));
- },
- addListIssue: ({ commit }, { list, issue, position }) => {
- commit(types.ADD_ISSUE_TO_LIST, { list, issue, position });
+ const rawIssue = data.createIssue?.issue;
+ const formattedIssue = formatIssue({ ...rawIssue, id: getIdFromGraphQLId(rawIssue.id) });
+ dispatch('removeListItem', { listId: list.id, itemId: placeholderId });
+ dispatch('addListItem', { list, item: formattedIssue, position: 0 });
+ })
+ .catch(() => {
+ dispatch('removeListItem', { listId: list.id, itemId: placeholderId });
+ commit(
+ types.SET_ERROR,
+ s__('Boards|An error occurred while creating the issue. Please try again.'),
+ );
+ });
},
- 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 }));
+ setActiveBoardItemLabels: ({ dispatch }, params) => {
+ dispatch('setActiveIssueLabels', params);
},
setActiveIssueLabels: async ({ commit, getters }, input) => {
- const { activeIssue } = getters;
+ const { activeBoardItem } = getters;
const { data } = await gqlClient.mutate({
mutation: issueSetLabelsMutation,
variables: {
input: {
- iid: String(activeIssue.iid),
+ iid: String(activeBoardItem.iid),
addLabelIds: input.addLabelIds ?? [],
removeLabelIds: input.removeLabelIds ?? [],
projectPath: input.projectPath,
@@ -463,20 +566,20 @@ export default {
throw new Error(data.updateIssue.errors);
}
- commit(types.UPDATE_ISSUE_BY_ID, {
- issueId: activeIssue.id,
+ commit(types.UPDATE_BOARD_ITEM_BY_ID, {
+ itemId: activeBoardItem.id,
prop: 'labels',
value: data.updateIssue.issue.labels.nodes,
});
},
setActiveIssueDueDate: async ({ commit, getters }, input) => {
- const { activeIssue } = getters;
+ const { activeBoardItem } = getters;
const { data } = await gqlClient.mutate({
mutation: issueSetDueDateMutation,
variables: {
input: {
- iid: String(activeIssue.iid),
+ iid: String(activeBoardItem.iid),
projectPath: input.projectPath,
dueDate: input.dueDate,
},
@@ -487,57 +590,66 @@ export default {
throw new Error(data.updateIssue.errors);
}
- commit(types.UPDATE_ISSUE_BY_ID, {
- issueId: activeIssue.id,
+ commit(types.UPDATE_BOARD_ITEM_BY_ID, {
+ itemId: activeBoardItem.id,
prop: 'dueDate',
value: data.updateIssue.issue.dueDate,
});
},
- setActiveIssueSubscribed: async ({ commit, getters }, input) => {
+ setActiveItemSubscribed: async ({ commit, getters, state }, input) => {
+ const { activeBoardItem, isEpicBoard } = getters;
+ const { fullPath, issuableType } = state;
+ const workspacePath = isEpicBoard
+ ? { groupPath: fullPath }
+ : { projectPath: input.projectPath };
const { data } = await gqlClient.mutate({
- mutation: issueSetSubscriptionMutation,
+ mutation: subscriptionQueries[issuableType].mutation,
variables: {
input: {
- iid: String(getters.activeIssue.iid),
- projectPath: input.projectPath,
+ ...workspacePath,
+ iid: String(activeBoardItem.iid),
subscribedState: input.subscribed,
},
},
});
- if (data.issueSetSubscription?.errors?.length > 0) {
- throw new Error(data.issueSetSubscription.errors);
+ if (data.updateIssuableSubscription?.errors?.length > 0) {
+ throw new Error(data.updateIssuableSubscription[issuableType].errors);
}
- commit(types.UPDATE_ISSUE_BY_ID, {
- issueId: getters.activeIssue.id,
+ commit(types.UPDATE_BOARD_ITEM_BY_ID, {
+ itemId: activeBoardItem.id,
prop: 'subscribed',
- value: data.issueSetSubscription.issue.subscribed,
+ value: data.updateIssuableSubscription[issuableType].subscribed,
});
},
- setActiveIssueTitle: async ({ commit, getters }, input) => {
- const { activeIssue } = getters;
+ setActiveItemTitle: async ({ commit, getters, state }, input) => {
+ const { activeBoardItem, isEpicBoard } = getters;
+ const { fullPath, issuableType } = state;
+ const workspacePath = isEpicBoard
+ ? { groupPath: fullPath }
+ : { projectPath: input.projectPath };
const { data } = await gqlClient.mutate({
- mutation: issueSetTitleMutation,
+ mutation: titleQueries[issuableType].mutation,
variables: {
input: {
- iid: String(activeIssue.iid),
- projectPath: input.projectPath,
+ ...workspacePath,
+ iid: String(activeBoardItem.iid),
title: input.title,
},
},
});
- if (data.updateIssue?.errors?.length > 0) {
- throw new Error(data.updateIssue.errors);
+ if (data.updateIssuableTitle?.errors?.length > 0) {
+ throw new Error(data.updateIssuableTitle.errors);
}
- commit(types.UPDATE_ISSUE_BY_ID, {
- issueId: activeIssue.id,
+ commit(types.UPDATE_BOARD_ITEM_BY_ID, {
+ itemId: activeBoardItem.id,
prop: 'title',
- value: data.updateIssue.issue.title,
+ value: data.updateIssuableTitle[issuableType].title,
});
},
@@ -576,10 +688,10 @@ export default {
const { selectedBoardItems } = state;
const index = selectedBoardItems.indexOf(boardItem);
- // If user already selected an item (activeIssue) without using mult-select,
+ // If user already selected an item (activeBoardItem) without using mult-select,
// include that item in the selection and unset state.ActiveId to hide the sidebar.
- if (getters.activeIssue) {
- commit(types.ADD_BOARD_ITEM_TO_SELECTION, getters.activeIssue);
+ if (getters.activeBoardItem) {
+ commit(types.ADD_BOARD_ITEM_TO_SELECTION, getters.activeBoardItem);
dispatch('unsetActiveId');
}
@@ -608,6 +720,18 @@ export default {
}
},
+ setError: ({ commit }, { message, error, captureError = false }) => {
+ commit(types.SET_ERROR, message);
+
+ if (captureError) {
+ Sentry.captureException(error);
+ }
+ },
+
+ unsetError: ({ commit }) => {
+ commit(types.SET_ERROR, undefined);
+ },
+
fetchBacklog: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index caa518f91ce..0589851c658 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -1,5 +1,5 @@
import { find } from 'lodash';
-import { BoardType, inactiveId } from '../constants';
+import { BoardType, inactiveId, issuableTypes } from '../constants';
export default {
isGroupBoard: (state) => state.boardType === BoardType.group,
@@ -15,17 +15,17 @@ export default {
return listItemsIds.map((id) => getters.getBoardItemById(id));
},
- activeIssue: (state) => {
+ activeBoardItem: (state) => {
return state.boardItems[state.activeId] || {};
},
groupPathForActiveIssue: (_, getters) => {
- const { referencePath = '' } = getters.activeIssue;
+ const { referencePath = '' } = getters.activeBoardItem;
return referencePath.slice(0, referencePath.indexOf('/'));
},
projectPathForActiveIssue: (_, getters) => {
- const { referencePath = '' } = getters.activeIssue;
+ const { referencePath = '' } = getters.activeBoardItem;
return referencePath.slice(0, referencePath.indexOf('#'));
},
@@ -44,6 +44,10 @@ export default {
return find(state.boardLists, (l) => l.title === title);
},
+ isIssueBoard: (state) => {
+ return state.issuableType === issuableTypes.issue;
+ },
+
isEpicBoard: () => {
return false;
},
diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js
deleted file mode 100644
index 8a8fa61361c..00000000000
--- a/app/assets/javascripts/boards/stores/modal_store.js
+++ /dev/null
@@ -1,95 +0,0 @@
-class ModalStore {
- constructor() {
- this.store = {
- columns: 3,
- issues: [],
- issuesCount: false,
- selectedIssues: [],
- showAddIssuesModal: false,
- activeTab: 'all',
- selectedList: null,
- searchTerm: '',
- loading: false,
- loadingNewPage: false,
- filterLoading: false,
- page: 1,
- perPage: 50,
- filter: {
- path: '',
- },
- };
- }
-
- selectedCount() {
- return this.getSelectedIssues().length;
- }
-
- toggleIssue(issueObj) {
- const issue = issueObj;
- const { selected } = issue;
-
- issue.selected = !selected;
-
- if (!selected) {
- this.addSelectedIssue(issue);
- } else {
- this.removeSelectedIssue(issue);
- }
- }
-
- toggleAll() {
- const select = this.selectedCount() !== this.store.issues.length;
-
- this.store.issues.forEach((issue) => {
- const issueUpdate = issue;
-
- if (issueUpdate.selected !== select) {
- issueUpdate.selected = select;
-
- if (select) {
- this.addSelectedIssue(issue);
- } else {
- this.removeSelectedIssue(issue);
- }
- }
- });
- }
-
- getSelectedIssues() {
- return this.store.selectedIssues.filter((issue) => issue.selected);
- }
-
- addSelectedIssue(issue) {
- const index = this.selectedIssueIndex(issue);
-
- if (index === -1) {
- this.store.selectedIssues.push(issue);
- }
- }
-
- removeSelectedIssue(issue, forcePurge = false) {
- if (this.store.activeTab === 'all' || forcePurge) {
- this.store.selectedIssues = this.store.selectedIssues.filter(
- (fIssue) => fIssue.id !== issue.id,
- );
- }
- }
-
- purgeUnselectedIssues() {
- this.store.selectedIssues.forEach((issue) => {
- if (!issue.selected) {
- this.removeSelectedIssue(issue, true);
- }
- });
- }
-
- selectedIssueIndex(issue) {
- return this.store.selectedIssues.indexOf(issue);
- }
-
- findSelectedIssue(issue) {
- return this.store.selectedIssues.filter((filteredIssue) => filteredIssue.id === issue.id)[0];
- }
-}
-
-export default new ModalStore();
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index e7c034fb087..22b9905ee62 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -20,23 +20,21 @@ export const REMOVE_LIST_FAILURE = 'REMOVE_LIST_FAILURE';
export const REQUEST_ITEMS_FOR_LIST = 'REQUEST_ITEMS_FOR_LIST';
export const RECEIVE_ITEMS_FOR_LIST_FAILURE = 'RECEIVE_ITEMS_FOR_LIST_FAILURE';
export const RECEIVE_ITEMS_FOR_LIST_SUCCESS = 'RECEIVE_ITEMS_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';
-export const MOVE_ISSUE = 'MOVE_ISSUE';
-export const MOVE_ISSUE_SUCCESS = 'MOVE_ISSUE_SUCCESS';
-export const MOVE_ISSUE_FAILURE = 'MOVE_ISSUE_FAILURE';
+export const UPDATE_BOARD_ITEM = 'UPDATE_BOARD_ITEM';
+export const REMOVE_BOARD_ITEM = 'REMOVE_BOARD_ITEM';
export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE';
+export const MUTATE_ISSUE_SUCCESS = 'MUTATE_ISSUE_SUCCESS';
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 ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST';
+export const REMOVE_BOARD_ITEM_FROM_LIST = 'REMOVE_BOARD_ITEM_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';
-export const UPDATE_ISSUE_BY_ID = 'UPDATE_ISSUE_BY_ID';
+export const UPDATE_BOARD_ITEM_BY_ID = 'UPDATE_BOARD_ITEM_BY_ID';
export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING';
export const RESET_ISSUES = 'RESET_ISSUES';
export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS';
@@ -49,3 +47,4 @@ export const SET_ADD_COLUMN_FORM_VISIBLE = 'SET_ADD_COLUMN_FORM_VISIBLE';
export const ADD_LIST_TO_HIGHLIGHTED_LISTS = 'ADD_LIST_TO_HIGHLIGHTED_LISTS';
export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS';
export const RESET_BOARD_ITEM_SELECTION = 'RESET_BOARD_ITEM_SELECTION';
+export const SET_ERROR = 'SET_ERROR';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 75b60366b6a..561c21b78c1 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -2,7 +2,7 @@ import { pull, union } from 'lodash';
import Vue from 'vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__ } from '~/locale';
-import { formatIssue, moveItemListHelper } from '../boards_util';
+import { formatIssue } from '../boards_util';
import { issuableTypes } from '../constants';
import * as mutationTypes from './mutation_types';
@@ -158,13 +158,13 @@ export default {
});
},
- [mutationTypes.UPDATE_ISSUE_BY_ID]: (state, { issueId, prop, value }) => {
- if (!state.boardItems[issueId]) {
+ [mutationTypes.UPDATE_BOARD_ITEM_BY_ID]: (state, { itemId, prop, value }) => {
+ if (!state.boardItems[itemId]) {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
throw new Error('No issue found.');
}
- Vue.set(state.boardItems[issueId], prop, value);
+ Vue.set(state.boardItems[itemId], prop, value);
},
[mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) {
@@ -183,40 +183,11 @@ export default {
notImplemented();
},
- [mutationTypes.MOVE_ISSUE]: (
- state,
- { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId },
- ) => {
- const fromList = state.boardLists[fromListId];
- const toList = state.boardLists[toListId];
-
- const issue = moveItemListHelper(originalIssue, fromList, toList);
- Vue.set(state.boardItems, issue.id, issue);
-
- removeItemFromList({ state, listId: fromListId, itemId: issue.id });
- addItemToList({ state, listId: toListId, itemId: issue.id, moveBeforeId, moveAfterId });
- },
-
- [mutationTypes.MOVE_ISSUE_SUCCESS]: (state, { issue }) => {
+ [mutationTypes.MUTATE_ISSUE_SUCCESS]: (state, { issue }) => {
const issueId = getIdFromGraphQLId(issue.id);
Vue.set(state.boardItems, issueId, formatIssue({ ...issue, id: issueId }));
},
- [mutationTypes.MOVE_ISSUE_FAILURE]: (
- state,
- { originalIssue, fromListId, toListId, originalIndex },
- ) => {
- state.error = s__('Boards|An error occurred while moving the issue. Please try again.');
- Vue.set(state.boardItems, originalIssue.id, originalIssue);
- removeItemFromList({ state, listId: toListId, itemId: originalIssue.id });
- addItemToList({
- state,
- listId: fromListId,
- itemId: originalIssue.id,
- atIndex: originalIndex,
- });
- },
-
[mutationTypes.REQUEST_UPDATE_ISSUE]: () => {
notImplemented();
},
@@ -229,28 +200,23 @@ export default {
notImplemented();
},
- [mutationTypes.CREATE_ISSUE_FAILURE]: (state) => {
- state.error = s__('Boards|An error occurred while creating the issue. Please try again.');
+ [mutationTypes.ADD_BOARD_ITEM_TO_LIST]: (
+ state,
+ { itemId, listId, moveBeforeId, moveAfterId, atIndex },
+ ) => {
+ addItemToList({ state, listId, itemId, moveBeforeId, moveAfterId, atIndex });
},
- [mutationTypes.ADD_ISSUE_TO_LIST]: (state, { list, issue, position }) => {
- addItemToList({
- state,
- listId: list.id,
- itemId: issue.id,
- atIndex: position,
- });
- Vue.set(state.boardItems, issue.id, issue);
+ [mutationTypes.REMOVE_BOARD_ITEM_FROM_LIST]: (state, { itemId, listId }) => {
+ removeItemFromList({ state, listId, itemId });
},
- [mutationTypes.ADD_ISSUE_TO_LIST_FAILURE]: (state, { list, issueId }) => {
- state.error = s__('Boards|An error occurred while creating the issue. Please try again.');
- removeItemFromList({ state, listId: list.id, itemId: issueId });
+ [mutationTypes.UPDATE_BOARD_ITEM]: (state, item) => {
+ Vue.set(state.boardItems, item.id, item);
},
- [mutationTypes.REMOVE_ISSUE_FROM_LIST]: (state, { list, issue }) => {
- removeItemFromList({ state, listId: list.id, itemId: issue.id });
- Vue.delete(state.boardItems, issue.id);
+ [mutationTypes.REMOVE_BOARD_ITEM]: (state, itemId) => {
+ Vue.delete(state.boardItems, itemId);
},
[mutationTypes.SET_CURRENT_PAGE]: () => {
@@ -309,4 +275,8 @@ export default {
[mutationTypes.RESET_BOARD_ITEM_SELECTION]: (state) => {
state.selectedBoardItems = [];
},
+
+ [mutationTypes.SET_ERROR]: (state, error) => {
+ state.error = error;
+ },
};