diff options
Diffstat (limited to 'app/assets/javascripts/boards/components')
25 files changed, 469 insertions, 342 deletions
diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue index afdf0290e8e..55e3e4a6329 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.vue +++ b/app/assets/javascripts/boards/components/board_blank_state.vue @@ -1,10 +1,14 @@ <script> +import { GlButton } from '@gitlab/ui'; import Cookies from 'js-cookie'; import { __ } from '~/locale'; import ListLabel from '~/boards/models/label'; import boardsStore from '../stores/boards_store'; export default { + components: { + GlButton, + }, data() { return { predefinedLabels: [ @@ -84,15 +88,17 @@ export default { ) }} </p> - <button - class="btn btn-success btn-inverted btn-block" - type="button" + <gl-button + category="secondary" + variant="success" + block="block" + class="gl-mb-0" @click.stop="addDefaultLists" > {{ s__('BoardBlankState|Add default lists') }} - </button> - <button class="btn btn-default btn-block" type="button" @click.stop="clearBlankState"> + </gl-button> + <gl-button category="secondary" variant="default" block="block" @click.stop="clearBlankState"> {{ s__("BoardBlankState|Nevermind, I'll use my own") }} - </button> + </gl-button> </div> </template> diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 246d3b9dcd1..31050eef83d 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -1,6 +1,5 @@ <script> -/* eslint-disable vue/require-default-prop */ -import IssueCardInner from './issue_card_inner.vue'; +import BoardCardLayout from './board_card_layout.vue'; import eventHub from '../eventhub'; import sidebarEventHub from '~/sidebar/event_hub'; import boardsStore from '../stores/boards_store'; @@ -8,7 +7,7 @@ import boardsStore from '../stores/boards_store'; export default { name: 'BoardsIssueCard', components: { - IssueCardInner, + BoardCardLayout, }, props: { list: { @@ -21,80 +20,29 @@ export default { default: () => ({}), required: false, }, - issueLinkBase: { - type: String, - default: '', - required: false, - }, - disabled: { - type: Boolean, - default: false, - required: false, - }, - index: { - type: Number, - default: 0, - required: false, - }, - rootPath: { - type: String, - default: '', - required: false, - }, - groupId: { - type: Number, - required: false, - }, - }, - data() { - return { - showDetail: false, - detailIssue: boardsStore.detail, - multiSelect: boardsStore.multiSelect, - }; - }, - computed: { - issueDetailVisible() { - return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; - }, - multiSelectVisible() { - return this.multiSelect.list.findIndex(issue => issue.id === this.issue.id) > -1; - }, - canMultiSelect() { - return gon.features && gon.features.multiSelectBoard; - }, }, methods: { - mouseDown() { - this.showDetail = true; + // These are methods instead of computed's, because boardsStore is not reactive. + isActive() { + return this.getActiveId() === this.issue.id; }, - mouseMove() { - this.showDetail = false; + getActiveId() { + return boardsStore.detail?.issue?.id; }, - showIssue(e) { - if (e.target.classList.contains('js-no-trigger')) return; - + showIssue({ isMultiSelect }) { // If no issues are opened, close all sidebars first - if (!boardsStore.detail?.issue?.id) { + if (!this.getActiveId()) { sidebarEventHub.$emit('sidebar.closeAll'); } + if (this.isActive()) { + eventHub.$emit('clearDetailIssue', isMultiSelect); - // If CMD or CTRL is clicked - const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey); - - if (this.showDetail || isMultiSelect) { - this.showDetail = false; - - if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) { - eventHub.$emit('clearDetailIssue', isMultiSelect); - - if (isMultiSelect) { - eventHub.$emit('newDetailIssue', this.issue, isMultiSelect); - } - } else { + if (isMultiSelect) { eventHub.$emit('newDetailIssue', this.issue, isMultiSelect); - boardsStore.setListDetail(this.list); } + } else { + eventHub.$emit('newDetailIssue', this.issue, isMultiSelect); + boardsStore.setListDetail(this.list); } }, }, @@ -102,28 +50,12 @@ export default { </script> <template> - <li - :class="{ - 'multi-select': multiSelectVisible, - 'user-can-drag': !disabled && issue.id, - 'is-disabled': disabled || !issue.id, - 'is-active': issueDetailVisible, - }" - :index="index" - :data-issue-id="issue.id" + <board-card-layout data-qa-selector="board_card" - class="board-card p-3 rounded" - @mousedown="mouseDown" - @mousemove="mouseMove" - @mouseup="showIssue($event)" - > - <issue-card-inner - :list="list" - :issue="issue" - :issue-link-base="issueLinkBase" - :group-id="groupId" - :root-path="rootPath" - :update-filters="true" - /> - </li> + :issue="issue" + :list="list" + :is-active="isActive()" + v-bind="$attrs" + @show="showIssue" + /> </template> diff --git a/app/assets/javascripts/boards/components/board_card_layout.vue b/app/assets/javascripts/boards/components/board_card_layout.vue new file mode 100644 index 00000000000..072dd87861a --- /dev/null +++ b/app/assets/javascripts/boards/components/board_card_layout.vue @@ -0,0 +1,93 @@ +<script> +import IssueCardInner from './issue_card_inner.vue'; +import boardsStore from '../stores/boards_store'; + +export default { + name: 'BoardsIssueCard', + components: { + IssueCardInner, + }, + props: { + list: { + type: Object, + default: () => ({}), + required: false, + }, + issue: { + type: Object, + default: () => ({}), + required: false, + }, + disabled: { + type: Boolean, + default: false, + required: false, + }, + index: { + type: Number, + default: 0, + required: false, + }, + isActive: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + showDetail: false, + multiSelect: boardsStore.multiSelect, + }; + }, + computed: { + multiSelectVisible() { + return this.multiSelect.list.findIndex(issue => issue.id === this.issue.id) > -1; + }, + canMultiSelect() { + return gon.features && gon.features.multiSelectBoard; + }, + }, + methods: { + mouseDown() { + this.showDetail = true; + }, + mouseMove() { + this.showDetail = false; + }, + showIssue(e) { + // Don't do anything if this happened on a no trigger element + if (e.target.classList.contains('js-no-trigger')) return; + + const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey); + + if (this.showDetail || isMultiSelect) { + this.showDetail = false; + this.$emit('show', { event: e, isMultiSelect }); + } + }, + }, +}; +</script> + +<template> + <li + :class="{ + 'multi-select': multiSelectVisible, + 'user-can-drag': !disabled && issue.id, + 'is-disabled': disabled || !issue.id, + 'is-active': isActive, + }" + :index="index" + :data-issue-id="issue.id" + :data-issue-iid="issue.iid" + :data-issue-path="issue.referencePath" + data-testid="board_card" + class="board-card p-3 rounded" + @mousedown="mouseDown" + @mousemove="mouseMove" + @mouseup="showIssue($event)" + > + <issue-card-inner :list="list" :issue="issue" :update-filters="true" /> + </li> +</template> diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue index dae24338e45..6d216911798 100644 --- a/app/assets/javascripts/boards/components/board_column.vue +++ b/app/assets/javascripts/boards/components/board_column.vue @@ -1,9 +1,11 @@ <script> +import { mapGetters, mapActions } from 'vuex'; import Sortable from 'sortablejs'; import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits'; import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue'; import Tooltip from '~/vue_shared/directives/tooltip'; import EmptyComponent from '~/vue_shared/components/empty_component'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import BoardBlankState from './board_blank_state.vue'; import BoardList from './board_list.vue'; import boardsStore from '../stores/boards_store'; @@ -21,7 +23,7 @@ export default { directives: { Tooltip, }, - mixins: [isWipLimitsOn], + mixins: [isWipLimitsOn, glFeatureFlagMixin()], props: { list: { type: Object, @@ -32,27 +34,15 @@ export default { type: Boolean, required: true, }, - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, - boardId: { - type: String, - required: true, - }, canAdminList: { type: Boolean, required: false, default: false, }, - groupId: { - type: Number, - required: false, - default: null, + }, + inject: { + boardId: { + type: String, }, }, data() { @@ -62,6 +52,7 @@ export default { }; }, computed: { + ...mapGetters(['getIssues']), showBoardListAndBoardInfo() { return this.list.type !== ListType.blank && this.list.type !== ListType.promotion; }, @@ -69,19 +60,36 @@ export default { // eslint-disable-next-line @gitlab/require-i18n-strings return `boards.${this.boardId}.${this.list.type}.${this.list.id}`; }, + listIssues() { + if (!this.glFeatures.graphqlBoardLists) { + return this.list.issues; + } + return this.getIssues(this.list.id); + }, + shouldFetchIssues() { + return this.glFeatures.graphqlBoardLists && this.list.type !== ListType.blank; + }, }, watch: { filter: { handler() { - this.list.page = 1; - this.list.getIssues(true).catch(() => { - // TODO: handle request error - }); + if (this.shouldFetchIssues) { + this.fetchIssuesForList(this.list.id); + } else { + this.list.page = 1; + this.list.getIssues(true).catch(() => { + // TODO: handle request error + }); + } }, deep: true, }, }, mounted() { + if (this.shouldFetchIssues) { + this.fetchIssuesForList(this.list.id); + } + const instance = this; const sortableOptions = getBoardSortableDefaultOptions({ @@ -108,6 +116,7 @@ export default { Sortable.create(this.$el.parentNode, sortableOptions); }, methods: { + ...mapActions(['fetchIssuesForList']), showListNewIssueForm(listId) { eventHub.$emit('showForm', listId); }, @@ -130,22 +139,14 @@ export default { <div class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base" > - <board-list-header - :can-admin-list="canAdminList" - :list="list" - :disabled="disabled" - :board-id="boardId" - /> + <board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" /> <board-list v-if="showBoardListAndBoardInfo" ref="board-list" :disabled="disabled" - :group-id="groupId || null" - :issue-link-base="issueLinkBase" - :issues="list.issues" + :issues="listIssues" :list="list" :loading="list.loading" - :root-path="rootPath" /> <board-blank-state v-if="canAdminList && list.id === 'blank'" /> diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue index c42295792f1..c7b3da0e672 100644 --- a/app/assets/javascripts/boards/components/board_content.vue +++ b/app/assets/javascripts/boards/components/board_content.vue @@ -1,13 +1,15 @@ <script> -import { mapState } from 'vuex'; +import { mapState, mapGetters, mapActions } from 'vuex'; import BoardColumn from 'ee_else_ce/boards/components/board_column.vue'; -import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue'; +import { GlAlert } from '@gitlab/ui'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { BoardColumn, - EpicsSwimlanes, + BoardContentSidebar: () => import('ee_component/boards/components/board_content_sidebar.vue'), + EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'), + GlAlert, }, mixins: [glFeatureFlagMixin()], props: { @@ -19,66 +21,58 @@ export default { type: Boolean, required: true, }, - groupId: { - type: Number, - required: false, - default: null, - }, disabled: { type: Boolean, required: true, }, - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, - boardId: { - type: String, - required: true, - }, }, computed: { - ...mapState(['isShowingEpicsSwimlanes', 'boardLists']), - isSwimlanesOn() { - return this.glFeatures.boardsWithSwimlanes && this.isShowingEpicsSwimlanes; + ...mapState(['boardLists', 'error']), + ...mapGetters(['isSwimlanesOn']), + boardListsToUse() { + return this.glFeatures.graphqlBoardLists ? this.boardLists : this.lists; }, }, + mounted() { + if (this.glFeatures.graphqlBoardLists) { + this.fetchLists(); + this.showPromotionList(); + } + }, + methods: { + ...mapActions(['fetchLists', 'showPromotionList']), + }, }; </script> <template> <div> + <gl-alert v-if="error" variant="danger" :dismissible="false"> + {{ error }} + </gl-alert> <div v-if="!isSwimlanesOn" class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap" data-qa-selector="boards_list" > <board-column - v-for="list in lists" + v-for="list in boardListsToUse" :key="list.id" ref="board" :can-admin-list="canAdminList" - :group-id="groupId" :list="list" :disabled="disabled" - :issue-link-base="issueLinkBase" - :root-path="rootPath" - :board-id="boardId" /> </div> - <epics-swimlanes - v-else - ref="swimlanes" - :lists="boardLists" - :can-admin-list="canAdminList" - :disabled="disabled" - :board-id="boardId" - :group-id="groupId" - :root-path="rootPath" - /> + + <template v-else> + <epics-swimlanes + ref="swimlanes" + :lists="boardLists" + :can-admin-list="canAdminList" + :disabled="disabled" + /> + <board-content-sidebar /> + </template> </div> </template> diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index 231059b895e..385dd5fdc71 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -25,11 +25,11 @@ export default { type: Boolean, required: true, }, - milestonePath: { + labelsPath: { type: String, required: true, }, - labelsPath: { + labelsWebUrl: { type: String, required: true, }, @@ -201,8 +201,8 @@ export default { :collapse-scope="isNewForm" :board="board" :can-admin-board="canAdminBoard" - :milestone-path="milestonePath" :labels-path="labelsPath" + :labels-web-url="labelsWebUrl" :enable-scoped-labels="enableScopedLabels" :project-id="projectId" :group-id="groupId" diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 1a26782f6f0..25f8ffca633 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -6,6 +6,7 @@ import boardCard from './board_card.vue'; import eventHub from '../eventhub'; import boardsStore from '../stores/boards_store'; import { sprintf, __ } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { getBoardSortableDefaultOptions, @@ -24,12 +25,8 @@ export default { boardNewIssue, GlLoadingIcon, }, + mixins: [glFeatureFlagMixin()], props: { - groupId: { - type: Number, - required: false, - default: 0, - }, disabled: { type: Boolean, required: true, @@ -46,14 +43,6 @@ export default { type: Boolean, required: true, }, - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, }, data() { return { @@ -83,6 +72,7 @@ export default { deep: true, }, issues() { + if (this.glFeatures.graphqlBoardLists) return; this.$nextTick(() => { if ( this.scrollHeight() <= this.listHeight() && @@ -413,6 +403,8 @@ export default { this.showIssueForm = !this.showIssueForm; }, onScroll() { + if (this.glFeatures.graphqlBoardLists) return; + if (!this.list.loadingMore && this.scrollTop() > this.scrollHeight() - this.scrollOffset) { this.loadNextPage(); } @@ -430,11 +422,7 @@ export default { <div v-if="loading" class="board-list-loading text-center" :aria-label="__('Loading issues')"> <gl-loading-icon /> </div> - <board-new-issue - v-if="list.type !== 'closed' && showIssueForm" - :group-id="groupId" - :list="list" - /> + <board-new-issue v-if="list.type !== 'closed' && showIssueForm" :list="list" /> <ul v-show="!loading" ref="list" @@ -450,9 +438,6 @@ export default { :index="index" :list="list" :issue="issue" - :issue-link-base="issueLinkBase" - :group-id="groupId" - :root-path="rootPath" :disabled="disabled" /> <li v-if="showCount" class="board-list-count text-center" data-issue-id="-1"> diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index bafe07afb48..361fe252afb 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -1,4 +1,5 @@ <script> +import { mapActions } from 'vuex'; import { GlButton, GlButtonGroup, @@ -17,6 +18,7 @@ import boardsStore from '../stores/boards_store'; import eventHub from '../eventhub'; import { ListType } from '../constants'; import { isScopedLabel } from '~/lib/utils/common_utils'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { @@ -32,7 +34,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - mixins: [isWipLimitsOn], + mixins: [isWipLimitsOn, glFeatureFlagMixin()], props: { list: { type: Object, @@ -43,10 +45,6 @@ export default { type: Boolean, required: true, }, - boardId: { - type: String, - required: true, - }, canAdminList: { type: Boolean, required: false, @@ -58,6 +56,11 @@ export default { default: false, }, }, + inject: { + boardId: { + type: String, + }, + }, data() { return { weightFeatureAvailable: false, @@ -94,10 +97,11 @@ export default { showAssigneeListDetails() { return this.list.type === 'assignee' && (this.list.isExpanded || !this.isSwimlanesHeader); }, + issuesCount() { + return this.list.issuesSize; + }, issuesTooltipLabel() { - const { issuesSize } = this.list; - - return n__(`%d issue`, `%d issues`, issuesSize); + return n__(`%d issue`, `%d issues`, this.issuesCount); }, chevronTooltip() { return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand'); @@ -126,8 +130,12 @@ export default { collapsedTooltipTitle() { return this.listTitle || this.listAssignee; }, + shouldDisplaySwimlanes() { + return this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn; + }, }, methods: { + ...mapActions(['updateList']), showScopedLabels(label) { return boardsStore.scopedLabels.enabled && isScopedLabel(label); }, @@ -136,20 +144,28 @@ export default { eventHub.$emit(`toggle-issue-form-${this.list.id}`); }, toggleExpanded() { - if (this.list.isExpandable) { - this.list.isExpanded = !this.list.isExpanded; - - if (AccessorUtilities.isLocalStorageAccessSafe() && !this.isLoggedIn) { - localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded); - } + this.list.isExpanded = !this.list.isExpanded; - if (this.isLoggedIn) { - this.list.update(); - } + if (!this.isLoggedIn) { + this.addToLocalStorage(); + } else { + this.updateListFunction(); + } - // When expanding/collapsing, the tooltip on the caret button sometimes stays open. - // Close all tooltips manually to prevent dangling tooltips. - this.$root.$emit('bv::hide::tooltip'); + // When expanding/collapsing, the tooltip on the caret button sometimes stays open. + // Close all tooltips manually to prevent dangling tooltips. + this.$root.$emit('bv::hide::tooltip'); + }, + addToLocalStorage() { + if (AccessorUtilities.isLocalStorageAccessSafe()) { + localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded); + } + }, + updateListFunction() { + if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) { + this.updateList({ listId: this.list.id, collapsed: !this.list.isExpanded }); + } else { + this.list.update(); } }, }, @@ -172,7 +188,7 @@ export default { <h3 :class="{ 'user-can-drag': !disabled && !list.preset, - 'gl-py-3': !list.isExpanded && !isSwimlanesHeader, + 'gl-py-3 gl-h-full': !list.isExpanded && !isSwimlanesHeader, 'gl-border-b-0': !list.isExpanded || isSwimlanesHeader, 'gl-py-2': !list.isExpanded && isSwimlanesHeader, }" @@ -288,7 +304,7 @@ export default { <gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltipLabel" /> <span ref="issueCount" class="issue-count-badge-count"> <gl-icon class="gl-mr-2" name="issues" /> - <issue-count :issues-size="list.issuesSize" :max-issue-count="list.maxIssueCount" /> + <issue-count :issues-size="issuesCount" :max-issue-count="list.maxIssueCount" /> </span> <!-- The following is only true in EE. --> <template v-if="weightFeatureAvailable"> diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index 34e8438ba4c..348d485ff37 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -1,11 +1,13 @@ <script> import $ from 'jquery'; +import { mapActions, mapGetters } from 'vuex'; import { GlButton } from '@gitlab/ui'; import { getMilestone } from 'ee_else_ce/boards/boards_util'; import ListIssue from 'ee_else_ce/boards/models/issue'; import eventHub from '../eventhub'; import ProjectSelect from './project_select.vue'; import boardsStore from '../stores/boards_store'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { name: 'BoardNewIssue', @@ -13,17 +15,18 @@ export default { ProjectSelect, GlButton, }, + mixins: [glFeatureFlagMixin()], props: { - groupId: { - type: Number, - required: false, - default: 0, - }, list: { type: Object, required: true, }, }, + inject: { + groupId: { + type: Number, + }, + }, data() { return { title: '', @@ -32,18 +35,23 @@ export default { }; }, computed: { + ...mapGetters(['isSwimlanesOn']), disabled() { if (this.groupId) { return this.title === '' || !this.selectedProject.name; } return this.title === ''; }, + shouldDisplaySwimlanes() { + return this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn; + }, }, mounted() { this.$refs.input.focus(); eventHub.$on('setSelectedProject', this.setSelectedProject); }, methods: { + ...mapActions(['addListIssue', 'addListIssueFailure']), submit(e) { e.preventDefault(); if (this.title.trim() === '') return Promise.resolve(); @@ -70,21 +78,31 @@ export default { eventHub.$emit(`scroll-board-list-${this.list.id}`); this.cancel(); + if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) { + this.addListIssue({ list: this.list, issue, position: 0 }); + } + return this.list .newIssue(issue) .then(() => { // Need this because our jQuery very kindly disables buttons on ALL form submissions $(this.$refs.submitButton).enable(); - boardsStore.setIssueDetail(issue); - boardsStore.setListDetail(this.list); + if (!this.shouldDisplaySwimlanes && !this.glFeatures.graphqlBoardLists) { + boardsStore.setIssueDetail(issue); + boardsStore.setListDetail(this.list); + } }) .catch(() => { // Need this because our jQuery very kindly disables buttons on ALL form submissions $(this.$refs.submitButton).enable(); // Remove the issue - this.list.removeIssue(issue); + if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) { + this.addListIssueFailure({ list: this.list, issue }); + } else { + this.list.removeIssue(issue); + } // Show error message this.error = true; @@ -121,7 +139,7 @@ export default { <project-select v-if="groupId" :group-id="groupId" :list="list" /> <div class="clearfix gl-mt-3"> <gl-button - ref="submit-button" + ref="submitButton" :disabled="disabled" class="float-left" variant="success" @@ -129,9 +147,14 @@ export default { type="submit" >{{ __('Submit issue') }}</gl-button > - <gl-button class="float-right" type="button" variant="default" @click="cancel">{{ - __('Cancel') - }}</gl-button> + <gl-button + ref="cancelButton" + class="float-right" + type="button" + variant="default" + @click="cancel" + >{{ __('Cancel') }}</gl-button + > </div> </form> </div> diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue index 3149762ecdf..e2600883e89 100644 --- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue @@ -1,11 +1,12 @@ <script> import { GlDrawer, GlLabel } from '@gitlab/ui'; -import { mapActions, mapState } from 'vuex'; +import { mapActions, mapState, mapGetters } from 'vuex'; import { __ } from '~/locale'; import boardsStore from '~/boards/stores/boards_store'; import eventHub from '~/sidebar/event_hub'; import { isScopedLabel } from '~/lib/utils/common_utils'; -import { inactiveId } from '~/boards/constants'; +import { LIST } from '~/boards/constants'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; // NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options. export default { @@ -23,18 +24,20 @@ export default { BoardSettingsListTypes: () => import('ee_component/boards/components/board_settings_list_types.vue'), }, + mixins: [glFeatureFlagMixin()], computed: { - ...mapState(['activeId']), + ...mapGetters(['isSidebarOpen']), + ...mapState(['activeId', 'sidebarType', 'boardLists']), activeList() { /* Warning: Though a computed property it is not reactive because we are referencing a List Model class. Reactivity only applies to plain JS objects */ + if (this.glFeatures.graphqlBoardLists) { + return this.boardLists.find(({ id }) => id === this.activeId); + } return boardsStore.state.lists.find(({ id }) => id === this.activeId); }, - isSidebarOpen() { - return this.activeId !== inactiveId; - }, activeListLabel() { return this.activeList.label; }, @@ -44,18 +47,18 @@ export default { listTypeTitle() { return this.$options.labelListText; }, + showSidebar() { + return this.sidebarType === LIST; + }, }, created() { - eventHub.$on('sidebar.closeAll', this.closeSidebar); + eventHub.$on('sidebar.closeAll', this.unsetActiveId); }, beforeDestroy() { - eventHub.$off('sidebar.closeAll', this.closeSidebar); + eventHub.$off('sidebar.closeAll', this.unsetActiveId); }, methods: { - ...mapActions(['setActiveId']), - closeSidebar() { - this.setActiveId(inactiveId); - }, + ...mapActions(['unsetActiveId']), showScopedLabels(label) { return boardsStore.scopedLabels.enabled && isScopedLabel(label); }, @@ -65,10 +68,11 @@ export default { <template> <gl-drawer + v-if="showSidebar" class="js-board-settings-sidebar" :open="isSidebarOpen" :header-height="$options.headerHeight" - @close="closeSidebar" + @close="unsetActiveId" > <template #header>{{ $options.listSettingsText }}</template> <template v-if="isSidebarOpen"> diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 3790c494085..d26f15c1723 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -83,7 +83,7 @@ export default Vue.extend({ $('.js-issue-board-sidebar', this.$el).each((i, el) => { $(el) - .data('glDropdown') + .data('deprecatedJQueryDropdown') .clearMenu(); }); } @@ -95,7 +95,7 @@ export default Vue.extend({ }, }, created() { - // Get events from glDropdown + // Get events from deprecatedJQueryDropdown eventHub.$on('sidebar.removeAssignee', this.removeAssignee); eventHub.$on('sidebar.addAssignee', this.addAssignee); eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees); diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index 48f6ba6cfc7..271e1fc4b5f 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -36,10 +36,6 @@ export default { type: Object, required: true, }, - milestonePath: { - type: String, - required: true, - }, throttleDuration: { type: Number, default: 200, @@ -65,6 +61,10 @@ export default { type: String, required: true, }, + labelsWebUrl: { + type: String, + required: true, + }, projectId: { type: Number, required: true, @@ -335,8 +335,8 @@ export default { <board-form v-if="currentPage" - :milestone-path="milestonePath" :labels-path="labelsPath" + :labels-web-url="labelsWebUrl" :project-id="projectId" :group-id="groupId" :can-admin-board="canAdminBoard" diff --git a/app/assets/javascripts/boards/components/issuable_title.vue b/app/assets/javascripts/boards/components/issuable_title.vue new file mode 100644 index 00000000000..40627a9fab8 --- /dev/null +++ b/app/assets/javascripts/boards/components/issuable_title.vue @@ -0,0 +1,21 @@ +<script> +export default { + props: { + title: { + type: String, + required: true, + }, + refPath: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <div data-testid="issue-title"> + <p class="gl-font-weight-bold">{{ title }}</p> + <p class="gl-mb-0">{{ refPath }}</p> + </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 d90928f35b6..8658f51e5cf 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -1,10 +1,9 @@ <script> import { sortBy } from 'lodash'; import { mapState } from 'vuex'; -import { GlLabel, GlTooltipDirective } from '@gitlab/ui'; +import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner'; import { sprintf, __ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import IssueDueDate from './issue_due_date.vue'; @@ -15,7 +14,7 @@ import { isScopedLabel } from '~/lib/utils/common_utils'; export default { components: { GlLabel, - Icon, + GlIcon, UserAvatarLink, TooltipOnTruncate, IssueDueDate, @@ -31,28 +30,23 @@ export default { type: Object, required: true, }, - issueLinkBase: { - type: String, - required: true, - }, list: { type: Object, required: false, default: () => ({}), }, - rootPath: { - type: String, - required: true, - }, updateFilters: { type: Boolean, required: false, default: false, }, + }, + inject: { groupId: { type: Number, - required: false, - default: null, + }, + rootPath: { + type: String, }, }, data() { @@ -148,7 +142,7 @@ export default { <div> <div class="d-flex board-card-header" dir="auto"> <h4 class="board-card-title gl-mb-0 gl-mt-0"> - <icon + <gl-icon v-if="issue.blocked" v-gl-tooltip name="issue-block" @@ -156,7 +150,7 @@ export default { class="issue-blocked-icon gl-mr-2" :aria-label="__('Blocked issue')" /> - <icon + <gl-icon v-if="issue.confidential" v-gl-tooltip name="eye-slash" diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue index 4add5ee646a..fb45de6e14d 100644 --- a/app/assets/javascripts/boards/components/issue_due_date.vue +++ b/app/assets/javascripts/boards/components/issue_due_date.vue @@ -1,7 +1,6 @@ <script> import dateFormat from 'dateformat'; -import { GlTooltip } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlTooltip, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import { getDayDifference, @@ -12,7 +11,7 @@ import { export default { components: { - Icon, + GlIcon, GlTooltip, }, props: { @@ -87,7 +86,7 @@ export default { <template> <span> <span ref="issueDueDate" :class="cssClass" class="board-card-info card-number"> - <icon :class="{ 'text-danger': isPastDue }" class="board-card-info-icon" name="calendar" /> + <gl-icon :class="{ 'text-danger': isPastDue }" class="board-card-info-icon" name="calendar" /> <time :class="{ 'text-danger': isPastDue }" datetime="date" class="board-card-info-text">{{ body }}</time> diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue index e8b7689da13..fe56833016e 100644 --- a/app/assets/javascripts/boards/components/issue_time_estimate.vue +++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue @@ -1,12 +1,11 @@ <script> -import { GlTooltip } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlTooltip, GlIcon } from '@gitlab/ui'; import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; import boardsStore from '../stores/boards_store'; export default { components: { - Icon, + GlIcon, GlTooltip, }, props: { @@ -34,7 +33,7 @@ export default { <template> <span> <span ref="issueTimeEstimate" class="board-card-info card-number"> - <icon name="hourglass" class="board-card-info-icon" /><time class="board-card-info-text">{{ + <gl-icon name="hourglass" class="board-card-info-icon" /><time class="board-card-info-text">{{ timeEstimate }}</time> </span> diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue index 66f59009714..cd4512f320f 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.vue +++ b/app/assets/javascripts/boards/components/modal/empty_state.vue @@ -1,9 +1,14 @@ <script> +/* eslint-disable vue/no-v-html */ +import { GlButton } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import ModalStore from '../../stores/modal_store'; import modalMixin from '../../mixins/modal_mixins'; export default { + components: { + GlButton, + }, mixins: [modalMixin], props: { newIssuePath: { @@ -53,17 +58,22 @@ 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> - <button + <gl-button + v-if="activeTab === 'all'" + :href="newIssuePath" + category="secondary" + variant="success" + > + {{ __('New issue') }} + </gl-button> + <gl-button v-if="activeTab === 'selected'" - class="btn btn-default" - type="button" + category="primary" + variant="default" @click="changeTab('all')" > {{ __('Open issues') }} - </button> + </gl-button> </div> </div> </div> diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue index c4953dda793..d28a03da97f 100644 --- a/app/assets/javascripts/boards/components/modal/footer.vue +++ b/app/assets/javascripts/boards/components/modal/footer.vue @@ -1,4 +1,5 @@ <script> +import { GlButton } from '@gitlab/ui'; import footerEEMixin from 'ee_else_ce/boards/mixins/modal_footer'; import { deprecatedCreateFlash as Flash } from '../../../flash'; import { __, n__ } from '../../../locale'; @@ -10,6 +11,7 @@ import boardsStore from '../../stores/boards_store'; export default { components: { ListsDropdown, + GlButton, }, mixins: [modalMixin, footerEEMixin], data() { @@ -65,14 +67,14 @@ export default { <template> <footer class="form-actions add-issues-footer"> <div class="float-left"> - <button :disabled="submitDisabled" class="btn btn-success" type="button" @click="addIssues"> + <gl-button :disabled="submitDisabled" category="primary" variant="success" @click="addIssues"> {{ submitText }} - </button> + </gl-button> <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)"> + <gl-button class="float-right" @click="toggleModal(false)"> {{ __('Cancel') }} - </button> + </gl-button> </footer> </template> diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue index 573284d2b44..3e96ecca24c 100644 --- a/app/assets/javascripts/boards/components/modal/header.vue +++ b/app/assets/javascripts/boards/components/modal/header.vue @@ -1,5 +1,6 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ +import { GlButton } from '@gitlab/ui'; import { __ } from '~/locale'; import ModalFilters from './filters'; import ModalTabs from './tabs.vue'; @@ -10,6 +11,7 @@ export default { components: { ModalTabs, ModalFilters, + GlButton, }, mixins: [modalMixin], props: { @@ -17,10 +19,6 @@ export default { type: Number, required: true, }, - milestonePath: { - type: String, - required: true, - }, labelPath: { type: String, required: true, @@ -43,7 +41,7 @@ export default { }, methods: { toggleAll() { - this.$refs.selectAllBtn.blur(); + this.$refs.selectAllBtn.$el.blur(); ModalStore.toggleAll(); }, @@ -55,28 +53,28 @@ export default { <header class="add-issues-header border-top-0 form-actions"> <h2 class="m-0"> Add issues - <button - type="button" + <gl-button + category="tertiary" + icon="close" class="close" data-dismiss="modal" :aria-label="__('Close')" @click="toggleModal(false)" - > - <span aria-hidden="true">×</span> - </button> + /> </h2> </header> <modal-tabs v-if="!loading && issuesCount > 0" /> <div v-if="showSearch" class="d-flex gl-mb-3"> <modal-filters :store="filter" /> - <button + <gl-button ref="selectAllBtn" - type="button" - class="btn btn-success btn-inverted gl-ml-3" + category="secondary" + variant="success" + class="gl-ml-3" @click="toggleAll" > {{ selectAllText }} - </button> + </gl-button> </div> </div> </template> diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue index 20344b66140..817b3bdddb0 100644 --- a/app/assets/javascripts/boards/components/modal/index.vue +++ b/app/assets/javascripts/boards/components/modal/index.vue @@ -26,22 +26,10 @@ export default { type: String, required: true, }, - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, projectId: { type: Number, required: true, }, - milestonePath: { - type: String, - required: true, - }, labelPath: { type: String, required: true, @@ -149,17 +137,8 @@ export default { class="add-issues-modal d-flex position-fixed position-top-0 position-bottom-0 position-left-0 position-right-0 h-100" > <div class="add-issues-container d-flex flex-column m-auto rounded"> - <modal-header - :project-id="projectId" - :milestone-path="milestonePath" - :label-path="labelPath" - /> - <modal-list - v-if="!loading && showList && !filterLoading" - :issue-link-base="issueLinkBase" - :root-path="rootPath" - :empty-state-svg="emptyStateSvg" - /> + <modal-header :project-id="projectId" :label-path="labelPath" /> + <modal-list v-if="!loading && showList && !filterLoading" :empty-state-svg="emptyStateSvg" /> <empty-state v-if="showEmptyState" :new-issue-path="newIssuePath" diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue index 78e3351a79e..219263bd9b9 100644 --- a/app/assets/javascripts/boards/components/modal/list.vue +++ b/app/assets/javascripts/boards/components/modal/list.vue @@ -1,23 +1,15 @@ <script> import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import ModalStore from '../../stores/modal_store'; import IssueCardInner from '../issue_card_inner.vue'; export default { components: { IssueCardInner, - Icon, + GlIcon, }, props: { - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, emptyStateSvg: { type: String, required: true, @@ -134,8 +126,8 @@ export default { class="board-card position-relative p-3 rounded" @click="toggleIssue($event, issue)" > - <issue-card-inner :issue="issue" :issue-link-base="issueLinkBase" :root-path="rootPath" /> - <icon + <issue-card-inner :issue="issue" /> + <gl-icon v-if="issue.selected" :aria-label="'Issue #' + issue.id + ' selected'" name="mobile-issue-close" diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue index 3fbe8fe1be7..fe10e7fb856 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue @@ -1,13 +1,12 @@ <script> -import { GlLink } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlLink, GlIcon } from '@gitlab/ui'; import ModalStore from '../../stores/modal_store'; import boardsStore from '../../stores/boards_store'; export default { components: { GlLink, - Icon, + GlIcon, }, data() { return { @@ -29,7 +28,7 @@ export default { <div class="dropdown inline"> <button class="dropdown-menu-toggle" type="button" data-toggle="dropdown" aria-expanded="false"> <span :style="{ backgroundColor: selected.label.color }" class="dropdown-label-box"> </span> - {{ selected.title }} <icon name="chevron-down" /> + {{ selected.title }} <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon" /> </button> <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"> <ul> diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 2b9fdf11b37..2e356f1353a 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -6,6 +6,7 @@ import axios from '~/lib/utils/axios_utils'; import { deprecatedCreateFlash as flash } from '~/flash'; import CreateLabelDropdown from '../../create_label'; import boardsStore from '../stores/boards_store'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; $(document) .off('created.label') @@ -36,7 +37,7 @@ export default function initNewListDropdown() { $dropdownToggle.data('projectPath'), ); - $dropdownToggle.glDropdown({ + initDeprecatedJQueryDropdown($dropdownToggle, { data(term, callback) { axios .get($dropdownToggle.attr('data-list-labels-path')) diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index 598e92726c1..59e7620962a 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -1,30 +1,30 @@ <script> import $ from 'jquery'; import { escape } from 'lodash'; -import { GlLoadingIcon } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import eventHub from '../eventhub'; import Api from '../../api'; import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default { name: 'BoardProjectSelect', components: { - Icon, + GlIcon, GlLoadingIcon, }, props: { - groupId: { - type: Number, - required: true, - default: 0, - }, list: { type: Object, required: true, }, }, + inject: { + groupId: { + type: Number, + }, + }, data() { return { loading: true, @@ -37,7 +37,7 @@ export default { }, }, mounted() { - $(this.$refs.projectsDropdown).glDropdown({ + initDeprecatedJQueryDropdown($(this.$refs.projectsDropdown), { filterable: true, filterRemote: true, search: { @@ -105,13 +105,13 @@ export default { data-toggle="dropdown" aria-expanded="false" > - {{ selectedProjectName }} <icon name="chevron-down" /> + {{ selectedProjectName }} <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon" /> </button> <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width"> <div class="dropdown-title">{{ __('Projects') }}</div> <div class="dropdown-input"> <input class="dropdown-input-field" type="search" :placeholder="__('Search projects')" /> - <icon name="search" class="dropdown-input-search" data-hidden="true" /> + <gl-icon name="search" class="dropdown-input-search" data-hidden="true" /> </div> <div class="dropdown-content"></div> <div class="dropdown-loading"><gl-loading-icon /></div> diff --git a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue new file mode 100644 index 00000000000..8df03ea581f --- /dev/null +++ b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue @@ -0,0 +1,79 @@ +<script> +import { GlButton, GlLoadingIcon } from '@gitlab/ui'; + +export default { + components: { GlButton, GlLoadingIcon }, + props: { + title: { + type: String, + required: false, + default: '', + }, + loading: { + type: Boolean, + required: false, + default: false, + }, + }, + inject: ['canUpdate'], + data() { + return { + edit: false, + }; + }, + destroyed() { + window.removeEventListener('click', this.collapseWhenOffClick); + }, + methods: { + collapseWhenOffClick({ target }) { + if (!this.$el.contains(target)) { + this.collapse(); + } + }, + expand() { + if (this.edit) { + return; + } + + this.edit = true; + this.$emit('changed', this.edit); + window.addEventListener('click', this.collapseWhenOffClick); + }, + collapse() { + if (!this.edit) { + return; + } + + this.edit = false; + this.$emit('changed', this.edit); + window.removeEventListener('click', this.collapseWhenOffClick); + }, + }, +}; +</script> + +<template> + <div> + <div class="gl-display-flex gl-justify-content-space-between gl-mb-3"> + <span class="gl-vertical-align-middle"> + <span data-testid="title">{{ title }}</span> + <gl-loading-icon v-if="loading" inline class="gl-ml-2" /> + </span> + <gl-button + v-if="canUpdate" + variant="link" + class="gl-text-gray-900!" + data-testid="edit-button" + @click="expand()" + > + {{ __('Edit') }} + </gl-button> + </div> + <div v-show="!edit" class="gl-text-gray-400" data-testid="collapsed-content"> + <slot name="collapsed">{{ __('None') }}</slot> + </div> + <div v-show="edit" data-testid="expanded-content"> + <slot></slot> + </div> + </div> +</template> |