Commit 8eae26a8 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents ddfb1b0f c5659029
import initSetHelperText from '~/pages/admin/application_settings/metrics_and_profiling/usage_statistics';
import PayloadPreviewer from '~/pages/admin/application_settings/payload_previewer'; import PayloadPreviewer from '~/pages/admin/application_settings/payload_previewer';
export default () => { export default () => {
...@@ -5,3 +6,5 @@ export default () => { ...@@ -5,3 +6,5 @@ export default () => {
new PayloadPreviewer(trigger).init(); new PayloadPreviewer(trigger).init();
}); });
}; };
initSetHelperText();
...@@ -42,6 +42,7 @@ import { ...@@ -42,6 +42,7 @@ import {
TRACKING_MULTIPLE_FILES_MODE, TRACKING_MULTIPLE_FILES_MODE,
} from '../constants'; } from '../constants';
import diffsEventHub from '../event_hub';
import { reviewStatuses } from '../utils/file_reviews'; import { reviewStatuses } from '../utils/file_reviews';
import { diffsApp } from '../utils/performance'; import { diffsApp } from '../utils/performance';
import { fileByFile } from '../utils/preferences'; import { fileByFile } from '../utils/preferences';
...@@ -52,7 +53,9 @@ import DiffFile from './diff_file.vue'; ...@@ -52,7 +53,9 @@ import DiffFile from './diff_file.vue';
import HiddenFilesWarning from './hidden_files_warning.vue'; import HiddenFilesWarning from './hidden_files_warning.vue';
import MergeConflictWarning from './merge_conflict_warning.vue'; import MergeConflictWarning from './merge_conflict_warning.vue';
import NoChanges from './no_changes.vue'; import NoChanges from './no_changes.vue';
import PreRenderer from './pre_renderer.vue';
import TreeList from './tree_list.vue'; import TreeList from './tree_list.vue';
import VirtualScrollerScrollSync from './virtual_scroller_scroll_sync';
export default { export default {
name: 'DiffsApp', name: 'DiffsApp',
...@@ -71,6 +74,8 @@ export default { ...@@ -71,6 +74,8 @@ export default {
GlSprintf, GlSprintf,
DynamicScroller, DynamicScroller,
DynamicScrollerItem, DynamicScrollerItem,
PreRenderer,
VirtualScrollerScrollSync,
}, },
alerts: { alerts: {
ALERT_OVERFLOW_HIDDEN, ALERT_OVERFLOW_HIDDEN,
...@@ -166,6 +171,7 @@ export default { ...@@ -166,6 +171,7 @@ export default {
return { return {
treeWidth, treeWidth,
diffFilesLength: 0, diffFilesLength: 0,
virtualScrollCurrentIndex: -1,
}; };
}, },
computed: { computed: {
...@@ -323,6 +329,11 @@ export default { ...@@ -323,6 +329,11 @@ export default {
this.setHighlightedRow(id.split('diff-content').pop().slice(1)); this.setHighlightedRow(id.split('diff-content').pop().slice(1));
} }
if (window.gon?.features?.diffsVirtualScrolling) {
diffsEventHub.$on('scrollToFileHash', this.scrollVirtualScrollerToFileHash);
diffsEventHub.$on('scrollToIndex', this.scrollVirtualScrollerToIndex);
}
if (window.gon?.features?.diffSettingsUsageData) { if (window.gon?.features?.diffSettingsUsageData) {
if (this.renderTreeList) { if (this.renderTreeList) {
api.trackRedisHllUserEvent(TRACKING_FILE_BROWSER_TREE); api.trackRedisHllUserEvent(TRACKING_FILE_BROWSER_TREE);
...@@ -377,6 +388,11 @@ export default { ...@@ -377,6 +388,11 @@ export default {
diffsApp.deinstrument(); diffsApp.deinstrument();
this.unsubscribeFromEvents(); this.unsubscribeFromEvents();
this.removeEventListeners(); this.removeEventListeners();
if (window.gon?.features?.diffsVirtualScrolling) {
diffsEventHub.$off('scrollToFileHash', this.scrollVirtualScrollerToFileHash);
diffsEventHub.$off('scrollToIndex', this.scrollVirtualScrollerToIndex);
}
}, },
methods: { methods: {
...mapActions(['startTaskList']), ...mapActions(['startTaskList']),
...@@ -508,6 +524,20 @@ export default { ...@@ -508,6 +524,20 @@ export default {
return this.setShowTreeList({ showTreeList, saving: false }); return this.setShowTreeList({ showTreeList, saving: false });
}, },
async scrollVirtualScrollerToFileHash(hash) {
const index = this.diffFiles.findIndex((f) => f.file_hash === hash);
if (index !== -1) {
this.scrollVirtualScrollerToIndex(index);
}
},
async scrollVirtualScrollerToIndex(index) {
this.virtualScrollCurrentIndex = index;
await this.$nextTick();
this.virtualScrollCurrentIndex = -1;
},
}, },
minTreeWidth: MIN_TREE_WIDTH, minTreeWidth: MIN_TREE_WIDTH,
maxTreeWidth: MAX_TREE_WIDTH, maxTreeWidth: MAX_TREE_WIDTH,
...@@ -572,6 +602,7 @@ export default { ...@@ -572,6 +602,7 @@ export default {
<template v-else-if="renderDiffFiles"> <template v-else-if="renderDiffFiles">
<dynamic-scroller <dynamic-scroller
v-if="isVirtualScrollingEnabled" v-if="isVirtualScrollingEnabled"
ref="virtualScroller"
:items="diffs" :items="diffs"
:min-item-size="70" :min-item-size="70"
:buffer="1000" :buffer="1000"
...@@ -579,7 +610,7 @@ export default { ...@@ -579,7 +610,7 @@ export default {
page-mode page-mode
> >
<template #default="{ item, index, active }"> <template #default="{ item, index, active }">
<dynamic-scroller-item :item="item" :active="active"> <dynamic-scroller-item :item="item" :active="active" :class="{ active }">
<diff-file <diff-file
:file="item" :file="item"
:reviewed="fileReviews[item.id]" :reviewed="fileReviews[item.id]"
...@@ -588,9 +619,29 @@ export default { ...@@ -588,9 +619,29 @@ export default {
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:can-current-user-fork="canCurrentUserFork" :can-current-user-fork="canCurrentUserFork"
:view-diffs-file-by-file="viewDiffsFileByFile" :view-diffs-file-by-file="viewDiffsFileByFile"
:active="active"
/> />
</dynamic-scroller-item> </dynamic-scroller-item>
</template> </template>
<template #before>
<pre-renderer :max-length="diffFilesLength">
<template #default="{ item, index, active }">
<dynamic-scroller-item :item="item" :active="active">
<diff-file
:file="item"
:reviewed="fileReviews[item.id]"
:is-first-file="index === 0"
:is-last-file="index === diffFilesLength - 1"
:help-page-path="helpPagePath"
:can-current-user-fork="canCurrentUserFork"
:view-diffs-file-by-file="viewDiffsFileByFile"
pre-render
/>
</dynamic-scroller-item>
</template>
</pre-renderer>
<virtual-scroller-scroll-sync :index="virtualScrollCurrentIndex" />
</template>
</dynamic-scroller> </dynamic-scroller>
<template v-else> <template v-else>
<diff-file <diff-file
......
...@@ -68,6 +68,16 @@ export default { ...@@ -68,6 +68,16 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
active: {
type: Boolean,
required: false,
default: true,
},
preRender: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -156,6 +166,8 @@ export default { ...@@ -156,6 +166,8 @@ export default {
watch: { watch: {
'file.id': { 'file.id': {
handler: function fileIdHandler() { handler: function fileIdHandler() {
if (this.preRender) return;
this.manageViewedEffects(); this.manageViewedEffects();
}, },
}, },
...@@ -163,7 +175,7 @@ export default { ...@@ -163,7 +175,7 @@ export default {
handler: function hashChangeWatch(newHash, oldHash) { handler: function hashChangeWatch(newHash, oldHash) {
this.isCollapsed = isCollapsed(this.file); this.isCollapsed = isCollapsed(this.file);
if (newHash && oldHash && !this.hasDiff) { if (newHash && oldHash && !this.hasDiff && !this.preRender) {
this.requestDiff(); this.requestDiff();
} }
}, },
...@@ -187,10 +199,14 @@ export default { ...@@ -187,10 +199,14 @@ export default {
}, },
}, },
created() { created() {
if (this.preRender) return;
notesEventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.requestDiff); notesEventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.requestDiff);
eventHub.$on(EVT_EXPAND_ALL_FILES, this.expandAllListener); eventHub.$on(EVT_EXPAND_ALL_FILES, this.expandAllListener);
}, },
mounted() { mounted() {
if (this.preRender) return;
if (this.hasDiff) { if (this.hasDiff) {
this.postRender(); this.postRender();
} }
...@@ -198,6 +214,8 @@ export default { ...@@ -198,6 +214,8 @@ export default {
this.manageViewedEffects(); this.manageViewedEffects();
}, },
beforeDestroy() { beforeDestroy() {
if (this.preRender) return;
eventHub.$off(EVT_EXPAND_ALL_FILES, this.expandAllListener); eventHub.$off(EVT_EXPAND_ALL_FILES, this.expandAllListener);
}, },
methods: { methods: {
...@@ -287,7 +305,7 @@ export default { ...@@ -287,7 +305,7 @@ export default {
<template> <template>
<div <div
:id="file.file_hash" :id="!preRender && active && file.file_hash"
:class="{ :class="{
'is-active': currentDiffFileId === file.file_hash, 'is-active': currentDiffFileId === file.file_hash,
'comments-disabled': Boolean(file.brokenSymlink), 'comments-disabled': Boolean(file.brokenSymlink),
...@@ -330,7 +348,7 @@ export default { ...@@ -330,7 +348,7 @@ export default {
</div> </div>
<template v-else> <template v-else>
<div <div
:id="`diff-content-${file.file_hash}`" :id="!preRender && active && `diff-content-${file.file_hash}`"
:class="hasBodyClasses.contentByHash" :class="hasBodyClasses.contentByHash"
data-testid="content-area" data-testid="content-area"
> >
......
<script>
export default {
inject: ['vscrollParent'],
props: {
maxLength: {
type: Number,
required: true,
},
},
data() {
return {
nextIndex: -1,
nextItem: null,
startedRender: false,
width: 0,
};
},
mounted() {
this.width = this.$el.parentNode.offsetWidth;
this.$_itemsWithSizeWatcher = this.$watch('vscrollParent.itemsWithSize', async () => {
await this.$nextTick();
const nextItem = this.findNextToRender();
if (nextItem) {
this.startedRender = true;
requestIdleCallback(() => {
this.nextItem = nextItem;
});
} else if (this.startedRender) {
this.clearRendering();
}
});
},
beforeDestroy() {
this.$_itemsWithSizeWatcher();
},
methods: {
clearRendering() {
this.nextItem = null;
if (this.maxLength === this.vscrollParent.itemsWithSize.length) {
this.$_itemsWithSizeWatcher();
}
},
findNextToRender() {
return this.vscrollParent.itemsWithSize.find(({ size }, index) => {
const isNext = size === 0;
if (isNext) {
this.nextIndex = index;
}
return isNext;
});
},
},
};
</script>
<template>
<div v-if="nextItem" :style="{ width: `${width}px` }" class="gl-absolute diff-file-offscreen">
<slot
v-bind="{ item: nextItem.item, index: nextIndex, active: true, itemWithSize: nextItem }"
></slot>
</div>
</template>
<style scoped>
.diff-file-offscreen {
top: -200%;
left: -200%;
}
</style>
import { handleLocationHash } from '~/lib/utils/common_utils';
export default {
inject: ['vscrollParent'],
props: {
index: {
type: Number,
required: true,
},
},
watch: {
index: {
handler() {
const { index } = this;
if (index < 0) return;
if (this.vscrollParent.itemsWithSize[index].size) {
this.scrollToIndex(index);
} else {
this.$_itemsWithSizeWatcher = this.$watch('vscrollParent.itemsWithSize', async () => {
await this.$nextTick();
if (this.vscrollParent.itemsWithSize[index].size) {
this.$_itemsWithSizeWatcher();
this.scrollToIndex(index);
await this.$nextTick();
}
});
}
},
immediate: true,
},
},
beforeDestroy() {
if (this.$_itemsWithSizeWatcher) this.$_itemsWithSizeWatcher();
},
methods: {
scrollToIndex(index) {
this.vscrollParent.scrollToItem(index);
setTimeout(() => {
handleLocationHash();
});
},
},
render(h) {
return h(null);
},
};
...@@ -100,7 +100,9 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { ...@@ -100,7 +100,9 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
w: state.showWhitespace ? '0' : '1', w: state.showWhitespace ? '0' : '1',
view: 'inline', view: 'inline',
}; };
const hash = window.location.hash.replace('#', '').split('diff-content-').pop();
let totalLoaded = 0; let totalLoaded = 0;
let scrolledVirtualScroller = false;
commit(types.SET_BATCH_LOADING, true); commit(types.SET_BATCH_LOADING, true);
commit(types.SET_RETRIEVING_BATCHES, true); commit(types.SET_RETRIEVING_BATCHES, true);
...@@ -115,6 +117,15 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { ...@@ -115,6 +117,15 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
commit(types.SET_DIFF_DATA_BATCH, { diff_files }); commit(types.SET_DIFF_DATA_BATCH, { diff_files });
commit(types.SET_BATCH_LOADING, false); commit(types.SET_BATCH_LOADING, false);
if (window.gon?.features?.diffsVirtualScrolling && !scrolledVirtualScroller) {
const index = state.diffFiles.findIndex((f) => f.file_hash === hash);
if (index >= 0) {
eventHub.$emit('scrollToIndex', index);
scrolledVirtualScroller = true;
}
}
if (!isNoteLink && !state.currentDiffFileId) { if (!isNoteLink && !state.currentDiffFileId) {
commit(types.VIEW_DIFF_FILE, diff_files[0].file_hash); commit(types.VIEW_DIFF_FILE, diff_files[0].file_hash);
} }
...@@ -171,7 +182,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { ...@@ -171,7 +182,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
.catch(() => commit(types.SET_RETRIEVING_BATCHES, false)); .catch(() => commit(types.SET_RETRIEVING_BATCHES, false));
return getBatch() return getBatch()
.then(handleLocationHash) .then(() => !window.gon?.features?.diffsVirtualScrolling && handleLocationHash())
.catch(() => null); .catch(() => null);
}; };
...@@ -510,9 +521,18 @@ export const scrollToFile = ({ state, commit }, path) => { ...@@ -510,9 +521,18 @@ export const scrollToFile = ({ state, commit }, path) => {
if (!state.treeEntries[path]) return; if (!state.treeEntries[path]) return;
const { fileHash } = state.treeEntries[path]; const { fileHash } = state.treeEntries[path];
document.location.hash = fileHash;
commit(types.VIEW_DIFF_FILE, fileHash); commit(types.VIEW_DIFF_FILE, fileHash);
if (window.gon?.features?.diffsVirtualScrolling) {
eventHub.$emit('scrollToFileHash', fileHash);
setTimeout(() => {
window.history.replaceState(null, null, `#${fileHash}`);
});
} else {
document.location.hash = fileHash;
}
}; };
export const setShowTreeList = ({ commit }, { showTreeList, saving = true }) => { export const setShowTreeList = ({ commit }, { showTreeList, saving = true }) => {
......
...@@ -381,9 +381,15 @@ function prepareDiffFileLines(file) { ...@@ -381,9 +381,15 @@ function prepareDiffFileLines(file) {
} }
function finalizeDiffFile(file, index) { function finalizeDiffFile(file, index) {
let renderIt = Boolean(window.gon?.features?.diffsVirtualScrolling);
if (!window.gon?.features?.diffsVirtualScrolling) {
renderIt =
index < 3 ? file[INLINE_DIFF_LINES_KEY].length < LINES_TO_BE_RENDERED_DIRECTLY : false;
}
Object.assign(file, { Object.assign(file, {
renderIt: renderIt,
index < 3 ? file[INLINE_DIFF_LINES_KEY].length < LINES_TO_BE_RENDERED_DIRECTLY : false,
isShowingFullFile: false, isShowingFullFile: false,
isLoadingFullFile: false, isLoadingFullFile: false,
discussions: [], discussions: [],
......
import { __ } from '~/locale';
export const HELPER_TEXT_USAGE_PING_DISABLED = __(
'To enable Registration Features, make sure "Enable service ping" is checked.',
);
export const HELPER_TEXT_USAGE_PING_ENABLED = __(
'You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in future, you will also need to register with GitLab via a new cloud licensing service.',
);
function setHelperText(usagePingCheckbox) {
const helperTextId = document.getElementById('usage_ping_features_helper_text');
const usagePingFeaturesLabel = document.getElementById('usage_ping_features_label');
const usagePingFeaturesCheckbox = document.getElementById(
'application_setting_usage_ping_features_enabled',
);
helperTextId.textContent = usagePingCheckbox.checked
? HELPER_TEXT_USAGE_PING_ENABLED
: HELPER_TEXT_USAGE_PING_DISABLED;
usagePingFeaturesLabel.classList.toggle('gl-cursor-not-allowed', !usagePingCheckbox.checked);
usagePingFeaturesCheckbox.disabled = !usagePingCheckbox.checked;
if (!usagePingCheckbox.checked) {
usagePingFeaturesCheckbox.disabled = true;
usagePingFeaturesCheckbox.checked = false;
}
}
export default function initSetHelperText() {
const usagePingCheckbox = document.getElementById('application_setting_usage_ping_enabled');
setHelperText(usagePingCheckbox);
usagePingCheckbox.addEventListener('change', () => {
setHelperText(usagePingCheckbox);
});
}
...@@ -1188,3 +1188,9 @@ table.code { ...@@ -1188,3 +1188,9 @@ table.code {
margin-top: 0; margin-top: 0;
} }
} }
// Note: Prevents tall files from appearing above sticky tabs
.diffs .vue-recycle-scroller__item-view > div:not(.active) {
position: absolute;
top: 100vh;
}
...@@ -331,6 +331,7 @@ module ApplicationSettingsHelper ...@@ -331,6 +331,7 @@ module ApplicationSettingsHelper
:unique_ips_limit_per_user, :unique_ips_limit_per_user,
:unique_ips_limit_time_window, :unique_ips_limit_time_window,
:usage_ping_enabled, :usage_ping_enabled,
:usage_ping_features_enabled,
:user_default_external, :user_default_external,
:user_show_add_ssh_key_message, :user_show_add_ssh_key_message,
:user_default_internal_regex, :user_default_internal_regex,
......
...@@ -377,6 +377,10 @@ module ApplicationSettingImplementation ...@@ -377,6 +377,10 @@ module ApplicationSettingImplementation
Settings.gitlab.usage_ping_enabled Settings.gitlab.usage_ping_enabled
end end
def usage_ping_features_enabled?
usage_ping_enabled? && usage_ping_features_enabled
end
def usage_ping_enabled def usage_ping_enabled
usage_ping_can_be_configured? && super usage_ping_can_be_configured? && super
end end
......
...@@ -230,7 +230,7 @@ module CascadingNamespaceSettingAttribute ...@@ -230,7 +230,7 @@ module CascadingNamespaceSettingAttribute
def namespace_ancestor_ids def namespace_ancestor_ids
strong_memoize(:namespace_ancestor_ids) do strong_memoize(:namespace_ancestor_ids) do
namespace.self_and_ancestors(hierarchy_order: :asc).pluck(:id).reject { |id| id == namespace_id } namespace.ancestor_ids(hierarchy_order: :asc)
end end
end end
......
...@@ -64,6 +64,13 @@ module Namespaces ...@@ -64,6 +64,13 @@ module Namespaces
traversal_ids.present? traversal_ids.present?
end end
def use_traversal_ids_for_ancestors?
return false unless use_traversal_ids?
return false unless Feature.enabled?(:use_traversal_ids_for_ancestors, root_ancestor, default_enabled: :yaml)
traversal_ids.present?
end
def root_ancestor def root_ancestor
return super if parent.nil? return super if parent.nil?
return super unless persisted? return super unless persisted?
...@@ -95,14 +102,33 @@ module Namespaces ...@@ -95,14 +102,33 @@ module Namespaces
end end
def ancestors(hierarchy_order: nil) def ancestors(hierarchy_order: nil)
return super() unless use_traversal_ids? return super unless use_traversal_ids_for_ancestors?
return super() unless Feature.enabled?(:use_traversal_ids_for_ancestors, root_ancestor, default_enabled: :yaml)
return self.class.none if parent_id.blank? return self.class.none if parent_id.blank?
lineage(bottom: parent, hierarchy_order: hierarchy_order) lineage(bottom: parent, hierarchy_order: hierarchy_order)
end end
def ancestor_ids(hierarchy_order: nil)
return super unless use_traversal_ids_for_ancestors?
hierarchy_order == :desc ? traversal_ids[0..-2] : traversal_ids[0..-2].reverse
end
def self_and_ancestors(hierarchy_order: nil)
return super unless use_traversal_ids_for_ancestors?
return self.class.where(id: id) if parent_id.blank?
lineage(bottom: self, hierarchy_order: hierarchy_order)
end
def self_and_ancestor_ids(hierarchy_order: nil)
return super unless use_traversal_ids_for_ancestors?
hierarchy_order == :desc ? traversal_ids : traversal_ids.reverse
end
private private
# Update the traversal_ids for the full hierarchy. # Update the traversal_ids for the full hierarchy.
......
...@@ -10,7 +10,7 @@ module Namespaces ...@@ -10,7 +10,7 @@ module Namespaces
if persisted? if persisted?
strong_memoize(:root_ancestor) do strong_memoize(:root_ancestor) do
self_and_ancestors.reorder(nil).find_by(parent_id: nil) recursive_self_and_ancestors.reorder(nil).find_by(parent_id: nil)
end end
else else
parent.root_ancestor parent.root_ancestor
...@@ -26,14 +26,19 @@ module Namespaces ...@@ -26,14 +26,19 @@ module Namespaces
alias_method :recursive_self_and_hierarchy, :self_and_hierarchy alias_method :recursive_self_and_hierarchy, :self_and_hierarchy
# Returns all the ancestors of the current namespaces. # Returns all the ancestors of the current namespaces.
def ancestors def ancestors(hierarchy_order: nil)
return self.class.none unless parent_id return self.class.none unless parent_id
object_hierarchy(self.class.where(id: parent_id)) object_hierarchy(self.class.where(id: parent_id))
.base_and_ancestors .base_and_ancestors(hierarchy_order: hierarchy_order)
end end
alias_method :recursive_ancestors, :ancestors alias_method :recursive_ancestors, :ancestors
def ancestor_ids(hierarchy_order: nil)
recursive_ancestors(hierarchy_order: hierarchy_order).pluck(:id)
end
alias_method :recursive_ancestor_ids, :ancestor_ids
# returns all ancestors upto but excluding the given namespace # returns all ancestors upto but excluding the given namespace
# when no namespace is given, all ancestors upto the top are returned # when no namespace is given, all ancestors upto the top are returned
def ancestors_upto(top = nil, hierarchy_order: nil) def ancestors_upto(top = nil, hierarchy_order: nil)
...@@ -49,6 +54,11 @@ module Namespaces ...@@ -49,6 +54,11 @@ module Namespaces
end end
alias_method :recursive_self_and_ancestors, :self_and_ancestors alias_method :recursive_self_and_ancestors, :self_and_ancestors
def self_and_ancestor_ids(hierarchy_order: nil)
recursive_self_and_ancestors(hierarchy_order: hierarchy_order).pluck(:id)
end
alias_method :recursive_self_and_ancestor_ids, :self_and_ancestor_ids
# Returns all the descendants of the current namespace. # Returns all the descendants of the current namespace.
def descendants def descendants
object_hierarchy(self.class.where(parent_id: id)) object_hierarchy(self.class.where(parent_id: id))
...@@ -63,7 +73,7 @@ module Namespaces ...@@ -63,7 +73,7 @@ module Namespaces
alias_method :recursive_self_and_descendants, :self_and_descendants alias_method :recursive_self_and_descendants, :self_and_descendants
def self_and_descendant_ids def self_and_descendant_ids
self_and_descendants.select(:id) recursive_self_and_descendants.select(:id)
end end
alias_method :recursive_self_and_descendant_ids, :self_and_descendant_ids alias_method :recursive_self_and_descendant_ids, :self_and_descendant_ids
......
...@@ -35,5 +35,26 @@ ...@@ -35,5 +35,26 @@
- deactivating_service_ping_path = help_page_path('development/usage_ping/index.md', anchor: 'disable-usage-ping') - deactivating_service_ping_path = help_page_path('development/usage_ping/index.md', anchor: 'disable-usage-ping')
- deactivating_service_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_service_ping_path } - deactivating_service_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_service_ping_path }
= s_('For more information, see the documentation on %{deactivating_service_ping_link_start}deactivating service ping%{deactivating_service_ping_link_end}.').html_safe % { deactivating_service_ping_link_start: deactivating_service_ping_link_start, deactivating_service_ping_link_end: '</a>'.html_safe } = s_('For more information, see the documentation on %{deactivating_service_ping_link_start}deactivating service ping%{deactivating_service_ping_link_end}.').html_safe % { deactivating_service_ping_link_start: deactivating_service_ping_link_start, deactivating_service_ping_link_end: '</a>'.html_safe }
.form-group
- usage_ping_enabled = @application_setting.usage_ping_enabled?
.form-check
= f.check_box :usage_ping_features_enabled?, disabled: !usage_ping_enabled, class: 'form-check-input'
= f.label :usage_ping_features_enabled?, class: 'form-check-label gl-cursor-not-allowed', id: 'usage_ping_features_label' do
= _('Enable Registration Features')
= link_to sprite_icon('question-o'), help_page_path('development/usage_ping/index.md', anchor: 'registration-features-program')
.form-text.text-muted
- if usage_ping_enabled
%p.gl-mb-3.text-muted{ id: 'usage_ping_features_helper_text' }= _('You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in future, you will also need to register with GitLab via a new cloud licensing service.')
- else
%p.gl-mb-3.text-muted{ id: 'usage_ping_features_helper_text' }= _('To enable Registration Features, make sure "Enable service ping" is checked.')
%p.gl-mb-3.text-muted= _('Registration Features include:')
.form-text
- email_from_gitlab_path = help_page_path('tools/email.md')
- link_end = '</a>'.html_safe
- email_from_gitlab_link = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: email_from_gitlab_path }
%ul
%li
= _('Email from GitLab - email users right from the Admin Area. %{link_start}Learn more%{link_end}.').html_safe % { link_start: email_from_gitlab_link, link_end: link_end }
= f.submit _('Save changes'), class: "gl-button btn btn-confirm" = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
# frozen_string_literal: true
class AddUsagePingFeaturesEnabledToApplicationSettings < ActiveRecord::Migration[6.1]
def up
add_column :application_settings, :usage_ping_features_enabled, :boolean, default: false, null: false
end
def down
remove_column :application_settings, :usage_ping_features_enabled
end
end
1a0df6210d9ee0e0229f3cdf3e95acaaa47ebf4ca31ac0fd9f57255115355f99
\ No newline at end of file
...@@ -9525,6 +9525,7 @@ CREATE TABLE application_settings ( ...@@ -9525,6 +9525,7 @@ CREATE TABLE application_settings (
encrypted_mailgun_signing_key bytea, encrypted_mailgun_signing_key bytea,
encrypted_mailgun_signing_key_iv bytea, encrypted_mailgun_signing_key_iv bytea,
mailgun_events_enabled boolean DEFAULT false NOT NULL, mailgun_events_enabled boolean DEFAULT false NOT NULL,
usage_ping_features_enabled boolean DEFAULT false NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)), CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)),
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)), CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
...@@ -282,7 +282,7 @@ class License < ApplicationRecord ...@@ -282,7 +282,7 @@ class License < ApplicationRecord
end end
def features_with_usage_ping def features_with_usage_ping
return FEATURES_WITH_USAGE_PING if Gitlab::CurrentSettings.usage_ping_enabled? return FEATURES_WITH_USAGE_PING if Gitlab::CurrentSettings.usage_ping_features_enabled?
[] []
end end
......
...@@ -20,7 +20,11 @@ module RequirementsManagement ...@@ -20,7 +20,11 @@ module RequirementsManagement
belongs_to :author, inverse_of: :requirements, class_name: 'User' belongs_to :author, inverse_of: :requirements, class_name: 'User'
belongs_to :project, inverse_of: :requirements belongs_to :project, inverse_of: :requirements
belongs_to :requirement_issue, class_name: 'Issue', foreign_key: :issue_id # deleting an issue would result in deleting requirement record due to cascade delete via foreign key
# but to sync the other way around, we require a temporary `dependent: :destroy`
# See https://gitlab.com/gitlab-org/gitlab/-/issues/323779 for details.
# This will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/329432
belongs_to :requirement_issue, class_name: 'Issue', foreign_key: :issue_id, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
validates :issue_id, uniqueness: true, allow_nil: true validates :issue_id, uniqueness: true, allow_nil: true
......
...@@ -31,7 +31,7 @@ RSpec.describe Admin::EmailsController, :clean_gitlab_redis_shared_state do ...@@ -31,7 +31,7 @@ RSpec.describe Admin::EmailsController, :clean_gitlab_redis_shared_state do
context 'when `send_emails_from_admin_area` feature is disabled' do context 'when `send_emails_from_admin_area` feature is disabled' do
before do before do
stub_licensed_features(send_emails_from_admin_area: false) stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(false) stub_application_setting(usage_ping_enabled: false)
end end
it 'returns 404' do it 'returns 404' do
...@@ -44,10 +44,20 @@ RSpec.describe Admin::EmailsController, :clean_gitlab_redis_shared_state do ...@@ -44,10 +44,20 @@ RSpec.describe Admin::EmailsController, :clean_gitlab_redis_shared_state do
context 'when usage ping is enabled' do context 'when usage ping is enabled' do
before do before do
stub_licensed_features(send_emails_from_admin_area: false) stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(true) stub_application_setting(usage_ping_enabled: true)
end end
it 'responds with 200' do it 'responds 404 when feature is not activated' do
stub_application_setting(usage_ping_features_enabled: false)
subject
expect(response).to have_gitlab_http_status(:not_found)
end
it 'responds with 200 when feature is activated' do
stub_application_setting(usage_ping_features_enabled: true)
subject subject
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
...@@ -136,7 +146,7 @@ RSpec.describe Admin::EmailsController, :clean_gitlab_redis_shared_state do ...@@ -136,7 +146,7 @@ RSpec.describe Admin::EmailsController, :clean_gitlab_redis_shared_state do
context 'when `send_emails_from_admin_area` feature is disabled' do context 'when `send_emails_from_admin_area` feature is disabled' do
before do before do
stub_licensed_features(send_emails_from_admin_area: false) stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(false) stub_application_setting(usage_ping_enabled: false)
end end
it 'does not trigger the service to send emails' do it 'does not trigger the service to send emails' do
...@@ -155,23 +165,47 @@ RSpec.describe Admin::EmailsController, :clean_gitlab_redis_shared_state do ...@@ -155,23 +165,47 @@ RSpec.describe Admin::EmailsController, :clean_gitlab_redis_shared_state do
context 'when usage ping is enabled' do context 'when usage ping is enabled' do
before do before do
stub_licensed_features(send_emails_from_admin_area: false) stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(true) stub_application_setting(usage_ping_enabled: true)
end end
it 'triggers the service to send emails' do context 'when feature is activated' do
expect_next_instance_of(Admin::EmailService, recipients, email_subject, body) do |email_service| before do
expect(email_service).to receive(:execute) stub_application_setting(usage_ping_features_enabled: true)
end end
subject it 'triggers the service to send emails' do
expect_next_instance_of(Admin::EmailService, recipients, email_subject, body) do |email_service|
expect(email_service).to receive(:execute)
end
subject
end
it 'redirects to `admin_email_path` with success notice' do
subject
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(admin_email_path)
expect(flash[:notice]).to eq('Email sent')
end
end end
it 'redirects to `admin_email_path` with success notice' do context 'when feature is deactivated' do
subject before do
stub_application_setting(usage_ping_features_enabled: false)
end
expect(response).to have_gitlab_http_status(:found) it 'does not trigger the service to send emails' do
expect(response).to redirect_to(admin_email_path) expect(Admin::EmailService).not_to receive(:new)
expect(flash[:notice]).to eq('Email sent')
subject
end
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end end
end end
end end
......
...@@ -14,7 +14,7 @@ RSpec.describe 'Admin::Emails', :clean_gitlab_redis_shared_state do ...@@ -14,7 +14,7 @@ RSpec.describe 'Admin::Emails', :clean_gitlab_redis_shared_state do
context 'when `send_emails_from_admin_area` feature is not licensed' do context 'when `send_emails_from_admin_area` feature is not licensed' do
before do before do
stub_licensed_features(send_emails_from_admin_area: false) stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(false) stub_application_setting(usage_ping_enabled: false)
end end
it 'returns 404' do it 'returns 404' do
...@@ -27,13 +27,31 @@ RSpec.describe 'Admin::Emails', :clean_gitlab_redis_shared_state do ...@@ -27,13 +27,31 @@ RSpec.describe 'Admin::Emails', :clean_gitlab_redis_shared_state do
context 'when usage ping is enabled' do context 'when usage ping is enabled' do
before do before do
stub_licensed_features(send_emails_from_admin_area: false) stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(true) stub_application_setting(usage_ping_enabled: true)
end end
it 'returns 200' do context 'when feature is activated' do
visit admin_email_path before do
stub_application_setting(usage_ping_features_enabled: true)
end
it 'returns 200' do
visit admin_email_path
expect(page.status_code).to eq(200)
end
end
context 'when feature is deactivated' do
before do
stub_application_setting(usage_ping_features_enabled: false)
end
expect(page.status_code).to eq(200) it 'returns 404' do
visit admin_email_path
expect(page.status_code).to eq(404)
end
end end
end end
......
...@@ -28,7 +28,7 @@ RSpec.describe "Admin::Users", :js do ...@@ -28,7 +28,7 @@ RSpec.describe "Admin::Users", :js do
context 'when `send_emails_from_admin_area` feature is disabled' do context 'when `send_emails_from_admin_area` feature is disabled' do
before do before do
stub_licensed_features(send_emails_from_admin_area: false) stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(false) stub_application_setting(usage_ping_enabled: false)
end end
it "does not show the 'Send email to users' link" do it "does not show the 'Send email to users' link" do
...@@ -41,13 +41,31 @@ RSpec.describe "Admin::Users", :js do ...@@ -41,13 +41,31 @@ RSpec.describe "Admin::Users", :js do
context 'when usage ping is enabled' do context 'when usage ping is enabled' do
before do before do
stub_licensed_features(send_emails_from_admin_area: false) stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(true) stub_application_setting(usage_ping_enabled: true)
end end
it "shows the 'Send email to users' link" do context 'when feature is activated' do
visit admin_users_path before do
stub_application_setting(usage_ping_features_enabled: true)
end
expect(page).to have_link(href: admin_email_path) it "shows the 'Send email to users' link" do
visit admin_users_path
expect(page).to have_link(href: admin_email_path)
end
end
context 'when feature is disabled' do
before do
stub_application_setting(usage_ping_features_enabled: false)
end
it "does not show the 'Send email to users' link" do
visit admin_users_path
expect(page).not_to have_link(href: admin_email_path)
end
end end
end end
end end
......
...@@ -22,7 +22,7 @@ RSpec.describe Admin::EmailsHelper, :clean_gitlab_redis_shared_state do ...@@ -22,7 +22,7 @@ RSpec.describe Admin::EmailsHelper, :clean_gitlab_redis_shared_state do
context 'when `send_emails_from_admin_area` feature is disabled' do context 'when `send_emails_from_admin_area` feature is disabled' do
before do before do
stub_licensed_features(send_emails_from_admin_area: false) stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(false) stub_application_setting(usage_ping_enabled: false)
end end
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
...@@ -31,11 +31,27 @@ RSpec.describe Admin::EmailsHelper, :clean_gitlab_redis_shared_state do ...@@ -31,11 +31,27 @@ RSpec.describe Admin::EmailsHelper, :clean_gitlab_redis_shared_state do
context 'when usage ping is enabled' do context 'when usage ping is enabled' do
before do before do
stub_licensed_features(send_emails_from_admin_area: false) stub_licensed_features(send_emails_from_admin_area: false)
allow(Gitlab::CurrentSettings).to receive(:usage_ping_enabled?).and_return(true) stub_application_setting(usage_ping_enabled: true)
end end
it 'returns true' do context 'when feature is activated' do
expect(subject).to eq(true) before do
stub_application_setting(usage_ping_features_enabled: true)
end
it 'returns true' do
expect(subject).to eq(true)
end
end
context 'when feature is deactivated' do
before do
stub_application_setting(usage_ping_features_enabled: false)
end
it 'returns false' do
expect(subject).to eq(false)
end
end end
end end
end end
......
...@@ -184,4 +184,21 @@ RSpec.describe RequirementsManagement::Requirement do ...@@ -184,4 +184,21 @@ RSpec.describe RequirementsManagement::Requirement do
end end
end end
end end
describe 'sync with requirement issues' do
let_it_be_with_reload(:requirement) { create(:requirement) }
let_it_be_with_reload(:requirement_issue) { create(:requirement_issue, requirement: requirement) }
context 'when destroying a requirement' do
it 'also destroys the associated requirement issue' do
expect { requirement.destroy! }.to change { Issue.where(issue_type: 'requirement').count }.by(-1)
end
end
context 'when destroying a requirement issue' do
it 'also destroys the associated requirement' do
expect { requirement_issue.destroy! }.to change { RequirementsManagement::Requirement.count }.by(-1)
end
end
end
end end
...@@ -11809,6 +11809,9 @@ msgstr "" ...@@ -11809,6 +11809,9 @@ msgstr ""
msgid "Email display name" msgid "Email display name"
msgstr "" msgstr ""
msgid "Email from GitLab - email users right from the Admin Area. %{link_start}Learn more%{link_end}."
msgstr ""
msgid "Email not verified. Please verify your email in Salesforce." msgid "Email not verified. Please verify your email in Salesforce."
msgstr "" msgstr ""
...@@ -11950,6 +11953,9 @@ msgstr "" ...@@ -11950,6 +11953,9 @@ msgstr ""
msgid "Enable Pseudonymizer data collection" msgid "Enable Pseudonymizer data collection"
msgstr "" msgstr ""
msgid "Enable Registration Features"
msgstr ""
msgid "Enable Repository Checks" msgid "Enable Repository Checks"
msgstr "" msgstr ""
...@@ -26848,6 +26854,9 @@ msgstr "" ...@@ -26848,6 +26854,9 @@ msgstr ""
msgid "Register with two-factor app" msgid "Register with two-factor app"
msgstr "" msgstr ""
msgid "Registration Features include:"
msgstr ""
msgid "Registration|Checkout" msgid "Registration|Checkout"
msgstr "" msgstr ""
...@@ -33913,6 +33922,9 @@ msgstr "" ...@@ -33913,6 +33922,9 @@ msgstr ""
msgid "To define internal users, first enable new users set to external" msgid "To define internal users, first enable new users set to external"
msgstr "" msgstr ""
msgid "To enable Registration Features, make sure \"Enable service ping\" is checked."
msgstr ""
msgid "To ensure no loss of personal content, this account should only be used for matters related to %{group_name}." msgid "To ensure no loss of personal content, this account should only be used for matters related to %{group_name}."
msgstr "" msgstr ""
...@@ -37086,6 +37098,9 @@ msgstr "" ...@@ -37086,6 +37098,9 @@ msgstr ""
msgid "You can easily contribute to them by requesting to join these groups." msgid "You can easily contribute to them by requesting to join these groups."
msgstr "" msgstr ""
msgid "You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in future, you will also need to register with GitLab via a new cloud licensing service."
msgstr ""
msgid "You can enable project access token creation in %{link_start}group settings%{link_end}." msgid "You can enable project access token creation in %{link_start}group settings%{link_end}."
msgstr "" msgstr ""
......
...@@ -34,4 +34,12 @@ RSpec.describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', ty ...@@ -34,4 +34,12 @@ RSpec.describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', ty
expect(response).to be_successful expect(response).to be_successful
end end
it 'application_settings/usage.html' do
stub_application_setting(usage_ping_enabled: false)
get :metrics_and_profiling
expect(response).to be_successful
end
end end
import initSetHelperText, {
HELPER_TEXT_USAGE_PING_DISABLED,
HELPER_TEXT_USAGE_PING_ENABLED,
} from '~/pages/admin/application_settings/metrics_and_profiling/usage_statistics';
describe('UsageStatistics', () => {
const FIXTURE = 'application_settings/usage.html';
let usagePingCheckBox;
let usagePingFeaturesCheckBox;
let usagePingFeaturesLabel;
let usagePingFeaturesHelperText;
beforeEach(() => {
loadFixtures(FIXTURE);
initSetHelperText();
usagePingCheckBox = document.getElementById('application_setting_usage_ping_enabled');
usagePingFeaturesCheckBox = document.getElementById(
'application_setting_usage_ping_features_enabled',
);
usagePingFeaturesLabel = document.getElementById('usage_ping_features_label');
usagePingFeaturesHelperText = document.getElementById('usage_ping_features_helper_text');
});
const expectEnabledUsagePingFeaturesCheckBox = () => {
expect(usagePingFeaturesCheckBox.classList.contains('gl-cursor-not-allowed')).toBe(false);
expect(usagePingFeaturesHelperText.textContent).toEqual(HELPER_TEXT_USAGE_PING_ENABLED);
};
const expectDisabledUsagePingFeaturesCheckBox = () => {
expect(usagePingFeaturesLabel.classList.contains('gl-cursor-not-allowed')).toBe(true);
expect(usagePingFeaturesHelperText.textContent).toEqual(HELPER_TEXT_USAGE_PING_DISABLED);
};
describe('Registration Features checkbox', () => {
it('is disabled when Usage Ping checkbox is unchecked', () => {
expect(usagePingCheckBox.checked).toBe(false);
expectDisabledUsagePingFeaturesCheckBox();
});
it('is enabled when Usage Ping checkbox is checked', () => {
usagePingCheckBox.click();
expect(usagePingCheckBox.checked).toBe(true);
expectEnabledUsagePingFeaturesCheckBox();
});
it('is switched to disabled when Usage Ping checkbox is unchecked ', () => {
usagePingCheckBox.click();
usagePingFeaturesCheckBox.click();
expectEnabledUsagePingFeaturesCheckBox();
usagePingCheckBox.click();
expect(usagePingCheckBox.checked).toBe(false);
expect(usagePingFeaturesCheckBox.checked).toBe(false);
expectDisabledUsagePingFeaturesCheckBox();
});
});
});
...@@ -996,6 +996,36 @@ RSpec.describe Namespace do ...@@ -996,6 +996,36 @@ RSpec.describe Namespace do
end end
end end
describe '#use_traversal_ids_for_ancestors?' do
let_it_be(:namespace, reload: true) { create(:namespace) }
subject { namespace.use_traversal_ids_for_ancestors? }
context 'when use_traversal_ids_for_ancestors? feature flag is true' do
before do
stub_feature_flags(use_traversal_ids_for_ancestors: true)
end
it { is_expected.to eq true }
end
context 'when use_traversal_ids_for_ancestors? feature flag is false' do
before do
stub_feature_flags(use_traversal_ids_for_ancestors: false)
end
it { is_expected.to eq false }
end
context 'when use_traversal_ids? feature flag is false' do
before do
stub_feature_flags(use_traversal_ids: false)
end
it { is_expected.to eq false }
end
end
describe '#users_with_descendants' do describe '#users_with_descendants' do
let(:user_a) { create(:user) } let(:user_a) { create(:user) }
let(:user_b) { create(:user) } let(:user_b) { create(:user) }
......
...@@ -13,10 +13,22 @@ RSpec.shared_examples 'a cascading setting' do ...@@ -13,10 +13,22 @@ RSpec.shared_examples 'a cascading setting' do
click_save_button click_save_button
end end
it 'disables setting in subgroups' do shared_examples 'subgroup settings are disabled' do
visit subgroup_path it 'disables setting in subgroups' do
visit subgroup_path
expect(find("#{setting_field_selector}[disabled]")).to be_checked
end
end
include_examples 'subgroup settings are disabled'
context 'when use_traversal_ids_for_ancestors is disabled' do
before do
stub_feature_flags(use_traversal_ids_for_ancestors: false)
end
expect(find("#{setting_field_selector}[disabled]")).to be_checked include_examples 'subgroup settings are disabled'
end end
it 'does not show enforcement checkbox in subgroups' do it 'does not show enforcement checkbox in subgroups' do
......
...@@ -12,16 +12,18 @@ RSpec.shared_examples 'namespace traversal' do ...@@ -12,16 +12,18 @@ RSpec.shared_examples 'namespace traversal' do
it "makes a recursive query" do it "makes a recursive query" do
groups.each do |group| groups.each do |group|
expect { group.public_send(recursive_method).load }.to make_queries_matching(/WITH RECURSIVE/) expect { group.public_send(recursive_method).try(:load) }.to make_queries_matching(/WITH RECURSIVE/)
end end
end end
end end
describe '#root_ancestor' do let_it_be(:group) { create(:group) }
let_it_be(:group) { create(:group) } let_it_be(:nested_group) { create(:group, parent: group) }
let_it_be(:nested_group) { create(:group, parent: group) } let_it_be(:deep_nested_group) { create(:group, parent: nested_group) }
let_it_be(:deep_nested_group) { create(:group, parent: nested_group) } let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
let_it_be(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] }
describe '#root_ancestor' do
it 'returns the correct root ancestor' do it 'returns the correct root ancestor' do
expect(group.root_ancestor).to eq(group) expect(group.root_ancestor).to eq(group)
expect(nested_group.root_ancestor).to eq(group) expect(nested_group.root_ancestor).to eq(group)
...@@ -29,8 +31,6 @@ RSpec.shared_examples 'namespace traversal' do ...@@ -29,8 +31,6 @@ RSpec.shared_examples 'namespace traversal' do
end end
describe '#recursive_root_ancestor' do describe '#recursive_root_ancestor' do
let(:groups) { [group, nested_group, deep_nested_group] }
it "is equivalent to #recursive_root_ancestor" do it "is equivalent to #recursive_root_ancestor" do
groups.each do |group| groups.each do |group|
expect(group.root_ancestor).to eq(group.recursive_root_ancestor) expect(group.root_ancestor).to eq(group.recursive_root_ancestor)
...@@ -40,12 +40,8 @@ RSpec.shared_examples 'namespace traversal' do ...@@ -40,12 +40,8 @@ RSpec.shared_examples 'namespace traversal' do
end end
describe '#self_and_hierarchy' do describe '#self_and_hierarchy' do
let!(:group) { create(:group, path: 'git_lab') } let!(:another_group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) } let!(:another_group_nested) { create(:group, parent: another_group) }
let!(:deep_nested_group) { create(:group, parent: nested_group) }
let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
let!(:another_group) { create(:group, path: 'gitllab') }
let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
it 'returns the correct tree' do it 'returns the correct tree' do
expect(group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) expect(group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
...@@ -54,18 +50,11 @@ RSpec.shared_examples 'namespace traversal' do ...@@ -54,18 +50,11 @@ RSpec.shared_examples 'namespace traversal' do
end end
describe '#recursive_self_and_hierarchy' do describe '#recursive_self_and_hierarchy' do
let(:groups) { [group, nested_group, very_deep_nested_group] }
it_behaves_like 'recursive version', :self_and_hierarchy it_behaves_like 'recursive version', :self_and_hierarchy
end end
end end
describe '#ancestors' do describe '#ancestors' do
let_it_be(:group) { create(:group) }
let_it_be(:nested_group) { create(:group, parent: group) }
let_it_be(:deep_nested_group) { create(:group, parent: nested_group) }
let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
it 'returns the correct ancestors' do it 'returns the correct ancestors' do
# #reload is called to make sure traversal_ids are reloaded # #reload is called to make sure traversal_ids are reloaded
expect(very_deep_nested_group.reload.ancestors).to contain_exactly(group, nested_group, deep_nested_group) expect(very_deep_nested_group.reload.ancestors).to contain_exactly(group, nested_group, deep_nested_group)
...@@ -75,18 +64,28 @@ RSpec.shared_examples 'namespace traversal' do ...@@ -75,18 +64,28 @@ RSpec.shared_examples 'namespace traversal' do
end end
describe '#recursive_ancestors' do describe '#recursive_ancestors' do
let(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] }
it_behaves_like 'recursive version', :ancestors it_behaves_like 'recursive version', :ancestors
end end
end end
describe '#self_and_ancestors' do describe '#ancestor_ids' do
let(:group) { create(:group) } it 'returns the correct ancestor ids' do
let(:nested_group) { create(:group, parent: group) } expect(very_deep_nested_group.ancestor_ids).to contain_exactly(group.id, nested_group.id, deep_nested_group.id)
let(:deep_nested_group) { create(:group, parent: nested_group) } expect(deep_nested_group.ancestor_ids).to contain_exactly(group.id, nested_group.id)
let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } expect(nested_group.ancestor_ids).to contain_exactly(group.id)
expect(group.ancestor_ids).to be_empty
end
describe '#recursive_ancestor_ids' do
let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] }
it_behaves_like 'recursive version', :ancestor_ids
end
end
describe '#self_and_ancestors' do
it 'returns the correct ancestors' do it 'returns the correct ancestors' do
expect(very_deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) expect(very_deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
expect(deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group) expect(deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group)
...@@ -95,19 +94,30 @@ RSpec.shared_examples 'namespace traversal' do ...@@ -95,19 +94,30 @@ RSpec.shared_examples 'namespace traversal' do
end end
describe '#recursive_self_and_ancestors' do describe '#recursive_self_and_ancestors' do
let(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] }
it_behaves_like 'recursive version', :self_and_ancestors it_behaves_like 'recursive version', :self_and_ancestors
end end
end end
describe '#self_and_ancestor_ids' do
it 'returns the correct ancestor ids' do
expect(very_deep_nested_group.self_and_ancestor_ids).to contain_exactly(group.id, nested_group.id, deep_nested_group.id, very_deep_nested_group.id)
expect(deep_nested_group.self_and_ancestor_ids).to contain_exactly(group.id, nested_group.id, deep_nested_group.id)
expect(nested_group.self_and_ancestor_ids).to contain_exactly(group.id, nested_group.id)
expect(group.self_and_ancestor_ids).to contain_exactly(group.id)
end
describe '#recursive_self_and_ancestor_ids' do
let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] }
it_behaves_like 'recursive version', :self_and_ancestor_ids
end
end
describe '#descendants' do describe '#descendants' do
let!(:group) { create(:group, path: 'git_lab') } let!(:another_group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) } let!(:another_group_nested) { create(:group, parent: another_group) }
let!(:deep_nested_group) { create(:group, parent: nested_group) }
let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
let!(:another_group) { create(:group, path: 'gitllab') }
let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
it 'returns the correct descendants' do it 'returns the correct descendants' do
expect(very_deep_nested_group.descendants.to_a).to eq([]) expect(very_deep_nested_group.descendants.to_a).to eq([])
...@@ -117,19 +127,13 @@ RSpec.shared_examples 'namespace traversal' do ...@@ -117,19 +127,13 @@ RSpec.shared_examples 'namespace traversal' do
end end
describe '#recursive_descendants' do describe '#recursive_descendants' do
let(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] }
it_behaves_like 'recursive version', :descendants it_behaves_like 'recursive version', :descendants
end end
end end
describe '#self_and_descendants' do describe '#self_and_descendants' do
let!(:group) { create(:group, path: 'git_lab') } let!(:another_group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) } let!(:another_group_nested) { create(:group, parent: another_group) }
let!(:deep_nested_group) { create(:group, parent: nested_group) }
let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
let!(:another_group) { create(:group, path: 'gitllab') }
let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
it 'returns the correct descendants' do it 'returns the correct descendants' do
expect(very_deep_nested_group.self_and_descendants).to contain_exactly(very_deep_nested_group) expect(very_deep_nested_group.self_and_descendants).to contain_exactly(very_deep_nested_group)
...@@ -139,24 +143,18 @@ RSpec.shared_examples 'namespace traversal' do ...@@ -139,24 +143,18 @@ RSpec.shared_examples 'namespace traversal' do
end end
describe '#recursive_self_and_descendants' do describe '#recursive_self_and_descendants' do
let(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] } let_it_be(:groups) { [group, nested_group, deep_nested_group] }
it_behaves_like 'recursive version', :self_and_descendants it_behaves_like 'recursive version', :self_and_descendants
end end
end end
describe '#self_and_descendant_ids' do describe '#self_and_descendant_ids' do
let!(:group) { create(:group, path: 'git_lab') }
let!(:nested_group) { create(:group, parent: group) }
let!(:deep_nested_group) { create(:group, parent: nested_group) }
subject { group.self_and_descendant_ids.pluck(:id) } subject { group.self_and_descendant_ids.pluck(:id) }
it { is_expected.to contain_exactly(group.id, nested_group.id, deep_nested_group.id) } it { is_expected.to contain_exactly(group.id, nested_group.id, deep_nested_group.id, very_deep_nested_group.id) }
describe '#recursive_self_and_descendant_ids' do describe '#recursive_self_and_descendant_ids' do
let(:groups) { [group, nested_group, deep_nested_group] }
it_behaves_like 'recursive version', :self_and_descendant_ids it_behaves_like 'recursive version', :self_and_descendant_ids
end end
end end
......
...@@ -195,14 +195,15 @@ export default { ...@@ -195,14 +195,15 @@ export default {
observeSize () { observeSize () {
if (!this.vscrollResizeObserver) return if (!this.vscrollResizeObserver) return
this.vscrollResizeObserver.observe(this.$el.parentNode) this.$_parentNode = this.$el.parentNode;
this.$el.parentNode.addEventListener('resize', this.onResize) this.vscrollResizeObserver.observe(this.$_parentNode)
this.$_parentNode.addEventListener('resize', this.onResize)
}, },
unobserveSize () { unobserveSize () {
if (!this.vscrollResizeObserver) return if (!this.vscrollResizeObserver) return
this.vscrollResizeObserver.unobserve(this.$el.parentNode) this.vscrollResizeObserver.unobserve(this.$_parentNode)
this.$el.parentNode.removeEventListener('resize', this.onResize) this.$_parentNode.removeEventListener('resize', this.onResize)
}, },
onResize (event) { onResize (event) {
......
...@@ -572,20 +572,49 @@ export default { ...@@ -572,20 +572,49 @@ export default {
}, },
scrollToItem (index) { scrollToItem (index) {
let scroll this.$_scrollDirty = true
if (this.itemSize === null) { const { viewport, scrollDirection, scrollDistance } = this.scrollToPosition(index)
scroll = index > 0 ? this.sizes[index - 1].accumulator : 0 viewport[scrollDirection] = scrollDistance
} else {
scroll = index * this.itemSize setTimeout(() => {
} this.$_scrollDirty = false
this.scrollToPosition(scroll) this.updateVisibleItems(false, true)
})
}, },
scrollToPosition (position) { scrollToPosition (index) {
if (this.direction === 'vertical') { const getPositionOfItem = (index) => {
this.$el.scrollTop = position if (this.itemSize === null) {
} else { return index > 0 ? this.sizes[index - 1].accumulator : 0
this.$el.scrollLeft = position } else {
return index * this.itemSize
}
}
const position = getPositionOfItem(index)
const direction = this.direction === 'vertical'
? { scroll: 'scrollTop', start: 'top' }
: { scroll: 'scrollLeft', start: 'left' }
if (this.pageMode) {
const viewportEl = ScrollParent(this.$el)
// HTML doesn't overflow like other elements
const scrollTop = viewportEl.tagName === 'HTML' ? 0 : viewportEl[direction.scroll]
const viewport = viewportEl.getBoundingClientRect()
const scroller = this.$el.getBoundingClientRect()
const scrollerPosition = scroller[direction.start] - viewport[direction.start]
return {
viewport: viewportEl,
scrollDirection: direction.scroll,
scrollDistance: position + scrollTop + scrollerPosition,
}
}
return {
viewport: this.$el,
scrollDirection: direction.scroll,
scrollDistance: position,
} }
}, },
......
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