Commit 59a89e43 authored by Florie Guibert's avatar Florie Guibert Committed by Enrique Alcántara

Swimlanes - Use VirtualList to improve loading experience

Introduce swimlanes_buffered_rendering feature flag wo test VirtualList
on Swimlanes
parent f264588c
......@@ -10,6 +10,7 @@ class Groups::BoardsController < Groups::ApplicationController
before_action do
push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: false)
push_frontend_feature_flag(:boards_filtered_search, group)
push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml)
end
feature_category :boards
......
......@@ -9,6 +9,7 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:add_issues_button)
push_frontend_feature_flag(:swimlanes_buffered_rendering, project, default_enabled: :yaml)
end
feature_category :boards
......
---
name: swimlanes_buffered_rendering
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56614
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/324994
milestone: '13.11'
type: development
group: group::product planning
default_enabled: false
\ No newline at end of file
......@@ -3,6 +3,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { urlParamsToObject } from '~/lib/utils/common_utils';
import { objectToQuery } from '~/lib/utils/url_utility';
import {
EPIC_LANE_BASE_HEIGHT,
IterationFilterType,
IterationIDs,
MilestoneFilterType,
......@@ -31,6 +32,10 @@ export function fullEpicBoardId(epicBoardId) {
return `gid://gitlab/Boards::EpicBoard/${epicBoardId}`;
}
export function calculateSwimlanesBufferSize(listTopCoordinate) {
return Math.ceil((window.innerHeight - listTopCoordinate) / EPIC_LANE_BASE_HEIGHT);
}
export function formatListEpics(listEpics) {
const boardItems = {};
let listItemsCount;
......
<script>
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import VirtualList from 'vue-virtual-scroll-list';
import Draggable from 'vuedraggable';
import { mapActions, mapGetters, mapState } from 'vuex';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
......@@ -7,11 +8,15 @@ import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue'
import { isListDraggable } from '~/boards/boards_util';
import { n__ } from '~/locale';
import defaultSortableConfig from '~/sortable/sortable_config';
import { DRAGGABLE_TAG } from '../constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { calculateSwimlanesBufferSize } from '../boards_util';
import { DRAGGABLE_TAG, EPIC_LANE_BASE_HEIGHT } from '../constants';
import EpicLane from './epic_lane.vue';
import IssuesLaneList from './issues_lane_list.vue';
export default {
EpicLane,
epicLaneBaseHeight: EPIC_LANE_BASE_HEIGHT,
components: {
BoardAddNewColumn,
BoardListHeader,
......@@ -19,10 +24,12 @@ export default {
IssuesLaneList,
GlButton,
GlIcon,
VirtualList,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
lists: {
type: Array,
......@@ -38,6 +45,11 @@ export default {
default: false,
},
},
data() {
return {
bufferSize: 0,
};
},
computed: {
...mapState(['epics', 'pageInfoByListId', 'listsFlags', 'addColumnForm']),
...mapGetters(['getUnassignedIssues']),
......@@ -78,6 +90,9 @@ export default {
);
},
},
mounted() {
this.bufferSize = calculateSwimlanesBufferSize(this.$el.offsetTop);
},
methods: {
...mapActions(['moveList', 'fetchItemsForList']),
handleDragOnEnd(params) {
......@@ -109,6 +124,17 @@ export default {
behavior: 'smooth',
});
},
getEpicLaneProps(index) {
return {
key: this.epics[index].id,
props: {
epic: this.epics[index],
lists: this.lists,
disabled: this.disabled,
canAdminList: this.canAdminList,
},
};
},
},
};
</script>
......@@ -148,6 +174,19 @@ export default {
</div>
</component>
<div class="board-epics-swimlanes gl-display-table">
<template v-if="glFeatures.swimlanesBufferedRendering">
<virtual-list
v-if="epics.length"
:size="$options.epicLaneBaseHeight"
:remain="bufferSize"
:bench="bufferSize"
:scrollelement="$refs.scrollableContainer"
:item="$options.EpicLane"
:itemcount="epics.length"
:itemprops="getEpicLaneProps"
/>
</template>
<template v-else>
<epic-lane
v-for="epic in epics"
:key="epic.id"
......@@ -156,6 +195,7 @@ export default {
:disabled="disabled"
:can-admin-list="canAdminList"
/>
</template>
<div class="board-lane-unassigned-issues-title gl-sticky gl-display-inline-block gl-left-0">
<div class="gl-left-0 gl-pb-5 gl-px-3 gl-display-flex gl-align-items-center">
<span
......
......@@ -2,6 +2,8 @@ import { s__ } from '~/locale';
export const DRAGGABLE_TAG = 'div';
export const EPIC_LANE_BASE_HEIGHT = 40;
/* eslint-disable @gitlab/require-i18n-strings */
export const EpicFilterType = {
any: 'Any',
......
import { GlIcon } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VirtualList from 'vue-virtual-scroll-list';
import Draggable from 'vuedraggable';
import Vuex from 'vuex';
import { calculateSwimlanesBufferSize } from 'ee/boards/boards_util';
import EpicLane from 'ee/boards/components/epic_lane.vue';
import EpicsSwimlanes from 'ee/boards/components/epics_swimlanes.vue';
import IssueLaneList from 'ee/boards/components/issues_lane_list.vue';
import { EPIC_LANE_BASE_HEIGHT } from 'ee/boards/constants';
import getters from 'ee/boards/stores/getters';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
import { mockLists, mockEpics, mockIssuesByListId, issues } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(Vuex);
jest.mock('ee/boards/boards_util');
describe('EpicsSwimlanes', () => {
let wrapper;
......@@ -38,7 +42,7 @@ describe('EpicsSwimlanes', () => {
});
};
const createComponent = (props = {}) => {
const createComponent = ({ canAdminList = false, swimlanesBufferedRendering = false } = {}) => {
const store = createStore();
const defaultProps = {
lists: mockLists,
......@@ -46,9 +50,11 @@ describe('EpicsSwimlanes', () => {
};
wrapper = shallowMount(EpicsSwimlanes, {
localVue,
propsData: { ...defaultProps, ...props },
propsData: { ...defaultProps, canAdminList },
store,
provide: {
glFeatures: { swimlanesBufferedRendering },
},
});
};
......@@ -114,4 +120,30 @@ describe('EpicsSwimlanes', () => {
).not.toContain('is-draggable');
});
});
describe('when swimlanesBufferedRendering is true', () => {
const bufferSize = 100;
beforeEach(() => {
calculateSwimlanesBufferSize.mockReturnValueOnce(bufferSize);
createComponent({ swimlanesBufferedRendering: true });
});
it('renders virtual-list', () => {
const virtualList = wrapper.find(VirtualList);
const scrollableContainer = wrapper.find({ ref: 'scrollableContainer' }).element;
expect(calculateSwimlanesBufferSize).toHaveBeenCalledWith(wrapper.element.offsetTop);
expect(virtualList.props()).toMatchObject({
remain: bufferSize,
bench: bufferSize,
item: EpicLane,
size: EPIC_LANE_BASE_HEIGHT,
itemcount: mockEpics.length,
itemprops: expect.any(Function),
});
expect(virtualList.props().scrollelement).toBe(scrollableContainer);
});
});
});
......@@ -16,6 +16,15 @@ RSpec.describe Groups::BoardsController do
expect { list_boards }.to change(group.boards, :count).by(1)
end
it 'pushes swimlanes_buffered_rendering feature flag' do
allow(controller).to receive(:push_frontend_feature_flag).and_call_original
expect(controller).to receive(:push_frontend_feature_flag)
.with(:swimlanes_buffered_rendering, group, default_enabled: :yaml)
list_boards
end
context 'when format is HTML' do
it 'renders template' do
list_boards
......@@ -98,6 +107,15 @@ RSpec.describe Groups::BoardsController do
describe 'GET show' do
let!(:board) { create(:board, group: group) }
it 'pushes swimlanes_buffered_rendering feature flag' do
allow(controller).to receive(:push_frontend_feature_flag).and_call_original
expect(controller).to receive(:push_frontend_feature_flag)
.with(:swimlanes_buffered_rendering, group, default_enabled: :yaml)
read_board board: board
end
context 'when format is HTML' do
it 'renders template' do
expect { read_board board: board }.to change(BoardGroupRecentVisit, :count).by(1)
......
......@@ -22,6 +22,15 @@ RSpec.describe Projects::BoardsController do
expect(assigns(:boards_endpoint)).to eq project_boards_path(project)
end
it 'pushes swimlanes_buffered_rendering feature flag' do
allow(controller).to receive(:push_frontend_feature_flag).and_call_original
expect(controller).to receive(:push_frontend_feature_flag)
.with(:swimlanes_buffered_rendering, project, default_enabled: :yaml)
list_boards
end
context 'when format is HTML' do
it 'renders template' do
list_boards
......@@ -116,6 +125,15 @@ RSpec.describe Projects::BoardsController do
describe 'GET show' do
let!(:board) { create(:board, project: project) }
it 'pushes swimlanes_buffered_rendering feature flag' do
allow(controller).to receive(:push_frontend_feature_flag).and_call_original
expect(controller).to receive(:push_frontend_feature_flag)
.with(:swimlanes_buffered_rendering, project, default_enabled: :yaml)
read_board board: board
end
it 'sets boards_endpoint instance variable to a boards path' do
read_board board: board
......
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