Commit cfccf903 authored by Florie Guibert's avatar Florie Guibert Committed by Simon Knox

Clean up :graphql_board_list feature flag

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