Commit c8f5576c authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'kp-fix-epics-tree-bugs' into 'master'

Prevent Epics Tree from being draggable when user is logged out

See merge request gitlab-org/gitlab!18739
parents 3aa3e639 d4ac8391
......@@ -42,7 +42,7 @@ export default {
},
},
computed: {
...mapState(['childrenFlags']),
...mapState(['childrenFlags', 'userSignedIn']),
itemReference() {
return this.item.reference;
},
......@@ -88,6 +88,9 @@ export default {
this.childrenFlags[this.itemReference].itemRemoveInProgress
);
},
showEmptySpacer() {
return !this.parentItem.userPermissions.adminEpic && this.userSignedIn;
},
},
methods: {
...mapActions(['setRemoveItemModalProps']),
......@@ -105,7 +108,10 @@ export default {
<template>
<div class="card card-slim sortable-row flex-grow-1">
<div class="item-body card-body d-flex align-items-center p-2 p-xl-1 pl-xl-3">
<div
class="item-body card-body d-flex align-items-center p-2 pl-xl-3"
:class="{ 'p-xl-1': userSignedIn, 'item-logged-out pt-xl-2 pb-xl-2': !userSignedIn }"
>
<div class="item-contents d-flex align-items-center flex-wrap flex-grow-1 flex-xl-nowrap">
<div class="item-title d-flex align-items-center mb-1 mb-xl-0">
<icon
......@@ -158,30 +164,30 @@ export default {
>{{ item.pathIdSeparator }}{{ itemId }}
</div>
<div
class="item-meta-child d-flex align-items-center order-0 flex-wrap mr-md-1 ml-md-auto ml-xl-2 flex-xl-nowrap"
class="item-meta-child d-flex align-items-center order-0 flex-wrap mr-md-1 ml-md-auto ml-xl-2 mt-2 mt-md-0 flex-xl-nowrap"
>
<item-milestone
v-if="hasMilestone"
:milestone="item.milestone"
class="d-flex align-items-center item-milestone"
class="d-flex align-items-center item-milestone mr-2 mr-md-0"
/>
<item-due-date
v-if="item.dueDate"
:date="item.dueDate"
tooltip-placement="top"
css-class="item-due-date d-flex align-items-center ml-2 mr-0"
css-class="item-due-date d-flex align-items-center ml-0 mr-2 ml-md-2 ml-sm-0 mr-sm-0"
/>
<item-weight
v-if="item.weight"
:weight="item.weight"
class="item-weight d-flex align-items-center ml-2 mr-0"
class="item-weight d-flex align-items-center ml-2 mr-0 ml-md-2"
tag-name="span"
/>
</div>
<item-assignees
v-if="hasAssignees"
:assignees="item.assignees"
class="item-assignees d-inline-flex align-items-center align-self-end ml-auto ml-md-0 mb-md-0 order-2 flex-xl-grow-0 mt-xl-0 mr-xl-1"
class="item-assignees d-inline-flex align-items-center align-self-end ml-0 ml-md-2 mt-2 mt-md-0 mt-xl-0 mr-xl-1 mb-md-0 order-2 flex-xl-grow-0"
/>
</div>
<gl-button
......@@ -195,7 +201,7 @@ export default {
>
<icon :size="16" name="close" class="btn-item-remove-icon" />
</gl-button>
<span v-if="!parentItem.userPermissions.adminEpic" class="p-3"></span>
<span v-if="showEmptySpacer" class="p-3"></span>
</div>
</div>
</div>
......
<script>
import { mapState, mapActions } from 'vuex';
import Draggable from 'vuedraggable';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { ChildType } from '../constants';
......@@ -8,7 +7,6 @@ import TreeDragAndDropMixin from '../mixins/tree_dd_mixin';
export default {
components: {
Draggable,
GlButton,
GlLoadingIcon,
},
......@@ -29,7 +27,7 @@ export default {
};
},
computed: {
...mapState(['childrenFlags']),
...mapState(['childrenFlags', 'userSignedIn']),
currentItemIssuesBeginAtIndex() {
return this.children.findIndex(item => item.type === ChildType.Issue);
},
......@@ -58,14 +56,10 @@ export default {
</script>
<template>
<draggable
tag="ul"
v-bind="dragOptions"
<component
:is="treeRootWrapper"
v-bind="treeRootOptions"
class="list-unstyled related-items-list tree-root"
ghost-class="tree-item-drag-active"
:data-parent-reference="parentItem.reference"
:value="children"
:move="handleDragOnMove"
@start="handleDragOnStart"
@end="handleDragOnEnd"
>
......@@ -80,5 +74,5 @@ export default {
>
<gl-loading-icon v-else size="sm" class="mt-1 mb-1" />
</li>
</draggable>
</component>
</template>
import Draggable from 'vuedraggable';
import defaultSortableConfig from '~/sortable/sortable_config';
import { ChildType, idProp, relativePositions } from '../constants';
export default {
computed: {
dragOptions() {
return {
treeRootWrapper() {
return this.userSignedIn ? Draggable : 'ul';
},
treeRootOptions() {
const options = {
...defaultSortableConfig,
fallbackOnBody: false,
group: this.parentItem.reference,
tag: 'ul',
'ghost-class': 'tree-item-drag-active',
'data-parent-reference': this.parentItem.reference,
value: this.children,
move: this.handleDragOnMove,
};
return this.userSignedIn ? options : {};
},
},
methods: {
......
......@@ -16,7 +16,7 @@ export default () => {
return false;
}
const { id, iid, fullPath, autoCompleteEpics, autoCompleteIssues } = el.dataset;
const { id, iid, fullPath, autoCompleteEpics, autoCompleteIssues, userSignedIn } = el.dataset;
const initialData = JSON.parse(el.dataset.initial);
Vue.component('tree-root', TreeRoot);
......@@ -44,6 +44,7 @@ export default () => {
issuesEndpoint: initialData.issueLinksEndpoint,
autoCompleteEpics: parseBoolean(autoCompleteEpics),
autoCompleteIssues: parseBoolean(autoCompleteIssues),
userSignedIn: parseBoolean(userSignedIn),
});
},
methods: {
......
......@@ -5,12 +5,13 @@ import * as types from './mutation_types';
export default {
[types.SET_INITIAL_CONFIG](
state,
{ epicsEndpoint, issuesEndpoint, autoCompleteEpics, autoCompleteIssues },
{ epicsEndpoint, issuesEndpoint, autoCompleteEpics, autoCompleteIssues, userSignedIn },
) {
state.epicsEndpoint = epicsEndpoint;
state.issuesEndpoint = issuesEndpoint;
state.autoCompleteEpics = autoCompleteEpics;
state.autoCompleteIssues = autoCompleteIssues;
state.userSignedIn = userSignedIn;
},
[types.SET_INITIAL_PARENT_ITEM](state, data) {
......
......@@ -3,6 +3,7 @@ export default () => ({
parentItem: {},
epicsEndpoint: '',
issuesEndpoint: '',
userSignedIn: false,
children: {},
childrenFlags: {},
......
......@@ -28,6 +28,11 @@
.item-body {
cursor: grab;
&.item-logged-out {
cursor: default;
min-height: $grid-size * 5;
}
}
.btn-tree-item-chevron {
......
......@@ -33,6 +33,7 @@
full_path: @group.full_path,
auto_complete_epics: 'true',
auto_complete_issues: 'false',
user_signed_in: current_user.present? ? 'true' : 'false',
initial: issuable_initial_data(@epic).to_json } }
#roadmap.tab-pane
.row
......
......@@ -14,7 +14,7 @@ import createDefaultStore from 'ee/related_items_tree/store';
import * as epicUtils from 'ee/related_items_tree/utils/epic_utils';
import { ChildType, ChildState } from 'ee/related_items_tree/constants';
import { mockParentItem, mockQueryResponse, mockIssue1 } from '../mock_data';
import { mockParentItem, mockInitialConfig, mockQueryResponse, mockIssue1 } from '../mock_data';
const mockItem = Object.assign({}, mockIssue1, {
type: ChildType.Issue,
......@@ -28,6 +28,7 @@ const createComponent = (parentItem = mockParentItem, item = mockItem) => {
const children = epicUtils.processQueryResponse(mockQueryResponse.data.group);
store.dispatch('setInitialParentItem', mockParentItem);
store.dispatch('setInitialConfig', mockInitialConfig);
store.dispatch('setItemChildren', {
parentItem: mockParentItem,
isSubItem: false,
......@@ -284,6 +285,10 @@ describe('RelatedItemsTree', () => {
});
describe('template', () => {
it('renders item body element without class `item-logged-out` when user is signed in', () => {
expect(wrapper.find('.item-body').classes()).not.toContain('item-logged-out');
});
it('renders item state icon for large screens', () => {
const statusIcon = wrapper.findAll(Icon).at(0);
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import Draggable from 'vuedraggable';
import TreeRoot from 'ee/related_items_tree/components/tree_root.vue';
import createDefaultStore from 'ee/related_items_tree/store';
import * as epicUtils from 'ee/related_items_tree/utils/epic_utils';
import { mockQueryResponse, mockParentItem, mockEpic1, mockIssue1 } from '../mock_data';
import {
mockQueryResponse,
mockInitialConfig,
mockParentItem,
mockEpic1,
mockIssue1,
} from '../mock_data';
const { epic } = mockQueryResponse.data.group;
......@@ -20,6 +28,7 @@ const createComponent = ({
const children = epicUtils.processQueryResponse(mockQueryResponse.data.group);
store.dispatch('setInitialParentItem', mockParentItem);
store.dispatch('setInitialConfig', mockInitialConfig);
store.dispatch('setItemChildrenFlags', {
isSubItem: false,
children,
......@@ -78,9 +87,24 @@ describe('RelatedItemsTree', () => {
});
describe('computed', () => {
describe('dragOptions', () => {
it('should return object containing Vue.Draggable config extended from `defaultSortableConfig`', () => {
expect(wrapper.vm.dragOptions).toEqual(
describe('treeRootWrapper', () => {
it('should return Draggable reference when userSignedIn prop is true', () => {
expect(wrapper.vm.treeRootWrapper).toBe(Draggable);
});
it('should return string "ul" when userSignedIn prop is false', () => {
wrapper.vm.$store.dispatch('setInitialConfig', {
...mockInitialConfig,
userSignedIn: false,
});
expect(wrapper.vm.treeRootWrapper).toBe('ul');
});
});
describe('treeRootOptions', () => {
it('should return object containing Vue.Draggable config extended from `defaultSortableConfig` when userSignedIn prop is true', () => {
expect(wrapper.vm.treeRootOptions).toEqual(
jasmine.objectContaining({
animation: 200,
forceFallback: true,
......@@ -88,9 +112,23 @@ describe('RelatedItemsTree', () => {
fallbackOnBody: false,
ghostClass: 'is-ghost',
group: mockParentItem.reference,
tag: 'ul',
'ghost-class': 'tree-item-drag-active',
'data-parent-reference': mockParentItem.reference,
value: wrapper.vm.children,
move: wrapper.vm.handleDragOnMove,
}),
);
});
it('should return an empty object when userSignedIn prop is false', () => {
wrapper.vm.$store.dispatch('setInitialConfig', {
...mockInitialConfig,
userSignedIn: false,
});
expect(wrapper.vm.treeRootOptions).toEqual(jasmine.objectContaining({}));
});
});
});
......
......@@ -3,6 +3,7 @@ export const mockInitialConfig = {
issuesEndpoint: 'http://test.host',
autoCompleteEpics: true,
autoCompleteIssues: false,
userSignedIn: true,
};
export const mockParentItem = {
......
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