Commit 575798ef authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '323415-design-review-mvc-1' into 'master'

Epic Boards - MVC1 bug fixes [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!55709
parents 7f0ad6bc 3ca1235e
......@@ -29,8 +29,8 @@ export default {
};
},
computed: {
...mapState(['labels', 'labelsLoading', 'isEpicBoard']),
...mapGetters(['getListByLabelId', 'shouldUseGraphQL']),
...mapState(['labels', 'labelsLoading']),
...mapGetters(['getListByLabelId', 'shouldUseGraphQL', 'isEpicBoard']),
selectedLabel() {
if (!this.selectedId) {
return null;
......
......@@ -13,7 +13,7 @@ export default {
</script>
<template>
<span class="gl-ml-3 gl-display-flex gl-align-items-center">
<span class="gl-ml-3 gl-display-flex gl-align-items-center" data-testid="boards-create-list">
<gl-button variant="confirm" @click="setAddColumnFormVisibility(true)"
>{{ __('Create list') }}
</gl-button>
......
<script>
import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { sortBy } from 'lodash';
import { mapActions, mapState } from 'vuex';
import { mapActions, mapGetters, mapState } from 'vuex';
import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
......@@ -52,7 +52,8 @@ export default {
};
},
computed: {
...mapState(['isShowingLabels', 'isEpicBoard']),
...mapState(['isShowingLabels']),
...mapGetters(['isEpicBoard']),
cappedAssignees() {
// e.g. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
......
......@@ -38,8 +38,8 @@ export default {
},
},
computed: {
...mapState(['boardLists', 'error', 'addColumnForm', 'isEpicBoard']),
...mapGetters(['isSwimlanesOn']),
...mapState(['boardLists', 'error', 'addColumnForm']),
...mapGetters(['isSwimlanesOn', 'isEpicBoard']),
addColumnFormVisible() {
return this.addColumnForm?.visible;
},
......
<script>
import { GlModal } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { deprecatedCreateFlash as Flash } from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName } from '~/lib/utils/common_utils';
......@@ -106,6 +107,7 @@ export default {
};
},
computed: {
...mapGetters(['isEpicBoard']),
isNewForm() {
return this.currentPage === formType.new;
},
......@@ -322,7 +324,7 @@ export default {
/>
<board-scope
v-if="scopedIssueBoardFeatureEnabled"
v-if="scopedIssueBoardFeatureEnabled && !isEpicBoard"
:collapse-scope="isNewForm"
:board="board"
:can-admin-board="canAdminBoard"
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import Draggable from 'vuedraggable';
import { mapActions, mapState } from 'vuex';
import { mapActions, mapGetters, mapState } from 'vuex';
import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_options';
import { sprintf, __ } from '~/locale';
import defaultSortableConfig from '~/sortable/sortable_config';
......@@ -49,6 +49,7 @@ export default {
},
computed: {
...mapState(['pageInfoByListId', 'listsFlags']),
...mapGetters(['isEpicBoard']),
paginatedIssueText() {
return sprintf(__('Showing %{pageSize} of %{total} issues'), {
pageSize: this.boardItems.length,
......@@ -69,13 +70,13 @@ export default {
},
listRef() {
// When list is draggable, the reference to the list needs to be accessed differently
return this.canAdminList ? this.$refs.list.$el : this.$refs.list;
return this.canAdminList && !this.isEpicBoard ? this.$refs.list.$el : this.$refs.list;
},
showingAllIssues() {
return this.boardItems.length === this.list.issuesCount;
},
treeRootWrapper() {
return this.canAdminList ? Draggable : 'ul';
return this.canAdminList && !this.isEpicBoard ? Draggable : 'ul';
},
treeRootOptions() {
const options = {
......
......@@ -8,7 +8,7 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import { mapActions, mapGetters, mapState } from 'vuex';
import { isListDraggable } from '~/boards/boards_util';
import { isScopedLabel, parseBoolean } from '~/lib/utils/common_utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
......@@ -69,7 +69,8 @@ export default {
},
},
computed: {
...mapState(['activeId', 'isEpicBoard']),
...mapState(['activeId']),
...mapGetters(['isEpicBoard']),
isLoggedIn() {
return Boolean(this.currentUserId);
},
......
import { __ } from '~/locale';
export const issuableTypes = {
issue: 'issue',
epic: 'epic',
};
export const BoardType = {
project: 'project',
group: 'group',
......
......@@ -23,6 +23,7 @@ import '~/boards/models/milestone';
import '~/boards/models/project';
import '~/boards/filters/due_date_filters';
import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
import modalMixin from '~/boards/mixins/modal_mixins';
......@@ -124,6 +125,7 @@ export default () => {
fullPath: $boardApp.dataset.fullPath,
boardType: this.parent,
disabled: this.disabled,
issuableType: issuableTypes.issue,
boardConfig: {
milestoneId: parseInt($boardApp.dataset.boardMilestoneId, 10),
milestoneTitle: $boardApp.dataset.boardMilestoneTitle || '',
......
......@@ -48,10 +48,10 @@ export default (params = {}) => {
return { boardsSelectorProps };
},
computed: {
...mapGetters(['shouldUseGraphQL']),
...mapGetters(['shouldUseGraphQL', 'isEpicBoard']),
},
render(createElement) {
if (this.shouldUseGraphQL || params.isEpicBoard) {
if (this.shouldUseGraphQL || this.isEpicBoard) {
return createElement(BoardsSelector, {
props: this.boardsSelectorProps,
});
......
......@@ -176,7 +176,7 @@ export default {
},
fetchLabels: ({ state, commit, getters }, searchTerm) => {
const { fullPath, boardType, isEpicBoard } = state;
const { fullPath, boardType } = state;
const variables = {
fullPath,
......@@ -195,7 +195,7 @@ export default {
.then(({ data }) => {
let labels = data[boardType]?.labels.nodes;
if (!getters.shouldUseGraphQL && !isEpicBoard) {
if (!getters.shouldUseGraphQL && !getters.isEpicBoard) {
labels = labels.map((label) => ({
...label,
id: getIdFromGraphQLId(label.id),
......
......@@ -38,6 +38,10 @@ export default {
return find(state.boardLists, (l) => l.title === title);
},
isEpicBoard: () => {
return false;
},
shouldUseGraphQL: () => {
return gon?.features?.graphqlBoardLists;
},
......
......@@ -32,13 +32,13 @@ export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfter
export default {
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
const { boardType, disabled, boardId, fullPath, boardConfig, isEpicBoard } = data;
const { boardType, disabled, boardId, fullPath, boardConfig, issuableType } = data;
state.boardId = boardId;
state.fullPath = fullPath;
state.boardType = boardType;
state.disabled = disabled;
state.boardConfig = boardConfig;
state.isEpicBoard = isEpicBoard;
state.issuableType = issuableType;
},
[mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, lists) => {
......
......@@ -2,6 +2,7 @@ import { inactiveId, ListType } from '~/boards/constants';
export default () => ({
boardType: null,
issuableType: null,
fullPath: null,
disabled: false,
isShowingLabels: true,
......
......@@ -30,6 +30,7 @@
color: var(--gray-500, $gray-500);
}
[data-page$='epic_boards:index'],
[data-page$='epic_boards:show'],
.issue-boards-page {
.content-wrapper {
......@@ -37,8 +38,11 @@
}
}
[data-page$='epic_boards:show'] .filter-form {
display: none;
[data-page$='epic_boards:index'],
[data-page$='epic_boards:show'] {
.filter-form {
display: none;
}
}
.boards-app {
......
......@@ -5,7 +5,10 @@
- placeholder = local_assigns[:placeholder] || _('Search or filter results...')
- is_not_boards_modal_or_productivity_analytics = type != :boards_modal && type != :productivity_analytics
- block_css_class = is_not_boards_modal_or_productivity_analytics ? 'row-content-block second-block' : ''
- user_can_admin_list = board && can?(current_user, :admin_issue_board_list, board.resource_parent)
- if board && board.to_type == "EpicBoard"
- user_can_admin_list = can?(current_user, :admin_epic_board_list, board.resource_parent)
- elsif board
- user_can_admin_list = can?(current_user, :admin_issue_board_list, board.resource_parent)
.issues-filters{ class: ("w-100" if type == :boards_modal) }
.issues-details-filters.filtered-search-block.d-flex.flex-column.flex-lg-row{ class: block_css_class, "v-pre" => type == :boards_modal }
......
......@@ -2,14 +2,14 @@
// This is a false violation of @gitlab/no-runtime-template-compiler, since it
// extends a valid Vue single file component.
/* eslint-disable @gitlab/no-runtime-template-compiler */
import { mapState } from 'vuex';
import { mapGetters } from 'vuex';
import BoardFormFoss from '~/boards/components/board_form.vue';
import createEpicBoardMutation from '../graphql/epic_board_create.mutation.graphql';
export default {
extends: BoardFormFoss,
computed: {
...mapState(['isEpicBoard']),
...mapGetters(['isEpicBoard']),
epicBoardCreateQuery() {
return createEpicBoardMutation;
},
......
<script>
import { mapState } from 'vuex';
import { mapGetters } from 'vuex';
// This is a false violation of @gitlab/no-runtime-template-compiler, since it
// extends a valid Vue single file component.
/* eslint-disable @gitlab/no-runtime-template-compiler */
......@@ -10,7 +10,7 @@ export default {
extends: BoardListHeaderFoss,
inject: ['weightFeatureAvailable'],
computed: {
...mapState(['isEpicBoard']),
...mapGetters(['isEpicBoard']),
countIcon() {
return this.isEpicBoard ? 'epic' : 'issues';
},
......
......@@ -2,7 +2,7 @@
// This is a false violation of @gitlab/no-runtime-template-compiler, since it
// extends a valid Vue single file component.
/* eslint-disable @gitlab/no-runtime-template-compiler */
import { mapState } from 'vuex';
import { mapGetters } from 'vuex';
import BoardsSelectorFoss from '~/boards/components/boards_selector.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import epicBoardsQuery from '../graphql/epic_boards.query.graphql';
......@@ -10,7 +10,7 @@ import epicBoardsQuery from '../graphql/epic_boards.query.graphql';
export default {
extends: BoardsSelectorFoss,
computed: {
...mapState(['isEpicBoard']),
...mapGetters(['isEpicBoard']),
showCreate() {
return this.isEpicBoard || this.multipleIssueBoardsAvailable;
},
......
......@@ -148,7 +148,7 @@ export default {
commit(types.SET_FILTERS, filterParams);
},
performSearch({ dispatch, getters, state }) {
performSearch({ dispatch, getters }) {
dispatch(
'setFilters',
convertObjectPropsToCamelCase(urlParamsToObject(window.location.search)),
......@@ -158,7 +158,7 @@ export default {
dispatch('resetEpics');
dispatch('resetIssues');
dispatch('fetchEpicsSwimlanes', {});
} else if (gon.features.graphqlBoardLists || state.isEpicBoard) {
} else if (gon.features.graphqlBoardLists || getters.isEpicBoard) {
dispatch('fetchLists');
dispatch('resetIssues');
}
......@@ -300,7 +300,10 @@ export default {
notImplemented();
},
fetchItemsForList: ({ state, commit }, { listId, fetchNext = false, noEpicIssues = false }) => {
fetchItemsForList: (
{ state, commit, getters },
{ listId, fetchNext = false, noEpicIssues = false },
) => {
commit(types.REQUEST_ITEMS_FOR_LIST, { listId, fetchNext });
const { epicId, ...filterParams } = state.filterParams;
......@@ -317,7 +320,7 @@ export default {
first: 20,
};
if (state.isEpicBoard) {
if (getters.isEpicBoard) {
return fetchAndFormatListEpics(state, variables)
.then(({ listItems, listPageInfo }) => {
commit(types.RECEIVE_ITEMS_FOR_LIST_SUCCESS, {
......@@ -525,10 +528,8 @@ export default {
);
},
fetchLists: ({ state, dispatch }) => {
const { isEpicBoard } = state;
if (!isEpicBoard) {
fetchLists: ({ getters, dispatch }) => {
if (!getters.isEpicBoard) {
dispatch('fetchIssueLists');
} else {
dispatch('fetchEpicLists');
......@@ -556,10 +557,8 @@ export default {
.catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE));
},
createList: ({ state, dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
const { isEpicBoard } = state;
if (!isEpicBoard) {
createList: ({ getters, dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
if (!getters.isEpicBoard) {
dispatch('createIssueList', { backlog, labelId, milestoneId, assigneeId });
} else {
dispatch('createEpicList', { backlog, labelId });
......
import { issuableTypes } from '~/boards/constants';
import gettersCE from '~/boards/stores/getters';
export default {
......@@ -16,6 +17,10 @@ export default {
return getters.getBoardItemsByList(listId).filter((i) => Boolean(i.epic) === false);
},
isEpicBoard: (state) => {
return state.issuableType === issuableTypes.epic;
},
shouldUseGraphQL: (state) => {
return state.isShowingEpicsSwimlanes || gon?.features?.graphqlBoardLists;
},
......
import { union, unionBy } from 'lodash';
import Vue from 'vue';
import { moveIssueListHelper } from '~/boards/boards_util';
import { issuableTypes } from '~/boards/constants';
import mutationsCE, { addIssueToList, removeIssueFromList } from '~/boards/stores/mutations';
import { s__ } from '~/locale';
import { ErrorMessages } from '../constants';
......@@ -84,7 +85,10 @@ export default {
},
[mutationTypes.RECEIVE_ITEMS_FOR_LIST_FAILURE]: (state, listId) => {
state.error = state.isEpicBoard ? ErrorMessages.fetchEpicsError : ErrorMessages.fetchIssueError;
state.error =
state.issuableType === issuableTypes.epic
? ErrorMessages.fetchEpicsError
: ErrorMessages.fetchIssueError;
Vue.set(state.listsFlags, listId, { isLoading: false, isLoadingMore: false });
},
......
......@@ -12,5 +12,4 @@ export default () => ({
epicsCacheById: {},
epicFetchInProgress: false,
epicsFlags: {},
isEpicBoard: false,
});
......@@ -12,6 +12,7 @@ import toggleLabels from 'ee_component/boards/toggle_labels';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
import { issuableTypes } from '~/boards/constants';
import mountMultipleBoardsSwitcher from '~/boards/mount_multiple_boards_switcher';
import store from '~/boards/stores';
import createDefaultClient from '~/lib/graphql';
......@@ -84,7 +85,7 @@ export default () => {
fullPath: $boardApp.dataset.fullPath,
boardType: this.parent,
disabled: this.disabled,
isEpicBoard: true,
issuableType: issuableTypes.epic,
boardConfig: {
milestoneId: parseInt($boardApp.dataset.boardMilestoneId, 10),
milestoneTitle: $boardApp.dataset.boardMilestoneTitle || '',
......@@ -131,6 +132,5 @@ export default () => {
mountMultipleBoardsSwitcher({
fullPath: $boardApp.dataset.fullPath,
rootPath: $boardApp.dataset.boardsEndpoint,
isEpicBoard: true,
});
};
......@@ -79,6 +79,32 @@ RSpec.describe 'epic boards', :js do
end
end
context 'when user can admin epic boards' do
before do
stub_licensed_features(epics: true)
group.add_maintainer(user)
sign_in(user)
visit_epic_boards_page
end
it "shows 'Create list' button" do
expect(page).to have_selector('[data-testid="boards-create-list"]')
end
end
context 'when user cannot admin epic boards' do
before do
stub_licensed_features(epics: true)
group.add_guest(user)
sign_in(user)
visit_epic_boards_page
end
it "does not show 'Create list'" do
expect(page).not_to have_selector('[data-testid="boards-create-list"]')
end
end
def visit_epic_boards_page
visit group_epic_boards_path(group)
wait_for_requests
......
......@@ -54,8 +54,8 @@ describe('BoardForm', () => {
const createStore = () => {
return new Vuex.Store({
state: {
isEpicBoard: true,
getters: {
isEpicBoard: () => true,
},
});
};
......
......@@ -34,8 +34,8 @@ describe('BoardsSelector', () => {
const createStore = () => {
return new Vuex.Store({
state: {
isEpicBoard: false,
getters: {
isEpicBoard: () => false,
},
});
};
......
......@@ -7,6 +7,7 @@ import * as types from 'ee/boards/stores/mutation_types';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import { formatListIssues, formatBoardLists } from '~/boards/boards_util';
import { issuableTypes } from '~/boards/constants';
import * as typesCE from '~/boards/stores/mutation_types';
import * as commonUtils from '~/lib/utils/common_utils';
import { mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
......@@ -136,18 +137,20 @@ describe('performSearch', () => {
});
describe('fetchLists', () => {
it('should dispatch fetchIssueLists action when isEpicBoard is false on state', async () => {
it('should dispatch fetchIssueLists action when isEpicBoard is false', async () => {
const getters = { isEpicBoard: false };
await testAction({
action: actions.fetchLists,
state: { isEpicBoard: false },
state: { issuableType: issuableTypes.issue, ...getters },
expectedActions: [{ type: 'fetchIssueLists' }],
});
});
it('should dispatch fetchEpicLists action when isEpicBoard is true on state', async () => {
it('should dispatch fetchEpicLists action when isEpicBoard is true', async () => {
const getters = { isEpicBoard: true };
await testAction({
action: actions.fetchLists,
state: { isEpicBoard: true },
state: { issuableType: issuableTypes.epic, ...getters },
expectedActions: [{ type: 'fetchEpicLists' }],
});
});
......@@ -949,15 +952,15 @@ describe('moveIssue', () => {
});
describe.each`
isEpicBoard | dispatchedAction
${false} | ${'createIssueList'}
${true} | ${'createEpicList'}
`('createList', ({ isEpicBoard, dispatchedAction }) => {
it(`should dispatch ${dispatchedAction} action when isEpicBoard is ${isEpicBoard} on state`, async () => {
isEpicBoard | issuableType | dispatchedAction
${false} | ${'issuableTypes.issue'} | ${'createIssueList'}
${true} | ${'issuableTypes.epic'} | ${'createEpicList'}
`('createList', ({ isEpicBoard, issuableType, dispatchedAction }) => {
it(`should dispatch ${dispatchedAction} action when isEpicBoard is ${isEpicBoard}`, async () => {
await testAction({
action: actions.createList,
payload: { backlog: true },
state: { isEpicBoard },
state: { isEpicBoard, issuableType },
expectedActions: [{ type: dispatchedAction, payload: { backlog: true } }],
});
});
......
......@@ -19,6 +19,7 @@ const createStore = (state = defaultState) => {
return new Vuex.Store({
state,
actions,
getters: { isEpicBoard: () => false },
});
};
......
......@@ -29,6 +29,7 @@ describe('Board card layout', () => {
actions: mockActions,
getters: {
isSwimlanesOn: () => isSwimlanesOn,
isEpicBoard: () => false,
},
});
};
......
......@@ -53,7 +53,7 @@ describe('Board List Header Component', () => {
store = new Vuex.Store({
state: {},
actions: { updateList: updateListSpy, toggleListCollapsed: toggleListCollapsedSpy },
getters: {},
getters: { isEpicBoard: () => false },
});
wrapper = extendedWrapper(
......
import { issuableTypes } from '~/boards/constants';
import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations';
import defaultState from '~/boards/stores/state';
......@@ -37,7 +38,7 @@ describe('Board Store Mutations', () => {
const boardConfig = {
milestoneTitle: 'Milestone 1',
};
const isEpicBoard = true;
const issuableType = issuableTypes.issue;
mutations[types.SET_INITIAL_BOARD_DATA](state, {
boardId,
......@@ -45,7 +46,7 @@ describe('Board Store Mutations', () => {
boardType,
disabled,
boardConfig,
isEpicBoard,
issuableType,
});
expect(state.boardId).toEqual(boardId);
......@@ -53,7 +54,7 @@ describe('Board Store Mutations', () => {
expect(state.boardType).toEqual(boardType);
expect(state.disabled).toEqual(disabled);
expect(state.boardConfig).toEqual(boardConfig);
expect(state.isEpicBoard).toEqual(isEpicBoard);
expect(state.issuableType).toEqual(issuableType);
});
});
......
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