Commit 8b881b9a authored by Lukas 'Eipi' Eipert's avatar Lukas 'Eipi' Eipert Committed by Luke Duncalfe

Remove add issues modal code from issue boards

This feature has been disabled since 13.6 with a feature flag. Seems
like we have not gotten any feedback that we should enable it, so let's
remove it from the codebase.

One motivation for removing it: It uses the Vue runtime template
compiler. Removing this will bring us one step closer to shipping Vue
without the template compiler which will bring a performance improvement
across all pages.

https://gitlab.com/gitlab-org/gitlab/-/issues/205169
https://gitlab.com/gitlab-org/gitlab/-/issues/292803
parent ada427b8
<script>
import { GlTooltip, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
name: 'BoardExtraActions',
components: {
GlTooltip,
GlButton,
},
props: {
canAdminList: {
type: Boolean,
required: true,
},
disabled: {
type: Boolean,
required: true,
},
openModal: {
type: Function,
required: true,
},
},
computed: {
tooltipTitle() {
if (this.disabled) {
return __('Please add a list to your board first');
}
return '';
},
},
};
</script>
<template>
<div class="board-extra-actions">
<span ref="addIssuesButtonTooltip" class="gl-ml-3">
<gl-button
v-if="canAdminList"
type="button"
data-placement="bottom"
data-track-event="click_button"
data-track-label="board_add_issues"
:disabled="disabled"
:aria-disabled="disabled"
@click="openModal"
>
{{ __('Add issues') }}
</gl-button>
</span>
<gl-tooltip v-if="disabled" :target="() => $refs.addIssuesButtonTooltip" placement="bottom">
{{ tooltipTitle }}
</gl-tooltip>
</div>
</template>
<script>
import { GlButton, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import modalMixin from '../../mixins/modal_mixins';
import ModalStore from '../../stores/modal_store';
export default {
components: {
GlButton,
GlSprintf,
},
mixins: [modalMixin],
props: {
newIssuePath: {
type: String,
required: true,
},
emptyStateSvg: {
type: String,
required: true,
},
},
data() {
return ModalStore.store;
},
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.',
),
};
if (this.activeTab === 'selected') {
obj.title = __("You haven't selected any issues yet");
obj.content = __(
'Go back to %{tagStart}Open issues%{tagEnd} and select some issues to add to your board.',
);
}
return obj;
},
},
};
</script>
<template>
<section class="empty-state d-flex mt-0 h-100">
<div class="row w-100 my-auto mx-0">
<div class="col-12 col-md-6 order-md-last">
<aside class="svg-content d-none d-md-block"><img :src="emptyStateSvg" /></aside>
</div>
<div class="col-12 col-md-6 order-md-first">
<div class="text-content">
<h4>{{ contents.title }}</h4>
<p>
<gl-sprintf :message="contents.content">
<template #tag="{ content }">
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
</p>
<gl-button
v-if="activeTab === 'all'"
:href="newIssuePath"
category="secondary"
variant="success"
>
{{ __('New issue') }}
</gl-button>
<gl-button
v-if="activeTab === 'selected'"
category="primary"
variant="default"
@click="changeTab('all')"
>
{{ __('Open issues') }}
</gl-button>
</div>
</div>
</div>
</section>
</template>
import FilteredSearchContainer from '../../../filtered_search/container';
import FilteredSearchBoards from '../../filtered_search_boards';
export default {
name: 'modal-filters',
props: {
store: {
type: Object,
required: true,
},
},
mounted() {
FilteredSearchContainer.container = this.$el;
this.filteredSearch = new FilteredSearchBoards(this.store);
this.filteredSearch.setup();
this.filteredSearch.removeTokens();
this.filteredSearch.handleInputPlaceholder();
this.filteredSearch.toggleClearSearchButton();
},
destroyed() {
this.filteredSearch.cleanup();
FilteredSearchContainer.container = document;
this.store.path = '';
},
template: '#js-board-modal-filter',
};
<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';
import modalMixin from '../../mixins/modal_mixins';
import boardsStore from '../../stores/boards_store';
import ModalStore from '../../stores/modal_store';
import ListsDropdown from './lists_dropdown.vue';
export default {
components: {
ListsDropdown,
GlButton,
},
mixins: [modalMixin, footerEEMixin],
data() {
return {
modal: ModalStore.store,
state: boardsStore.state,
};
},
computed: {
submitDisabled() {
return !ModalStore.selectedCount();
},
submitText() {
const count = ModalStore.selectedCount();
if (!count) return __('Add issues');
return n__(`Add %d issue`, `Add %d issues`, count);
},
},
methods: {
buildUpdateRequest(list) {
return {
add_label_ids: [list.label.id],
};
},
addIssues() {
const firstListIndex = 1;
const list = this.modal.selectedList || this.state.lists[firstListIndex];
const selectedIssues = ModalStore.getSelectedIssues();
const issueIds = selectedIssues.map((issue) => issue.id);
const req = this.buildUpdateRequest(list);
// Post the data to the backend
boardsStore.bulkUpdate(issueIds, req).catch(() => {
Flash(__('Failed to update issues, please try again.'));
selectedIssues.forEach((issue) => {
list.removeIssue(issue);
list.issuesSize -= 1;
});
});
// Add the issues on the frontend
selectedIssues.forEach((issue) => {
list.addIssue(issue);
list.issuesSize += 1;
});
this.toggleModal(false);
},
},
};
</script>
<template>
<footer class="form-actions add-issues-footer">
<div class="float-left">
<gl-button :disabled="submitDisabled" category="primary" variant="success" @click="addIssues">
{{ submitText }}
</gl-button>
<span class="inline add-issues-footer-to-list">{{ __('to list') }}</span>
<lists-dropdown />
</div>
<gl-button class="float-right" @click="toggleModal(false)">
{{ __('Cancel') }}
</gl-button>
</footer>
</template>
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import modalMixin from '../../mixins/modal_mixins';
import ModalStore from '../../stores/modal_store';
import ModalFilters from './filters';
import ModalTabs from './tabs.vue';
export default {
components: {
ModalTabs,
ModalFilters,
GlButton,
},
mixins: [modalMixin],
props: {
projectId: {
type: Number,
required: true,
},
labelPath: {
type: String,
required: true,
},
},
data() {
return ModalStore.store;
},
computed: {
selectAllText() {
if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
return __('Select all');
}
return __('Deselect all');
},
showSearch() {
return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
},
},
methods: {
toggleAll() {
this.$refs.selectAllBtn.$el.blur();
ModalStore.toggleAll();
},
},
};
</script>
<template>
<div>
<header class="add-issues-header border-top-0 form-actions">
<h2 class="m-0">
Add issues
<gl-button
category="tertiary"
icon="close"
class="close"
data-dismiss="modal"
:aria-label="__('Close')"
@click="toggleModal(false)"
/>
</h2>
</header>
<modal-tabs v-if="!loading && issuesCount > 0" />
<div v-if="showSearch" class="d-flex gl-mb-3">
<modal-filters :store="filter" />
<gl-button
ref="selectAllBtn"
category="secondary"
variant="success"
class="gl-ml-3"
@click="toggleAll"
>
{{ selectAllText }}
</gl-button>
</div>
</div>
</template>
<script>
/* global ListIssue */
import { GlLoadingIcon } from '@gitlab/ui';
import boardsStore from '~/boards/stores/boards_store';
import { urlParamsToObject } from '~/lib/utils/common_utils';
import ModalStore from '../../stores/modal_store';
import EmptyState from './empty_state.vue';
import ModalFooter from './footer.vue';
import ModalHeader from './header.vue';
import ModalList from './list.vue';
export default {
components: {
EmptyState,
ModalHeader,
ModalList,
ModalFooter,
GlLoadingIcon,
},
props: {
newIssuePath: {
type: String,
required: true,
},
emptyStateSvg: {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
labelPath: {
type: String,
required: true,
},
},
data() {
return ModalStore.store;
},
computed: {
showList() {
if (this.activeTab === 'selected') {
return this.selectedIssues.length > 0;
}
return this.issuesCount > 0;
},
showEmptyState() {
if (!this.loading && this.issuesCount === 0) {
return true;
}
return this.activeTab === 'selected' && this.selectedIssues.length === 0;
},
},
watch: {
page() {
this.loadIssues();
},
showAddIssuesModal() {
if (this.showAddIssuesModal && !this.issues.length) {
this.loading = true;
const loadingDone = () => {
this.loading = false;
};
this.loadIssues().then(loadingDone).catch(loadingDone);
} else if (!this.showAddIssuesModal) {
this.issues = [];
this.selectedIssues = [];
this.issuesCount = false;
}
},
filter: {
handler() {
if (this.$el.tagName) {
this.page = 1;
this.filterLoading = true;
const loadingDone = () => {
this.filterLoading = false;
};
this.loadIssues(true).then(loadingDone).catch(loadingDone);
}
},
deep: true,
},
},
created() {
this.page = 1;
},
methods: {
loadIssues(clearIssues = false) {
if (!this.showAddIssuesModal) return false;
return boardsStore
.getBacklog({
...urlParamsToObject(this.filter.path),
page: this.page,
per: this.perPage,
})
.then((res) => res.data)
.then((data) => {
if (clearIssues) {
this.issues = [];
}
data.issues.forEach((issueObj) => {
const issue = new ListIssue(issueObj);
const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
issue.selected = Boolean(foundSelectedIssue);
this.issues.push(issue);
});
this.loadingNewPage = false;
if (!this.issuesCount) {
this.issuesCount = data.size;
}
})
.catch(() => {
// TODO: handle request error
});
},
},
};
</script>
<template>
<div
v-if="showAddIssuesModal"
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" :label-path="labelPath" />
<modal-list v-if="!loading && showList && !filterLoading" :empty-state-svg="emptyStateSvg" />
<empty-state
v-if="showEmptyState"
:new-issue-path="newIssuePath"
:empty-state-svg="emptyStateSvg"
/>
<section v-if="loading || filterLoading" class="add-issues-list d-flex h-100 text-center">
<div class="add-issues-list-loading w-100 align-self-center">
<gl-loading-icon size="md" />
</div>
</section>
<modal-footer />
</div>
</div>
</template>
<script>
import { GlIcon } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import ModalStore from '../../stores/modal_store';
import BoardCardInner from '../board_card_inner.vue';
export default {
components: {
BoardCardInner,
GlIcon,
},
props: {
emptyStateSvg: {
type: String,
required: true,
},
},
data() {
return ModalStore.store;
},
computed: {
loopIssues() {
if (this.activeTab === 'all') {
return this.issues;
}
return this.selectedIssues;
},
groupedIssues() {
const groups = [];
this.loopIssues.forEach((issue, i) => {
const index = i % this.columns;
if (!groups[index]) {
groups.push([]);
}
groups[index].push(issue);
});
return groups;
},
},
watch: {
activeTab() {
if (this.activeTab === 'all') {
ModalStore.purgeUnselectedIssues();
}
},
},
mounted() {
this.scrollHandlerWrapper = this.scrollHandler.bind(this);
this.setColumnCountWrapper = this.setColumnCount.bind(this);
this.setColumnCount();
this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
window.addEventListener('resize', this.setColumnCountWrapper);
},
beforeDestroy() {
this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
window.removeEventListener('resize', this.setColumnCountWrapper);
},
methods: {
scrollHandler() {
const currentPage = Math.floor(this.issues.length / this.perPage);
if (
this.scrollTop() > this.scrollHeight() - 100 &&
!this.loadingNewPage &&
currentPage === this.page
) {
this.loadingNewPage = true;
this.page += 1;
}
},
toggleIssue(e, issue) {
if (e.target.tagName !== 'A') {
ModalStore.toggleIssue(issue);
}
},
listHeight() {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
showIssue(issue) {
if (this.activeTab === 'all') return true;
const index = ModalStore.selectedIssueIndex(issue);
return index !== -1;
},
setColumnCount() {
const breakpoint = bp.getBreakpointSize();
if (breakpoint === 'xl' || breakpoint === 'lg') {
this.columns = 3;
} else if (breakpoint === 'md') {
this.columns = 2;
} else {
this.columns = 1;
}
},
},
};
</script>
<template>
<section ref="list" class="add-issues-list add-issues-list-columns d-flex h-100">
<div
v-if="issuesCount > 0 && issues.length === 0"
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>
<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">
<div
:class="{ 'is-active': issue.selected }"
class="board-card position-relative p-3 rounded"
@click="toggleIssue($event, issue)"
>
<board-card-inner :item="issue" />
<gl-icon
v-if="issue.selected"
:aria-label="'Issue #' + issue.id + ' selected'"
name="mobile-issue-close"
aria-checked="true"
class="issue-card-selected text-center"
/>
</div>
</div>
</div>
</section>
</template>
<script>
import { GlLink, GlIcon } from '@gitlab/ui';
import boardsStore from '../../stores/boards_store';
import ModalStore from '../../stores/modal_store';
export default {
components: {
GlLink,
GlIcon,
},
data() {
return {
modal: ModalStore.store,
state: boardsStore.state,
};
},
computed: {
selected() {
return this.modal.selectedList || this.state.lists[1];
},
},
destroyed() {
this.modal.selectedList = null;
},
};
</script>
<template>
<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 }} <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon" />
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
<ul>
<li v-for="(list, i) in state.lists" v-if="list.type == 'label'" :key="i">
<gl-link
:class="{ 'is-active': list.id == selected.id }"
href="#"
role="button"
@click.prevent="modal.selectedList = list"
>
<span :style="{ backgroundColor: list.label.color }" class="dropdown-label-box"> </span>
{{ list.title }}
</gl-link>
</li>
</ul>
</div>
</div>
</template>
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
import modalMixin from '../../mixins/modal_mixins';
import ModalStore from '../../stores/modal_store';
export default {
components: {
GlTabs,
GlTab,
GlBadge,
},
mixins: [modalMixin],
data() {
return ModalStore.store;
},
computed: {
selectedCount() {
return ModalStore.selectedCount();
},
},
destroyed() {
this.activeTab = 'all';
},
};
</script>
<template>
<gl-tabs class="gl-mt-3">
<gl-tab @click.prevent="changeTab('all')">
<template slot="title">
<span>Open issues</span>
<gl-badge size="sm" class="gl-tab-counter-badge">{{ issuesCount }}</gl-badge>
</template>
</gl-tab>
<gl-tab @click.prevent="changeTab('selected')">
<template slot="title">
<span>Selected issues</span>
<gl-badge size="sm" class="gl-tab-counter-badge">{{ selectedCount }}</gl-badge>
</template>
</gl-tab>
</gl-tabs>
</template>
...@@ -38,7 +38,7 @@ export default { ...@@ -38,7 +38,7 @@ export default {
</script> </script>
<template> <template>
<div class="board-extra-actions gl-ml-3 gl-display-none gl-md-display-flex gl-align-items-center"> <div class="gl-ml-3 gl-display-none gl-md-display-flex gl-align-items-center">
<gl-button <gl-button
ref="toggleFocusModeButton" ref="toggleFocusModeButton"
v-gl-tooltip v-gl-tooltip
......
...@@ -2,4 +2,3 @@ export const setWeightFetchingState = () => {}; ...@@ -2,4 +2,3 @@ export const setWeightFetchingState = () => {};
export const setEpicFetchingState = () => {}; export const setEpicFetchingState = () => {};
export const getMilestoneTitle = () => ({}); export const getMilestoneTitle = () => ({});
export const getBoardsModalData = () => ({});
...@@ -10,26 +10,21 @@ import { ...@@ -10,26 +10,21 @@ import {
setWeightFetchingState, setWeightFetchingState,
setEpicFetchingState, setEpicFetchingState,
getMilestoneTitle, getMilestoneTitle,
getBoardsModalData,
} from 'ee_else_ce/boards/ee_functions'; } from 'ee_else_ce/boards/ee_functions';
import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes'; import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
import toggleLabels from 'ee_else_ce/boards/toggle_labels'; import toggleLabels from 'ee_else_ce/boards/toggle_labels';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue'; import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardContent from '~/boards/components/board_content.vue'; import BoardContent from '~/boards/components/board_content.vue';
import BoardExtraActions from '~/boards/components/board_extra_actions.vue';
import './models/label'; import './models/label';
import './models/assignee'; import './models/assignee';
import '~/boards/models/milestone'; import '~/boards/models/milestone';
import '~/boards/models/project'; import '~/boards/models/project';
import '~/boards/filters/due_date_filters'; import '~/boards/filters/due_date_filters';
import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
import { issuableTypes } from '~/boards/constants'; import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub'; import eventHub from '~/boards/eventhub';
import FilteredSearchBoards from '~/boards/filtered_search_boards'; import FilteredSearchBoards from '~/boards/filtered_search_boards';
import modalMixin from '~/boards/mixins/modal_mixins';
import store from '~/boards/stores'; import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store'; import boardsStore from '~/boards/stores/boards_store';
import ModalStore from '~/boards/stores/modal_store';
import toggleFocusMode from '~/boards/toggle_focus'; import toggleFocusMode from '~/boards/toggle_focus';
import { deprecatedCreateFlash as Flash } from '~/flash'; import { deprecatedCreateFlash as Flash } from '~/flash';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
...@@ -78,7 +73,6 @@ export default () => { ...@@ -78,7 +73,6 @@ export default () => {
components: { components: {
BoardContent, BoardContent,
BoardSidebar, BoardSidebar,
BoardAddIssuesModal,
BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'), BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'),
}, },
provide: { provide: {
...@@ -316,49 +310,7 @@ export default () => { ...@@ -316,49 +310,7 @@ export default () => {
boardConfigToggle(boardsStore); boardConfigToggle(boardsStore);
const issueBoardsModal = document.getElementById('js-add-issues-btn'); toggleFocusMode();
if (issueBoardsModal && gon.features.addIssuesButton) {
// eslint-disable-next-line no-new
new Vue({
el: issueBoardsModal,
mixins: [modalMixin],
data() {
return {
modal: ModalStore.store,
store: boardsStore.state,
...getBoardsModalData(),
canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
};
},
computed: {
disabled() {
if (!this.store || !this.store.lists) {
return true;
}
return !this.store.lists.filter((list) => !list.preset).length;
},
},
methods: {
openModal() {
if (!this.disabled) {
this.toggleModal(true);
}
},
},
render(createElement) {
return createElement(BoardExtraActions, {
props: {
canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
openModal: this.openModal,
disabled: this.disabled,
},
});
},
});
}
toggleFocusMode(ModalStore, boardsStore);
toggleLabels(); toggleLabels();
if (gon.licensed_features?.swimlanes) { if (gon.licensed_features?.swimlanes) {
......
import ModalStore from '../stores/modal_store';
export default {
methods: {
toggleModal(toggle) {
ModalStore.store.showAddIssuesModal = toggle;
},
changeTab(tab) {
ModalStore.store.activeTab = tab;
},
},
};
class ModalStore {
constructor() {
this.store = {
columns: 3,
issues: [],
issuesCount: false,
selectedIssues: [],
showAddIssuesModal: false,
activeTab: 'all',
selectedList: null,
searchTerm: '',
loading: false,
loadingNewPage: false,
filterLoading: false,
page: 1,
perPage: 50,
filter: {
path: '',
},
};
}
selectedCount() {
return this.getSelectedIssues().length;
}
toggleIssue(issueObj) {
const issue = issueObj;
const { selected } = issue;
issue.selected = !selected;
if (!selected) {
this.addSelectedIssue(issue);
} else {
this.removeSelectedIssue(issue);
}
}
toggleAll() {
const select = this.selectedCount() !== this.store.issues.length;
this.store.issues.forEach((issue) => {
const issueUpdate = issue;
if (issueUpdate.selected !== select) {
issueUpdate.selected = select;
if (select) {
this.addSelectedIssue(issue);
} else {
this.removeSelectedIssue(issue);
}
}
});
}
getSelectedIssues() {
return this.store.selectedIssues.filter((issue) => issue.selected);
}
addSelectedIssue(issue) {
const index = this.selectedIssueIndex(issue);
if (index === -1) {
this.store.selectedIssues.push(issue);
}
}
removeSelectedIssue(issue, forcePurge = false) {
if (this.store.activeTab === 'all' || forcePurge) {
this.store.selectedIssues = this.store.selectedIssues.filter(
(fIssue) => fIssue.id !== issue.id,
);
}
}
purgeUnselectedIssues() {
this.store.selectedIssues.forEach((issue) => {
if (!issue.selected) {
this.removeSelectedIssue(issue, true);
}
});
}
selectedIssueIndex(issue) {
return this.store.selectedIssues.indexOf(issue);
}
findSelectedIssue(issue) {
return this.store.selectedIssues.filter((filteredIssue) => filteredIssue.id === issue.id)[0];
}
}
export default new ModalStore();
/* eslint-disable no-useless-return, func-names, no-underscore-dangle, no-new, consistent-return, no-shadow, no-param-reassign, no-lonely-if, dot-notation, no-empty */ /* eslint-disable func-names, no-underscore-dangle, no-new, consistent-return, no-shadow, no-param-reassign, no-lonely-if, no-empty */
/* global Issuable */ /* global Issuable */
/* global ListLabel */ /* global ListLabel */
...@@ -7,7 +7,6 @@ import { difference, isEqual, escape, sortBy, template, union } from 'lodash'; ...@@ -7,7 +7,6 @@ import { difference, isEqual, escape, sortBy, template, union } from 'lodash';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { isScopedLabel } from '~/lib/utils/common_utils'; import { isScopedLabel } from '~/lib/utils/common_utils';
import boardsStore from './boards/stores/boards_store'; import boardsStore from './boards/stores/boards_store';
import ModalStore from './boards/stores/modal_store';
import CreateLabelDropdown from './create_label'; import CreateLabelDropdown from './create_label';
import { deprecatedCreateFlash as flash } from './flash'; import { deprecatedCreateFlash as flash } from './flash';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
...@@ -361,21 +360,7 @@ export default class LabelsSelect { ...@@ -361,21 +360,7 @@ export default class LabelsSelect {
return; return;
} }
let boardsModel; if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if ($dropdown.closest('.add-issues-modal').length) {
boardsModel = ModalStore.store.filter;
}
if (boardsModel) {
if (label.isAny) {
boardsModel['label_name'] = [];
} else if ($el.hasClass('is-active')) {
boardsModel['label_name'].push(label.title);
}
e.preventDefault();
return;
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (!$dropdown.hasClass('js-multiselect')) { if (!$dropdown.hasClass('js-multiselect')) {
selectedLabel = label.title; selectedLabel = label.title;
return Issuable.filterResults($dropdown.closest('form')); return Issuable.filterResults($dropdown.closest('form'));
......
...@@ -11,7 +11,6 @@ import boardsStore, { ...@@ -11,7 +11,6 @@ import boardsStore, {
boardStoreIssueSet, boardStoreIssueSet,
boardStoreIssueDelete, boardStoreIssueDelete,
} from './boards/stores/boards_store'; } from './boards/stores/boards_store';
import ModalStore from './boards/stores/modal_store';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { timeFor, parsePikadayDate, dateInWords } from './lib/utils/datetime_utility'; import { timeFor, parsePikadayDate, dateInWords } from './lib/utils/datetime_utility';
...@@ -211,7 +210,7 @@ export default class MilestoneSelect { ...@@ -211,7 +210,7 @@ export default class MilestoneSelect {
const { e } = clickEvent; const { e } = clickEvent;
let selected = clickEvent.selectedObj; let selected = clickEvent.selectedObj;
let data, modalStoreFilter; let data;
if (!selected) return; if (!selected) return;
if (options.handleClick) { if (options.handleClick) {
...@@ -234,14 +233,7 @@ export default class MilestoneSelect { ...@@ -234,14 +233,7 @@ export default class MilestoneSelect {
return; return;
} }
if ($dropdown.closest('.add-issues-modal').length) { if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
modalStoreFilter = ModalStore.store.filter;
}
if (modalStoreFilter) {
modalStoreFilter[$dropdown.data('fieldName')] = selected.name;
e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form')); return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) { } else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit(); return $dropdown.closest('form').submit();
......
...@@ -11,7 +11,6 @@ import { ...@@ -11,7 +11,6 @@ import {
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { isUserBusy } from '~/set_status_modal/utils'; import { isUserBusy } from '~/set_status_modal/utils';
import { fixTitle, dispose } from '~/tooltips'; import { fixTitle, dispose } from '~/tooltips';
import ModalStore from '../boards/stores/modal_store';
import axios from '../lib/utils/axios_utils'; import axios from '../lib/utils/axios_utils';
import { parseBoolean, spriteIcon } from '../lib/utils/common_utils'; import { parseBoolean, spriteIcon } from '../lib/utils/common_utils';
import { loadCSSFile } from '../lib/utils/css_utils'; import { loadCSSFile } from '../lib/utils/css_utils';
...@@ -504,9 +503,7 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -504,9 +503,7 @@ function UsersSelect(currentUser, els, options = {}) {
} }
return; return;
} }
if ($el.closest('.add-issues-modal').length) { if (handleClick) {
ModalStore.store.filter[$dropdown.data('fieldName')] = user.id;
} else if (handleClick) {
e.preventDefault(); e.preventDefault();
handleClick(user, isMarking); handleClick(user, isMarking);
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
......
...@@ -401,99 +401,6 @@ ...@@ -401,99 +401,6 @@
} }
} }
.add-issues-modal {
background-color: rgba($black, 0.3);
z-index: 9999;
}
.add-issues-container {
width: 90vw;
height: 85vh;
max-width: 1100px;
min-height: 500px;
padding: 25px 15px 0;
background-color: var(--white, $white);
box-shadow: 0 2px 12px rgba(var(--black, $black), 0.5);
.empty-state {
&.add-issues-empty-state-filter {
flex-direction: column;
justify-content: center;
}
.svg-content {
margin-top: -40px;
}
}
}
.add-issues-header {
margin: -25px -15px -5px;
border-bottom: 1px solid $border-color;
border-top-right-radius: $border-radius-default;
border-top-left-radius: $border-radius-default;
> h2 {
font-size: 18px;
}
}
.add-issues-list-column {
width: 100%;
@include media-breakpoint-up(sm) {
width: 50%;
}
@include media-breakpoint-up(md) {
width: (100% / 3);
}
}
.add-issues-list {
padding-top: 3px;
margin-left: -$gl-vert-padding;
margin-right: -$gl-vert-padding;
overflow-y: scroll;
.board-card-parent {
padding: 0 5px 5px;
}
.board-card {
border: 1px solid var(--gray-900, $gray-900);
box-shadow: 0 1px 2px rgba(var(--black, $black), 0.4);
cursor: pointer;
}
}
.add-issues-footer {
margin: auto -15px 0;
padding-left: 15px;
padding-right: 15px;
border-bottom-right-radius: $border-radius-default;
border-bottom-left-radius: $border-radius-default;
}
.add-issues-footer-to-list {
padding-left: $gl-vert-padding;
padding-right: $gl-vert-padding;
line-height: $input-height;
}
.issue-card-selected {
position: absolute;
right: -3px;
top: -3px;
width: 17px;
background-color: var(--blue-500, $blue-500);
color: $white;
border: 1px solid var(--blue-600, $blue-600);
font-size: 9px;
line-height: 15px;
border-radius: 50%;
}
.board-card-info { .board-card-info {
color: var(--gray-500, $gray-500); color: var(--gray-500, $gray-500);
white-space: nowrap; white-space: nowrap;
......
...@@ -8,7 +8,6 @@ class Projects::BoardsController < Projects::ApplicationController ...@@ -8,7 +8,6 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :authorize_read_board!, only: [:index, :show] before_action :authorize_read_board!, only: [:index, :show]
before_action :assign_endpoint_vars before_action :assign_endpoint_vars
before_action do before_action do
push_frontend_feature_flag(:add_issues_button)
push_frontend_feature_flag(:swimlanes_buffered_rendering, project, default_enabled: :yaml) push_frontend_feature_flag(:swimlanes_buffered_rendering, project, default_enabled: :yaml)
push_frontend_feature_flag(:graphql_board_lists, project, default_enabled: :yaml) push_frontend_feature_flag(:graphql_board_lists, project, default_enabled: :yaml)
end end
......
...@@ -115,17 +115,6 @@ module TimeboxesHelper ...@@ -115,17 +115,6 @@ module TimeboxesHelper
end end
end end
def milestones_filter_dropdown_path
project = @target_project || @project
if project
project_milestones_path(project, :json)
elsif @group
group_milestones_path(@group, :json)
else
dashboard_milestones_path(:json)
end
end
def milestone_time_for(date, date_type) def milestone_time_for(date, date_type)
title = date_type == :start ? "Start date" : "End date" title = date_type == :start ? "Start date" : "End date"
......
...@@ -13,10 +13,6 @@ ...@@ -13,10 +13,6 @@
- page_title("#{board.name}", _("Boards")) - page_title("#{board.name}", _("Boards"))
- add_page_specific_style 'page_bundles/boards' - add_page_specific_style 'page_bundles/boards'
- content_for :page_specific_javascripts do
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal, show_sorting_dropdown: false
= render 'shared/issuable/search_bar', type: :boards, board: board = render 'shared/issuable/search_bar', type: :boards, board: board
#board-app.boards-app.position-relative{ "v-cloak" => "true", data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" } #board-app.boards-app.position-relative{ "v-cloak" => "true", data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
%board-content{ "v-cloak" => "true", %board-content{ "v-cloak" => "true",
...@@ -27,9 +23,3 @@ ...@@ -27,9 +23,3 @@
data: { qa_selector: "boards_list" } } data: { qa_selector: "boards_list" } }
= render "shared/boards/components/sidebar", group: group = render "shared/boards/components/sidebar", group: group
%board-settings-sidebar{ ":can-admin-list" => can_admin_list } %board-settings-sidebar{ ":can-admin-list" => can_admin_list }
- if @project
%board-add-issues-modal{ "new-issue-path" => new_project_issue_path(@project),
"milestone-path" => milestones_filter_dropdown_path,
"label-path" => labels_filter_path_with_defaults,
"empty-state-svg" => image_path('illustrations/issues.svg'),
":project-id" => @project.id }
...@@ -3,16 +3,15 @@ ...@@ -3,16 +3,15 @@
- show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true) - show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true)
- disable_target_branch = local_assigns.fetch(:disable_target_branch, false) - disable_target_branch = local_assigns.fetch(:disable_target_branch, false)
- placeholder = local_assigns[:placeholder] || _('Search or filter results...') - placeholder = local_assigns[:placeholder] || _('Search or filter results...')
- is_not_boards_modal_or_productivity_analytics = type != :boards_modal && type != :productivity_analytics - block_css_class = type != :productivity_analytics ? 'row-content-block second-block' : ''
- block_css_class = is_not_boards_modal_or_productivity_analytics ? 'row-content-block second-block' : ''
- is_epic_board = board&.to_type == "EpicBoard" - is_epic_board = board&.to_type == "EpicBoard"
- if is_epic_board - if is_epic_board
- user_can_admin_list = can?(current_user, :admin_epic_board_list, board.resource_parent) - user_can_admin_list = can?(current_user, :admin_epic_board_list, board.resource_parent)
- elsif board - elsif board
- user_can_admin_list = can?(current_user, :admin_issue_board_list, board.resource_parent) - user_can_admin_list = can?(current_user, :admin_issue_board_list, board.resource_parent)
.issues-filters{ class: ("w-100" if type == :boards_modal) } .issues-filters
.issues-details-filters.filtered-search-block.d-flex.flex-column.flex-lg-row{ class: block_css_class, "v-pre" => type == :boards_modal } .issues-details-filters.filtered-search-block.d-flex.flex-column.flex-lg-row{ class: block_css_class }
.d-flex.flex-column.flex-md-row.flex-grow-1.mb-lg-0.mb-md-2.mb-sm-0.w-100 .d-flex.flex-column.flex-md-row.flex-grow-1.mb-lg-0.mb-md-2.mb-sm-0.w-100
- if type == :boards - if type == :boards
= render "shared/boards/switcher", board: board = render "shared/boards/switcher", board: board
...@@ -27,7 +26,7 @@ ...@@ -27,7 +26,7 @@
- else - else
.issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row .issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row
.filtered-search-box .filtered-search-box
- if type != :boards_modal && type != :boards - if type != :boards
- text = tag.span(sprite_icon('history'), class: "d-md-none") + tag.span(_('Recent searches'), class: "d-none d-md-inline") - text = tag.span(sprite_icon('history'), class: "d-md-none") + tag.span(_('Recent searches'), class: "d-none d-md-inline")
= dropdown_tag(text, = dropdown_tag(text,
options: { wrapper_class: "filtered-search-history-dropdown-wrapper", options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
...@@ -208,8 +207,6 @@ ...@@ -208,8 +207,6 @@
.js-create-column-trigger{ data: board_list_data } .js-create-column-trigger{ data: board_list_data }
- else - else
= render 'shared/issuable/board_create_list_dropdown', board: board = render 'shared/issuable/board_create_list_dropdown', board: board
- if @project
#js-add-issues-btn{ data: { can_admin_list: can?(current_user, :admin_issue_board_list, @project) } }
#js-toggle-focus-btn #js-toggle-focus-btn
- elsif is_not_boards_modal_or_productivity_analytics && show_sorting_dropdown - elsif type != :productivity_analytics && show_sorting_dropdown
= render 'shared/issuable/sort_dropdown' = render 'shared/issuable/sort_dropdown'
---
title: Remove add issues modal from issue boards (this has been disabled since 13.6)
merge_request: 57329
author:
type: removed
---
name: add_issues_button
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47898
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/292803
milestone: '13.6'
type: development
group: group::project management
default_enabled: false
...@@ -450,7 +450,6 @@ The feature is enabled by default when you use group issue boards with epic swim ...@@ -450,7 +450,6 @@ The feature is enabled by default when you use group issue boards with epic swim
- [Create a new list](#create-a-new-list). - [Create a new list](#create-a-new-list).
- [Remove an existing list](#remove-a-list). - [Remove an existing list](#remove-a-list).
- [Add issues to a list](#add-issues-to-a-list).
- [Remove an issue from a list](#remove-an-issue-from-a-list). - [Remove an issue from a list](#remove-an-issue-from-a-list).
- [Filter issues](#filter-issues) that appear across your issue board. - [Filter issues](#filter-issues) that appear across your issue board.
- [Create workflows](#create-workflows). - [Create workflows](#create-workflows).
...@@ -489,31 +488,19 @@ To remove a list from an issue board: ...@@ -489,31 +488,19 @@ To remove a list from an issue board:
1. Select **Remove list**. A confirmation dialog appears. 1. Select **Remove list**. A confirmation dialog appears.
1. Select **OK**. 1. Select **OK**.
### Add issues to a list **(FREE SELF)** ### Add issues to a list
> - Feature flag [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47898) in GitLab 13.7. > The **Add issues** button was [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57329) in GitLab 13.11.
> - It's [deployed behind a feature flag](../feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-adding-issues-to-the-list). **(FREE SELF)**
You can add issues to a list in a project issue board by clicking the **Add issues** button If your board is scoped to one or more attributes, go to the issues you want to add and apply the
in the top right corner of the issue board. This opens up a modal same attributes as your board scope.
window where you can see all the issues that do not belong to any list.
Select one or more issues by clicking the cards and then click **Add issues** For example, to add an issue to a list scoped to the `Doing` label, in a group issue board:
to add them to the selected list. You can limit the issues you want to add to
the list by filtering by the following:
- Assignee 1. Go to an issue in the group or one of the subgroups or projects.
- Author 1. Add the `Doing` label.
- Epic
- Iteration ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118742) in GitLab 13.6) The issue should now show in the `Doing` list on your issue board.
- Label
- Milestone
- My Reaction
- Release
- Weight
### Remove an issue from a list ### Remove an issue from a list
...@@ -657,24 +644,6 @@ To disable it: ...@@ -657,24 +644,6 @@ To disable it:
Feature.disable(:graphql_board_lists) Feature.disable(:graphql_board_lists)
``` ```
## Enable or disable adding issues to the list **(FREE SELF)**
Adding issues to the list is deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:add_issues_button)
```
To disable it:
```ruby
Feature.disable(:add_issues_button)
```
### Enable or disable new add list form **(FREE SELF)** ### Enable or disable new add list form **(FREE SELF)**
The new form for adding lists is under development and not ready for production use. It is The new form for adding lists is under development and not ready for production use. It is
......
...@@ -8,7 +8,3 @@ export const setEpicFetchingState = (issue, value) => { ...@@ -8,7 +8,3 @@ export const setEpicFetchingState = (issue, value) => {
export const getMilestoneTitle = ($boardApp) => ({ export const getMilestoneTitle = ($boardApp) => ({
milestoneTitle: $boardApp.dataset.boardMilestoneTitle, milestoneTitle: $boardApp.dataset.boardMilestoneTitle,
}); });
export const getBoardsModalData = () => ({
isFullscreen: false,
});
export default {
methods: {
buildUpdateRequest(list) {
const { currentBoard } = this.state;
const boardLabelIds = currentBoard.labels.map((label) => label.id);
const assigneeIds = currentBoard.assignee && [currentBoard.assignee.id];
return {
add_label_ids: [list.label.id, ...boardLabelIds],
milestone_id: currentBoard.milestone_id,
assignee_ids: assigneeIds,
weight: currentBoard.weight,
};
},
},
};
...@@ -12,7 +12,6 @@ import toggleLabels from 'ee_component/boards/toggle_labels'; ...@@ -12,7 +12,6 @@ import toggleLabels from 'ee_component/boards/toggle_labels';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue'; import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardContent from '~/boards/components/board_content.vue'; import BoardContent from '~/boards/components/board_content.vue';
import BoardAddIssuesModal from '~/boards/components/modal/index.vue';
import boardConfigToggle from '~/boards/config_toggle'; import boardConfigToggle from '~/boards/config_toggle';
import { issuableTypes } from '~/boards/constants'; import { issuableTypes } from '~/boards/constants';
import mountMultipleBoardsSwitcher from '~/boards/mount_multiple_boards_switcher'; import mountMultipleBoardsSwitcher from '~/boards/mount_multiple_boards_switcher';
...@@ -56,7 +55,6 @@ export default () => { ...@@ -56,7 +55,6 @@ export default () => {
components: { components: {
BoardContent, BoardContent,
BoardSidebar, BoardSidebar,
BoardAddIssuesModal,
BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'), BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'),
}, },
provide: { provide: {
......
- type = local_assigns.fetch(:type) - type = local_assigns.fetch(:type)
- return unless type == :issues || type == :boards || type == :boards_modal || type == :issues_analytics - return unless type == :issues || type == :boards || type == :issues_analytics
#js-dropdown-epic.filtered-search-input-dropdown-menu.dropdown-menu #js-dropdown-epic.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true }
......
- type = local_assigns.fetch(:type) - type = local_assigns.fetch(:type)
- return unless type == :issues || type == :boards || type == :boards_modal - return unless type == :issues || type == :boards
#js-dropdown-iteration.filtered-search-input-dropdown-menu.dropdown-menu #js-dropdown-iteration.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true }
......
- type = local_assigns.fetch(:type) - type = local_assigns.fetch(:type)
- return unless type == :issues || type == :boards || type == :boards_modal || type == :issues_analytics - return unless type == :issues || type == :boards || type == :issues_analytics
#js-dropdown-weight.filtered-search-input-dropdown-menu.dropdown-menu #js-dropdown-weight.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Issue Boards add issue modal', :js do
let(:project) { create(:project, :public) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let!(:label) { create(:label, project: project) }
let!(:list) { create(:list, board: board, label: label, position: 0) }
let!(:issue) { create(:issue, project: project, title: 'abc', description: 'def') }
before do
stub_feature_flags(graphql_board_lists: false)
stub_feature_flags(add_issues_button: true)
project.add_maintainer(user)
sign_in(user)
visit project_board_path(project, board)
wait_for_requests
end
it 'shows weight filter' do
click_button('Add issues')
wait_for_requests
find('.add-issues-modal .filtered-search').click
expect(page.find('.filter-dropdown')).to have_content 'Weight'
end
end
...@@ -1773,11 +1773,6 @@ msgstr "" ...@@ -1773,11 +1773,6 @@ msgstr ""
msgid "Add \"%{value}\"" msgid "Add \"%{value}\""
msgstr "" msgstr ""
msgid "Add %d issue"
msgid_plural "Add %d issues"
msgstr[0] ""
msgstr[1] ""
msgid "Add %{linkStart}assets%{linkEnd} to your Release. GitLab automatically includes read-only assets, like source code and release evidence." msgid "Add %{linkStart}assets%{linkEnd} to your Release. GitLab automatically includes read-only assets, like source code and release evidence."
msgstr "" msgstr ""
...@@ -1928,9 +1923,6 @@ msgstr "" ...@@ -1928,9 +1923,6 @@ msgstr ""
msgid "Add image comment" msgid "Add image comment"
msgstr "" msgstr ""
msgid "Add issues"
msgstr ""
msgid "Add italic text" msgid "Add italic text"
msgstr "" msgstr ""
...@@ -3676,9 +3668,6 @@ msgstr "" ...@@ -3676,9 +3668,6 @@ msgstr ""
msgid "An issue already exists" msgid "An issue already exists"
msgstr "" msgstr ""
msgid "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."
msgstr ""
msgid "An issue title is required" msgid "An issue title is required"
msgstr "" msgstr ""
...@@ -10581,9 +10570,6 @@ msgstr "" ...@@ -10581,9 +10570,6 @@ msgstr ""
msgid "Descriptive label" msgid "Descriptive label"
msgstr "" msgstr ""
msgid "Deselect all"
msgstr ""
msgid "Design Management files and data" msgid "Design Management files and data"
msgstr "" msgstr ""
...@@ -12944,9 +12930,6 @@ msgstr "" ...@@ -12944,9 +12930,6 @@ msgstr ""
msgid "Failed to update issue status" msgid "Failed to update issue status"
msgstr "" msgstr ""
msgid "Failed to update issues, please try again."
msgstr ""
msgid "Failed to update the Canary Ingress." msgid "Failed to update the Canary Ingress."
msgstr "" msgstr ""
...@@ -14452,9 +14435,6 @@ msgstr "" ...@@ -14452,9 +14435,6 @@ msgstr ""
msgid "Go back (while searching for files)" msgid "Go back (while searching for files)"
msgstr "" msgstr ""
msgid "Go back to %{tagStart}Open issues%{tagEnd} and select some issues to add to your board."
msgstr ""
msgid "Go full screen" msgid "Go full screen"
msgstr "" msgstr ""
...@@ -21670,9 +21650,6 @@ msgstr "" ...@@ -21670,9 +21650,6 @@ msgstr ""
msgid "Open in your IDE" msgid "Open in your IDE"
msgstr "" msgstr ""
msgid "Open issues"
msgstr ""
msgid "Open raw" msgid "Open raw"
msgstr "" msgstr ""
...@@ -22912,9 +22889,6 @@ msgstr "" ...@@ -22912,9 +22889,6 @@ msgstr ""
msgid "Please add a comment in the text area above" msgid "Please add a comment in the text area above"
msgstr "" msgstr ""
msgid "Please add a list to your board first"
msgstr ""
msgid "Please check the configuration file for this chart" msgid "Please check the configuration file for this chart"
msgstr "" msgstr ""
...@@ -30602,9 +30576,6 @@ msgstr "" ...@@ -30602,9 +30576,6 @@ msgstr ""
msgid "There are no issues to show" msgid "There are no issues to show"
msgstr "" msgstr ""
msgid "There are no issues to show."
msgstr ""
msgid "There are no issues with the selected labels" msgid "There are no issues with the selected labels"
msgstr "" msgstr ""
...@@ -34997,12 +34968,6 @@ msgstr "" ...@@ -34997,12 +34968,6 @@ msgstr ""
msgid "You have successfully purchased a %{plan} plan subscription for %{seats}. You’ll receive a receipt via email." msgid "You have successfully purchased a %{plan} plan subscription for %{seats}. You’ll receive a receipt via email."
msgstr "" msgstr ""
msgid "You haven't added any issues to your project yet"
msgstr ""
msgid "You haven't selected any issues yet"
msgstr ""
msgid "You left the \"%{membershipable_human_name}\" %{source_type}." msgid "You left the \"%{membershipable_human_name}\" %{source_type}."
msgstr "" msgstr ""
...@@ -36912,9 +36877,6 @@ msgstr "" ...@@ -36912,9 +36877,6 @@ msgstr ""
msgid "to join %{source_name}" msgid "to join %{source_name}"
msgstr "" msgstr ""
msgid "to list"
msgstr ""
msgid "toggle collapse" msgid "toggle collapse"
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Issue Boards add issue modal', :js do
let(:project) { create(:project, :public) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let!(:planning) { create(:label, project: project, name: 'Planning') }
let!(:label) { create(:label, project: project) }
let!(:list1) { create(:list, board: board, label: planning, position: 0) }
let!(:list2) { create(:list, board: board, label: label, position: 1) }
let!(:issue) { create(:issue, project: project, title: 'abc', description: 'def') }
let!(:issue2) { create(:issue, project: project, title: 'hij', description: 'klm') }
before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user)
sign_in(user)
visit project_board_path(project, board)
wait_for_requests
end
it 'resets filtered search state' do
visit project_board_path(project, board, search: 'testing')
wait_for_requests
click_button('Add issues')
page.within('.add-issues-modal') do
expect(find('.form-control').value).to eq('')
expect(page).to have_selector('.clear-search', visible: false)
expect(find('.form-control')[:placeholder]).to eq('Search or filter results...')
end
end
context 'modal interaction' do
before do
stub_feature_flags(add_issues_button: true)
end
it 'opens modal' do
click_button('Add issues')
expect(page).to have_selector('.add-issues-modal')
end
it 'closes modal' do
click_button('Add issues')
page.within('.add-issues-modal') do
find('.close').click
end
expect(page).not_to have_selector('.add-issues-modal')
end
it 'closes modal if cancel button clicked' do
click_button('Add issues')
page.within('.add-issues-modal') do
click_button 'Cancel'
end
expect(page).not_to have_selector('.add-issues-modal')
end
it 'does not show tooltip on add issues button' do
button = page.find('.filter-dropdown-container button', text: 'Add issues')
expect(button[:title]).not_to eq("Please add a list to your board first")
end
end
context 'issues list' do
before do
stub_feature_flags(add_issues_button: true)
click_button('Add issues')
wait_for_requests
end
it 'loads issues' do
page.within('.add-issues-modal') do
page.within('.gl-tabs') do
expect(page).to have_content('2')
end
expect(page).to have_selector('.board-card', count: 2)
end
end
it 'shows selected issues tab and empty state message' do
page.within('.add-issues-modal') do
click_link 'Selected issues'
expect(page).not_to have_selector('.board-card')
expect(page).to have_content("Go back to Open issues and select some issues to add to your board.")
end
end
context 'list dropdown' do
it 'resets after deleting list' do
page.within('.add-issues-modal') do
expect(find('.add-issues-footer')).to have_button(planning.title)
click_button 'Cancel'
end
page.within(find('.board:nth-child(2)')) do
find('button[title="List settings"]').click
end
page.within(find('.js-board-settings-sidebar')) do
accept_confirm { find('[data-testid="remove-list"]').click }
end
click_button('Add issues')
wait_for_requests
page.within('.add-issues-modal') do
expect(find('.add-issues-footer')).not_to have_button(planning.title)
expect(find('.add-issues-footer')).to have_button(label.title)
end
end
end
context 'search' do
it 'returns issues' do
page.within('.add-issues-modal') do
find('.form-control').native.send_keys(issue.title)
find('.form-control').native.send_keys(:enter)
wait_for_requests
expect(page).to have_selector('.board-card', count: 1)
end
end
it 'returns no issues' do
page.within('.add-issues-modal') do
find('.form-control').native.send_keys('testing search')
find('.form-control').native.send_keys(:enter)
wait_for_requests
expect(page).not_to have_selector('.board-card')
expect(page).not_to have_content("You haven't added any issues to your project yet")
end
end
end
context 'selecting issues' do
it 'selects single issue' do
page.within('.add-issues-modal') do
first('.board-card .board-card-number').click
page.within('.gl-tabs') do
expect(page).to have_content('Selected issues 1')
end
end
end
it 'changes button text' do
page.within('.add-issues-modal') do
first('.board-card .board-card-number').click
expect(first('.add-issues-footer .btn')).to have_content('Add 1 issue')
end
end
it 'changes button text with plural' do
page.within('.add-issues-modal') do
all('.board-card .js-board-card-number-container').each do |el|
el.click
end
expect(first('.add-issues-footer .btn')).to have_content('Add 2 issues')
end
end
it 'shows only selected issues on selected tab' do
page.within('.add-issues-modal') do
first('.board-card .board-card-number').click
click_link 'Selected issues'
expect(page).to have_selector('.board-card', count: 1)
end
end
it 'selects all issues' do
page.within('.add-issues-modal') do
click_button 'Select all'
expect(page).to have_selector('.is-active', count: 2)
end
end
it 'deselects all issues' do
page.within('.add-issues-modal') do
click_button 'Select all'
expect(page).to have_selector('.is-active', count: 2)
click_button 'Deselect all'
expect(page).not_to have_selector('.is-active')
end
end
it "selects all that aren't already selected" do
page.within('.add-issues-modal') do
first('.board-card .board-card-number').click
expect(page).to have_selector('.is-active', count: 1)
click_button 'Select all'
expect(page).to have_selector('.is-active', count: 2)
end
end
it 'unselects from selected tab' do
page.within('.add-issues-modal') do
first('.board-card .board-card-number').click
click_link 'Selected issues'
first('.board-card .board-card-number').click
expect(page).not_to have_selector('.is-active')
end
end
end
context 'adding issues' do
it 'adds to board' do
page.within('.add-issues-modal') do
first('.board-card .board-card-number').click
click_button 'Add 1 issue'
end
page.within(find('.board:nth-child(2)')) do
expect(page).to have_selector('.board-card')
end
end
it 'adds to second list' do
page.within('.add-issues-modal') do
first('.board-card .board-card-number').click
click_button planning.title
click_link label.title
click_button 'Add 1 issue'
end
page.within(find('.board:nth-child(3)')) do
expect(page).to have_selector('.board-card')
end
end
end
end
end
...@@ -11,7 +11,7 @@ RSpec.describe 'Issue Boards focus mode', :js do ...@@ -11,7 +11,7 @@ RSpec.describe 'Issue Boards focus mode', :js do
wait_for_requests wait_for_requests
end end
it 'shows focus mode button to guest users' do it 'shows focus mode button to anonymous users' do
expect(page).to have_selector('.board-extra-actions .js-focus-mode-btn') expect(page).to have_selector('.js-focus-mode-btn')
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Issue Boards add issue modal filtering', :js do
let(:project) { create(:project, :public) }
let(:board) { create(:board, project: project) }
let(:planning) { create(:label, project: project, name: 'Planning') }
let!(:list1) { create(:list, board: board, label: planning, position: 0) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:issue1) { create(:issue, project: project) }
before do
stub_feature_flags(graphql_board_lists: false)
stub_feature_flags(add_issues_button: true)
project.add_maintainer(user)
sign_in(user)
end
it 'shows empty state when no results found' do
visit_board
page.within('.add-issues-modal') do
find('.form-control').native.send_keys('testing empty state')
find('.form-control').native.send_keys(:enter)
wait_for_requests
expect(page).to have_content('There are no issues to show.')
end
end
it 'restores filters when closing' do
visit_board
set_filter('milestone')
click_filter_link('Upcoming')
submit_filter
page.within('.add-issues-modal') do
wait_for_requests
expect(page).to have_selector('.board-card', count: 0)
click_button 'Cancel'
end
click_button('Add issues')
page.within('.add-issues-modal') do
wait_for_requests
expect(page).to have_selector('.board-card', count: 1)
end
end
it 'resotres filters after clicking clear button' do
visit_board
set_filter('milestone')
click_filter_link('Upcoming')
submit_filter
page.within('.add-issues-modal') do
wait_for_requests
expect(page).to have_selector('.board-card', count: 0)
find('.clear-search').click
wait_for_requests
expect(page).to have_selector('.board-card', count: 1)
end
end
context 'author' do
let!(:issue) { create(:issue, project: project, author: user2) }
before do
project.add_developer(user2)
visit_board
end
it 'filters by selected user' do
set_filter('author')
click_filter_link(user2.name)
submit_filter
page.within('.add-issues-modal') do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: user2.name)
expect(page).to have_selector('.board-card', count: 1)
end
end
end
context 'assignee' do
let!(:issue) { create(:issue, project: project, assignees: [user2]) }
before do
project.add_developer(user2)
visit_board
end
it 'filters by unassigned' do
set_filter('assignee')
click_filter_link('None')
submit_filter
page.within('.add-issues-modal') do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: 'None')
expect(page).to have_selector('.board-card', count: 1)
end
end
it 'filters by selected user' do
set_filter('assignee')
click_filter_link(user2.name)
submit_filter
page.within('.add-issues-modal') do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: user2.name)
expect(page).to have_selector('.board-card', count: 1)
end
end
end
context 'milestone' do
let(:milestone) { create(:milestone, project: project) }
let!(:issue) { create(:issue, project: project, milestone: milestone) }
before do
visit_board
end
it 'filters by upcoming milestone' do
set_filter('milestone')
click_filter_link('Upcoming')
submit_filter
page.within('.add-issues-modal') do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: 'Upcoming')
expect(page).to have_selector('.board-card', count: 0)
end
end
it 'filters by selected milestone' do
set_filter('milestone')
click_filter_link(milestone.name)
submit_filter
page.within('.add-issues-modal') do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: milestone.name)
expect(page).to have_selector('.board-card', count: 1)
end
end
end
context 'label' do
let(:label) { create(:label, project: project) }
let!(:issue) { create(:labeled_issue, project: project, labels: [label]) }
before do
visit_board
end
it 'filters by no label' do
set_filter('label')
click_filter_link('None')
submit_filter
page.within('.add-issues-modal') do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: 'None')
expect(page).to have_selector('.board-card', count: 1)
end
end
it 'filters by label' do
set_filter('label')
click_filter_link(label.title)
submit_filter
page.within('.add-issues-modal') do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: label.title)
expect(page).to have_selector('.board-card', count: 1)
end
end
end
def visit_board
visit project_board_path(project, board)
wait_for_requests
click_button('Add issues')
end
def set_filter(type, text = '')
find('.add-issues-modal .filtered-search').native.send_keys("#{type}:=#{text}")
end
def submit_filter
find('.add-issues-modal .filtered-search').native.send_keys(:enter)
end
def click_filter_link(link_text)
page.within('.add-issues-modal .filtered-search-box') do
expect(page).to have_button(link_text)
click_button(link_text)
end
end
end
/* global ListIssue */
import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import Store from '~/boards/stores/modal_store';
describe('Modal store', () => {
let issue;
let issue2;
beforeEach(() => {
// Set up default state
Store.store.issues = [];
Store.store.selectedIssues = [];
issue = new ListIssue({
title: 'Testing',
id: 1,
iid: 1,
confidential: false,
labels: [],
assignees: [],
});
issue2 = new ListIssue({
title: 'Testing',
id: 2,
iid: 2,
confidential: false,
labels: [],
assignees: [],
});
Store.store.issues.push(issue);
Store.store.issues.push(issue2);
});
it('returns selected count', () => {
expect(Store.selectedCount()).toBe(0);
});
it('toggles the issue as selected', () => {
Store.toggleIssue(issue);
expect(issue.selected).toBe(true);
expect(Store.selectedCount()).toBe(1);
});
it('toggles the issue as un-selected', () => {
Store.toggleIssue(issue);
Store.toggleIssue(issue);
expect(issue.selected).toBe(false);
expect(Store.selectedCount()).toBe(0);
});
it('toggles all issues as selected', () => {
Store.toggleAll();
expect(issue.selected).toBe(true);
expect(issue2.selected).toBe(true);
expect(Store.selectedCount()).toBe(2);
});
it('toggles all issues as un-selected', () => {
Store.toggleAll();
Store.toggleAll();
expect(issue.selected).toBe(false);
expect(issue2.selected).toBe(false);
expect(Store.selectedCount()).toBe(0);
});
it('toggles all if a single issue is selected', () => {
Store.toggleIssue(issue);
Store.toggleAll();
expect(issue.selected).toBe(true);
expect(issue2.selected).toBe(true);
expect(Store.selectedCount()).toBe(2);
});
it('adds issue to selected array', () => {
issue.selected = true;
Store.addSelectedIssue(issue);
expect(Store.selectedCount()).toBe(1);
});
it('removes issue from selected array', () => {
Store.addSelectedIssue(issue);
Store.removeSelectedIssue(issue);
expect(Store.selectedCount()).toBe(0);
});
it('returns selected issue index if present', () => {
Store.toggleIssue(issue);
expect(Store.selectedIssueIndex(issue)).toBe(0);
});
it('returns -1 if issue is not selected', () => {
expect(Store.selectedIssueIndex(issue)).toBe(-1);
});
it('finds the selected issue', () => {
Store.toggleIssue(issue);
expect(Store.findSelectedIssue(issue)).toBe(issue);
});
it('does not find a selected issue', () => {
expect(Store.findSelectedIssue(issue)).toBe(undefined);
});
it('does not remove from selected issue if tab is not all', () => {
Store.store.activeTab = 'selected';
Store.toggleIssue(issue);
Store.toggleIssue(issue);
expect(Store.store.selectedIssues.length).toBe(1);
expect(Store.selectedCount()).toBe(0);
});
it('gets selected issue array with only selected issues', () => {
Store.toggleIssue(issue);
Store.toggleIssue(issue2);
Store.toggleIssue(issue2);
expect(Store.getSelectedIssues().length).toBe(1);
});
});
...@@ -3,42 +3,6 @@ ...@@ -3,42 +3,6 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe TimeboxesHelper do RSpec.describe TimeboxesHelper do
describe '#milestones_filter_dropdown_path' do
let(:project) { create(:project) }
let(:project2) { create(:project) }
let(:group) { create(:group) }
context 'when @project present' do
it 'returns project milestones JSON URL' do
assign(:project, project)
expect(helper.milestones_filter_dropdown_path).to eq(project_milestones_path(project, :json))
end
end
context 'when @target_project present' do
it 'returns targeted project milestones JSON URL' do
assign(:target_project, project2)
expect(helper.milestones_filter_dropdown_path).to eq(project_milestones_path(project2, :json))
end
end
context 'when @group present' do
it 'returns group milestones JSON URL' do
assign(:group, group)
expect(helper.milestones_filter_dropdown_path).to eq(group_milestones_path(group, :json))
end
end
context 'when neither of @project/@target_project/@group present' do
it 'returns dashboard milestones JSON URL' do
expect(helper.milestones_filter_dropdown_path).to eq(dashboard_milestones_path(:json))
end
end
end
describe "#timebox_date_range" do describe "#timebox_date_range" do
let(:yesterday) { Date.yesterday } let(:yesterday) { Date.yesterday }
let(:tomorrow) { yesterday + 2 } let(:tomorrow) { yesterday + 2 }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment