Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
28fd41cf28
commit
d08bee6aaf
|
@ -1,3 +1,4 @@
|
|||
import initSetHelperText from '~/pages/admin/application_settings/metrics_and_profiling/usage_statistics';
|
||||
import PayloadPreviewer from '~/pages/admin/application_settings/payload_previewer';
|
||||
|
||||
export default () => {
|
||||
|
@ -5,3 +6,5 @@ export default () => {
|
|||
new PayloadPreviewer(trigger).init();
|
||||
});
|
||||
};
|
||||
|
||||
initSetHelperText();
|
||||
|
|
|
@ -54,6 +54,7 @@ export function formatListIssues(listIssues) {
|
|||
const listIssue = {
|
||||
...i,
|
||||
id,
|
||||
fullId: i.id,
|
||||
labels: i.labels?.nodes || [],
|
||||
assignees: i.assignees?.nodes || [],
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { GlIcon, GlLink, GlPopover, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { blockingIssuablesQueries, issuableTypes } from '~/boards/constants';
|
||||
import { IssueType } from '~/graphql_shared/constants';
|
||||
import { TYPE_ISSUE } from '~/graphql_shared/constants';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { truncate } from '~/lib/utils/text_utility';
|
||||
import { __, n__, s__, sprintf } from '~/locale';
|
||||
|
@ -13,7 +13,7 @@ export default {
|
|||
},
|
||||
},
|
||||
graphQLIdType: {
|
||||
[issuableTypes.issue]: IssueType,
|
||||
[issuableTypes.issue]: TYPE_ISSUE,
|
||||
},
|
||||
referenceFormatter: {
|
||||
[issuableTypes.issue]: (r) => r.split('/')[1],
|
||||
|
|
|
@ -82,7 +82,9 @@ export default {
|
|||
class="boards-sidebar gl-absolute"
|
||||
@close="handleClose"
|
||||
>
|
||||
<template #header>{{ __('Issue details') }}</template>
|
||||
<template #header>
|
||||
<h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">{{ __('Issue details') }}</h2>
|
||||
</template>
|
||||
<template #default>
|
||||
<board-sidebar-title />
|
||||
<sidebar-assignees-widget
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { GlModal, GlAlert } from '@gitlab/ui';
|
||||
import { mapGetters, mapActions, mapState } from 'vuex';
|
||||
import ListLabel from '~/boards/models/label';
|
||||
import { TYPE_ITERATION, TYPE_MILESTONE, TYPE_USER } from '~/graphql_shared/constants';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { getParameterByName } from '~/lib/utils/common_utils';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
|
@ -188,21 +189,19 @@ export default {
|
|||
};
|
||||
},
|
||||
issueBoardScopeMutationVariables() {
|
||||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
return {
|
||||
weight: this.board.weight,
|
||||
assigneeId: this.board.assignee?.id
|
||||
? convertToGraphQLId('User', this.board.assignee.id)
|
||||
? convertToGraphQLId(TYPE_USER, this.board.assignee.id)
|
||||
: null,
|
||||
milestoneId:
|
||||
this.board.milestone?.id || this.board.milestone?.id === 0
|
||||
? convertToGraphQLId('Milestone', this.board.milestone.id)
|
||||
? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id)
|
||||
: null,
|
||||
iterationId: this.board.iteration_id
|
||||
? convertToGraphQLId('Iteration', this.board.iteration_id)
|
||||
? convertToGraphQLId(TYPE_ITERATION, this.board.iteration_id)
|
||||
: null,
|
||||
};
|
||||
/* eslint-enable @gitlab/require-i18n-strings */
|
||||
},
|
||||
boardScopeMutationVariables() {
|
||||
return {
|
||||
|
|
|
@ -60,22 +60,6 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
updateGlobalTodoCount(additionalTodoCount) {
|
||||
const currentCount = parseInt(document.querySelector('.js-todos-count').innerText, 10);
|
||||
const todoToggleEvent = new CustomEvent('todo:toggle', {
|
||||
detail: {
|
||||
count: Math.max(currentCount + additionalTodoCount, 0),
|
||||
},
|
||||
});
|
||||
|
||||
document.dispatchEvent(todoToggleEvent);
|
||||
},
|
||||
incrementGlobalTodoCount() {
|
||||
this.updateGlobalTodoCount(1);
|
||||
},
|
||||
decrementGlobalTodoCount() {
|
||||
this.updateGlobalTodoCount(-1);
|
||||
},
|
||||
createTodo() {
|
||||
this.todoLoading = true;
|
||||
return this.$apollo
|
||||
|
@ -92,9 +76,6 @@ export default {
|
|||
}
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.incrementGlobalTodoCount();
|
||||
})
|
||||
.catch((err) => {
|
||||
this.$emit('error', Error(CREATE_DESIGN_TODO_ERROR));
|
||||
throw err;
|
||||
|
@ -130,9 +111,6 @@ export default {
|
|||
}
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.decrementGlobalTodoCount();
|
||||
})
|
||||
.catch((err) => {
|
||||
this.$emit('error', Error(DELETE_DESIGN_TODO_ERROR));
|
||||
throw err;
|
||||
|
|
|
@ -42,6 +42,7 @@ import {
|
|||
TRACKING_MULTIPLE_FILES_MODE,
|
||||
} from '../constants';
|
||||
|
||||
import diffsEventHub from '../event_hub';
|
||||
import { reviewStatuses } from '../utils/file_reviews';
|
||||
import { diffsApp } from '../utils/performance';
|
||||
import { fileByFile } from '../utils/preferences';
|
||||
|
@ -52,7 +53,9 @@ import DiffFile from './diff_file.vue';
|
|||
import HiddenFilesWarning from './hidden_files_warning.vue';
|
||||
import MergeConflictWarning from './merge_conflict_warning.vue';
|
||||
import NoChanges from './no_changes.vue';
|
||||
import PreRenderer from './pre_renderer.vue';
|
||||
import TreeList from './tree_list.vue';
|
||||
import VirtualScrollerScrollSync from './virtual_scroller_scroll_sync';
|
||||
|
||||
export default {
|
||||
name: 'DiffsApp',
|
||||
|
@ -71,6 +74,8 @@ export default {
|
|||
GlSprintf,
|
||||
DynamicScroller,
|
||||
DynamicScrollerItem,
|
||||
PreRenderer,
|
||||
VirtualScrollerScrollSync,
|
||||
},
|
||||
alerts: {
|
||||
ALERT_OVERFLOW_HIDDEN,
|
||||
|
@ -166,6 +171,7 @@ export default {
|
|||
return {
|
||||
treeWidth,
|
||||
diffFilesLength: 0,
|
||||
virtualScrollCurrentIndex: -1,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -323,6 +329,11 @@ export default {
|
|||
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 (this.renderTreeList) {
|
||||
api.trackRedisHllUserEvent(TRACKING_FILE_BROWSER_TREE);
|
||||
|
@ -377,6 +388,11 @@ export default {
|
|||
diffsApp.deinstrument();
|
||||
this.unsubscribeFromEvents();
|
||||
this.removeEventListeners();
|
||||
|
||||
if (window.gon?.features?.diffsVirtualScrolling) {
|
||||
diffsEventHub.$off('scrollToFileHash', this.scrollVirtualScrollerToFileHash);
|
||||
diffsEventHub.$off('scrollToIndex', this.scrollVirtualScrollerToIndex);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['startTaskList']),
|
||||
|
@ -508,6 +524,20 @@ export default {
|
|||
|
||||
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,
|
||||
maxTreeWidth: MAX_TREE_WIDTH,
|
||||
|
@ -572,6 +602,7 @@ export default {
|
|||
<template v-else-if="renderDiffFiles">
|
||||
<dynamic-scroller
|
||||
v-if="isVirtualScrollingEnabled"
|
||||
ref="virtualScroller"
|
||||
:items="diffs"
|
||||
:min-item-size="70"
|
||||
:buffer="1000"
|
||||
|
@ -579,7 +610,7 @@ export default {
|
|||
page-mode
|
||||
>
|
||||
<template #default="{ item, index, active }">
|
||||
<dynamic-scroller-item :item="item" :active="active">
|
||||
<dynamic-scroller-item :item="item" :active="active" :class="{ active }">
|
||||
<diff-file
|
||||
:file="item"
|
||||
:reviewed="fileReviews[item.id]"
|
||||
|
@ -588,9 +619,29 @@ export default {
|
|||
:help-page-path="helpPagePath"
|
||||
:can-current-user-fork="canCurrentUserFork"
|
||||
:view-diffs-file-by-file="viewDiffsFileByFile"
|
||||
:active="active"
|
||||
/>
|
||||
</dynamic-scroller-item>
|
||||
</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>
|
||||
<template v-else>
|
||||
<diff-file
|
||||
|
|
|
@ -68,6 +68,16 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
preRender: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -156,6 +166,8 @@ export default {
|
|||
watch: {
|
||||
'file.id': {
|
||||
handler: function fileIdHandler() {
|
||||
if (this.preRender) return;
|
||||
|
||||
this.manageViewedEffects();
|
||||
},
|
||||
},
|
||||
|
@ -163,7 +175,7 @@ export default {
|
|||
handler: function hashChangeWatch(newHash, oldHash) {
|
||||
this.isCollapsed = isCollapsed(this.file);
|
||||
|
||||
if (newHash && oldHash && !this.hasDiff) {
|
||||
if (newHash && oldHash && !this.hasDiff && !this.preRender) {
|
||||
this.requestDiff();
|
||||
}
|
||||
},
|
||||
|
@ -187,10 +199,14 @@ export default {
|
|||
},
|
||||
},
|
||||
created() {
|
||||
if (this.preRender) return;
|
||||
|
||||
notesEventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.requestDiff);
|
||||
eventHub.$on(EVT_EXPAND_ALL_FILES, this.expandAllListener);
|
||||
},
|
||||
mounted() {
|
||||
if (this.preRender) return;
|
||||
|
||||
if (this.hasDiff) {
|
||||
this.postRender();
|
||||
}
|
||||
|
@ -198,6 +214,8 @@ export default {
|
|||
this.manageViewedEffects();
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.preRender) return;
|
||||
|
||||
eventHub.$off(EVT_EXPAND_ALL_FILES, this.expandAllListener);
|
||||
},
|
||||
methods: {
|
||||
|
@ -287,7 +305,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<div
|
||||
:id="file.file_hash"
|
||||
:id="!preRender && active && file.file_hash"
|
||||
:class="{
|
||||
'is-active': currentDiffFileId === file.file_hash,
|
||||
'comments-disabled': Boolean(file.brokenSymlink),
|
||||
|
@ -330,7 +348,7 @@ export default {
|
|||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
:id="`diff-content-${file.file_hash}`"
|
||||
:id="!preRender && active && `diff-content-${file.file_hash}`"
|
||||
:class="hasBodyClasses.contentByHash"
|
||||
data-testid="content-area"
|
||||
>
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
<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>
|
|
@ -0,0 +1,51 @@
|
|||
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 }) => {
|
|||
w: state.showWhitespace ? '0' : '1',
|
||||
view: 'inline',
|
||||
};
|
||||
const hash = window.location.hash.replace('#', '').split('diff-content-').pop();
|
||||
let totalLoaded = 0;
|
||||
let scrolledVirtualScroller = false;
|
||||
|
||||
commit(types.SET_BATCH_LOADING, true);
|
||||
commit(types.SET_RETRIEVING_BATCHES, true);
|
||||
|
@ -115,6 +117,15 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
|
|||
commit(types.SET_DIFF_DATA_BATCH, { diff_files });
|
||||
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) {
|
||||
commit(types.VIEW_DIFF_FILE, diff_files[0].file_hash);
|
||||
}
|
||||
|
@ -171,7 +182,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
|
|||
.catch(() => commit(types.SET_RETRIEVING_BATCHES, false));
|
||||
|
||||
return getBatch()
|
||||
.then(handleLocationHash)
|
||||
.then(() => !window.gon?.features?.diffsVirtualScrolling && handleLocationHash())
|
||||
.catch(() => null);
|
||||
};
|
||||
|
||||
|
@ -510,9 +521,18 @@ export const scrollToFile = ({ state, commit }, path) => {
|
|||
if (!state.treeEntries[path]) return;
|
||||
|
||||
const { fileHash } = state.treeEntries[path];
|
||||
document.location.hash = 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 }) => {
|
||||
|
|
|
@ -381,9 +381,15 @@ function prepareDiffFileLines(file) {
|
|||
}
|
||||
|
||||
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, {
|
||||
renderIt:
|
||||
index < 3 ? file[INLINE_DIFF_LINES_KEY].length < LINES_TO_BE_RENDERED_DIRECTLY : false,
|
||||
renderIt,
|
||||
isShowingFullFile: false,
|
||||
isLoadingFullFile: false,
|
||||
discussions: [],
|
||||
|
|
|
@ -1,2 +1,11 @@
|
|||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
export const IssueType = 'Issue';
|
||||
export const TYPE_CI_RUNNER = 'Ci::Runner';
|
||||
export const TYPE_GROUP = 'Group';
|
||||
export const TYPE_ISSUE = 'Issue';
|
||||
export const TYPE_ITERATION = 'Iteration';
|
||||
export const TYPE_ITERATIONS_CADENCE = 'Iterations::Cadence';
|
||||
export const TYPE_MERGE_REQUEST = 'MergeRequest';
|
||||
export const TYPE_MILESTONE = 'Milestone';
|
||||
export const TYPE_SCANNER_PROFILE = 'DastScannerProfile';
|
||||
export const TYPE_SITE_PROFILE = 'DastSiteProfile';
|
||||
export const TYPE_USER = 'User';
|
||||
export const TYPE_VULNERABILITY = 'Vulnerability';
|
||||
|
|
|
@ -17,11 +17,6 @@ export const MutationOperationMode = {
|
|||
Replace: 'REPLACE',
|
||||
};
|
||||
|
||||
/**
|
||||
* Possible GraphQL entity types.
|
||||
*/
|
||||
export const TYPE_GROUP = 'Group';
|
||||
|
||||
/**
|
||||
* Ids generated by GraphQL endpoints are usually in the format
|
||||
* gid://gitlab/Groups/123. This method takes a type and an id
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
|
||||
import createFlash from '~/flash';
|
||||
import { TYPE_USER } from '~/graphql_shared/constants';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
|
||||
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
|
||||
|
@ -268,7 +269,7 @@ export default {
|
|||
|
||||
if (gon.current_user_id) {
|
||||
preloadedAuthors.push({
|
||||
id: convertToGraphQLId('User', gon.current_user_id), // eslint-disable-line @gitlab/require-i18n-strings
|
||||
id: convertToGraphQLId(TYPE_USER, gon.current_user_id),
|
||||
name: gon.current_user_fullname,
|
||||
username: gon.current_username,
|
||||
avatar_url: gon.current_user_avatar_url,
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
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);
|
||||
});
|
||||
}
|
|
@ -105,7 +105,7 @@ export default {
|
|||
return this.pipeline;
|
||||
}
|
||||
|
||||
return unwrapPipelineData(this.pipelineProjectPath, data);
|
||||
return unwrapPipelineData(this.pipelineProjectPath, JSON.parse(JSON.stringify(data)));
|
||||
},
|
||||
error(err) {
|
||||
this.reportFailure({ type: LOAD_FAILURE, skipSentry: true });
|
||||
|
|
|
@ -118,7 +118,7 @@ export default {
|
|||
return this.currentPipeline;
|
||||
}
|
||||
|
||||
return unwrapPipelineData(projectPath, data);
|
||||
return unwrapPipelineData(projectPath, JSON.parse(JSON.stringify(data)));
|
||||
},
|
||||
result() {
|
||||
this.loadingPipelineId = null;
|
||||
|
|
|
@ -5,6 +5,7 @@ export const apolloProvider = new VueApollo({
|
|||
defaultClient: createDefaultClient(
|
||||
{},
|
||||
{
|
||||
assumeImmutableResults: true,
|
||||
useGet: true,
|
||||
},
|
||||
),
|
||||
|
|
|
@ -4,8 +4,6 @@ export const RUNNER_PAGE_SIZE = 20;
|
|||
|
||||
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
|
||||
|
||||
export const RUNNER_ENTITY_TYPE = 'Ci::Runner';
|
||||
|
||||
export const RUNNER_TAG_BADGE_VARIANT = 'info';
|
||||
export const RUNNER_TAG_BG_CLASS = 'gl-bg-blue-100';
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<script>
|
||||
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import RunnerTypeAlert from '../components/runner_type_alert.vue';
|
||||
import RunnerTypeBadge from '../components/runner_type_badge.vue';
|
||||
import RunnerUpdateForm from '../components/runner_update_form.vue';
|
||||
import { I18N_DETAILS_TITLE, RUNNER_ENTITY_TYPE } from '../constants';
|
||||
import { I18N_DETAILS_TITLE } from '../constants';
|
||||
import getRunnerQuery from '../graphql/get_runner.query.graphql';
|
||||
|
||||
export default {
|
||||
|
@ -31,7 +32,7 @@ export default {
|
|||
query: getRunnerQuery,
|
||||
variables() {
|
||||
return {
|
||||
id: convertToGraphQLId(RUNNER_ENTITY_TYPE, this.runnerId),
|
||||
id: convertToGraphQLId(TYPE_CI_RUNNER, this.runnerId),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
|
||||
import createFlash from '~/flash';
|
||||
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { formatDate, parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
|
||||
import { __ } from '~/locale';
|
||||
|
@ -52,8 +53,7 @@ export default {
|
|||
return this.issuableType === 'issue';
|
||||
},
|
||||
getGraphQLEntityType() {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
return this.isIssue() ? 'Issue' : 'MergeRequest';
|
||||
return this.isIssue() ? TYPE_ISSUE : TYPE_MERGE_REQUEST;
|
||||
},
|
||||
extractTimelogs(data) {
|
||||
const timelogs = data?.issuable?.timelogs?.nodes || [];
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
<script>
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
import { produce } from 'immer';
|
||||
import createFlash from '~/flash';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { todoQueries, TodoMutationTypes, todoMutations } from '~/sidebar/constants';
|
||||
import TodoButton from '~/vue_shared/components/todo_button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TodoButton,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
issuableId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
issuableIid: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fullPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
issuableType: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
todoId: {
|
||||
query() {
|
||||
return todoQueries[this.issuableType].query;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
iid: String(this.issuableIid),
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return data.workspace?.issuable?.currentUserTodos.nodes[0]?.id;
|
||||
},
|
||||
result({ data }) {
|
||||
const currentUserTodos = data.workspace?.issuable?.currentUserTodos?.nodes ?? [];
|
||||
this.todoId = currentUserTodos[0]?.id;
|
||||
this.$emit('todoUpdated', currentUserTodos.length > 0);
|
||||
},
|
||||
error() {
|
||||
createFlash({
|
||||
message: sprintf(__('Something went wrong while setting %{issuableType} to-do item.'), {
|
||||
issuableType: this.issuableType,
|
||||
}),
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
todoIdQuery() {
|
||||
return todoQueries[this.issuableType].query;
|
||||
},
|
||||
todoIdQueryVariables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
iid: String(this.issuableIid),
|
||||
};
|
||||
},
|
||||
isLoading() {
|
||||
return this.$apollo.queries?.todoId?.loading || this.loading;
|
||||
},
|
||||
hasTodo() {
|
||||
return Boolean(this.todoId);
|
||||
},
|
||||
todoMutationType() {
|
||||
if (this.hasTodo) {
|
||||
return TodoMutationTypes.MarkDone;
|
||||
}
|
||||
return TodoMutationTypes.Create;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleTodo() {
|
||||
this.loading = true;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: todoMutations[this.todoMutationType],
|
||||
variables: {
|
||||
input: {
|
||||
targetId: !this.hasTodo ? this.issuableId : undefined,
|
||||
id: this.hasTodo ? this.todoId : undefined,
|
||||
},
|
||||
},
|
||||
update: (
|
||||
store,
|
||||
{
|
||||
data: {
|
||||
todoMutation: { todo },
|
||||
},
|
||||
},
|
||||
) => {
|
||||
const queryProps = {
|
||||
query: this.todoIdQuery,
|
||||
variables: this.todoIdQueryVariables,
|
||||
};
|
||||
|
||||
const sourceData = store.readQuery(queryProps);
|
||||
const data = produce(sourceData, (draftState) => {
|
||||
draftState.workspace.issuable.currentUserTodos.nodes = this.hasTodo ? [] : [todo];
|
||||
});
|
||||
store.writeQuery({
|
||||
data,
|
||||
...queryProps,
|
||||
});
|
||||
},
|
||||
})
|
||||
.then(
|
||||
({
|
||||
data: {
|
||||
todoMutation: { errors },
|
||||
},
|
||||
}) => {
|
||||
if (errors.length) {
|
||||
createFlash({
|
||||
message: errors[0],
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
.catch(() => {
|
||||
createFlash({
|
||||
message: sprintf(__('Something went wrong while setting %{issuableType} to-do item.'), {
|
||||
issuableType: this.issuableType,
|
||||
}),
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-testid="sidebar-todo">
|
||||
<todo-button
|
||||
:issuable-type="issuableType"
|
||||
:issuable-id="issuableId"
|
||||
:is-todo="hasTodo"
|
||||
:loading="isLoading"
|
||||
size="small"
|
||||
@click.stop.prevent="toggleTodo"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -4,6 +4,7 @@ import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql';
|
|||
import epicParticipantsQuery from '~/sidebar/queries/epic_participants.query.graphql';
|
||||
import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql';
|
||||
import epicSubscribedQuery from '~/sidebar/queries/epic_subscribed.query.graphql';
|
||||
import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
|
||||
import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
|
||||
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
|
||||
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
|
||||
|
@ -13,6 +14,8 @@ import issueTimeTrackingQuery from '~/sidebar/queries/issue_time_tracking.query.
|
|||
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
|
||||
import mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql';
|
||||
import mergeRequestTimeTrackingQuery from '~/sidebar/queries/merge_request_time_tracking.query.graphql';
|
||||
import todoCreateMutation from '~/sidebar/queries/todo_create.mutation.graphql';
|
||||
import todoMarkDoneMutation from '~/sidebar/queries/todo_mark_done.mutation.graphql';
|
||||
import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
|
||||
import updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mutation.graphql';
|
||||
import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql';
|
||||
|
@ -189,3 +192,19 @@ export const issuableAttributesQueries = {
|
|||
list: milestonesQueries,
|
||||
},
|
||||
};
|
||||
|
||||
export const todoQueries = {
|
||||
[IssuableType.Epic]: {
|
||||
query: epicTodoQuery,
|
||||
},
|
||||
};
|
||||
|
||||
export const TodoMutationTypes = {
|
||||
Create: 'create',
|
||||
MarkDone: 'mark-done',
|
||||
};
|
||||
|
||||
export const todoMutations = {
|
||||
[TodoMutationTypes.Create]: todoCreateMutation,
|
||||
[TodoMutationTypes.MarkDone]: todoMarkDoneMutation,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
query epicTodos($fullPath: ID!, $iid: ID) {
|
||||
workspace: group(fullPath: $fullPath) {
|
||||
__typename
|
||||
issuable: epic(iid: $iid) {
|
||||
__typename
|
||||
id
|
||||
currentUserTodos(state: pending) {
|
||||
nodes {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
mutation issuableTodoCreate($input: TodoCreateInput!) {
|
||||
todoMutation: todoCreate(input: $input) {
|
||||
__typename
|
||||
todo {
|
||||
id
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
mutation issuableTodoMarkDone($input: TodoMarkDoneInput!) {
|
||||
todoMutation: todoMarkDone(input: $input) {
|
||||
__typename
|
||||
todo {
|
||||
id
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import sidebarDetailsIssueQuery from 'ee_else_ce/sidebar/queries/sidebarDetails.query.graphql';
|
||||
import { TYPE_USER } from '~/graphql_shared/constants';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import createGqClient, { fetchPolicies } from '~/lib/graphql';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
@ -88,7 +89,7 @@ export default class SidebarService {
|
|||
return gqClient.mutate({
|
||||
mutation: reviewerRereviewMutation,
|
||||
variables: {
|
||||
userId: convertToGraphQLId('User', `${userId}`), // eslint-disable-line @gitlab/require-i18n-strings
|
||||
userId: convertToGraphQLId(TYPE_USER, `${userId}`),
|
||||
projectPath: this.fullPath,
|
||||
iid: this.iid.toString(),
|
||||
},
|
||||
|
|
|
@ -18,11 +18,39 @@ export default {
|
|||
return this.isTodo ? __('Mark as done') : __('Add a to do');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateGlobalTodoCount(additionalTodoCount) {
|
||||
const countContainer = document.querySelector('.js-todos-count');
|
||||
if (countContainer === null) return;
|
||||
const currentCount = parseInt(countContainer.innerText, 10);
|
||||
const todoToggleEvent = new CustomEvent('todo:toggle', {
|
||||
detail: {
|
||||
count: Math.max(currentCount + additionalTodoCount, 0),
|
||||
},
|
||||
});
|
||||
|
||||
document.dispatchEvent(todoToggleEvent);
|
||||
},
|
||||
incrementGlobalTodoCount() {
|
||||
this.updateGlobalTodoCount(1);
|
||||
},
|
||||
decrementGlobalTodoCount() {
|
||||
this.updateGlobalTodoCount(-1);
|
||||
},
|
||||
onToggle(event) {
|
||||
if (this.isTodo) {
|
||||
this.decrementGlobalTodoCount();
|
||||
} else {
|
||||
this.incrementGlobalTodoCount();
|
||||
}
|
||||
this.$emit('click', event);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-button v-bind="$attrs" :aria-label="buttonLabel" @click="$emit('click', $event)">
|
||||
<gl-button v-bind="$attrs" :aria-label="buttonLabel" @click="onToggle($event)">
|
||||
{{ buttonLabel }}
|
||||
</gl-button>
|
||||
</template>
|
||||
|
|
|
@ -1188,3 +1188,9 @@ table.code {
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -30,7 +30,9 @@
|
|||
.dropdown-menu {
|
||||
@include set-visible;
|
||||
min-height: $dropdown-min-height;
|
||||
max-height: $dropdown-max-height;
|
||||
// Prevents double scrollbar on dropdowns that also
|
||||
// have max-height set on an inner scrollable element
|
||||
max-height: $dropdown-max-height-lg;
|
||||
overflow-y: auto;
|
||||
|
||||
&.dropdown-extended-height {
|
||||
|
|
|
@ -472,6 +472,10 @@
|
|||
.sidebar-collapsed-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gl-drawer-header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.board-header-collapsed-info-icon:hover {
|
||||
|
|
|
@ -331,6 +331,7 @@ module ApplicationSettingsHelper
|
|||
:unique_ips_limit_per_user,
|
||||
:unique_ips_limit_time_window,
|
||||
:usage_ping_enabled,
|
||||
:usage_ping_features_enabled,
|
||||
:user_default_external,
|
||||
:user_show_add_ssh_key_message,
|
||||
:user_default_internal_regex,
|
||||
|
|
|
@ -377,6 +377,10 @@ module ApplicationSettingImplementation
|
|||
Settings.gitlab.usage_ping_enabled
|
||||
end
|
||||
|
||||
def usage_ping_features_enabled?
|
||||
usage_ping_enabled? && usage_ping_features_enabled
|
||||
end
|
||||
|
||||
def usage_ping_enabled
|
||||
usage_ping_can_be_configured? && super
|
||||
end
|
||||
|
|
|
@ -230,7 +230,7 @@ module CascadingNamespaceSettingAttribute
|
|||
|
||||
def namespace_ancestor_ids
|
||||
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
|
||||
|
||||
|
|
|
@ -64,6 +64,13 @@ module Namespaces
|
|||
traversal_ids.present?
|
||||
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
|
||||
return super if parent.nil?
|
||||
return super unless persisted?
|
||||
|
@ -95,14 +102,33 @@ module Namespaces
|
|||
end
|
||||
|
||||
def ancestors(hierarchy_order: nil)
|
||||
return super() unless use_traversal_ids?
|
||||
return super() unless Feature.enabled?(:use_traversal_ids_for_ancestors, root_ancestor, default_enabled: :yaml)
|
||||
return super unless use_traversal_ids_for_ancestors?
|
||||
|
||||
return self.class.none if parent_id.blank?
|
||||
|
||||
lineage(bottom: parent, hierarchy_order: hierarchy_order)
|
||||
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
|
||||
|
||||
# Update the traversal_ids for the full hierarchy.
|
||||
|
|
|
@ -10,7 +10,7 @@ module Namespaces
|
|||
|
||||
if persisted?
|
||||
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
|
||||
else
|
||||
parent.root_ancestor
|
||||
|
@ -26,14 +26,19 @@ module Namespaces
|
|||
alias_method :recursive_self_and_hierarchy, :self_and_hierarchy
|
||||
|
||||
# Returns all the ancestors of the current namespaces.
|
||||
def ancestors
|
||||
def ancestors(hierarchy_order: nil)
|
||||
return self.class.none unless parent_id
|
||||
|
||||
object_hierarchy(self.class.where(id: parent_id))
|
||||
.base_and_ancestors
|
||||
.base_and_ancestors(hierarchy_order: hierarchy_order)
|
||||
end
|
||||
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
|
||||
# when no namespace is given, all ancestors upto the top are returned
|
||||
def ancestors_upto(top = nil, hierarchy_order: nil)
|
||||
|
@ -49,6 +54,11 @@ module Namespaces
|
|||
end
|
||||
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.
|
||||
def descendants
|
||||
object_hierarchy(self.class.where(parent_id: id))
|
||||
|
@ -63,7 +73,7 @@ module Namespaces
|
|||
alias_method :recursive_self_and_descendants, :self_and_descendants
|
||||
|
||||
def self_and_descendant_ids
|
||||
self_and_descendants.select(:id)
|
||||
recursive_self_and_descendants.select(:id)
|
||||
end
|
||||
alias_method :recursive_self_and_descendant_ids, :self_and_descendant_ids
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ class Wiki
|
|||
end
|
||||
|
||||
def create_wiki_repository
|
||||
repository.create_if_not_exists
|
||||
change_head_to_default_branch if repository.create_if_not_exists
|
||||
|
||||
raise CouldNotCreateWikiError unless repository_exists?
|
||||
rescue StandardError => err
|
||||
|
@ -174,6 +174,7 @@ class Wiki
|
|||
commit = commit_details(:created, message, title)
|
||||
|
||||
wiki.write_page(title, format.to_sym, content, commit)
|
||||
repository.expire_status_cache if repository.empty?
|
||||
after_wiki_activity
|
||||
|
||||
true
|
||||
|
@ -248,7 +249,9 @@ class Wiki
|
|||
|
||||
override :default_branch
|
||||
def default_branch
|
||||
wiki.class.default_ref
|
||||
return 'master' if Feature.disabled?(:wiki_uses_default_branch, user, default_enabled: :yaml)
|
||||
|
||||
super || wiki.class.default_ref(container)
|
||||
end
|
||||
|
||||
def wiki_base_path
|
||||
|
@ -320,6 +323,10 @@ class Wiki
|
|||
|
||||
false
|
||||
end
|
||||
|
||||
def change_head_to_default_branch
|
||||
repository.raw_repository.write_ref('HEAD', "refs/heads/#{default_branch}")
|
||||
end
|
||||
end
|
||||
|
||||
Wiki.prepend_mod_with('Wiki')
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ServicePing
|
||||
class SubmitService
|
||||
PRODUCTION_URL = 'https://version.gitlab.com/usage_data'
|
||||
STAGING_URL = 'https://gitlab-services-version-gitlab-com-staging.gs-staging.gitlab.org/usage_data'
|
||||
|
||||
METRICS = %w[leader_issues instance_issues percentage_issues leader_notes instance_notes
|
||||
percentage_notes leader_milestones instance_milestones percentage_milestones
|
||||
leader_boards instance_boards percentage_boards leader_merge_requests
|
||||
instance_merge_requests percentage_merge_requests leader_ci_pipelines
|
||||
instance_ci_pipelines percentage_ci_pipelines leader_environments instance_environments
|
||||
percentage_environments leader_deployments instance_deployments percentage_deployments
|
||||
leader_projects_prometheus_active instance_projects_prometheus_active
|
||||
percentage_projects_prometheus_active leader_service_desk_issues instance_service_desk_issues
|
||||
percentage_service_desk_issues].freeze
|
||||
|
||||
SubmissionError = Class.new(StandardError)
|
||||
|
||||
def execute
|
||||
return unless Gitlab::CurrentSettings.usage_ping_enabled?
|
||||
return if User.single_user&.requires_usage_stats_consent?
|
||||
|
||||
usage_data = Gitlab::UsageData.data(force_refresh: true)
|
||||
|
||||
raise SubmissionError, 'Usage data is blank' if usage_data.blank?
|
||||
|
||||
raw_usage_data = save_raw_usage_data(usage_data)
|
||||
|
||||
response = Gitlab::HTTP.post(
|
||||
url,
|
||||
body: usage_data.to_json,
|
||||
allow_local_requests: true,
|
||||
headers: { 'Content-type' => 'application/json' }
|
||||
)
|
||||
|
||||
raise SubmissionError, "Unsuccessful response code: #{response.code}" unless response.success?
|
||||
|
||||
version_usage_data_id = response.dig('conv_index', 'usage_data_id') || response.dig('dev_ops_score', 'usage_data_id')
|
||||
|
||||
unless version_usage_data_id.is_a?(Integer) && version_usage_data_id > 0
|
||||
raise SubmissionError, "Invalid usage_data_id in response: #{version_usage_data_id}"
|
||||
end
|
||||
|
||||
raw_usage_data.update_version_metadata!(usage_data_id: version_usage_data_id)
|
||||
|
||||
store_metrics(response)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def save_raw_usage_data(usage_data)
|
||||
RawUsageData.safe_find_or_create_by(recorded_at: usage_data[:recorded_at]) do |record|
|
||||
record.payload = usage_data
|
||||
end
|
||||
end
|
||||
|
||||
def store_metrics(response)
|
||||
metrics = response['conv_index'] || response['dev_ops_score'] # leaving dev_ops_score here, as the response data comes from the gitlab-version-com
|
||||
|
||||
return unless metrics.present?
|
||||
|
||||
DevOpsReport::Metric.create!(
|
||||
metrics.slice(*METRICS)
|
||||
)
|
||||
end
|
||||
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/233615 for details
|
||||
def url
|
||||
if Rails.env.production?
|
||||
PRODUCTION_URL
|
||||
else
|
||||
STAGING_URL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ServicePing::SubmitService.prepend_mod
|
|
@ -1,77 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SubmitUsagePingService
|
||||
PRODUCTION_URL = 'https://version.gitlab.com/usage_data'
|
||||
STAGING_URL = 'https://gitlab-services-version-gitlab-com-staging.gs-staging.gitlab.org/usage_data'
|
||||
|
||||
METRICS = %w[leader_issues instance_issues percentage_issues leader_notes instance_notes
|
||||
percentage_notes leader_milestones instance_milestones percentage_milestones
|
||||
leader_boards instance_boards percentage_boards leader_merge_requests
|
||||
instance_merge_requests percentage_merge_requests leader_ci_pipelines
|
||||
instance_ci_pipelines percentage_ci_pipelines leader_environments instance_environments
|
||||
percentage_environments leader_deployments instance_deployments percentage_deployments
|
||||
leader_projects_prometheus_active instance_projects_prometheus_active
|
||||
percentage_projects_prometheus_active leader_service_desk_issues instance_service_desk_issues
|
||||
percentage_service_desk_issues].freeze
|
||||
|
||||
SubmissionError = Class.new(StandardError)
|
||||
|
||||
def execute
|
||||
return unless Gitlab::CurrentSettings.usage_ping_enabled?
|
||||
return if User.single_user&.requires_usage_stats_consent?
|
||||
|
||||
usage_data = Gitlab::UsageData.data(force_refresh: true)
|
||||
|
||||
raise SubmissionError, 'Usage data is blank' if usage_data.blank?
|
||||
|
||||
raw_usage_data = save_raw_usage_data(usage_data)
|
||||
|
||||
response = Gitlab::HTTP.post(
|
||||
url,
|
||||
body: usage_data.to_json,
|
||||
allow_local_requests: true,
|
||||
headers: { 'Content-type' => 'application/json' }
|
||||
)
|
||||
|
||||
raise SubmissionError, "Unsuccessful response code: #{response.code}" unless response.success?
|
||||
|
||||
version_usage_data_id = response.dig('conv_index', 'usage_data_id') || response.dig('dev_ops_score', 'usage_data_id')
|
||||
|
||||
unless version_usage_data_id.is_a?(Integer) && version_usage_data_id > 0
|
||||
raise SubmissionError, "Invalid usage_data_id in response: #{version_usage_data_id}"
|
||||
end
|
||||
|
||||
raw_usage_data.update_version_metadata!(usage_data_id: version_usage_data_id)
|
||||
|
||||
store_metrics(response)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def save_raw_usage_data(usage_data)
|
||||
RawUsageData.safe_find_or_create_by(recorded_at: usage_data[:recorded_at]) do |record|
|
||||
record.payload = usage_data
|
||||
end
|
||||
end
|
||||
|
||||
def store_metrics(response)
|
||||
metrics = response['conv_index'] || response['dev_ops_score'] # leaving dev_ops_score here, as the response data comes from the gitlab-version-com
|
||||
|
||||
return unless metrics.present?
|
||||
|
||||
DevOpsReport::Metric.create!(
|
||||
metrics.slice(*METRICS)
|
||||
)
|
||||
end
|
||||
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/233615 for details
|
||||
def url
|
||||
if Rails.env.production?
|
||||
PRODUCTION_URL
|
||||
else
|
||||
STAGING_URL
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
SubmitUsagePingService.prepend_mod
|
|
@ -35,5 +35,26 @@
|
|||
- 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 }
|
||||
= 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"
|
||||
|
|
|
@ -22,7 +22,7 @@ class GitlabUsagePingWorker # rubocop:disable Scalability/IdempotentWorker
|
|||
# Splay the request over a minute to avoid thundering herd problems.
|
||||
sleep(rand(0.0..60.0).round(3))
|
||||
|
||||
SubmitUsagePingService.new.execute
|
||||
ServicePing::SubmitService.new.execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: wiki_uses_default_branch
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64891
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334754
|
||||
milestone: '14.1'
|
||||
type: development
|
||||
group: group::editor
|
||||
default_enabled: false
|
|
@ -0,0 +1,11 @@
|
|||
# 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
|
|
@ -0,0 +1 @@
|
|||
1a0df6210d9ee0e0229f3cdf3e95acaaa47ebf4ca31ac0fd9f57255115355f99
|
|
@ -9525,6 +9525,7 @@ CREATE TABLE application_settings (
|
|||
encrypted_mailgun_signing_key bytea,
|
||||
encrypted_mailgun_signing_key_iv bytea,
|
||||
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_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)),
|
||||
|
|
|
@ -65,6 +65,9 @@ To use this feature, define a [CI/CD variable](../../ci/variables/index.md#custo
|
|||
demonstrates how you might use a pre-clone step to seed the build
|
||||
directory.
|
||||
|
||||
NOTE:
|
||||
The `CI_PRE_CLONE_SCRIPT` variable does not work on Windows runners.
|
||||
|
||||
### `config.toml`
|
||||
|
||||
The full contents of our `config.toml` are:
|
||||
|
|
|
@ -54,8 +54,11 @@ module Gitlab
|
|||
|
||||
attr_reader :repository
|
||||
|
||||
def self.default_ref
|
||||
'master'
|
||||
# TODO remove argument when issue
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/329190
|
||||
# is closed.
|
||||
def self.default_ref(container = nil)
|
||||
Gitlab::DefaultBranch.value(object: container)
|
||||
end
|
||||
|
||||
# Initialize with a Gitlab::Git::Repository instance
|
||||
|
|
|
@ -51,6 +51,10 @@ module Sidebars
|
|||
end
|
||||
|
||||
def labels_menu_item
|
||||
unless can?(context.current_user, :read_label, context.project)
|
||||
return ::Sidebars::NilMenuItem.new(item_id: :labels)
|
||||
end
|
||||
|
||||
::Sidebars::MenuItem.new(
|
||||
title: _('Labels'),
|
||||
link: project_labels_path(context.project),
|
||||
|
@ -60,6 +64,10 @@ module Sidebars
|
|||
end
|
||||
|
||||
def members_menu_item
|
||||
unless can?(context.current_user, :read_project_member, context.project)
|
||||
return ::Sidebars::NilMenuItem.new(item_id: :members)
|
||||
end
|
||||
|
||||
::Sidebars::MenuItem.new(
|
||||
title: _('Members'),
|
||||
link: project_project_members_path(context.project),
|
||||
|
|
|
@ -11809,6 +11809,9 @@ msgstr ""
|
|||
msgid "Email display name"
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
|
@ -11950,6 +11953,9 @@ msgstr ""
|
|||
msgid "Enable Pseudonymizer data collection"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Registration Features"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Repository Checks"
|
||||
msgstr ""
|
||||
|
||||
|
@ -26845,6 +26851,9 @@ msgstr ""
|
|||
msgid "Register with two-factor app"
|
||||
msgstr ""
|
||||
|
||||
msgid "Registration Features include:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Registration|Checkout"
|
||||
msgstr ""
|
||||
|
||||
|
@ -30339,6 +30348,9 @@ msgstr ""
|
|||
msgid "Something went wrong while setting %{issuableType} notifications."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while setting %{issuableType} to-do item."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while stopping this environment. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
@ -33913,6 +33925,9 @@ msgstr ""
|
|||
msgid "To define internal users, first enable new users set to external"
|
||||
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}."
|
||||
msgstr ""
|
||||
|
||||
|
@ -37086,6 +37101,9 @@ msgstr ""
|
|||
msgid "You can easily contribute to them by requesting to join these groups."
|
||||
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}."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -34,4 +34,12 @@ RSpec.describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', ty
|
|||
|
||||
expect(response).to be_successful
|
||||
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
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createFlash from '~/flash';
|
||||
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
|
||||
import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
|
||||
import TodoButton from '~/vue_shared/components/todo_button.vue';
|
||||
import { todosResponse, noTodosResponse } from '../../mock_data';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('Sidebar Todo Widget', () => {
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
|
||||
const findTodoButton = () => wrapper.findComponent(TodoButton);
|
||||
|
||||
const createComponent = ({
|
||||
todosQueryHandler = jest.fn().mockResolvedValue(noTodosResponse),
|
||||
} = {}) => {
|
||||
fakeApollo = createMockApollo([[epicTodoQuery, todosQueryHandler]]);
|
||||
|
||||
wrapper = shallowMount(SidebarTodoWidget, {
|
||||
apolloProvider: fakeApollo,
|
||||
provide: {
|
||||
canUpdate: true,
|
||||
},
|
||||
propsData: {
|
||||
fullPath: 'group',
|
||||
issuableIid: '1',
|
||||
issuableId: 'gid://gitlab/Epic/4',
|
||||
issuableType: 'epic',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
fakeApollo = null;
|
||||
});
|
||||
|
||||
describe('when user does not have a todo for the issuable', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('passes false isTodo prop to Todo button component', () => {
|
||||
expect(findTodoButton().props('isTodo')).toBe(false);
|
||||
});
|
||||
|
||||
it('emits `todoUpdated` event with a `false` payload', () => {
|
||||
expect(wrapper.emitted('todoUpdated')).toEqual([[false]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user has a todo for the issuable', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
todosQueryHandler: jest.fn().mockResolvedValue(todosResponse),
|
||||
});
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('passes true isTodo prop to Todo button component', () => {
|
||||
expect(findTodoButton().props('isTodo')).toBe(true);
|
||||
});
|
||||
|
||||
it('emits `todoUpdated` event with a `true` payload', () => {
|
||||
expect(wrapper.emitted('todoUpdated')).toEqual([[true]]);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays a flash message when query is rejected', async () => {
|
||||
createComponent({
|
||||
todosQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(createFlash).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -609,4 +609,38 @@ export const issuableTimeTrackingResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const todosResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
__typename: 'Group',
|
||||
issuable: {
|
||||
__typename: 'Epic',
|
||||
id: 'gid://gitlab/Epic/4',
|
||||
currentUserTodos: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Todo/433',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const noTodosResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
__typename: 'Group',
|
||||
issuable: {
|
||||
__typename: 'Epic',
|
||||
id: 'gid://gitlab/Epic/4',
|
||||
currentUserTodos: {
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default mockData;
|
||||
|
|
|
@ -4,6 +4,7 @@ import TodoButton from '~/vue_shared/components/todo_button.vue';
|
|||
|
||||
describe('Todo Button', () => {
|
||||
let wrapper;
|
||||
let dispatchEventSpy;
|
||||
|
||||
const createComponent = (props = {}, mountFn = shallowMount) => {
|
||||
wrapper = mountFn(TodoButton, {
|
||||
|
@ -13,8 +14,17 @@ describe('Todo Button', () => {
|
|||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
|
||||
jest.spyOn(document, 'querySelector').mockReturnValue({
|
||||
innerText: 2,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
dispatchEventSpy = null;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders GlButton', () => {
|
||||
|
@ -30,6 +40,16 @@ describe('Todo Button', () => {
|
|||
expect(wrapper.emitted().click).toBeTruthy();
|
||||
});
|
||||
|
||||
it('calls dispatchDocumentEvent to update global To-Do counter correctly', () => {
|
||||
createComponent({}, mount);
|
||||
wrapper.find(GlButton).trigger('click');
|
||||
const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
|
||||
|
||||
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
|
||||
expect(dispatchedEvent.detail).toEqual({ count: 1 });
|
||||
expect(dispatchedEvent.type).toBe('todo:toggle');
|
||||
});
|
||||
|
||||
it.each`
|
||||
label | isTodo
|
||||
${'Mark as done'} | ${true}
|
||||
|
|
|
@ -209,11 +209,10 @@ RSpec.describe Gitlab::ProjectSearchResults do
|
|||
|
||||
describe 'wiki search' do
|
||||
let(:project) { create(:project, :public, :wiki_repo) }
|
||||
let(:wiki) { build(:project_wiki, project: project) }
|
||||
|
||||
before do
|
||||
wiki.create_page('Files/Title', 'Content')
|
||||
wiki.create_page('CHANGELOG', 'Files example')
|
||||
project.wiki.create_page('Files/Title', 'Content')
|
||||
project.wiki.create_page('CHANGELOG', 'Files example')
|
||||
end
|
||||
|
||||
it_behaves_like 'general blob search', 'wiki', 'wiki_blobs' do
|
||||
|
|
|
@ -4,12 +4,11 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Gitlab::WikiFileFinder do
|
||||
describe '#find' do
|
||||
let(:project) { create(:project, :public, :wiki_repo) }
|
||||
let(:wiki) { build(:project_wiki, project: project) }
|
||||
|
||||
before do
|
||||
wiki.create_page('Files/Title', 'Content')
|
||||
wiki.create_page('CHANGELOG', 'Files example')
|
||||
let_it_be(:project) do
|
||||
create(:project, :public, :wiki_repo).tap do |project|
|
||||
project.wiki.create_page('Files/Title', 'Content')
|
||||
project.wiki.create_page('CHANGELOG', 'Files example')
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'file finder' do
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be_with_reload(:project) { create(:project, :repository) }
|
||||
|
||||
let(:user) { project.owner }
|
||||
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
|
||||
|
@ -21,12 +21,43 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
|
|||
let(:item_id) { :labels }
|
||||
|
||||
specify { is_expected.not_to be_nil }
|
||||
|
||||
context 'when merge requests are disabled' do
|
||||
before do
|
||||
project.project_feature.update_attribute(:merge_requests_access_level, Featurable::DISABLED)
|
||||
end
|
||||
|
||||
specify { is_expected.not_to be_nil }
|
||||
end
|
||||
|
||||
context 'when issues are disabled' do
|
||||
before do
|
||||
project.project_feature.update_attribute(:issues_access_level, Featurable::DISABLED)
|
||||
end
|
||||
|
||||
specify { is_expected.not_to be_nil }
|
||||
end
|
||||
|
||||
context 'when merge requests and issues are disabled' do
|
||||
before do
|
||||
project.project_feature.update_attribute(:merge_requests_access_level, Featurable::DISABLED)
|
||||
project.project_feature.update_attribute(:issues_access_level, Featurable::DISABLED)
|
||||
end
|
||||
|
||||
specify { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Members' do
|
||||
let(:item_id) { :members }
|
||||
|
||||
specify { is_expected.not_to be_nil }
|
||||
|
||||
describe 'when the user does not have access' do
|
||||
let(:user) { nil }
|
||||
|
||||
specify { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ RSpec.describe Integrations::Irker do
|
|||
end
|
||||
|
||||
describe 'Validations' do
|
||||
context 'when service is active' do
|
||||
context 'when integration is active' do
|
||||
before do
|
||||
subject.active = true
|
||||
end
|
||||
|
@ -19,7 +19,7 @@ RSpec.describe Integrations::Irker do
|
|||
it { is_expected.to validate_presence_of(:recipients) }
|
||||
end
|
||||
|
||||
context 'when service is inactive' do
|
||||
context 'when integration is inactive' do
|
||||
before do
|
||||
subject.active = false
|
||||
end
|
||||
|
|
|
@ -45,7 +45,7 @@ RSpec.describe Integrations::Jenkins do
|
|||
|
||||
subject { jenkins_integration }
|
||||
|
||||
context 'when the service is active' do
|
||||
context 'when the integration is active' do
|
||||
let(:active) { true }
|
||||
|
||||
context 'when password was not touched' do
|
||||
|
@ -74,7 +74,7 @@ RSpec.describe Integrations::Jenkins do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the service is inactive' do
|
||||
context 'when the integration is inactive' do
|
||||
let(:active) { false }
|
||||
|
||||
it { is_expected.not_to validate_presence_of :username }
|
||||
|
|
|
@ -996,6 +996,36 @@ RSpec.describe Namespace do
|
|||
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
|
||||
let(:user_a) { create(:user) }
|
||||
let(:user_b) { create(:user) }
|
||||
|
|
|
@ -5,11 +5,12 @@ require 'spec_helper'
|
|||
RSpec.describe Git::WikiPushService, services: true do
|
||||
include RepoHelpers
|
||||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:key_id) { create(:key, user: current_user).shell_id }
|
||||
let_it_be(:wiki) { create(:project_wiki) }
|
||||
let_it_be(:current_user) { wiki.container.default_owner }
|
||||
let_it_be(:git_wiki) { wiki.wiki }
|
||||
let_it_be(:repository) { wiki.repository }
|
||||
|
||||
let(:wiki) { create(:project_wiki, user: current_user) }
|
||||
let(:git_wiki) { wiki.wiki }
|
||||
let(:repository) { wiki.repository }
|
||||
|
||||
describe '#execute' do
|
||||
it 'executes model-specific callbacks' do
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe SubmitUsagePingService do
|
||||
RSpec.describe ServicePing::SubmitService do
|
||||
include StubRequests
|
||||
include UsageDataHelpers
|
||||
|
|
@ -13,10 +13,22 @@ RSpec.shared_examples 'a cascading setting' do
|
|||
click_save_button
|
||||
end
|
||||
|
||||
it 'disables setting in subgroups' do
|
||||
visit subgroup_path
|
||||
shared_examples 'subgroup settings are disabled' do
|
||||
it 'disables setting in subgroups' do
|
||||
visit subgroup_path
|
||||
|
||||
expect(find("#{setting_field_selector}[disabled]")).to be_checked
|
||||
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
|
||||
|
||||
include_examples 'subgroup settings are disabled'
|
||||
end
|
||||
|
||||
it 'does not show enforcement checkbox in subgroups' do
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
RSpec.shared_examples 'wiki model' do
|
||||
let_it_be(:user) { create(:user, :commit_email) }
|
||||
|
||||
let(:wiki_container) { raise NotImplementedError }
|
||||
let(:wiki_container_without_repo) { raise NotImplementedError }
|
||||
let(:wiki_lfs_enabled) { false }
|
||||
|
@ -536,4 +537,94 @@ RSpec.shared_examples 'wiki model' do
|
|||
expect(subject.hook_attrs.keys).to contain_exactly(:web_url, :git_ssh_url, :git_http_url, :path_with_namespace, :default_branch)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#default_branch' do
|
||||
subject { wiki.default_branch }
|
||||
|
||||
before do
|
||||
allow(Gitlab::DefaultBranch).to receive(:value).and_return('main')
|
||||
end
|
||||
|
||||
shared_examples 'feature flag wiki_uses_default_branch is disabled' do
|
||||
it 'returns "master"' do
|
||||
stub_feature_flags(wiki_uses_default_branch: false)
|
||||
|
||||
expect(subject).to eq 'master'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when repository is not created' do
|
||||
let(:wiki_container) { wiki_container_without_repo }
|
||||
|
||||
it 'returns the instance default branch' do
|
||||
expect(subject).to eq 'main'
|
||||
end
|
||||
|
||||
it_behaves_like 'feature flag wiki_uses_default_branch is disabled'
|
||||
end
|
||||
|
||||
context 'when repository is empty' do
|
||||
let(:wiki_container) { wiki_container_without_repo }
|
||||
|
||||
before do
|
||||
wiki.repository.create_if_not_exists
|
||||
end
|
||||
|
||||
it 'returns the instance default branch' do
|
||||
expect(subject).to eq 'main'
|
||||
end
|
||||
|
||||
it_behaves_like 'feature flag wiki_uses_default_branch is disabled'
|
||||
end
|
||||
|
||||
context 'when repository is not empty' do
|
||||
it 'returns the repository default branch' do
|
||||
wiki.create_page('index', 'test content')
|
||||
|
||||
expect(subject).to eq wiki.repository.root_ref
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#create_wiki_repository' do
|
||||
subject { wiki.create_wiki_repository }
|
||||
|
||||
context 'when repository is not created' do
|
||||
let(:wiki_container) { wiki_container_without_repo }
|
||||
let(:head_path) { Rails.root.join(TestEnv.repos_path, "#{wiki.disk_path}.git", 'HEAD') }
|
||||
let(:default_branch) { 'foo' }
|
||||
|
||||
it 'changes the HEAD reference to the default branch' do
|
||||
expect(wiki.empty?).to eq true
|
||||
|
||||
allow(Gitlab::CurrentSettings).to receive(:default_branch_name).and_return(default_branch)
|
||||
|
||||
subject
|
||||
|
||||
expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}"
|
||||
end
|
||||
end
|
||||
|
||||
context 'when repository is empty' do
|
||||
let(:wiki_container) { wiki_container_without_repo }
|
||||
|
||||
it 'does nothing' do
|
||||
wiki.repository.create_if_not_exists
|
||||
|
||||
expect(wiki).not_to receive(:change_head_to_default_branch)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'when repository is not empty' do
|
||||
it 'does nothing' do
|
||||
wiki.create_page('index', 'test content')
|
||||
|
||||
expect(wiki).not_to receive(:change_head_to_default_branch)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,16 +12,18 @@ RSpec.shared_examples 'namespace traversal' do
|
|||
|
||||
it "makes a recursive query" do
|
||||
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
|
||||
|
||||
describe '#root_ancestor' 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(: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) }
|
||||
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
|
||||
expect(group.root_ancestor).to eq(group)
|
||||
expect(nested_group.root_ancestor).to eq(group)
|
||||
|
@ -29,8 +31,6 @@ RSpec.shared_examples 'namespace traversal' do
|
|||
end
|
||||
|
||||
describe '#recursive_root_ancestor' do
|
||||
let(:groups) { [group, nested_group, deep_nested_group] }
|
||||
|
||||
it "is equivalent to #recursive_root_ancestor" do
|
||||
groups.each do |group|
|
||||
expect(group.root_ancestor).to eq(group.recursive_root_ancestor)
|
||||
|
@ -40,12 +40,8 @@ RSpec.shared_examples 'namespace traversal' do
|
|||
end
|
||||
|
||||
describe '#self_and_hierarchy' do
|
||||
let!(:group) { create(:group, path: 'git_lab') }
|
||||
let!(:nested_group) { create(:group, parent: 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) }
|
||||
let!(:another_group) { create(:group) }
|
||||
let!(:another_group_nested) { create(:group, parent: another_group) }
|
||||
|
||||
it 'returns the correct tree' do
|
||||
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
|
|||
end
|
||||
|
||||
describe '#recursive_self_and_hierarchy' do
|
||||
let(:groups) { [group, nested_group, very_deep_nested_group] }
|
||||
|
||||
it_behaves_like 'recursive version', :self_and_hierarchy
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
# #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)
|
||||
|
@ -75,18 +64,28 @@ RSpec.shared_examples 'namespace traversal' do
|
|||
end
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
describe '#self_and_ancestors' do
|
||||
let(:group) { create(:group) }
|
||||
let(:nested_group) { create(:group, parent: group) }
|
||||
let(:deep_nested_group) { create(:group, parent: nested_group) }
|
||||
let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
|
||||
describe '#ancestor_ids' do
|
||||
it 'returns the correct ancestor ids' do
|
||||
expect(very_deep_nested_group.ancestor_ids).to contain_exactly(group.id, nested_group.id, deep_nested_group.id)
|
||||
expect(deep_nested_group.ancestor_ids).to contain_exactly(group.id, nested_group.id)
|
||||
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
|
||||
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)
|
||||
|
@ -95,19 +94,30 @@ RSpec.shared_examples 'namespace traversal' do
|
|||
end
|
||||
|
||||
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
|
||||
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
|
||||
let!(:group) { create(:group, path: 'git_lab') }
|
||||
let!(:nested_group) { create(:group, parent: 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) }
|
||||
let!(:another_group) { create(:group) }
|
||||
let!(:another_group_nested) { create(:group, parent: another_group) }
|
||||
|
||||
it 'returns the correct descendants' do
|
||||
expect(very_deep_nested_group.descendants.to_a).to eq([])
|
||||
|
@ -117,19 +127,13 @@ RSpec.shared_examples 'namespace traversal' do
|
|||
end
|
||||
|
||||
describe '#recursive_descendants' do
|
||||
let(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] }
|
||||
|
||||
it_behaves_like 'recursive version', :descendants
|
||||
end
|
||||
end
|
||||
|
||||
describe '#self_and_descendants' do
|
||||
let!(:group) { create(:group, path: 'git_lab') }
|
||||
let!(:nested_group) { create(:group, parent: 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) }
|
||||
let!(:another_group) { create(:group) }
|
||||
let!(:another_group_nested) { create(:group, parent: another_group) }
|
||||
|
||||
it 'returns the correct descendants' do
|
||||
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
|
|||
end
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
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) }
|
||||
|
||||
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
|
||||
let(:groups) { [group, nested_group, deep_nested_group] }
|
||||
|
||||
it_behaves_like 'recursive version', :self_and_descendant_ids
|
||||
end
|
||||
end
|
||||
|
|
|
@ -62,7 +62,7 @@ RSpec.describe 'search/_results' do
|
|||
let_it_be(:merge_request) { create(:merge_request, title: '*', source_project: project, target_project: project) }
|
||||
let_it_be(:milestone) { create(:milestone, title: '*', project: project) }
|
||||
let_it_be(:note) { create(:discussion_note_on_issue, project: project, note: '*') }
|
||||
let_it_be(:wiki_blob) { create(:wiki_page, project: project, content: '*') }
|
||||
let_it_be(:wiki_blob) { create(:wiki_page, wiki: project.wiki, content: '*') }
|
||||
let_it_be(:user) { create(:admin) }
|
||||
|
||||
%w[issues merge_requests].each do |search_scope|
|
||||
|
|
|
@ -4,19 +4,19 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe GitlabUsagePingWorker, :clean_gitlab_redis_shared_state do
|
||||
before do
|
||||
allow_next_instance_of(SubmitUsagePingService) { |service| allow(service).to receive(:execute) }
|
||||
allow_next_instance_of(ServicePing::SubmitService) { |service| allow(service).to receive(:execute) }
|
||||
allow(subject).to receive(:sleep)
|
||||
end
|
||||
|
||||
it 'does not run for GitLab.com' do
|
||||
allow(Gitlab).to receive(:com?).and_return(true)
|
||||
expect(SubmitUsagePingService).not_to receive(:new)
|
||||
expect(ServicePing::SubmitService).not_to receive(:new)
|
||||
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'delegates to SubmitUsagePingService' do
|
||||
expect_next_instance_of(SubmitUsagePingService) { |service| expect(service).to receive(:execute) }
|
||||
it 'delegates to ServicePing::SubmitService' do
|
||||
expect_next_instance_of(ServicePing::SubmitService) { |service| expect(service).to receive(:execute) }
|
||||
|
||||
subject.perform
|
||||
end
|
||||
|
@ -41,8 +41,8 @@ RSpec.describe GitlabUsagePingWorker, :clean_gitlab_redis_shared_state do
|
|||
Gitlab::ExclusiveLease.new(described_class::LEASE_KEY, timeout: described_class::LEASE_TIMEOUT).try_obtain
|
||||
end
|
||||
|
||||
it 'does not invoke SubmitUsagePingService' do
|
||||
allow_next_instance_of(SubmitUsagePingService) { |service| expect(service).not_to receive(:execute) }
|
||||
it 'does not invoke ServicePing::SubmitService' do
|
||||
allow_next_instance_of(ServicePing::SubmitService) { |service| expect(service).not_to receive(:execute) }
|
||||
|
||||
expect { subject.perform }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
|
||||
end
|
||||
|
|
|
@ -195,14 +195,15 @@ export default {
|
|||
|
||||
observeSize () {
|
||||
if (!this.vscrollResizeObserver) return
|
||||
this.vscrollResizeObserver.observe(this.$el.parentNode)
|
||||
this.$el.parentNode.addEventListener('resize', this.onResize)
|
||||
this.$_parentNode = this.$el.parentNode;
|
||||
this.vscrollResizeObserver.observe(this.$_parentNode)
|
||||
this.$_parentNode.addEventListener('resize', this.onResize)
|
||||
},
|
||||
|
||||
unobserveSize () {
|
||||
if (!this.vscrollResizeObserver) return
|
||||
this.vscrollResizeObserver.unobserve(this.$el.parentNode)
|
||||
this.$el.parentNode.removeEventListener('resize', this.onResize)
|
||||
this.vscrollResizeObserver.unobserve(this.$_parentNode)
|
||||
this.$_parentNode.removeEventListener('resize', this.onResize)
|
||||
},
|
||||
|
||||
onResize (event) {
|
||||
|
|
|
@ -572,20 +572,49 @@ export default {
|
|||
},
|
||||
|
||||
scrollToItem (index) {
|
||||
let scroll
|
||||
if (this.itemSize === null) {
|
||||
scroll = index > 0 ? this.sizes[index - 1].accumulator : 0
|
||||
} else {
|
||||
scroll = index * this.itemSize
|
||||
}
|
||||
this.scrollToPosition(scroll)
|
||||
this.$_scrollDirty = true
|
||||
const { viewport, scrollDirection, scrollDistance } = this.scrollToPosition(index)
|
||||
viewport[scrollDirection] = scrollDistance
|
||||
|
||||
setTimeout(() => {
|
||||
this.$_scrollDirty = false
|
||||
this.updateVisibleItems(false, true)
|
||||
})
|
||||
},
|
||||
|
||||
scrollToPosition (position) {
|
||||
if (this.direction === 'vertical') {
|
||||
this.$el.scrollTop = position
|
||||
} else {
|
||||
this.$el.scrollLeft = position
|
||||
scrollToPosition (index) {
|
||||
const getPositionOfItem = (index) => {
|
||||
if (this.itemSize === null) {
|
||||
return index > 0 ? this.sizes[index - 1].accumulator : 0
|
||||
} 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,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
Loading…
Reference in New Issue