Commit 053b6d29 authored by Simon Knox's avatar Simon Knox

Merge branch '355130-consolidate-boards' into 'master'

Consolidate boards into one Vue app

See merge request gitlab-org/gitlab!83359
parents 1ed82e08 b8c60584
...@@ -2,11 +2,13 @@ ...@@ -2,11 +2,13 @@
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import BoardContent from '~/boards/components/board_content.vue'; import BoardContent from '~/boards/components/board_content.vue';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue'; import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import BoardTopBar from '~/boards/components/board_top_bar.vue';
export default { export default {
components: { components: {
BoardContent, BoardContent,
BoardSettingsSidebar, BoardSettingsSidebar,
BoardTopBar,
}, },
inject: ['disabled'], inject: ['disabled'],
computed: { computed: {
...@@ -23,6 +25,7 @@ export default { ...@@ -23,6 +25,7 @@ export default {
<template> <template>
<div class="boards-app gl-relative" :class="{ 'is-compact': isSidebarOpen }"> <div class="boards-app gl-relative" :class="{ 'is-compact': isSidebarOpen }">
<board-top-bar />
<board-content :disabled="disabled" /> <board-content :disabled="disabled" />
<board-settings-sidebar /> <board-settings-sidebar />
</div> </div>
......
...@@ -48,7 +48,7 @@ export default { ...@@ -48,7 +48,7 @@ export default {
fullPath: { fullPath: {
default: '', default: '',
}, },
rootPath: { boardBaseUrl: {
default: '', default: '',
}, },
}, },
...@@ -209,7 +209,7 @@ export default { ...@@ -209,7 +209,7 @@ export default {
if (this.isDeleteForm) { if (this.isDeleteForm) {
try { try {
await this.deleteBoard(); await this.deleteBoard();
visitUrl(this.rootPath); visitUrl(this.boardBaseUrl);
} catch { } catch {
this.setError({ message: this.$options.i18n.deleteErrorMessage }); this.setError({ message: this.$options.i18n.deleteErrorMessage });
} finally { } finally {
......
<script>
import { mapGetters } from 'vuex';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue';
import IssueBoardFilteredSearch from 'ee_else_ce/boards/components/issue_board_filtered_search.vue';
import ConfigToggle from './config_toggle.vue';
import NewBoardButton from './new_board_button.vue';
import ToggleFocus from './toggle_focus.vue';
export default {
components: {
BoardAddNewColumnTrigger,
BoardsSelector,
IssueBoardFilteredSearch,
ConfigToggle,
NewBoardButton,
ToggleFocus,
ToggleLabels: () => import('ee_component/boards/components/toggle_labels.vue'),
ToggleEpicsSwimlanes: () => import('ee_component/boards/components/toggle_epics_swimlanes.vue'),
EpicBoardFilteredSearch: () =>
import('ee_component/boards/components/epic_filtered_search.vue'),
},
inject: ['swimlanesFeatureAvailable', 'canAdminList', 'isSignedIn'],
computed: {
...mapGetters(['isEpicBoard']),
},
};
</script>
<template>
<div class="issues-filters">
<div
class="issues-details-filters filtered-search-block gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row row-content-block second-block"
>
<div
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-flex-grow-1 gl-lg-mb-0! mb-md-2 mb-sm-0 gl-w-full"
>
<boards-selector />
<new-board-button />
<epic-board-filtered-search v-if="isEpicBoard" />
<issue-board-filtered-search v-else />
</div>
<div
class="filter-dropdown-container gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-align-items-flex-start"
>
<toggle-labels />
<toggle-epics-swimlanes v-if="swimlanesFeatureAvailable && isSignedIn" />
<config-toggle />
<board-add-new-column-trigger v-if="canAdminList" />
<toggle-focus />
</div>
</div>
</div>
</template>
...@@ -40,37 +40,21 @@ export default { ...@@ -40,37 +40,21 @@ export default {
directives: { directives: {
GlModalDirective, GlModalDirective,
}, },
inject: ['fullPath'], inject: [
'boardBaseUrl',
'fullPath',
'canAdminBoard',
'multipleIssueBoardsAvailable',
'hasMissingBoards',
'scopedIssueBoardFeatureEnabled',
'weights',
],
props: { props: {
throttleDuration: { throttleDuration: {
type: Number, type: Number,
default: 200, default: 200,
required: false, required: false,
}, },
boardBaseUrl: {
type: String,
required: true,
},
hasMissingBoards: {
type: Boolean,
required: true,
},
canAdminBoard: {
type: Boolean,
required: true,
},
multipleIssueBoardsAvailable: {
type: Boolean,
required: true,
},
scopedIssueBoardFeatureEnabled: {
type: Boolean,
required: true,
},
weights: {
type: Array,
required: true,
},
}, },
data() { data() {
return { return {
......
...@@ -14,16 +14,7 @@ export default { ...@@ -14,16 +14,7 @@ export default {
GlModalDirective, GlModalDirective,
}, },
mixins: [Tracking.mixin()], mixins: [Tracking.mixin()],
props: { inject: ['canAdminList', 'hasScope'],
canAdminList: {
type: Boolean,
required: true,
},
hasScope: {
type: Boolean,
required: true,
},
},
computed: { computed: {
buttonText() { buttonText() {
return this.canAdminList ? s__('Boards|Edit board') : s__('Boards|View scope'); return this.canAdminList ? s__('Boards|Edit board') : s__('Boards|View scope');
......
...@@ -41,17 +41,7 @@ export default { ...@@ -41,17 +41,7 @@ export default {
confidential: __('Confidential'), confidential: __('Confidential'),
}, },
components: { BoardFilteredSearch }, components: { BoardFilteredSearch },
inject: ['isSignedIn', 'releasesFetchPath'], inject: ['isSignedIn', 'releasesFetchPath', 'fullPath', 'boardType'],
props: {
fullPath: {
type: String,
required: true,
},
boardType: {
type: String,
required: true,
},
},
computed: { computed: {
isGroupBoard() { isGroupBoard() {
return this.boardType === BoardType.group; return this.boardType === BoardType.group;
......
...@@ -10,12 +10,6 @@ export default { ...@@ -10,12 +10,6 @@ export default {
directives: { directives: {
GlTooltip, GlTooltip,
}, },
props: {
issueBoardsContentSelector: {
type: String,
required: true,
},
},
data() { data() {
return { return {
isFullscreen: false, isFullscreen: false,
...@@ -25,7 +19,7 @@ export default { ...@@ -25,7 +19,7 @@ export default {
toggleFocusMode() { toggleFocusMode() {
hide(this.$refs.toggleFocusModeButton); hide(this.$refs.toggleFocusModeButton);
const issueBoardsContent = document.querySelector(this.issueBoardsContentSelector); const issueBoardsContent = document.querySelector('.content-wrapper > .js-focus-mode-board');
issueBoardsContent.classList.toggle('is-focused'); issueBoardsContent.classList.toggle('is-focused');
this.isFullscreen = !this.isFullscreen; this.isFullscreen = !this.isFullscreen;
......
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 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 BoardApp from '~/boards/components/board_app.vue'; import BoardApp from '~/boards/components/board_app.vue';
import '~/boards/filters/due_date_filters'; import '~/boards/filters/due_date_filters';
import { issuableTypes } from '~/boards/constants'; import { issuableTypes } from '~/boards/constants';
import initBoardsFilteredSearch from '~/boards/mount_filtered_search_issue_boards';
import store from '~/boards/stores'; import store from '~/boards/stores';
import toggleFocusMode from '~/boards/toggle_focus'; import {
import { NavigationType, isLoggedIn, parseBoolean } from '~/lib/utils/common_utils'; NavigationType,
isLoggedIn,
parseBoolean,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
import { fullBoardId } from './boards_util'; import { fullBoardId } from './boards_util';
import boardConfigToggle from './config_toggle';
import initNewBoard from './new_board'; import initNewBoard from './new_board';
import { gqlClient } from './graphql'; import { gqlClient } from './graphql';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
Vue.use(VueApollo); Vue.use(VueApollo);
Vue.use(PortalVue); Vue.use(PortalVue);
...@@ -28,6 +26,12 @@ const apolloProvider = new VueApollo({ ...@@ -28,6 +26,12 @@ const apolloProvider = new VueApollo({
function mountBoardApp(el) { function mountBoardApp(el) {
const { boardId, groupId, fullPath, rootPath } = el.dataset; const { boardId, groupId, fullPath, rootPath } = el.dataset;
const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
const initialFilterParams = {
...convertObjectPropsToCamelCase(rawFilterParams),
};
store.dispatch('fetchBoard', { store.dispatch('fetchBoard', {
fullPath, fullPath,
fullBoardId: fullBoardId(boardId), fullBoardId: fullBoardId(boardId),
...@@ -54,26 +58,41 @@ function mountBoardApp(el) { ...@@ -54,26 +58,41 @@ function mountBoardApp(el) {
boardId, boardId,
groupId: Number(groupId), groupId: Number(groupId),
rootPath, rootPath,
fullPath,
initialFilterParams,
boardBaseUrl: el.dataset.boardBaseUrl,
boardType: el.dataset.parent,
currentUserId: gon.current_user_id || null, currentUserId: gon.current_user_id || null,
canUpdate: parseBoolean(el.dataset.canUpdate), boardWeight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null,
canAdminList: parseBoolean(el.dataset.canAdminList),
labelsManagePath: el.dataset.labelsManagePath, labelsManagePath: el.dataset.labelsManagePath,
labelsFilterBasePath: el.dataset.labelsFilterBasePath, labelsFilterBasePath: el.dataset.labelsFilterBasePath,
releasesFetchPath: el.dataset.releasesFetchPath,
timeTrackingLimitToHours: parseBoolean(el.dataset.timeTrackingLimitToHours), timeTrackingLimitToHours: parseBoolean(el.dataset.timeTrackingLimitToHours),
issuableType: issuableTypes.issue,
emailsDisabled: parseBoolean(el.dataset.emailsDisabled),
hasScope: parseBoolean(el.dataset.hasScope),
hasMissingBoards: parseBoolean(el.dataset.hasMissingBoards),
weights: el.dataset.weights ? JSON.parse(el.dataset.weights) : [],
// Permissions
canUpdate: parseBoolean(el.dataset.canUpdate),
canAdminList: parseBoolean(el.dataset.canAdminList),
canAdminBoard: parseBoolean(el.dataset.canAdminBoard),
allowLabelCreate: parseBoolean(el.dataset.canUpdate),
allowLabelEdit: parseBoolean(el.dataset.canUpdate),
isSignedIn: isLoggedIn(),
// Features
multipleAssigneesFeatureAvailable: parseBoolean(el.dataset.multipleAssigneesFeatureAvailable), multipleAssigneesFeatureAvailable: parseBoolean(el.dataset.multipleAssigneesFeatureAvailable),
epicFeatureAvailable: parseBoolean(el.dataset.epicFeatureAvailable), epicFeatureAvailable: parseBoolean(el.dataset.epicFeatureAvailable),
iterationFeatureAvailable: parseBoolean(el.dataset.iterationFeatureAvailable), iterationFeatureAvailable: parseBoolean(el.dataset.iterationFeatureAvailable),
weightFeatureAvailable: parseBoolean(el.dataset.weightFeatureAvailable), weightFeatureAvailable: parseBoolean(el.dataset.weightFeatureAvailable),
boardWeight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null,
scopedLabelsAvailable: parseBoolean(el.dataset.scopedLabels), scopedLabelsAvailable: parseBoolean(el.dataset.scopedLabels),
milestoneListsAvailable: parseBoolean(el.dataset.milestoneListsAvailable), milestoneListsAvailable: parseBoolean(el.dataset.milestoneListsAvailable),
assigneeListsAvailable: parseBoolean(el.dataset.assigneeListsAvailable), assigneeListsAvailable: parseBoolean(el.dataset.assigneeListsAvailable),
iterationListsAvailable: parseBoolean(el.dataset.iterationListsAvailable), iterationListsAvailable: parseBoolean(el.dataset.iterationListsAvailable),
issuableType: issuableTypes.issue,
emailsDisabled: parseBoolean(el.dataset.emailsDisabled),
allowLabelCreate: parseBoolean(el.dataset.canUpdate),
allowLabelEdit: parseBoolean(el.dataset.canUpdate),
allowScopedLabels: parseBoolean(el.dataset.scopedLabels), allowScopedLabels: parseBoolean(el.dataset.scopedLabels),
swimlanesFeatureAvailable: gon.licensed_features?.swimlanes,
multipleIssueBoardsAvailable: parseBoolean(el.dataset.multipleBoardsAvailable),
scopedIssueBoardFeatureEnabled: parseBoolean(el.dataset.scopedIssueBoardFeatureEnabled),
}, },
render: (createComponent) => createComponent(BoardApp), render: (createComponent) => createComponent(BoardApp),
}); });
...@@ -92,47 +111,7 @@ export default () => { ...@@ -92,47 +111,7 @@ export default () => {
} }
}); });
const { releasesFetchPath, epicFeatureAvailable, iterationFeatureAvailable } = $boardApp.dataset;
initBoardsFilteredSearch(
apolloProvider,
isLoggedIn(),
releasesFetchPath,
parseBoolean(epicFeatureAvailable),
parseBoolean(iterationFeatureAvailable),
);
mountBoardApp($boardApp); mountBoardApp($boardApp);
const createColumnTriggerEl = document.querySelector('.js-create-column-trigger');
if (createColumnTriggerEl) {
// eslint-disable-next-line no-new
new Vue({
el: createColumnTriggerEl,
name: 'BoardAddNewColumnTriggerRoot',
components: {
BoardAddNewColumnTrigger,
},
store,
render(createElement) {
return createElement('board-add-new-column-trigger');
},
});
}
boardConfigToggle();
initNewBoard(); initNewBoard();
toggleFocusMode();
toggleLabels();
if (gon.licensed_features?.swimlanes) {
toggleEpicsSwimlanes();
}
mountMultipleBoardsSwitcher({
fullPath: $boardApp.dataset.fullPath,
rootPath: $boardApp.dataset.boardsEndpoint,
allowScopedLabels: $boardApp.dataset.scopedLabels,
labelsManagePath: $boardApp.dataset.labelsManagePath,
});
}; };
...@@ -16,6 +16,7 @@ module BoardsHelper ...@@ -16,6 +16,7 @@ module BoardsHelper
bulk_update_path: @bulk_issues_path, bulk_update_path: @bulk_issues_path,
can_update: can_update?.to_s, can_update: can_update?.to_s,
can_admin_list: can_admin_list?.to_s, can_admin_list: can_admin_list?.to_s,
can_admin_board: can_admin_board?.to_s,
time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s, time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s,
parent: current_board_parent.model_name.param_key, parent: current_board_parent.model_name.param_key,
group_id: group_id, group_id: group_id,
...@@ -23,7 +24,11 @@ module BoardsHelper ...@@ -23,7 +24,11 @@ module BoardsHelper
labels_fetch_path: labels_fetch_path, labels_fetch_path: labels_fetch_path,
labels_manage_path: labels_manage_path, labels_manage_path: labels_manage_path,
releases_fetch_path: releases_fetch_path, releases_fetch_path: releases_fetch_path,
board_type: board.to_type board_type: board.to_type,
has_scope: board.scoped?.to_s,
has_missing_boards: has_missing_boards?.to_s,
multiple_boards_available: multiple_boards_available?.to_s,
board_base_url: board_base_url
} }
end end
...@@ -85,6 +90,11 @@ module BoardsHelper ...@@ -85,6 +90,11 @@ module BoardsHelper
current_board_parent.multiple_issue_boards_available? current_board_parent.multiple_issue_boards_available?
end end
# Boards are hidden when extra boards were created but the license does not allow multiple boards
def has_missing_boards?
!multiple_boards_available? && current_board_parent.boards.size > 1
end
def current_board_path(board) def current_board_path(board)
@current_board_path ||= if board.group_board? @current_board_path ||= if board.group_board?
group_board_path(current_board_parent, board) group_board_path(current_board_parent, board)
...@@ -109,6 +119,10 @@ module BoardsHelper ...@@ -109,6 +119,10 @@ module BoardsHelper
can?(current_user, :admin_issue_board_list, current_board_parent) can?(current_user, :admin_issue_board_list, current_board_parent)
end end
def can_admin_board?
can?(current_user, :admin_issue_board, current_board_parent)
end
def can_admin_issue? def can_admin_issue?
can?(current_user, :admin_issue, current_board_parent) can?(current_user, :admin_issue, current_board_parent)
end end
......
...@@ -16,6 +16,4 @@ ...@@ -16,6 +16,4 @@
- page_title("#{board.name}", _("Boards")) - page_title("#{board.name}", _("Boards"))
- add_page_specific_style 'page_bundles/boards' - add_page_specific_style 'page_bundles/boards'
= render 'shared/issuable/search_bar', type: :boards, board: board
#js-issuable-board-app{ data: board_data } #js-issuable-board-app{ data: board_data }
...@@ -16,16 +16,7 @@ export default { ...@@ -16,16 +16,7 @@ export default {
author: __('Author'), author: __('Author'),
}, },
components: { BoardFilteredSearch }, components: { BoardFilteredSearch },
props: { inject: ['fullPath', 'boardType'],
fullPath: {
type: String,
required: true,
},
boardType: {
type: String,
required: true,
},
},
computed: { computed: {
tokens() { tokens() {
const { fetchLabels, fetchAuthors } = issueBoardFilter( const { fetchLabels, fetchAuthors } = issueBoardFilter(
......
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import initFilteredSearch from 'ee/boards/epic_filtered_search';
import { fullEpicBoardId } from 'ee_component/boards/boards_util'; import { fullEpicBoardId } 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 BoardApp from '~/boards/components/board_app.vue'; import BoardApp from '~/boards/components/board_app.vue';
import boardConfigToggle from '~/boards/config_toggle';
import { issuableTypes } from '~/boards/constants'; import { issuableTypes } from '~/boards/constants';
import mountMultipleBoardsSwitcher from '~/boards/mount_multiple_boards_switcher';
import store from '~/boards/stores'; import store from '~/boards/stores';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import '~/boards/filters/due_date_filters'; import '~/boards/filters/due_date_filters';
import { NavigationType, parseBoolean } from '~/lib/utils/common_utils'; import {
NavigationType,
isLoggedIn,
parseBoolean,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -25,6 +26,12 @@ const apolloProvider = new VueApollo({ ...@@ -25,6 +26,12 @@ const apolloProvider = new VueApollo({
function mountBoardApp(el) { function mountBoardApp(el) {
const { boardId, groupId, fullPath, rootPath } = el.dataset; const { boardId, groupId, fullPath, rootPath } = el.dataset;
const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
const initialFilterParams = {
...convertObjectPropsToCamelCase(rawFilterParams),
};
store.dispatch('setInitialBoardData', { store.dispatch('setInitialBoardData', {
allowSubEpics: parseBoolean(el.dataset.subEpicsFeatureAvailable), allowSubEpics: parseBoolean(el.dataset.subEpicsFeatureAvailable),
boardType: el.dataset.parent, boardType: el.dataset.parent,
...@@ -46,27 +53,41 @@ function mountBoardApp(el) { ...@@ -46,27 +53,41 @@ function mountBoardApp(el) {
boardId, boardId,
groupId: parseInt(groupId, 10), groupId: parseInt(groupId, 10),
rootPath, rootPath,
fullPath,
initialFilterParams,
boardBaseUrl: el.dataset.boardBaseUrl,
boardType: el.dataset.parent,
currentUserId: gon.current_user_id || null, currentUserId: gon.current_user_id || null,
canUpdate: parseBoolean(el.dataset.canUpdate),
canAdminList: parseBoolean(el.dataset.canAdminList),
labelsFetchPath: el.dataset.labelsFetchPath, labelsFetchPath: el.dataset.labelsFetchPath,
labelsManagePath: el.dataset.labelsManagePath, labelsManagePath: el.dataset.labelsManagePath,
labelsFilterBasePath: el.dataset.labelsFilterBasePath, labelsFilterBasePath: el.dataset.labelsFilterBasePath,
timeTrackingLimitToHours: parseBoolean(el.dataset.timeTrackingLimitToHours), timeTrackingLimitToHours: parseBoolean(el.dataset.timeTrackingLimitToHours),
boardWeight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null,
issuableType: issuableTypes.epic,
emailsDisabled: parseBoolean(el.dataset.emailsDisabled),
hasMissingBoards: parseBoolean(el.dataset.hasMissingBoards),
hasScope: parseBoolean(el.dataset.hasScope),
weights: JSON.parse(el.dataset.weights),
// Permissions
canUpdate: parseBoolean(el.dataset.canUpdate),
canAdminList: parseBoolean(el.dataset.canAdminList),
canAdminBoard: parseBoolean(el.dataset.canAdminBoard),
allowLabelCreate: parseBoolean(el.dataset.canUpdate),
allowLabelEdit: parseBoolean(el.dataset.canUpdate),
allowScopedLabels: parseBoolean(el.dataset.scopedLabels),
isSignedIn: isLoggedIn(),
// Features
multipleAssigneesFeatureAvailable: parseBoolean(el.dataset.multipleAssigneesFeatureAvailable), multipleAssigneesFeatureAvailable: parseBoolean(el.dataset.multipleAssigneesFeatureAvailable),
epicFeatureAvailable: parseBoolean(el.dataset.epicFeatureAvailable), epicFeatureAvailable: parseBoolean(el.dataset.epicFeatureAvailable),
iterationFeatureAvailable: parseBoolean(el.dataset.iterationFeatureAvailable), iterationFeatureAvailable: parseBoolean(el.dataset.iterationFeatureAvailable),
weightFeatureAvailable: parseBoolean(el.dataset.weightFeatureAvailable), weightFeatureAvailable: parseBoolean(el.dataset.weightFeatureAvailable),
boardWeight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null,
scopedLabelsAvailable: parseBoolean(el.dataset.scopedLabels), scopedLabelsAvailable: parseBoolean(el.dataset.scopedLabels),
milestoneListsAvailable: false, milestoneListsAvailable: false,
assigneeListsAvailable: false, assigneeListsAvailable: false,
iterationListsAvailable: false, iterationListsAvailable: false,
issuableType: issuableTypes.epic, swimlanesFeatureAvailable: false,
emailsDisabled: parseBoolean(el.dataset.emailsDisabled), multipleIssueBoardsAvailable: true,
allowLabelCreate: parseBoolean(el.dataset.canUpdate), scopedIssueBoardFeatureEnabled: true,
allowLabelEdit: parseBoolean(el.dataset.canUpdate),
allowScopedLabels: parseBoolean(el.dataset.scopedLabels),
}, },
render: (createComponent) => createComponent(BoardApp), render: (createComponent) => createComponent(BoardApp),
}); });
...@@ -85,31 +106,5 @@ export default () => { ...@@ -85,31 +106,5 @@ export default () => {
} }
}); });
initFilteredSearch(apolloProvider);
mountBoardApp($boardApp); mountBoardApp($boardApp);
const createColumnTriggerEl = document.querySelector('.js-create-column-trigger');
if (createColumnTriggerEl) {
// eslint-disable-next-line no-new
new Vue({
el: createColumnTriggerEl,
name: 'BoardAddNewColumnTriggerRoot',
components: {
BoardAddNewColumnTrigger,
},
store,
render(createElement) {
return createElement(BoardAddNewColumnTrigger);
},
});
}
toggleLabels();
boardConfigToggle();
mountMultipleBoardsSwitcher({
fullPath: $boardApp.dataset.fullPath,
rootPath: $boardApp.dataset.boardsEndpoint,
});
}; };
...@@ -29,8 +29,10 @@ module EE ...@@ -29,8 +29,10 @@ module EE
show_promotion: show_feature_promotion, show_promotion: show_feature_promotion,
can_update: can_update?.to_s, can_update: can_update?.to_s,
can_admin_list: can_admin_list?.to_s, can_admin_list: can_admin_list?.to_s,
can_admin_board: can_admin_board?.to_s,
disabled: board.disabled_for?(current_user).to_s, disabled: board.disabled_for?(current_user).to_s,
emails_disabled: current_board_parent.emails_disabled?.to_s emails_disabled: current_board_parent.emails_disabled?.to_s,
weights: ::Issue.weight_options
} }
super.merge(data).merge(licensed_features).merge(group_level_features) super.merge(data).merge(licensed_features).merge(group_level_features)
...@@ -43,7 +45,8 @@ module EE ...@@ -43,7 +45,8 @@ module EE
weight_feature_available: current_board_parent.feature_available?(:issue_weights).to_s, weight_feature_available: current_board_parent.feature_available?(:issue_weights).to_s,
milestone_lists_available: current_board_parent.feature_available?(:board_milestone_lists).to_s, milestone_lists_available: current_board_parent.feature_available?(:board_milestone_lists).to_s,
assignee_lists_available: current_board_parent.feature_available?(:board_assignee_lists).to_s, assignee_lists_available: current_board_parent.feature_available?(:board_assignee_lists).to_s,
scoped_labels: current_board_parent.feature_available?(:scoped_labels)&.to_s scoped_labels: current_board_parent.feature_available?(:scoped_labels)&.to_s,
scoped_issue_board_feature_enabled: current_board_parent.feature_available?(:scoped_issue_board).to_s
} }
end end
...@@ -71,6 +74,13 @@ module EE ...@@ -71,6 +74,13 @@ module EE
super super
end end
override :can_admin_board?
def can_admin_board?
return can?(current_user, :admin_epic_board, current_board_parent) if board.is_a?(::Boards::EpicBoard)
super
end
override :build_issue_link_base override :build_issue_link_base
def build_issue_link_base def build_issue_link_base
return group_epics_path(@group) if board.is_a?(::Boards::EpicBoard) return group_epics_path(@group) if board.is_a?(::Boards::EpicBoard)
......
...@@ -220,7 +220,7 @@ module EE ...@@ -220,7 +220,7 @@ module EE
end end
def weight_options def weight_options
[WEIGHT_NONE] + WEIGHT_RANGE.to_a [WEIGHT_NONE, WEIGHT_ANY] + WEIGHT_RANGE.to_a
end end
end end
......
...@@ -73,7 +73,7 @@ describe('BoardForm', () => { ...@@ -73,7 +73,7 @@ describe('BoardForm', () => {
wrapper = shallowMountExtended(BoardForm, { wrapper = shallowMountExtended(BoardForm, {
propsData: { ...defaultProps, ...props }, propsData: { ...defaultProps, ...props },
provide: { provide: {
rootPath: 'root', boardBaseUrl: 'root',
glFeatures: { iterationCadences }, glFeatures: { iterationCadences },
}, },
mocks: { mocks: {
......
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import ToggleEpicsSwimlanes from 'ee/boards/components/toggle_epics_swimlanes.vue';
import IssueBoardFilteredSearch from 'ee/boards/components/issue_board_filtered_search.vue';
import EpicBoardFilteredSearch from 'ee/boards/components/epic_filtered_search.vue';
import ToggleLabels from 'ee/boards/components/toggle_labels.vue';
import BoardTopBar from '~/boards/components/board_top_bar.vue';
import BoardsSelector from '~/boards/components/boards_selector.vue';
import ConfigToggle from '~/boards/components/config_toggle.vue';
import NewBoardButton from '~/boards/components/new_board_button.vue';
import ToggleFocus from '~/boards/components/toggle_focus.vue';
describe('BoardTopBar', () => {
let wrapper;
Vue.use(Vuex);
const createStore = ({ mockGetters = {} } = {}) => {
return new Vuex.Store({
state: {},
getters: {
isEpicBoard: () => false,
...mockGetters,
},
});
};
const createComponent = ({ provide = {}, mockGetters = {} } = {}) => {
const store = createStore({ mockGetters });
wrapper = shallowMount(BoardTopBar, {
store,
provide: {
swimlanesFeatureAvailable: false,
canAdminList: false,
isSignedIn: false,
fullPath: 'gitlab-org',
boardType: 'group',
releasesFetchPath: '/releases',
epicFeatureAvailable: true,
iterationFeatureAvailable: true,
...provide,
},
stubs: { IssueBoardFilteredSearch, EpicBoardFilteredSearch },
});
};
afterEach(() => {
wrapper.destroy();
});
describe('base template', () => {
beforeEach(() => {
createComponent();
});
it('renders BoardsSelector component', () => {
expect(wrapper.findComponent(BoardsSelector).exists()).toBe(true);
});
it('renders NewBoardButton component', () => {
expect(wrapper.findComponent(NewBoardButton).exists()).toBe(true);
});
it('renders ConfigToggle component', () => {
expect(wrapper.findComponent(ConfigToggle).exists()).toBe(true);
});
it('renders ToggleFocus component', () => {
expect(wrapper.findComponent(ToggleFocus).exists()).toBe(true);
});
it('renders ToggleLabels component', () => {
expect(wrapper.findComponent(ToggleLabels).exists()).toBe(true);
});
it('does not render ToggleEpicsSwimlanes component', () => {
expect(wrapper.findComponent(ToggleEpicsSwimlanes).exists()).toBe(false);
});
});
describe('filter bar', () => {
it.each`
isEpicBoard | filterBarComponent | filterBarName | otherFilterBar
${false} | ${IssueBoardFilteredSearch} | ${'IssueBoardFilteredSearch'} | ${EpicBoardFilteredSearch}
${true} | ${EpicBoardFilteredSearch} | ${'EpicBoardFilteredSearch'} | ${IssueBoardFilteredSearch}
`(
'renders $filterBarName when isEpicBoard is $isEpicBoard',
async ({ isEpicBoard, filterBarComponent, otherFilterBar }) => {
createComponent({ mockGetters: { isEpicBoard: () => isEpicBoard } });
await nextTick();
expect(wrapper.findComponent(filterBarComponent).exists()).toBe(true);
expect(wrapper.findComponent(otherFilterBar).exists()).toBe(false);
},
);
});
describe('when user is logged in and swimlanes are available', () => {
beforeEach(() => {
createComponent({
provide: {
swimlanesFeatureAvailable: true,
isSignedIn: true,
},
});
it('renders ToggleEpicsSwimlanes component', () => {
expect(wrapper.findComponent(ToggleEpicsSwimlanes).exists()).toBe(true);
});
});
});
});
...@@ -80,6 +80,10 @@ describe('BoardsSelector', () => { ...@@ -80,6 +80,10 @@ describe('BoardsSelector', () => {
apolloProvider: fakeApollo, apolloProvider: fakeApollo,
propsData: { propsData: {
throttleDuration, throttleDuration,
},
attachTo: document.body,
provide: {
fullPath: '',
boardBaseUrl: `${TEST_HOST}/board/base/url`, boardBaseUrl: `${TEST_HOST}/board/base/url`,
hasMissingBoards: false, hasMissingBoards: false,
canAdminBoard: true, canAdminBoard: true,
...@@ -87,10 +91,6 @@ describe('BoardsSelector', () => { ...@@ -87,10 +91,6 @@ describe('BoardsSelector', () => {
scopedIssueBoardFeatureEnabled: true, scopedIssueBoardFeatureEnabled: true,
weights: [], weights: [],
}, },
attachTo: document.body,
provide: {
fullPath: '',
},
}); });
}; };
......
...@@ -13,11 +13,7 @@ describe('EpicFilteredSearch', () => { ...@@ -13,11 +13,7 @@ describe('EpicFilteredSearch', () => {
const createComponent = ({ initialFilterParams = {} } = {}) => { const createComponent = ({ initialFilterParams = {} } = {}) => {
wrapper = shallowMount(EpicFilteredSearch, { wrapper = shallowMount(EpicFilteredSearch, {
provide: { initialFilterParams }, provide: { initialFilterParams, fullPath: '', boardType: '' },
props: {
fullPath: '',
boardType: '',
},
}); });
}; };
......
...@@ -12,10 +12,11 @@ describe('IssueBoardFilter', () => { ...@@ -12,10 +12,11 @@ describe('IssueBoardFilter', () => {
const createComponent = () => { const createComponent = () => {
wrapper = shallowMount(IssueBoardFilteredSpec, { wrapper = shallowMount(IssueBoardFilteredSpec, {
propsData: { fullPath: 'gitlab-org', boardType: 'group' },
provide: { provide: {
isSignedIn: true, isSignedIn: true,
releasesFetchPath: '/releases', releasesFetchPath: '/releases',
fullPath: 'gitlab-org',
boardType: 'group',
epicFeatureAvailable: true, epicFeatureAvailable: true,
iterationFeatureAvailable: true, iterationFeatureAvailable: true,
}, },
......
...@@ -74,6 +74,7 @@ RSpec.describe BoardsHelper do ...@@ -74,6 +74,7 @@ RSpec.describe BoardsHelper do
allow(helper).to receive(:can?).with(user, :create_non_backlog_issues, project_board).and_return(true) allow(helper).to receive(:can?).with(user, :create_non_backlog_issues, project_board).and_return(true)
allow(helper).to receive(:can?).with(user, :admin_issue, project_board).and_return(true) allow(helper).to receive(:can?).with(user, :admin_issue, project_board).and_return(true)
allow(helper).to receive(:can?).with(user, :admin_issue_board_list, project).and_return(false) allow(helper).to receive(:can?).with(user, :admin_issue_board_list, project).and_return(false)
allow(helper).to receive(:can?).with(user, :admin_issue_board, project).and_return(false)
end end
shared_examples 'serializes the availability of a licensed feature' do |feature_name, feature_key| shared_examples 'serializes the availability of a licensed feature' do |feature_name, feature_key|
...@@ -123,7 +124,8 @@ RSpec.describe BoardsHelper do ...@@ -123,7 +124,8 @@ RSpec.describe BoardsHelper do
[:issue_weights, :weight_feature_available], [:issue_weights, :weight_feature_available],
[:board_milestone_lists, :milestone_lists_available], [:board_milestone_lists, :milestone_lists_available],
[:board_assignee_lists, :assignee_lists_available], [:board_assignee_lists, :assignee_lists_available],
[:scoped_labels, :scoped_labels]].each do |feature_name, feature_key| [:scoped_labels, :scoped_labels],
[:scoped_issue_board, :scoped_issue_board_feature_enabled]].each do |feature_name, feature_key|
include_examples "serializes the availability of a licensed feature", feature_name, feature_key include_examples "serializes the availability of a licensed feature", feature_name, feature_key
end end
end end
...@@ -148,9 +150,11 @@ RSpec.describe BoardsHelper do ...@@ -148,9 +150,11 @@ RSpec.describe BoardsHelper do
allow(helper).to receive(:can?).with(user, :create_non_backlog_issues, epic_board).and_return(false) allow(helper).to receive(:can?).with(user, :create_non_backlog_issues, epic_board).and_return(false)
allow(helper).to receive(:can?).with(user, :admin_epic, epic_board).and_return(true) allow(helper).to receive(:can?).with(user, :admin_epic, epic_board).and_return(true)
allow(helper).to receive(:can?).with(user, :admin_epic_board_list, group).and_return(true) allow(helper).to receive(:can?).with(user, :admin_epic_board_list, group).and_return(true)
allow(helper).to receive(:can?).with(user, :admin_epic_board, group).and_return(true)
allow(helper).to receive(:can?).with(user, :admin_issue, group).and_return(false) allow(helper).to receive(:can?).with(user, :admin_issue, group).and_return(false)
allow(helper).to receive(:can?).with(user, :admin_issue_board_list, group).and_return(false) allow(helper).to receive(:can?).with(user, :admin_issue_board_list, group).and_return(false)
allow(helper).to receive(:can?).with(user, :admin_issue_board, group).and_return(false)
end end
it 'returns the correct permission for updating the board' do it 'returns the correct permission for updating the board' do
...@@ -160,6 +164,10 @@ RSpec.describe BoardsHelper do ...@@ -160,6 +164,10 @@ RSpec.describe BoardsHelper do
it 'returns the correct permission for administering the boards lists' do it 'returns the correct permission for administering the boards lists' do
expect(board_data[:can_admin_list]).to eq "true" expect(board_data[:can_admin_list]).to eq "true"
end end
it 'returns the correct permission for administering the boards' do
expect(board_data[:can_admin_board]).to eq "true"
end
end end
end end
end end
...@@ -62,7 +62,7 @@ describe('BoardForm', () => { ...@@ -62,7 +62,7 @@ describe('BoardForm', () => {
}; };
}, },
provide: { provide: {
rootPath: 'root', boardBaseUrl: 'root',
}, },
mocks: { mocks: {
$apollo: { $apollo: {
......
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import BoardTopBar from '~/boards/components/board_top_bar.vue';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardsSelector from '~/boards/components/boards_selector.vue';
import ConfigToggle from '~/boards/components/config_toggle.vue';
import IssueBoardFilteredSearch from '~/boards/components/issue_board_filtered_search.vue';
import NewBoardButton from '~/boards/components/new_board_button.vue';
import ToggleFocus from '~/boards/components/toggle_focus.vue';
describe('BoardTopBar', () => {
let wrapper;
Vue.use(Vuex);
const createStore = ({ mockGetters = {} } = {}) => {
return new Vuex.Store({
state: {},
getters: {
isEpicBoard: () => false,
...mockGetters,
},
});
};
const createComponent = ({ provide = {}, mockGetters = {} } = {}) => {
const store = createStore({ mockGetters });
wrapper = shallowMount(BoardTopBar, {
store,
provide: {
swimlanesFeatureAvailable: false,
canAdminList: false,
isSignedIn: false,
fullPath: 'gitlab-org',
boardType: 'group',
releasesFetchPath: '/releases',
...provide,
},
stubs: { IssueBoardFilteredSearch },
});
};
afterEach(() => {
wrapper.destroy();
});
describe('base template', () => {
beforeEach(() => {
createComponent();
});
it('renders BoardsSelector component', () => {
expect(wrapper.findComponent(BoardsSelector).exists()).toBe(true);
});
it('renders IssueBoardFilteredSearch component', () => {
expect(wrapper.findComponent(IssueBoardFilteredSearch).exists()).toBe(true);
});
it('renders NewBoardButton component', () => {
expect(wrapper.findComponent(NewBoardButton).exists()).toBe(true);
});
it('renders ConfigToggle component', () => {
expect(wrapper.findComponent(ConfigToggle).exists()).toBe(true);
});
it('renders ToggleFocus component', () => {
expect(wrapper.findComponent(ToggleFocus).exists()).toBe(true);
});
it('does not render BoardAddNewColumnTrigger component', () => {
expect(wrapper.findComponent(BoardAddNewColumnTrigger).exists()).toBe(false);
});
});
describe('when user can admin list', () => {
beforeEach(() => {
createComponent({ provide: { canAdminList: true } });
});
it('renders BoardAddNewColumnTrigger component', () => {
expect(wrapper.findComponent(BoardAddNewColumnTrigger).exists()).toBe(true);
});
});
});
...@@ -105,6 +105,10 @@ describe('BoardsSelector', () => { ...@@ -105,6 +105,10 @@ describe('BoardsSelector', () => {
apolloProvider: fakeApollo, apolloProvider: fakeApollo,
propsData: { propsData: {
throttleDuration, throttleDuration,
},
attachTo: document.body,
provide: {
fullPath: '',
boardBaseUrl: `${TEST_HOST}/board/base/url`, boardBaseUrl: `${TEST_HOST}/board/base/url`,
hasMissingBoards: false, hasMissingBoards: false,
canAdminBoard: true, canAdminBoard: true,
...@@ -112,10 +116,6 @@ describe('BoardsSelector', () => { ...@@ -112,10 +116,6 @@ describe('BoardsSelector', () => {
scopedIssueBoardFeatureEnabled: true, scopedIssueBoardFeatureEnabled: true,
weights: [], weights: [],
}, },
attachTo: document.body,
provide: {
fullPath: '',
},
}); });
}; };
......
...@@ -14,10 +14,11 @@ describe('IssueBoardFilter', () => { ...@@ -14,10 +14,11 @@ describe('IssueBoardFilter', () => {
const createComponent = ({ isSignedIn = false } = {}) => { const createComponent = ({ isSignedIn = false } = {}) => {
wrapper = shallowMount(IssueBoardFilteredSpec, { wrapper = shallowMount(IssueBoardFilteredSpec, {
propsData: { fullPath: 'gitlab-org', boardType: 'group' },
provide: { provide: {
isSignedIn, isSignedIn,
releasesFetchPath: '/releases', releasesFetchPath: '/releases',
fullPath: 'gitlab-org',
boardType: 'group',
}, },
}); });
}; };
......
...@@ -102,6 +102,7 @@ RSpec.describe BoardsHelper do ...@@ -102,6 +102,7 @@ RSpec.describe BoardsHelper do
allow(helper).to receive(:can?).with(user, :create_non_backlog_issues, project_board).and_return(true) allow(helper).to receive(:can?).with(user, :create_non_backlog_issues, project_board).and_return(true)
allow(helper).to receive(:can?).with(user, :admin_issue, project_board).and_return(true) allow(helper).to receive(:can?).with(user, :admin_issue, project_board).and_return(true)
allow(helper).to receive(:can?).with(user, :admin_issue_board_list, project).and_return(false) allow(helper).to receive(:can?).with(user, :admin_issue_board_list, project).and_return(false)
allow(helper).to receive(:can?).with(user, :admin_issue_board, project).and_return(false)
end end
it 'returns a board_lists_path as lists_endpoint' do it 'returns a board_lists_path as lists_endpoint' do
...@@ -129,12 +130,23 @@ RSpec.describe BoardsHelper do ...@@ -129,12 +130,23 @@ RSpec.describe BoardsHelper do
it 'returns can_admin_list as false by default' do it 'returns can_admin_list as false by default' do
expect(helper.board_data[:can_admin_list]).to eq('false') expect(helper.board_data[:can_admin_list]).to eq('false')
end end
it 'returns can_admin_list as true when user can admin the board' do it 'returns can_admin_list as true when user can admin the board lists' do
allow(helper).to receive(:can?).with(user, :admin_issue_board_list, project).and_return(true) allow(helper).to receive(:can?).with(user, :admin_issue_board_list, project).and_return(true)
expect(helper.board_data[:can_admin_list]).to eq('true') expect(helper.board_data[:can_admin_list]).to eq('true')
end end
end end
context 'can_admin_board' do
it 'returns can_admin_board as false by default' do
expect(helper.board_data[:can_admin_board]).to eq('false')
end
it 'returns can_admin_board as true when user can admin the board' do
allow(helper).to receive(:can?).with(user, :admin_issue_board, project).and_return(true)
expect(helper.board_data[:can_admin_board]).to eq('true')
end
end
end end
context 'group board' do context 'group board' do
...@@ -146,6 +158,7 @@ RSpec.describe BoardsHelper do ...@@ -146,6 +158,7 @@ RSpec.describe BoardsHelper do
allow(helper).to receive(:can?).with(user, :create_non_backlog_issues, group_board).and_return(true) allow(helper).to receive(:can?).with(user, :create_non_backlog_issues, group_board).and_return(true)
allow(helper).to receive(:can?).with(user, :admin_issue, group_board).and_return(true) allow(helper).to receive(:can?).with(user, :admin_issue, group_board).and_return(true)
allow(helper).to receive(:can?).with(user, :admin_issue_board_list, base_group).and_return(false) allow(helper).to receive(:can?).with(user, :admin_issue_board_list, base_group).and_return(false)
allow(helper).to receive(:can?).with(user, :admin_issue_board, base_group).and_return(false)
end end
it 'returns correct path for base group' do it 'returns correct path for base group' do
...@@ -165,7 +178,7 @@ RSpec.describe BoardsHelper do ...@@ -165,7 +178,7 @@ RSpec.describe BoardsHelper do
it 'returns can_admin_list as false by default' do it 'returns can_admin_list as false by default' do
expect(helper.board_data[:can_admin_list]).to eq('false') expect(helper.board_data[:can_admin_list]).to eq('false')
end end
it 'returns can_admin_list as true when user can admin the board' do it 'returns can_admin_list as true when user can admin the board lists' do
allow(helper).to receive(:can?).with(user, :admin_issue_board_list, base_group).and_return(true) allow(helper).to receive(:can?).with(user, :admin_issue_board_list, base_group).and_return(true)
expect(helper.board_data[:can_admin_list]).to eq('true') expect(helper.board_data[:can_admin_list]).to eq('true')
......
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