Merge branch 'winh-multiple-issueboards-core' into 'master'
Move multiple issue boards frontend to core (CE-backport) See merge request gitlab-org/gitlab-ce!30503
This commit is contained in:
commit
b3b7d2c166
|
@ -0,0 +1,216 @@
|
|||
<script>
|
||||
import Flash from '~/flash';
|
||||
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import boardsStore from '~/boards/stores/boards_store';
|
||||
|
||||
const boardDefaults = {
|
||||
id: false,
|
||||
name: '',
|
||||
labels: [],
|
||||
milestone_id: undefined,
|
||||
assignee: {},
|
||||
assignee_id: undefined,
|
||||
weight: null,
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BoardScope: () => import('ee_component/boards/components/board_scope.vue'),
|
||||
DeprecatedModal,
|
||||
},
|
||||
props: {
|
||||
canAdminBoard: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
milestonePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelsPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
scopedIssueBoardFeatureEnabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
groupId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
weights: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
enableScopedLabels: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
scopedLabelsDocumentationLink: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '#',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
board: { ...boardDefaults, ...this.currentBoard },
|
||||
currentBoard: boardsStore.state.currentBoard,
|
||||
currentPage: boardsStore.state.currentPage,
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isNewForm() {
|
||||
return this.currentPage === 'new';
|
||||
},
|
||||
isDeleteForm() {
|
||||
return this.currentPage === 'delete';
|
||||
},
|
||||
isEditForm() {
|
||||
return this.currentPage === 'edit';
|
||||
},
|
||||
isVisible() {
|
||||
return this.currentPage !== '';
|
||||
},
|
||||
buttonText() {
|
||||
if (this.isNewForm) {
|
||||
return 'Create board';
|
||||
}
|
||||
if (this.isDeleteForm) {
|
||||
return 'Delete';
|
||||
}
|
||||
return 'Save changes';
|
||||
},
|
||||
buttonKind() {
|
||||
if (this.isNewForm) {
|
||||
return 'success';
|
||||
}
|
||||
if (this.isDeleteForm) {
|
||||
return 'danger';
|
||||
}
|
||||
return 'info';
|
||||
},
|
||||
title() {
|
||||
if (this.isNewForm) {
|
||||
return 'Create new board';
|
||||
}
|
||||
if (this.isDeleteForm) {
|
||||
return 'Delete board';
|
||||
}
|
||||
if (this.readonly) {
|
||||
return 'Board scope';
|
||||
}
|
||||
return 'Edit board';
|
||||
},
|
||||
readonly() {
|
||||
return !this.canAdminBoard;
|
||||
},
|
||||
submitDisabled() {
|
||||
return this.isLoading || this.board.name.length === 0;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.resetFormState();
|
||||
if (this.$refs.name) {
|
||||
this.$refs.name.focus();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
if (this.board.name.length === 0) return;
|
||||
this.isLoading = true;
|
||||
if (this.isDeleteForm) {
|
||||
gl.boardService
|
||||
.deleteBoard(this.currentBoard)
|
||||
.then(() => {
|
||||
visitUrl(boardsStore.rootPath);
|
||||
})
|
||||
.catch(() => {
|
||||
Flash('Failed to delete board. Please try again.');
|
||||
this.isLoading = false;
|
||||
});
|
||||
} else {
|
||||
gl.boardService
|
||||
.createBoard(this.board)
|
||||
.then(resp => resp.data)
|
||||
.then(data => {
|
||||
visitUrl(data.board_path);
|
||||
})
|
||||
.catch(() => {
|
||||
Flash('Unable to save your changes. Please try again.');
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
boardsStore.showPage('');
|
||||
},
|
||||
resetFormState() {
|
||||
if (this.isNewForm) {
|
||||
// Clear the form when we open the "New board" modal
|
||||
this.board = { ...boardDefaults };
|
||||
} else if (this.currentBoard && Object.keys(this.currentBoard).length) {
|
||||
this.board = { ...boardDefaults, ...this.currentBoard };
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<deprecated-modal
|
||||
v-show="isVisible"
|
||||
:hide-footer="readonly"
|
||||
:title="title"
|
||||
:primary-button-label="buttonText"
|
||||
:kind="buttonKind"
|
||||
:submit-disabled="submitDisabled"
|
||||
modal-dialog-class="board-config-modal"
|
||||
@cancel="cancel"
|
||||
@submit="submit"
|
||||
>
|
||||
<template slot="body">
|
||||
<p v-if="isDeleteForm">Are you sure you want to delete this board?</p>
|
||||
<form v-else class="js-board-config-modal" @submit.prevent>
|
||||
<div v-if="!readonly" class="append-bottom-20">
|
||||
<label class="form-section-title label-bold" for="board-new-name"> Board name </label>
|
||||
<input
|
||||
id="board-new-name"
|
||||
ref="name"
|
||||
v-model="board.name"
|
||||
class="form-control"
|
||||
type="text"
|
||||
placeholder="Enter board name"
|
||||
@keyup.enter="submit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<board-scope
|
||||
v-if="scopedIssueBoardFeatureEnabled"
|
||||
:collapse-scope="isNewForm"
|
||||
:board="board"
|
||||
:can-admin-board="canAdminBoard"
|
||||
:milestone-path="milestonePath"
|
||||
:labels-path="labelsPath"
|
||||
:scoped-labels-documentation-link="scopedLabelsDocumentationLink"
|
||||
:enable-scoped-labels="enableScopedLabels"
|
||||
:project-id="projectId"
|
||||
:group-id="groupId"
|
||||
:weights="weights"
|
||||
/>
|
||||
</form>
|
||||
</template>
|
||||
</deprecated-modal>
|
||||
</template>
|
|
@ -0,0 +1,334 @@
|
|||
<script>
|
||||
import { throttle } from 'underscore';
|
||||
import {
|
||||
GlLoadingIcon,
|
||||
GlSearchBoxByType,
|
||||
GlDropdown,
|
||||
GlDropdownDivider,
|
||||
GlDropdownHeader,
|
||||
GlDropdownItem,
|
||||
} from '@gitlab/ui';
|
||||
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import httpStatusCodes from '~/lib/utils/http_status';
|
||||
import boardsStore from '../stores/boards_store';
|
||||
import BoardForm from './board_form.vue';
|
||||
|
||||
const MIN_BOARDS_TO_VIEW_RECENT = 10;
|
||||
|
||||
export default {
|
||||
name: 'BoardsSelector',
|
||||
components: {
|
||||
Icon,
|
||||
BoardForm,
|
||||
GlLoadingIcon,
|
||||
GlSearchBoxByType,
|
||||
GlDropdown,
|
||||
GlDropdownDivider,
|
||||
GlDropdownHeader,
|
||||
GlDropdownItem,
|
||||
},
|
||||
props: {
|
||||
currentBoard: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
milestonePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
throttleDuration: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
},
|
||||
boardBaseUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
hasMissingBoards: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
canAdminBoard: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
multipleIssueBoardsAvailable: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
labelsPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
groupId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
scopedIssueBoardFeatureEnabled: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
weights: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
enabledScopedLabels: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
scopedLabelsDocumentationLink: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '#',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
hasScrollFade: false,
|
||||
scrollFadeInitialized: false,
|
||||
boards: [],
|
||||
recentBoards: [],
|
||||
state: boardsStore.state,
|
||||
throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
|
||||
contentClientHeight: 0,
|
||||
maxPosition: 0,
|
||||
store: boardsStore,
|
||||
filterTerm: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentPage() {
|
||||
return this.state.currentPage;
|
||||
},
|
||||
filteredBoards() {
|
||||
return this.boards.filter(board =>
|
||||
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
|
||||
);
|
||||
},
|
||||
reload: {
|
||||
get() {
|
||||
return this.state.reload;
|
||||
},
|
||||
set(newValue) {
|
||||
this.state.reload = newValue;
|
||||
},
|
||||
},
|
||||
board() {
|
||||
return this.state.currentBoard;
|
||||
},
|
||||
showDelete() {
|
||||
return this.boards.length > 1;
|
||||
},
|
||||
scrollFadeClass() {
|
||||
return {
|
||||
'fade-out': !this.hasScrollFade,
|
||||
};
|
||||
},
|
||||
showRecentSection() {
|
||||
return (
|
||||
this.recentBoards.length &&
|
||||
this.boards.length > MIN_BOARDS_TO_VIEW_RECENT &&
|
||||
!this.filterTerm.length
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
filteredBoards() {
|
||||
this.scrollFadeInitialized = false;
|
||||
this.$nextTick(this.setScrollFade);
|
||||
},
|
||||
reload() {
|
||||
if (this.reload) {
|
||||
this.boards = [];
|
||||
this.recentBoards = [];
|
||||
this.loading = true;
|
||||
this.reload = false;
|
||||
|
||||
this.loadBoards(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
boardsStore.setCurrentBoard(this.currentBoard);
|
||||
},
|
||||
methods: {
|
||||
showPage(page) {
|
||||
boardsStore.showPage(page);
|
||||
},
|
||||
loadBoards(toggleDropdown = true) {
|
||||
if (toggleDropdown && this.boards.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recentBoardsPromise = new Promise((resolve, reject) =>
|
||||
gl.boardService
|
||||
.recentBoards()
|
||||
.then(resolve)
|
||||
.catch(err => {
|
||||
/**
|
||||
* If user is unauthorized we'd still want to resolve the
|
||||
* request to display all boards.
|
||||
*/
|
||||
if (err.response.status === httpStatusCodes.UNAUTHORIZED) {
|
||||
resolve({ data: [] }); // recent boards are empty
|
||||
return;
|
||||
}
|
||||
reject(err);
|
||||
}),
|
||||
);
|
||||
|
||||
Promise.all([gl.boardService.allBoards(), recentBoardsPromise])
|
||||
.then(([allBoards, recentBoards]) => [allBoards.data, recentBoards.data])
|
||||
.then(([allBoardsJson, recentBoardsJson]) => {
|
||||
this.loading = false;
|
||||
this.boards = allBoardsJson;
|
||||
this.recentBoards = recentBoardsJson;
|
||||
})
|
||||
.then(() => this.$nextTick()) // Wait for boards list in DOM
|
||||
.then(() => {
|
||||
this.setScrollFade();
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
isScrolledUp() {
|
||||
const { content } = this.$refs;
|
||||
const currentPosition = this.contentClientHeight + content.scrollTop;
|
||||
|
||||
return content && currentPosition < this.maxPosition;
|
||||
},
|
||||
initScrollFade() {
|
||||
this.scrollFadeInitialized = true;
|
||||
|
||||
const { content } = this.$refs;
|
||||
|
||||
this.contentClientHeight = content.clientHeight;
|
||||
this.maxPosition = content.scrollHeight;
|
||||
},
|
||||
setScrollFade() {
|
||||
if (!this.scrollFadeInitialized) this.initScrollFade();
|
||||
|
||||
this.hasScrollFade = this.isScrolledUp();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="boards-switcher js-boards-selector append-right-10">
|
||||
<span class="boards-selector-wrapper js-boards-selector-wrapper">
|
||||
<gl-dropdown
|
||||
toggle-class="dropdown-menu-toggle js-dropdown-toggle"
|
||||
menu-class="flex-column dropdown-extended-height"
|
||||
:text="board.name"
|
||||
@show="loadBoards"
|
||||
>
|
||||
<div>
|
||||
<div class="dropdown-title mb-0" @mousedown.prevent>
|
||||
{{ s__('IssueBoards|Switch board') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<gl-dropdown-header class="mt-0">
|
||||
<gl-search-box-by-type ref="searchBox" v-model="filterTerm" />
|
||||
</gl-dropdown-header>
|
||||
|
||||
<div
|
||||
v-if="!loading"
|
||||
ref="content"
|
||||
class="dropdown-content flex-fill"
|
||||
@scroll.passive="throttledSetScrollFade"
|
||||
>
|
||||
<gl-dropdown-item
|
||||
v-show="filteredBoards.length === 0"
|
||||
class="no-pointer-events text-secondary"
|
||||
>
|
||||
{{ s__('IssueBoards|No matching boards found') }}
|
||||
</gl-dropdown-item>
|
||||
|
||||
<h6 v-if="showRecentSection" class="dropdown-bold-header my-0">
|
||||
{{ __('Recent') }}
|
||||
</h6>
|
||||
|
||||
<template v-if="showRecentSection">
|
||||
<gl-dropdown-item
|
||||
v-for="recentBoard in recentBoards"
|
||||
:key="`recent-${recentBoard.id}`"
|
||||
class="js-dropdown-item"
|
||||
:href="`${boardBaseUrl}/${recentBoard.id}`"
|
||||
>
|
||||
{{ recentBoard.name }}
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
|
||||
<hr v-if="showRecentSection" class="my-1" />
|
||||
|
||||
<h6 v-if="showRecentSection" class="dropdown-bold-header my-0">
|
||||
{{ __('All') }}
|
||||
</h6>
|
||||
|
||||
<gl-dropdown-item
|
||||
v-for="otherBoard in filteredBoards"
|
||||
:key="otherBoard.id"
|
||||
class="js-dropdown-item"
|
||||
:href="`${boardBaseUrl}/${otherBoard.id}`"
|
||||
>
|
||||
{{ otherBoard.name }}
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-item v-if="hasMissingBoards" class="small unclickable">
|
||||
{{
|
||||
s__(
|
||||
'IssueBoards|Some of your boards are hidden, activate a license to see them again.',
|
||||
)
|
||||
}}
|
||||
</gl-dropdown-item>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="filteredBoards.length > 0"
|
||||
class="dropdown-content-faded-mask"
|
||||
:class="scrollFadeClass"
|
||||
></div>
|
||||
|
||||
<gl-loading-icon v-if="loading" />
|
||||
|
||||
<div v-if="canAdminBoard">
|
||||
<gl-dropdown-divider />
|
||||
|
||||
<gl-dropdown-item v-if="multipleIssueBoardsAvailable" @click.prevent="showPage('new')">
|
||||
{{ s__('IssueBoards|Create new board') }}
|
||||
</gl-dropdown-item>
|
||||
|
||||
<gl-dropdown-item
|
||||
v-if="showDelete"
|
||||
class="text-danger"
|
||||
@click.prevent="showPage('delete')"
|
||||
>
|
||||
{{ s__('IssueBoards|Delete board') }}
|
||||
</gl-dropdown-item>
|
||||
</div>
|
||||
</gl-dropdown>
|
||||
|
||||
<board-form
|
||||
v-if="currentPage"
|
||||
:milestone-path="milestonePath"
|
||||
:labels-path="labelsPath"
|
||||
:project-id="projectId"
|
||||
:group-id="groupId"
|
||||
:can-admin-board="canAdminBoard"
|
||||
:scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
|
||||
:weights="weights"
|
||||
:enable-scoped-labels="enabledScopedLabels"
|
||||
:scoped-labels-documentation-link="scopedLabelsDocumentationLink"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
|
@ -1,7 +1,6 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
|
||||
import mountMultipleBoardsSwitcher from 'ee_else_ce/boards/mount_multiple_boards_switcher';
|
||||
import Flash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import './models/label';
|
||||
|
@ -31,6 +30,7 @@ import {
|
|||
} from '~/lib/utils/common_utils';
|
||||
import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
|
||||
import toggleFocusMode from 'ee_else_ce/boards/toggle_focus';
|
||||
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
|
||||
|
||||
let issueBoardsApp;
|
||||
|
||||
|
|
|
@ -1,2 +1,35 @@
|
|||
// this will be moved from EE to CE as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/53811
|
||||
export default () => {};
|
||||
import Vue from 'vue';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import BoardsSelector from '~/boards/components/boards_selector.vue';
|
||||
|
||||
export default () => {
|
||||
const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher');
|
||||
return new Vue({
|
||||
el: boardsSwitcherElement,
|
||||
components: {
|
||||
BoardsSelector,
|
||||
},
|
||||
data() {
|
||||
const { dataset } = boardsSwitcherElement;
|
||||
|
||||
const boardsSelectorProps = {
|
||||
...dataset,
|
||||
currentBoard: JSON.parse(dataset.currentBoard),
|
||||
hasMissingBoards: parseBoolean(dataset.hasMissingBoards),
|
||||
canAdminBoard: parseBoolean(dataset.canAdminBoard),
|
||||
multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable),
|
||||
projectId: Number(dataset.projectId),
|
||||
groupId: Number(dataset.groupId),
|
||||
scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled),
|
||||
weights: JSON.parse(dataset.weights),
|
||||
};
|
||||
|
||||
return { boardsSelectorProps };
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(BoardsSelector, {
|
||||
props: this.boardsSelectorProps,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
- parent = board.parent
|
||||
- milestone_filter_opts = { format: :json }
|
||||
- milestone_filter_opts = milestone_filter_opts.merge(only_group_milestones: true) if board.group_board?
|
||||
- weights = Gitlab.ee? ? ([Issue::WEIGHT_ANY] + Issue.weight_options) : []
|
||||
|
||||
#js-multiple-boards-switcher.inline.boards-switcher{ data: { current_board: current_board_json.to_json,
|
||||
milestone_path: milestones_filter_path(milestone_filter_opts),
|
||||
board_base_url: board_base_url,
|
||||
has_missing_boards: (!multiple_boards_available? && current_board_parent.boards.size > 1).to_s,
|
||||
can_admin_board: can?(current_user, :admin_board, parent).to_s,
|
||||
multiple_issue_boards_available: parent.multiple_issue_boards_available?.to_s,
|
||||
labels_path: labels_filter_path_with_defaults(only_group_labels: true, include_descendant_groups: true),
|
||||
project_id: @project&.id,
|
||||
group_id: @group&.id,
|
||||
scoped_issue_board_feature_enabled: Gitlab.ee? && parent.feature_available?(:scoped_issue_board) ? 'true' : 'false',
|
||||
weights: weights.to_json } }
|
|
@ -6,7 +6,7 @@
|
|||
.issues-filters{ class: ("w-100" if type == :boards_modal) }
|
||||
.issues-details-filters.filtered-search-block.d-flex.flex-column.flex-md-row{ class: block_css_class, "v-pre" => type == :boards_modal }
|
||||
- if type == :boards
|
||||
= render_if_exists "shared/boards/switcher", board: board
|
||||
= render "shared/boards/switcher", board: board
|
||||
= form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form w-100' do
|
||||
- if params[:search].present?
|
||||
= hidden_field_tag :search, params[:search]
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move multiple issue boards to core
|
||||
merge_request: 30503
|
||||
author:
|
||||
type: changed
|
|
@ -5717,6 +5717,21 @@ msgstr ""
|
|||
msgid "IssueBoards|Boards"
|
||||
msgstr ""
|
||||
|
||||
msgid "IssueBoards|Create new board"
|
||||
msgstr ""
|
||||
|
||||
msgid "IssueBoards|Delete board"
|
||||
msgstr ""
|
||||
|
||||
msgid "IssueBoards|No matching boards found"
|
||||
msgstr ""
|
||||
|
||||
msgid "IssueBoards|Some of your boards are hidden, activate a license to see them again."
|
||||
msgstr ""
|
||||
|
||||
msgid "IssueBoards|Switch board"
|
||||
msgstr ""
|
||||
|
||||
msgid "IssueTracker|Bugzilla issue tracker"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8698,6 +8713,9 @@ msgstr ""
|
|||
msgid "Receive notifications about your own activity"
|
||||
msgstr ""
|
||||
|
||||
msgid "Recent"
|
||||
msgstr ""
|
||||
|
||||
msgid "Recent Project Activity"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import boardsStore from '~/boards/stores/boards_store';
|
||||
import boardForm from '~/boards/components/board_form.vue';
|
||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||
|
||||
describe('board_form.vue', () => {
|
||||
const props = {
|
||||
canAdminBoard: false,
|
||||
labelsPath: `${gl.TEST_HOST}/labels/path`,
|
||||
milestonePath: `${gl.TEST_HOST}/milestone/path`,
|
||||
};
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn($, 'ajax');
|
||||
boardsStore.state.currentPage = 'edit';
|
||||
const Component = Vue.extend(boardForm);
|
||||
vm = mountComponent(Component, props);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('cancel', () => {
|
||||
it('resets currentPage', done => {
|
||||
vm.cancel();
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
expect(boardsStore.state.currentPage).toBe('');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('buttons', () => {
|
||||
it('cancel button triggers cancel()', done => {
|
||||
spyOn(vm, 'cancel');
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
const cancelButton = vm.$el.querySelector('button[data-dismiss="modal"]');
|
||||
cancelButton.click();
|
||||
|
||||
expect(vm.cancel).toHaveBeenCalled();
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,205 @@
|
|||
import Vue from 'vue';
|
||||
import BoardService from '~/boards/services/board_service';
|
||||
import BoardsSelector from '~/boards/components/boards_selector.vue';
|
||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import boardsStore from '~/boards/stores/boards_store';
|
||||
|
||||
const throttleDuration = 1;
|
||||
|
||||
function boardGenerator(n) {
|
||||
return new Array(n).fill().map((board, id) => {
|
||||
const name = `board${id}`;
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
describe('BoardsSelector', () => {
|
||||
let vm;
|
||||
let allBoardsResponse;
|
||||
let recentBoardsResponse;
|
||||
let fillSearchBox;
|
||||
const boards = boardGenerator(20);
|
||||
const recentBoards = boardGenerator(5);
|
||||
|
||||
beforeEach(done => {
|
||||
setFixtures('<div class="js-boards-selector"></div>');
|
||||
window.gl = window.gl || {};
|
||||
|
||||
boardsStore.setEndpoints({
|
||||
boardsEndpoint: '',
|
||||
recentBoardsEndpoint: '',
|
||||
listsEndpoint: '',
|
||||
bulkUpdatePath: '',
|
||||
boardId: '',
|
||||
});
|
||||
window.gl.boardService = new BoardService();
|
||||
|
||||
allBoardsResponse = Promise.resolve({
|
||||
data: boards,
|
||||
});
|
||||
recentBoardsResponse = Promise.resolve({
|
||||
data: recentBoards,
|
||||
});
|
||||
|
||||
spyOn(BoardService.prototype, 'allBoards').and.returnValue(allBoardsResponse);
|
||||
spyOn(BoardService.prototype, 'recentBoards').and.returnValue(recentBoardsResponse);
|
||||
|
||||
const Component = Vue.extend(BoardsSelector);
|
||||
vm = mountComponent(
|
||||
Component,
|
||||
{
|
||||
throttleDuration,
|
||||
currentBoard: {
|
||||
id: 1,
|
||||
name: 'Development',
|
||||
milestone_id: null,
|
||||
weight: null,
|
||||
assignee_id: null,
|
||||
labels: [],
|
||||
},
|
||||
milestonePath: `${TEST_HOST}/milestone/path`,
|
||||
boardBaseUrl: `${TEST_HOST}/board/base/url`,
|
||||
hasMissingBoards: false,
|
||||
canAdminBoard: true,
|
||||
multipleIssueBoardsAvailable: true,
|
||||
labelsPath: `${TEST_HOST}/labels/path`,
|
||||
projectId: 42,
|
||||
groupId: 19,
|
||||
scopedIssueBoardFeatureEnabled: true,
|
||||
weights: [],
|
||||
},
|
||||
document.querySelector('.js-boards-selector'),
|
||||
);
|
||||
|
||||
vm.$el.querySelector('.js-dropdown-toggle').click();
|
||||
|
||||
Promise.all([allBoardsResponse, recentBoardsResponse])
|
||||
.then(() => vm.$nextTick())
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
|
||||
fillSearchBox = filterTerm => {
|
||||
const { searchBox } = vm.$refs;
|
||||
const searchBoxInput = searchBox.$el.querySelector('input');
|
||||
searchBoxInput.value = filterTerm;
|
||||
searchBoxInput.dispatchEvent(new Event('input'));
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
window.gl.boardService = undefined;
|
||||
});
|
||||
|
||||
describe('filtering', () => {
|
||||
it('shows all boards without filtering', done => {
|
||||
vm.$nextTick()
|
||||
.then(() => {
|
||||
const dropdownItem = vm.$el.querySelectorAll('.js-dropdown-item');
|
||||
|
||||
expect(dropdownItem.length).toBe(boards.length + recentBoards.length);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('shows only matching boards when filtering', done => {
|
||||
const filterTerm = 'board1';
|
||||
const expectedCount = boards.filter(board => board.name.includes(filterTerm)).length;
|
||||
|
||||
fillSearchBox(filterTerm);
|
||||
|
||||
vm.$nextTick()
|
||||
.then(() => {
|
||||
const dropdownItems = vm.$el.querySelectorAll('.js-dropdown-item');
|
||||
|
||||
expect(dropdownItems.length).toBe(expectedCount);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('shows message if there are no matching boards', done => {
|
||||
fillSearchBox('does not exist');
|
||||
|
||||
vm.$nextTick()
|
||||
.then(() => {
|
||||
const dropdownItems = vm.$el.querySelectorAll('.js-dropdown-item');
|
||||
|
||||
expect(dropdownItems.length).toBe(0);
|
||||
expect(vm.$el).toContainText('No matching boards found');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recent boards section', () => {
|
||||
it('shows only when boards are greater than 10', done => {
|
||||
vm.$nextTick()
|
||||
.then(() => {
|
||||
const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header');
|
||||
|
||||
const expectedCount = 2; // Recent + All
|
||||
|
||||
expect(expectedCount).toBe(headerEls.length);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('does not show when boards are less than 10', done => {
|
||||
spyOn(vm, 'initScrollFade');
|
||||
spyOn(vm, 'setScrollFade');
|
||||
|
||||
vm.$nextTick()
|
||||
.then(() => {
|
||||
vm.boards = vm.boards.slice(0, 5);
|
||||
})
|
||||
.then(vm.$nextTick)
|
||||
.then(() => {
|
||||
const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header');
|
||||
const expectedCount = 0;
|
||||
|
||||
expect(expectedCount).toBe(headerEls.length);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('does not show when recentBoards api returns empty array', done => {
|
||||
vm.$nextTick()
|
||||
.then(() => {
|
||||
vm.recentBoards = [];
|
||||
})
|
||||
.then(vm.$nextTick)
|
||||
.then(() => {
|
||||
const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header');
|
||||
const expectedCount = 0;
|
||||
|
||||
expect(expectedCount).toBe(headerEls.length);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('does not show when search is active', done => {
|
||||
fillSearchBox('Random string');
|
||||
|
||||
vm.$nextTick()
|
||||
.then(() => {
|
||||
const headerEls = vm.$el.querySelectorAll('.dropdown-bold-header');
|
||||
const expectedCount = 0;
|
||||
|
||||
expect(expectedCount).toBe(headerEls.length);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue