Commit 16cb01a6 authored by Illya Klymov's avatar Illya Klymov

Merge branch 'graphql-board-sfc' into 'master'

Create a Single File Component for Board [RUN-AS-IF-FOSS]

See merge request gitlab-org/gitlab!69332
parents 52e0c4e2 162500a6
<script>
import { mapActions, mapGetters } from 'vuex';
import BoardContent from '~/boards/components/board_content.vue';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
export default {
components: {
BoardContent,
BoardSettingsSidebar,
},
inject: ['disabled'],
computed: {
...mapGetters(['isSidebarOpen']),
},
mounted() {
this.performSearch();
},
methods: {
...mapActions(['performSearch']),
},
};
</script>
<template>
<div class="boards-app gl-relative" :class="{ 'is-compact': isSidebarOpen }">
<board-content :disabled="disabled" />
<board-settings-sidebar />
</div>
</template>
......@@ -2,12 +2,11 @@ import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import PortalVue from 'portal-vue';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapActions } from 'vuex';
import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
import toggleLabels from 'ee_else_ce/boards/toggle_labels';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardApp from '~/boards/components/board_app.vue';
import '~/boards/filters/due_date_filters';
import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
......@@ -41,10 +40,75 @@ const apolloProvider = new VueApollo({
),
});
let issueBoardsApp;
function mountBoardApp(el) {
const { boardId, groupId, fullPath, rootPath } = el.dataset;
store.dispatch('setInitialBoardData', {
boardId,
fullBoardId: fullBoardId(boardId),
fullPath,
boardType: el.dataset.parent,
disabled: parseBoolean(el.dataset.disabled) || true,
issuableType: issuableTypes.issue,
boardConfig: {
milestoneId: parseInt(el.dataset.boardMilestoneId, 10),
milestoneTitle: el.dataset.boardMilestoneTitle || '',
iterationId: parseInt(el.dataset.boardIterationId, 10),
iterationTitle: el.dataset.boardIterationTitle || '',
assigneeId: el.dataset.boardAssigneeId,
assigneeUsername: el.dataset.boardAssigneeUsername,
labels: el.dataset.labels ? JSON.parse(el.dataset.labels) : [],
labelIds: el.dataset.labelIds ? JSON.parse(el.dataset.labelIds) : [],
weight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null,
},
});
if (!gon?.features?.issueBoardsFilteredSearch) {
// Warning: FilteredSearchBoards has an implicit dependency on the Vuex state 'boardConfig'
// Improve this situation in the future.
const filterManager = new FilteredSearchBoards({ path: '' }, true, []);
filterManager.setup();
eventHub.$on('updateTokens', () => {
filterManager.updateTokens();
});
}
// eslint-disable-next-line no-new
new Vue({
el,
store,
apolloProvider,
provide: {
disabled: parseBoolean(el.dataset.disabled),
boardId,
groupId: Number(groupId),
rootPath,
currentUserId: gon.current_user_id || null,
canUpdate: parseBoolean(el.dataset.canUpdate),
canAdminList: parseBoolean(el.dataset.canAdminList),
labelsManagePath: el.dataset.labelsManagePath,
labelsFilterBasePath: el.dataset.labelsFilterBasePath,
timeTrackingLimitToHours: parseBoolean(el.dataset.timeTrackingLimitToHours),
multipleAssigneesFeatureAvailable: parseBoolean(el.dataset.multipleAssigneesFeatureAvailable),
epicFeatureAvailable: parseBoolean(el.dataset.epicFeatureAvailable),
iterationFeatureAvailable: parseBoolean(el.dataset.iterationFeatureAvailable),
weightFeatureAvailable: parseBoolean(el.dataset.weightFeatureAvailable),
boardWeight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null,
scopedLabelsAvailable: parseBoolean(el.dataset.scopedLabels),
milestoneListsAvailable: parseBoolean(el.dataset.milestoneListsAvailable),
assigneeListsAvailable: parseBoolean(el.dataset.assigneeListsAvailable),
iterationListsAvailable: parseBoolean(el.dataset.iterationListsAvailable),
issuableType: issuableTypes.issue,
emailsDisabled: parseBoolean(el.dataset.emailsDisabled),
},
render: (createComponent) => createComponent(BoardApp),
});
}
export default () => {
const $boardApp = document.getElementById('board-app');
const $boardApp = document.getElementById('js-issuable-board-app');
// check for browser back and trigger a hard reload to circumvent browser caching.
window.addEventListener('pageshow', (event) => {
const isNavTypeBackForward =
......@@ -55,106 +119,11 @@ export default () => {
}
});
if (issueBoardsApp) {
issueBoardsApp.$destroy(true);
}
if (gon?.features?.issueBoardsFilteredSearch) {
initBoardsFilteredSearch(apolloProvider);
}
// eslint-disable-next-line @gitlab/no-runtime-template-compiler
issueBoardsApp = new Vue({
el: $boardApp,
components: {
BoardContent,
BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'),
},
provide: {
boardId: $boardApp.dataset.boardId,
groupId: Number($boardApp.dataset.groupId),
rootPath: $boardApp.dataset.rootPath,
currentUserId: gon.current_user_id || null,
canUpdate: parseBoolean($boardApp.dataset.canUpdate),
canAdminList: parseBoolean($boardApp.dataset.canAdminList),
labelsManagePath: $boardApp.dataset.labelsManagePath,
labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath,
timeTrackingLimitToHours: parseBoolean($boardApp.dataset.timeTrackingLimitToHours),
multipleAssigneesFeatureAvailable: parseBoolean(
$boardApp.dataset.multipleAssigneesFeatureAvailable,
),
epicFeatureAvailable: parseBoolean($boardApp.dataset.epicFeatureAvailable),
iterationFeatureAvailable: parseBoolean($boardApp.dataset.iterationFeatureAvailable),
weightFeatureAvailable: parseBoolean($boardApp.dataset.weightFeatureAvailable),
boardWeight: $boardApp.dataset.boardWeight
? parseInt($boardApp.dataset.boardWeight, 10)
: null,
scopedLabelsAvailable: parseBoolean($boardApp.dataset.scopedLabels),
milestoneListsAvailable: parseBoolean($boardApp.dataset.milestoneListsAvailable),
assigneeListsAvailable: parseBoolean($boardApp.dataset.assigneeListsAvailable),
iterationListsAvailable: parseBoolean($boardApp.dataset.iterationListsAvailable),
issuableType: issuableTypes.issue,
emailsDisabled: parseBoolean($boardApp.dataset.emailsDisabled),
},
store,
apolloProvider,
data() {
return {
loading: 0,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
disabled: parseBoolean($boardApp.dataset.disabled),
parent: $boardApp.dataset.parent,
detailIssueVisible: false,
};
},
created() {
this.setInitialBoardData({
boardId: $boardApp.dataset.boardId,
fullBoardId: fullBoardId($boardApp.dataset.boardId),
fullPath: $boardApp.dataset.fullPath,
boardType: this.parent,
disabled: this.disabled,
issuableType: issuableTypes.issue,
boardConfig: {
milestoneId: parseInt($boardApp.dataset.boardMilestoneId, 10),
milestoneTitle: $boardApp.dataset.boardMilestoneTitle || '',
iterationId: parseInt($boardApp.dataset.boardIterationId, 10),
iterationTitle: $boardApp.dataset.boardIterationTitle || '',
assigneeId: $boardApp.dataset.boardAssigneeId,
assigneeUsername: $boardApp.dataset.boardAssigneeUsername,
labels: $boardApp.dataset.labels ? JSON.parse($boardApp.dataset.labels) : [],
labelIds: $boardApp.dataset.labelIds ? JSON.parse($boardApp.dataset.labelIds) : [],
weight: $boardApp.dataset.boardWeight
? parseInt($boardApp.dataset.boardWeight, 10)
: null,
},
});
eventHub.$on('updateTokens', this.updateTokens);
eventHub.$on('toggleDetailIssue', this.toggleDetailIssue);
},
beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens);
eventHub.$off('toggleDetailIssue', this.toggleDetailIssue);
},
mounted() {
if (!gon?.features?.issueBoardsFilteredSearch) {
this.filterManager = new FilteredSearchBoards({ path: '' }, true, []);
this.filterManager.setup();
}
this.performSearch();
},
methods: {
...mapActions(['setInitialBoardData', 'performSearch', 'setError']),
updateTokens() {
this.filterManager.updateTokens();
},
toggleDetailIssue(hasSidebar) {
this.detailIssueVisible = hasSidebar;
},
},
});
mountBoardApp($boardApp);
const createColumnTriggerEl = document.querySelector('.js-create-column-trigger');
if (createColumnTriggerEl) {
......
......@@ -18,7 +18,6 @@ import {
} from 'ee_else_ce/boards/constants';
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import eventHub from '~/boards/eventhub';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
......@@ -62,12 +61,10 @@ export default {
setActiveId({ commit }, { id, sidebarType }) {
commit(types.SET_ACTIVE_ID, { id, sidebarType });
eventHub.$emit('toggleDetailIssue', true);
},
unsetActiveId({ dispatch }) {
dispatch('setActiveId', { id: inactiveId, sidebarType: '' });
eventHub.$emit('toggleDetailIssue', false);
},
setFilters: ({ commit, state: { issuableType } }, filters) => {
......
......@@ -17,6 +17,5 @@
- add_page_specific_style 'page_bundles/boards'
= render 'shared/issuable/search_bar', type: :boards, board: board
#board-app.boards-app.position-relative{ "v-cloak" => "true", data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
%board-content{ ":disabled" => "disabled" }
%board-settings-sidebar
#js-issuable-board-app{ data: board_data }
// This is a true violation of @gitlab/no-runtime-template-compiler, as it
// relies on app/views/shared/boards/_show.html.haml for its
// template.
/* eslint-disable @gitlab/no-runtime-template-compiler */
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapActions, mapState } from 'vuex';
import initFilteredSearch from 'ee/boards/epic_filtered_search';
import { fullEpicBoardId, transformBoardConfig } from 'ee_component/boards/boards_util';
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 BoardApp from '~/boards/components/board_app.vue';
import boardConfigToggle from '~/boards/config_toggle';
import { issuableTypes } from '~/boards/constants';
import mountMultipleBoardsSwitcher from '~/boards/mount_multiple_boards_switcher';
......@@ -42,8 +37,74 @@ const apolloProvider = new VueApollo({
),
});
function mountBoardApp(el) {
const { boardId, groupId, fullPath, rootPath } = el.dataset;
store.dispatch('setInitialBoardData', {
allowSubEpics: parseBoolean(el.dataset.subEpicsFeatureAvailable),
boardType: el.dataset.parent,
disabled: parseBoolean(el.dataset.disabled) || true,
issuableType: issuableTypes.epic,
boardId,
fullBoardId: fullEpicBoardId(boardId),
fullPath,
boardConfig: {
milestoneId: parseInt(el.dataset.boardMilestoneId, 10),
milestoneTitle: el.dataset.boardMilestoneTitle || '',
iterationId: parseInt(el.dataset.boardIterationId, 10),
iterationTitle: el.dataset.boardIterationTitle || '',
assigneeId: el.dataset.boardAssigneeId,
assigneeUsername: el.dataset.boardAssigneeUsername,
labels: el.dataset.labels ? JSON.parse(el.dataset.labels) : [],
labelIds: el.dataset.labelIds ? JSON.parse(el.dataset.labelIds) : [],
weight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null,
},
});
const boardConfigPath = transformBoardConfig(store.state.boardConfig);
if (boardConfigPath !== '') {
const filterPath = window.location.search ? `${window.location.search}&` : '?';
updateHistory({
url: `${filterPath}${transformBoardConfig(store.state.boardConfig)}`,
});
}
// eslint-disable-next-line no-new
new Vue({
el,
store,
apolloProvider,
provide: {
disabled: parseBoolean(el.dataset.disabled),
boardId,
groupId: parseInt(groupId, 10),
rootPath,
currentUserId: gon.current_user_id || null,
canUpdate: parseBoolean(el.dataset.canUpdate),
canAdminList: parseBoolean(el.dataset.canAdminList),
labelsFetchPath: el.dataset.labelsFetchPath,
labelsManagePath: el.dataset.labelsManagePath,
labelsFilterBasePath: el.dataset.labelsFilterBasePath,
timeTrackingLimitToHours: parseBoolean(el.dataset.timeTrackingLimitToHours),
multipleAssigneesFeatureAvailable: parseBoolean(el.dataset.multipleAssigneesFeatureAvailable),
epicFeatureAvailable: parseBoolean(el.dataset.epicFeatureAvailable),
iterationFeatureAvailable: parseBoolean(el.dataset.iterationFeatureAvailable),
weightFeatureAvailable: parseBoolean(el.dataset.weightFeatureAvailable),
boardWeight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null,
scopedLabelsAvailable: parseBoolean(el.dataset.scopedLabels),
milestoneListsAvailable: false,
assigneeListsAvailable: false,
iterationListsAvailable: false,
issuableType: issuableTypes.epic,
emailsDisabled: parseBoolean(el.dataset.emailsDisabled),
},
render: (createComponent) => createComponent(BoardApp),
});
}
export default () => {
const $boardApp = document.getElementById('board-app');
const $boardApp = document.getElementById('js-issuable-board-app');
// check for browser back and trigger a hard reload to circumvent browser caching.
window.addEventListener('pageshow', (event) => {
......@@ -57,91 +118,7 @@ export default () => {
initFilteredSearch(apolloProvider);
// eslint-disable-next-line no-new
new Vue({
el: $boardApp,
components: {
BoardContent,
BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'),
},
provide: {
boardId: $boardApp.dataset.boardId,
groupId: parseInt($boardApp.dataset.groupId, 10),
rootPath: $boardApp.dataset.rootPath,
currentUserId: gon.current_user_id || null,
canUpdate: parseBoolean($boardApp.dataset.canUpdate),
canAdminList: parseBoolean($boardApp.dataset.canAdminList),
labelsFetchPath: $boardApp.dataset.labelsFetchPath,
labelsManagePath: $boardApp.dataset.labelsManagePath,
labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath,
timeTrackingLimitToHours: parseBoolean($boardApp.dataset.timeTrackingLimitToHours),
weightFeatureAvailable: parseBoolean($boardApp.dataset.weightFeatureAvailable),
boardWeight: $boardApp.dataset.boardWeight
? parseInt($boardApp.dataset.boardWeight, 10)
: null,
scopedLabelsAvailable: parseBoolean($boardApp.dataset.scopedLabels),
milestoneListsAvailable: false,
assigneeListsAvailable: false,
iterationListsAvailable: false,
emailsDisabled: parseBoolean($boardApp.dataset.emailsDisabled),
},
store,
apolloProvider,
data() {
return {
state: {},
loading: 0,
allowSubEpics: parseBoolean($boardApp.dataset.subEpicsFeatureAvailable),
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
disabled: parseBoolean($boardApp.dataset.disabled),
parent: $boardApp.dataset.parent,
detailIssueVisible: false,
};
},
computed: {
...mapState(['boardConfig']),
},
created() {
this.setInitialBoardData({
allowSubEpics: this.allowSubEpics,
boardId: $boardApp.dataset.boardId,
fullBoardId: fullEpicBoardId($boardApp.dataset.boardId),
fullPath: $boardApp.dataset.fullPath,
boardType: this.parent,
disabled: this.disabled,
issuableType: issuableTypes.epic,
boardConfig: {
milestoneId: parseInt($boardApp.dataset.boardMilestoneId, 10),
milestoneTitle: $boardApp.dataset.boardMilestoneTitle || '',
iterationId: parseInt($boardApp.dataset.boardIterationId, 10),
iterationTitle: $boardApp.dataset.boardIterationTitle || '',
assigneeId: $boardApp.dataset.boardAssigneeId,
assigneeUsername: $boardApp.dataset.boardAssigneeUsername,
labels: $boardApp.dataset.labels ? JSON.parse($boardApp.dataset.labels) : [],
labelIds: $boardApp.dataset.labelIds ? JSON.parse($boardApp.dataset.labelIds) : [],
weight: $boardApp.dataset.boardWeight
? parseInt($boardApp.dataset.boardWeight, 10)
: null,
},
});
},
mounted() {
const boardConfigPath = transformBoardConfig(this.boardConfig);
if (boardConfigPath !== '') {
const filterPath = window.location.search ? `${window.location.search}&` : '?';
updateHistory({
url: `${filterPath}${transformBoardConfig(this.boardConfig)}`,
});
}
this.performSearch();
},
methods: {
...mapActions(['setInitialBoardData', 'performSearch']),
getNodes(data) {
return data[this.parent]?.board?.lists.nodes;
},
},
});
mountBoardApp($boardApp);
const createColumnTriggerEl = document.querySelector('.js-create-column-trigger');
if (createColumnTriggerEl) {
......
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import BoardApp from '~/boards/components/board_app.vue';
describe('BoardApp', () => {
let wrapper;
let store;
Vue.use(Vuex);
const createStore = ({ mockGetters = {} } = {}) => {
store = new Vuex.Store({
state: {},
actions: {
performSearch: jest.fn(),
},
getters: {
isSidebarOpen: () => true,
...mockGetters,
},
});
};
const createComponent = ({ provide = { disabled: true } } = {}) => {
wrapper = shallowMount(BoardApp, {
store,
provide: {
...provide,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
store = null;
});
it("should have 'is-compact' class when sidebar is open", () => {
createStore();
createComponent();
expect(wrapper.classes()).toContain('is-compact');
});
it("should not have 'is-compact' class when sidebar is closed", () => {
createStore({ mockGetters: { isSidebarOpen: () => false } });
createComponent();
expect(wrapper.classes()).not.toContain('is-compact');
});
});
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