Commit ff89a3dc authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'psi-new-new-column-column' into 'master'

Add column with list of labels and search field [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!52061
parents 77f2ad0a 07df00da
<script>
import {
GlButton,
GlFormGroup,
GlFormRadio,
GlFormRadioGroup,
GlLabel,
GlSearchBoxByType,
GlSkeletonLoader,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import boardsStore from '../stores/boards_store';
export default {
i18n: {
add: __('Add'),
cancel: __('Cancel'),
formDescription: __('A label list displays all issues with the selected label.'),
newLabelList: __('New label list'),
noLabelSelected: __('No label selected'),
searchPlaceholder: __('Search labels'),
selectLabel: __('Select label'),
selected: __('Selected'),
},
components: {
GlButton,
GlFormGroup,
GlFormRadio,
GlFormRadioGroup,
GlLabel,
GlSearchBoxByType,
GlSkeletonLoader,
},
directives: {
GlTooltip,
},
inject: ['scopedLabelsAvailable'],
data() {
return {
searchTerm: '',
selectedLabelId: null,
};
},
computed: {
...mapState(['labels', 'labelsLoading']),
...mapGetters(['getListByLabelId', 'shouldUseGraphQL']),
selectedLabel() {
return this.labels.find(({ id }) => id === this.selectedLabelId);
},
},
created() {
this.filterLabels();
},
methods: {
...mapActions(['createList', 'fetchLabels', 'highlightList', 'setAddColumnFormVisibility']),
getListByLabel(label) {
if (this.shouldUseGraphQL) {
return this.getListByLabelId(label);
}
return boardsStore.findListByLabelId(label.id);
},
columnExists(label) {
return Boolean(this.getListByLabel(label));
},
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.selectedLabelId) {
return;
}
const label = this.selectedLabel;
if (!label) {
return;
}
this.setAddColumnFormVisibility(false);
if (this.columnExists({ id: this.selectedLabelId })) {
const listId = this.getListByLabel(label).id;
this.highlight(listId);
return;
}
if (this.shouldUseGraphQL) {
this.createList({ labelId: this.selectedLabelId });
} else {
boardsStore.new({
title: label.title,
position: boardsStore.state.lists.length - 2,
list_type: 'label',
label: {
id: label.id,
title: label.title,
color: label.color,
},
});
this.highlight(boardsStore.findListByLabelId(label.id).id);
}
},
filterLabels() {
this.fetchLabels(this.searchTerm);
},
showScopedLabels(label) {
return this.scopedLabelsAvailable && isScopedLabel(label);
},
},
};
</script>
<template>
<div
class="board-add-new-list board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal gl-flex-shrink-0"
data-qa-selector="board_add_new_list"
>
<div
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-white"
>
<h3
class="gl-font-base gl-px-5 gl-py-5 gl-m-0 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
data-testid="board-add-column-form-title"
>
{{ $options.i18n.newLabelList }}
</h3>
<div class="gl-display-flex gl-flex-direction-column gl-h-full gl-overflow-hidden">
<!-- selectbox is here in EE -->
<p class="gl-m-5">{{ $options.i18n.formDescription }}</p>
<div class="gl-px-5 gl-pb-4">
<label class="gl-mb-2">{{ $options.i18n.selected }}</label>
<div>
<gl-label
v-if="selectedLabel"
v-gl-tooltip
:title="selectedLabel.title"
:description="selectedLabel.description"
:background-color="selectedLabel.color"
:scoped="showScopedLabels(selectedLabel)"
/>
<div v-else class="gl-text-gray-500">{{ $options.i18n.noLabelSelected }}</div>
</div>
</div>
<gl-form-group
class="gl-mx-5 gl-mb-3"
:label="$options.i18n.selectLabel"
label-for="board-available-labels"
>
<gl-search-box-by-type
id="board-available-labels"
v-model.trim="searchTerm"
debounce="250"
:placeholder="$options.i18n.searchPlaceholder"
@input="filterLabels"
/>
</gl-form-group>
<div v-if="labelsLoading" class="gl-m-5">
<gl-skeleton-loader :width="500" :height="172">
<rect width="480" height="20" x="10" y="15" rx="4" />
<rect width="380" height="20" x="10" y="50" rx="4" />
<rect width="430" height="20" x="10" y="85" rx="4" />
</gl-skeleton-loader>
</div>
<gl-form-radio-group
v-else
v-model="selectedLabelId"
class="gl-overflow-y-auto gl-px-5 gl-pt-3"
>
<label
v-for="label in labels"
:key="label.id"
class="gl-display-flex gl-flex-align-items-center gl-mb-5 gl-font-weight-normal"
>
<gl-form-radio :value="label.id" class="gl-mb-0 gl-mr-3" />
<span
class="dropdown-label-box gl-top-0"
:style="{
backgroundColor: label.color,
}"
></span>
<span>{{ label.title }}</span>
</label>
</gl-form-radio-group>
</div>
<div
class="gl-display-flex gl-p-3 gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10"
>
<gl-button
data-testid="cancelAddNewColumn"
class="gl-ml-auto gl-mr-3"
@click="setAddColumnFormVisibility(false)"
>{{ $options.i18n.cancel }}</gl-button
>
<gl-button
data-testid="addNewColumnButton"
:disabled="!selectedLabelId"
variant="success"
class="gl-mr-4"
@click="addList"
>{{ $options.i18n.add }}</gl-button
>
</div>
</div>
</div>
</template>
...@@ -46,11 +46,20 @@ export default { ...@@ -46,11 +46,20 @@ export default {
watch: { watch: {
filterParams: { filterParams: {
handler() { handler() {
this.fetchItemsForList({ listId: this.list.id }); if (this.list.id) {
this.fetchItemsForList({ listId: this.list.id });
}
}, },
deep: true, deep: true,
immediate: true, immediate: true,
}, },
'list.id': {
handler(id) {
if (id) {
this.fetchItemsForList({ listId: this.list.id });
}
},
},
highlighted: { highlighted: {
handler(highlighted) { handler(highlighted) {
if (highlighted) { if (highlighted) {
......
...@@ -6,11 +6,13 @@ import { mapState, mapGetters, mapActions } from 'vuex'; ...@@ -6,11 +6,13 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import { sortableEnd, sortableStart } from '~/boards/mixins/sortable_default_options'; import { sortableEnd, sortableStart } from '~/boards/mixins/sortable_default_options';
import defaultSortableConfig from '~/sortable/sortable_config'; import defaultSortableConfig from '~/sortable/sortable_config';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import BoardAddNewColumn from './board_add_new_column.vue';
import BoardColumn from './board_column.vue'; import BoardColumn from './board_column.vue';
import BoardColumnDeprecated from './board_column_deprecated.vue'; import BoardColumnDeprecated from './board_column_deprecated.vue';
export default { export default {
components: { components: {
BoardAddNewColumn,
BoardColumn: BoardColumn:
gon.features?.graphqlBoardLists || gon.features?.epicBoards gon.features?.graphqlBoardLists || gon.features?.epicBoards
? BoardColumn ? BoardColumn
...@@ -36,8 +38,11 @@ export default { ...@@ -36,8 +38,11 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['boardLists', 'error', 'isEpicBoard']), ...mapState(['boardLists', 'error', 'addColumnForm', 'isEpicBoard']),
...mapGetters(['isSwimlanesOn']), ...mapGetters(['isSwimlanesOn']),
addColumnFormVisible() {
return this.addColumnForm?.visible;
},
boardListsToUse() { boardListsToUse() {
return this.glFeatures.graphqlBoardLists || this.isSwimlanesOn || this.isEpicBoard return this.glFeatures.graphqlBoardLists || this.isSwimlanesOn || this.isEpicBoard
? sortBy([...Object.values(this.boardLists)], 'position') ? sortBy([...Object.values(this.boardLists)], 'position')
...@@ -65,6 +70,10 @@ export default { ...@@ -65,6 +70,10 @@ export default {
}, },
methods: { methods: {
...mapActions(['moveList']), ...mapActions(['moveList']),
afterFormEnters() {
const el = this.canDragColumns ? this.$refs.list.$el : this.$refs.list;
el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' });
},
handleDragOnStart() { handleDragOnStart() {
sortableStart(); sortableStart();
}, },
...@@ -103,13 +112,17 @@ export default { ...@@ -103,13 +112,17 @@ export default {
@end="handleDragOnEnd" @end="handleDragOnEnd"
> >
<board-column <board-column
v-for="list in boardListsToUse" v-for="(list, index) in boardListsToUse"
:key="list.id" :key="index"
ref="board" ref="board"
:can-admin-list="canAdminList" :can-admin-list="canAdminList"
:list="list" :list="list"
:disabled="disabled" :disabled="disabled"
/> />
<transition name="slide" @after-enter="afterFormEnters">
<board-add-new-column v-if="addColumnFormVisible" />
</transition>
</component> </component>
<epics-swimlanes <epics-swimlanes
......
...@@ -7,14 +7,14 @@ query BoardLabels( ...@@ -7,14 +7,14 @@ query BoardLabels(
$isProject: Boolean = false $isProject: Boolean = false
) { ) {
group(fullPath: $fullPath) @include(if: $isGroup) { group(fullPath: $fullPath) @include(if: $isGroup) {
labels(searchTerm: $searchTerm) { labels(searchTerm: $searchTerm, onlyGroupLabels: true, includeAncestorGroups: true) {
nodes { nodes {
...Label ...Label
} }
} }
} }
project(fullPath: $fullPath) @include(if: $isProject) { project(fullPath: $fullPath) @include(if: $isProject) {
labels(searchTerm: $searchTerm) { labels(searchTerm: $searchTerm, includeAncestorGroups: true) {
nodes { nodes {
...Label ...Label
} }
......
#import "ee_else_ce/boards/graphql/board_list.fragment.graphql" #import "./board_list.fragment.graphql"
mutation CreateBoardList( mutation CreateBoardList($boardId: BoardID!, $backlog: Boolean, $labelId: LabelID) {
$boardId: BoardID! boardListCreate(input: { boardId: $boardId, backlog: $backlog, labelId: $labelId }) {
$backlog: Boolean
$labelId: LabelID
$milestoneId: MilestoneID
$assigneeId: UserID
) {
boardListCreate(
input: {
boardId: $boardId
backlog: $backlog
labelId: $labelId
milestoneId: $milestoneId
assigneeId: $assigneeId
}
) {
list { list {
...BoardListFragment ...BoardListFragment
} }
......
import { pick } from 'lodash'; import { pick } from 'lodash';
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql'; import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import { import {
BoardType, BoardType,
...@@ -21,7 +22,6 @@ import { ...@@ -21,7 +22,6 @@ import {
transformNotFilters, transformNotFilters,
} from '../boards_util'; } from '../boards_util';
import boardLabelsQuery from '../graphql/board_labels.query.graphql'; import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import createBoardListMutation from '../graphql/board_list_create.mutation.graphql';
import destroyBoardListMutation from '../graphql/board_list_destroy.mutation.graphql'; import destroyBoardListMutation from '../graphql/board_list_destroy.mutation.graphql';
import updateBoardListMutation from '../graphql/board_list_update.mutation.graphql'; import updateBoardListMutation from '../graphql/board_list_update.mutation.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql'; import groupProjectsQuery from '../graphql/group_projects.query.graphql';
...@@ -161,14 +161,17 @@ export default { ...@@ -161,14 +161,17 @@ export default {
dispatch('highlightList', list.id); dispatch('highlightList', list.id);
} }
}) })
.catch(() => commit(types.CREATE_LIST_FAILURE)); .catch((e) => {
commit(types.CREATE_LIST_FAILURE);
throw e;
});
}, },
addList: ({ commit }, list) => { addList: ({ commit }, list) => {
commit(types.RECEIVE_ADD_LIST_SUCCESS, updateListPosition(list)); commit(types.RECEIVE_ADD_LIST_SUCCESS, updateListPosition(list));
}, },
fetchLabels: ({ state, commit }, searchTerm) => { fetchLabels: ({ state, commit, getters }, searchTerm) => {
const { fullPath, boardType } = state; const { fullPath, boardType } = state;
const variables = { const variables = {
...@@ -178,15 +181,29 @@ export default { ...@@ -178,15 +181,29 @@ export default {
isProject: boardType === BoardType.project, isProject: boardType === BoardType.project,
}; };
commit(types.RECEIVE_LABELS_REQUEST);
return gqlClient return gqlClient
.query({ .query({
query: boardLabelsQuery, query: boardLabelsQuery,
variables, variables,
}) })
.then(({ data }) => { .then(({ data }) => {
const labels = data[boardType]?.labels.nodes; let labels = data[boardType]?.labels.nodes;
if (!getters.shouldUseGraphQL) {
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;
})
.catch((e) => {
commit(types.RECEIVE_LABELS_FAILURE);
throw e;
}); });
}, },
......
...@@ -2,7 +2,9 @@ export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA'; ...@@ -2,7 +2,9 @@ export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';
export const SET_FILTERS = 'SET_FILTERS'; export const SET_FILTERS = 'SET_FILTERS';
export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS'; export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS';
export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE'; export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE';
export const RECEIVE_LABELS_REQUEST = 'RECEIVE_LABELS_REQUEST';
export const RECEIVE_LABELS_SUCCESS = 'RECEIVE_LABELS_SUCCESS'; export const RECEIVE_LABELS_SUCCESS = 'RECEIVE_LABELS_SUCCESS';
export const RECEIVE_LABELS_FAILURE = 'RECEIVE_LABELS_FAILURE';
export const GENERATE_DEFAULT_LISTS_FAILURE = 'GENERATE_DEFAULT_LISTS_FAILURE'; export const GENERATE_DEFAULT_LISTS_FAILURE = 'GENERATE_DEFAULT_LISTS_FAILURE';
export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS'; export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS';
export const RECEIVE_BOARD_LISTS_FAILURE = 'RECEIVE_BOARD_LISTS_FAILURE'; export const RECEIVE_BOARD_LISTS_FAILURE = 'RECEIVE_BOARD_LISTS_FAILURE';
......
...@@ -64,8 +64,18 @@ export default { ...@@ -64,8 +64,18 @@ export default {
state.error = s__('Boards|An error occurred while creating the list. Please try again.'); state.error = s__('Boards|An error occurred while creating the list. Please try again.');
}, },
[mutationTypes.RECEIVE_LABELS_REQUEST]: (state) => {
state.labelsLoading = true;
},
[mutationTypes.RECEIVE_LABELS_SUCCESS]: (state, labels) => { [mutationTypes.RECEIVE_LABELS_SUCCESS]: (state, labels) => {
state.labels = labels; state.labels = labels;
state.labelsLoading = false;
},
[mutationTypes.RECEIVE_LABELS_FAILURE]: (state) => {
state.error = s__('Boards|An error occurred while fetching labels. Please reload the page.');
state.labelsLoading = false;
}, },
[mutationTypes.GENERATE_DEFAULT_LISTS_FAILURE]: (state) => { [mutationTypes.GENERATE_DEFAULT_LISTS_FAILURE]: (state) => {
...@@ -270,7 +280,7 @@ export default { ...@@ -270,7 +280,7 @@ export default {
}, },
[mutationTypes.SET_ADD_COLUMN_FORM_VISIBLE]: (state, visible) => { [mutationTypes.SET_ADD_COLUMN_FORM_VISIBLE]: (state, visible) => {
state.addColumnFormVisible = visible; Vue.set(state.addColumnForm, 'visible', visible);
}, },
[mutationTypes.ADD_LIST_TO_HIGHLIGHTED_LISTS]: (state, listId) => { [mutationTypes.ADD_LIST_TO_HIGHLIGHTED_LISTS]: (state, listId) => {
......
import { inactiveId } from '~/boards/constants'; import { inactiveId, ListType } from '~/boards/constants';
export default () => ({ export default () => ({
boardType: null, boardType: null,
...@@ -15,6 +15,7 @@ export default () => ({ ...@@ -15,6 +15,7 @@ export default () => ({
boardItems: {}, boardItems: {},
filterParams: {}, filterParams: {},
boardConfig: {}, boardConfig: {},
labelsLoading: false,
labels: [], labels: [],
highlightedLists: [], highlightedLists: [],
selectedBoardItems: [], selectedBoardItems: [],
...@@ -26,7 +27,10 @@ export default () => ({ ...@@ -26,7 +27,10 @@ export default () => ({
}, },
selectedProject: {}, selectedProject: {},
error: undefined, error: undefined,
addColumnFormVisible: false, addColumnForm: {
visible: false,
columnType: ListType.label,
},
// TODO: remove after ce/ee split of board_content.vue // TODO: remove after ce/ee split of board_content.vue
isShowingEpicsSwimlanes: false, isShowingEpicsSwimlanes: false,
}); });
...@@ -4,6 +4,7 @@ import Draggable from 'vuedraggable'; ...@@ -4,6 +4,7 @@ import Draggable from 'vuedraggable';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue'; import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
import { isListDraggable } from '~/boards/boards_util'; import { isListDraggable } from '~/boards/boards_util';
import BoardAddNewColumn from '~/boards/components/board_add_new_column.vue';
import { n__ } from '~/locale'; import { n__ } from '~/locale';
import defaultSortableConfig from '~/sortable/sortable_config'; import defaultSortableConfig from '~/sortable/sortable_config';
import { DRAGGABLE_TAG } from '../constants'; import { DRAGGABLE_TAG } from '../constants';
...@@ -12,6 +13,7 @@ import IssuesLaneList from './issues_lane_list.vue'; ...@@ -12,6 +13,7 @@ import IssuesLaneList from './issues_lane_list.vue';
export default { export default {
components: { components: {
BoardAddNewColumn,
BoardListHeader, BoardListHeader,
EpicLane, EpicLane,
IssuesLaneList, IssuesLaneList,
...@@ -37,8 +39,11 @@ export default { ...@@ -37,8 +39,11 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['epics', 'pageInfoByListId', 'listsFlags']), ...mapState(['epics', 'pageInfoByListId', 'listsFlags', 'addColumnForm']),
...mapGetters(['getUnassignedIssues']), ...mapGetters(['getUnassignedIssues']),
addColumnFormVisible() {
return this.addColumnForm?.visible;
},
unassignedIssues() { unassignedIssues() {
return (listId) => this.getUnassignedIssues(listId); return (listId) => this.getUnassignedIssues(listId);
}, },
...@@ -98,6 +103,13 @@ export default { ...@@ -98,6 +103,13 @@ export default {
isListDraggable(list) { isListDraggable(list) {
return isListDraggable(list); return isListDraggable(list);
}, },
afterFormEnters() {
const container = this.$refs.scrollableContainer;
container.scrollTo({
left: container.scrollWidth,
behavior: 'smooth',
});
},
}, },
}; };
</script> </script>
...@@ -136,7 +148,7 @@ export default { ...@@ -136,7 +148,7 @@ export default {
/> />
</div> </div>
</component> </component>
<div class="board-epics-swimlanes gl-display-table gl-pb-5"> <div class="board-epics-swimlanes gl-display-table">
<epic-lane <epic-lane
v-for="epic in epics" v-for="epic in epics"
:key="epic.id" :key="epic.id"
...@@ -193,6 +205,12 @@ export default { ...@@ -193,6 +205,12 @@ export default {
{{ s__('Board|Load more issues') }} {{ s__('Board|Load more issues') }}
</gl-button> </gl-button>
</div> </div>
<!-- placeholder for some space below lane lists -->
<div v-else class="gl-pb-5"></div>
</div> </div>
<transition name="slide" @after-enter="afterFormEnters">
<board-add-new-column v-if="addColumnFormVisible" class="gl-sticky gl-top-5" />
</transition>
</div> </div>
</template> </template>
#import "./board_list.fragment.graphql"
mutation CreateBoardList(
$boardId: BoardID!
$backlog: Boolean
$labelId: LabelID
$milestoneId: MilestoneID
$assigneeId: UserID
) {
boardListCreate(
input: {
boardId: $boardId
backlog: $backlog
labelId: $labelId
milestoneId: $milestoneId
assigneeId: $assigneeId
}
) {
list {
...BoardListFragment
}
errors
}
}
...@@ -91,6 +91,10 @@ ...@@ -91,6 +91,10 @@
.board-swimlanes { .board-swimlanes {
overflow-x: auto; overflow-x: auto;
.board-add-new-list {
height: calc(100% - 1rem);
}
} }
$epic-icons-spacing: 40px; $epic-icons-spacing: 40px;
......
...@@ -1305,6 +1305,9 @@ msgstr "" ...@@ -1305,6 +1305,9 @@ msgstr ""
msgid "A job artifact is an archive of files and directories saved by a job when it finishes." msgid "A job artifact is an archive of files and directories saved by a job when it finishes."
msgstr "" msgstr ""
msgid "A label list displays all issues with the selected label."
msgstr ""
msgid "A limit of %{ci_project_subscriptions_limit} subscriptions to or from a project applies." msgid "A limit of %{ci_project_subscriptions_limit} subscriptions to or from a project applies."
msgstr "" msgstr ""
...@@ -4814,6 +4817,9 @@ msgstr "" ...@@ -4814,6 +4817,9 @@ msgstr ""
msgid "Boards|An error occurred while fetching issues. Please reload the page." msgid "Boards|An error occurred while fetching issues. Please reload the page."
msgstr "" msgstr ""
msgid "Boards|An error occurred while fetching labels. Please reload the page."
msgstr ""
msgid "Boards|An error occurred while fetching the board epics. Please reload the page." msgid "Boards|An error occurred while fetching the board epics. Please reload the page."
msgstr "" msgstr ""
...@@ -20058,6 +20064,9 @@ msgstr "" ...@@ -20058,6 +20064,9 @@ msgstr ""
msgid "New label" msgid "New label"
msgstr "" msgstr ""
msgid "New label list"
msgstr ""
msgid "New merge request" msgid "New merge request"
msgstr "" msgstr ""
...@@ -20283,6 +20292,9 @@ msgstr "" ...@@ -20283,6 +20292,9 @@ msgstr ""
msgid "No label" msgid "No label"
msgstr "" msgstr ""
msgid "No label selected"
msgstr ""
msgid "No labels with such name or description" msgid "No labels with such name or description"
msgstr "" msgstr ""
...@@ -26026,6 +26038,9 @@ msgstr "" ...@@ -26026,6 +26038,9 @@ msgstr ""
msgid "Search forks" msgid "Search forks"
msgstr "" msgstr ""
msgid "Search labels"
msgstr ""
msgid "Search merge requests" msgid "Search merge requests"
msgstr "" msgstr ""
...@@ -26746,6 +26761,9 @@ msgstr "" ...@@ -26746,6 +26761,9 @@ msgstr ""
msgid "Select user" msgid "Select user"
msgstr "" msgstr ""
msgid "Selected"
msgstr ""
msgid "Selected commits" msgid "Selected commits"
msgstr "" msgstr ""
......
...@@ -13,8 +13,6 @@ RSpec.describe 'Issue Boards', :js do ...@@ -13,8 +13,6 @@ RSpec.describe 'Issue Boards', :js do
let_it_be(:user2) { create(:user) } let_it_be(:user2) { create(:user) }
before do before do
stub_feature_flags(board_new_list: false)
project.add_maintainer(user) project.add_maintainer(user)
project.add_maintainer(user2) project.add_maintainer(user2)
...@@ -68,6 +66,8 @@ RSpec.describe 'Issue Boards', :js do ...@@ -68,6 +66,8 @@ RSpec.describe 'Issue Boards', :js do
let_it_be(:issue10) { create(:labeled_issue, project: project, title: 'issue +', description: 'A+ great issue', labels: [a_plus]) } let_it_be(:issue10) { create(:labeled_issue, project: project, title: 'issue +', description: 'A+ great issue', labels: [a_plus]) }
before do before do
stub_feature_flags(board_new_list: false)
visit project_board_path(project, board) visit project_board_path(project, board)
wait_for_requests wait_for_requests
...@@ -168,19 +168,6 @@ RSpec.describe 'Issue Boards', :js do ...@@ -168,19 +168,6 @@ RSpec.describe 'Issue Boards', :js do
expect(page).to have_selector('.board', count: 3) expect(page).to have_selector('.board', count: 3)
end end
it 'removes checkmark in new list dropdown after deleting' do
click_button 'Add list'
wait_for_requests
find('.js-new-board-list').click
remove_list
wait_for_requests
expect(page).to have_selector('.board', count: 3)
end
it 'infinite scrolls list' do it 'infinite scrolls list' do
create_list(:labeled_issue, 50, project: project, labels: [planning]) create_list(:labeled_issue, 50, project: project, labels: [planning])
...@@ -321,6 +308,7 @@ RSpec.describe 'Issue Boards', :js do ...@@ -321,6 +308,7 @@ RSpec.describe 'Issue Boards', :js do
context 'new list' do context 'new list' do
it 'shows all labels in new list dropdown' do it 'shows all labels in new list dropdown' do
click_button 'Add list' click_button 'Add list'
wait_for_requests wait_for_requests
page.within('.dropdown-menu-issues-board-new') do page.within('.dropdown-menu-issues-board-new') do
......
# frozen_string_literal: true
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) }
let_it_be(:project_board) { create(:board, project: project) }
let_it_be(:user) { create(:user) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:group_label) { create(:group_label, group: group) }
let_it_be(:project_label) { create(:label, project: project) }
let_it_be(:group_backlog_list) { create(:backlog_list, board: group_board) }
let_it_be(:project_backlog_list) { create(:backlog_list, board: project_board) }
let_it_be(:issue) { create(:labeled_issue, project: project, labels: [group_label, project_label]) }
before_all do
project.add_maintainer(user)
group.add_owner(user)
end
where(:board_type, :graphql_board_lists_enabled, :board_new_list_enabled) do
:project | true | true
:project | false | true
:project | true | false
:project | false | false
:group | true | true
:group | false | true
:group | true | false
:group | false | false
end
with_them do
before do
sign_in(user)
set_cookie('sidebar_collapsed', 'true')
stub_feature_flags(
graphql_board_lists: graphql_board_lists_enabled,
board_new_list: board_new_list_enabled
)
if board_type == :project
visit project_board_path(project, project_board)
elsif board_type == :group
visit group_board_path(group, group_board)
end
wait_for_all_requests
end
it 'creates new column for label containing labeled issue' do
click_button button_text(board_new_list_enabled)
wait_for_all_requests
select_label(board_new_list_enabled, 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
end
def select_label(board_new_list_enabled, label)
if board_new_list_enabled
page.within('.board-add-new-list') do
find('label', text: label.title).click
click_button 'Add'
end
else
page.within('.dropdown-menu-issues-board-new') do
click_link label.title
end
end
end
def button_text(board_new_list_enabled)
if board_new_list_enabled
'Create list'
else
'Add list'
end
end
end
import { GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardAddNewColumn from '~/boards/components/board_add_new_column.vue';
import defaultState from '~/boards/stores/state';
import { mockLabelList } from '../mock_data';
Vue.use(Vuex);
describe('Board card layout', () => {
let wrapper;
let shouldUseGraphQL;
const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => {
return new Vuex.Store({
state: {
...defaultState,
...state,
},
actions,
getters,
});
};
const mountComponent = ({
selectedLabelId,
labels = [],
getListByLabelId = jest.fn(),
actions = {},
} = {}) => {
wrapper = extendedWrapper(
shallowMount(BoardAddNewColumn, {
stubs: {
GlFormGroup: true,
},
data() {
return {
selectedLabelId,
};
},
store: createStore({
actions: {
fetchLabels: jest.fn(),
setAddColumnFormVisibility: jest.fn(),
...actions,
},
getters: {
shouldUseGraphQL: () => shouldUseGraphQL,
getListByLabelId: () => getListByLabelId,
},
state: {
labels,
labelsLoading: false,
},
}),
provide: {
scopedLabelsAvailable: true,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text();
const findSearchInput = () => wrapper.find(GlSearchBoxByType);
const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn');
const submitButton = () => wrapper.findByTestId('addNewColumnButton');
beforeEach(() => {
shouldUseGraphQL = true;
});
it('shows form title & search input', () => {
mountComponent();
expect(formTitle()).toEqual(BoardAddNewColumn.i18n.newLabelList);
expect(findSearchInput().exists()).toBe(true);
});
it('clicking cancel hides the form', () => {
const setAddColumnFormVisibility = jest.fn();
mountComponent({
actions: {
setAddColumnFormVisibility,
},
});
cancelButton().vm.$emit('click');
expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false);
});
describe('Add list button', () => {
it('is disabled if no item is selected', () => {
mountComponent();
expect(submitButton().props('disabled')).toBe(true);
});
it('adds a new list on click', async () => {
const labelId = mockLabelList.label.id;
const highlightList = jest.fn();
const createList = jest.fn();
mountComponent({
labels: [mockLabelList.label],
selectedLabelId: labelId,
actions: {
createList,
highlightList,
},
});
await nextTick();
submitButton().vm.$emit('click');
expect(highlightList).not.toHaveBeenCalled();
expect(createList).toHaveBeenCalledWith(expect.anything(), { labelId });
});
it('highlights existing list if trying to re-add', async () => {
const getListByLabelId = jest.fn().mockReturnValue(mockLabelList);
const highlightList = jest.fn();
const createList = jest.fn();
mountComponent({
labels: [mockLabelList.label],
selectedLabelId: mockLabelList.label.id,
getListByLabelId,
actions: {
createList,
highlightList,
},
});
await nextTick();
submitButton().vm.$emit('click');
expect(highlightList).toHaveBeenCalledWith(expect.anything(), mockLabelList.id);
expect(createList).not.toHaveBeenCalled();
});
});
});
...@@ -327,11 +327,15 @@ describe('fetchLabels', () => { ...@@ -327,11 +327,15 @@ describe('fetchLabels', () => {
}; };
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse); jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
await testAction({ const commit = jest.fn();
action: actions.fetchLabels, const getters = {
state: { boardType: 'group' }, shouldUseGraphQL: () => true,
expectedMutations: [{ type: types.RECEIVE_LABELS_SUCCESS, payload: labels }], };
}); const state = { boardType: 'group' };
await actions.fetchLabels({ getters, state, commit });
expect(commit).toHaveBeenCalledWith(types.RECEIVE_LABELS_SUCCESS, labels);
}); });
}); });
......
...@@ -109,11 +109,31 @@ describe('Board Store Mutations', () => { ...@@ -109,11 +109,31 @@ describe('Board Store Mutations', () => {
}); });
}); });
describe('RECEIVE_LABELS_REQUEST', () => {
it('sets labelsLoading on state', () => {
mutations.RECEIVE_LABELS_REQUEST(state);
expect(state.labelsLoading).toEqual(true);
});
});
describe('RECEIVE_LABELS_SUCCESS', () => { describe('RECEIVE_LABELS_SUCCESS', () => {
it('sets labels on state', () => { it('sets labels on state', () => {
mutations.RECEIVE_LABELS_SUCCESS(state, labels); mutations.RECEIVE_LABELS_SUCCESS(state, labels);
expect(state.labels).toEqual(labels); expect(state.labels).toEqual(labels);
expect(state.labelsLoading).toEqual(false);
});
});
describe('RECEIVE_LABELS_FAILURE', () => {
it('sets error message', () => {
mutations.RECEIVE_LABELS_FAILURE(state);
expect(state.error).toEqual(
'An error occurred while fetching labels. Please reload the page.',
);
expect(state.labelsLoading).toEqual(false);
}); });
}); });
......
...@@ -9,6 +9,8 @@ RSpec.shared_examples 'multiple issue boards' do ...@@ -9,6 +9,8 @@ RSpec.shared_examples 'multiple issue boards' do
login_as(user) login_as(user)
stub_feature_flags(board_new_list: false)
visit boards_path visit boards_path
wait_for_requests wait_for_requests
end end
......
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