Commit f59fcd60 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents fa153a3c 26a10be0
add5f3dd182c99b4d9e1cf93e45fec1214c00659 9fd57cbd0b63d448f9a9555b53f065ee1c110199
...@@ -2,7 +2,6 @@ import { find } from 'lodash'; ...@@ -2,7 +2,6 @@ import { find } from 'lodash';
import { inactiveId } from '../constants'; import { inactiveId } from '../constants';
export default { export default {
labelToggleState: state => (state.isShowingLabels ? 'on' : 'off'),
isSidebarOpen: state => state.activeId !== inactiveId, isSidebarOpen: state => state.activeId !== inactiveId,
isSwimlanesOn: () => false, isSwimlanesOn: () => false,
getIssueById: state => id => { getIssueById: state => id => {
......
...@@ -10,7 +10,7 @@ import notesEventHub from '../../notes/event_hub'; ...@@ -10,7 +10,7 @@ import notesEventHub from '../../notes/event_hub';
import DiffFileHeader from './diff_file_header.vue'; import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue'; import DiffContent from './diff_content.vue';
import { diffViewerErrors } from '~/ide/constants'; import { diffViewerErrors } from '~/ide/constants';
import { collapsedType, isCollapsed } from '../diff_file'; import { collapsedType, isCollapsed } from '../utils/diff_file';
import { import {
DIFF_FILE_AUTOMATIC_COLLAPSE, DIFF_FILE_AUTOMATIC_COLLAPSE,
DIFF_FILE_MANUAL_COLLAPSE, DIFF_FILE_MANUAL_COLLAPSE,
......
...@@ -19,7 +19,7 @@ import { __, s__, sprintf } from '~/locale'; ...@@ -19,7 +19,7 @@ import { __, s__, sprintf } from '~/locale';
import { diffViewerModes } from '~/ide/constants'; import { diffViewerModes } from '~/ide/constants';
import DiffStats from './diff_stats.vue'; import DiffStats from './diff_stats.vue';
import { scrollToElement } from '~/lib/utils/common_utils'; import { scrollToElement } from '~/lib/utils/common_utils';
import { isCollapsed } from '../diff_file'; import { isCollapsed } from '../utils/diff_file';
import { DIFF_FILE_HEADER } from '../i18n'; import { DIFF_FILE_HEADER } from '../i18n';
export default { export default {
......
...@@ -49,7 +49,7 @@ import { ...@@ -49,7 +49,7 @@ import {
DIFF_FILE_BY_FILE_COOKIE_NAME, DIFF_FILE_BY_FILE_COOKIE_NAME,
} from '../constants'; } from '../constants';
import { diffViewerModes } from '~/ide/constants'; import { diffViewerModes } from '~/ide/constants';
import { isCollapsed } from '../diff_file'; import { isCollapsed } from '../utils/diff_file';
export const setBaseConfig = ({ commit }, options) => { export const setBaseConfig = ({ commit }, options) => {
const { const {
......
...@@ -16,7 +16,7 @@ import { ...@@ -16,7 +16,7 @@ import {
SHOW_WHITESPACE, SHOW_WHITESPACE,
NO_SHOW_WHITESPACE, NO_SHOW_WHITESPACE,
} from '../constants'; } from '../constants';
import { prepareRawDiffFile } from '../diff_file'; import { prepareRawDiffFile } from '../utils/diff_file';
export const isAdded = line => ['new', 'new-nonewline'].includes(line.type); export const isAdded = line => ['new', 'new-nonewline'].includes(line.type);
export const isRemoved = line => ['old', 'old-nonewline'].includes(line.type); export const isRemoved = line => ['old', 'old-nonewline'].includes(line.type);
......
...@@ -3,7 +3,7 @@ import { ...@@ -3,7 +3,7 @@ import {
DIFF_FILE_DELETED_MODE, DIFF_FILE_DELETED_MODE,
DIFF_FILE_MANUAL_COLLAPSE, DIFF_FILE_MANUAL_COLLAPSE,
DIFF_FILE_AUTOMATIC_COLLAPSE, DIFF_FILE_AUTOMATIC_COLLAPSE,
} from './constants'; } from '../constants';
function fileSymlinkInformation(file, fileList) { function fileSymlinkInformation(file, fileList) {
const duplicates = fileList.filter(iteratedFile => iteratedFile.file_hash === file.file_hash); const duplicates = fileList.filter(iteratedFile => iteratedFile.file_hash === file.file_hash);
......
...@@ -7,7 +7,7 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; ...@@ -7,7 +7,7 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue'; import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { getDiffMode } from '~/diffs/store/utils'; import { getDiffMode } from '~/diffs/store/utils';
import { diffViewerModes } from '~/ide/constants'; import { diffViewerModes } from '~/ide/constants';
import { isCollapsed } from '../../diffs/diff_file'; import { isCollapsed } from '../../diffs/utils/diff_file';
const FIRST_CHAR_REGEX = /^(\+|-| )/; const FIRST_CHAR_REGEX = /^(\+|-| )/;
......
...@@ -2,13 +2,15 @@ ...@@ -2,13 +2,15 @@
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { import {
GlDrawer, GlDrawer,
GlBadge,
GlIcon,
GlLink,
GlInfiniteScroll, GlInfiniteScroll,
GlResizeObserverDirective, GlResizeObserverDirective,
GlTabs,
GlTab,
GlBadge,
GlLoadingIcon,
} from '@gitlab/ui'; } from '@gitlab/ui';
import SkeletonLoader from './skeleton_loader.vue'; import SkeletonLoader from './skeleton_loader.vue';
import Feature from './feature.vue';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { getDrawerBodyHeight } from '../utils/get_drawer_body_height'; import { getDrawerBodyHeight } from '../utils/get_drawer_body_height';
...@@ -17,11 +19,13 @@ const trackingMixin = Tracking.mixin(); ...@@ -17,11 +19,13 @@ const trackingMixin = Tracking.mixin();
export default { export default {
components: { components: {
GlDrawer, GlDrawer,
GlBadge,
GlIcon,
GlLink,
GlInfiniteScroll, GlInfiniteScroll,
GlTabs,
GlTab,
SkeletonLoader, SkeletonLoader,
Feature,
GlBadge,
GlLoadingIcon,
}, },
directives: { directives: {
GlResizeObserver: GlResizeObserverDirective, GlResizeObserver: GlResizeObserverDirective,
...@@ -31,11 +35,19 @@ export default { ...@@ -31,11 +35,19 @@ export default {
storageKey: { storageKey: {
type: String, type: String,
required: true, required: true,
default: null, },
versions: {
type: Array,
required: true,
},
gitlabDotCom: {
type: Boolean,
required: false,
default: false,
}, },
}, },
computed: { computed: {
...mapState(['open', 'features', 'pageInfo', 'drawerBodyHeight']), ...mapState(['open', 'features', 'pageInfo', 'drawerBodyHeight', 'fetching']),
}, },
mounted() { mounted() {
this.openDrawer(this.storageKey); this.openDrawer(this.storageKey);
...@@ -49,14 +61,25 @@ export default { ...@@ -49,14 +61,25 @@ export default {
methods: { methods: {
...mapActions(['openDrawer', 'closeDrawer', 'fetchItems', 'setDrawerBodyHeight']), ...mapActions(['openDrawer', 'closeDrawer', 'fetchItems', 'setDrawerBodyHeight']),
bottomReached() { bottomReached() {
if (this.pageInfo.nextPage) { const page = this.pageInfo.nextPage;
this.fetchItems(this.pageInfo.nextPage); if (page) {
this.fetchItems({ page });
} }
}, },
handleResize() { handleResize() {
const height = getDrawerBodyHeight(this.$refs.drawer.$el); const height = getDrawerBodyHeight(this.$refs.drawer.$el);
this.setDrawerBodyHeight(height); this.setDrawerBodyHeight(height);
}, },
featuresForVersion(version) {
return this.features.filter(feature => {
return feature.release === parseFloat(version);
});
},
fetchVersion(version) {
if (this.featuresForVersion(version).length === 0) {
this.fetchItems({ version });
}
},
}, },
}; };
</script> </script>
...@@ -73,64 +96,39 @@ export default { ...@@ -73,64 +96,39 @@ export default {
<template #header> <template #header>
<h4 class="page-title gl-my-2">{{ __("What's new at GitLab") }}</h4> <h4 class="page-title gl-my-2">{{ __("What's new at GitLab") }}</h4>
</template> </template>
<gl-infinite-scroll <template v-if="features.length">
v-if="features.length" <gl-infinite-scroll
:fetched-items="features.length" v-if="gitlabDotCom"
:max-list-height="drawerBodyHeight" :fetched-items="features.length"
class="gl-p-0" :max-list-height="drawerBodyHeight"
@bottomReached="bottomReached" class="gl-p-0"
> @bottomReached="bottomReached"
<template #items> >
<div <template #items>
v-for="feature in features" <feature v-for="feature in features" :key="feature.title" :feature="feature" />
:key="feature.title" </template>
class="gl-pb-7 gl-pt-5 gl-px-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100" </gl-infinite-scroll>
<gl-tabs v-else :style="{ height: `${drawerBodyHeight}px` }" class="gl-p-0">
<gl-tab
v-for="(version, index) in versions"
:key="version"
@click="fetchVersion(version)"
> >
<gl-link <template #title>
:href="feature.url" <span>{{ version }}</span>
target="_blank" <gl-badge v-if="index === 0">{{ __('Your Version') }}</gl-badge>
class="whats-new-item-title-link" </template>
data-track-event="click_whats_new_item" <gl-loading-icon v-if="fetching" size="lg" class="text-center" />
:data-track-label="feature.title" <template v-else>
:data-track-property="feature.url" <feature
> v-for="feature in featuresForVersion(version)"
<h5 class="gl-font-lg">{{ feature.title }}</h5> :key="feature.title"
</gl-link> :feature="feature"
<div v-if="feature.packages" class="gl-mb-3">
<gl-badge
v-for="package_name in feature.packages"
:key="package_name"
size="sm"
class="whats-new-item-badge gl-mr-2"
>
<gl-icon name="license" />{{ package_name }}
</gl-badge>
</div>
<gl-link
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<img
:alt="feature.title"
:src="feature.image_url"
class="img-thumbnail gl-px-8 gl-py-3 whats-new-item-image"
/> />
</gl-link> </template>
<p class="gl-pt-3">{{ feature.body }}</p> </gl-tab>
<gl-link </gl-tabs>
:href="feature.url" </template>
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>{{ __('Learn more') }}</gl-link
>
</div>
</template>
</gl-infinite-scroll>
<div v-else class="gl-mt-5"> <div v-else class="gl-mt-5">
<skeleton-loader /> <skeleton-loader />
<skeleton-loader /> <skeleton-loader />
......
<script>
import { GlBadge, GlIcon, GlLink } from '@gitlab/ui';
export default {
components: {
GlBadge,
GlIcon,
GlLink,
},
props: {
feature: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="gl-pb-7 gl-pt-5 gl-px-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
<gl-link
:href="feature.url"
target="_blank"
class="whats-new-item-title-link"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<h5 class="gl-font-lg" data-test-id="feature-title">{{ feature.title }}</h5>
</gl-link>
<div v-if="feature.packages" class="gl-mb-3">
<gl-badge
v-for="packageName in feature.packages"
:key="packageName"
size="sm"
class="whats-new-item-badge gl-mr-2"
>
<gl-icon name="license" />{{ packageName }}
</gl-badge>
</div>
<gl-link
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
<img
:alt="feature.title"
:src="feature.image_url"
class="img-thumbnail gl-px-8 gl-py-3 whats-new-item-image"
/>
</gl-link>
<p class="gl-pt-3">{{ feature.body }}</p>
<gl-link
:href="feature.url"
target="_blank"
data-track-event="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>{{ __('Learn more') }}</gl-link
>
</div>
</template>
...@@ -10,8 +10,6 @@ export default el => { ...@@ -10,8 +10,6 @@ export default el => {
if (whatsNewApp) { if (whatsNewApp) {
store.dispatch('openDrawer'); store.dispatch('openDrawer');
} else { } else {
const storageKey = getStorageKey(el);
whatsNewApp = new Vue({ whatsNewApp = new Vue({
el, el,
store, store,
...@@ -28,7 +26,11 @@ export default el => { ...@@ -28,7 +26,11 @@ export default el => {
}, },
render(createElement) { render(createElement) {
return createElement('app', { return createElement('app', {
props: { storageKey }, props: {
storageKey: getStorageKey(el),
versions: JSON.parse(el.getAttribute('data-versions')),
gitlabDotCom: el.getAttribute('data-gitlab-dot-com'),
},
}); });
}, },
}); });
......
...@@ -13,7 +13,7 @@ export default { ...@@ -13,7 +13,7 @@ export default {
localStorage.setItem(storageKey, JSON.stringify(false)); localStorage.setItem(storageKey, JSON.stringify(false));
} }
}, },
fetchItems({ commit, state }, page) { fetchItems({ commit, state }, { page, version } = { page: null, version: null }) {
if (state.fetching) { if (state.fetching) {
return false; return false;
} }
...@@ -24,6 +24,7 @@ export default { ...@@ -24,6 +24,7 @@ export default {
.get('/-/whats_new', { .get('/-/whats_new', {
params: { params: {
page, page,
version,
}, },
}) })
.then(({ data, headers }) => { .then(({ data, headers }) => {
......
...@@ -6,6 +6,32 @@ ...@@ -6,6 +6,32 @@
.gl-infinite-scroll-legend { .gl-infinite-scroll-legend {
@include gl-display-none; @include gl-display-none;
} }
.gl-tabs {
@include gl-overflow-y-auto;
}
.gl-tabs-nav {
flex-wrap: nowrap;
overflow-x: scroll;
align-items: stretch;
.nav-item {
@include gl-flex-shrink-0;
a {
@include gl-h-full;
line-height: 1.5;
}
}
}
.gl-spinner-container {
@include gl-w-full;
@include gl-absolute;
top: 50%;
transform: translateY(-50%);
}
} }
.with-performance-bar .whats-new-drawer { .with-performance-bar .whats-new-drawer {
......
# frozen_string_literal: true # frozen_string_literal: true
class WhatsNewController < ApplicationController class WhatsNewController < ApplicationController
include Gitlab::Utils::StrongMemoize
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
before_action :check_feature_flag, :check_valid_page_param, :set_pagination_headers before_action :check_feature_flag
before_action :check_valid_page_param, :set_pagination_headers, unless: -> { has_version_param? }
feature_category :navigation feature_category :navigation
def index def index
respond_to do |format| respond_to do |format|
format.js do format.js do
render json: most_recent_items render json: highlight_items
end end
end end
end end
...@@ -29,15 +32,25 @@ class WhatsNewController < ApplicationController ...@@ -29,15 +32,25 @@ class WhatsNewController < ApplicationController
params[:page]&.to_i || 1 params[:page]&.to_i || 1
end end
def most_recent def highlights
@most_recent ||= ReleaseHighlight.paginated(page: current_page) strong_memoize(:highlights) do
if has_version_param?
ReleaseHighlight.for_version(version: params[:version])
else
ReleaseHighlight.paginated(page: current_page)
end
end
end end
def most_recent_items def highlight_items
most_recent[:items].map {|item| Gitlab::WhatsNew::ItemPresenter.present(item) } highlights.map {|item| Gitlab::WhatsNew::ItemPresenter.present(item) }
end end
def set_pagination_headers def set_pagination_headers
response.set_header('X-Next-Page', most_recent[:next_page]) response.set_header('X-Next-Page', highlights.next_page)
end
def has_version_param?
params[:version].present?
end end
end end
...@@ -6,10 +6,14 @@ module WhatsNewHelper ...@@ -6,10 +6,14 @@ module WhatsNewHelper
end end
def whats_new_storage_key def whats_new_storage_key
most_recent_version = ReleaseHighlight.most_recent_version most_recent_version = ReleaseHighlight.versions&.first
return unless most_recent_version return unless most_recent_version
['display-whats-new-notification', most_recent_version].join('-') ['display-whats-new-notification', most_recent_version].join('-')
end end
def whats_new_versions
ReleaseHighlight.versions
end
end end
...@@ -3,6 +3,17 @@ ...@@ -3,6 +3,17 @@
class ReleaseHighlight class ReleaseHighlight
CACHE_DURATION = 1.hour CACHE_DURATION = 1.hour
FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml') FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml')
RELEASE_VERSIONS_IN_A_YEAR = 12
def self.for_version(version:)
index = self.versions.index(version)
return if index.nil?
page = index + 1
self.paginated(page: page)
end
def self.paginated(page: 1) def self.paginated(page: 1)
Rails.cache.fetch(cache_key(page), expires_in: CACHE_DURATION) do Rails.cache.fetch(cache_key(page), expires_in: CACHE_DURATION) do
...@@ -10,10 +21,7 @@ class ReleaseHighlight ...@@ -10,10 +21,7 @@ class ReleaseHighlight
next if items.nil? next if items.nil?
{ QueryResult.new(items: items, next_page: next_page(current_page: page))
items: items,
next_page: next_page(current_page: page)
}
end end
end end
...@@ -53,15 +61,25 @@ class ReleaseHighlight ...@@ -53,15 +61,25 @@ class ReleaseHighlight
next_page if self.file_paths[next_index] next_page if self.file_paths[next_index]
end end
def self.most_recent_version def self.most_recent_item_count
Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:release_version', expires_in: CACHE_DURATION) do Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:recent_item_count', expires_in: CACHE_DURATION) do
self.paginated&.[](:items)&.first&.[]('release') self.paginated&.items&.count
end end
end end
def self.most_recent_item_count def self.versions
Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:recent_item_count', expires_in: CACHE_DURATION) do Gitlab::ProcessMemoryCache.cache_backend.fetch('release_highlight:versions', expires_in: CACHE_DURATION) do
self.paginated&.[](:items)&.count versions = self.file_paths.first(RELEASE_VERSIONS_IN_A_YEAR).map do |path|
/\d*\_(\d*\_\d*)\.yml$/.match(path).captures[0].gsub(/0(?=\d)/, "").tr("_", ".")
end
versions.uniq
end end
end end
QueryResult = Struct.new(:items, :next_page, keyword_init: true) do
include Enumerable
delegate :each, to: :items
end
end end
...@@ -102,7 +102,7 @@ ...@@ -102,7 +102,7 @@
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
- if ::Feature.enabled?(:whats_new_drawer, current_user) - if ::Feature.enabled?(:whats_new_drawer, current_user)
#whats-new-app{ data: { storage_key: whats_new_storage_key } } #whats-new-app{ data: { storage_key: whats_new_storage_key, versions: whats_new_versions, gitlab_dot_com: Gitlab.dev_env_org_or_com? } }
- if can?(current_user, :update_user_status, current_user) - if can?(current_user, :update_user_status, current_user)
.js-set-status-modal-wrapper{ data: user_status_data } .js-set-status-modal-wrapper{ data: user_status_data }
<script>
import { mapState, mapActions } from 'vuex';
import { GlToggle } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
export default {
components: {
GlToggle,
LocalStorageSync,
},
computed: {
...mapState(['isShowingLabels']),
trackProperty() {
return this.isShowingLabels ? 'on' : 'off';
},
},
methods: {
...mapActions(['setShowLabels']),
onToggle(val) {
this.setShowLabels(val);
},
onStorageUpdate(val) {
this.setShowLabels(parseBoolean(val));
},
},
};
</script>
<template>
<div class="board-labels-toggle-wrapper gl-display-flex gl-align-items-center gl-ml-3">
<local-storage-sync
storage-key="gl-show-board-labels"
:value="JSON.stringify(isShowingLabels)"
@input="onStorageUpdate"
/>
<gl-toggle
:value="isShowingLabels"
:label="__('Show labels')"
:data-track-property="trackProperty"
data-track-event="toggle"
data-track-label="show_labels"
label-position="left"
aria-describedby="board-labels-toggle-text"
data-qa-selector="show_labels_toggle"
@change="onToggle"
/>
</div>
</template>
import Vue from 'vue'; import Vue from 'vue';
import { mapState, mapGetters, mapActions } from 'vuex';
import { GlToggle } from '@gitlab/ui';
import Tracking from '~/tracking';
import store from '~/boards/stores'; import store from '~/boards/stores';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import ToggleLabels from './components/toggle_labels.vue';
export default () => export default () =>
new Vue({ new Vue({
el: document.getElementById('js-board-labels-toggle'), el: document.getElementById('js-board-labels-toggle'),
components: { components: {
GlToggle, ToggleLabels,
LocalStorageSync,
}, },
store, store,
computed: { render: createElement => createElement('toggle-labels'),
...mapState(['isShowingLabels']),
...mapGetters(['labelToggleState']),
},
methods: {
...mapActions(['setShowLabels']),
onToggle(val) {
this.setShowLabels(val);
Tracking.event(document.body.dataset.page, 'toggle', {
label: 'show_labels',
property: this.labelToggleState,
});
},
onStorageUpdate(val) {
this.setShowLabels(JSON.parse(val));
},
},
template: `
<div class="board-labels-toggle-wrapper d-flex align-items-center gl-ml-3">
<local-storage-sync storage-key="gl-show-board-labels" :value="JSON.stringify(isShowingLabels)" @input="onStorageUpdate" />
<gl-toggle
:value="isShowingLabels"
label="Show labels"
label-position="left"
aria-describedby="board-labels-toggle-text"
data-qa-selector="show_labels_toggle"
@change="onToggle"
/>
</div>
`,
}); });
import { GlToggle } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import ToggleLabels from 'ee/boards/components/toggle_labels';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('ToggleLabels component', () => {
let wrapper;
let setShowLabels;
function createComponent(state = {}) {
setShowLabels = jest.fn();
return shallowMount(ToggleLabels, {
localVue,
store: new Vuex.Store({
state: {
isShowingLabels: true,
...state,
},
actions: {
setShowLabels,
},
}),
stubs: {
LocalStorageSync,
},
});
}
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('onStorageUpdate parses empty value as false', async () => {
wrapper = createComponent();
const localStorageSync = wrapper.find(LocalStorageSync);
localStorageSync.vm.$emit('input', '');
await wrapper.vm.$nextTick();
expect(setShowLabels).toHaveBeenCalledWith(expect.any(Object), false);
});
it('sets GlToggle value from store.isShowingLabels', () => {
wrapper = createComponent({ isShowingLabels: true });
expect(wrapper.find(GlToggle).props('value')).toEqual(true);
wrapper = createComponent({ isShowingLabels: false });
expect(wrapper.find(GlToggle).props('value')).toEqual(false);
});
});
...@@ -25177,6 +25177,9 @@ msgstr "" ...@@ -25177,6 +25177,9 @@ msgstr ""
msgid "Show file contents" msgid "Show file contents"
msgstr "" msgstr ""
msgid "Show labels"
msgstr ""
msgid "Show latest version" msgid "Show latest version"
msgstr "" msgstr ""
...@@ -31714,6 +31717,9 @@ msgstr "" ...@@ -31714,6 +31717,9 @@ msgstr ""
msgid "Your U2F device was registered!" msgid "Your U2F device was registered!"
msgstr "" msgstr ""
msgid "Your Version"
msgstr ""
msgid "Your WebAuthn device did not send a valid JSON response." msgid "Your WebAuthn device did not send a valid JSON response."
msgstr "" msgstr ""
......
...@@ -10,24 +10,6 @@ import { ...@@ -10,24 +10,6 @@ import {
} from '../mock_data'; } from '../mock_data';
describe('Boards - Getters', () => { describe('Boards - Getters', () => {
describe('labelToggleState', () => {
it('should return "on" when isShowingLabels is true', () => {
const state = {
isShowingLabels: true,
};
expect(getters.labelToggleState(state)).toBe('on');
});
it('should return "off" when isShowingLabels is false', () => {
const state = {
isShowingLabels: false,
};
expect(getters.labelToggleState(state)).toBe('off');
});
});
describe('isSidebarOpen', () => { describe('isSidebarOpen', () => {
it('returns true when activeId is not equal to 0', () => { it('returns true when activeId is not equal to 0', () => {
const state = { const state = {
......
import { prepareRawDiffFile } from '~/diffs/diff_file'; import { prepareRawDiffFile } from '~/diffs/utils/diff_file';
const DIFF_FILES = [ const DIFF_FILES = [
{ {
......
import { createLocalVue, mount } from '@vue/test-utils'; import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { GlDrawer, GlInfiniteScroll } from '@gitlab/ui'; import { GlDrawer, GlInfiniteScroll, GlTabs } from '@gitlab/ui';
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper'; import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import App from '~/whats_new/components/app.vue'; import App from '~/whats_new/components/app.vue';
...@@ -16,12 +16,18 @@ const localVue = createLocalVue(); ...@@ -16,12 +16,18 @@ const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
describe('App', () => { describe('App', () => {
const propsData = { storageKey: 'storage-key' };
let wrapper; let wrapper;
let store; let store;
let actions; let actions;
let state; let state;
let trackingSpy; let trackingSpy;
let gitlabDotCom = true;
const buildProps = () => ({
storageKey: 'storage-key',
versions: ['3.11', '3.10'],
gitlabDotCom,
});
const buildWrapper = () => { const buildWrapper = () => {
actions = { actions = {
...@@ -45,7 +51,7 @@ describe('App', () => { ...@@ -45,7 +51,7 @@ describe('App', () => {
wrapper = mount(App, { wrapper = mount(App, {
localVue, localVue,
store, store,
propsData, propsData: buildProps(),
directives: { directives: {
GlResizeObserver: createMockDirective(), GlResizeObserver: createMockDirective(),
}, },
...@@ -53,112 +59,171 @@ describe('App', () => { ...@@ -53,112 +59,171 @@ describe('App', () => {
}; };
const findInfiniteScroll = () => wrapper.find(GlInfiniteScroll); const findInfiniteScroll = () => wrapper.find(GlInfiniteScroll);
const emitBottomReached = () => findInfiniteScroll().vm.$emit('bottomReached');
beforeEach(async () => { const setup = async () => {
document.body.dataset.page = 'test-page'; document.body.dataset.page = 'test-page';
document.body.dataset.namespaceId = 'namespace-840'; document.body.dataset.namespaceId = 'namespace-840';
trackingSpy = mockTracking('_category_', null, jest.spyOn); trackingSpy = mockTracking('_category_', null, jest.spyOn);
buildWrapper(); buildWrapper();
wrapper.vm.$store.state.features = [{ title: 'Whats New Drawer', url: 'www.url.com' }]; wrapper.vm.$store.state.features = [
{ title: 'Whats New Drawer', url: 'www.url.com', release: 3.11 },
];
wrapper.vm.$store.state.drawerBodyHeight = MOCK_DRAWER_BODY_HEIGHT; wrapper.vm.$store.state.drawerBodyHeight = MOCK_DRAWER_BODY_HEIGHT;
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
}); };
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
unmockTracking(); unmockTracking();
}); });
const getDrawer = () => wrapper.find(GlDrawer); describe('gitlab.com', () => {
beforeEach(() => {
setup();
});
it('contains a drawer', () => { const getDrawer = () => wrapper.find(GlDrawer);
expect(getDrawer().exists()).toBe(true);
});
it('dispatches openDrawer and tracking calls when mounted', () => { it('contains a drawer', () => {
expect(actions.openDrawer).toHaveBeenCalledWith(expect.any(Object), 'storage-key'); expect(getDrawer().exists()).toBe(true);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_whats_new_drawer', {
label: 'namespace_id',
value: 'namespace-840',
}); });
});
it('dispatches closeDrawer when clicking close', () => { it('dispatches openDrawer and tracking calls when mounted', () => {
getDrawer().vm.$emit('close'); expect(actions.openDrawer).toHaveBeenCalledWith(expect.any(Object), 'storage-key');
expect(actions.closeDrawer).toHaveBeenCalled(); expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_whats_new_drawer', {
}); label: 'namespace_id',
value: 'namespace-840',
});
});
it.each([true, false])('passes open property', async openState => { it('dispatches closeDrawer when clicking close', () => {
wrapper.vm.$store.state.open = openState; getDrawer().vm.$emit('close');
expect(actions.closeDrawer).toHaveBeenCalled();
});
await wrapper.vm.$nextTick(); it.each([true, false])('passes open property', async openState => {
wrapper.vm.$store.state.open = openState;
expect(getDrawer().props('open')).toBe(openState); await wrapper.vm.$nextTick();
});
it('renders features when provided via ajax', () => { expect(getDrawer().props('open')).toBe(openState);
expect(actions.fetchItems).toHaveBeenCalled(); });
expect(wrapper.find('h5').text()).toBe('Whats New Drawer');
});
it('send an event when feature item is clicked', () => { it('renders features when provided via ajax', () => {
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn); expect(actions.fetchItems).toHaveBeenCalled();
expect(wrapper.find('[data-test-id="feature-title"]').text()).toBe('Whats New Drawer');
});
const link = wrapper.find('.whats-new-item-title-link'); it('send an event when feature item is clicked', () => {
triggerEvent(link.element); trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
expect(trackingSpy.mock.calls[1]).toMatchObject([ const link = wrapper.find('.whats-new-item-title-link');
'_category_', triggerEvent(link.element);
'click_whats_new_item',
{ expect(trackingSpy.mock.calls[1]).toMatchObject([
label: 'Whats New Drawer', '_category_',
property: 'www.url.com', 'click_whats_new_item',
}, {
]); label: 'Whats New Drawer',
}); property: 'www.url.com',
},
]);
});
it('renders infinite scroll', () => {
const scroll = findInfiniteScroll();
expect(scroll.props()).toMatchObject({
fetchedItems: wrapper.vm.$store.state.features.length,
maxListHeight: MOCK_DRAWER_BODY_HEIGHT,
});
});
describe('bottomReached', () => {
const emitBottomReached = () => findInfiniteScroll().vm.$emit('bottomReached');
it('renders infinite scroll', () => { beforeEach(() => {
const scroll = findInfiniteScroll(); actions.fetchItems.mockClear();
});
expect(scroll.props()).toMatchObject({ it('when nextPage exists it calls fetchItems', () => {
fetchedItems: wrapper.vm.$store.state.features.length, wrapper.vm.$store.state.pageInfo = { nextPage: 840 };
maxListHeight: MOCK_DRAWER_BODY_HEIGHT, emitBottomReached();
expect(actions.fetchItems).toHaveBeenCalledWith(expect.anything(), { page: 840 });
});
it('when nextPage does not exist it does not call fetchItems', () => {
wrapper.vm.$store.state.pageInfo = { nextPage: null };
emitBottomReached();
expect(actions.fetchItems).not.toHaveBeenCalled();
});
});
it('calls getDrawerBodyHeight and setDrawerBodyHeight when resize directive is triggered', () => {
const { value } = getBinding(getDrawer().element, 'gl-resize-observer');
value();
expect(getDrawerBodyHeight).toHaveBeenCalledWith(wrapper.find(GlDrawer).element);
expect(actions.setDrawerBodyHeight).toHaveBeenCalledWith(
expect.any(Object),
MOCK_DRAWER_BODY_HEIGHT,
);
}); });
}); });
describe('bottomReached', () => { describe('self managed', () => {
const findTabs = () => wrapper.find(GlTabs);
const clickSecondTab = async () => {
const secondTab = wrapper.findAll('.nav-link').at(1);
await secondTab.trigger('click');
await new Promise(resolve => requestAnimationFrame(resolve));
};
beforeEach(() => { beforeEach(() => {
actions.fetchItems.mockClear(); gitlabDotCom = false;
setup();
}); });
it('when nextPage exists it calls fetchItems', () => { it('renders tabs with drawer body height and content', () => {
wrapper.vm.$store.state.pageInfo = { nextPage: 840 }; const scroll = findInfiniteScroll();
emitBottomReached(); const tabs = findTabs();
expect(actions.fetchItems).toHaveBeenCalledWith(expect.anything(), 840); expect(scroll.exists()).toBe(false);
expect(tabs.attributes().style).toBe(`height: ${MOCK_DRAWER_BODY_HEIGHT}px;`);
expect(wrapper.find('h5').text()).toBe('Whats New Drawer');
}); });
it('when nextPage does not exist it does not call fetchItems', () => { describe('fetchVersion', () => {
wrapper.vm.$store.state.pageInfo = { nextPage: null }; beforeEach(() => {
emitBottomReached(); actions.fetchItems.mockClear();
});
expect(actions.fetchItems).not.toHaveBeenCalled(); it('when version isnt fetched, clicking a tab calls fetchItems', async () => {
}); const fetchVersionSpy = jest.spyOn(wrapper.vm, 'fetchVersion');
}); await clickSecondTab();
it('calls getDrawerBodyHeight and setDrawerBodyHeight when resize directive is triggered', () => { expect(fetchVersionSpy).toHaveBeenCalledWith('3.10');
const { value } = getBinding(getDrawer().element, 'gl-resize-observer'); expect(actions.fetchItems).toHaveBeenCalledWith(expect.anything(), { version: '3.10' });
});
value(); it('when version has been fetched, clicking a tab calls fetchItems', async () => {
wrapper.vm.$store.state.features.push({ title: 'GitLab Stories', release: 3.1 });
await wrapper.vm.$nextTick();
expect(getDrawerBodyHeight).toHaveBeenCalledWith(wrapper.find(GlDrawer).element); const fetchVersionSpy = jest.spyOn(wrapper.vm, 'fetchVersion');
await clickSecondTab();
expect(actions.setDrawerBodyHeight).toHaveBeenCalledWith( expect(fetchVersionSpy).toHaveBeenCalledWith('3.10');
expect.any(Object), expect(actions.fetchItems).not.toHaveBeenCalled();
MOCK_DRAWER_BODY_HEIGHT, expect(wrapper.find('.tab-pane.active h5').text()).toBe('GitLab Stories');
); });
});
}); });
}); });
...@@ -41,6 +41,23 @@ describe('whats new actions', () => { ...@@ -41,6 +41,23 @@ describe('whats new actions', () => {
axiosMock.restore(); axiosMock.restore();
}); });
it('passes arguments', () => {
axiosMock.reset();
axiosMock
.onGet('/-/whats_new', { params: { page: 8, version: 40 } })
.replyOnce(200, [{ title: 'GitLab Stories' }]);
testAction(
actions.fetchItems,
{ page: 8, version: 40 },
{},
expect.arrayContaining([
{ type: types.ADD_FEATURES, payload: [{ title: 'GitLab Stories' }] },
]),
);
});
it('if already fetching, does not fetch', () => { it('if already fetching, does not fetch', () => {
testAction(actions.fetchItems, {}, { fetching: true }, []); testAction(actions.fetchItems, {}, { fetching: true }, []);
}); });
......
...@@ -10,7 +10,7 @@ RSpec.describe WhatsNewHelper do ...@@ -10,7 +10,7 @@ RSpec.describe WhatsNewHelper do
let(:release_item) { double(:item) } let(:release_item) { double(:item) }
before do before do
allow(ReleaseHighlight).to receive(:most_recent_version).and_return(84.0) allow(ReleaseHighlight).to receive(:versions).and_return([84.0])
end end
it { is_expected.to eq('display-whats-new-notification-84.0') } it { is_expected.to eq('display-whats-new-notification-84.0') }
...@@ -18,7 +18,7 @@ RSpec.describe WhatsNewHelper do ...@@ -18,7 +18,7 @@ RSpec.describe WhatsNewHelper do
context 'when most recent release highlights do NOT exist' do context 'when most recent release highlights do NOT exist' do
before do before do
allow(ReleaseHighlight).to receive(:most_recent_version).and_return(nil) allow(ReleaseHighlight).to receive(:versions).and_return(nil)
end end
it { is_expected.to be_nil } it { is_expected.to be_nil }
...@@ -44,4 +44,14 @@ RSpec.describe WhatsNewHelper do ...@@ -44,4 +44,14 @@ RSpec.describe WhatsNewHelper do
end end
end end
end end
describe '#whats_new_versions' do
let(:versions) { [84.0] }
it 'returns ReleaseHighlight.versions' do
expect(ReleaseHighlight).to receive(:versions).and_return(versions)
expect(helper.whats_new_versions).to eq(versions)
end
end
end end
...@@ -3,21 +3,44 @@ ...@@ -3,21 +3,44 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ReleaseHighlight do RSpec.describe ReleaseHighlight do
describe '#paginated' do let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) }
let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) } let(:cache_mock) { double(:cache_mock) }
let(:cache_mock) { double(:cache_mock) }
before do
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
allow(cache_mock).to receive(:fetch).with('release_highlight:file_paths', expires_in: 1.hour).and_yield
end
after do
ReleaseHighlight.instance_variable_set(:@file_paths, nil)
end
describe '.for_version' do
subject { ReleaseHighlight.for_version(version: version) }
let(:version) { '1.1' }
context 'with version param that exists' do
it 'returns items from that version' do
expect(subject.items.first['title']).to eq("It's gonna be a bright")
end
end
context 'with version param that does NOT exist' do
let(:version) { '84.0' }
it 'returns nil' do
expect(subject).to be_nil
end
end
end
describe '.paginated' do
let(:dot_com) { false } let(:dot_com) { false }
before do before do
allow(Gitlab).to receive(:com?).and_return(dot_com) allow(Gitlab).to receive(:com?).and_return(dot_com)
allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
expect(Rails).to receive(:cache).twice.and_return(cache_mock) expect(Rails).to receive(:cache).twice.and_return(cache_mock)
expect(cache_mock).to receive(:fetch).with('release_highlight:file_paths', expires_in: 1.hour).and_yield
end
after do
ReleaseHighlight.instance_variable_set(:@file_paths, nil)
end end
context 'with page param' do context 'with page param' do
...@@ -90,46 +113,51 @@ RSpec.describe ReleaseHighlight do ...@@ -90,46 +113,51 @@ RSpec.describe ReleaseHighlight do
end end
end end
describe '.most_recent_version' do describe '.most_recent_item_count' do
subject { ReleaseHighlight.most_recent_version } subject { ReleaseHighlight.most_recent_item_count }
context 'when version exist' do context 'when recent release items exist' do
let(:release_item) { double(:item) } it 'returns the count from the most recent file' do
allow(ReleaseHighlight).to receive(:paginated).and_return(double(:paginated, items: [double(:item)]))
before do expect(subject).to eq(1)
allow(ReleaseHighlight).to receive(:paginated).and_return({ items: [release_item] })
allow(release_item).to receive(:[]).with('release').and_return(84.0)
end end
it { is_expected.to eq(84.0) }
end end
context 'when most recent release highlights do NOT exist' do context 'when recent release items do NOT exist' do
before do it 'returns nil' do
allow(ReleaseHighlight).to receive(:paginated).and_return(nil) allow(ReleaseHighlight).to receive(:paginated).and_return(nil)
end
it { is_expected.to be_nil } expect(subject).to be_nil
end
end end
end end
describe '#most_recent_item_count' do describe '.versions' do
subject { ReleaseHighlight.most_recent_item_count } it 'returns versions from the file paths' do
expect(ReleaseHighlight.versions).to eq(['1.5', '1.2', '1.1'])
end
context 'when recent release items exist' do context 'when there are more than 12 versions' do
it 'returns the count from the most recent file' do let(:file_paths) do
allow(ReleaseHighlight).to receive(:paginated).and_return({ items: [double(:item)] }) i = 0
Array.new(20) { "20201225_01_#{i += 1}.yml" }
end
expect(subject).to eq(1) it 'limits to 12 versions' do
allow(ReleaseHighlight).to receive(:file_paths).and_return(file_paths)
expect(ReleaseHighlight.versions.count).to eq(12)
end end
end end
end
context 'when recent release items do NOT exist' do describe 'QueryResult' do
it 'returns nil' do subject { ReleaseHighlight::QueryResult.new(items: items, next_page: 2) }
allow(ReleaseHighlight).to receive(:paginated).and_return(nil)
expect(subject).to be_nil let(:items) { [:item] }
end
it 'responds to map' do
expect(subject.map(&:to_s)).to eq(items.map(&:to_s))
end end
end end
end end
...@@ -4,22 +4,22 @@ require 'spec_helper' ...@@ -4,22 +4,22 @@ require 'spec_helper'
RSpec.describe WhatsNewController do RSpec.describe WhatsNewController do
describe 'whats_new_path' do describe 'whats_new_path' do
let(:item) { double(:item) }
let(:highlights) { double(:highlight, items: [item], map: [item].map, next_page: 2) }
context 'with whats_new_drawer feature enabled' do context 'with whats_new_drawer feature enabled' do
before do before do
stub_feature_flags(whats_new_drawer: true) stub_feature_flags(whats_new_drawer: true)
end end
context 'with no page param' do context 'with no page param' do
let(:most_recent) { { items: [item], next_page: 2 } }
let(:item) { double(:item) }
it 'responds with paginated data and headers' do it 'responds with paginated data and headers' do
allow(ReleaseHighlight).to receive(:paginated).with(page: 1).and_return(most_recent) allow(ReleaseHighlight).to receive(:paginated).with(page: 1).and_return(highlights)
allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item) allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item)
get whats_new_path, xhr: true get whats_new_path, xhr: true
expect(response.body).to eq(most_recent[:items].to_json) expect(response.body).to eq(highlights.items.to_json)
expect(response.headers['X-Next-Page']).to eq(2) expect(response.headers['X-Next-Page']).to eq(2)
end end
end end
...@@ -37,6 +37,18 @@ RSpec.describe WhatsNewController do ...@@ -37,6 +37,18 @@ RSpec.describe WhatsNewController do
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
context 'with version param' do
it 'returns items without pagination headers' do
allow(ReleaseHighlight).to receive(:for_version).with(version: '42').and_return(highlights)
allow(Gitlab::WhatsNew::ItemPresenter).to receive(:present).with(item).and_return(item)
get whats_new_path(version: 42), xhr: true
expect(response.body).to eq(highlights.items.to_json)
expect(response.headers['X-Next-Page']).to be_nil
end
end
end end
context 'with whats_new_drawer feature disabled' do context 'with whats_new_drawer feature disabled' do
......
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