Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-30 09:08:07 +00:00
parent 28fd41cf28
commit d08bee6aaf
70 changed files with 1266 additions and 240 deletions

View File

@ -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();

View File

@ -54,6 +54,7 @@ export function formatListIssues(listIssues) {
const listIssue = {
...i,
id,
fullId: i.id,
labels: i.labels?.nodes || [],
assignees: i.assignees?.nodes || [],
};

View File

@ -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],

View File

@ -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

View File

@ -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 {

View File

@ -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;

View File

@ -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

View 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"
>

View File

@ -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>

View File

@ -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);
},
};

View File

@ -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 }) => {

View File

@ -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: [],

View File

@ -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';

View File

@ -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

View File

@ -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,

View File

@ -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);
});
}

View File

@ -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 });

View File

@ -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;

View File

@ -5,6 +5,7 @@ export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
assumeImmutableResults: true,
useGet: true,
},
),

View File

@ -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';

View File

@ -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),
};
},
},

View File

@ -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 || [];

View File

@ -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>

View File

@ -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,
};

View File

@ -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
}
}
}
}
}

View File

@ -0,0 +1,9 @@
mutation issuableTodoCreate($input: TodoCreateInput!) {
todoMutation: todoCreate(input: $input) {
__typename
todo {
id
}
errors
}
}

View File

@ -0,0 +1,9 @@
mutation issuableTodoMarkDone($input: TodoMarkDoneInput!) {
todoMutation: todoMarkDone(input: $input) {
__typename
todo {
id
}
errors
}
}

View File

@ -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(),
},

View File

@ -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>

View File

@ -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;
}

View File

@ -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 {

View File

@ -472,6 +472,10 @@
.sidebar-collapsed-icon {
display: none;
}
.gl-drawer-header {
align-items: flex-start;
}
}
.board-header-collapsed-info-icon:hover {

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
1a0df6210d9ee0e0229f3cdf3e95acaaa47ebf4ca31ac0fd9f57255115355f99

View File

@ -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)),

View File

@ -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:

View File

@ -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

View File

@ -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),

View File

@ -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 ""

View File

@ -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

View File

@ -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();
});
});
});

View File

@ -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();
});
});

View File

@ -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;

View File

@ -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}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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) }

View File

@ -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

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe SubmitUsagePingService do
RSpec.describe ServicePing::SubmitService do
include StubRequests
include UsageDataHelpers

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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|

View File

@ -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

View File

@ -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) {

View File

@ -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,
}
},