Commit 0a315e69 authored by Simon Knox's avatar Simon Knox

Merge branch '248908-clean-up-graphql-boards-ff' into 'master'

Clean up :graphql_board_list feature flag [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!67815
parents 45150e27 cfccf903
......@@ -2,9 +2,6 @@
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';
export default {
components: {
......@@ -24,7 +21,7 @@ export default {
},
computed: {
...mapState(['labels', 'labelsLoading']),
...mapGetters(['getListByLabelId', 'shouldUseGraphQL']),
...mapGetters(['getListByLabelId']),
columnForSelected() {
return this.getListByLabelId(this.selectedId);
},
......@@ -34,17 +31,6 @@ export default {
},
methods: {
...mapActions(['createList', 'fetchLabels', 'highlightList', 'setAddColumnFormVisibility']),
highlight(listId) {
if (this.shouldUseGraphQL) {
this.highlightList(listId);
} else {
const list = boardsStore.state.lists.find(({ id }) => id === listId);
list.highlighted = true;
setTimeout(() => {
list.highlighted = false;
}, 2000);
}
},
addList() {
if (!this.selectedLabel) {
return;
......@@ -54,23 +40,11 @@ export default {
if (this.columnForSelected) {
const listId = this.columnForSelected.id;
this.highlight(listId);
this.highlightList(listId);
return;
}
if (this.shouldUseGraphQL) {
this.createList({ labelId: this.selectedId });
} else {
const listObj = {
labelId: getIdFromGraphQLId(this.selectedId),
title: this.selectedLabel.title,
position: boardsStore.state.lists.length - 2,
list_type: ListType.label,
label: this.selectedLabel,
};
boardsStore.new(listObj);
}
this.createList({ labelId: this.selectedId });
},
filterItems(searchTerm) {
......
......@@ -62,17 +62,7 @@ export default {
// Don't do anything if this happened on a no trigger element
if (e.target.classList.contains('js-no-trigger')) return;
if (this.glFeatures.graphqlBoardLists || this.isSwimlanesOn) {
this.setActiveId({ id: this.issue.id, sidebarType: ISSUABLE });
return;
}
const isMultiSelect = e.ctrlKey || e.metaKey;
if (this.showDetail || isMultiSelect) {
this.showDetail = false;
this.$emit('show', { event: e, isMultiSelect });
}
this.setActiveId({ id: this.issue.id, sidebarType: ISSUABLE });
},
},
};
......
......@@ -5,24 +5,20 @@ import Draggable from 'vuedraggable';
import { mapState, mapGetters, mapActions } from 'vuex';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import defaultSortableConfig from '~/sortable/sortable_config';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DraggableItemTypes } from '../constants';
import BoardColumn from './board_column.vue';
import BoardColumnDeprecated from './board_column_deprecated.vue';
export default {
draggableItemTypes: DraggableItemTypes,
components: {
BoardAddNewColumn,
BoardColumn,
BoardColumnDeprecated,
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: {
......@@ -37,20 +33,15 @@ export default {
},
computed: {
...mapState(['boardLists', 'error', 'addColumnForm']),
...mapGetters(['isSwimlanesOn', 'isEpicBoard']),
useNewBoardColumnComponent() {
return this.glFeatures.graphqlBoardLists || this.isSwimlanesOn || this.isEpicBoard;
},
...mapGetters(['isSwimlanesOn', 'isEpicBoard', 'isIssueBoard']),
addColumnFormVisible() {
return this.addColumnForm?.visible;
},
boardListsToUse() {
return this.useNewBoardColumnComponent
? sortBy([...Object.values(this.boardLists)], 'position')
: this.lists;
return sortBy([...Object.values(this.boardLists)], 'position');
},
canDragColumns() {
return (this.isEpicBoard || this.glFeatures.graphqlBoardLists) && this.canAdminList;
return this.canAdminList;
},
boardColumnWrapper() {
return this.canDragColumns ? Draggable : 'div';
......@@ -68,9 +59,6 @@ export default {
return this.canDragColumns ? options : {};
},
boardColumnComponent() {
return this.useNewBoardColumnComponent ? BoardColumn : BoardColumnDeprecated;
},
},
methods: {
...mapActions(['moveList', 'unsetError']),
......@@ -95,8 +83,7 @@ export default {
class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
@end="moveList"
>
<component
:is="boardColumnComponent"
<board-column
v-for="(list, index) in boardListsToUse"
:key="index"
ref="board"
......@@ -118,10 +105,7 @@ export default {
:disabled="disabled"
/>
<board-content-sidebar
v-if="isSwimlanesOn || glFeatures.graphqlBoardLists"
data-testid="issue-boards-sidebar"
/>
<board-content-sidebar v-if="isIssueBoard" data-testid="issue-boards-sidebar" />
<epic-board-content-sidebar v-else-if="isEpicBoard" data-testid="epic-boards-sidebar" />
</div>
......
......@@ -31,20 +31,13 @@ export default {
};
},
computed: {
...mapGetters(['isSidebarOpen', 'shouldUseGraphQL', 'isEpicBoard']),
...mapGetters(['isSidebarOpen', 'isEpicBoard']),
...mapState(['activeId', 'sidebarType', 'boardLists']),
isWipLimitsOn() {
return this.glFeatures.wipLimits && !this.isEpicBoard;
},
activeList() {
/*
Warning: Though a computed property it is not reactive because we are
referencing a List Model class. Reactivity only applies to plain JS objects
*/
if (this.shouldUseGraphQL || this.isEpicBoard) {
return this.boardLists[this.activeId];
}
return boardsStore.state.lists.find(({ id }) => id === this.activeId);
return this.boardLists[this.activeId];
},
activeListLabel() {
return this.activeList.label;
......@@ -73,12 +66,8 @@ export default {
deleteBoard() {
// 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();
}
this.track('click_button', { label: 'remove_list' });
this.removeList(this.activeId);
this.unsetActiveId();
}
},
......
......@@ -4,7 +4,6 @@ import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable
import { updateHistory } from '~/lib/utils/url_utility';
import FilteredSearchContainer from '../filtered_search/container';
import vuexstore from './stores';
import boardsStore from './stores/boards_store';
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
......@@ -26,7 +25,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
this.cantEdit = cantEdit.filter((i) => typeof i === 'string');
this.cantEditWithValue = cantEdit.filter((i) => typeof i === 'object');
if (vuexstore.getters.shouldUseGraphQL && vuexstore.state.boardConfig) {
if (vuexstore.state.boardConfig) {
const boardConfigPath = transformBoardConfig(vuexstore.state.boardConfig);
// TODO Refactor: https://gitlab.com/gitlab-org/gitlab/-/issues/329274
// here we are using "window.location.search" as a temporary store
......@@ -45,14 +44,10 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
const groupByParam = new URLSearchParams(window.location.search).get('group_by');
this.store.path = `${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`;
if (vuexstore.getters.shouldUseGraphQL) {
updateHistory({
url: `?${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`,
});
vuexstore.dispatch('performSearch');
} else if (this.updateUrl) {
boardsStore.updateFiltersUrl();
}
updateHistory({
url: `?${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`,
});
vuexstore.dispatch('performSearch');
}
removeTokens() {
......
......@@ -2,7 +2,7 @@ import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import PortalVue from 'portal-vue';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapActions, mapGetters } from 'vuex';
import { mapActions } from 'vuex';
import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list';
......@@ -78,10 +78,7 @@ export default () => {
initBoardsFilteredSearch(apolloProvider);
}
if (!gon?.features?.graphqlBoardLists) {
boardsStore.create();
boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours);
}
boardsStore.create();
// eslint-disable-next-line @gitlab/no-runtime-template-compiler
issueBoardsApp = new Vue({
......@@ -133,7 +130,6 @@ export default () => {
};
},
computed: {
...mapGetters(['shouldUseGraphQL']),
detailIssueVisible() {
return Object.keys(this.detailIssue.issue).length;
},
......@@ -174,14 +170,12 @@ export default () => {
eventHub.$on('newDetailIssue', this.updateDetailIssue);
eventHub.$on('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
eventHub.$on('initialBoardLoad', this.initialBoardLoad);
},
beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens);
eventHub.$off('newDetailIssue', this.updateDetailIssue);
eventHub.$off('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
eventHub.$off('initialBoardLoad', this.initialBoardLoad);
},
mounted() {
if (!gon?.features?.issueBoardsFilteredSearch) {
......@@ -196,28 +190,9 @@ export default () => {
this.performSearch();
boardsStore.disabled = this.disabled;
if (!this.shouldUseGraphQL) {
this.initialBoardLoad();
}
},
methods: {
...mapActions(['setInitialBoardData', 'performSearch', 'setError']),
initialBoardLoad() {
boardsStore
.all()
.then((res) => res.data)
.then((lists) => {
lists.forEach((list) => boardsStore.addList(list));
this.loading = false;
})
.catch((error) => {
this.setError({
error,
message: __('An error occurred while fetching the board lists. Please try again.'),
});
});
},
updateTokens() {
this.filterManager.updateTokens();
},
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapGetters } from 'vuex';
import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue';
import BoardsSelectorDeprecated from '~/boards/components/boards_selector_deprecated.vue';
import store from '~/boards/stores';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
Vue.use(VueApollo);
......@@ -25,9 +22,7 @@ export default (params = {}) => {
el: boardsSwitcherElement,
components: {
BoardsSelector,
BoardsSelectorDeprecated,
},
mixins: [glFeatureFlagMixin()],
apolloProvider,
store,
provide: {
......@@ -52,16 +47,8 @@ export default (params = {}) => {
return { boardsSelectorProps };
},
computed: {
...mapGetters(['shouldUseGraphQL', 'isEpicBoard']),
},
render(createElement) {
if (this.shouldUseGraphQL || this.isEpicBoard) {
return createElement(BoardsSelector, {
props: this.boardsSelectorProps,
});
}
return createElement(BoardsSelectorDeprecated, {
return createElement(BoardsSelector, {
props: this.boardsSelectorProps,
});
},
......
......@@ -82,11 +82,8 @@ export default {
'setFilters',
convertObjectPropsToCamelCase(queryToObject(window.location.search, { gatherArrays: true })),
);
if (gon.features.graphqlBoardLists) {
dispatch('fetchLists');
dispatch('resetIssues');
}
dispatch('fetchLists');
dispatch('resetIssues');
},
fetchLists: ({ commit, state, dispatch }) => {
......@@ -182,7 +179,7 @@ export default {
});
},
fetchLabels: ({ state, commit, getters }, searchTerm) => {
fetchLabels: ({ state, commit }, searchTerm) => {
const { fullPath, boardType } = state;
const variables = {
......@@ -200,14 +197,7 @@ export default {
variables,
})
.then(({ data }) => {
let labels = data[boardType]?.labels.nodes;
if (!getters.shouldUseGraphQL && !getters.isEpicBoard) {
labels = labels.map((label) => ({
...label,
id: getIdFromGraphQLId(label.id),
}));
}
const labels = data[boardType]?.labels.nodes;
commit(types.RECEIVE_LABELS_SUCCESS, labels);
return labels;
......
......@@ -51,8 +51,4 @@ export default {
isEpicBoard: () => {
return false;
},
shouldUseGraphQL: () => {
return gon?.features?.graphqlBoardLists;
},
};
......@@ -7,7 +7,6 @@ class Groups::BoardsController < Groups::ApplicationController
before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: :yaml)
push_frontend_feature_flag(:issue_boards_filtered_search, group, default_enabled: :yaml)
push_frontend_feature_flag(:board_multi_select, group, default_enabled: :yaml)
push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml)
......
......@@ -8,7 +8,6 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:swimlanes_buffered_rendering, project, default_enabled: :yaml)
push_frontend_feature_flag(:graphql_board_lists, project, default_enabled: :yaml)
push_frontend_feature_flag(:issue_boards_filtered_search, project, default_enabled: :yaml)
push_frontend_feature_flag(:board_multi_select, project, default_enabled: :yaml)
push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml)
......
- board = local_assigns.fetch(:board, nil)
- group = local_assigns.fetch(:group, false)
- @no_breadcrumb_container = true
- @no_container = true
- @content_wrapper_class = "#{@content_wrapper_class} gl-relative"
......@@ -20,6 +19,4 @@
= render 'shared/issuable/search_bar', type: :boards, board: board
#board-app.boards-app.position-relative{ "v-cloak" => "true", data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
%board-content{ ":lists" => "state.lists", ":disabled" => "disabled" }
- if !is_epic_board && !Feature.enabled?(:graphql_board_lists, default_enabled: :yaml)
= render "shared/boards/components/sidebar", group: group
%board-settings-sidebar
%board-sidebar{ "inline-template" => true, ":current-user" => (UserSerializer.new.represent(current_user) || {}).to_json }
%transition{ name: "boards-sidebar-slide" }
%aside.right-sidebar.right-sidebar-expanded.boards-sidebar{ "v-show" => "showSidebar", 'aria-label': s_('Boards|Board'), 'data-testid': 'issue-boards-sidebar' }
.issuable-sidebar
.block.issuable-sidebar-header.position-relative
%span.issuable-header-text.hide-collapsed.float-left
%strong.bold
{{ issue.title }}
%br/
%span
= render_if_exists "shared/boards/components/sidebar/issue_project_path"
= precede "#" do
{{ issue.iid }}
%a.gutter-toggle.position-absolute.position-top-0.position-right-0{ role: "button",
href: "#",
"@click.prevent" => "closeSidebar",
"aria-label" => "Toggle sidebar" }
= custom_icon("icon_close", size: 15)
.js-issuable-update
= render "shared/boards/components/sidebar/assignee"
= render_if_exists "shared/boards/components/sidebar/epic"
= render "shared/boards/components/sidebar/milestone"
= render "shared/boards/components/sidebar/time_tracker"
= render "shared/boards/components/sidebar/due_date"
= render "shared/boards/components/sidebar/labels"
= render_if_exists "shared/boards/components/sidebar/weight"
= render "shared/boards/components/sidebar/notifications"
---
name: graphql_board_lists
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37905
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/248908
milestone: '13.4'
type: development
group: group::project management
default_enabled: true
......@@ -229,8 +229,7 @@ and vice versa.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285074) in GitLab 13.9.
> - [Deployed behind a feature flag](../feature_flags.md), enabled by default.
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/248908) in GitLab 14.1
> - Recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-graphql-based-issue-boards). **(FREE SELF)**
> - [Feature flag `graphql_board_lists`](https://gitlab.com/gitlab-org/gitlab/-/issues/248908) removed in GitLab 14.3
There can be
[risks when disabling released features](../../administration/feature_flags.md#risks-when-disabling-released-features).
......@@ -673,24 +672,6 @@ A few things to remember:
by default. If you have more than 20 issues, start scrolling down and the next
20 appear.
### Enable or disable GraphQL-based issue boards **(FREE SELF)**
It is deployed behind a feature flag that is **enabled by default** as of GitLab 14.1.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can disable it.
To enable it:
```ruby
Feature.enable(:graphql_board_lists)
```
To disable it:
```ruby
Feature.disable(:graphql_board_lists)
```
### Enable or disable iteration lists in boards **(PREMIUM SELF)**
The iteration list is under development but ready for production use. It is
......
......@@ -11,8 +11,6 @@ import {
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';
import { __ } from '~/locale';
......@@ -87,7 +85,7 @@ export default {
'assignees',
'assigneesLoading',
]),
...mapGetters(['getListByTypeId', 'shouldUseGraphQL', 'isEpicBoard']),
...mapGetters(['getListByTypeId', 'isEpicBoard']),
info() {
return listTypeInfo[this.columnType] || {};
......@@ -132,16 +130,10 @@ export default {
return false;
}
if (this.shouldUseGraphQL || this.isEpicBoard) {
const key = `${this.columnType}Id`;
return this.getListByTypeId({
[key]: this.selectedId,
});
}
return boardsStore.state.lists.find(
(list) => list[this.columnType]?.id === getIdFromGraphQLId(this.selectedId),
);
const key = `${this.columnType}Id`;
return this.getListByTypeId({
[key]: this.selectedId,
});
},
loading() {
......@@ -187,17 +179,6 @@ export default {
'fetchIterations',
'fetchMilestones',
]),
highlight(listId) {
if (this.shouldUseGraphQL || this.isEpicBoard) {
this.highlightList(listId);
} else {
const list = boardsStore.state.lists.find(({ id }) => id === listId);
list.highlighted = true;
setTimeout(() => {
list.highlighted = false;
}, 2000);
}
},
addList() {
if (!this.selectedItem) {
return;
......@@ -207,45 +188,12 @@ export default {
if (this.columnForSelected) {
const listId = this.columnForSelected.id;
this.highlight(listId);
this.highlightList(listId);
return;
}
if (this.shouldUseGraphQL || this.isEpicBoard) {
// eslint-disable-next-line @gitlab/require-i18n-strings
this.createList({ [`${this.columnType}Id`]: this.selectedId });
} else {
const { length } = boardsStore.state.lists;
const position = this.hideClosed ? length - 1 : length - 2;
const listObj = {
// eslint-disable-next-line @gitlab/require-i18n-strings
[`${this.columnType}Id`]: getIdFromGraphQLId(this.selectedId),
title: this.selectedItem.title,
position,
list_type: this.columnType,
};
if (this.labelTypeSelected) {
listObj.label = this.selectedItem;
} else if (this.milestoneTypeSelected) {
listObj.milestone = {
...this.selectedItem,
id: getIdFromGraphQLId(this.selectedItem.id),
};
} else if (this.iterationTypeSelected) {
listObj.iteration = {
...this.selectedItem,
id: getIdFromGraphQLId(this.selectedItem.id),
};
} else if (this.assigneeTypeSelected) {
listObj.assignee = {
...this.selectedItem,
id: getIdFromGraphQLId(this.selectedItem.id),
};
}
boardsStore.new(listObj);
}
// eslint-disable-next-line @gitlab/require-i18n-strings
this.createList({ [`${this.columnType}Id`]: this.selectedId });
},
filterItems(searchTerm) {
......
<script>
import { GlButton, GlFormInput } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import boardsStoreEE from 'ee/boards/stores/boards_store_ee';
import { inactiveId } from '~/boards/constants';
import { mapActions, mapState } from 'vuex';
import { __, n__ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
......@@ -36,7 +34,6 @@ export default {
},
computed: {
...mapState(['activeId']),
...mapGetters(['shouldUseGraphQL']),
wipLimitTypeText() {
return n__('%d issue', '%d issues', this.maxIssueCount);
},
......@@ -76,11 +73,6 @@ export default {
const id = this.activeId;
this.updateListWipLimit({ maxIssueCount: wipLimit, listId: id })
.then(() => {
if (!this.shouldUseGraphQL) {
boardsStoreEE.setMaxIssueCountOnList(id, wipLimit);
}
})
.catch(() => {
this.unsetActiveId();
this.setError({
......@@ -96,11 +88,6 @@ export default {
},
clearWipLimit() {
this.updateListWipLimit({ maxIssueCount: 0, listId: this.activeId })
.then(() => {
if (!this.shouldUseGraphQL) {
boardsStoreEE.setMaxIssueCountOnList(this.activeId, inactiveId);
}
})
.catch(() => {
this.unsetActiveId();
this.setError({
......
......@@ -65,19 +65,12 @@ export default Vue.extend({
return list;
},
handleItemClick(item) {
if (
this.vuexStore.getters.shouldUseGraphQL &&
!this.vuexStore.getters.getListByTitle(item.title)
) {
if (!this.vuexStore.getters.getListByTitle(item.title)) {
if (this.listType === 'milestones') {
this.vuexStore.dispatch('createList', { milestoneId: fullMilestoneId(item.id) });
} else if (this.listType === 'assignees') {
this.vuexStore.dispatch('createList', { assigneeId: fullUserId(item.id) });
}
} else if (!this.store.findList('title', item.title)) {
const list = this.prepareListObject(item);
this.store.new(list);
}
},
},
......
......@@ -6,15 +6,12 @@ import {
filterVariables,
} from '~/boards/boards_util';
import { BoardType } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import groupBoardMembersQuery from '~/boards/graphql/group_board_members.query.graphql';
import listsIssuesQuery from '~/boards/graphql/lists_issues.query.graphql';
import projectBoardMembersQuery from '~/boards/graphql/project_board_members.query.graphql';
import actionsCE, { gqlClient } from '~/boards/stores/actions';
import boardsStore from '~/boards/stores/boards_store';
import * as typesCE from '~/boards/stores/mutation_types';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils';
import { historyPushState, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { mergeUrlParams, removeParams, queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
......@@ -39,7 +36,6 @@ import projectBoardIterationsQuery from '../graphql/project_board_iterations.que
import updateBoardEpicUserPreferencesMutation from '../graphql/update_board_epic_user_preferences.mutation.graphql';
import updateEpicLabelsMutation from '../graphql/update_epic_labels.mutation.graphql';
import boardsStoreEE from './boards_store_ee';
import * as types from './mutation_types';
const fetchAndFormatListIssues = (state, extraVariables) => {
......@@ -121,13 +117,11 @@ export default {
if (getters.isSwimlanesOn) {
dispatch('resetEpics');
dispatch('resetIssues');
dispatch('fetchEpicsSwimlanes');
dispatch('fetchLists');
} else if (gon.features.graphqlBoardLists || getters.isEpicBoard) {
dispatch('fetchLists');
dispatch('resetIssues');
}
dispatch('fetchLists');
dispatch('resetIssues');
},
fetchEpicsSwimlanes({ state, commit }, { fetchNext = false } = {}) {
......@@ -221,38 +215,30 @@ export default {
commit(types.SET_SHOW_LABELS, val);
},
updateListWipLimit({ commit, getters, dispatch }, { maxIssueCount, listId }) {
if (getters.shouldUseGraphQL) {
return gqlClient
.mutate({
mutation: listUpdateLimitMetricsMutation,
variables: {
input: {
listId,
maxIssueCount,
},
updateListWipLimit({ commit, dispatch }, { maxIssueCount, listId }) {
return gqlClient
.mutate({
mutation: listUpdateLimitMetricsMutation,
variables: {
input: {
listId,
maxIssueCount,
},
})
.then(({ data }) => {
if (data?.boardListUpdateLimitMetrics?.errors.length) {
throw new Error();
}
},
})
.then(({ data }) => {
if (data?.boardListUpdateLimitMetrics?.errors.length) {
throw new Error();
}
commit(types.UPDATE_LIST_SUCCESS, {
listId,
list: data.boardListUpdateLimitMetrics?.list,
});
})
.catch(() => {
dispatch('handleUpdateListFailure');
commit(types.UPDATE_LIST_SUCCESS, {
listId,
list: data.boardListUpdateLimitMetrics?.list,
});
}
return axios.put(`${boardsStoreEE.store.state.endpoints.listsEndpoint}/${listId}`, {
list: {
max_issue_count: maxIssueCount,
},
});
})
.catch(() => {
dispatch('handleUpdateListFailure');
});
},
fetchItemsForList: (
......@@ -316,10 +302,6 @@ export default {
);
dispatch('fetchEpicsSwimlanes');
dispatch('fetchLists');
} else if (!gon.features.graphqlBoardLists) {
historyPushState(removeParams(['group_by']), window.location.href, true);
boardsStore.create();
eventHub.$emit('initialBoardLoad');
} else {
historyPushState(removeParams(['group_by']), window.location.href, true);
}
......
......@@ -57,9 +57,6 @@ class BoardsStoreEE {
this.store.scopedLabels = {
enabled: parseBoolean(scopedLabels),
};
if (!gon.features.graphqlBoardLists) {
this.initBoardFilters();
}
}
};
......
......@@ -54,8 +54,4 @@ export default {
isEpicBoard: (state) => {
return state.issuableType === issuableTypes.epic;
},
shouldUseGraphQL: (state) => {
return state.isShowingEpicsSwimlanes || gon?.features?.graphqlBoardLists;
},
};
......@@ -61,13 +61,4 @@ RSpec.describe 'Multiple Issue Boards', :js do
it_behaves_like 'multiple issue boards'
end
context 'when graphql_board_lists FF disabled' do
before do
stub_feature_flags(graphql_board_lists: false)
stub_licensed_features(multiple_group_issue_boards: true)
end
it_behaves_like 'multiple issue boards'
end
end
# frozen_string_literal: true
# To be removed as :graphql_board_lists gets removed
# https://gitlab.com/gitlab-org/gitlab/-/issues/248908
require 'spec_helper'
RSpec.describe 'label issues', :js do
include BoardHelpers
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, namespace: group) }
let(:board) { create(:board, group: group) }
let!(:development) { create(:label, project: project, name: 'Development') }
let!(:issue) { create(:labeled_issue, project: project, labels: [development]) }
let!(:list) { create(:list, board: board, label: development, position: 0) }
before do
stub_licensed_features(multiple_group_issue_boards: true)
# stubbing until sidebar work is done: https://gitlab.com/gitlab-org/gitlab/-/issues/230711
stub_feature_flags(graphql_board_lists: false)
group.add_maintainer(user)
sign_in(user)
visit group_boards_path(group)
wait_for_requests
end
it 'adds a new group label from sidebar' do
card = find('.board:nth-child(2)').first('.board-card')
click_card(card)
page.within '.right-sidebar .labels' do
click_link 'Edit'
click_link 'Create group label'
fill_in 'new_label_name', with: 'test label'
first('.suggest-colors-dropdown a').click
# We need to hover before clicking to trigger
# dropdown repositioning so that the click isn't flaky
create_button = find_button('Create')
create_button.hover
create_button.click
end
page.within '.labels' do
expect(page).to have_link 'test label'
end
end
end
This diff is collapsed.
......@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe 'User adds milestone lists', :js do
using RSpec::Parameterized::TableSyntax
let_it_be(:group) { create(:group, :nested) }
let_it_be(:project) { create(:project, :public, namespace: group) }
let_it_be(:group_board) { create(:board, group: group) }
......@@ -25,11 +23,8 @@ RSpec.describe 'User adds milestone lists', :js do
group.add_owner(user)
end
where(:board_type, :graphql_board_lists_enabled) do
:project | true
:project | false
:group | true
:group | false
where(:board_type) do
[[:project], [:group]]
end
with_them do
......@@ -43,10 +38,6 @@ RSpec.describe 'User adds milestone lists', :js do
set_cookie('sidebar_collapsed', 'true')
stub_feature_flags(
graphql_board_lists: graphql_board_lists_enabled
)
if board_type == :project
visit project_board_path(project, project_board)
elsif board_type == :group
......
......@@ -145,14 +145,6 @@ RSpec.describe 'Filter issues by iteration', :js do
let(:issue_title_selector) { '.board-card .board-card-title' }
it_behaves_like 'filters by iteration'
context 'when graphql_board_lists is disabled' do
before do
stub_feature_flags(graphql_board_lists: false)
end
it_behaves_like 'filters by iteration'
end
end
context 'group board' do
......
......@@ -15,7 +15,6 @@ Vue.use(Vuex);
describe('BoardAddNewColumn', () => {
let wrapper;
let shouldUseGraphQL;
const selectItem = (id) => {
wrapper.findByTestId('selectItem').vm.$emit('change', id);
......@@ -59,7 +58,6 @@ describe('BoardAddNewColumn', () => {
...actions,
},
getters: {
shouldUseGraphQL: () => shouldUseGraphQL,
getListByTypeId: () => getListByTypeId,
isEpicBoard: () => false,
},
......@@ -103,10 +101,6 @@ describe('BoardAddNewColumn', () => {
radio.vm.$emit('change', type);
};
beforeEach(() => {
shouldUseGraphQL = true;
});
it('clicking cancel hides the form', () => {
const setAddColumnFormVisibility = jest.fn();
mountComponent({
......
import { shallowMount } from '@vue/test-utils';
import EpicBoardContentSidebar from 'ee/boards/components/epic_board_content_sidebar.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import { createStore } from '~/boards/stores';
......@@ -35,20 +36,22 @@ describe('ee/BoardContent', () => {
});
describe.each`
licenseEnabled | state | result
${true} | ${{ isShowingEpicsSwimlanes: true }} | ${true}
${true} | ${{ isShowingEpicsSwimlanes: false }} | ${false}
${false} | ${{ isShowingEpicsSwimlanes: true }} | ${false}
${false} | ${{ isShowingEpicsSwimlanes: false }} | ${false}
`('with licenseEnabled=$licenseEnabled and state=$state', ({ licenseEnabled, state, result }) => {
state | resultIssue | resultEpic
${{ isShowingEpicsSwimlanes: true, issuableType: 'issue' }} | ${true} | ${false}
${{ isShowingEpicsSwimlanes: false, issuableType: 'issue' }} | ${true} | ${false}
${{ isShowingEpicsSwimlanes: false, issuableType: 'epic' }} | ${false} | ${true}
`('with state=$state', ({ state, resultIssue, resultEpic }) => {
beforeEach(() => {
gon.licensed_features.swimlanes = licenseEnabled;
Object.assign(store.state, state);
createComponent();
});
it(`renders BoardContentSidebar = ${result}`, () => {
expect(wrapper.find(BoardContentSidebar).exists()).toBe(result);
it(`renders BoardContentSidebar = ${resultIssue}`, () => {
expect(wrapper.find(BoardContentSidebar).exists()).toBe(resultIssue);
});
it(`renders EpicBoardContentSidebar = ${resultEpic}`, () => {
expect(wrapper.find(EpicBoardContentSidebar).exists()).toBe(resultEpic);
});
});
});
......@@ -11,11 +11,6 @@ import axios from '~/lib/utils/axios_utils';
jest.mock('~/flash');
describe('BoardListSelector', () => {
global.gon.features = {
...(global.gon.features || {}),
graphqlBoardLists: false,
};
const dummyEndpoint = `${TEST_HOST}/users.json`;
const createComponent = () =>
......@@ -93,19 +88,7 @@ describe('BoardListSelector', () => {
});
describe('handleItemClick', () => {
it('graphqlBoardLists FF off - creates new list in a store instance', () => {
jest.spyOn(vm.store, 'new').mockReturnValue({});
const assignee = mockAssigneesList[0];
expect(vm.store.findList('title', assignee.name)).not.toBeDefined();
vm.handleItemClick(assignee);
expect(vm.store.new).toHaveBeenCalledWith(expect.any(Object));
});
it('graphqlBoardLists FF on - creates new list in a store instance', () => {
global.gon.features.graphqlBoardLists = true;
it('creates new list in a store instance', () => {
jest.spyOn(vm.vuexStore, 'dispatch').mockReturnValue({});
const assignee = mockAssigneesList[0];
......
import '~/boards/models/list';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import BoardSettingsListTypes from 'ee_component/boards/components/board_settings_list_types.vue';
import BoardSettingsWipLimit from 'ee_component/boards/components/board_settings_wip_limit.vue';
import { mockLabelList, mockMilestoneList } from 'jest/boards/mock_data';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import { LIST } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
import getters from '~/boards/stores/getters';
const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(Vuex);
describe('ee/BoardSettingsSidebar', () => {
let wrapper;
let storeActions;
const labelTitle = 'test';
const labelColor = '#FFFF';
const listId = 1;
let mock;
const createComponent = (actions = {}, isWipLimitsOn = false) => {
const createComponent = ({ actions = {}, isWipLimitsOn = false, list = {} }) => {
storeActions = actions;
const boardLists = {
[list.id]: { ...list, maxIssueCount: 0 },
};
const store = new Vuex.Store({
state: { sidebarType: LIST, activeId: listId },
state: { sidebarType: LIST, activeId: list.id, boardLists },
getters,
actions: storeActions,
});
wrapper = shallowMount(BoardSettingsSidebar, {
store,
localVue,
provide: {
glFeatures: {
wipLimits: isWipLimitsOn,
......@@ -47,41 +41,18 @@ describe('ee/BoardSettingsSidebar', () => {
});
};
beforeEach(() => {
mock = new MockAdapter(axios);
boardsStore.create();
});
afterEach(() => {
mock.restore();
wrapper.destroy();
});
it('confirms we render BoardSettingsSidebarWipLimit', () => {
boardsStore.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
max_issue_count: 0,
list_type: 'label',
});
createComponent({}, true);
createComponent({ list: mockLabelList, isWipLimitsOn: true });
expect(wrapper.find(BoardSettingsWipLimit).exists()).toBe(true);
});
it('confirms we render BoardSettingsListTypes', () => {
boardsStore.addList({
id: 1,
milestone: {
webUrl: 'https://gitlab.com/h5bp/html5-boilerplate/-/milestones/1',
title: 'Backlog',
},
max_issue_count: 1,
list_type: 'milestone',
});
createComponent();
createComponent({ list: mockMilestoneList });
expect(wrapper.find(BoardSettingsListTypes).exists()).toBe(true);
});
......
import '~/boards/models/list';
import { GlFormInput } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
import { noop } from 'lodash';
import Vue from 'vue';
import Vuex from 'vuex';
import BoardSettingsWipLimit from 'ee_component/boards/components/board_settings_wip_limit.vue';
import waitForPromises from 'helpers/wait_for_promises';
import boardsStore from '~/boards/stores/boards_store';
import { mockLabelList } from 'jest/boards/mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(Vuex);
describe('BoardSettingsWipLimit', () => {
let wrapper;
let storeActions;
const labelTitle = 'test';
const labelColor = '#FFFF';
const listId = 1;
const listId = mockLabelList.id;
const currentWipLimit = 1; // Needs to be other than null to trigger requests
let mock;
const addList = (maxIssueCount = 0) => {
boardsStore.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
max_issue_count: maxIssueCount,
list_type: 'label',
});
};
const clickEdit = () => wrapper.find('.js-edit-button').vm.$emit('click');
const findRemoveWipLimit = () => wrapper.find('.js-remove-limit');
const findWipLimit = () => wrapper.find('.js-wip-limit');
......@@ -46,13 +31,11 @@ describe('BoardSettingsWipLimit', () => {
const store = new Vuex.Store({
state: vuexState,
actions: storeActions,
getters: { shouldUseGraphQL: () => false },
});
wrapper = shallowMount(BoardSettingsWipLimit, {
propsData: props,
store,
localVue,
data() {
return localState;
},
......@@ -69,13 +52,7 @@ describe('BoardSettingsWipLimit', () => {
}
};
beforeEach(() => {
boardsStore.create();
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
jest.restoreAllMocks();
wrapper.destroy();
});
......@@ -83,25 +60,28 @@ describe('BoardSettingsWipLimit', () => {
describe('when activeList is present', () => {
describe('when activeListWipLimit is 0', () => {
it('renders "None" in the block', () => {
createComponent({ vuexState: { activeId: listId } });
createComponent({
vuexState: {
activeId: listId,
},
});
expect(findWipLimit().text()).toBe('None');
});
});
describe('when activeId is greater than 0', () => {
afterEach(() => {
boardsStore.removeList(listId);
});
describe('when activeListWipLimit is greater than 0', () => {
it.each`
num | expected
${1} | ${'1 issue'}
${11} | ${'11 issues'}
`('it renders $num', ({ num, expected }) => {
addList(4);
createComponent({ vuexState: { activeId: num }, props: { maxIssueCount: num } });
createComponent({
vuexState: {
activeId: listId,
},
props: { maxIssueCount: num },
});
expect(findWipLimit().text()).toBe(expected);
});
......@@ -112,7 +92,9 @@ describe('BoardSettingsWipLimit', () => {
const maxIssueCount = 4;
beforeEach(async () => {
createComponent({
vuexState: { activeId: listId },
vuexState: {
activeId: listId,
},
actions: { updateListWipLimit: noop },
props: { maxIssueCount },
});
......@@ -137,15 +119,14 @@ describe('BoardSettingsWipLimit', () => {
describe('remove limit', () => {
describe('when wipLimit is set', () => {
const spy = jest.fn().mockResolvedValue({
data: { boardListUpdateLimitMetrics: { list: { maxIssueCount: 0 } } },
});
beforeEach(() => {
addList(4);
const spy = jest.fn().mockResolvedValue({
config: { data: JSON.stringify({ list: { max_issue_count: 0 } }) },
});
createComponent({
vuexState: { activeId: listId },
vuexState: {
activeId: listId,
},
actions: { updateListWipLimit: spy },
props: { maxIssueCount: 4 },
});
......@@ -156,18 +137,22 @@ describe('BoardSettingsWipLimit', () => {
findRemoveWipLimit().vm.$emit('click');
await waitForPromises();
await wrapper.vm.$nextTick();
// WARNING: https://gitlab.com/gitlab-org/gitlab/-/issues/232573
expect(boardsStore.findList('id', listId).maxIssueCount).toBe(0);
expect(spy).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ listId, maxIssueCount: 0 }),
);
});
});
describe('when wipLimit is not set', () => {
beforeEach(() => {
addList();
createComponent({ vuexState: { activeId: listId }, actions: { updateListWipLimit: noop } });
createComponent({
vuexState: { activeId: listId },
actions: { updateListWipLimit: noop },
});
});
it('does not render the remove limit button', () => {
......@@ -177,14 +162,6 @@ describe('BoardSettingsWipLimit', () => {
});
describe('when edit is true', () => {
beforeEach(() => {
addList(2);
});
afterEach(() => {
boardsStore.removeList(listId);
});
describe.each`
blurMethod
${'enter'}
......@@ -193,10 +170,12 @@ describe('BoardSettingsWipLimit', () => {
describe(`when blur is triggered by ${blurMethod}`, () => {
it('calls updateListWipLimit', async () => {
const spy = jest.fn().mockResolvedValue({
config: { data: JSON.stringify({ list: { max_issue_count: '4' } }) },
data: { boardListUpdateLimitMetrics: { list: { maxIssueCount: 4 } } },
});
createComponent({
vuexState: { activeId: listId },
vuexState: {
activeId: listId,
},
actions: { updateListWipLimit: spy },
localState: { edit: true, currentWipLimit },
});
......@@ -209,10 +188,12 @@ describe('BoardSettingsWipLimit', () => {
});
describe('when component wipLimit and List.maxIssueCount are equal', () => {
it('doesnt call updateListWipLimit', async () => {
it('does not call updateListWipLimit', async () => {
const spy = jest.fn().mockResolvedValue({});
createComponent({
vuexState: { activeId: listId },
vuexState: {
activeId: listId,
},
actions: { updateListWipLimit: spy },
localState: { edit: true, currentWipLimit: 2 },
props: { maxIssueCount: 2 },
......@@ -227,7 +208,7 @@ describe('BoardSettingsWipLimit', () => {
});
describe('when currentWipLimit is null', () => {
it('doesnt call updateListWipLimit', async () => {
it('does not call updateListWipLimit', async () => {
const spy = jest.fn().mockResolvedValue({});
createComponent({
vuexState: { activeId: listId },
......@@ -249,9 +230,12 @@ describe('BoardSettingsWipLimit', () => {
beforeEach(() => {
const spy = jest.fn().mockResolvedValue({});
createComponent({
vuexState: { activeId: listId },
vuexState: {
activeId: listId,
},
actions: { updateListWipLimit: spy },
localState: { edit: true, currentWipLimit: maxIssueCount },
props: { maxIssueCount },
});
triggerBlur(blurMethod);
......@@ -260,14 +244,7 @@ describe('BoardSettingsWipLimit', () => {
});
it('sets activeWipLimit to new maxIssueCount value', () => {
/*
* DANGER: bad coupling to the computed prop of the component because the
* computed prop relys on the list from boardStore, for now this is the way around
* stale values from boardsStore being updated, when we move List and BoardsStore to Vuex
* or Graphql we will be able to query the DOM for the new value.
*/
expect(boardsStore.findList('id', 1).maxIssueCount).toBe(maxIssueCount);
expect(findWipLimit().text()).toContain(maxIssueCount);
});
it('toggles GlFormInput on blur', () => {
......
......@@ -112,12 +112,7 @@ describe('setFilters', () => {
});
describe('performSearch', () => {
it('should dispatch setFilters action', (done) => {
testAction(actions.performSearch, {}, {}, [], [{ type: 'setFilters', payload: {} }], done);
});
it('should dispatch setFilters, fetchLists and resetIssues action when graphqlBoardLists FF is on', async () => {
window.gon = { features: { graphqlBoardLists: true } };
it('should dispatch setFilters, fetchLists and resetIssues action', async () => {
const getters = { isSwimlanesOn: false };
await testAction({
......@@ -139,9 +134,9 @@ describe('performSearch', () => {
expectedActions: [
{ type: 'setFilters', payload: {} },
{ type: 'resetEpics' },
{ type: 'resetIssues' },
{ type: 'fetchEpicsSwimlanes' },
{ type: 'fetchLists' },
{ type: 'resetIssues' },
],
});
});
......@@ -464,7 +459,6 @@ describe('setShowLabels', () => {
describe('updateListWipLimit', () => {
let storeMock;
const getters = { shouldUseGraphQL: false };
beforeEach(() => {
storeMock = {
......@@ -483,26 +477,9 @@ describe('updateListWipLimit', () => {
jest.restoreAllMocks();
});
it('axios - should call the correct url', () => {
const maxIssueCount = 0;
const activeId = 1;
return actions
.updateListWipLimit({ state: { activeId }, getters }, { maxIssueCount, listId: activeId })
.then(() => {
expect(axios.put).toHaveBeenCalledWith(
`${boardsStoreEE.store.state.endpoints.listsEndpoint}/${activeId}`,
{
list: { max_issue_count: maxIssueCount },
},
);
});
});
it('graphql - commit UPDATE_LIST_SUCCESS mutation on success', () => {
it('commit UPDATE_LIST_SUCCESS mutation on success', () => {
const maxIssueCount = 0;
const activeId = 1;
getters.shouldUseGraphQL = true;
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
boardListUpdateLimitMetrics: {
......@@ -517,7 +494,7 @@ describe('updateListWipLimit', () => {
return testAction(
actions.updateListWipLimit,
{ maxIssueCount, listId: activeId },
{ isShowingEpicsSwimlanes: true, ...getters },
{ isShowingEpicsSwimlanes: true },
[
{
type: types.UPDATE_LIST_SUCCESS,
......@@ -533,16 +510,15 @@ describe('updateListWipLimit', () => {
);
});
it('graphql - dispatch handleUpdateListFailure on failure', () => {
it('dispatch handleUpdateListFailure on failure', () => {
const maxIssueCount = 0;
const activeId = 1;
getters.shouldUseGraphQL = true;
jest.spyOn(gqlClient, 'mutate').mockResolvedValue(Promise.reject());
return testAction(
actions.updateListWipLimit,
{ maxIssueCount, listId: activeId },
{ isShowingEpicsSwimlanes: true, ...getters },
{ isShowingEpicsSwimlanes: true },
[],
[{ type: 'handleUpdateListFailure' }],
);
......
......@@ -3626,9 +3626,6 @@ msgstr ""
msgid "An error occurred while fetching terraform reports."
msgstr ""
msgid "An error occurred while fetching the board lists. Please try again."
msgstr ""
msgid "An error occurred while fetching the job log."
msgstr ""
......@@ -5513,9 +5510,6 @@ msgid_plural "Boards|Blocked by %{blockedByCount} %{issuableType}s"
msgstr[0] ""
msgstr[1] ""
msgid "Boards|Board"
msgstr ""
msgid "Boards|Collapse"
msgstr ""
......
......@@ -43,12 +43,12 @@ RSpec.describe 'Multi Select Issue', :js do
# Multi select drag&drop support is temporarily disabled
# https://gitlab.com/gitlab-org/gitlab/-/issues/289797
stub_feature_flags(graphql_board_lists: false, board_multi_select: project)
stub_feature_flags(board_multi_select: project)
sign_in(user)
end
context 'with lists' do
xcontext 'with lists' do
let(:label1) { create(:label, project: project, name: 'Label 1', description: 'Test') }
let(:label2) { create(:label, project: project, name: 'Label 2', description: 'Test') }
let!(:list1) { create(:list, board: board, label: label1, position: 0) }
......
......@@ -5,8 +5,9 @@ require 'spec_helper'
RSpec.describe 'Project issue boards sidebar labels', :js do
include BoardHelpers
let_it_be(:group) { create(:group, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:project) { create(:project, :public, namespace: group) }
let_it_be(:development) { create(:label, project: project, name: 'Development') }
let_it_be(:bug) { create(:label, project: project, name: 'Bug') }
let_it_be(:regression) { create(:label, project: project, name: 'Regression') }
......
......@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe 'User adds lists', :js do
using RSpec::Parameterized::TableSyntax
let_it_be(:group) { create(:group, :nested) }
let_it_be(:project) { create(:project, :public, namespace: group) }
let_it_be(:group_board) { create(:board, group: group) }
......@@ -27,11 +25,8 @@ RSpec.describe 'User adds lists', :js do
group.add_owner(user)
end
where(:board_type, :graphql_board_lists_enabled) do
:project | true
:project | false
:group | true
:group | false
where(:board_type) do
[[:project], [:group]]
end
with_them do
......@@ -40,10 +35,6 @@ RSpec.describe 'User adds lists', :js do
set_cookie('sidebar_collapsed', 'true')
stub_feature_flags(
graphql_board_lists: graphql_board_lists_enabled
)
if board_type == :project
visit project_board_path(project, project_board)
elsif board_type == :group
......@@ -53,14 +44,12 @@ RSpec.describe 'User adds lists', :js do
wait_for_all_requests
end
it 'creates new column for label containing labeled issue' do
it 'creates new column for label containing labeled issue', :aggregate_failures do
click_button 'Create list'
wait_for_all_requests
select_label(group_label)
wait_for_all_requests
expect(page).to have_selector('.board', text: group_label.title)
expect(find('.board:nth-child(2) .board-card')).to have_content(issue.title)
end
......
......@@ -42,30 +42,4 @@ RSpec.describe 'Group Issue Boards', :js do
end
end
end
context 'when graphql_board_lists FF disabled' do
before do
stub_feature_flags(graphql_board_lists: false)
sign_in(user)
visit group_board_path(group, board)
wait_for_requests
end
it 'only shows valid labels for the issue project and group' do
click_card(card)
page.within('.labels') do
click_link 'Edit'
wait_for_requests
page.within('.selectbox') do
expect(page).to have_content(project_1_label.title)
expect(page).to have_content(group_label.title)
expect(page).not_to have_content(project_2_label.title)
end
end
end
end
end
......@@ -214,44 +214,6 @@ RSpec.describe 'Labels Hierarchy', :js do
end
end
context 'issuable sidebar when graphql_board_lists FF disabled' do
let!(:issue) { create(:issue, project: project_1) }
before do
stub_feature_flags(graphql_board_lists: false)
end
context 'on project board issue sidebar' do
before do
project_1.add_developer(user)
board = create(:board, project: project_1)
visit project_board_path(project_1, board)
wait_for_requests
find('.board-card').click
end
it_behaves_like 'assigning labels from sidebar'
end
context 'on group board issue sidebar' do
before do
parent.add_developer(user)
board = create(:board, group: parent)
visit group_board_path(parent, board)
wait_for_requests
find('.board-card').click
end
it_behaves_like 'assigning labels from sidebar'
end
end
context 'issuable filtering' do
let!(:labeled_issue) { create(:labeled_issue, project: project_1, labels: [grandparent_group_label, parent_group_label, project_label_1]) }
let!(:issue) { create(:issue, project: project_1) }
......
......@@ -48,7 +48,6 @@ describe('Board card layout', () => {
...actions,
},
getters: {
shouldUseGraphQL: () => true,
getListByLabelId: () => getListByLabelId,
},
state: {
......
......@@ -8,7 +8,6 @@ import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import BoardCardDeprecated from '~/boards/components/board_card_deprecated.vue';
import issueCardInner from '~/boards/components/issue_card_inner_deprecated.vue';
import eventHub from '~/boards/eventhub';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import axios from '~/lib/utils/axios_utils';
......@@ -165,46 +164,9 @@ describe('BoardCard', () => {
expect(boardsStore.detail.issue).toEqual({});
});
it('sets detail issue to card issue on mouse up', () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
mountComponent();
wrapper.trigger('mousedown');
wrapper.trigger('mouseup');
expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', wrapper.vm.issue, false);
expect(boardsStore.detail.list).toEqual(wrapper.vm.list);
});
it('resets detail issue to empty if already set', () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
const [issue] = list.issues;
boardsStore.detail.issue = issue;
mountComponent();
wrapper.trigger('mousedown');
wrapper.trigger('mouseup');
expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', false);
});
});
describe('sidebarHub events', () => {
it('closes all sidebars before showing an issue if no issues are opened', () => {
jest.spyOn(sidebarEventHub, '$emit').mockImplementation(() => {});
boardsStore.detail.issue = {};
mountComponent();
// sets conditional so that event is emitted.
wrapper.trigger('mousedown');
wrapper.trigger('mouseup');
expect(sidebarEventHub.$emit).toHaveBeenCalledWith('sidebar.closeAll');
});
it('it does not closes all sidebars before showing an issue if an issue is opened', () => {
jest.spyOn(sidebarEventHub, '$emit').mockImplementation(() => {});
const [issue] = list.issues;
......
......@@ -111,18 +111,14 @@ describe('Board card layout', () => {
expect(wrapper.vm.showDetail).toBe(false);
});
it("calls 'setActiveId' when 'graphqlBoardLists' feature flag is turned on", async () => {
it("calls 'setActiveId'", async () => {
const setActiveId = jest.fn();
createStore({
actions: {
setActiveId,
},
});
mountComponent({
provide: {
glFeatures: { graphqlBoardLists: true },
},
});
mountComponent();
wrapper.trigger('mouseup');
await wrapper.vm.$nextTick();
......
......@@ -5,7 +5,7 @@ import Draggable from 'vuedraggable';
import Vuex from 'vuex';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import getters from 'ee_else_ce/boards/stores/getters';
import BoardColumnDeprecated from '~/boards/components/board_column_deprecated.vue';
import BoardColumn from '~/boards/components/board_column.vue';
import BoardContent from '~/boards/components/board_content.vue';
import { mockLists, mockListsWithModel } from '../mock_data';
......@@ -33,12 +33,7 @@ describe('BoardContent', () => {
});
};
const createComponent = ({
state,
props = {},
graphqlBoardListsEnabled = false,
canAdminList = true,
} = {}) => {
const createComponent = ({ state, props = {}, canAdminList = true } = {}) => {
const store = createStore({
...defaultState,
...state,
......@@ -51,63 +46,41 @@ describe('BoardContent', () => {
},
provide: {
canAdminList,
glFeatures: { graphqlBoardLists: graphqlBoardListsEnabled },
},
store,
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders a BoardColumnDeprecated component per list', () => {
createComponent();
expect(wrapper.findAllComponents(BoardColumnDeprecated)).toHaveLength(
mockListsWithModel.length,
);
it('renders a BoardColumn component per list', () => {
expect(wrapper.findAllComponents(BoardColumn)).toHaveLength(mockListsWithModel.length);
});
it('does not display EpicsSwimlanes component', () => {
createComponent();
expect(wrapper.find(EpicsSwimlanes).exists()).toBe(false);
expect(wrapper.find(GlAlert).exists()).toBe(false);
});
describe('graphqlBoardLists feature flag enabled', () => {
describe('can admin list', () => {
beforeEach(() => {
createComponent({ graphqlBoardListsEnabled: true });
gon.features = {
graphqlBoardLists: true,
};
createComponent({ canAdminList: true });
});
describe('can admin list', () => {
beforeEach(() => {
createComponent({ graphqlBoardListsEnabled: true, canAdminList: true });
});
it('renders draggable component', () => {
expect(wrapper.find(Draggable).exists()).toBe(true);
});
});
describe('can not admin list', () => {
beforeEach(() => {
createComponent({ graphqlBoardListsEnabled: true, canAdminList: false });
});
it('does not render draggable component', () => {
expect(wrapper.find(Draggable).exists()).toBe(false);
});
it('renders draggable component', () => {
expect(wrapper.find(Draggable).exists()).toBe(true);
});
});
describe('graphqlBoardLists feature flag disabled', () => {
describe('can not admin list', () => {
beforeEach(() => {
createComponent({ graphqlBoardListsEnabled: false });
createComponent({ canAdminList: false });
});
it('does not render draggable component', () => {
......
......@@ -2,7 +2,6 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
import { createStore } from '~/boards/stores';
import * as urlUtility from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
......@@ -44,6 +43,12 @@ describe('BoardFilteredSearch', () => {
];
const createComponent = ({ initialFilterParams = {} } = {}) => {
store = new Vuex.Store({
actions: {
performSearch: jest.fn(),
},
});
wrapper = shallowMount(BoardFilteredSearch, {
provide: { initialFilterParams, fullPath: '' },
store,
......@@ -55,22 +60,15 @@ describe('BoardFilteredSearch', () => {
const findFilteredSearch = () => wrapper.findComponent(FilteredSearchBarRoot);
beforeEach(() => {
// this needed for actions call for performSearch
window.gon = { features: {} };
});
afterEach(() => {
wrapper.destroy();
});
describe('default', () => {
beforeEach(() => {
store = createStore();
createComponent();
jest.spyOn(store, 'dispatch');
createComponent();
});
it('renders FilteredSearch', () => {
......@@ -103,8 +101,6 @@ describe('BoardFilteredSearch', () => {
describe('when searching', () => {
beforeEach(() => {
store = createStore();
createComponent();
jest.spyOn(wrapper.vm, 'performSearch').mockImplementation();
......@@ -133,11 +129,9 @@ describe('BoardFilteredSearch', () => {
describe('when url params are already set', () => {
beforeEach(() => {
store = createStore();
createComponent({ initialFilterParams: { authorUsername: 'root', labelName: ['label'] } });
jest.spyOn(store, 'dispatch');
createComponent({ initialFilterParams: { authorUsername: 'root', labelName: ['label'] } });
});
it('passes the correct props to FilterSearchBar', () => {
......
import '~/boards/models/list';
import { GlDrawer, GlLabel } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
import { MountingPortal } from 'portal-vue';
import Vue from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import { inactiveId, LIST } from '~/boards/constants';
import { createStore } from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import actions from '~/boards/stores/actions';
import getters from '~/boards/stores/getters';
import mutations from '~/boards/stores/mutations';
import sidebarEventHub from '~/sidebar/event_hub';
import { mockLabelList } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(Vuex);
describe('BoardSettingsSidebar', () => {
let wrapper;
let mock;
let store;
const labelTitle = 'test';
const labelColor = '#FFFF';
const listId = 1;
const labelTitle = mockLabelList.label.title;
const labelColor = mockLabelList.label.color;
const listId = mockLabelList.id;
const findRemoveButton = () => wrapper.findByTestId('remove-list');
const createComponent = ({ canAdminList = false } = {}) => {
const createComponent = ({
canAdminList = false,
list = {},
sidebarType = LIST,
activeId = inactiveId,
} = {}) => {
const boardLists = {
[listId]: list,
};
const store = new Vuex.Store({
state: { sidebarType, activeId, boardLists },
getters,
mutations,
actions,
});
wrapper = extendedWrapper(
shallowMount(BoardSettingsSidebar, {
store,
localVue,
provide: {
canAdminList,
},
......@@ -40,16 +51,10 @@ describe('BoardSettingsSidebar', () => {
const findLabel = () => wrapper.find(GlLabel);
const findDrawer = () => wrapper.find(GlDrawer);
beforeEach(() => {
store = createStore();
store.state.activeId = inactiveId;
store.state.sidebarType = LIST;
boardsStore.create();
});
afterEach(() => {
jest.restoreAllMocks();
wrapper.destroy();
wrapper = null;
});
it('finds a MountingPortal component', () => {
......@@ -100,86 +105,40 @@ describe('BoardSettingsSidebar', () => {
});
describe('when activeId is greater than zero', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
boardsStore.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
list_type: 'label',
});
store.state.activeId = 1;
store.state.sidebarType = LIST;
});
afterEach(() => {
boardsStore.removeList(listId);
});
it('renders GlDrawer with open false', () => {
createComponent();
it('renders GlDrawer with open true', () => {
createComponent({ list: mockLabelList, activeId: listId });
expect(findDrawer().props('open')).toBe(true);
});
});
describe('when activeId is in boardsStore', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
boardsStore.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
list_type: 'label',
});
store.state.activeId = listId;
store.state.sidebarType = LIST;
createComponent();
});
afterEach(() => {
mock.restore();
});
describe('when activeId is in state', () => {
it('renders label title', () => {
createComponent({ list: mockLabelList, activeId: listId });
expect(findLabel().props('title')).toBe(labelTitle);
});
it('renders label background color', () => {
createComponent({ list: mockLabelList, activeId: listId });
expect(findLabel().props('backgroundColor')).toBe(labelColor);
});
});
describe('when activeId is not in boardsStore', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
boardsStore.addList({ id: listId, label: { title: labelTitle, color: labelColor } });
store.state.activeId = inactiveId;
createComponent();
});
afterEach(() => {
mock.restore();
});
describe('when activeId is not in state', () => {
it('does not render GlLabel', () => {
createComponent({ list: mockLabelList });
expect(findLabel().exists()).toBe(false);
});
});
});
describe('when sidebarType is not List', () => {
beforeEach(() => {
store.state.sidebarType = '';
createComponent();
});
it('does not render GlDrawer', () => {
createComponent({ sidebarType: '' });
expect(findDrawer().exists()).toBe(false);
});
});
......@@ -191,20 +150,9 @@ describe('BoardSettingsSidebar', () => {
});
describe('when user can admin the boards list', () => {
beforeEach(() => {
store.state.activeId = listId;
store.state.sidebarType = LIST;
boardsStore.addList({
id: listId,
label: { title: labelTitle, color: labelColor },
list_type: 'label',
});
createComponent({ canAdminList: true });
});
it('renders "Remove list" button', () => {
createComponent({ canAdminList: true, activeId: listId, list: mockLabelList });
expect(findRemoveButton().exists()).toBe(true);
});
});
......
......@@ -336,6 +336,22 @@ export const mockLabelList = {
issuesCount: 0,
};
export const mockMilestoneList = {
id: 'gid://gitlab/List/3',
title: 'To Do',
position: 0,
listType: 'milestone',
collapsed: false,
label: null,
assignee: null,
milestone: {
webUrl: 'https://gitlab.com/h5bp/html5-boilerplate/-/milestones/1',
title: 'Backlog',
},
loading: false,
issuesCount: 0,
};
export const mockLists = [mockList, mockLabelList];
export const mockListsById = keyBy(mockLists, 'id');
......
......@@ -107,12 +107,7 @@ describe('setFilters', () => {
});
describe('performSearch', () => {
it('should dispatch setFilters action', (done) => {
testAction(actions.performSearch, {}, {}, [], [{ type: 'setFilters', payload: {} }], done);
});
it('should dispatch setFilters, fetchLists and resetIssues action when graphqlBoardLists FF is on', (done) => {
window.gon = { features: { graphqlBoardLists: true } };
it('should dispatch setFilters, fetchLists and resetIssues action', (done) => {
testAction(
actions.performSearch,
{},
......@@ -496,12 +491,9 @@ describe('fetchLabels', () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
const commit = jest.fn();
const getters = {
shouldUseGraphQL: () => true,
};
const state = { boardType: 'group' };
await actions.fetchLabels({ getters, state, commit });
await actions.fetchLabels({ state, commit });
expect(commit).toHaveBeenCalledWith(types.RECEIVE_LABELS_SUCCESS, labels);
});
......@@ -954,7 +946,7 @@ describe('moveIssue', () => {
});
describe('moveIssueCard and undoMoveIssueCard', () => {
describe('card should move without clonning', () => {
describe('card should move without cloning', () => {
let state;
let params;
let moveMutations;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment