Commit 454f3b81 authored by Florie Guibert's avatar Florie Guibert

Lock a newly created item card in boards

Add loading state for card when creating issue in board
parent 1a7f813b
...@@ -40,6 +40,12 @@ export default { ...@@ -40,6 +40,12 @@ export default {
this.selectedBoardItems.findIndex((boardItem) => boardItem.id === this.item.id) > -1 this.selectedBoardItems.findIndex((boardItem) => boardItem.id === this.item.id) > -1
); );
}, },
isDisabled() {
return this.disabled || !this.item.id || this.item.isLoading;
},
isDraggable() {
return !this.disabled && this.item.id && !this.item.isLoading;
},
}, },
methods: { methods: {
...mapActions(['toggleBoardItemMultiSelection', 'toggleBoardItem']), ...mapActions(['toggleBoardItemMultiSelection', 'toggleBoardItem']),
...@@ -63,9 +69,10 @@ export default { ...@@ -63,9 +69,10 @@ export default {
data-qa-selector="board_card" data-qa-selector="board_card"
:class="{ :class="{
'multi-select': multiSelectVisible, 'multi-select': multiSelectVisible,
'user-can-drag': !disabled && item.id, 'user-can-drag': isDraggable,
'is-disabled': disabled || !item.id, 'is-disabled': isDisabled,
'is-active': isActive, 'is-active': isActive,
'gl-cursor-not-allowed gl-bg-gray-10': item.isLoading,
}" }"
:index="index" :index="index"
:data-item-id="item.id" :data-item-id="item.id"
......
<script> <script>
import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { GlLabel, GlTooltipDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { sortBy } from 'lodash'; import { sortBy } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner'; import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
...@@ -17,6 +17,7 @@ import IssueTimeEstimate from './issue_time_estimate.vue'; ...@@ -17,6 +17,7 @@ import IssueTimeEstimate from './issue_time_estimate.vue';
export default { export default {
components: { components: {
GlLabel, GlLabel,
GlLoadingIcon,
GlIcon, GlIcon,
UserAvatarLink, UserAvatarLink,
TooltipOnTruncate, TooltipOnTruncate,
...@@ -181,9 +182,13 @@ export default { ...@@ -181,9 +182,13 @@ export default {
class="confidential-icon gl-mr-2" class="confidential-icon gl-mr-2"
:aria-label="__('Confidential')" :aria-label="__('Confidential')"
/> />
<a :href="item.path || item.webUrl || ''" :title="item.title" @mousemove.stop>{{ <a
item.title :href="item.path || item.webUrl || ''"
}}</a> :title="item.title"
:class="{ 'gl-text-gray-400': item.isLoading }"
@mousemove.stop
>{{ item.title }}</a
>
</h4> </h4>
</div> </div>
<div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap"> <div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap">
...@@ -206,6 +211,7 @@ export default { ...@@ -206,6 +211,7 @@ export default {
<div <div
class="gl-display-flex align-items-start flex-wrap-reverse board-card-number-container gl-overflow-hidden js-board-card-number-container" class="gl-display-flex align-items-start flex-wrap-reverse board-card-number-container gl-overflow-hidden js-board-card-number-container"
> >
<gl-loading-icon v-if="item.isLoading" size="md" class="mt-3" />
<span <span
v-if="item.referencePath" v-if="item.referencePath"
class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3" class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3"
......
...@@ -509,7 +509,7 @@ export default { ...@@ -509,7 +509,7 @@ export default {
input.projectPath = fullPath; input.projectPath = fullPath;
} }
const placeholderIssue = formatIssue({ ...issueInput, id: placeholderId }); const placeholderIssue = formatIssue({ ...issueInput, id: placeholderId, isLoading: true });
dispatch('addListItem', { list, item: placeholderIssue, position: 0 }); dispatch('addListItem', { list, item: placeholderIssue, position: 0 });
gqlClient gqlClient
......
---
title: Lock a newly created item card in boards
merge_request: 61958
author:
type: changed
import { GlLabel } from '@gitlab/ui'; import { GlLabel, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { range } from 'lodash'; import { range } from 'lodash';
import Vuex from 'vuex'; import Vuex from 'vuex';
...@@ -63,6 +63,7 @@ describe('Board card component', () => { ...@@ -63,6 +63,7 @@ describe('Board card component', () => {
}, },
stubs: { stubs: {
GlLabel: true, GlLabel: true,
GlLoadingIcon: true,
}, },
mocks: { mocks: {
$apollo: { $apollo: {
...@@ -121,6 +122,10 @@ describe('Board card component', () => { ...@@ -121,6 +122,10 @@ describe('Board card component', () => {
expect(wrapper.find('.board-card-assignee .avatar').exists()).toBe(false); expect(wrapper.find('.board-card-assignee .avatar').exists()).toBe(false);
}); });
it('does not render loading icon', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
});
describe('blocked', () => { describe('blocked', () => {
it('renders blocked icon if issue is blocked', async () => { it('renders blocked icon if issue is blocked', async () => {
createWrapper({ createWrapper({
...@@ -399,4 +404,17 @@ describe('Board card component', () => { ...@@ -399,4 +404,17 @@ describe('Board card component', () => {
}); });
}); });
}); });
describe('loading', () => {
it('renders loading icon', async () => {
createWrapper({
item: {
...issue,
isLoading: true,
},
});
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
}); });
import { GlLabel } from '@gitlab/ui'; import { GlLabel } from '@gitlab/ui';
import { createLocalVue, shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import BoardCard from '~/boards/components/board_card.vue'; import BoardCard from '~/boards/components/board_card.vue';
...@@ -12,8 +13,7 @@ describe('Board card', () => { ...@@ -12,8 +13,7 @@ describe('Board card', () => {
let store; let store;
let mockActions; let mockActions;
const localVue = createLocalVue(); Vue.use(Vuex);
localVue.use(Vuex);
const createStore = ({ initialState = {} } = {}) => { const createStore = ({ initialState = {} } = {}) => {
mockActions = { mockActions = {
...@@ -41,14 +41,14 @@ describe('Board card', () => { ...@@ -41,14 +41,14 @@ describe('Board card', () => {
provide = {}, provide = {},
mountFn = shallowMount, mountFn = shallowMount,
stubs = { BoardCardInner }, stubs = { BoardCardInner },
item = mockIssue,
} = {}) => { } = {}) => {
wrapper = mountFn(BoardCard, { wrapper = mountFn(BoardCard, {
localVue,
stubs, stubs,
store, store,
propsData: { propsData: {
list: mockLabelList, list: mockLabelList,
item: mockIssue, item,
disabled: false, disabled: false,
index: 0, index: 0,
...propsData, ...propsData,
...@@ -151,4 +151,24 @@ describe('Board card', () => { ...@@ -151,4 +151,24 @@ describe('Board card', () => {
}); });
}); });
}); });
describe('when card is loading', () => {
it('card is disabled and user cannot drag', () => {
createStore();
mountComponent({ item: { ...mockIssue, isLoading: true } });
expect(wrapper.classes()).toContain('is-disabled');
expect(wrapper.classes()).not.toContain('user-can-drag');
});
});
describe('when card is not loading', () => {
it('user can drag', () => {
createStore();
mountComponent();
expect(wrapper.classes()).not.toContain('is-disabled');
expect(wrapper.classes()).toContain('user-can-drag');
});
});
}); });
...@@ -1244,7 +1244,7 @@ describe('addListNewIssue', () => { ...@@ -1244,7 +1244,7 @@ describe('addListNewIssue', () => {
type: 'addListItem', type: 'addListItem',
payload: { payload: {
list: fakeList, list: fakeList,
item: formatIssue({ ...mockIssue, id: 'tmp' }), item: formatIssue({ ...mockIssue, id: 'tmp', isLoading: true }),
position: 0, position: 0,
}, },
}, },
...@@ -1286,7 +1286,7 @@ describe('addListNewIssue', () => { ...@@ -1286,7 +1286,7 @@ describe('addListNewIssue', () => {
type: 'addListItem', type: 'addListItem',
payload: { payload: {
list: fakeList, list: fakeList,
item: formatIssue({ ...mockIssue, id: 'tmp' }), item: formatIssue({ ...mockIssue, id: 'tmp', isLoading: true }),
position: 0, position: 0,
}, },
}, },
......
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