Skip to content
Snippets Groups Projects
Commit 02fd2935 authored by Kushal Pandya's avatar Kushal Pandya :speech_balloon:
Browse files

Fix scoped roadmap loading error and layout

Fixes bug around scope roadmap (view which opens from within an Epic)
where loading the view caused JS error, also fixes a bug where height
was calculated incorrectly.

EE: true
Changelog: fixed
parent d0e0924b
No related branches found
No related tags found
2 merge requests!122439Fix scoped roadmap loading error and layout,!119439Draft: Prevent file variable content expansion in downstream pipeline
Showing
with 97 additions and 54 deletions
......@@ -66,6 +66,7 @@ export default () => {
allowScopedLabels: epicMeta.scopedLabels,
labelsManagePath: epicMeta.labelsWebUrl,
allowSubEpics: parseBoolean(el.dataset.allowSubEpics),
hasIterationsFeature: false,
treeElementSelector,
roadmapElementSelector,
containerElementSelector,
......
......@@ -57,12 +57,10 @@ export default {
</script>
<template>
<div class="gl-mb-3">
<div id="roadmap" class="roadmap-app gl-rounded-bottom-base gl-border-t gl-bg-white">
<gl-alert v-if="loadingError" variant="danger" :dismissible="false">
{{ $options.loadingFailedText }}
</gl-alert>
<div id="roadmap" class="roadmap-app border gl-rounded-base gl-bg-white">
<div id="js-roadmap" v-bind="roadmapAttrs"></div>
</div>
<div id="js-roadmap" v-bind="roadmapAttrs"></div>
</div>
</template>
......@@ -144,7 +144,12 @@ export default {
</script>
<template>
<gl-empty-state :title="message" :svg-path="emptyStateIllustrationPath" v-bind="extraProps">
<gl-empty-state
:title="message"
:svg-path="emptyStateIllustrationPath"
class="gl-mt-0"
v-bind="extraProps"
>
<template #description>
<p v-safe-html="subMessage" data-testid="sub-title"></p>
</template>
......
<script>
import { GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { mapState, mapActions, mapGetters } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { EPIC_DETAILS_CELL_WIDTH, TIMELINE_CELL_MIN_WIDTH, EPIC_ITEM_HEIGHT } from '../constants';
......@@ -60,6 +60,10 @@ export default {
'pageInfo',
'epicsFetchForNextPageInProgress',
]),
...mapGetters(['isScopedRoadmap']),
hasNextPage() {
return this.pageInfo?.hasNextPage;
},
emptyRowContainerVisible() {
return this.displayedEpics.length < this.bufferSize;
},
......@@ -97,8 +101,11 @@ export default {
methods: {
...mapActions(['setBufferSize', 'toggleEpic', 'fetchEpics']),
initMounted() {
const containerInnerHeight = this.isScopedRoadmap
? this.$root.$el.clientHeight
: window.innerHeight;
this.roadmapShellEl = this.$root.$el && this.$root.$el.querySelector('.js-roadmap-shell');
this.setBufferSize(Math.ceil((window.innerHeight - this.$el.offsetTop) / EPIC_ITEM_HEIGHT));
this.setBufferSize(Math.ceil((containerInnerHeight - this.$el.offsetTop) / EPIC_ITEM_HEIGHT));
// Wait for component render to complete
this.$nextTick(() => {
......@@ -123,8 +130,11 @@ export default {
getEmptyRowContainerStyles() {
if (this.displayedEpics.length && this.$refs.emptyRowContainer) {
const { top } = this.$refs.emptyRowContainer.getBoundingClientRect();
const { offsetTop } = this.$refs.emptyRowContainer;
return {
height: `calc(100vh - ${top}px)`,
height: this.isScopedRoadmap
? `calc(${this.$root.$el.clientHeight}px - ${offsetTop}px)`
: `calc(100vh - ${top}px)`,
};
}
return {};
......@@ -133,6 +143,8 @@ export default {
this.showBottomShadow = Math.ceil(scrollTop) + clientHeight < scrollHeight;
},
handleScrolledToEnd() {
if (!this.pageInfo) return;
const { hasNextPage, endCursor } = this.pageInfo;
if (!this.epicsFetchForNextPageInProgress && hasNextPage) {
this.fetchEpics({ endCursor });
......@@ -162,22 +174,23 @@ export default {
:children-flags="childrenFlags"
:has-filters-applied="hasFiltersApplied"
/>
<div
v-if="emptyRowContainerVisible"
ref="emptyRowContainer"
:style="emptyRowContainerStyles"
class="epics-list-item epics-list-item-empty clearfix"
>
<span class="epic-details-cell"></span>
<span
v-for="(timeframeItem, index) in timeframe"
:key="index"
class="epic-timeline-cell gl-display-flex"
<div v-if="emptyRowContainerVisible" class="epic-item-container">
<div
ref="emptyRowContainer"
:style="emptyRowContainerStyles"
class="epics-list-item epics-list-item-empty clearfix"
>
<current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" />
</span>
<span class="epic-details-cell"></span>
<span
v-for="(timeframeItem, index) in timeframe"
:key="index"
class="epic-timeline-cell gl-display-flex"
>
<current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" />
</span>
</div>
</div>
<gl-intersection-observer @appear="handleScrolledToEnd">
<gl-intersection-observer v-if="hasNextPage" @appear="handleScrolledToEnd">
<div
v-if="epicsFetchForNextPageInProgress"
class="gl-text-center gl-py-3"
......
......@@ -72,8 +72,15 @@ export default {
<template>
<div class="roadmap-app-container gl-h-full">
<roadmap-filters v-if="showFilteredSearchbar && !epicIid" @toggleSettings="toggleSettings" />
<div :class="{ 'overflow-reset': epicsFetchResultEmpty }" class="roadmap-container gl-relative">
<roadmap-filters
v-if="showFilteredSearchbar && !epicIid"
ref="roadmapFilters"
@toggleSettings="toggleSettings"
/>
<div
:class="{ 'overflow-reset': epicsFetchResultEmpty }"
class="roadmap-container gl-rounded-bottom-base gl-relative"
>
<gl-loading-icon v-if="epicsFetchInProgress" class="gl-my-5" size="lg" />
<epics-list-empty
v-else-if="epicsFetchResultEmpty"
......@@ -94,12 +101,12 @@ export default {
:has-filters-applied="hasFiltersApplied"
:is-settings-sidebar-open="isSettingsSidebarOpen"
/>
<roadmap-settings
:is-open="isSettingsSidebarOpen"
:timeframe-range-type="timeframeRangeType"
data-testid="roadmap-settings"
@toggleSettings="toggleSettings"
/>
</div>
<roadmap-settings
:is-open="isSettingsSidebarOpen"
:timeframe-range-type="timeframeRangeType"
data-testid="roadmap-settings"
@toggleSettings="toggleSettings"
/>
</div>
</template>
......@@ -81,7 +81,7 @@ export default {
</script>
<template>
<div class="epics-filters epics-roadmap-filters epics-roadmap-filters-gl-ui">
<div class="epics-filters epics-roadmap-filters epics-roadmap-filters-gl-ui gl-relative">
<div
class="epics-details-filters filtered-search-block gl-display-flex gl-flex-direction-column gl-xl-flex-direction-row gl-p-3 row-content-block second-block"
>
......@@ -101,7 +101,7 @@ export default {
/>
<gl-button
icon="settings"
class="gl-xl-ml-3 gl-inset-border-1-gray-400!"
class="gl-xl-ml-3 gl-lg-mt-0 gl-mt-3 gl-inset-border-1-gray-400!"
:aria-label="$options.i18n.settings"
data-testid="settings-button"
@click="$emit('toggleSettings', $event)"
......
......@@ -25,6 +25,19 @@ export default {
required: true,
},
},
data() {
return {
headerHeight: '',
};
},
mounted() {
this.$nextTick(() => {
const { offsetTop = 0 } = this.$root.$el;
const clientHeight = this.$parent.$refs?.roadmapFilters?.$el.clientHeight || 0;
this.headerHeight = `${offsetTop + clientHeight}px`;
});
},
};
</script>
......@@ -32,7 +45,8 @@ export default {
<gl-drawer
v-bind="$attrs"
:open="isOpen"
class="gl-absolute"
:z-index="20"
:header-height="headerHeight"
@close="$emit('toggleSettings', $event)"
>
<template #title>
......
<script>
import { mapActions, mapState } from 'vuex';
import { mapActions, mapState, mapGetters } from 'vuex';
import eventHub from '../event_hub';
import { MILESTONES_GROUP, MILESTONES_SUBGROUP, MILESTONES_PROJECT } from '../constants';
......@@ -43,6 +43,7 @@ export default {
},
computed: {
...mapState(['defaultInnerHeight', 'isShowingMilestones', 'milestonesType', 'milestones']),
...mapGetters(['isScopedRoadmap']),
displayMilestones() {
return Boolean(this.milestones.length) && this.isShowingMilestones;
},
......@@ -77,7 +78,7 @@ export default {
getContainerStyles() {
const { top } = this.$el.getBoundingClientRect();
return {
height: `calc(100vh - ${top}px)`,
height: this.isScopedRoadmap ? '100%' : `calc(100vh - ${top}px)`,
};
},
},
......
......@@ -12,4 +12,7 @@ export default () =>
actions,
mutations,
state: state(),
getters: {
isScopedRoadmap: (s) => Boolean(s.epicIid),
},
});
......@@ -72,22 +72,8 @@
}
.related-items-tree-container {
.roadmap-app-container {
.js-roadmap-shell {
border-radius: $gl-border-radius-base;
}
.epics-list-item-empty {
display: none;
}
// This is a hacky CSS to remove the border-bottom from the
// last list in the roadmap.
.epic-item-container:nth-last-child(4) {
.epic-details-cell,
.epic-timeline-cell {
border-bottom: 0;
}
}
.roadmap-app {
min-height: 600px;
height: 50vh;
}
}
......@@ -49,6 +49,10 @@ html.group-epics-roadmap-html {
}
}
.epics-roadmap-filters {
z-index: $fixed-items-z-index + 1;
}
.epics-details-filters {
.btn-group {
.dropdown-toggle {
......@@ -236,6 +240,14 @@ html.group-epics-roadmap-html {
border-bottom: $border-style;
}
// Ensure that last epic item doesn't have bottom border
.epic-item-container:nth-last-of-type(2) {
.epic-details-cell,
.epic-timeline-cell {
border-bottom: 0;
}
}
.epic-details-cell {
position: sticky;
position: -webkit-sticky;
......
......@@ -144,6 +144,7 @@ describe('EpicsListSectionComponent', () => {
// $nextTick call in EpicsListSectionComponent's mounted hook.
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27992#note_319213990
wrapper.destroy();
wrapper.vm.$store.state.epicIid = undefined;
wrapper = createComponent();
});
......
......@@ -29,8 +29,10 @@ describe('RoadmapSettings', () => {
describe('template', () => {
it('renders drawer and title', () => {
expect(findSettingsDrawer().exists()).toBe(true);
expect(findSettingsDrawer().text()).toContain('Roadmap settings');
const settingsDrawer = findSettingsDrawer();
expect(settingsDrawer.exists()).toBe(true);
expect(settingsDrawer.text()).toContain('Roadmap settings');
expect(settingsDrawer.props('headerHeight')).toBe('0px');
});
it('renders roadmap daterange component', () => {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment