Commit d4ac8391 authored by Kushal Pandya's avatar Kushal Pandya

Handle logged out state, fix small screen layout

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