summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/boards
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/boards')
-rw-r--r--app/assets/javascripts/boards/components/board.js40
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.vue33
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue219
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue13
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js2
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue335
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue25
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.vue30
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.vue16
-rw-r--r--app/assets/javascripts/boards/components/modal/header.vue8
-rw-r--r--app/assets/javascripts/boards/components/modal/list.vue4
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.vue1
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue17
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.vue2
-rw-r--r--app/assets/javascripts/boards/config_toggle.js1
-rw-r--r--app/assets/javascripts/boards/ee_functions.js7
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js3
-rw-r--r--app/assets/javascripts/boards/index.js56
-rw-r--r--app/assets/javascripts/boards/mixins/modal_footer.js1
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js2
-rw-r--r--app/assets/javascripts/boards/models/issue.js6
-rw-r--r--app/assets/javascripts/boards/models/list.js16
-rw-r--r--app/assets/javascripts/boards/models/milestone.js4
-rw-r--r--app/assets/javascripts/boards/mount_multiple_boards_switcher.js35
-rw-r--r--app/assets/javascripts/boards/services/board_service.js97
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js149
-rw-r--r--app/assets/javascripts/boards/toggle_focus.js1
29 files changed, 962 insertions, 168 deletions
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 45b9e57f9ab..c6122fbc686 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -1,6 +1,7 @@
+import $ from 'jquery';
import Sortable from 'sortablejs';
import Vue from 'vue';
-import { n__ } from '~/locale';
+import { n__, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
import AccessorUtilities from '../../lib/utils/accessor';
@@ -53,12 +54,19 @@ export default Vue.extend({
const { issuesSize } = this.list;
return `${n__('%d issue', '%d issues', issuesSize)}`;
},
+ caretTooltip() {
+ return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand');
+ },
isNewIssueShown() {
return (
this.list.type === 'backlog' ||
(!this.disabled && this.list.type !== 'closed' && this.list.type !== 'blank')
);
},
+ uniqueKey() {
+ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+ return `boards.${this.boardId}.${this.list.type}.${this.list.id}`;
+ },
},
watch: {
filter: {
@@ -72,31 +80,34 @@ export default Vue.extend({
},
},
mounted() {
- this.sortableOptions = getBoardSortableDefaultOptions({
+ const instance = this;
+
+ const sortableOptions = getBoardSortableDefaultOptions({
disabled: this.disabled,
group: 'boards',
draggable: '.is-draggable',
handle: '.js-board-handle',
- onEnd: e => {
+ onEnd(e) {
sortableEnd();
+ const sortable = this;
+
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
- const order = this.sortable.toArray();
+ const order = sortable.toArray();
const list = boardsStore.findList('id', parseInt(e.item.dataset.id, 10));
- this.$nextTick(() => {
+ instance.$nextTick(() => {
boardsStore.moveList(list, order);
});
}
},
});
- this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
+ Sortable.create(this.$el.parentNode, sortableOptions);
},
created() {
if (this.list.isExpandable && AccessorUtilities.isLocalStorageAccessSafe()) {
- const isCollapsed =
- localStorage.getItem(`boards.${this.boardId}.${this.list.type}.expanded`) === 'false';
+ const isCollapsed = localStorage.getItem(`${this.uniqueKey}.expanded`) === 'false';
this.list.isExpanded = !isCollapsed;
}
@@ -105,16 +116,17 @@ export default Vue.extend({
showNewIssueForm() {
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
},
- toggleExpanded(e) {
- if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) {
+ toggleExpanded() {
+ if (this.list.isExpandable) {
this.list.isExpanded = !this.list.isExpanded;
if (AccessorUtilities.isLocalStorageAccessSafe()) {
- localStorage.setItem(
- `boards.${this.boardId}.${this.list.type}.expanded`,
- this.list.isExpanded,
- );
+ localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded);
}
+
+ // When expanding/collapsing, the tooltip on the caret button sometimes stays open.
+ // Close all tooltips manually to prevent dangling tooltips.
+ $('.tooltip').tooltip('hide');
}
},
},
diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue
index 1cbd31729cd..9f26337d153 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.vue
+++ b/app/assets/javascripts/boards/components/board_blank_state.vue
@@ -1,5 +1,6 @@
<script>
-/* global ListLabel */
+import { __ } from '~/locale';
+import ListLabel from '~/boards/models/label';
import Cookies from 'js-cookie';
import boardsStore from '../stores/boards_store';
@@ -7,8 +8,8 @@ export default {
data() {
return {
predefinedLabels: [
- new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
- new ListLabel({ title: 'Doing', color: '#5CB85C' }),
+ new ListLabel({ title: __('To Do'), color: '#F0AD4E' }),
+ new ListLabel({ title: __('Doing'), color: '#5CB85C' }),
],
};
},
@@ -29,13 +30,17 @@ export default {
});
// Save the labels
- gl.boardService
+ boardsStore
.generateDefaultLists()
.then(res => res.data)
.then(data => {
data.forEach(listObj => {
const list = boardsStore.findList('title', listObj.title);
+ if (!list) {
+ return;
+ }
+
list.id = listObj.id;
list.label.id = listObj.label.id;
list.getIssues().catch(() => {
@@ -58,30 +63,36 @@ export default {
<template>
<div class="board-blank-state p-3">
- <p>Add the following default lists to your Issue Board with one click:</p>
+ <p>
+ {{
+ s__('BoardBlankState|Add the following default lists to your Issue Board with one click:')
+ }}
+ </p>
<ul class="list-unstyled board-blank-state-list">
<li v-for="(label, index) in predefinedLabels" :key="index">
<span
:style="{ backgroundColor: label.color }"
class="label-color position-relative d-inline-block rounded"
- >
- </span>
+ ></span>
{{ label.title }}
</li>
</ul>
<p>
- Starting out with the default set of lists will get you right on the way to making the most of
- your board.
+ {{
+ s__(
+ 'BoardBlankState|Starting out with the default set of lists will get you right on the way to making the most of your board.',
+ )
+ }}
</p>
<button
class="btn btn-success btn-inverted btn-block"
type="button"
@click.stop="addDefaultLists"
>
- Add default lists
+ {{ s__('BoardBlankState|Add default lists') }}
</button>
<button class="btn btn-default btn-block" type="button" @click.stop="clearBlankState">
- Nevermind, I'll use my own
+ {{ s__("BoardBlankState|Nevermind, I'll use my own") }}
</button>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 179148b6887..faf722f61af 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -83,6 +83,7 @@ export default {
}"
:index="index"
:data-issue-id="issue.id"
+ data-qa-selector="board_card"
class="board-card p-3 rounded"
@mousedown="mouseDown"
@mousemove="mouseMove"
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
new file mode 100644
index 00000000000..ebf48cee2ae
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -0,0 +1,219 @@
+<script>
+import { __ } from '~/locale';
+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>
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index b1a8b13f3ac..de41698ca04 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
import Sortable from 'sortablejs';
import { GlLoadingIcon } from '@gitlab/ui';
import boardNewIssue from './board_new_issue.vue';
@@ -226,8 +227,9 @@ export default {
<div
:class="{ 'd-none': !list.isExpanded, 'd-flex flex-column': list.isExpanded }"
class="board-list-component position-relative h-100"
+ data-qa-selector="board_list_cards_area"
>
- <div v-if="loading" class="board-list-loading text-center" aria-label="Loading issues">
+ <div v-if="loading" class="board-list-loading text-center" :aria-label="__('Loading issues')">
<gl-loading-icon />
</div>
<board-new-issue
@@ -257,7 +259,7 @@ export default {
/>
<li v-if="showCount" class="board-list-count text-center" data-issue-id="-1">
<gl-loading-icon v-show="list.loadingMore" label="Loading more issues" />
- <span v-if="list.issues.length === list.issuesSize"> Showing all issues </span>
+ <span v-if="list.issues.length === list.issuesSize">{{ __('Showing all issues') }}</span>
<span v-else> Showing {{ list.issues.length }} of {{ list.issuesSize }} issues </span>
</li>
</ul>
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index cc6af8e88cd..4180023b7db 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -102,9 +102,9 @@ export default {
<div class="board-card position-relative p-3 rounded">
<form @submit="submit($event)">
<div v-if="error" class="flash-container">
- <div class="flash-alert">An error occurred. Please try again.</div>
+ <div class="flash-alert">{{ __('An error occurred. Please try again.') }}</div>
</div>
- <label :for="list.id + '-title'" class="label-bold"> Title </label>
+ <label :for="list.id + '-title'" class="label-bold">{{ __('Title') }}</label>
<input
:id="list.id + '-title'"
ref="input"
@@ -122,12 +122,11 @@ export default {
class="float-left"
variant="success"
type="submit"
+ >{{ __('Submit issue') }}</gl-button
>
- Submit issue
- </gl-button>
- <gl-button class="float-right" type="button" variant="default" @click="cancel">
- Cancel
- </gl-button>
+ <gl-button class="float-right" type="button" variant="default" @click="cancel">{{
+ __('Cancel')
+ }}</gl-button>
</div>
</form>
</div>
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 2ace0060c42..ba1fe9202fc 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -22,6 +22,8 @@ export default Vue.extend({
components: {
AssigneeTitle,
Assignees,
+ SidebarEpicsSelect: () =>
+ import('ee_component/sidebar/components/sidebar_item_epics_select.vue'),
RemoveBtn,
Subscriptions,
TimeTracker,
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
new file mode 100644
index 00000000000..7296426549a
--- /dev/null
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -0,0 +1,335 @@
+<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
+ data-qa-selector="boards_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>
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index a8516f178fc..7f554c99669 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -124,7 +124,7 @@ export default {
return `${this.rootPath}${assignee.username}`;
},
avatarUrlTitle(assignee) {
- return `Avatar for ${assignee.name}`;
+ return sprintf(__(`Avatar for %{assigneeName}`), { assigneeName: assignee.name });
},
showLabel(label) {
if (!label.id) return false;
@@ -160,9 +160,10 @@ export default {
:title="__('Confidential')"
class="confidential-icon append-right-4"
:aria-label="__('Confidential')"
- /><a :href="issue.path" :title="issue.title" class="js-no-trigger" @mousemove.stop>{{
- issue.title
- }}</a>
+ />
+ <a :href="issue.path" :title="issue.title" class="js-no-trigger" @mousemove.stop>
+ {{ issue.title }}
+ </a>
</h4>
</div>
<div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap">
@@ -204,13 +205,13 @@ export default {
placement="bottom"
class="board-issue-path block-truncated bold"
>{{ issueReferencePath }}</tooltip-on-truncate
- >#{{ issue.iid }}
+ >
+ #{{ issue.iid }}
</span>
<span class="board-info-items prepend-top-8 d-inline-block">
- <issue-due-date v-if="issue.dueDate" :date="issue.dueDate" /><issue-time-estimate
- v-if="issue.timeEstimate"
- :estimate="issue.timeEstimate"
- /><issue-card-weight
+ <issue-due-date v-if="issue.dueDate" :date="issue.dueDate" />
+ <issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" />
+ <issue-card-weight
v-if="issue.weight"
:weight="issue.weight"
@click="filterByWeight(issue.weight)"
@@ -230,7 +231,8 @@ export default {
tooltip-placement="bottom"
>
<span class="js-assignee-tooltip">
- <span class="bold d-block">Assignee</span> {{ assignee.name }}
+ <span class="bold d-block">{{ __('Assignee') }}</span>
+ {{ assignee.name }}
<span class="text-white-50">@{{ assignee.username }}</span>
</span>
</user-avatar-link>
@@ -240,9 +242,8 @@ export default {
:title="assigneeCounterTooltip"
class="avatar-counter"
data-placement="bottom"
+ >{{ assigneeCounterLabel }}</span
>
- {{ assigneeCounterLabel }}
- </span>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue
index 091700de93f..66f59009714 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.vue
+++ b/app/assets/javascripts/boards/components/modal/empty_state.vue
@@ -1,4 +1,5 @@
<script>
+import { __, sprintf } from '~/locale';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
@@ -20,19 +21,20 @@ export default {
computed: {
contents() {
const obj = {
- title: "You haven't added any issues to your project yet",
- content: `
- An issue can be a bug, a todo or a feature request that needs to be
- discussed in a project. Besides, issues are searchable and filterable.
- `,
+ title: __("You haven't added any issues to your project yet"),
+ content: __(
+ 'An issue can be a bug, a todo or a feature request that needs to be discussed in a project. Besides, issues are searchable and filterable.',
+ ),
};
if (this.activeTab === 'selected') {
- obj.title = "You haven't selected any issues yet";
- obj.content = `
- Go back to <strong>Open issues</strong> and select some issues
- to add to your board.
- `;
+ obj.title = __("You haven't selected any issues yet");
+ obj.content = sprintf(
+ __(
+ 'Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board.',
+ ),
+ { startTag: '<strong>', endTag: '</strong>' },
+ );
}
return obj;
@@ -51,16 +53,16 @@ export default {
<div class="text-content">
<h4>{{ contents.title }}</h4>
<p v-html="contents.content"></p>
- <a v-if="activeTab === 'all'" :href="newIssuePath" class="btn btn-success btn-inverted">
- New issue
- </a>
+ <a v-if="activeTab === 'all'" :href="newIssuePath" class="btn btn-success btn-inverted">{{
+ __('New issue')
+ }}</a>
<button
v-if="activeTab === 'selected'"
class="btn btn-default"
type="button"
@click="changeTab('all')"
>
- Open issues
+ {{ __('Open issues') }}
</button>
</div>
</div>
diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue
index d4afd9d59da..5f100c617a0 100644
--- a/app/assets/javascripts/boards/components/modal/footer.vue
+++ b/app/assets/javascripts/boards/components/modal/footer.vue
@@ -1,8 +1,8 @@
<script>
+import footerEEMixin from 'ee_else_ce/boards/mixins/modal_footer';
import Flash from '../../../flash';
-import { __ } from '../../../locale';
+import { __, n__ } from '../../../locale';
import ListsDropdown from './lists_dropdown.vue';
-import { pluralize } from '../../../lib/utils/text_utility';
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
import boardsStore from '../../stores/boards_store';
@@ -11,7 +11,7 @@ export default {
components: {
ListsDropdown,
},
- mixins: [modalMixin],
+ mixins: [modalMixin, footerEEMixin],
data() {
return {
modal: ModalStore.store,
@@ -24,8 +24,8 @@ export default {
},
submitText() {
const count = ModalStore.selectedCount();
-
- return `Add ${count > 0 ? count : ''} ${pluralize('issue', count)}`;
+ if (!count) return __('Add issues');
+ return n__(`Add %d issue`, `Add %d issues`, count);
},
},
methods: {
@@ -42,7 +42,7 @@ export default {
const req = this.buildUpdateRequest(list);
// Post the data to the backend
- gl.boardService.bulkUpdate(issueIds, req).catch(() => {
+ boardsStore.bulkUpdate(issueIds, req).catch(() => {
Flash(__('Failed to update issues, please try again.'));
selectedIssues.forEach(issue => {
@@ -68,11 +68,11 @@ export default {
<button :disabled="submitDisabled" class="btn btn-success" type="button" @click="addIssues">
{{ submitText }}
</button>
- <span class="inline add-issues-footer-to-list"> to list </span>
+ <span class="inline add-issues-footer-to-list">{{ __('to list') }}</span>
<lists-dropdown />
</div>
<button class="btn btn-default float-right" type="button" @click="toggleModal(false)">
- Cancel
+ {{ __('Cancel') }}
</button>
</footer>
</template>
diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue
index 1cfa6d39362..8cd4840d3d6 100644
--- a/app/assets/javascripts/boards/components/modal/header.vue
+++ b/app/assets/javascripts/boards/components/modal/header.vue
@@ -1,4 +1,6 @@
<script>
+/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
+import { __ } from '~/locale';
import ModalFilters from './filters';
import ModalTabs from './tabs.vue';
import ModalStore from '../../stores/modal_store';
@@ -30,10 +32,10 @@ export default {
computed: {
selectAllText() {
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
- return 'Select all';
+ return __('Select all');
}
- return 'Deselect all';
+ return __('Deselect all');
},
showSearch() {
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
@@ -57,7 +59,7 @@ export default {
type="button"
class="close"
data-dismiss="modal"
- aria-label="Close"
+ :aria-label="__('Close')"
@click="toggleModal(false)"
>
<span aria-hidden="true">×</span>
diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue
index 28d2019af2f..1802b543687 100644
--- a/app/assets/javascripts/boards/components/modal/list.vue
+++ b/app/assets/javascripts/boards/components/modal/list.vue
@@ -123,7 +123,9 @@ export default {
class="empty-state add-issues-empty-state-filter text-center"
>
<div class="svg-content"><img :src="emptyStateSvg" /></div>
- <div class="text-content"><h4>There are no issues to show.</h4></div>
+ <div class="text-content">
+ <h4>{{ __('There are no issues to show.') }}</h4>
+ </div>
</div>
<div v-for="(group, index) in groupedIssues" :key="index" class="add-issues-list-column">
<div v-for="issue in group" v-if="showIssue(issue)" :key="issue.id" class="board-card-parent">
diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue
index 2d2920e312e..7430fc96654 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.vue
+++ b/app/assets/javascripts/boards/components/modal/tabs.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 8274647744f..e8d25e84be1 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -1,4 +1,5 @@
<script>
+import { __ } from '~/locale';
import $ from 'jquery';
import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue';
@@ -27,7 +28,7 @@ export default {
},
computed: {
selectedProjectName() {
- return this.selectedProject.name || 'Select a project';
+ return this.selectedProject.name || __('Select a project');
},
},
mounted() {
@@ -67,13 +68,15 @@ export default {
<li>
<a href='#' class='dropdown-menu-link' data-project-id="${
project.id
- }" data-project-name="${project.name}">
- ${_.escape(project.name)}
+ }" data-project-name="${project.name}" data-project-name-with-namespace="${
+ project.name_with_namespace
+ }">
+ ${_.escape(project.name_with_namespace)}
</a>
</li>
`;
},
- text: project => project.name,
+ text: project => project.name_with_namespace,
});
},
};
@@ -81,7 +84,7 @@ export default {
<template>
<div>
- <label class="label-bold prepend-top-10"> Project </label>
+ <label class="label-bold prepend-top-10">{{ __('Project') }}</label>
<div ref="projectsDropdown" class="dropdown dropdown-projects">
<button
class="dropdown-menu-toggle wide"
@@ -92,9 +95,9 @@ export default {
{{ selectedProjectName }} <icon name="chevron-down" />
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
- <div class="dropdown-title">Projects</div>
+ <div class="dropdown-title">{{ __('Projects') }}</div>
<div class="dropdown-input">
- <input class="dropdown-input-field" type="search" placeholder="Search projects" />
+ <input class="dropdown-input-field" type="search" :placeholder="__('Search projects')" />
<icon name="search" class="dropdown-input-search" data-hidden="true" />
</div>
<div class="dropdown-content"></div>
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
index 4ab2b17301f..b84722244d1 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
@@ -76,7 +76,7 @@ export default Vue.extend({
<template>
<div class="block list">
<button class="btn btn-default btn-block" type="button" @click="removeIssue">
- Remove from board
+ {{ __('Remove from board') }}
</button>
</div>
</template>
diff --git a/app/assets/javascripts/boards/config_toggle.js b/app/assets/javascripts/boards/config_toggle.js
new file mode 100644
index 00000000000..2d1ec238274
--- /dev/null
+++ b/app/assets/javascripts/boards/config_toggle.js
@@ -0,0 +1 @@
+export default () => {};
diff --git a/app/assets/javascripts/boards/ee_functions.js b/app/assets/javascripts/boards/ee_functions.js
new file mode 100644
index 00000000000..583270fcae5
--- /dev/null
+++ b/app/assets/javascripts/boards/ee_functions.js
@@ -0,0 +1,7 @@
+export const setPromotionState = () => {};
+
+export const setWeigthFetchingState = () => {};
+export const setEpicFetchingState = () => {};
+
+export const getMilestoneTitle = () => ({});
+export const getBoardsModalData = () => ({});
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index 6b54e8baefb..b1b4b1c5508 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -2,7 +2,6 @@ import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable
import FilteredSearchContainer from '../filtered_search/container';
import FilteredSearchManager from '../filtered_search/filtered_search_manager';
import boardsStore from './stores/boards_store';
-import { isEE } from '~/lib/utils/common_utils';
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
@@ -10,7 +9,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
page: 'boards',
isGroupDecendent: true,
stateFiltersSelector: '.issues-state-filters',
- isGroup: isEE(),
+ isGroup: IS_EE,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index a020765f335..3bded4a3258 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -6,28 +6,38 @@ import { __ } from '~/locale';
import './models/label';
import './models/assignee';
-import FilteredSearchBoards from './filtered_search_boards';
-import eventHub from './eventhub';
+import FilteredSearchBoards from '~/boards/filtered_search_boards';
+import eventHub from '~/boards/eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
-import './models/issue';
-import './models/list';
-import './models/milestone';
-import './models/project';
-import boardsStore from './stores/boards_store';
-import ModalStore from './stores/modal_store';
-import BoardService from './services/board_service';
-import modalMixin from './mixins/modal_mixins';
-import './filters/due_date_filters';
-import Board from './components/board';
-import BoardSidebar from './components/board_sidebar';
-import initNewListDropdown from './components/new_list_dropdown';
-import BoardAddIssuesModal from './components/modal/index.vue';
+import 'ee_else_ce/boards/models/issue';
+import 'ee_else_ce/boards/models/list';
+import '~/boards/models/milestone';
+import '~/boards/models/project';
+import boardsStore from '~/boards/stores/boards_store';
+import ModalStore from '~/boards/stores/modal_store';
+import BoardService from 'ee_else_ce/boards/services/board_service';
+import modalMixin from '~/boards/mixins/modal_mixins';
+import '~/boards/filters/due_date_filters';
+import Board from 'ee_else_ce/boards/components/board';
+import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
+import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
+import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
import '~/vue_shared/vue_resource_interceptor';
import {
NavigationType,
convertObjectPropsToCamelCase,
parseBoolean,
} from '~/lib/utils/common_utils';
+import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
+import toggleFocusMode from 'ee_else_ce/boards/toggle_focus';
+import {
+ setPromotionState,
+ setWeigthFetchingState,
+ setEpicFetchingState,
+ getMilestoneTitle,
+ getBoardsModalData,
+} from 'ee_else_ce/boards/ee_functions';
+import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
let issueBoardsApp;
@@ -78,13 +88,14 @@ export default () => {
},
},
created() {
- gl.boardService = new BoardService({
+ boardsStore.setEndpoints({
boardsEndpoint: this.boardsEndpoint,
recentBoardsEndpoint: this.recentBoardsEndpoint,
listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath,
boardId: this.boardId,
});
+ gl.boardService = new BoardService();
boardsStore.rootPath = this.boardsEndpoint;
eventHub.$on('updateTokens', this.updateTokens);
@@ -125,6 +136,7 @@ export default () => {
});
boardsStore.addBlankState();
+ setPromotionState(boardsStore);
this.loading = false;
})
.catch(() => {
@@ -139,6 +151,8 @@ export default () => {
const { sidebarInfoEndpoint } = newIssue;
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
newIssue.setFetchingState('subscriptions', true);
+ setWeigthFetchingState(newIssue, true);
+ setEpicFetchingState(newIssue, true);
BoardService.getIssueInfo(sidebarInfoEndpoint)
.then(res => res.data)
.then(data => {
@@ -153,6 +167,8 @@ export default () => {
} = convertObjectPropsToCamelCase(data);
newIssue.setFetchingState('subscriptions', false);
+ setWeigthFetchingState(newIssue, false);
+ setEpicFetchingState(newIssue, false);
newIssue.updateData({
humanTimeSpent: humanTotalTimeSpent,
timeSpent: totalTimeSpent,
@@ -165,6 +181,7 @@ export default () => {
})
.catch(() => {
newIssue.setFetchingState('subscriptions', false);
+ setWeigthFetchingState(newIssue, false);
Flash(__('An error occurred while fetching sidebar data'));
});
}
@@ -199,12 +216,15 @@ export default () => {
el: document.getElementById('js-add-list'),
data: {
filters: boardsStore.state.filters,
+ ...getMilestoneTitle($boardApp),
},
mounted() {
initNewListDropdown();
},
});
+ boardConfigToggle(boardsStore);
+
const issueBoardsModal = document.getElementById('js-add-issues-btn');
if (issueBoardsModal) {
@@ -216,6 +236,7 @@ export default () => {
return {
modal: ModalStore.store,
store: boardsStore.state,
+ ...getBoardsModalData($boardApp),
canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
};
},
@@ -278,4 +299,7 @@ export default () => {
`,
});
}
+
+ toggleFocusMode(ModalStore, boardsStore, $boardApp);
+ mountMultipleBoardsSwitcher();
};
diff --git a/app/assets/javascripts/boards/mixins/modal_footer.js b/app/assets/javascripts/boards/mixins/modal_footer.js
new file mode 100644
index 00000000000..ff8b4c56321
--- /dev/null
+++ b/app/assets/javascripts/boards/mixins/modal_footer.js
@@ -0,0 +1 @@
+export default {};
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index 636ca99952c..68ea28e68d9 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
@@ -20,7 +20,7 @@ export function getBoardSortableDefaultOptions(obj) {
'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch);
const defaultSortOptions = Object.assign({}, sortableConfig, {
- filter: '.board-delete, .btn',
+ filter: '.no-drag',
delay: touchEnabled ? 100 : 0,
scrollSensitivity: touchEnabled ? 60 : 100,
scrollSpeed: 20,
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index f858b162c6b..9069b35db9a 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -5,7 +5,7 @@
import Vue from 'vue';
import './label';
-import { isEE, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import IssueProject from './project';
import boardsStore from '../stores/boards_store';
@@ -91,13 +91,13 @@ class ListIssue {
addMilestone(milestone) {
const miletoneId = this.milestone ? this.milestone.id : null;
- if (isEE && milestone.id !== miletoneId) {
+ if (IS_EE && milestone.id !== miletoneId) {
this.milestone = new ListMilestone(milestone);
}
}
removeMilestone(removeMilestone) {
- if (isEE && removeMilestone && removeMilestone.id === this.milestone.id) {
+ if (IS_EE && removeMilestone && removeMilestone.id === this.milestone.id) {
this.milestone = {};
}
}
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index a9d88f19146..7e0ccb9bd2a 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -4,7 +4,7 @@
import { __ } from '~/locale';
import ListLabel from './label';
import ListAssignee from './assignee';
-import { isEE, urlParamsToObject } from '~/lib/utils/common_utils';
+import { urlParamsToObject } from '~/lib/utils/common_utils';
import boardsStore from '../stores/boards_store';
import ListMilestone from './milestone';
@@ -26,6 +26,12 @@ const TYPES = {
isExpandable: false,
isBlank: true,
},
+ default: {
+ // includes label, assignee, and milestone lists
+ isPreset: false,
+ isExpandable: true,
+ isBlank: false,
+ },
};
class List {
@@ -52,7 +58,7 @@ class List {
} else if (obj.user) {
this.assignee = new ListAssignee(obj.user);
this.title = this.assignee.name;
- } else if (isEE && obj.milestone) {
+ } else if (IS_EE && obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
this.title = this.milestone.title;
}
@@ -79,7 +85,7 @@ class List {
entityType = 'label_id';
} else if (this.assignee) {
entityType = 'assignee_id';
- } else if (isEE && this.milestone) {
+ } else if (IS_EE && this.milestone) {
entityType = 'milestone_id';
}
@@ -199,7 +205,7 @@ class List {
issue.addAssignee(this.assignee);
}
- if (isEE && this.milestone) {
+ if (IS_EE && this.milestone) {
if (listFrom && listFrom.type === 'milestone') {
issue.removeMilestone(listFrom.milestone);
}
@@ -249,7 +255,7 @@ class List {
}
getTypeInfo(type) {
- return TYPES[type] || {};
+ return TYPES[type] || TYPES.default;
}
onNewIssueResponse(issue, data) {
diff --git a/app/assets/javascripts/boards/models/milestone.js b/app/assets/javascripts/boards/models/milestone.js
index 6f81d6bc6f8..7201b6e91f5 100644
--- a/app/assets/javascripts/boards/models/milestone.js
+++ b/app/assets/javascripts/boards/models/milestone.js
@@ -1,11 +1,9 @@
-import { isEE } from '~/lib/utils/common_utils';
-
export default class ListMilestone {
constructor(obj) {
this.id = obj.id;
this.title = obj.title;
- if (isEE) {
+ if (IS_EE) {
this.path = obj.path;
this.state = obj.state;
this.webUrl = obj.web_url || obj.webUrl;
diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
new file mode 100644
index 00000000000..8d22f009784
--- /dev/null
+++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
@@ -0,0 +1,35 @@
+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,
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 7d463f17ab1..56a6cab6c73 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -1,106 +1,87 @@
-import axios from '../../lib/utils/axios_utils';
-import { mergeUrlParams } from '../../lib/utils/url_utility';
+/* eslint-disable class-methods-use-this */
+/**
+ * This file is intended to be deleted.
+ * The existing functions will removed one by one in favor of using the board store directly.
+ * see https://gitlab.com/gitlab-org/gitlab-ce/issues/61621
+ */
-export default class BoardService {
- constructor({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) {
- this.boardsEndpoint = boardsEndpoint;
- this.boardId = boardId;
- this.listsEndpoint = listsEndpoint;
- this.listsEndpointGenerate = `${listsEndpoint}/generate.json`;
- this.bulkUpdatePath = bulkUpdatePath;
- this.recentBoardsEndpoint = `${recentBoardsEndpoint}.json`;
- }
+import boardsStore from '~/boards/stores/boards_store';
+export default class BoardService {
generateBoardsPath(id) {
- return `${this.boardsEndpoint}${id ? `/${id}` : ''}.json`;
+ return boardsStore.generateBoardsPath(id);
}
generateIssuesPath(id) {
- return `${this.listsEndpoint}${id ? `/${id}` : ''}/issues`;
+ return boardsStore.generateIssuesPath(id);
}
static generateIssuePath(boardId, id) {
- return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${
- id ? `/${id}` : ''
- }`;
+ return boardsStore.generateIssuePath(boardId, id);
}
all() {
- return axios.get(this.listsEndpoint);
+ return boardsStore.all();
}
generateDefaultLists() {
- return axios.post(this.listsEndpointGenerate, {});
+ return boardsStore.generateDefaultLists();
}
createList(entityId, entityType) {
- const list = {
- [entityType]: entityId,
- };
-
- return axios.post(this.listsEndpoint, {
- list,
- });
+ return boardsStore.createList(entityId, entityType);
}
updateList(id, position) {
- return axios.put(`${this.listsEndpoint}/${id}`, {
- list: {
- position,
- },
- });
+ return boardsStore.updateList(id, position);
}
destroyList(id) {
- return axios.delete(`${this.listsEndpoint}/${id}`);
+ return boardsStore.destroyList(id);
}
getIssuesForList(id, filter = {}) {
- const data = { id };
- Object.keys(filter).forEach(key => {
- data[key] = filter[key];
- });
-
- return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
+ return boardsStore.getIssuesForList(id, filter);
}
moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) {
- return axios.put(BoardService.generateIssuePath(this.boardId, id), {
- from_list_id: fromListId,
- to_list_id: toListId,
- move_before_id: moveBeforeId,
- move_after_id: moveAfterId,
- });
+ return boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId);
}
newIssue(id, issue) {
- return axios.post(this.generateIssuesPath(id), {
- issue,
- });
+ return boardsStore.newIssue(id, issue);
}
getBacklog(data) {
- return axios.get(
- mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`),
- );
+ return boardsStore.getBacklog(data);
}
bulkUpdate(issueIds, extraData = {}) {
- const data = {
- update: Object.assign(extraData, {
- issuable_ids: issueIds.join(','),
- }),
- };
-
- return axios.post(this.bulkUpdatePath, data);
+ return boardsStore.bulkUpdate(issueIds, extraData);
}
static getIssueInfo(endpoint) {
- return axios.get(endpoint);
+ return boardsStore.getIssueInfo(endpoint);
}
static toggleIssueSubscription(endpoint) {
- return axios.post(endpoint);
+ return boardsStore.toggleIssueSubscription(endpoint);
+ }
+
+ allBoards() {
+ return boardsStore.allBoards();
+ }
+
+ recentBoards() {
+ return boardsStore.recentBoards();
+ }
+
+ createBoard(board) {
+ return boardsStore.createBoard(board);
+ }
+
+ deleteBoard({ id }) {
+ return boardsStore.deleteBoard({ id });
}
}
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 4ba4cde6bae..f57c684691c 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -8,6 +8,8 @@ import Cookies from 'js-cookie';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../eventhub';
const boardsStore = {
@@ -28,6 +30,7 @@ const boardsStore = {
},
currentPage: '',
reload: false,
+ endpoints: {},
},
detail: {
issue: {},
@@ -36,6 +39,19 @@ const boardsStore = {
issue: {},
list: {},
},
+
+ setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) {
+ const listsEndpointGenerate = `${listsEndpoint}/generate.json`;
+ this.state.endpoints = {
+ boardsEndpoint,
+ boardId,
+ listsEndpoint,
+ listsEndpointGenerate,
+ bulkUpdatePath,
+ recentBoardsEndpoint: `${recentBoardsEndpoint}.json`,
+ };
+ },
+
create() {
this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&');
@@ -229,6 +245,139 @@ const boardsStore = {
setTimeTrackingLimitToHours(limitToHours) {
this.timeTracking.limitToHours = parseBoolean(limitToHours);
},
+
+ generateBoardsPath(id) {
+ return `${this.state.endpoints.boardsEndpoint}${id ? `/${id}` : ''}.json`;
+ },
+
+ generateIssuesPath(id) {
+ return `${this.state.endpoints.listsEndpoint}${id ? `/${id}` : ''}/issues`;
+ },
+
+ generateIssuePath(boardId, id) {
+ return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${
+ id ? `/${id}` : ''
+ }`;
+ },
+
+ all() {
+ return axios.get(this.state.endpoints.listsEndpoint);
+ },
+
+ generateDefaultLists() {
+ return axios.post(this.state.endpoints.listsEndpointGenerate, {});
+ },
+
+ createList(entityId, entityType) {
+ const list = {
+ [entityType]: entityId,
+ };
+
+ return axios.post(this.state.endpoints.listsEndpoint, {
+ list,
+ });
+ },
+
+ updateList(id, position) {
+ return axios.put(`${this.state.endpoints.listsEndpoint}/${id}`, {
+ list: {
+ position,
+ },
+ });
+ },
+
+ destroyList(id) {
+ return axios.delete(`${this.state.endpoints.listsEndpoint}/${id}`);
+ },
+
+ getIssuesForList(id, filter = {}) {
+ const data = { id };
+ Object.keys(filter).forEach(key => {
+ data[key] = filter[key];
+ });
+
+ return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
+ },
+
+ moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) {
+ return axios.put(this.generateIssuePath(this.state.endpoints.boardId, id), {
+ from_list_id: fromListId,
+ to_list_id: toListId,
+ move_before_id: moveBeforeId,
+ move_after_id: moveAfterId,
+ });
+ },
+
+ newIssue(id, issue) {
+ return axios.post(this.generateIssuesPath(id), {
+ issue,
+ });
+ },
+
+ getBacklog(data) {
+ return axios.get(
+ mergeUrlParams(
+ data,
+ `${gon.relative_url_root}/-/boards/${this.state.endpoints.boardId}/issues.json`,
+ ),
+ );
+ },
+
+ bulkUpdate(issueIds, extraData = {}) {
+ const data = {
+ update: Object.assign(extraData, {
+ issuable_ids: issueIds.join(','),
+ }),
+ };
+
+ return axios.post(this.state.endpoints.bulkUpdatePath, data);
+ },
+
+ getIssueInfo(endpoint) {
+ return axios.get(endpoint);
+ },
+
+ toggleIssueSubscription(endpoint) {
+ return axios.post(endpoint);
+ },
+
+ allBoards() {
+ return axios.get(this.generateBoardsPath());
+ },
+
+ recentBoards() {
+ return axios.get(this.state.endpoints.recentBoardsEndpoint);
+ },
+
+ createBoard(board) {
+ const boardPayload = { ...board };
+ boardPayload.label_ids = (board.labels || []).map(b => b.id);
+
+ if (boardPayload.label_ids.length === 0) {
+ boardPayload.label_ids = [''];
+ }
+
+ if (boardPayload.assignee) {
+ boardPayload.assignee_id = boardPayload.assignee.id;
+ }
+
+ if (boardPayload.milestone) {
+ boardPayload.milestone_id = boardPayload.milestone.id;
+ }
+
+ if (boardPayload.id) {
+ return axios.put(this.generateBoardsPath(boardPayload.id), { board: boardPayload });
+ }
+ return axios.post(this.generateBoardsPath(), { board: boardPayload });
+ },
+
+ deleteBoard({ id }) {
+ return axios.delete(this.generateBoardsPath(id));
+ },
+
+ setCurrentBoard(board) {
+ this.state.currentBoard = board;
+ },
};
BoardsStoreEE.initEESpecific(boardsStore);
diff --git a/app/assets/javascripts/boards/toggle_focus.js b/app/assets/javascripts/boards/toggle_focus.js
new file mode 100644
index 00000000000..2d1ec238274
--- /dev/null
+++ b/app/assets/javascripts/boards/toggle_focus.js
@@ -0,0 +1 @@
+export default () => {};