Commit ae46a11c authored by Simon Knox's avatar Simon Knox Committed by David O'Regan

Improve board add list interactions

Radio buttons for list type select as there are only
four options max. Regular dropdown for items. Some
other polish issues and little bugfixes that didn't
make it into earlier MRs

Dropdown item text wrapping requires gitlab-ui update
parent 211cccbe
<script> <script>
import { import { GlFormRadio, GlFormRadioGroup, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
GlFormRadio,
GlFormRadioGroup,
GlLabel,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
import { ListType } from '~/boards/constants'; import { ListType } from '~/boards/constants';
...@@ -17,7 +12,6 @@ export default { ...@@ -17,7 +12,6 @@ export default {
BoardAddNewColumnForm, BoardAddNewColumnForm,
GlFormRadio, GlFormRadio,
GlFormRadioGroup, GlFormRadioGroup,
GlLabel,
}, },
directives: { directives: {
GlTooltip, GlTooltip,
...@@ -99,25 +93,25 @@ export default { ...@@ -99,25 +93,25 @@ export default {
<template> <template>
<board-add-new-column-form <board-add-new-column-form
:loading="labelsLoading" :loading="labelsLoading"
:form-description="__('A label list displays issues with the selected label.')" :none-selected="__('Select a label')"
:search-label="__('Select label')"
:search-placeholder="__('Search labels')" :search-placeholder="__('Search labels')"
:selected-id="selectedId" :selected-id="selectedId"
@filter-items="filterItems" @filter-items="filterItems"
@add-list="addList" @add-list="addList"
> >
<template slot="selected"> <template #selected>
<gl-label <template v-if="selectedLabel">
v-if="selectedLabel" <span
v-gl-tooltip class="dropdown-label-box gl-top-0 gl-flex-shrink-0"
:title="selectedLabel.title" :style="{
:description="selectedLabel.description" backgroundColor: selectedLabel.color,
:background-color="selectedLabel.color" }"
:scoped="showScopedLabels(selectedLabel)" ></span>
/> <div class="gl-text-truncate">{{ selectedLabel.title }}</div>
</template>
</template> </template>
<template slot="items"> <template #items>
<gl-form-radio-group <gl-form-radio-group
v-if="labels.length > 0" v-if="labels.length > 0"
v-model="selectedId" v-model="selectedId"
...@@ -126,11 +120,11 @@ export default { ...@@ -126,11 +120,11 @@ export default {
<label <label
v-for="label in labels" v-for="label in labels"
:key="label.id" :key="label.id"
class="gl-display-flex gl-flex-align-items-center gl-mb-5 gl-font-weight-normal" class="gl-display-flex gl-mb-5 gl-font-weight-normal gl-overflow-break-word"
> >
<gl-form-radio :value="label.id" class="gl-mb-0" /> <gl-form-radio :value="label.id" />
<span <span
class="dropdown-label-box gl-top-0" class="dropdown-label-box gl-top-0 gl-flex-shrink-0"
:style="{ :style="{
backgroundColor: label.color, backgroundColor: label.color,
}" }"
......
<script> <script>
import { GlButton, GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; import {
GlButton,
GlDropdown,
GlFormGroup,
GlIcon,
GlSearchBoxByType,
GlSkeletonLoader,
} from '@gitlab/ui';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -8,13 +15,16 @@ export default { ...@@ -8,13 +15,16 @@ export default {
add: __('Add to board'), add: __('Add to board'),
cancel: __('Cancel'), cancel: __('Cancel'),
newList: __('New list'), newList: __('New list'),
noneSelected: __('None'),
noResults: __('No matching results'), noResults: __('No matching results'),
scope: __('Scope'),
scopeDescription: __('Issues must match this scope to appear in this list.'),
selected: __('Selected'), selected: __('Selected'),
}, },
components: { components: {
GlButton, GlButton,
GlDropdown,
GlFormGroup, GlFormGroup,
GlIcon,
GlSearchBoxByType, GlSearchBoxByType,
GlSkeletonLoader, GlSkeletonLoader,
}, },
...@@ -23,11 +33,12 @@ export default { ...@@ -23,11 +33,12 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
formDescription: { searchLabel: {
type: String, type: String,
required: true, required: false,
default: null,
}, },
searchLabel: { noneSelected: {
type: String, type: String,
required: true, required: true,
}, },
...@@ -46,8 +57,23 @@ export default { ...@@ -46,8 +57,23 @@ export default {
searchValue: '', searchValue: '',
}; };
}, },
watch: {
selectedId(val) {
if (val) {
this.$refs.dropdown.hide(true);
}
},
},
methods: { methods: {
...mapActions(['setAddColumnFormVisibility']), ...mapActions(['setAddColumnFormVisibility']),
setFocus() {
this.$refs.searchBox.focusInput();
},
onHide() {
this.searchValue = '';
this.$emit('filter-items', '');
this.$emit('hide');
},
}, },
}; };
</script> </script>
...@@ -62,51 +88,64 @@ export default { ...@@ -62,51 +88,64 @@ export default {
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-white" class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-white"
> >
<h3 <h3
class="gl-font-base gl-px-5 gl-py-5 gl-m-0 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100" class="gl-font-size-h2 gl-px-5 gl-py-4 gl-m-0 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
data-testid="board-add-column-form-title" data-testid="board-add-column-form-title"
> >
{{ $options.i18n.newList }} {{ $options.i18n.newList }}
</h3> </h3>
<div class="gl-display-flex gl-flex-direction-column gl-h-full gl-overflow-hidden"> <div
<slot name="select-list-type"> class="gl-display-flex gl-flex-direction-column gl-h-full gl-overflow-y-auto gl-align-items-flex-start"
<div class="gl-mb-5"></div> >
</slot> <div class="gl-px-5">
<h3 class="gl-font-lg gl-mt-5 gl-mb-2">
{{ $options.i18n.scope }}
</h3>
<p class="gl-mb-3">{{ $options.i18n.scopeDescription }}</p>
</div>
<p class="gl-px-5">{{ formDescription }}</p> <slot name="select-list-type"></slot>
<div class="gl-px-5 gl-pb-4"> <gl-form-group class="gl-px-5 lg-mb-3 gl-max-w-full" :label="searchLabel">
<label class="gl-mb-2">{{ $options.i18n.selected }}</label> <gl-dropdown
<slot name="selected"> ref="dropdown"
<div class="gl-text-gray-500">{{ $options.i18n.noneSelected }}</div> class="gl-mb-3 gl-max-w-full"
</slot> toggle-class="gl-max-w-full gl-display-flex gl-align-items-center gl-text-trunate"
</div> boundary="viewport"
@shown="setFocus"
@hide="onHide"
>
<template #button-content>
<slot name="selected">
<div>{{ noneSelected }}</div>
</slot>
<gl-icon class="dropdown-chevron gl-flex-shrink-0" name="chevron-down" />
</template>
<gl-form-group <template #header>
class="gl-mx-5 gl-mb-3" <gl-search-box-by-type
:label="searchLabel" ref="searchBox"
label-for="board-available-column-entities" v-model="searchValue"
> debounce="250"
<gl-search-box-by-type class="gl-mt-0!"
id="board-available-column-entities" :placeholder="searchPlaceholder"
v-model="searchValue" @input="$emit('filter-items', $event)"
debounce="250" />
:placeholder="searchPlaceholder" </template>
@input="$emit('filter-items', $event)"
/>
</gl-form-group>
<div v-if="loading" class="gl-px-5"> <div v-if="loading" class="gl-px-5">
<gl-skeleton-loader :width="500" :height="172"> <gl-skeleton-loader :width="400" :height="172">
<rect width="480" height="20" x="10" y="15" rx="4" /> <rect width="380" height="20" x="10" y="15" rx="4" />
<rect width="380" height="20" x="10" y="50" rx="4" /> <rect width="280" height="20" x="10" y="50" rx="4" />
<rect width="430" height="20" x="10" y="85" rx="4" /> <rect width="330" height="20" x="10" y="85" rx="4" />
</gl-skeleton-loader> </gl-skeleton-loader>
</div> </div>
<slot v-else name="items"> <slot v-else name="items">
<p class="gl-mx-5">{{ $options.i18n.noResults }}</p> <p class="gl-mx-5">{{ $options.i18n.noResults }}</p>
</slot> </slot>
</gl-dropdown>
</gl-form-group>
</div> </div>
<div <div
class="gl-display-flex gl-p-3 gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base" class="gl-display-flex gl-p-3 gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
......
...@@ -13,9 +13,9 @@ export default { ...@@ -13,9 +13,9 @@ export default {
</script> </script>
<template> <template>
<span class="gl-ml-3 gl-display-flex gl-align-items-center" data-testid="boards-create-list"> <div class="gl-ml-3 gl-display-flex gl-align-items-center" data-testid="boards-create-list">
<gl-button variant="confirm" @click="setAddColumnFormVisibility(true)" <gl-button variant="confirm" @click="setAddColumnFormVisibility(true)"
>{{ __('Create list') }} >{{ __('Create list') }}
</gl-button> </gl-button>
</span> </div>
</template> </template>
...@@ -134,9 +134,10 @@ export default { ...@@ -134,9 +134,10 @@ export default {
e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list'); e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list');
const toBoardType = containerEl.dataset.boardType; const toBoardType = containerEl.dataset.boardType;
const cloneActions = { const cloneActions = {
label: ['milestone', 'assignee'], label: ['milestone', 'assignee', 'iteration'],
assignee: ['milestone', 'label'], assignee: ['milestone', 'label', 'iteration'],
milestone: ['label', 'assignee'], milestone: ['label', 'assignee', 'iteration'],
iteration: ['label', 'assignee', 'milestone'],
}; };
if (toBoardType) { if (toBoardType) {
......
<script> <script>
import { import {
GlAvatar,
GlAvatarLabeled, GlAvatarLabeled,
GlIcon,
GlFormGroup, GlFormGroup,
GlFormRadio, GlFormRadio,
GlFormRadioGroup, GlFormRadioGroup,
GlFormSelect,
GlLabel,
GlTooltipDirective as GlTooltip, GlTooltipDirective as GlTooltip,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
...@@ -21,48 +21,44 @@ export const listTypeInfo = { ...@@ -21,48 +21,44 @@ export const listTypeInfo = {
listPropertyName: 'labels', listPropertyName: 'labels',
loadingPropertyName: 'labelsLoading', loadingPropertyName: 'labelsLoading',
fetchMethodName: 'fetchLabels', fetchMethodName: 'fetchLabels',
formDescription: __('A label list displays issues with the selected label.'), noneSelected: __('Select a label'),
searchLabel: __('Select label'),
searchPlaceholder: __('Search labels'), searchPlaceholder: __('Search labels'),
}, },
[ListType.assignee]: { [ListType.assignee]: {
listPropertyName: 'assignees', listPropertyName: 'assignees',
loadingPropertyName: 'assigneesLoading', loadingPropertyName: 'assigneesLoading',
fetchMethodName: 'fetchAssignees', fetchMethodName: 'fetchAssignees',
formDescription: __('An assignee list displays issues assigned to the selected user'), noneSelected: __('Select an assignee'),
searchLabel: __('Select assignee'),
searchPlaceholder: __('Search assignees'), searchPlaceholder: __('Search assignees'),
}, },
[ListType.milestone]: { [ListType.milestone]: {
listPropertyName: 'milestones', listPropertyName: 'milestones',
loadingPropertyName: 'milestonesLoading', loadingPropertyName: 'milestonesLoading',
fetchMethodName: 'fetchMilestones', fetchMethodName: 'fetchMilestones',
formDescription: __('A milestone list displays issues in the selected milestone.'), noneSelected: __('Select a milestone'),
searchLabel: __('Select milestone'),
searchPlaceholder: __('Search milestones'), searchPlaceholder: __('Search milestones'),
}, },
[ListType.iteration]: { [ListType.iteration]: {
listPropertyName: 'iterations', listPropertyName: 'iterations',
loadingPropertyName: 'iterationsLoading', loadingPropertyName: 'iterationsLoading',
fetchMethodName: 'fetchIterations', fetchMethodName: 'fetchIterations',
formDescription: __('An iteration list displays issues in the selected iteration.'), noneSelected: __('Select an iteration'),
searchLabel: __('Select iteration'),
searchPlaceholder: __('Search iterations'), searchPlaceholder: __('Search iterations'),
}, },
}; };
export default { export default {
i18n: { i18n: {
listType: __('List type'), value: __('Value'),
}, },
components: { components: {
BoardAddNewColumnForm, BoardAddNewColumnForm,
GlAvatar,
GlAvatarLabeled, GlAvatarLabeled,
GlIcon,
GlFormGroup, GlFormGroup,
GlFormRadio, GlFormRadio,
GlFormRadioGroup, GlFormRadioGroup,
GlFormSelect,
GlLabel,
}, },
directives: { directives: {
GlTooltip, GlTooltip,
...@@ -100,6 +96,10 @@ export default { ...@@ -100,6 +96,10 @@ export default {
return this[this.info.listPropertyName] || []; return this[this.info.listPropertyName] || [];
}, },
hasItems() {
return this.items.length > 0;
},
labelTypeSelected() { labelTypeSelected() {
return this.columnType === ListType.label; return this.columnType === ListType.label;
}, },
...@@ -131,14 +131,20 @@ export default { ...@@ -131,14 +131,20 @@ export default {
}, },
columnForSelected() { columnForSelected() {
if (!this.columnType) { if (!this.columnType || !this.selectedId) {
return false; return false;
} }
const key = `${this.columnType}Id`; if (this.shouldUseGraphQL || this.isEpicBoard) {
return this.getListByTypeId({ const key = `${this.columnType}Id`;
[key]: this.selectedId, return this.getListByTypeId({
}); [key]: this.selectedId,
});
}
return boardsStore.state.lists.find(
(list) => list[this.columnType]?.id === getIdFromGraphQLId(this.selectedId),
);
}, },
loading() { loading() {
...@@ -163,6 +169,10 @@ export default { ...@@ -163,6 +169,10 @@ export default {
return types; return types;
}, },
searchLabel() {
return this.showListTypeSelector ? this.$options.i18n.value : null;
},
showListTypeSelector() { showListTypeSelector() {
return !this.isEpicBoard && this.columnTypes.length > 1; return !this.isEpicBoard && this.columnTypes.length > 1;
}, },
...@@ -254,6 +264,10 @@ export default { ...@@ -254,6 +264,10 @@ export default {
this.selectedId = null; this.selectedId = null;
this.filterItems(); this.filterItems();
}, },
hideDropdown() {
this.$root.$emit('bv::dropdown::hide');
},
}, },
}; };
</script> </script>
...@@ -261,68 +275,82 @@ export default { ...@@ -261,68 +275,82 @@ export default {
<template> <template>
<board-add-new-column-form <board-add-new-column-form
:loading="loading" :loading="loading"
:form-description="info.formDescription" :none-selected="info.noneSelected"
:search-label="info.searchLabel" :search-label="searchLabel"
:search-placeholder="info.searchPlaceholder" :search-placeholder="info.searchPlaceholder"
:selected-id="selectedId" :selected-id="selectedId"
@filter-items="filterItems" @filter-items="filterItems"
@add-list="addList" @add-list="addList"
> >
<template slot="select-list-type"> <template #select-list-type>
<gl-form-group <gl-form-group
v-if="showListTypeSelector" v-if="showListTypeSelector"
:label="$options.i18n.listType" :description="$options.i18n.scopeDescription"
class="gl-px-5 gl-py-0 gl-mt-5" class="gl-px-5 gl-py-0 gl-mb-3"
label-for="list-type" label-for="list-type"
> >
<gl-form-select <gl-form-radio-group v-model="columnType">
id="list-type" <gl-form-radio
v-model="columnType" v-for="{ text, value } in columnTypes"
:options="columnTypes" :key="value"
@change="setColumnType" :value="value"
/> class="gl-mb-0 gl-align-self-center"
@change="setColumnType"
>
{{ text }}
</gl-form-radio>
</gl-form-radio-group>
</gl-form-group> </gl-form-group>
</template> </template>
<template slot="selected"> <template #selected>
<div v-if="hasLabelSelection"> <template v-if="hasLabelSelection">
<gl-label <span
v-gl-tooltip class="dropdown-label-box gl-top-0 gl-flex-shrink-0"
:title="selectedItem.title" :style="{
:description="selectedItem.description" backgroundColor: selectedItem.color,
:background-color="selectedItem.color" }"
:scoped="showScopedLabels(selectedItem)" ></span>
/> <div class="gl-text-truncate">{{ selectedItem.title }}</div>
</div> </template>
<div v-else-if="hasAssigneeSelection"> <template v-else-if="hasMilestoneSelection">
<gl-avatar-labeled <gl-icon class="gl-flex-shrink-0" name="clock" />
:size="32" <span class="gl-text-truncate">{{ selectedItem.title }}</span>
:label="selectedItem.name" </template>
:sub-label="selectedItem.username"
:src="selectedItem.avatarUrl" <template v-else-if="hasIterationSelection">
/> <gl-icon class="gl-flex-shrink-0" name="iteration" />
</div> <span class="gl-text-truncate">{{ selectedItem.title }}</span>
<div v-else-if="hasMilestoneSelection || hasIterationSelection" class="gl-text-truncate"> </template>
{{ selectedItem.title }}
</div> <template v-else-if="hasAssigneeSelection">
<gl-avatar class="gl-mr-2 gl-flex-shrink-0" :size="16" :src="selectedItem.avatarUrl" />
<div class="gl-text-truncate">
<b class="gl-mr-2">{{ selectedItem.name }}</b>
<span class="gl-text-gray-700">@{{ selectedItem.username }}</span>
</div>
</template>
</template> </template>
<template slot="items"> <template v-if="hasItems" #items>
<gl-form-radio-group <gl-form-radio-group
v-if="items.length > 0"
v-model="selectedId" v-model="selectedId"
class="gl-overflow-y-auto gl-px-5 gl-pt-3" class="gl-overflow-y-auto gl-px-5"
@change="hideDropdown"
> >
<label <label
v-for="item in items" v-for="item in items"
:key="item.id" :key="item.id"
class="gl-display-flex gl-flex-align-items-center gl-mb-5 gl-font-weight-normal" class="gl-display-flex gl-font-weight-normal gl-overflow-break-word gl-py-3 gl-mb-0"
> >
<gl-form-radio :value="item.id" class="gl-mb-0 gl-align-self-center" /> <gl-form-radio
:value="item.id"
:class="assigneeTypeSelected ? 'gl-align-self-center' : ''"
/>
<span <span
v-if="labelTypeSelected" v-if="labelTypeSelected"
class="dropdown-label-box gl-top-0" class="dropdown-label-box gl-top-0 gl-flex-shrink-0"
:style="{ :style="{
backgroundColor: item.color, backgroundColor: item.color,
}" }"
...@@ -330,14 +358,17 @@ export default { ...@@ -330,14 +358,17 @@ export default {
<gl-avatar-labeled <gl-avatar-labeled
v-if="assigneeTypeSelected" v-if="assigneeTypeSelected"
class="gl-display-flex gl-align-items-center"
:size="32" :size="32"
:label="item.name" :label="item.name"
:sub-label="item.username" :sub-label="`@${item.username}`"
:src="item.avatarUrl" :src="item.avatarUrl"
/> />
<span v-else>{{ item.title }}</span> <span v-else>{{ item.title }}</span>
</label> </label>
</gl-form-radio-group> </gl-form-radio-group>
<div class="dropdown-content-faded-mask gl-fixed gl-bottom-0 gl-w-full"></div>
</template> </template>
</board-add-new-column-form> </board-add-new-column-form>
</template> </template>
...@@ -9,7 +9,7 @@ export default { ...@@ -9,7 +9,7 @@ export default {
return Boolean(gon?.licensed_features?.swimlanes && state.isShowingEpicsSwimlanes); return Boolean(gon?.licensed_features?.swimlanes && state.isShowingEpicsSwimlanes);
}, },
getListByTypeId: (state) => ({ assigneeId, labelId, milestoneId }) => { getListByTypeId: (state) => ({ assigneeId, labelId, milestoneId, iterationId }) => {
if (assigneeId) { if (assigneeId) {
return find( return find(
state.boardLists, state.boardLists,
...@@ -31,6 +31,13 @@ export default { ...@@ -31,6 +31,13 @@ export default {
); );
} }
if (iterationId) {
return find(
state.boardLists,
(l) => l.listType === ListType.iteration && l.iteration?.id === iterationId,
);
}
return null; return null;
}, },
......
...@@ -74,6 +74,9 @@ export default () => { ...@@ -74,6 +74,9 @@ export default () => {
? parseInt($boardApp.dataset.boardWeight, 10) ? parseInt($boardApp.dataset.boardWeight, 10)
: null, : null,
scopedLabelsAvailable: parseBoolean($boardApp.dataset.scopedLabels), scopedLabelsAvailable: parseBoolean($boardApp.dataset.scopedLabels),
milestoneListsAvailable: false,
assigneeListsAvailable: false,
iterationListsAvailable: false,
}, },
store, store,
apolloProvider, apolloProvider,
......
...@@ -37,6 +37,14 @@ ...@@ -37,6 +37,14 @@
} }
} }
.board-add-new-list .gl-new-dropdown-inner {
max-height: 12rem !important;
.gl-form-radio {
min-height: 1em;
}
}
.tab-pane-labels { .tab-pane-labels {
.dropdown-page-one .dropdown-content { .dropdown-page-one .dropdown-content {
height: 140px; height: 140px;
......
...@@ -98,7 +98,7 @@ RSpec.describe 'User adds milestone lists', :js do ...@@ -98,7 +98,7 @@ RSpec.describe 'User adds milestone lists', :js do
wait_for_all_requests wait_for_all_requests
end end
it 'does not show other list types' do it 'does not show other list types', :aggregate_failures do
click_button 'Create list' click_button 'Create list'
wait_for_all_requests wait_for_all_requests
...@@ -106,7 +106,6 @@ RSpec.describe 'User adds milestone lists', :js do ...@@ -106,7 +106,6 @@ RSpec.describe 'User adds milestone lists', :js do
expect(page).not_to have_text('Iteration') expect(page).not_to have_text('Iteration')
expect(page).not_to have_text('Assignee') expect(page).not_to have_text('Assignee')
expect(page).not_to have_text('Milestone') expect(page).not_to have_text('Milestone')
expect(page).not_to have_text('List type')
end end
end end
end end
...@@ -115,13 +114,16 @@ RSpec.describe 'User adds milestone lists', :js do ...@@ -115,13 +114,16 @@ RSpec.describe 'User adds milestone lists', :js do
click_button 'Create list' click_button 'Create list'
wait_for_all_requests wait_for_all_requests
select(list_type, from: 'List type') page.choose(list_type)
page.within('.board-add-new-list') do find_button("Select a").click
page.within('.dropdown-menu') do
find('label', text: title).click find('label', text: title).click
click_button 'Add'
end end
click_button 'Add to board'
wait_for_all_requests wait_for_all_requests
end end
end end
...@@ -73,11 +73,14 @@ RSpec.describe 'epic boards', :js do ...@@ -73,11 +73,14 @@ RSpec.describe 'epic boards', :js do
click_button 'Create list' click_button 'Create list'
wait_for_all_requests wait_for_all_requests
page.within("[data-testid='board-add-new-column']") do click_button 'Select a label'
page.within(".dropdown-menu") do
find('label', text: label2.title).click find('label', text: label2.title).click
click_button 'Add'
end end
click_button 'Add to board'
wait_for_all_requests wait_for_all_requests
expect(page).to have_selector('.board', text: label2.title) expect(page).to have_selector('.board', text: label2.title)
......
import { GlAvatarLabeled, GlSearchBoxByType, GlFormRadio, GlFormSelect } from '@gitlab/ui'; import { GlAvatarLabeled, GlDropdown, GlFormRadio, GlFormRadioGroup } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
...@@ -40,6 +40,8 @@ describe('BoardAddNewColumn', () => { ...@@ -40,6 +40,8 @@ describe('BoardAddNewColumn', () => {
shallowMount(BoardAddNewColumn, { shallowMount(BoardAddNewColumn, {
stubs: { stubs: {
BoardAddNewColumnForm, BoardAddNewColumnForm,
GlFormRadio,
GlFormRadioGroup,
}, },
data() { data() {
return { return {
...@@ -78,27 +80,24 @@ describe('BoardAddNewColumn', () => { ...@@ -78,27 +80,24 @@ describe('BoardAddNewColumn', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
const findForm = () => wrapper.findComponent(BoardAddNewColumnForm); const findForm = () => wrapper.findComponent(BoardAddNewColumnForm);
const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text();
const findSearchInput = () => wrapper.find(GlSearchBoxByType);
const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn'); const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn');
const submitButton = () => wrapper.findByTestId('addNewColumnButton'); const submitButton = () => wrapper.findByTestId('addNewColumnButton');
const listTypeSelect = () => wrapper.findComponent(GlFormSelect); const listTypeSelect = (type) => {
const radio = wrapper
.findAllComponents(GlFormRadio)
.filter((r) => r.attributes('value') === type)
.at(0);
radio.element.value = type;
radio.vm.$emit('change', type);
};
beforeEach(() => { beforeEach(() => {
shouldUseGraphQL = true; shouldUseGraphQL = true;
}); });
it('shows form title & search input', () => {
mountComponent();
expect(formTitle()).toEqual(BoardAddNewColumnForm.i18n.newList);
expect(findSearchInput().exists()).toBe(true);
});
it('clicking cancel hides the form', () => { it('clicking cancel hides the form', () => {
const setAddColumnFormVisibility = jest.fn(); const setAddColumnFormVisibility = jest.fn();
mountComponent({ mountComponent({
...@@ -174,15 +173,15 @@ describe('BoardAddNewColumn', () => { ...@@ -174,15 +173,15 @@ describe('BoardAddNewColumn', () => {
}, },
}); });
listTypeSelect().vm.$emit('change', ListType.assignee); listTypeSelect(ListType.assignee);
await nextTick(); await nextTick();
}); });
it('sets assignee placeholder text in form', async () => { it('sets assignee placeholder text in form', async () => {
expect(findForm().props()).toMatchObject({ expect(findForm().props()).toMatchObject({
formDescription: listTypeInfo.assignee.formDescription, noneSelected: listTypeInfo.assignee.noneSelected,
searchLabel: listTypeInfo.assignee.searchLabel, searchLabel: BoardAddNewColumn.i18n.value,
searchPlaceholder: listTypeInfo.assignee.searchPlaceholder, searchPlaceholder: listTypeInfo.assignee.searchPlaceholder,
}); });
}); });
...@@ -195,7 +194,7 @@ describe('BoardAddNewColumn', () => { ...@@ -195,7 +194,7 @@ describe('BoardAddNewColumn', () => {
expect(userList).toHaveLength(mockAssignees.length); expect(userList).toHaveLength(mockAssignees.length);
expect(userList.at(0).props()).toMatchObject({ expect(userList.at(0).props()).toMatchObject({
label: firstUser.name, label: firstUser.name,
subLabel: firstUser.username, subLabel: `@${firstUser.username}`,
}); });
}); });
}); });
...@@ -209,21 +208,20 @@ describe('BoardAddNewColumn', () => { ...@@ -209,21 +208,20 @@ describe('BoardAddNewColumn', () => {
}, },
}); });
listTypeSelect().vm.$emit('change', ListType.iteration); listTypeSelect(ListType.iteration);
await nextTick(); await nextTick();
}); });
it('sets iteration placeholder text in form', async () => { it('sets iteration placeholder text in form', () => {
expect(findForm().props()).toMatchObject({ expect(findForm().props()).toMatchObject({
formDescription: listTypeInfo.iteration.formDescription, searchLabel: BoardAddNewColumn.i18n.value,
searchLabel: listTypeInfo.iteration.searchLabel,
searchPlaceholder: listTypeInfo.iteration.searchPlaceholder, searchPlaceholder: listTypeInfo.iteration.searchPlaceholder,
}); });
}); });
it('shows list of iterations', () => { it('shows list of iterations', () => {
const itemList = wrapper.findAllComponents(GlFormRadio); const itemList = wrapper.findComponent(GlDropdown).findAllComponents(GlFormRadio);
expect(itemList).toHaveLength(mockIterations.length); expect(itemList).toHaveLength(mockIterations.length);
expect(itemList.at(0).attributes('value')).toBe(mockIterations[0].id); expect(itemList.at(0).attributes('value')).toBe(mockIterations[0].id);
......
...@@ -1305,7 +1305,7 @@ describe('fetchIterations', () => { ...@@ -1305,7 +1305,7 @@ describe('fetchIterations', () => {
}); });
} }
it('sets iterationsLoading to true', async () => { it('sets iterationsLoading to true', () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse); jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
const store = createStore(); const store = createStore();
......
...@@ -1365,9 +1365,6 @@ msgstr "" ...@@ -1365,9 +1365,6 @@ msgstr ""
msgid "A job artifact is an archive of files and directories saved by a job when it finishes." msgid "A job artifact is an archive of files and directories saved by a job when it finishes."
msgstr "" msgstr ""
msgid "A label list displays issues with the selected label."
msgstr ""
msgid "A limit of %{ci_project_subscriptions_limit} subscriptions to or from a project applies." msgid "A limit of %{ci_project_subscriptions_limit} subscriptions to or from a project applies."
msgstr "" msgstr ""
...@@ -1386,9 +1383,6 @@ msgstr "" ...@@ -1386,9 +1383,6 @@ msgstr ""
msgid "A merge request hasn't yet been merged" msgid "A merge request hasn't yet been merged"
msgstr "" msgstr ""
msgid "A milestone list displays issues in the selected milestone."
msgstr ""
msgid "A new Auto DevOps pipeline has been created, go to %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details" msgid "A new Auto DevOps pipeline has been created, go to %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details"
msgstr "" msgstr ""
...@@ -3262,9 +3256,6 @@ msgstr "" ...@@ -3262,9 +3256,6 @@ msgstr ""
msgid "An application called %{link_to_client} is requesting access to your GitLab account." msgid "An application called %{link_to_client} is requesting access to your GitLab account."
msgstr "" msgstr ""
msgid "An assignee list displays issues assigned to the selected user"
msgstr ""
msgid "An email notification was recently sent from the admin panel. Please wait %{wait_time_in_words} before attempting to send another message." msgid "An email notification was recently sent from the admin panel. Please wait %{wait_time_in_words} before attempting to send another message."
msgstr "" msgstr ""
...@@ -3652,9 +3643,6 @@ msgstr "" ...@@ -3652,9 +3643,6 @@ msgstr ""
msgid "An issue title is required" msgid "An issue title is required"
msgstr "" msgstr ""
msgid "An iteration list displays issues in the selected iteration."
msgstr ""
msgid "An unauthenticated user" msgid "An unauthenticated user"
msgstr "" msgstr ""
...@@ -17206,6 +17194,9 @@ msgstr "" ...@@ -17206,6 +17194,9 @@ msgstr ""
msgid "Issues closed" msgid "Issues closed"
msgstr "" msgstr ""
msgid "Issues must match this scope to appear in this list."
msgstr ""
msgid "Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities" msgid "Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities"
msgstr "" msgstr ""
...@@ -18422,9 +18413,6 @@ msgstr "" ...@@ -18422,9 +18413,6 @@ msgstr ""
msgid "List the merge requests that must be merged before this one." msgid "List the merge requests that must be merged before this one."
msgstr "" msgstr ""
msgid "List type"
msgstr ""
msgid "List view" msgid "List view"
msgstr "" msgstr ""
...@@ -27304,6 +27292,9 @@ msgstr "" ...@@ -27304,6 +27292,9 @@ msgstr ""
msgid "Select a label" msgid "Select a label"
msgstr "" msgstr ""
msgid "Select a milestone"
msgstr ""
msgid "Select a new namespace" msgid "Select a new namespace"
msgstr "" msgstr ""
...@@ -27331,9 +27322,15 @@ msgstr "" ...@@ -27331,9 +27322,15 @@ msgstr ""
msgid "Select all" msgid "Select all"
msgstr "" msgstr ""
msgid "Select an assignee"
msgstr ""
msgid "Select an existing Kubernetes cluster or create a new one." msgid "Select an existing Kubernetes cluster or create a new one."
msgstr "" msgstr ""
msgid "Select an iteration"
msgstr ""
msgid "Select assignee" msgid "Select assignee"
msgstr "" msgstr ""
......
...@@ -71,10 +71,13 @@ RSpec.describe 'User adds lists', :js do ...@@ -71,10 +71,13 @@ RSpec.describe 'User adds lists', :js do
def select_label(board_new_list_enabled, label) def select_label(board_new_list_enabled, label)
if board_new_list_enabled if board_new_list_enabled
page.within('.board-add-new-list') do click_button 'Select a label'
find('label', text: label.title).click
click_button 'Add' find('label', text: label.title).click
end
click_button 'Add to board'
wait_for_all_requests
else else
page.within('.dropdown-menu-issues-board-new') do page.within('.dropdown-menu-issues-board-new') do
click_link label.title click_link label.title
......
import { GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; import { GlDropdown, GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue'; import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
...@@ -25,7 +25,7 @@ describe('Board card layout', () => { ...@@ -25,7 +25,7 @@ describe('Board card layout', () => {
const mountComponent = ({ const mountComponent = ({
loading = false, loading = false,
formDescription = '', noneSelected = '',
searchLabel = '', searchLabel = '',
searchPlaceholder = '', searchPlaceholder = '',
selectedId, selectedId,
...@@ -34,12 +34,9 @@ describe('Board card layout', () => { ...@@ -34,12 +34,9 @@ describe('Board card layout', () => {
} = {}) => { } = {}) => {
wrapper = extendedWrapper( wrapper = extendedWrapper(
shallowMount(BoardAddNewColumnForm, { shallowMount(BoardAddNewColumnForm, {
stubs: {
GlFormGroup: true,
},
propsData: { propsData: {
loading, loading,
formDescription, noneSelected,
searchLabel, searchLabel,
searchPlaceholder, searchPlaceholder,
selectedId, selectedId,
...@@ -51,13 +48,15 @@ describe('Board card layout', () => { ...@@ -51,13 +48,15 @@ describe('Board card layout', () => {
...actions, ...actions,
}, },
}), }),
stubs: {
GlDropdown,
},
}), }),
); );
}; };
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text(); const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text();
...@@ -65,10 +64,13 @@ describe('Board card layout', () => { ...@@ -65,10 +64,13 @@ describe('Board card layout', () => {
const findSearchLabel = () => wrapper.find(GlFormGroup); const findSearchLabel = () => wrapper.find(GlFormGroup);
const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn'); const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn');
const submitButton = () => wrapper.findByTestId('addNewColumnButton'); const submitButton = () => wrapper.findByTestId('addNewColumnButton');
const findDropdown = () => wrapper.findComponent(GlDropdown);
it('shows form title & search input', () => { it('shows form title & search input', () => {
mountComponent(); mountComponent();
findDropdown().vm.$emit('show');
expect(formTitle()).toEqual(BoardAddNewColumnForm.i18n.newList); expect(formTitle()).toEqual(BoardAddNewColumnForm.i18n.newList);
expect(findSearchInput().exists()).toBe(true); expect(findSearchInput().exists()).toBe(true);
}); });
...@@ -86,16 +88,6 @@ describe('Board card layout', () => { ...@@ -86,16 +88,6 @@ describe('Board card layout', () => {
expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false); expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false);
}); });
it('sets placeholder and description from props', () => {
const props = {
formDescription: 'Some description of a list',
};
mountComponent(props);
expect(wrapper.html()).toHaveText(props.formDescription);
});
describe('items', () => { describe('items', () => {
const mountWithItems = (loading) => const mountWithItems = (loading) =>
mountComponent({ mountComponent({
...@@ -151,13 +143,11 @@ describe('Board card layout', () => { ...@@ -151,13 +143,11 @@ describe('Board card layout', () => {
expect(submitButton().props('disabled')).toBe(true); expect(submitButton().props('disabled')).toBe(true);
}); });
it('emits add-list event on click', async () => { it('emits add-list event on click', () => {
mountComponent({ mountComponent({
selectedId: mockLabelList.label.id, selectedId: mockLabelList.label.id,
}); });
await nextTick();
submitButton().vm.$emit('click'); submitButton().vm.$emit('click');
expect(wrapper.emitted('add-list')).toEqual([[]]); expect(wrapper.emitted('add-list')).toEqual([[]]);
......
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