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.js144
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_trigger.vue10
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue16
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue14
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue15
-rw-r--r--app/assets/javascripts/boards/components/board_column_deprecated.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue67
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue5
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue51
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue89
-rw-r--r--app/assets/javascripts/boards/components/board_list_deprecated.vue13
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue20
-rw-r--r--app/assets/javascripts/boards/components/board_list_header_deprecated.vue16
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue4
-rw-r--r--app/assets/javascripts/boards/components/config_toggle.vue3
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue6
-rw-r--r--app/assets/javascripts/boards/components/issue_time_estimate_deprecated.vue2
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue110
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue11
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue158
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue6
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue20
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue5
-rw-r--r--app/assets/javascripts/boards/constants.js27
-rw-r--r--app/assets/javascripts/boards/graphql/group_board_members.query.graphql16
-rw-r--r--app/assets/javascripts/boards/graphql/issue.fragment.graphql4
-rw-r--r--app/assets/javascripts/boards/graphql/issue_set_due_date.mutation.graphql8
-rw-r--r--app/assets/javascripts/boards/graphql/issue_set_milestone.mutation.graphql12
-rw-r--r--app/assets/javascripts/boards/graphql/lists_issues.query.graphql8
-rw-r--r--app/assets/javascripts/boards/graphql/project_board_members.query.graphql16
-rw-r--r--app/assets/javascripts/boards/index.js16
-rw-r--r--app/assets/javascripts/boards/models/project.js1
-rw-r--r--app/assets/javascripts/boards/stores/actions.js103
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js9
-rw-r--r--app/assets/javascripts/boards/stores/state.js1
38 files changed, 464 insertions, 553 deletions
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index f53d41dd0f4..e14a770411e 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,6 +1,6 @@
import { sortBy, cloneDeep } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { ListType, NOT_FILTER, AssigneeIdParamValues } from './constants';
+import { ListType } from './constants';
export function getMilestone() {
return null;
@@ -40,7 +40,7 @@ export function formatListIssues(listIssues) {
let listItemsCount;
const listData = listIssues.nodes.reduce((map, list) => {
- listItemsCount = list.issues.count;
+ listItemsCount = list.issuesCount;
let sortedIssues = list.issues.edges.map((issueNode) => ({
...issueNode.node,
}));
@@ -175,45 +175,106 @@ export function isListDraggable(list) {
return list.listType !== ListType.backlog && list.listType !== ListType.closed;
}
-export function transformNotFilters(filters) {
- return Object.keys(filters)
- .filter((key) => key.startsWith(NOT_FILTER))
- .reduce((obj, key) => {
- return {
- ...obj,
- [key.substring(4, key.length - 1)]: filters[key],
- };
- }, {});
-}
-
-export function getSupportedParams(filters, supportedFilters) {
- return supportedFilters.reduce((acc, f) => {
- /**
- * TODO the API endpoint for the classic boards
- * accepts assignee wildcard value as 'assigneeId' param -
- * while the GraphQL query accepts the value in 'assigneWildcardId' field.
- * Once we deprecate the classics boards,
- * we should change the filtered search bar to use 'asssigneeWildcardId' as a token name.
- */
- if (f === 'assigneeId' && filters[f]) {
- return AssigneeIdParamValues.includes(filters[f])
- ? {
- ...acc,
- assigneeWildcardId: filters[f].toUpperCase(),
- }
- : acc;
- }
-
- if (filters[f]) {
- return {
- ...acc,
- [f]: filters[f],
- };
- }
-
- return acc;
- }, {});
-}
+export const FiltersInfo = {
+ assigneeUsername: {
+ negatedSupport: true,
+ },
+ assigneeId: {
+ // assigneeId should be renamed to assigneeWildcardId.
+ // Classic boards used 'assigneeId'
+ remap: () => 'assigneeWildcardId',
+ },
+ assigneeWildcardId: {
+ negatedSupport: false,
+ transform: (val) => val.toUpperCase(),
+ },
+ authorUsername: {
+ negatedSupport: true,
+ },
+ labelName: {
+ negatedSupport: true,
+ },
+ milestoneTitle: {
+ negatedSupport: true,
+ },
+ myReactionEmoji: {
+ negatedSupport: true,
+ },
+ releaseTag: {
+ negatedSupport: true,
+ },
+ search: {
+ negatedSupport: false,
+ },
+};
+
+/**
+ * @param {Object} filters - ex. { search: "foobar", "not[authorUsername]": "root", }
+ * @returns {Object} - ex. [ ["search", "foobar", false], ["authorUsername", "root", true], ]
+ */
+const parseFilters = (filters) => {
+ /* eslint-disable-next-line @gitlab/require-i18n-strings */
+ const isNegated = (x) => x.startsWith('not[') && x.endsWith(']');
+
+ return Object.entries(filters).map(([k, v]) => {
+ const isNot = isNegated(k);
+ const filterKey = isNot ? k.slice(4, -1) : k;
+
+ return [filterKey, v, isNot];
+ });
+};
+
+/**
+ * Returns an object of filter key/value pairs used as variables in GraphQL requests.
+ * (warning: filter values are not validated)
+ *
+ * @param {Object} objParam.filters - filters extracted from url params. ex. { search: "foobar", "not[authorUsername]": "root", }
+ * @param {string} objParam.issuableType - issuable type e.g., issue.
+ * @param {Object} objParam.filterInfo - data on filters such as how to transform filter value, if filter can be negated, etc.
+ * @param {Object} objParam.filterFields - data on what filters are available for given issuableType (based on GraphQL schema)
+ */
+export const filterVariables = ({ filters, issuableType, filterInfo, filterFields }) =>
+ parseFilters(filters)
+ .map(([k, v, negated]) => {
+ // for legacy reasons, some filters need to be renamed to correct GraphQL fields.
+ const remapAvailable = filterInfo[k]?.remap;
+ const remappedKey = remapAvailable ? filterInfo[k].remap(k, v) : k;
+
+ return [remappedKey, v, negated];
+ })
+ .filter(([k, , negated]) => {
+ // remove unsupported filters (+ check if the filters support negation)
+ const supported = filterFields[issuableType].includes(k);
+ if (supported) {
+ return negated ? filterInfo[k].negatedSupport : true;
+ }
+
+ return false;
+ })
+ .map(([k, v, negated]) => {
+ // if the filter value needs a special transformation, apply it (e.g., capitalization)
+ const transform = filterInfo[k]?.transform;
+ const newVal = transform ? transform(v) : v;
+
+ return [k, newVal, negated];
+ })
+ .reduce(
+ (acc, [k, v, negated]) => {
+ return negated
+ ? {
+ ...acc,
+ not: {
+ ...acc.not,
+ [k]: v,
+ },
+ }
+ : {
+ ...acc,
+ [k]: v,
+ };
+ },
+ { not: {} },
+ );
// EE-specific feature. Find the implementation in the `ee/`-folder
export function transformBoardConfig() {
@@ -228,5 +289,4 @@ export default {
fullLabelId,
fullIterationId,
isListDraggable,
- transformNotFilters,
};
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 85f001d9d61..2aee84b805f 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
@@ -1,21 +1,25 @@
<script>
import { GlButton } from '@gitlab/ui';
import { mapActions } from 'vuex';
+import Tracking from '~/tracking';
export default {
components: {
GlButton,
},
+ mixins: [Tracking.mixin()],
methods: {
...mapActions(['setAddColumnFormVisibility']),
+ handleClick() {
+ this.setAddColumnFormVisibility(true);
+ this.track('click_button', { label: 'create_list' });
+ },
},
};
</script>
<template>
<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>
+ <gl-button variant="confirm" @click="handleClick">{{ __('Create list') }} </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 2821b799cef..1e780f9ef84 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions, mapState } from 'vuex';
+import Tracking from '~/tracking';
import BoardCardInner from './board_card_inner.vue';
export default {
@@ -7,6 +8,7 @@ export default {
components: {
BoardCardInner,
},
+ mixins: [Tracking.mixin()],
props: {
list: {
type: Object,
@@ -40,6 +42,12 @@ export default {
this.selectedBoardItems.findIndex((boardItem) => boardItem.id === this.item.id) > -1
);
},
+ isDisabled() {
+ return this.disabled || !this.item.id || this.item.isLoading;
+ },
+ isDraggable() {
+ return !this.disabled && this.item.id && !this.item.isLoading;
+ },
},
methods: {
...mapActions(['toggleBoardItemMultiSelection', 'toggleBoardItem']),
@@ -48,10 +56,11 @@ export default {
if (e.target.closest('.js-no-trigger')) return;
const isMultiSelect = e.ctrlKey || e.metaKey;
- if (isMultiSelect) {
+ if (isMultiSelect && gon?.features?.boardMultiSelect) {
this.toggleBoardItemMultiSelection(this.item);
} else {
this.toggleBoardItem({ boardItem: this.item });
+ this.track('click_card', { label: 'right_sidebar' });
}
},
},
@@ -63,9 +72,10 @@ export default {
data-qa-selector="board_card"
:class="{
'multi-select': multiSelectVisible,
- 'user-can-drag': !disabled && item.id,
- 'is-disabled': disabled || !item.id,
+ 'user-can-drag': isDraggable,
+ 'is-disabled': isDisabled,
'is-active': isActive,
+ 'gl-cursor-not-allowed gl-bg-gray-10': item.isLoading,
}"
:index="index"
:data-item-id="item.id"
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 0cb2e64042e..2f4e9044b9e 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlLabel, GlTooltipDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { sortBy } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
@@ -17,6 +17,7 @@ import IssueTimeEstimate from './issue_time_estimate.vue';
export default {
components: {
GlLabel,
+ GlLoadingIcon,
GlIcon,
UserAvatarLink,
TooltipOnTruncate,
@@ -181,9 +182,13 @@ export default {
class="confidential-icon gl-mr-2"
:aria-label="__('Confidential')"
/>
- <a :href="item.path || item.webUrl || ''" :title="item.title" @mousemove.stop>{{
- item.title
- }}</a>
+ <a
+ :href="item.path || item.webUrl || ''"
+ :title="item.title"
+ :class="{ 'gl-text-gray-400!': item.isLoading }"
+ @mousemove.stop
+ >{{ item.title }}</a
+ >
</h4>
</div>
<div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap">
@@ -206,6 +211,7 @@ export default {
<div
class="gl-display-flex align-items-start flex-wrap-reverse board-card-number-container gl-overflow-hidden js-board-card-number-container"
>
+ <gl-loading-icon v-if="item.isLoading" size="md" class="mt-3" />
<span
v-if="item.referencePath"
class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3"
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index c9e667d526c..cc7262f3a39 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -24,11 +24,6 @@ export default {
type: Boolean,
required: true,
},
- canAdminList: {
- type: Boolean,
- required: false,
- default: false,
- },
},
computed: {
...mapState(['filterParams', 'highlightedLists']),
@@ -92,14 +87,8 @@ export default {
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
:class="{ 'board-column-highlighted': highlighted }"
>
- <board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" />
- <board-list
- ref="board-list"
- :disabled="disabled"
- :board-items="listItems"
- :list="list"
- :can-admin-list="canAdminList"
- />
+ <board-list-header :list="list" :disabled="disabled" />
+ <board-list ref="board-list" :disabled="disabled" :board-items="listItems" :list="list" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_column_deprecated.vue b/app/assets/javascripts/boards/components/board_column_deprecated.vue
index 3dc77654e28..7c090dfaa53 100644
--- a/app/assets/javascripts/boards/components/board_column_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_column_deprecated.vue
@@ -26,11 +26,6 @@ export default {
type: Boolean,
required: true,
},
- canAdminList: {
- type: Boolean,
- required: false,
- default: false,
- },
},
data() {
return {
@@ -110,7 +105,7 @@ export default {
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
:class="{ 'board-column-highlighted': list.highlighted }"
>
- <board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" />
+ <board-list-header :list="list" :disabled="disabled" />
<board-list ref="board-list" :disabled="disabled" :issues="listIssues" :list="list" />
</div>
</div>
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index b8a38d833ad..b770ac06e89 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -106,7 +106,6 @@ export default {
v-for="(list, index) in boardListsToUse"
:key="index"
ref="board"
- :can-admin-list="canAdminList"
:list="list"
:disabled="disabled"
/>
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index e1f8457c0e2..16a8a9d253f 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -1,16 +1,17 @@
<script>
import { GlDrawer } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
-import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
+import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
-import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.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 SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
+import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
headerHeight: `${contentTop()}px`,
@@ -18,19 +19,18 @@ export default {
GlDrawer,
BoardSidebarTitle,
SidebarAssigneesWidget,
+ SidebarDateWidget,
SidebarConfidentialityWidget,
BoardSidebarTimeTracker,
BoardSidebarLabelsSelect,
- BoardSidebarDueDate,
SidebarSubscriptionsWidget,
- BoardSidebarMilestoneSelect,
- BoardSidebarEpicSelect: () =>
- import('ee_component/boards/components/sidebar/board_sidebar_epic_select.vue'),
+ SidebarDropdownWidget,
BoardSidebarWeightInput: () =>
import('ee_component/boards/components/sidebar/board_sidebar_weight_input.vue'),
- SidebarIterationWidget: () =>
- import('ee_component/sidebar/components/sidebar_iteration_widget.vue'),
+ IterationSidebarDropdownWidget: () =>
+ import('ee_component/sidebar/components/iteration_sidebar_dropdown_widget.vue'),
},
+ mixins: [glFeatureFlagMixin()],
inject: {
multipleAssigneesFeatureAvailable: {
default: false,
@@ -89,20 +89,57 @@ export default {
:allow-multiple-assignees="multipleAssigneesFeatureAvailable"
@assignees-updated="setAssignees"
/>
- <board-sidebar-epic-select v-if="epicFeatureAvailable" class="epic" />
+ <sidebar-dropdown-widget
+ v-if="epicFeatureAvailable"
+ :iid="activeBoardItem.iid"
+ issuable-attribute="epic"
+ :workspace-path="projectPathForActiveIssue"
+ :attr-workspace-path="groupPathForActiveIssue"
+ :issuable-type="issuableType"
+ data-testid="sidebar-epic"
+ />
<div>
- <board-sidebar-milestone-select />
- <sidebar-iteration-widget
- v-if="iterationFeatureAvailable"
+ <sidebar-dropdown-widget
:iid="activeBoardItem.iid"
+ issuable-attribute="milestone"
:workspace-path="projectPathForActiveIssue"
- :iterations-workspace-path="groupPathForActiveIssue"
+ :attr-workspace-path="projectPathForActiveIssue"
:issuable-type="issuableType"
- class="gl-mt-5"
+ data-testid="sidebar-milestones"
/>
+ <template v-if="!glFeatures.iterationCadences">
+ <sidebar-dropdown-widget
+ v-if="iterationFeatureAvailable"
+ :iid="activeBoardItem.iid"
+ issuable-attribute="iteration"
+ :workspace-path="projectPathForActiveIssue"
+ :attr-workspace-path="groupPathForActiveIssue"
+ :issuable-type="issuableType"
+ class="gl-mt-5"
+ data-testid="iteration-edit"
+ data-qa-selector="iteration_container"
+ />
+ </template>
+ <template v-else>
+ <iteration-sidebar-dropdown-widget
+ v-if="iterationFeatureAvailable"
+ :iid="activeBoardItem.iid"
+ :workspace-path="projectPathForActiveIssue"
+ :attr-workspace-path="groupPathForActiveIssue"
+ :issuable-type="issuableType"
+ class="gl-mt-5"
+ data-testid="iteration-edit"
+ data-qa-selector="iteration_container"
+ />
+ </template>
</div>
<board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" />
- <board-sidebar-due-date />
+ <sidebar-date-widget
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ data-testid="sidebar-due-date"
+ />
<board-sidebar-labels-select class="labels" />
<board-sidebar-weight-input v-if="weightFeatureAvailable" class="weight" />
<sidebar-confidentiality-widget
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index e564af0c353..13388f02f1f 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -3,6 +3,7 @@ import { pickBy } from 'lodash';
import { mapActions } from 'vuex';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
export default {
@@ -104,7 +105,9 @@ export default {
},
getFilterParams(filters = []) {
const notFilters = filters.filter((item) => item.value.operator === '!=');
- const equalsFilters = filters.filter((item) => item.value.operator === '=');
+ const equalsFilters = filters.filter(
+ (item) => item?.value?.operator === '=' || item.type === FILTERED_SEARCH_TERM,
+ );
return { ...this.generateParams(equalsFilters), not: { ...this.generateParams(notFilters) } };
},
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 78da4137d69..aa75a0d68f5 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -1,7 +1,7 @@
<script>
-import { GlModal } from '@gitlab/ui';
-import { mapGetters } from 'vuex';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import { GlModal, GlAlert } from '@gitlab/ui';
+import { mapGetters, mapActions, mapState } from 'vuex';
+import ListLabel from '~/boards/models/label';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName } from '~/lib/utils/common_utils';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -44,6 +44,7 @@ export default {
BoardScope: () => import('ee_component/boards/components/board_scope.vue'),
GlModal,
BoardConfigurationOptions,
+ GlAlert,
},
inject: {
fullPath: {
@@ -107,6 +108,7 @@ export default {
};
},
computed: {
+ ...mapState(['error']),
...mapGetters(['isIssueBoard', 'isGroupBoard', 'isProjectBoard']),
isNewForm() {
return this.currentPage === formType.new;
@@ -222,9 +224,7 @@ export default {
}
},
methods: {
- setIteration(iterationId) {
- this.board.iteration_id = iterationId;
- },
+ ...mapActions(['setError', 'unsetError']),
boardCreateResponse(data) {
return data.createBoard.board.webPath;
},
@@ -235,6 +235,9 @@ export default {
: '';
return `${path}${param}`;
},
+ cancel() {
+ this.$emit('cancel');
+ },
async createOrUpdateBoard() {
const response = await this.$apollo.mutate({
mutation: this.currentMutation,
@@ -263,7 +266,7 @@ export default {
await this.deleteBoard();
visitUrl(this.rootPath);
} catch {
- Flash(this.$options.i18n.deleteErrorMessage);
+ this.setError({ message: this.$options.i18n.deleteErrorMessage });
} finally {
this.isLoading = false;
}
@@ -272,15 +275,12 @@ export default {
const url = await this.createOrUpdateBoard();
visitUrl(url);
} catch {
- Flash(this.$options.i18n.saveErrorMessage);
+ this.setError({ message: this.$options.i18n.saveErrorMessage });
} finally {
this.isLoading = false;
}
}
},
- cancel() {
- this.$emit('cancel');
- },
resetFormState() {
if (this.isNewForm) {
// Clear the form when we open the "New board" modal
@@ -289,6 +289,25 @@ export default {
this.board = { ...boardDefaults, ...this.currentBoard };
}
},
+ setIteration(iterationId) {
+ this.board.iteration_id = iterationId;
+ },
+ setBoardLabels(labels) {
+ labels.forEach((label) => {
+ if (label.set && !this.board.labels.find((l) => l.id === label.id)) {
+ this.board.labels.push(
+ new ListLabel({
+ id: label.id,
+ title: label.title,
+ color: label.color,
+ textColor: label.text_color,
+ }),
+ );
+ } else if (!label.set) {
+ this.board.labels = this.board.labels.filter((selected) => selected.id !== label.id);
+ }
+ });
+ },
},
};
</script>
@@ -308,6 +327,15 @@ export default {
@close="cancel"
@hide.prevent
>
+ <gl-alert
+ v-if="error"
+ class="gl-mb-3"
+ variant="danger"
+ :dismissible="true"
+ @dismiss="unsetError"
+ >
+ {{ error }}
+ </gl-alert>
<p v-if="isDeleteForm" data-testid="delete-confirmation-message">
{{ $options.i18n.deleteConfirmationMessage }}
</p>
@@ -346,6 +374,7 @@ export default {
:group-id="groupId"
:weights="weights"
@set-iteration="setIteration"
+ @set-board-labels="setBoardLabels"
/>
</form>
</gl-modal>
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 94e29f3ad86..81740b5cd17 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -1,10 +1,11 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import Draggable from 'vuedraggable';
import { mapActions, mapGetters, mapState } from 'vuex';
import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_options';
import { sprintf, __ } from '~/locale';
import defaultSortableConfig from '~/sortable/sortable_config';
+import Tracking from '~/tracking';
import eventHub from '../eventhub';
import BoardCard from './board_card.vue';
import BoardNewIssue from './board_new_issue.vue';
@@ -21,6 +22,13 @@ export default {
BoardCard,
BoardNewIssue,
GlLoadingIcon,
+ GlIntersectionObserver,
+ },
+ mixins: [Tracking.mixin()],
+ inject: {
+ canAdminList: {
+ default: false,
+ },
},
props: {
disabled: {
@@ -35,11 +43,6 @@ export default {
type: Array,
required: true,
},
- canAdminList: {
- type: Boolean,
- required: false,
- default: false,
- },
},
data() {
return {
@@ -65,7 +68,7 @@ export default {
return this.list.maxIssueCount > 0 && this.listItemsCount > this.list.maxIssueCount;
},
hasNextPage() {
- return this.pageInfoByListId[this.list.id].hasNextPage;
+ return this.pageInfoByListId[this.list.id]?.hasNextPage;
},
loading() {
return this.listsFlags[this.list.id]?.isLoading;
@@ -86,7 +89,9 @@ export default {
: this.$options.i18n.showingAllIssues;
},
treeRootWrapper() {
- return this.canAdminList ? Draggable : 'ul';
+ return this.canAdminList && !this.listsFlags[this.list.id]?.addItemToListInProgress
+ ? Draggable
+ : 'ul';
},
treeRootOptions() {
const options = {
@@ -108,19 +113,21 @@ export default {
this.showCount = this.scrollHeight() > Math.ceil(this.listHeight());
});
},
- },
- created() {
- eventHub.$on(`toggle-issue-form-${this.list.id}`, this.toggleForm);
- eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop);
- },
- mounted() {
- // Scroll event on list to load more
- this.listRef.addEventListener('scroll', this.onScroll);
+ 'list.id': {
+ handler(id, oldVal) {
+ if (id) {
+ eventHub.$on(`toggle-issue-form-${this.list.id}`, this.toggleForm);
+ eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop);
+ eventHub.$off(`toggle-issue-form-${oldVal}`, this.toggleForm);
+ eventHub.$off(`scroll-board-list-${oldVal}`, this.scrollToTop);
+ }
+ },
+ immediate: true,
+ },
},
beforeDestroy() {
eventHub.$off(`toggle-issue-form-${this.list.id}`, this.toggleForm);
eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop);
- this.listRef.removeEventListener('scroll', this.onScroll);
},
methods: {
...mapActions(['fetchItemsForList', 'moveItem']),
@@ -142,28 +149,31 @@ export default {
toggleForm() {
this.showIssueForm = !this.showIssueForm;
},
- onScroll() {
- window.requestAnimationFrame(() => {
- if (
- !this.loadingMore &&
- this.scrollTop() > this.scrollHeight() - this.scrollOffset &&
- this.hasNextPage
- ) {
- this.loadNextPage();
- }
- });
+ onReachingListBottom() {
+ if (!this.loadingMore && this.hasNextPage) {
+ this.showCount = true;
+ this.loadNextPage();
+ }
},
handleDragOnStart() {
sortableStart();
+ this.track('drag_card', { label: 'board' });
},
handleDragOnEnd(params) {
sortableEnd();
- const { newIndex, oldIndex, from, to, item } = params;
+ const { oldIndex, from, to, item } = params;
+ let { newIndex } = params;
const { itemId, itemIid, itemPath } = item.dataset;
- const { children } = to;
+ let { children } = to;
let moveBeforeId;
let moveAfterId;
+ children = Array.from(children).filter((card) => card.classList.contains('board-card'));
+
+ if (newIndex > children.length) {
+ newIndex = children.length;
+ }
+
const getItemId = (el) => Number(el.dataset.itemId);
// If item is being moved within the same list
@@ -226,6 +236,7 @@ export default {
:data-board="list.id"
:data-board-type="list.listType"
:class="{ 'bg-danger-100': boardItemsSizeExceedsMax }"
+ draggable=".board-card"
class="board-list gl-w-full gl-h-full gl-list-style-none gl-mb-0 gl-p-2 js-board-list"
data-testid="tree-root-wrapper"
@start="handleDragOnStart"
@@ -240,15 +251,17 @@ export default {
:item="item"
:disabled="disabled"
/>
- <li v-if="showCount" class="board-list-count gl-text-center" data-issue-id="-1">
- <gl-loading-icon
- v-if="loadingMore"
- :label="$options.i18n.loadingMoreboardItems"
- data-testid="count-loading-icon"
- />
- <span v-if="showingAllItems">{{ showingAllItemsText }}</span>
- <span v-else>{{ paginatedIssueText }}</span>
- </li>
+ <gl-intersection-observer @appear="onReachingListBottom">
+ <li v-if="showCount" class="board-list-count gl-text-center" data-issue-id="-1">
+ <gl-loading-icon
+ v-if="loadingMore"
+ :label="$options.i18n.loadingMoreboardItems"
+ data-testid="count-loading-icon"
+ />
+ <span v-if="showingAllItems">{{ showingAllItemsText }}</span>
+ <span v-else>{{ paginatedIssueText }}</span>
+ </li>
+ </gl-intersection-observer>
</component>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_list_deprecated.vue b/app/assets/javascripts/boards/components/board_list_deprecated.vue
index 0534e027c86..9b3e7e1547d 100644
--- a/app/assets/javascripts/boards/components/board_list_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_list_deprecated.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { Sortable, MultiDrag } from 'sortablejs';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { sprintf, __ } from '~/locale';
import eventHub from '../eventhub';
@@ -91,6 +91,13 @@ export default {
}
});
},
+ 'list.id': {
+ handler(id) {
+ if (id) {
+ eventHub.$on(`toggle-issue-form-${this.list.id}`, this.toggleForm);
+ }
+ },
+ },
},
created() {
eventHub.$on(`toggle-issue-form-${this.list.id}`, this.toggleForm);
@@ -295,7 +302,9 @@ export default {
}
if (!toList) {
- createFlash(__('Something went wrong while performing the action.'));
+ createFlash({
+ message: __('Something went wrong while performing the action.'),
+ });
}
if (!isSameList) {
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index f94697172ac..bf8396f52a6 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -14,6 +14,7 @@ import { isScopedLabel, parseBoolean } from '~/lib/utils/common_utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { n__, s__, __ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
+import Tracking from '~/tracking';
import AccessorUtilities from '../../lib/utils/accessor';
import { inactiveId, LIST, ListType } from '../constants';
import eventHub from '../eventhub';
@@ -38,6 +39,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [Tracking.mixin()],
inject: {
boardId: {
default: '',
@@ -98,6 +100,12 @@ export default {
showListDetails() {
return !this.list.collapsed || !this.isSwimlanesHeader;
},
+ showListHeaderActions() {
+ if (this.isLoggedIn) {
+ return this.isNewIssueShown || this.isSettingsShown;
+ }
+ return false;
+ },
itemsCount() {
return this.list.issuesCount;
},
@@ -149,6 +157,8 @@ export default {
}
this.setActiveId({ id: this.list.id, sidebarType: LIST });
+
+ this.track('click_button', { label: 'list_settings' });
},
showScopedLabels(label) {
return this.scopedLabelsAvailable && isScopedLabel(label);
@@ -170,6 +180,11 @@ export default {
// When expanding/collapsing, the tooltip on the caret button sometimes stays open.
// Close all tooltips manually to prevent dangling tooltips.
this.$root.$emit(BV_HIDE_TOOLTIP);
+
+ this.track('click_toggle_button', {
+ label: 'toggle_list',
+ property: collapsed ? 'closed' : 'open',
+ });
},
addToLocalStorage() {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
@@ -351,10 +366,7 @@ export default {
<!-- EE end -->
</span>
</div>
- <gl-button-group
- v-if="isNewIssueShown || isSettingsShown"
- class="board-list-button-group pl-2"
- >
+ <gl-button-group v-if="showListHeaderActions" class="board-list-button-group gl-pl-2">
<gl-button
v-if="isNewIssueShown"
v-show="!list.collapsed"
diff --git a/app/assets/javascripts/boards/components/board_list_header_deprecated.vue b/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
index 429ffd4cd06..bc29728fc55 100644
--- a/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
@@ -35,6 +35,9 @@ export default {
GlTooltip: GlTooltipDirective,
},
inject: {
+ currentUserId: {
+ default: null,
+ },
boardId: {
default: '',
},
@@ -63,7 +66,7 @@ export default {
computed: {
...mapState(['activeId']),
isLoggedIn() {
- return Boolean(gon.current_user_id);
+ return Boolean(this.currentUserId);
},
listType() {
return this.list.type;
@@ -89,6 +92,12 @@ export default {
showListDetails() {
return this.list.isExpanded || !this.isSwimlanesHeader;
},
+ showListHeaderActions() {
+ if (this.isLoggedIn) {
+ return this.isNewIssueShown || this.isSettingsShown;
+ }
+ return false;
+ },
issuesCount() {
return this.list.issuesSize;
},
@@ -320,10 +329,7 @@ export default {
</template>
</span>
</div>
- <gl-button-group
- v-if="isNewIssueShown || isSettingsShown"
- class="board-list-button-group pl-2"
- >
+ <gl-button-group v-if="showListHeaderActions" class="board-list-button-group pl-2">
<gl-button
v-if="isNewIssueShown"
ref="newIssueBtn"
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 144cae15ab3..a63b49f9508 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -102,7 +102,7 @@ export default {
ref="submitButton"
:disabled="disabled"
class="float-left js-no-auto-disable"
- variant="success"
+ variant="confirm"
category="primary"
type="submit"
>
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 3d7f1f38a34..75975c77df5 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -6,6 +6,7 @@ import boardsStore from '~/boards/stores/boards_store';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
+import Tracking from '~/tracking';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
// NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options.
@@ -21,7 +22,7 @@ export default {
BoardSettingsListTypes: () =>
import('ee_component/boards/components/board_settings_list_types.vue'),
},
- mixins: [glFeatureFlagMixin()],
+ mixins: [glFeatureFlagMixin(), Tracking.mixin()],
inject: ['canAdminList'],
data() {
return {
@@ -72,6 +73,7 @@ export default {
// eslint-disable-next-line no-alert
if (window.confirm(__('Are you sure you want to remove this list?'))) {
if (this.shouldUseGraphQL || this.isEpicBoard) {
+ this.track('click_button', { label: 'remove_list' });
this.removeList(this.activeId);
} else {
this.activeList.destroy();
diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue
index fdb60d0ae6a..30e304b8a65 100644
--- a/app/assets/javascripts/boards/components/config_toggle.vue
+++ b/app/assets/javascripts/boards/components/config_toggle.vue
@@ -3,6 +3,7 @@ import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import { formType } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import { s__, __ } from '~/locale';
+import Tracking from '~/tracking';
export default {
components: {
@@ -12,6 +13,7 @@ export default {
GlTooltip: GlTooltipDirective,
GlModalDirective,
},
+ mixins: [Tracking.mixin()],
props: {
boardsStore: {
type: Object,
@@ -37,6 +39,7 @@ export default {
},
methods: {
showPage() {
+ this.track('click_button', { label: 'edit_board' });
eventHub.$emit('showBoardModal', formType.edit);
if (this.boardsStore) {
this.boardsStore.showPage(formType.edit);
diff --git a/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue b/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
index 2652fac1818..6e90731cc2f 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
@@ -95,6 +95,9 @@ export default {
}
return __('Blocked issue');
},
+ assignees() {
+ return this.issue.assignees.filter((_, index) => this.shouldRenderAssignee(index));
+ },
},
methods: {
isIndexLessThanlimit(index) {
@@ -215,8 +218,7 @@ export default {
</div>
<div class="board-card-assignee gl-display-flex">
<user-avatar-link
- v-for="(assignee, index) in issue.assignees"
- v-if="shouldRenderAssignee(index)"
+ v-for="assignee in assignees"
:key="assignee.id"
:link-href="assigneeUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)"
diff --git a/app/assets/javascripts/boards/components/issue_time_estimate_deprecated.vue b/app/assets/javascripts/boards/components/issue_time_estimate_deprecated.vue
index fe56833016e..8ddf50cb357 100644
--- a/app/assets/javascripts/boards/components/issue_time_estimate_deprecated.vue
+++ b/app/assets/javascripts/boards/components/issue_time_estimate_deprecated.vue
@@ -10,7 +10,7 @@ export default {
},
props: {
estimate: {
- type: Number,
+ type: [Number, String],
required: true,
},
},
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
deleted file mode 100644
index 13e1e232676..00000000000
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
+++ /dev/null
@@ -1,110 +0,0 @@
-<script>
-import { GlButton, GlDatepicker } from '@gitlab/ui';
-import { mapGetters, mapActions } from 'vuex';
-import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import createFlash from '~/flash';
-import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
-import { __ } from '~/locale';
-
-export default {
- components: {
- BoardEditableItem,
- GlButton,
- GlDatepicker,
- },
- data() {
- return {
- loading: false,
- };
- },
- computed: {
- ...mapGetters(['activeBoardItem', 'projectPathForActiveIssue']),
- hasDueDate() {
- return this.activeBoardItem.dueDate != null;
- },
- parsedDueDate() {
- if (!this.hasDueDate) {
- return null;
- }
-
- return parsePikadayDate(this.activeBoardItem.dueDate);
- },
- formattedDueDate() {
- if (!this.hasDueDate) {
- return '';
- }
-
- return dateInWords(this.parsedDueDate, true);
- },
- },
- methods: {
- ...mapActions(['setActiveIssueDueDate']),
- async openDatePicker() {
- await this.$nextTick();
- this.$refs.datePicker.calendar.show();
- },
- async setDueDate(date) {
- this.loading = true;
- this.$refs.sidebarItem.collapse();
-
- try {
- const dueDate = date ? formatDate(date, 'yyyy-mm-dd') : null;
- await this.setActiveIssueDueDate({ dueDate, projectPath: this.projectPathForActiveIssue });
- } catch (e) {
- createFlash({ message: this.$options.i18n.updateDueDateError });
- } finally {
- this.loading = false;
- }
- },
- },
- i18n: {
- dueDate: __('Due date'),
- removeDueDate: __('remove due date'),
- updateDueDateError: __('An error occurred when updating the issue due date'),
- },
-};
-</script>
-
-<template>
- <board-editable-item
- ref="sidebarItem"
- class="board-sidebar-due-date"
- data-testid="sidebar-due-date"
- :title="$options.i18n.dueDate"
- :loading="loading"
- @open="openDatePicker"
- >
- <template v-if="hasDueDate" #collapsed>
- <div class="gl-display-flex gl-align-items-center">
- <strong class="gl-text-gray-900">{{ formattedDueDate }}</strong>
- <span class="gl-mx-2">-</span>
- <gl-button
- variant="link"
- class="gl-text-gray-500!"
- data-testid="reset-button"
- :disabled="loading"
- @click="setDueDate(null)"
- >
- {{ $options.i18n.removeDueDate }}
- </gl-button>
- </div>
- </template>
- <gl-datepicker
- ref="datePicker"
- :value="parsedDueDate"
- show-clear-button
- @input="setDueDate"
- @clear="setDueDate(null)"
- />
- </board-editable-item>
-</template>
-<style>
-/*
- * This can be removed after closing:
- * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1048
- */
-.board-sidebar-due-date .gl-datepicker,
-.board-sidebar-due-date .gl-datepicker-input {
- width: 100%;
-}
-</style>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
index 919ef0d3783..29febd0fa51 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
@@ -3,7 +3,6 @@ import { GlLabel } from '@gitlab/ui';
import { mapGetters, mapActions } from 'vuex';
import Api from '~/api';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@@ -50,10 +49,10 @@ export default {
/*
Labels fetched in epic boards are always group-level labels
and the correct path are passed from the backend (injected through labelsFetchPath)
-
+
For issue boards, we should always include project-level labels and use a different endpoint.
(it requires knowing the project path of a selected issue.)
-
+
Note 1. that we will be using GraphQL to fetch labels when we create a labels select widget.
And this component will be removed _wholesale_ https://gitlab.com/gitlab-org/gitlab/-/issues/300653.
@@ -74,7 +73,7 @@ export default {
},
},
methods: {
- ...mapActions(['setActiveBoardItemLabels']),
+ ...mapActions(['setActiveBoardItemLabels', 'setError']),
async setLabels(payload) {
this.loading = true;
this.$refs.sidebarItem.collapse();
@@ -88,7 +87,7 @@ export default {
const input = { addLabelIds, removeLabelIds, projectPath: this.projectPathForActiveIssue };
await this.setActiveBoardItemLabels(input);
} catch (e) {
- createFlash({ message: __('An error occurred while updating labels.') });
+ this.setError({ error: e, message: __('An error occurred while updating labels.') });
} finally {
this.loading = false;
}
@@ -101,7 +100,7 @@ export default {
const input = { removeLabelIds, projectPath: this.projectPathForActiveIssue };
await this.setActiveBoardItemLabels(input);
} catch (e) {
- createFlash({ message: __('An error occurred when removing the label.') });
+ this.setError({ error: e, message: __('An error occurred when removing the label.') });
} finally {
this.loading = false;
}
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
deleted file mode 100644
index ad225c7bf5c..00000000000
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
+++ /dev/null
@@ -1,158 +0,0 @@
-<script>
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlSearchBoxByType,
- GlDropdownDivider,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { mapGetters, mapActions } from 'vuex';
-import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import createFlash from '~/flash';
-import { __, s__ } from '~/locale';
-import projectMilestones from '../../graphql/project_milestones.query.graphql';
-
-export default {
- components: {
- BoardEditableItem,
- GlDropdown,
- GlLoadingIcon,
- GlDropdownItem,
- GlDropdownText,
- GlSearchBoxByType,
- GlDropdownDivider,
- },
- data() {
- return {
- milestones: [],
- searchTitle: '',
- loading: false,
- edit: false,
- };
- },
- apollo: {
- milestones: {
- query: projectMilestones,
- debounce: 250,
- skip() {
- return !this.edit;
- },
- variables() {
- return {
- fullPath: this.projectPath,
- searchTitle: this.searchTitle,
- state: 'active',
- includeAncestors: true,
- };
- },
- update(data) {
- const edges = data?.project?.milestones?.edges ?? [];
- return edges.map((item) => item.node);
- },
- error() {
- createFlash({ message: this.$options.i18n.fetchMilestonesError });
- },
- },
- },
- computed: {
- ...mapGetters(['activeBoardItem']),
- hasMilestone() {
- return this.activeBoardItem.milestone !== null;
- },
- groupFullPath() {
- const { referencePath = '' } = this.activeBoardItem;
- return referencePath.slice(0, referencePath.indexOf('/'));
- },
- projectPath() {
- const { referencePath = '' } = this.activeBoardItem;
- return referencePath.slice(0, referencePath.indexOf('#'));
- },
- dropdownText() {
- return this.activeBoardItem.milestone?.title ?? this.$options.i18n.noMilestone;
- },
- },
- methods: {
- ...mapActions(['setActiveIssueMilestone']),
- handleOpen() {
- this.edit = true;
- this.$refs.dropdown.show();
- },
- handleClose() {
- this.edit = false;
- this.$refs.sidebarItem.collapse();
- },
- async setMilestone(milestoneId) {
- this.loading = true;
- this.searchTitle = '';
- this.handleClose();
-
- try {
- const input = { milestoneId, projectPath: this.projectPath };
- await this.setActiveIssueMilestone(input);
- } catch (e) {
- createFlash({ message: this.$options.i18n.updateMilestoneError });
- } finally {
- this.loading = false;
- }
- },
- },
- i18n: {
- milestone: __('Milestone'),
- noMilestone: __('No milestone'),
- assignMilestone: __('Assign milestone'),
- noMilestonesFound: s__('Milestones|No milestones found'),
- fetchMilestonesError: __('There was a problem fetching milestones.'),
- updateMilestoneError: __('An error occurred while updating the milestone.'),
- },
-};
-</script>
-
-<template>
- <board-editable-item
- ref="sidebarItem"
- :title="$options.i18n.milestone"
- :loading="loading"
- data-testid="sidebar-milestones"
- @open="handleOpen"
- @close="handleClose"
- >
- <template v-if="hasMilestone" #collapsed>
- <strong class="gl-text-gray-900">{{ activeBoardItem.milestone.title }}</strong>
- </template>
- <gl-dropdown
- ref="dropdown"
- :text="dropdownText"
- :header-text="$options.i18n.assignMilestone"
- block
- @hide="handleClose"
- >
- <gl-search-box-by-type ref="search" v-model.trim="searchTitle" class="gl-m-3" />
- <gl-dropdown-item
- data-testid="no-milestone-item"
- :is-check-item="true"
- :is-checked="!activeBoardItem.milestone"
- @click="setMilestone(null)"
- >
- {{ $options.i18n.noMilestone }}
- </gl-dropdown-item>
- <gl-dropdown-divider />
- <gl-loading-icon v-if="$apollo.loading" class="gl-py-4" />
- <template v-else-if="milestones.length > 0">
- <gl-dropdown-item
- v-for="milestone in milestones"
- :key="milestone.id"
- :is-check-item="true"
- :is-checked="activeBoardItem.milestone && milestone.id === activeBoardItem.milestone.id"
- data-testid="milestone-item"
- @click="setMilestone(milestone.id)"
- >
- {{ milestone.title }}
- </gl-dropdown-item>
- </template>
- <gl-dropdown-text v-else data-testid="no-milestones-found">
- {{ $options.i18n.noMilestonesFound }}
- </gl-dropdown-text>
- </gl-dropdown>
- </board-editable-item>
-</template>
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 376985f7cb6..4f5c55d0c5d 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
@@ -1,7 +1,6 @@
<script>
import { GlToggle } from '@gitlab/ui';
import { mapGetters, mapActions } from 'vuex';
-import createFlash from '~/flash';
import { __, s__ } from '~/locale';
export default {
@@ -39,17 +38,16 @@ export default {
},
},
methods: {
- ...mapActions(['setActiveItemSubscribed']),
+ ...mapActions(['setActiveItemSubscribed', 'setError']),
async handleToggleSubscription() {
this.loading = true;
-
try {
await this.setActiveItemSubscribed({
subscribed: !this.activeBoardItem.subscribed,
projectPath: this.projectPathForActiveIssue,
});
} catch (error) {
- createFlash({ message: this.$options.i18n.updateSubscribedErrorMessage });
+ this.setError({ error, message: this.$options.i18n.updateSubscribedErrorMessage });
} finally {
this.loading = false;
}
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
index 96d444980a8..5d61f7b2887 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue
@@ -9,17 +9,29 @@ export default {
inject: ['timeTrackingLimitToHours'],
computed: {
...mapGetters(['activeBoardItem']),
+ initialTimeTracking() {
+ const {
+ timeEstimate,
+ totalTimeSpent,
+ humanTimeEstimate,
+ humanTotalTimeSpent,
+ } = this.activeBoardItem;
+ return {
+ timeEstimate,
+ totalTimeSpent,
+ humanTimeEstimate,
+ humanTotalTimeSpent,
+ };
+ },
},
};
</script>
<template>
<issuable-time-tracker
- :time-estimate="activeBoardItem.timeEstimate"
- :time-spent="activeBoardItem.totalTimeSpent"
- :human-time-estimate="activeBoardItem.humanTimeEstimate"
- :human-time-spent="activeBoardItem.humanTotalTimeSpent"
+ :issuable-iid="activeBoardItem.iid.toString()"
:limit-to-hours="timeTrackingLimitToHours"
+ :initial-time-tracking="initialTimeTracking"
:show-collapsed="false"
/>
</template>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
index b8d3107c377..e77aadfa50e 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
@@ -2,7 +2,6 @@
import { GlAlert, GlButton, GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { mapGetters, mapActions } from 'vuex';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import createFlash from '~/flash';
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
@@ -53,7 +52,7 @@ export default {
},
},
methods: {
- ...mapActions(['setActiveItemTitle']),
+ ...mapActions(['setActiveItemTitle', 'setError']),
getPendingChangesKey(item) {
if (!item) {
return '';
@@ -97,7 +96,7 @@ export default {
this.showChangesAlert = false;
} catch (e) {
this.title = this.item.title;
- createFlash({ message: this.$options.i18n.updateTitleError });
+ this.setError({ error: e, message: this.$options.i18n.updateTitleError });
} finally {
this.loading = false;
}
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index d88774d11c1..80a8fc99895 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -9,17 +9,6 @@ import updateBoardListMutation from './graphql/board_list_update.mutation.graphq
import issueSetSubscriptionMutation from './graphql/issue_set_subscription.mutation.graphql';
import issueSetTitleMutation from './graphql/issue_set_title.mutation.graphql';
-export const SupportedFilters = [
- 'assigneeUsername',
- 'authorUsername',
- 'labelName',
- 'milestoneTitle',
- 'releaseTag',
- 'search',
- 'myReactionEmoji',
- 'assigneeId',
-];
-
/* eslint-disable-next-line @gitlab/require-i18n-strings */
export const AssigneeIdParamValues = ['Any', 'None'];
@@ -47,6 +36,7 @@ export const ListTypeTitles = {
milestone: __('Milestone'),
iteration: __('Iteration'),
label: __('Label'),
+ backlog: __('Open'),
};
export const formType = {
@@ -60,8 +50,6 @@ export const inactiveId = 0;
export const ISSUABLE = 'issuable';
export const LIST = 'list';
-export const NOT_FILTER = 'not[';
-
export const flashAnimationDuration = 2000;
export const listsQuery = {
@@ -106,6 +94,19 @@ export const subscriptionQueries = {
},
};
+export const FilterFields = {
+ [issuableTypes.issue]: [
+ 'assigneeUsername',
+ 'assigneeWildcardId',
+ 'authorUsername',
+ 'labelName',
+ 'milestoneTitle',
+ 'myReactionEmoji',
+ 'releaseTag',
+ 'search',
+ ],
+};
+
export default {
BoardType,
ListType,
diff --git a/app/assets/javascripts/boards/graphql/group_board_members.query.graphql b/app/assets/javascripts/boards/graphql/group_board_members.query.graphql
new file mode 100644
index 00000000000..3b8c5389725
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/group_board_members.query.graphql
@@ -0,0 +1,16 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+query GroupBoardMembers($fullPath: ID!, $search: String) {
+ workspace: group(fullPath: $fullPath) {
+ __typename
+ assignees: groupMembers(search: $search) {
+ __typename
+ nodes {
+ id
+ user {
+ ...User
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/issue.fragment.graphql b/app/assets/javascripts/boards/graphql/issue.fragment.graphql
index 47ecb55c72b..0ff70703e1a 100644
--- a/app/assets/javascripts/boards/graphql/issue.fragment.graphql
+++ b/app/assets/javascripts/boards/graphql/issue.fragment.graphql
@@ -14,10 +14,6 @@ fragment IssueNode on Issue {
confidential
webUrl
relativePosition
- milestone {
- id
- title
- }
assignees {
nodes {
...User
diff --git a/app/assets/javascripts/boards/graphql/issue_set_due_date.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_due_date.mutation.graphql
deleted file mode 100644
index bbea248cf85..00000000000
--- a/app/assets/javascripts/boards/graphql/issue_set_due_date.mutation.graphql
+++ /dev/null
@@ -1,8 +0,0 @@
-mutation issueSetDueDate($input: UpdateIssueInput!) {
- updateIssue(input: $input) {
- issue {
- dueDate
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/boards/graphql/issue_set_milestone.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_milestone.mutation.graphql
deleted file mode 100644
index 5dc78a03a06..00000000000
--- a/app/assets/javascripts/boards/graphql/issue_set_milestone.mutation.graphql
+++ /dev/null
@@ -1,12 +0,0 @@
-mutation issueSetMilestone($input: UpdateIssueInput!) {
- updateIssue(input: $input) {
- issue {
- milestone {
- id
- title
- description
- }
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/boards/graphql/lists_issues.query.graphql b/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
index 43af7d2b2f1..d1cb1ecf834 100644
--- a/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
+++ b/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
@@ -12,11 +12,11 @@ query ListIssues(
) {
group(fullPath: $fullPath) @include(if: $isGroup) {
board(id: $boardId) {
- lists(id: $id) {
+ lists(id: $id, issueFilters: $filters) {
nodes {
id
+ issuesCount
issues(first: $first, filters: $filters, after: $after) {
- count
edges {
node {
...IssueNode
@@ -33,11 +33,11 @@ query ListIssues(
}
project(fullPath: $fullPath) @include(if: $isProject) {
board(id: $boardId) {
- lists(id: $id) {
+ lists(id: $id, issueFilters: $filters) {
nodes {
id
+ issuesCount
issues(first: $first, filters: $filters, after: $after) {
- count
edges {
node {
...IssueNode
diff --git a/app/assets/javascripts/boards/graphql/project_board_members.query.graphql b/app/assets/javascripts/boards/graphql/project_board_members.query.graphql
new file mode 100644
index 00000000000..fc6cc6b832c
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/project_board_members.query.graphql
@@ -0,0 +1,16 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+query ProjectBoardMembers($fullPath: ID!, $search: String) {
+ workspace: project(fullPath: $fullPath) {
+ __typename
+ assignees: projectMembers(search: $search) {
+ __typename
+ nodes {
+ id
+ user {
+ ...User
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 1888645ef78..fb347ce852d 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -27,7 +27,6 @@ import FilteredSearchBoards from '~/boards/filtered_search_boards';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import toggleFocusMode from '~/boards/toggle_focus';
-import { deprecatedCreateFlash as Flash } from '~/flash';
import createDefaultClient from '~/lib/graphql';
import {
NavigationType,
@@ -196,7 +195,7 @@ export default () => {
}
},
methods: {
- ...mapActions(['setInitialBoardData', 'performSearch']),
+ ...mapActions(['setInitialBoardData', 'performSearch', 'setError']),
initialBoardLoad() {
boardsStore
.all()
@@ -205,8 +204,11 @@ export default () => {
lists.forEach((list) => boardsStore.addList(list));
this.loading = false;
})
- .catch(() => {
- Flash(__('An error occurred while fetching the board lists. Please try again.'));
+ .catch((error) => {
+ this.setError({
+ error,
+ message: __('An error occurred while fetching the board lists. Please try again.'),
+ });
});
},
updateTokens() {
@@ -250,7 +252,7 @@ export default () => {
.catch(() => {
newIssue.setFetchingState('subscriptions', false);
setWeightFetchingState(newIssue, false);
- Flash(__('An error occurred while fetching sidebar data'));
+ this.setError({ message: __('An error occurred while fetching sidebar data') });
});
}
@@ -287,7 +289,9 @@ export default () => {
})
.catch(() => {
issue.setFetchingState('subscriptions', false);
- Flash(__('An error occurred when toggling the notification subscription'));
+ this.setError({
+ message: __('An error occurred when toggling the notification subscription'),
+ });
});
}
},
diff --git a/app/assets/javascripts/boards/models/project.js b/app/assets/javascripts/boards/models/project.js
index a3d5c7af7ac..9468a02856e 100644
--- a/app/assets/javascripts/boards/models/project.js
+++ b/app/assets/javascripts/boards/models/project.js
@@ -2,5 +2,6 @@ export default class IssueProject {
constructor(obj) {
this.id = obj.id;
this.path = obj.path;
+ this.fullPath = obj.path_with_namespace;
}
}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 5158e82c320..d4893f9eca7 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -7,11 +7,12 @@ import {
ISSUABLE,
titleQueries,
subscriptionQueries,
- SupportedFilters,
deleteListQueries,
listsQuery,
updateListQueries,
issuableTypes,
+ FilterFields,
+ ListTypeTitles,
} from 'ee_else_ce/boards/constants';
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
@@ -26,17 +27,15 @@ import {
formatIssue,
formatIssueInput,
updateListPosition,
- transformNotFilters,
moveItemListHelper,
getMoveData,
- getSupportedParams,
+ FiltersInfo,
+ filterVariables,
} from '../boards_util';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.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 listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import * as types from './mutation_types';
@@ -60,13 +59,16 @@ export default {
dispatch('setActiveId', { id: inactiveId, sidebarType: '' });
},
- setFilters: ({ commit }, filters) => {
- const filterParams = {
- ...getSupportedParams(filters, SupportedFilters),
- not: transformNotFilters(filters),
- };
-
- commit(types.SET_FILTERS, filterParams);
+ setFilters: ({ commit, state: { issuableType } }, filters) => {
+ commit(
+ types.SET_FILTERS,
+ filterVariables({
+ filters,
+ issuableType,
+ filterInfo: FiltersInfo,
+ filterFields: FilterFields,
+ }),
+ );
},
performSearch({ dispatch }) {
@@ -166,8 +168,11 @@ export default {
});
},
- addList: ({ commit }, list) => {
+ addList: ({ commit, dispatch, getters }, list) => {
commit(types.RECEIVE_ADD_LIST_SUCCESS, updateListPosition(list));
+ dispatch('fetchItemsForList', {
+ listId: getters.getListByTitle(ListTypeTitles.backlog).id,
+ });
},
fetchLabels: ({ state, commit, getters }, searchTerm) => {
@@ -258,7 +263,7 @@ export default {
commit(types.TOGGLE_LIST_COLLAPSED, { listId, collapsed });
},
- removeList: ({ state: { issuableType, boardLists }, commit }, listId) => {
+ removeList: ({ state: { issuableType, boardLists }, commit, dispatch, getters }, listId) => {
const listsBackup = { ...boardLists };
commit(types.REMOVE_LIST, listId);
@@ -278,6 +283,10 @@ export default {
}) => {
if (errors.length > 0) {
commit(types.REMOVE_LIST_FAILURE, listsBackup);
+ } else {
+ dispatch('fetchItemsForList', {
+ listId: getters.getListByTitle(ListTypeTitles.backlog).id,
+ });
}
},
)
@@ -287,6 +296,9 @@ export default {
},
fetchItemsForList: ({ state, commit }, { listId, fetchNext = false }) => {
+ if (!fetchNext) {
+ commit(types.RESET_ITEMS_FOR_LIST, listId);
+ }
commit(types.REQUEST_ITEMS_FOR_LIST, { listId, fetchNext });
const { fullPath, fullBoardId, boardType, filterParams } = state;
@@ -298,7 +310,7 @@ export default {
filters: filterParams,
isGroup: boardType === BoardType.group,
isProject: boardType === BoardType.project,
- first: 20,
+ first: 10,
after: fetchNext ? state.pageInfoByListId[listId].endCursor : undefined,
};
@@ -465,32 +477,13 @@ export default {
});
},
- setActiveIssueMilestone: async ({ commit, getters }, input) => {
- const { activeBoardItem } = getters;
- const { data } = await gqlClient.mutate({
- mutation: issueSetMilestoneMutation,
- variables: {
- input: {
- iid: String(activeBoardItem.iid),
- milestoneId: getIdFromGraphQLId(input.milestoneId),
- projectPath: input.projectPath,
- },
- },
- });
-
- if (data.updateIssue.errors?.length > 0) {
- throw new Error(data.updateIssue.errors);
- }
-
- commit(types.UPDATE_BOARD_ITEM_BY_ID, {
- itemId: activeBoardItem.id,
- prop: 'milestone',
- value: data.updateIssue.issue.milestone,
+ addListItem: ({ commit }, { list, item, position, inProgress = false }) => {
+ commit(types.ADD_BOARD_ITEM_TO_LIST, {
+ listId: list.id,
+ itemId: item.id,
+ atIndex: position,
+ inProgress,
});
- },
-
- 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);
},
@@ -509,8 +502,8 @@ export default {
input.projectPath = fullPath;
}
- const placeholderIssue = formatIssue({ ...issueInput, id: placeholderId });
- dispatch('addListItem', { list, item: placeholderIssue, position: 0 });
+ const placeholderIssue = formatIssue({ ...issueInput, id: placeholderId, isLoading: true });
+ dispatch('addListItem', { list, item: placeholderIssue, position: 0, inProgress: true });
gqlClient
.mutate({
@@ -565,30 +558,6 @@ export default {
});
},
- setActiveIssueDueDate: async ({ commit, getters }, input) => {
- const { activeBoardItem } = getters;
- const { data } = await gqlClient.mutate({
- mutation: issueSetDueDateMutation,
- variables: {
- input: {
- iid: String(activeBoardItem.iid),
- projectPath: input.projectPath,
- dueDate: input.dueDate,
- },
- },
- });
-
- if (data.updateIssue?.errors?.length > 0) {
- throw new Error(data.updateIssue.errors);
- }
-
- commit(types.UPDATE_BOARD_ITEM_BY_ID, {
- itemId: activeBoardItem.id,
- prop: 'dueDate',
- value: data.updateIssue.issue.dueDate,
- });
- },
-
setActiveItemSubscribed: async ({ commit, getters, state }, input) => {
const { activeBoardItem, isEpicBoard } = getters;
const { fullPath, issuableType } = state;
@@ -721,7 +690,7 @@ export default {
}
},
- setError: ({ commit }, { message, error, captureError = false }) => {
+ setError: ({ commit }, { message, error, captureError = true }) => {
commit(types.SET_ERROR, message);
if (captureError) {
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index ccea2917c2c..38c54bc8c5d 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -15,6 +15,7 @@ export const UPDATE_LIST_FAILURE = 'UPDATE_LIST_FAILURE';
export const TOGGLE_LIST_COLLAPSED = 'TOGGLE_LIST_COLLAPSED';
export const REMOVE_LIST = 'REMOVE_LIST';
export const REMOVE_LIST_FAILURE = 'REMOVE_LIST_FAILURE';
+export const RESET_ITEMS_FOR_LIST = 'RESET_ITEMS_FOR_LIST';
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';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 667628b2998..6cd0a62657e 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -117,6 +117,11 @@ export default {
state.boardLists = listsBackup;
},
+ [mutationTypes.RESET_ITEMS_FOR_LIST]: (state, listId) => {
+ Vue.set(state, 'backupItemsList', state.boardItemsByListId[listId]);
+ Vue.set(state.boardItemsByListId, listId, []);
+ },
+
[mutationTypes.REQUEST_ITEMS_FOR_LIST]: (state, { listId, fetchNext }) => {
Vue.set(state.listsFlags, listId, { [fetchNext ? 'isLoadingMore' : 'isLoading']: true });
},
@@ -138,6 +143,7 @@ export default {
'Boards|An error occurred while fetching the board issues. Please reload the page.',
);
Vue.set(state.listsFlags, listId, { isLoading: false, isLoadingMore: false });
+ Vue.set(state.boardItemsByListId, listId, state.backupItemsList);
},
[mutationTypes.RESET_ISSUES]: (state) => {
@@ -166,8 +172,9 @@ export default {
[mutationTypes.ADD_BOARD_ITEM_TO_LIST]: (
state,
- { itemId, listId, moveBeforeId, moveAfterId, atIndex },
+ { itemId, listId, moveBeforeId, moveAfterId, atIndex, inProgress = false },
) => {
+ Vue.set(state.listsFlags, listId, { ...state.listsFlags, addItemToListInProgress: inProgress });
addItemToList({ state, listId, itemId, moveBeforeId, moveAfterId, atIndex });
},
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index 19ba2a5df83..7be5ae8b583 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -11,6 +11,7 @@ export default () => ({
boardLists: {},
listsFlags: {},
boardItemsByListId: {},
+ backupItemsList: [],
isSettingAssignees: false,
pageInfoByListId: {},
boardItems: {},