Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d58a3afef8
commit
eb7e5e27af
|
|
@ -16,7 +16,7 @@ variables:
|
|||
# Retry failed specs in separate process
|
||||
QA_RETRY_FAILED_SPECS: "true"
|
||||
# Helm chart ref used by test-on-cng pipeline
|
||||
GITLAB_HELM_CHART_REF: "67dde1999693aa85eb97bebdb6ba0177814e9c6c"
|
||||
GITLAB_HELM_CHART_REF: "26a3a2715a90b3a5d9cc01fff87dac0c27c42c49"
|
||||
# Specific ref for cng-mirror project to trigger builds for
|
||||
GITLAB_CNG_MIRROR_REF: "df7aafcccafdbab732a7cf757efb3b7b74c851dd"
|
||||
# Makes sure some of the common scripts from pipeline-common use bundler to execute commands
|
||||
|
|
|
|||
|
|
@ -2595,10 +2595,8 @@ Layout/LineLength:
|
|||
- 'spec/features/profiles/user_edit_profile_spec.rb'
|
||||
- 'spec/features/projects/artifacts/file_spec.rb'
|
||||
- 'spec/features/projects/artifacts/user_downloads_artifacts_spec.rb'
|
||||
- 'spec/features/projects/blobs/blob_line_permalink_updater_spec.rb'
|
||||
- 'spec/features/projects/blobs/blob_show_spec.rb'
|
||||
- 'spec/features/projects/blobs/edit_spec.rb'
|
||||
- 'spec/features/projects/blobs/shortcuts_blob_spec.rb'
|
||||
- 'spec/features/projects/blobs/user_views_pipeline_editor_button_spec.rb'
|
||||
- 'spec/features/projects/ci/editor_spec.rb'
|
||||
- 'spec/features/projects/commit/cherry_pick_spec.rb'
|
||||
|
|
|
|||
|
|
@ -390,12 +390,9 @@ export const PROJECT_FILES_GO_BACK = {
|
|||
overrideGlobalHotkey: true,
|
||||
};
|
||||
|
||||
const { blobOverflowMenu } = gon.features ?? {};
|
||||
export const PROJECT_FILES_GO_TO_PERMALINK = {
|
||||
id: 'projectFiles.goToFilePermalink',
|
||||
description: blobOverflowMenu
|
||||
? __('Copy file permalink')
|
||||
: __('Go to file permalink (while viewing a file)'),
|
||||
export const PROJECT_FILES_COPY_FILE_PERMALINK = {
|
||||
id: 'projectFiles.copyFilePermalink',
|
||||
description: __('Copy file permalink'),
|
||||
defaultKeys: ['y'],
|
||||
};
|
||||
|
||||
|
|
@ -681,7 +678,7 @@ const PROJECT_FILES_SHORTCUTS_GROUP = {
|
|||
PROJECT_FILES_MOVE_SELECTION_DOWN,
|
||||
PROJECT_FILES_OPEN_SELECTION,
|
||||
PROJECT_FILES_GO_BACK,
|
||||
PROJECT_FILES_GO_TO_PERMALINK,
|
||||
PROJECT_FILES_COPY_FILE_PERMALINK,
|
||||
PROJECT_FILES_GO_TO_COMPARE,
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import { PROJECT_FILES_GO_TO_PERMALINK } from '~/behaviors/shortcuts/keybindings';
|
||||
import { moveToFilePermalink } from '~/blob/utils';
|
||||
|
||||
export default class ShortcutsBlob {
|
||||
constructor(shortcuts) {
|
||||
const { blobOverflowMenu } = gon.features ?? {};
|
||||
if (blobOverflowMenu) {
|
||||
// TODO: Remove ShortcutsBlob entirely once these feature flags are removed.
|
||||
return;
|
||||
}
|
||||
|
||||
shortcuts.add(PROJECT_FILES_GO_TO_PERMALINK, moveToFilePermalink);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { getLocationHash } from '~/lib/utils/url_utility';
|
||||
import { getPageParamValue, getPageSearchString } from './utils';
|
||||
|
||||
const lineNumberRe = /^(L|LC)[0-9]+/;
|
||||
|
||||
const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
|
||||
const hash = getLocationHash();
|
||||
if (hash && lineNumberRe.test(hash)) {
|
||||
const hashUrlString = `#${hash}`;
|
||||
|
||||
[].concat(Array.prototype.slice.call(linksToUpdate)).forEach((permalinkButton) => {
|
||||
const baseHref =
|
||||
permalinkButton.dataset.originalHref ||
|
||||
(() => {
|
||||
const href = permalinkButton.getAttribute('href');
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
permalinkButton.dataset.originalHref = href;
|
||||
return href;
|
||||
})();
|
||||
const lineNum = parseInt(hash.split('L')[1], 10);
|
||||
const page = getPageParamValue(lineNum);
|
||||
const searchString = getPageSearchString(baseHref, page);
|
||||
permalinkButton.setAttribute('href', `${baseHref}${searchString}${hashUrlString}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function BlobLinePermalinkUpdater(blobContentHolder, lineNumberSelector, elementsToUpdate) {
|
||||
if (!blobContentHolder) return;
|
||||
const updateBlameAndBlobPermalinkCb = () => {
|
||||
// Wait for the hash to update from the LineHighlighter callback
|
||||
setTimeout(() => {
|
||||
updateLineNumbersOnBlobPermalinks(elementsToUpdate);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
blobContentHolder.addEventListener('click', (e) => {
|
||||
if (e.target.matches(lineNumberSelector)) {
|
||||
updateBlameAndBlobPermalinkCb();
|
||||
}
|
||||
});
|
||||
updateBlameAndBlobPermalinkCb();
|
||||
}
|
||||
|
||||
export default BlobLinePermalinkUpdater;
|
||||
|
|
@ -1,9 +1,5 @@
|
|||
<script>
|
||||
import DefaultActions from 'jh_else_ce/blob/components/blob_header_default_actions.vue';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import userInfoQuery from '../queries/user_info.query.graphql';
|
||||
import applicationInfoQuery from '../queries/application_info.query.graphql';
|
||||
import BlobFilepath from './blob_header_filepath.vue';
|
||||
import ViewerSwitcher from './blob_header_viewer_switcher.vue';
|
||||
import { SIMPLE_BLOB_VIEWER, BLAME_VIEWER } from './constants';
|
||||
|
|
@ -15,23 +11,6 @@ export default {
|
|||
DefaultActions,
|
||||
BlobFilepath,
|
||||
TableOfContents,
|
||||
WebIdeLink: () => import('ee_else_ce/vue_shared/components/web_ide_link.vue'),
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
apollo: {
|
||||
// eslint-disable-next-line @gitlab/vue-no-undef-apollo-properties
|
||||
currentUser: {
|
||||
query: userInfoQuery,
|
||||
error() {
|
||||
this.$emit('error');
|
||||
},
|
||||
},
|
||||
gitpodEnabled: {
|
||||
query: applicationInfoQuery,
|
||||
error() {
|
||||
this.$emit('error');
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
blob: {
|
||||
|
|
@ -73,21 +52,8 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
showForkSuggestion: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
showWebIdeForkSuggestion: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
// Used in EE component
|
||||
// eslint-disable-next-line vue/no-unused-properties
|
||||
projectId: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
|
@ -103,36 +69,16 @@ export default {
|
|||
required: false,
|
||||
default: true,
|
||||
},
|
||||
editButtonVariant: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'confirm',
|
||||
},
|
||||
isUsingLfs: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
viewer: this.hideViewerSwitcher ? null : this.activeViewerType,
|
||||
gitpodEnabled: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showWebIdeLink() {
|
||||
return !this.blob.archived && this.blob.editBlobPath;
|
||||
},
|
||||
isEmpty() {
|
||||
return this.blob.rawSize === '0';
|
||||
},
|
||||
blobSwitcherDocIcon() {
|
||||
return this.blob.richViewer?.fileType === 'csv' ? 'table' : 'document';
|
||||
},
|
||||
projectIdAsNumber() {
|
||||
return getIdFromGraphQLId(this.projectId);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
viewer(newVal, oldVal) {
|
||||
|
|
@ -151,8 +97,6 @@ export default {
|
|||
<template>
|
||||
<div class="js-file-title file-title-flex-parent">
|
||||
<div class="gl-mb-3 gl-flex gl-gap-3 md:gl-mb-0">
|
||||
<table-of-contents v-if="!glFeatures.blobOverflowMenu" class="gl-pr-2" />
|
||||
|
||||
<blob-filepath
|
||||
:blob="blob"
|
||||
:show-path="showPath"
|
||||
|
|
@ -169,37 +113,12 @@ export default {
|
|||
<viewer-switcher
|
||||
v-if="!hideViewerSwitcher"
|
||||
v-model="viewer"
|
||||
:doc-icon="blobSwitcherDocIcon"
|
||||
:show-blame-toggle="showBlameToggle"
|
||||
:show-viewer-toggles="Boolean(blob.simpleViewer && blob.richViewer)"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
|
||||
<table-of-contents v-if="glFeatures.blobOverflowMenu" class="gl-pr-2" />
|
||||
<table-of-contents class="gl-pr-2" />
|
||||
<slot name="ee-duo-workflow-action" data-test-id="ee-duo-workflow-action"></slot>
|
||||
<web-ide-link
|
||||
v-if="!glFeatures.blobOverflowMenu && showWebIdeLink"
|
||||
:disabled="isUsingLfs"
|
||||
:show-edit-button="!isBinary"
|
||||
:button-variant="editButtonVariant"
|
||||
class="sm:!gl-ml-0"
|
||||
:edit-url="blob.editBlobPath"
|
||||
:web-ide-url="blob.ideEditPath"
|
||||
:needs-to-fork="showForkSuggestion"
|
||||
:needs-to-fork-with-web-ide="showWebIdeForkSuggestion"
|
||||
:show-pipeline-editor-button="Boolean(blob.pipelineEditorPath)"
|
||||
:pipeline-editor-url="blob.pipelineEditorPath"
|
||||
:gitpod-url="blob.gitpodBlobUrl"
|
||||
:is-gitpod-enabled-for-instance="gitpodEnabled"
|
||||
:is-gitpod-enabled-for-user="currentUser && currentUser.gitpodEnabled"
|
||||
:project-path="projectPath"
|
||||
:project-id="projectIdAsNumber"
|
||||
:user-preferences-gitpod-path="currentUser && currentUser.preferencesGitpodPath"
|
||||
:user-profile-enable-gitpod-path="currentUser && currentUser.profileEnableGitpodPath"
|
||||
is-blob
|
||||
disable-fork-modal
|
||||
v-on="$listeners"
|
||||
/>
|
||||
|
||||
<slot name="actions"></slot>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { sprintf, s__ } from '~/locale';
|
||||
import { setUrlParams, relativePathToAbsolute, getBaseURL } from '~/lib/utils/url_utility';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import {
|
||||
BTN_COPY_CONTENTS_TITLE,
|
||||
BTN_DOWNLOAD_TITLE,
|
||||
|
|
@ -19,7 +18,6 @@ export default {
|
|||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: {
|
||||
blobHash: {
|
||||
default: '',
|
||||
|
|
@ -105,10 +103,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-button-group
|
||||
:class="{ 'gl-hidden sm:gl-inline-flex': glFeatures.blobOverflowMenu }"
|
||||
data-testid="default-actions-container"
|
||||
>
|
||||
<gl-button-group class="gl-hidden sm:gl-inline-flex" data-testid="default-actions-container">
|
||||
<gl-button
|
||||
v-if="!isEmpty && showCopyButton"
|
||||
v-gl-tooltip.hover
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { InternalEvents } from '~/tracking';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import {
|
||||
RICH_BLOB_VIEWER,
|
||||
RICH_BLOB_VIEWER_TITLE,
|
||||
|
|
@ -21,18 +20,13 @@ export default {
|
|||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [InternalEvents.mixin(), glFeatureFlagMixin()],
|
||||
mixins: [InternalEvents.mixin()],
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: SIMPLE_BLOB_VIEWER,
|
||||
required: false,
|
||||
},
|
||||
docIcon: {
|
||||
type: String,
|
||||
default: 'document',
|
||||
required: false,
|
||||
},
|
||||
showViewerToggles: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
@ -86,13 +80,12 @@ export default {
|
|||
:title="$options.SIMPLE_BLOB_VIEWER_TITLE"
|
||||
:selected="isSimpleViewer"
|
||||
data-testid="simple-blob-viewer-button"
|
||||
:icon="glFeatures.blobOverflowMenu ? '' : 'code'"
|
||||
category="primary"
|
||||
variant="default"
|
||||
class="js-blob-viewer-switch-btn"
|
||||
data-viewer="simple"
|
||||
@click="switchToViewer($options.SIMPLE_BLOB_VIEWER)"
|
||||
>{{ glFeatures.blobOverflowMenu ? $options.SIMPLE_BLOB_VIEWER_LABEL : '' }}</gl-button
|
||||
>{{ $options.SIMPLE_BLOB_VIEWER_LABEL }}</gl-button
|
||||
>
|
||||
<gl-button
|
||||
v-if="showViewerToggles"
|
||||
|
|
@ -100,14 +93,14 @@ export default {
|
|||
:aria-label="$options.RICH_BLOB_VIEWER_TITLE"
|
||||
:title="$options.RICH_BLOB_VIEWER_TITLE"
|
||||
:selected="isRichViewer"
|
||||
:icon="glFeatures.blobOverflowMenu ? '' : docIcon"
|
||||
icon=""
|
||||
data-testid="rich-blob-viewer-button"
|
||||
category="primary"
|
||||
variant="default"
|
||||
class="js-blob-viewer-switch-btn"
|
||||
data-viewer="rich"
|
||||
@click="switchToViewer($options.RICH_BLOB_VIEWER)"
|
||||
>{{ glFeatures.blobOverflowMenu ? $options.RICH_BLOB_VIEWER_LABEL : '' }}</gl-button
|
||||
>{{ $options.RICH_BLOB_VIEWER_LABEL }}</gl-button
|
||||
>
|
||||
<gl-button
|
||||
v-if="showBlameToggle"
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
query getUserInfo {
|
||||
currentUser {
|
||||
id
|
||||
gitpodEnabled
|
||||
preferencesGitpodPath
|
||||
profileEnableGitpodPath
|
||||
}
|
||||
}
|
||||
|
|
@ -112,7 +112,14 @@ export default {
|
|||
class="js-user-link gl-relative"
|
||||
data-testid="assigned-user"
|
||||
>
|
||||
<gl-avatar :src="user.avatarUrl" :size="32" class="!gl-bg-subtle" />
|
||||
<gl-avatar
|
||||
:src="user.avatarUrl"
|
||||
:size="32"
|
||||
width="32"
|
||||
height="32"
|
||||
class="!gl-bg-subtle"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span
|
||||
v-if="reviewStateIcon(user)"
|
||||
class="gl-absolute -gl-bottom-2 -gl-left-2 gl-flex gl-h-5 gl-w-5 gl-items-center gl-justify-center gl-rounded-full gl-p-1"
|
||||
|
|
|
|||
|
|
@ -8,14 +8,4 @@ initNewResourceDropdown({
|
|||
query: searchUserProjectsWithMergeRequestsEnabled,
|
||||
});
|
||||
|
||||
const el = document.getElementById('js-merge-request-dashboard');
|
||||
|
||||
if (el) {
|
||||
requestIdleCallback(async () => {
|
||||
const { initMergeRequestDashboard } = await import('~/merge_request_dashboard');
|
||||
|
||||
initMergeRequestDashboard(el);
|
||||
});
|
||||
} else {
|
||||
initMergeRequestsDashboard();
|
||||
}
|
||||
initMergeRequestsDashboard();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import { RESOURCE_TYPE_MERGE_REQUEST } from '~/vue_shared/components/new_resource_dropdown/constants';
|
||||
import searchUserProjectsWithMergeRequestsEnabled from '~/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_merge_requests_enabled.query.graphql';
|
||||
import { initMergeRequestDashboard } from '~/merge_request_dashboard';
|
||||
|
||||
initMergeRequestDashboard(document.getElementById('js-merge-request-dashboard'));
|
||||
|
||||
requestIdleCallback(async () => {
|
||||
const { initNewResourceDropdown } = await import(
|
||||
'~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown'
|
||||
);
|
||||
|
||||
initNewResourceDropdown({
|
||||
resourceType: RESOURCE_TYPE_MERGE_REQUEST,
|
||||
query: searchUserProjectsWithMergeRequestsEnabled,
|
||||
});
|
||||
});
|
||||
|
|
@ -1,25 +1,15 @@
|
|||
import { addShortcutsExtension } from '~/behaviors/shortcuts';
|
||||
import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob';
|
||||
import { shortcircuitPermalinkButton } from '~/blob/utils';
|
||||
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
||||
import BlobForkSuggestion from '~/blob/blob_fork_suggestion';
|
||||
import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater';
|
||||
import LineHighlighter from '~/blob/line_highlighter';
|
||||
|
||||
export default () => {
|
||||
new LineHighlighter(); // eslint-disable-line no-new
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new BlobLinePermalinkUpdater(
|
||||
document.querySelector('#blob-content-holder'),
|
||||
'.file-line-num[data-line-number], .file-line-num[data-line-number] *',
|
||||
document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'),
|
||||
);
|
||||
|
||||
shortcircuitPermalinkButton();
|
||||
|
||||
addShortcutsExtension(ShortcutsNavigation);
|
||||
addShortcutsExtension(ShortcutsBlob);
|
||||
|
||||
new BlobForkSuggestion({
|
||||
openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'),
|
||||
|
|
|
|||
|
|
@ -1,155 +0,0 @@
|
|||
<script>
|
||||
import { GlButtonGroup, GlButton } from '@gitlab/ui';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import getRefMixin from '../mixins/get_ref';
|
||||
import DeleteBlobModal from './delete_blob_modal.vue';
|
||||
import UploadBlobModal from './upload_blob_modal.vue';
|
||||
|
||||
const REPLACE_BLOB_MODAL_ID = 'modal-replace-blob';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
replace: __('Replace'),
|
||||
delete: __('Delete'),
|
||||
},
|
||||
components: {
|
||||
GlButtonGroup,
|
||||
GlButton,
|
||||
UploadBlobModal,
|
||||
DeleteBlobModal,
|
||||
LockFileButton: () => import('ee_component/repository/components/lock_file_button.vue'),
|
||||
},
|
||||
mixins: [getRefMixin, glFeatureFlagMixin()],
|
||||
inject: {
|
||||
targetBranch: {
|
||||
default: '',
|
||||
},
|
||||
originalBranch: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
replacePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
deletePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
canPushCode: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
canPushToBranch: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
emptyRepo: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isLocked: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
canLock: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
showForkSuggestion: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isUsingLfs: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
deleteModalId() {
|
||||
return uniqueId('delete-modal');
|
||||
},
|
||||
replaceCommitMessage() {
|
||||
return sprintf(__('Replace %{name}'), { name: this.name });
|
||||
},
|
||||
deleteModalCommitMessage() {
|
||||
return sprintf(__('Delete %{name}'), { name: this.name });
|
||||
},
|
||||
lockBtnTestId() {
|
||||
return this.canLock ? 'lock-button' : 'disabled-lock-button';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showModal(modalId) {
|
||||
if (this.showForkSuggestion) {
|
||||
this.$emit('fork', 'view');
|
||||
return;
|
||||
}
|
||||
|
||||
this.$refs[modalId].show();
|
||||
},
|
||||
},
|
||||
replaceBlobModalId: REPLACE_BLOB_MODAL_ID,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<gl-button-group>
|
||||
<lock-file-button
|
||||
v-if="glFeatures.fileLocks"
|
||||
:name="name"
|
||||
:path="path"
|
||||
:project-path="projectPath"
|
||||
:is-locked="isLocked"
|
||||
:can-lock="canLock"
|
||||
:data-testid="lockBtnTestId"
|
||||
/>
|
||||
<gl-button data-testid="replace" @click="showModal($options.replaceBlobModalId)">
|
||||
{{ $options.i18n.replace }}
|
||||
</gl-button>
|
||||
<gl-button data-testid="delete" @click="showModal(deleteModalId)">
|
||||
{{ $options.i18n.delete }}
|
||||
</gl-button>
|
||||
</gl-button-group>
|
||||
<upload-blob-modal
|
||||
:ref="$options.replaceBlobModalId"
|
||||
:modal-id="$options.replaceBlobModalId"
|
||||
:commit-message="replaceCommitMessage"
|
||||
:target-branch="targetBranch || ref"
|
||||
:original-branch="originalBranch || ref"
|
||||
:can-push-code="canPushCode"
|
||||
:can-push-to-branch="canPushToBranch"
|
||||
:path="path"
|
||||
:replace-path="replacePath"
|
||||
/>
|
||||
<delete-blob-modal
|
||||
:ref="deleteModalId"
|
||||
:delete-path="deletePath"
|
||||
:modal-id="deleteModalId"
|
||||
:commit-message="deleteModalCommitMessage"
|
||||
:target-branch="targetBranch || ref"
|
||||
:original-branch="originalBranch || ref"
|
||||
:can-push-code="canPushCode"
|
||||
:can-push-to-branch="canPushToBranch"
|
||||
:empty-repo="emptyRepo"
|
||||
:is-using-lfs="isUsingLfs"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -29,8 +29,6 @@ import {
|
|||
EMPTY_FILE,
|
||||
EVENT_FILE_SIZE_LIMIT_EXCEEDED,
|
||||
} from '../constants';
|
||||
import BlobButtonGroup from './blob_button_group.vue';
|
||||
import ForkSuggestion from './fork_suggestion.vue';
|
||||
import { loadViewer } from './blob_viewers';
|
||||
|
||||
const trackingMixin = InternalEvents.mixin();
|
||||
|
|
@ -38,11 +36,9 @@ const trackingMixin = InternalEvents.mixin();
|
|||
export default {
|
||||
components: {
|
||||
BlobHeader,
|
||||
BlobButtonGroup,
|
||||
BlobContent,
|
||||
GlLoadingIcon,
|
||||
GlButton,
|
||||
ForkSuggestion,
|
||||
CodeIntelligence,
|
||||
AiGenie: () => import('ee_component/ai/components/ai_genie.vue'),
|
||||
},
|
||||
|
|
@ -408,30 +404,6 @@ export default {
|
|||
@edit="editBlob"
|
||||
@error="displayError"
|
||||
@blame="handleToggleBlame"
|
||||
>
|
||||
<template #actions>
|
||||
<blob-button-group
|
||||
v-if="isLoggedIn && !blobInfo.archived && !glFeatures.blobOverflowMenu"
|
||||
:path="path"
|
||||
:name="blobInfo.name"
|
||||
:replace-path="blobInfo.replacePath"
|
||||
:delete-path="blobInfo.webPath"
|
||||
:can-push-code="userPermissions.pushCode"
|
||||
:can-push-to-branch="blobInfo.canCurrentUserPushToBranch"
|
||||
:empty-repo="isEmptyRepository"
|
||||
:project-path="projectPath"
|
||||
:is-locked="Boolean(pathLockedByUser)"
|
||||
:can-lock="canLock"
|
||||
:show-fork-suggestion="showSingleFileEditorForkSuggestion"
|
||||
:is-using-lfs="isUsingLfs"
|
||||
@fork="setForkTarget('view')"
|
||||
/>
|
||||
</template>
|
||||
</blob-header>
|
||||
<fork-suggestion
|
||||
v-if="forkTarget && showForkSuggestion"
|
||||
:fork-path="forkPath"
|
||||
@cancel="setForkTarget(null)"
|
||||
/>
|
||||
<blob-content
|
||||
v-if="!blobViewer"
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
message: __(
|
||||
'You can’t edit files directly in this project. Fork this project and submit a merge request with your changes.',
|
||||
),
|
||||
fork: __('Fork'),
|
||||
cancel: __('Cancel'),
|
||||
},
|
||||
components: {
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
forkPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="gl-flex gl-items-center gl-justify-end gl-border-1 gl-border-default gl-bg-subtle gl-px-5 gl-py-2 gl-border-b-solid"
|
||||
>
|
||||
<span class="gl-mr-6" data-testid="message">{{ $options.i18n.message }}</span>
|
||||
|
||||
<gl-button
|
||||
class="gl-mr-3"
|
||||
category="secondary"
|
||||
variant="confirm"
|
||||
data-method="post"
|
||||
:href="forkPath"
|
||||
data-testid="fork"
|
||||
>
|
||||
{{ $options.i18n.fork }}
|
||||
</gl-button>
|
||||
|
||||
<gl-button data-testid="cancel" @click="$emit('cancel')">
|
||||
{{ $options.i18n.cancel }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -10,15 +10,12 @@ import getRefMixin from '~/repository/mixins/get_ref';
|
|||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import initSourcegraph from '~/sourcegraph';
|
||||
import Shortcuts from '~/behaviors/shortcuts/shortcuts';
|
||||
import { addShortcutsExtension } from '~/behaviors/shortcuts';
|
||||
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
|
||||
import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob';
|
||||
import { shortcircuitPermalinkButton } from '~/blob/utils';
|
||||
import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater';
|
||||
import {
|
||||
keysFor,
|
||||
START_SEARCH_PROJECT_FILE,
|
||||
PROJECT_FILES_GO_TO_PERMALINK,
|
||||
PROJECT_FILES_COPY_FILE_PERMALINK,
|
||||
} from '~/behaviors/shortcuts/keybindings';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
import { InternalEvents } from '~/tracking';
|
||||
|
|
@ -32,7 +29,7 @@ import {
|
|||
import { showBlameButton, isUsingLfs } from '~/repository/utils/storage_info_utils';
|
||||
import blobControlsQuery from '~/repository/queries/blob_controls.query.graphql';
|
||||
import userGitpodInfo from '~/repository/queries/user_gitpod_info.query.graphql';
|
||||
import applicationInfoQuery from '~/blob/queries/application_info.query.graphql';
|
||||
import applicationInfoQuery from '~/repository/queries/application_info.query.graphql';
|
||||
import { getRefType } from '~/repository/utils/ref_type';
|
||||
import OpenMrBadge from '~/repository/components/header_area/open_mr_badge.vue';
|
||||
import BlobOverflowMenu from 'ee_else_ce/repository/components/header_area/blob_overflow_menu.vue';
|
||||
|
|
@ -75,6 +72,7 @@ export default {
|
|||
return !this.filePath;
|
||||
},
|
||||
error(error) {
|
||||
this.hasProjectQueryErrors = true;
|
||||
createAlert({ message: this.$options.i18n.errorMessage });
|
||||
logError(
|
||||
`Failed to fetch blob controls. See exception details for more information.`,
|
||||
|
|
@ -139,6 +137,7 @@ export default {
|
|||
currentUser: {},
|
||||
gitpodEnabled: false,
|
||||
isForkSuggestionModalVisible: false,
|
||||
hasProjectQueryErrors: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -151,6 +150,9 @@ export default {
|
|||
blobInfo() {
|
||||
return this.project?.repository?.blobs?.nodes[0] || {};
|
||||
},
|
||||
repository() {
|
||||
return this.project?.repository || DEFAULT_BLOB_INFO.repository;
|
||||
},
|
||||
userPermissions() {
|
||||
return this.project?.userPermissions || DEFAULT_BLOB_INFO.userPermissions;
|
||||
},
|
||||
|
|
@ -172,7 +174,7 @@ export default {
|
|||
},
|
||||
shortcuts() {
|
||||
const findFileKey = keysFor(START_SEARCH_PROJECT_FILE)[0];
|
||||
const permalinkKey = keysFor(PROJECT_FILES_GO_TO_PERMALINK)[0];
|
||||
const permalinkKey = keysFor(PROJECT_FILES_COPY_FILE_PERMALINK)[0];
|
||||
|
||||
return {
|
||||
findFile: findFileKey,
|
||||
|
|
@ -220,7 +222,6 @@ export default {
|
|||
initSourcegraph();
|
||||
this.$nextTick(() => {
|
||||
this.initShortcuts();
|
||||
this.initLinksUpdate();
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
@ -230,15 +231,6 @@ export default {
|
|||
},
|
||||
initShortcuts() {
|
||||
shortcircuitPermalinkButton();
|
||||
addShortcutsExtension(ShortcutsBlob);
|
||||
},
|
||||
initLinksUpdate() {
|
||||
// eslint-disable-next-line no-new
|
||||
new BlobLinePermalinkUpdater(
|
||||
document.querySelector('.tree-holder'),
|
||||
'.file-line-num[data-line-number], .file-line-num[data-line-number] *',
|
||||
document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'),
|
||||
);
|
||||
},
|
||||
handleFindFile() {
|
||||
this.trackEvent(FIND_FILE_BUTTON_CLICK);
|
||||
|
|
@ -287,10 +279,7 @@ export default {
|
|||
v-gl-tooltip.html="findFileTooltip"
|
||||
:aria-keyshortcuts="findFileShortcutKey"
|
||||
data-testid="find"
|
||||
:class="[
|
||||
$options.buttonClassList,
|
||||
{ 'gl-hidden sm:gl-inline-flex': glFeatures.blobOverflowMenu },
|
||||
]"
|
||||
:class="[$options.buttonClassList, 'gl-hidden sm:gl-inline-flex']"
|
||||
@click="handleFindFile"
|
||||
>
|
||||
{{ $options.i18n.findFile }}
|
||||
|
|
@ -299,30 +288,15 @@ export default {
|
|||
v-if="showBlameButton"
|
||||
data-testid="blame"
|
||||
:href="blobInfo.blamePath"
|
||||
:class="[
|
||||
$options.buttonClassList,
|
||||
{ 'gl-hidden sm:gl-inline-flex': glFeatures.blobOverflowMenu },
|
||||
]"
|
||||
:class="[$options.buttonClassList, 'gl-hidden sm:gl-inline-flex']"
|
||||
class="js-blob-blame-link"
|
||||
@click="handleBlameClick"
|
||||
>
|
||||
{{ $options.i18n.blame }}
|
||||
</gl-button>
|
||||
|
||||
<gl-button
|
||||
v-if="!glFeatures.blobOverflowMenu"
|
||||
v-gl-tooltip.html="permalinkTooltip"
|
||||
:aria-keyshortcuts="permalinkShortcutKey"
|
||||
data-testid="permalink"
|
||||
:href="blobInfo.permalinkPath"
|
||||
:class="$options.buttonClassList"
|
||||
class="js-data-file-blob-permalink-url"
|
||||
>
|
||||
{{ $options.i18n.permalink }}
|
||||
</gl-button>
|
||||
|
||||
<web-ide-link
|
||||
v-if="glFeatures.blobOverflowMenu && showWebIdeLink"
|
||||
v-if="showWebIdeLink && !hasProjectQueryErrors"
|
||||
:show-edit-button="!isBinaryFileType"
|
||||
class="!gl-m-0"
|
||||
:edit-url="blobInfo.editBlobPath"
|
||||
|
|
@ -344,7 +318,7 @@ export default {
|
|||
@edit="onEdit"
|
||||
/>
|
||||
<fork-suggestion-modal
|
||||
v-if="!isLoadingRepositoryBlob"
|
||||
v-if="!isLoadingRepositoryBlob && !hasProjectQueryErrors"
|
||||
:visible="isForkSuggestionModalVisible"
|
||||
:fork-path="blobInfo.forkAndViewPath"
|
||||
@hide="isForkSuggestionModalVisible = false"
|
||||
|
|
@ -360,11 +334,11 @@ export default {
|
|||
:inline="false"
|
||||
/>
|
||||
<blob-overflow-menu
|
||||
v-if="!isLoadingRepositoryBlob && glFeatures.blobOverflowMenu"
|
||||
v-if="!isLoadingRepositoryBlob && !hasProjectQueryErrors"
|
||||
:project-path="projectPath"
|
||||
:is-binary-file-type="isBinaryFileType"
|
||||
:override-copy="true"
|
||||
:is-empty-repository="project.repository.empty"
|
||||
:is-empty-repository="repository.empty"
|
||||
:is-using-lfs="isUsingLfs"
|
||||
@copy="onCopy"
|
||||
@showForkSuggestion="onShowForkSuggestion"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { GlDisclosureDropdown, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { computed } from 'vue';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { __ } from '~/locale';
|
||||
import { createAlert } from '~/alert';
|
||||
import { isLoggedIn } from '~/lib/utils/common_utils';
|
||||
|
|
@ -31,6 +32,7 @@ export default {
|
|||
directives: {
|
||||
GlTooltipDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: ['blobInfo', 'currentRef'],
|
||||
provide() {
|
||||
return {
|
||||
|
|
@ -65,22 +67,22 @@ export default {
|
|||
eeCanModifyFile: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: undefined,
|
||||
default: false,
|
||||
},
|
||||
eeCanCreateLock: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: undefined,
|
||||
default: false,
|
||||
},
|
||||
eeCanDestroyLock: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: undefined,
|
||||
default: false,
|
||||
},
|
||||
eeIsLocked: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: undefined,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
|
|
@ -120,10 +122,10 @@ export default {
|
|||
return Boolean(this.viewer.renderError);
|
||||
},
|
||||
canModifyFile() {
|
||||
return this.eeCanModifyFile !== undefined ? this.eeCanModifyFile : true;
|
||||
return this.glFeatures.fileLocks ? this.eeCanModifyFile : true;
|
||||
},
|
||||
isLocked() {
|
||||
return this.eeIsLocked !== undefined ? this.eeIsLocked : false;
|
||||
return this.glFeatures.fileLocks ? this.eeIsLocked : false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import Vue from 'vue';
|
|||
import { GlDisclosureDropdownItem, GlToast } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { InternalEvents } from '~/tracking';
|
||||
import { keysFor, PROJECT_FILES_GO_TO_PERMALINK } from '~/behaviors/shortcuts/keybindings';
|
||||
import { keysFor, PROJECT_FILES_COPY_FILE_PERMALINK } from '~/behaviors/shortcuts/keybindings';
|
||||
import { Mousetrap } from '~/lib/mousetrap';
|
||||
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
|
||||
import { hashState, updateHash } from '~/blob/state';
|
||||
|
|
@ -34,7 +34,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
permalinkShortcutKey() {
|
||||
return keysFor(PROJECT_FILES_GO_TO_PERMALINK)[0];
|
||||
return keysFor(PROJECT_FILES_COPY_FILE_PERMALINK)[0];
|
||||
},
|
||||
shortcutsDisabled() {
|
||||
return shouldDisableShortcuts();
|
||||
|
|
@ -45,11 +45,11 @@ export default {
|
|||
},
|
||||
mounted() {
|
||||
this.mousetrap = new Mousetrap();
|
||||
this.mousetrap.bind(keysFor(PROJECT_FILES_GO_TO_PERMALINK), this.triggerCopyPermalink);
|
||||
this.mousetrap.bind(keysFor(PROJECT_FILES_COPY_FILE_PERMALINK), this.triggerCopyPermalink);
|
||||
window.addEventListener('hashchange', this.onHashChange);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.mousetrap.unbind(keysFor(PROJECT_FILES_GO_TO_PERMALINK));
|
||||
this.mousetrap.unbind(keysFor(PROJECT_FILES_COPY_FILE_PERMALINK));
|
||||
window.removeEventListener('hashchange', this.onHashChange);
|
||||
},
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div data-testid="snippet-header">
|
||||
<div class="gl-flex gl-flex-col gl-items-start gl-gap-3 gl-pt-3 sm:gl-flex-row">
|
||||
<span
|
||||
v-if="snippet.hidden"
|
||||
|
|
|
|||
|
|
@ -596,8 +596,7 @@ export default {
|
|||
@mouseout="clearPrefetching"
|
||||
@removeChild="removeChild"
|
||||
@error="$emit('error', $event)"
|
||||
@click.stop="onClick($event, child)"
|
||||
@click.native="onClick($event, child)"
|
||||
@toggleDrawer="onClick($event, child)"
|
||||
/>
|
||||
</component>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ export default {
|
|||
:work-item-full-path="workItemFullPath"
|
||||
:show-weight="shouldShowWeight"
|
||||
:contextual-view-enabled="contextualViewEnabled"
|
||||
@click="$emit('click', $event)"
|
||||
@click="$emit('toggleDrawer', $event)"
|
||||
@removeChild="$emit('removeChild', childItem)"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -320,7 +320,7 @@ export default {
|
|||
@drop="$emit('drop')"
|
||||
@removeChild="$emit('removeChild', childItem)"
|
||||
@error="$emit('error', $event)"
|
||||
@click="$emit('click', $event)"
|
||||
@click="$emit('toggleDrawer', $event)"
|
||||
/>
|
||||
<work-item-children-load-more
|
||||
v-if="hasNextPage && isExpanded"
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:inline_blame, @project)
|
||||
push_frontend_feature_flag(:blob_overflow_menu, current_user)
|
||||
push_frontend_feature_flag(:filter_blob_path, current_user)
|
||||
push_frontend_feature_flag(:blob_repository_vue_header_app, @project)
|
||||
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ class Projects::TreeController < Projects::ApplicationController
|
|||
before_action do
|
||||
push_frontend_feature_flag(:inline_blame, @project)
|
||||
push_frontend_feature_flag(:blob_repository_vue_header_app, @project)
|
||||
push_frontend_feature_flag(:blob_overflow_menu, current_user)
|
||||
push_frontend_feature_flag(:filter_blob_path, current_user)
|
||||
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
|
||||
push_frontend_feature_flag(:directory_code_dropdown_updates, current_user)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ class ProjectsController < Projects::ApplicationController
|
|||
# TODO: We need to remove the FF eventually when we rollout page_specific_styles
|
||||
push_frontend_feature_flag(:page_specific_styles, current_user)
|
||||
push_frontend_feature_flag(:blob_repository_vue_header_app, @project)
|
||||
push_frontend_feature_flag(:blob_overflow_menu, current_user)
|
||||
push_frontend_feature_flag(:filter_blob_path, current_user)
|
||||
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
|
||||
push_frontend_feature_flag(:directory_code_dropdown_updates, current_user)
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ module WorkItems
|
|||
context = { original: original_work_item, user: current_user }
|
||||
|
||||
# We need this because move work item is supposed to work with epics and for EpicWorkItem
|
||||
# Issues::CloseService also enqueues a job for ::WorkItems::ValidateEpicWorkItemSyncWorker and because
|
||||
# this is being run within a transaction, we are delaying the close operation until after commit.
|
||||
# Issues::CloseService. Because this is being run within a transaction,
|
||||
# we are delaying the close operation until after commit.
|
||||
new_work_item.run_after_commit_or_now do
|
||||
close_service = ::Issues::CloseService.new(
|
||||
container: context[:original].namespace, current_user: context[:user]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
page_title title
|
||||
@breadcrumb_link = merge_requests_dashboard_path(assignee_username: current_user.username)
|
||||
add_page_specific_style 'page_bundles/issuable_list'
|
||||
@js_action_name = 'merge_requests_v2' if current_user.merge_request_dashboard_enabled? && !current_page?(merge_requests_search_dashboard_path)
|
||||
|
||||
= render_dashboard_ultimate_trial(current_user)
|
||||
= render_product_usage_data_collection_changes(current_user)
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: blob_overflow_menu
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/450774
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/174992
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/522993
|
||||
milestone: '17.10'
|
||||
group: group::source code
|
||||
type: beta
|
||||
default_enabled: true
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
migration_job_name: BackfillPlaceholderUsersDetailsFromSourceUsers
|
||||
description: Backfill placeholder user details for all existing placeholder users into the Import::PlaceholderUserDetail table.
|
||||
feature_category: importers
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/192497
|
||||
milestone: '18.1'
|
||||
queued_migration_version: 20250526065102
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class QueueBackfillPlaceholderUsersDetailsFromSourceUsers < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.1'
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
MIGRATION = "BackfillPlaceholderUsersDetailsFromSourceUsers"
|
||||
BATCH_SIZE = 1000
|
||||
SUB_BATCH_SIZE = 100
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
|
||||
def up
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:import_source_users,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
delete_batched_background_migration(MIGRATION, :import_source_users, :id, [])
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveCustomerRelationContactsForEpicType < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.1'
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
disable_ddl_transaction!
|
||||
|
||||
BATCH_SIZE = 1000
|
||||
EPIC_WORK_ITEM_TYPE_ID = 8
|
||||
|
||||
def up
|
||||
each_batch(:issue_customer_relations_contacts, of: BATCH_SIZE) do |batch|
|
||||
connection.execute(
|
||||
<<~SQL
|
||||
DELETE FROM issue_customer_relations_contacts
|
||||
WHERE id IN (
|
||||
SELECT icrc.id
|
||||
FROM issue_customer_relations_contacts icrc
|
||||
JOIN issues i ON icrc.issue_id = i.id
|
||||
WHERE i.work_item_type_id = #{EPIC_WORK_ITEM_TYPE_ID}
|
||||
AND icrc.id IN (#{batch.select(:id).to_sql})
|
||||
)
|
||||
SQL
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
467b28e2bf4b3952f6cb8a4f206bf6d7294f9c8fa42348e30d7a2210f55f92a0
|
||||
|
|
@ -0,0 +1 @@
|
|||
8e0241d5b539046c0754bdde1f5821cb5f4cc1e1c3ad2d50a7f579b022af9418
|
||||
|
|
@ -347,6 +347,11 @@ If you frequently experience connection issues between your LDAP server and GitL
|
|||
For information on adding group links by using CNs and filters, refer to the
|
||||
[GitLab groups documentation](../../../user/group/access_and_permissions.md#manage-group-memberships-with-ldap).
|
||||
|
||||
### Link a custom admin role with an LDAP group
|
||||
|
||||
For information on adding custom admin role links by using CNs and filters, refer to the
|
||||
[manage users using LDAP documentation](../../../user/custom_roles/_index.md#sync-ldap-groups-to-admin-roles).
|
||||
|
||||
### Administrator sync
|
||||
|
||||
As an extension of group sync, you can automatically manage your global GitLab
|
||||
|
|
|
|||
|
|
@ -752,17 +752,6 @@ Prerequisites:
|
|||
|
||||
- You must be an administrator.
|
||||
|
||||
When you create a user, you must specify at least one of the following:
|
||||
|
||||
- `password`
|
||||
- `reset_password`
|
||||
- `force_random_password`
|
||||
|
||||
If `reset_password` and `force_random_password` are both `false`, then `password` is required.
|
||||
|
||||
`force_random_password` and `reset_password` take priority over `password`. Also, `reset_password` and
|
||||
`force_random_password` can be used together.
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
`private_profile` defaults to the value of the
|
||||
|
|
@ -779,6 +768,10 @@ Supported attributes:
|
|||
|
||||
| Attribute | Required | Description |
|
||||
|:-------------------------------------|:---------|:------------|
|
||||
| `username` | Yes | The username of the user |
|
||||
| `name` | Yes | The name of the user |
|
||||
| `email` | Yes | The email of the user |
|
||||
| `password` | Conditionally | The password for the user. Required if `force_random_password` or `reset_password` are not defined. If either force_random_password` or `reset_password` are defined, those settings take priority. |
|
||||
| `admin` | No | User is an administrator. Valid values are `true` or `false`. Defaults to false. |
|
||||
| `auditor` | No | User is an auditor. Valid values are `true` or `false`. Defaults to false. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/366404) in GitLab 15.3. Premium and Ultimate only. |
|
||||
| `avatar` | No | Image file for user's avatar |
|
||||
|
|
@ -786,24 +779,21 @@ Supported attributes:
|
|||
| `can_create_group` | No | User can create top-level groups - true or false |
|
||||
| `color_scheme_id` | No | User's color scheme for the file viewer (for more information, see the [user preference documentation](../user/profile/preferences.md#change-the-syntax-highlighting-theme)) |
|
||||
| `commit_email` | No | User's commit email address |
|
||||
| `email` | Yes | Email |
|
||||
| `extern_uid` | No | External UID |
|
||||
| `external` | No | Flags the user as external - true or false (default) |
|
||||
| `extra_shared_runners_minutes_limit` | No | Can be set by administrators only. Additional compute minutes for this user. Premium and Ultimate only. |
|
||||
| `force_random_password` | No | Set user password to a random value - true or false (default) |
|
||||
| `force_random_password` | No | If `true`, set the user password to a random value. Can be used with `reset_password`. Takes priority over `password`. |
|
||||
| `group_id_for_saml` | No | ID of group where SAML has been configured |
|
||||
| `linkedin` | No | LinkedIn |
|
||||
| `location` | No | User's location |
|
||||
| `name` | Yes | Name |
|
||||
| `note` | No | Administrator notes for this user |
|
||||
| `organization` | No | Organization name |
|
||||
| `password` | No | Password |
|
||||
| `private_profile` | No | User's profile is private - true or false. The default value is determined by [a setting](../administration/settings/account_and_limit_settings.md#set-profiles-of-new-users-to-private-by-default). |
|
||||
| `projects_limit` | No | Number of projects user can create |
|
||||
| `pronouns` | No | User's pronouns |
|
||||
| `provider` | No | External provider name |
|
||||
| `public_email` | No | User's public email address |
|
||||
| `reset_password` | No | Send user password reset link - true or false(default) |
|
||||
| `reset_password` | No | If `true`, sends the user a link to reset their password. Can be used with `force_random_password`. Takes priority over `password`. |
|
||||
| `shared_runners_minutes_limit` | No | Can be set by administrators only. Maximum number of monthly compute minutes for this user. Can be `nil` (default; inherit system default), `0` (unlimited), or `> 0`. Premium and Ultimate only. |
|
||||
| `skip_confirmation` | No | Skip confirmation - true or false (default) |
|
||||
| `skype` | No | Skype ID |
|
||||
|
|
@ -811,7 +801,6 @@ Supported attributes:
|
|||
| `twitter` | No | X (formerly Twitter) account |
|
||||
| `discord` | No | Discord account |
|
||||
| `github` | No | GitHub username |
|
||||
| `username` | Yes | Username |
|
||||
| `view_diffs_file_by_file` | No | Flag indicating the user sees only one file diff per page |
|
||||
| `website_url` | No | Website URL |
|
||||
|
||||
|
|
@ -854,7 +843,7 @@ Supported attributes:
|
|||
| `can_create_group` | No | User can create groups - true or false |
|
||||
| `color_scheme_id` | No | User's color scheme for the file viewer (for more information, see the [user preference documentation](../user/profile/preferences.md#change-the-syntax-highlighting-theme) for more information) |
|
||||
| `commit_email` | No | User's commit email. Set to `_private` to use the private commit email. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/375148) in GitLab 15.5. |
|
||||
| `email` | No | Email |
|
||||
| `email` | No | The email of the user |
|
||||
| `extern_uid` | No | External UID |
|
||||
| `external` | No | Flags the user as external - true or false (default) |
|
||||
| `extra_shared_runners_minutes_limit` | No | Can be set by administrators only. Additional compute minutes for this user. Premium and Ultimate only. |
|
||||
|
|
@ -862,10 +851,10 @@ Supported attributes:
|
|||
| `id` | Yes | ID of the user |
|
||||
| `linkedin` | No | LinkedIn |
|
||||
| `location` | No | User's location |
|
||||
| `name` | No | Name |
|
||||
| `name` | No | The name of the user |
|
||||
| `note` | No | Administration notes for this user |
|
||||
| `organization` | No | Organization name |
|
||||
| `password` | No | Password |
|
||||
| `password` | No | The password for the user |
|
||||
| `private_profile` | No | User's profile is private - true or false. |
|
||||
| `projects_limit` | No | Limit projects each user can create |
|
||||
| `pronouns` | No | Pronouns |
|
||||
|
|
@ -878,7 +867,7 @@ Supported attributes:
|
|||
| `twitter` | No | X (formerly Twitter) account |
|
||||
| `discord` | No | Discord account |
|
||||
| `github` | No | GitHub username |
|
||||
| `username` | No | Username |
|
||||
| `username` | No | The username of the user |
|
||||
| `view_diffs_file_by_file` | No | Flag indicating the user sees only one file diff per page |
|
||||
| `website_url` | No | Website URL |
|
||||
|
||||
|
|
|
|||
|
|
@ -1084,12 +1084,10 @@ Examples:
|
|||
|
||||
### Link to specific lines of code
|
||||
|
||||
<!-- when blob_overflow_menu is removed, change 'button' to 'dropdown item' -->
|
||||
|
||||
When linking to specific lines in a file, link to a commit instead of to the
|
||||
branch. Lines of code change over time. Linking to a line by using
|
||||
the commit link ensures the user lands on the line you're referring to. The
|
||||
**Permalink** button, displayed when viewing a file in a project,
|
||||
**Permalink** dropdown item in the ellipsis menu, displayed when viewing a file in a project,
|
||||
provides a link to the most recent commit of that file.
|
||||
|
||||
- Do: `[link to line 3](https://gitlab.com/gitlab-org/gitlab/-/blob/11f17c56d8b7f0b752562d78a4298a3a95b5ce66/.gitlab/issue_templates/Feature%20proposal.md#L3)`
|
||||
|
|
@ -1696,7 +1694,7 @@ covered in the documentation, you should:
|
|||
- Add this detail to the documentation text.
|
||||
- Create an issue to review the video and update the page.
|
||||
|
||||
Do not upload videos to the product repositories. [Link](#link-to-video) or
|
||||
Do not upload videos to the product repositories. [Add a link](#link-to-video) or
|
||||
[embed](#embed-videos) them instead.
|
||||
|
||||
### Link to video
|
||||
|
|
@ -1719,7 +1717,7 @@ The [GitLab documentation site](https://docs.gitlab.com) supports embedded
|
|||
videos.
|
||||
|
||||
You can embed videos from [the official YouTube account for GitLab](https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg) only.
|
||||
For videos from other sources, [link](#link-to-video) them instead.
|
||||
For videos from other sources, [link them](#link-to-video) instead.
|
||||
|
||||
In most cases, [link to a video](#link-to-video), because
|
||||
embedded videos take up a lot of space on the page and can be distracting to readers.
|
||||
|
|
|
|||
|
|
@ -268,6 +268,57 @@ users to custom roles. For more information, see:
|
|||
- [Configure SAML Group Links](../group/saml_sso/group_sync.md#configure-saml-group-links).
|
||||
- [Manage group memberships via LDAP](../group/access_and_permissions.md#manage-group-memberships-with-ldap).
|
||||
|
||||
## Sync LDAP groups to admin roles
|
||||
|
||||
You can link a custom admin role to an LDAP group. This link assigns the custom admin role to all users in the group.
|
||||
|
||||
If a user belongs to multiple LDAP groups with different assigned custom admin roles, GitLab assigns the role associated with whichever LDAP link was created earlier. For example, if a user is a member of the LDAP groups `owner` and `dev`. If the `owner` group was linked to a custom admin role before the `dev` group, the user would be assigned the role associated with the `owner` group.
|
||||
|
||||
For more information on the administration of LDAP and group sync, see [LDAP synchronization](../../administration/auth/ldap/ldap_synchronization.md#group-sync).
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
If an LDAP user with a custom admin role is removed from the LDAP group after configuring a sync, the custom role is not removed until the next sync.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
### Link a custom admin role with an LDAP CN
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have integrated an LDAP server with your instance.
|
||||
|
||||
To link a custom admin role with an LDAP CN:
|
||||
|
||||
1. On the left sidebar, at the bottom, select **Admin**.
|
||||
1. Select **Settings > Roles and permissions**.
|
||||
1. On the **LDAP Synchronization** tab, select an **LDAP Server**.
|
||||
1. In the **Sync method** field, select `Group cn`.
|
||||
1. In the **Group cn** field, begin typing the CN of the group. A dropdown list appears with matching CNs in the configured `group_base`.
|
||||
1. From the dropdown list, select your CN.
|
||||
1. In the **Custom admin role** field, select a custom admin role.
|
||||
1. Select **Add**.
|
||||
|
||||
GitLab begins linking the role to any matching LDAP users. This process may take over an hour to complete.
|
||||
|
||||
### Link a custom admin role with an LDAP filter
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have integrated an LDAP server with your instance.
|
||||
|
||||
To link a custom admin role with an LDAP filter:
|
||||
|
||||
1. On the left sidebar, at the bottom, select **Admin**.
|
||||
1. Select **Settings > Roles and permissions**.
|
||||
1. On the **LDAP Synchronization** tab, select an **LDAP Server**.
|
||||
1. In the **Sync method** field, select `User filter`.
|
||||
1. In **User filter** box, enter a filter. For details, see [Set up LDAP user filter](../../administration/auth/ldap/_index.md#set-up-ldap-user-filter).
|
||||
1. In the **Custom admin role** field, select a custom admin role.
|
||||
1. Select **Add**.
|
||||
|
||||
GitLab begins linking the role to any matching LDAP users. This process may take over an hour to complete.
|
||||
|
||||
## Contribute new permissions
|
||||
|
||||
If a permission does not exist, you can:
|
||||
|
|
|
|||
|
|
@ -157,3 +157,7 @@ used in the search.
|
|||
|
||||
Because this is an experimental feature, your feedback is valuable in helping us improve it.
|
||||
Share your experiences, suggestions, or issues in [issue 542198](https://gitlab.com/gitlab-org/gitlab/-/issues/542198).
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Blog: GitLab Duo Chat gets agentic AI makeover](https://about.gitlab.com/blog/2025/05/29/gitlab-duo-chat-gets-agentic-ai-makeover/)
|
||||
|
|
|
|||
|
|
@ -82,15 +82,10 @@ similar functionality for locked files is discussed in
|
|||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/519325) in GitLab 17.10 [with a flag](../../administration/feature_flags.md) named `blob_overflow_menu`. Disabled by default.
|
||||
- [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/522993) in GitLab 18.1. Feature flag `blob_overflow_menu` removed.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
{{< alert type="flag" >}}
|
||||
|
||||
The availability of this feature is controlled by a feature flag. For more information, see the history.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
To lock a file:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
|
|
|
|||
|
|
@ -114,6 +114,13 @@ use it for the project's group.
|
|||
|
||||
## Configure group insights
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
Creating a custom Insights configuration file will override the default configuration.
|
||||
If you want to retain the original configuration, you should copy the content of the [default configuration file](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/fixtures/insights/default.yml) as a base.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
GitLab reads insights from the
|
||||
[default configuration file](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/fixtures/insights/default.yml).
|
||||
|
||||
|
|
|
|||
|
|
@ -263,15 +263,10 @@ To cancel changes, edit, upload, or delete a file, from the Web Editor:
|
|||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/519325) in GitLab 17.10 [with a flag](../../../administration/feature_flags.md) named `blob_overflow_menu`. Disabled by default.
|
||||
- [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/522993) in GitLab 18.1. Feature flag `blob_overflow_menu` removed.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
{{< alert type="flag" >}}
|
||||
|
||||
The availability of this feature is controlled by a feature flag. For more information, see the history.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
The **Actions** ({{< icon name="ellipsis_v" >}}) menu consolidates file operations into the
|
||||
dropdown list. From this menu, you can:
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
class BackfillPlaceholderUsersDetailsFromSourceUsers < BatchedMigrationJob
|
||||
operation_name :backfill_placeholder_users_details_from_source_users
|
||||
feature_category :importers
|
||||
|
||||
class ImportSourceUser < ApplicationRecord
|
||||
self.table_name = 'import_source_users'
|
||||
|
||||
belongs_to :namespace
|
||||
end
|
||||
|
||||
class Namespace < ApplicationRecord
|
||||
self.table_name = 'namespaces'
|
||||
end
|
||||
|
||||
class ImportPlaceholderUserDetail < ApplicationRecord
|
||||
self.table_name = 'import_placeholder_user_details'
|
||||
end
|
||||
|
||||
def perform
|
||||
each_sub_batch do |sub_batch|
|
||||
backfill_placeholder_user_details(sub_batch)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def backfill_placeholder_user_details(sub_batch)
|
||||
source_users_with_data = prepare_source_users_data(sub_batch)
|
||||
return if source_users_with_data.empty?
|
||||
|
||||
placeholder_user_ids = source_users_with_data.keys
|
||||
|
||||
existing_placeholder_ids = get_existing_placeholder_ids(placeholder_user_ids)
|
||||
|
||||
users_to_process = source_users_with_data.reject do |placeholder_id, _|
|
||||
existing_placeholder_ids.include?(placeholder_id)
|
||||
end
|
||||
return if users_to_process.empty?
|
||||
|
||||
bulk_insert_placeholder_details(users_to_process)
|
||||
end
|
||||
|
||||
def prepare_source_users_data(sub_batch)
|
||||
source_users = ImportSourceUser
|
||||
.where(id: sub_batch)
|
||||
.joins(:namespace)
|
||||
.where.not(placeholder_user_id: nil)
|
||||
.where.not(namespace_id: nil)
|
||||
.select(:placeholder_user_id, :namespace_id, 'namespaces.organization_id as organization_id')
|
||||
|
||||
source_users.each_with_object({}) do |user, hash|
|
||||
hash[user.placeholder_user_id] = {
|
||||
namespace_id: user.namespace_id,
|
||||
organization_id: user.organization_id
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def get_existing_placeholder_ids(placeholder_user_ids)
|
||||
return [] if placeholder_user_ids.empty?
|
||||
|
||||
ImportPlaceholderUserDetail
|
||||
.where(placeholder_user_id: placeholder_user_ids)
|
||||
.pluck(:placeholder_user_id)
|
||||
end
|
||||
|
||||
def bulk_insert_placeholder_details(users_data)
|
||||
return if users_data.empty?
|
||||
|
||||
timestamp = Time.current
|
||||
|
||||
records_to_insert = users_data.map do |placeholder_user_id, data|
|
||||
{
|
||||
placeholder_user_id: placeholder_user_id,
|
||||
namespace_id: data[:namespace_id],
|
||||
organization_id: data[:organization_id],
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp
|
||||
}
|
||||
end
|
||||
|
||||
begin
|
||||
ImportPlaceholderUserDetail.upsert_all(records_to_insert)
|
||||
rescue StandardError => e
|
||||
logger.error(
|
||||
message: "Error bulk creating placeholder user details: #{e.message}",
|
||||
count: users_data.size,
|
||||
placeholder_user_ids: users_data.keys
|
||||
)
|
||||
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
def logger
|
||||
@logger ||= Gitlab::BackgroundMigration::Logger.build
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -7,7 +7,7 @@ module Gitlab
|
|||
scope_to ->(relation) { relation.where.not(pipeline_id: nil) }
|
||||
feature_category :dependency_management
|
||||
|
||||
class DependencyListExport < ::ApplicationRecord
|
||||
class DependencyListExport < SecApplicationRecord
|
||||
FINISHED = 2
|
||||
FAILED = -1
|
||||
|
||||
|
|
|
|||
|
|
@ -29257,9 +29257,6 @@ msgstr ""
|
|||
msgid "Go to file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Go to file permalink (while viewing a file)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Go to files"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -71206,9 +71203,6 @@ msgstr ""
|
|||
msgid "You can’t edit files directly in this project."
|
||||
msgstr ""
|
||||
|
||||
msgid "You can’t edit files directly in this project. Fork this project and submit a merge request with your changes."
|
||||
msgstr ""
|
||||
|
||||
msgid "You commented"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ module Gitlab
|
|||
resources: resources("300m", "300Mi")
|
||||
},
|
||||
toolbox: {
|
||||
resources: resources("50m", "128Mi", "1100m", "1Gi")
|
||||
resources: resources("50m", "128Mi", no_limits: true)
|
||||
}
|
||||
},
|
||||
registry: {
|
||||
|
|
@ -124,7 +124,7 @@ module Gitlab
|
|||
# Toolbox create peak load during startup but then consumes very little
|
||||
# Set high limit value but don't request full amount to avoid unnecessary lock
|
||||
toolbox: {
|
||||
resources: resources("50m", "128Mi", "1100m", "1Gi")
|
||||
resources: resources("50m", "128Mi", no_limits: true)
|
||||
}
|
||||
},
|
||||
registry: {
|
||||
|
|
|
|||
|
|
@ -44,8 +44,7 @@ RSpec.describe Gitlab::Orchestrator::Deployment::ResourcePresets do
|
|||
},
|
||||
toolbox: {
|
||||
resources: {
|
||||
requests: { cpu: "50m", memory: "128Mi" },
|
||||
limits: { cpu: "1100m", memory: "1Gi" }
|
||||
requests: { cpu: "50m", memory: "128Mi" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -159,8 +158,7 @@ RSpec.describe Gitlab::Orchestrator::Deployment::ResourcePresets do
|
|||
},
|
||||
toolbox: {
|
||||
resources: {
|
||||
requests: { cpu: "50m", memory: "128Mi" },
|
||||
limits: { cpu: "1100m", memory: "1Gi" }
|
||||
requests: { cpu: "50m", memory: "128Mi" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,10 +9,6 @@ module QA
|
|||
include Page::Component::BlobContent
|
||||
include Shared::Editor
|
||||
|
||||
view 'app/assets/javascripts/repository/components/blob_button_group.vue' do
|
||||
element 'lock-button'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/vue_shared/components/web_ide_link.vue' do
|
||||
element 'action-dropdown'
|
||||
element 'edit-menu-item', ':data-testid="`${action.key}-menu-item`"' # rubocop:disable QA/ElementWithPattern
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ module QA
|
|||
RSpec.describe 'Tenant Scale' do
|
||||
describe(
|
||||
'User', :requires_admin,
|
||||
product_group: :organizations,
|
||||
feature_flag: { name: :blob_overflow_menu }
|
||||
product_group: :organizations
|
||||
) do
|
||||
let(:admin_api_client) { Runtime::API::Client.as_admin }
|
||||
|
||||
|
|
@ -18,7 +17,6 @@ module QA
|
|||
context 'with terminated parent group membership' do
|
||||
before do
|
||||
group.add_member(user)
|
||||
Runtime::Feature.enable(:blob_overflow_menu)
|
||||
Flow::Login.while_signed_in_as_admin do
|
||||
group.visit!
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ module QA
|
|||
RSpec.describe 'Tenant Scale' do
|
||||
describe(
|
||||
'User', :requires_admin,
|
||||
product_group: :organizations,
|
||||
feature_flag: { name: :blob_overflow_menu }
|
||||
product_group: :organizations
|
||||
) do
|
||||
let!(:parent_group) do
|
||||
create(:group, path: "parent-group-to-test-user-access-#{SecureRandom.hex(8)}")
|
||||
|
|
@ -25,7 +24,6 @@ module QA
|
|||
|
||||
before do
|
||||
parent_group.add_member(parent_group_user)
|
||||
Runtime::Feature.enable(:blob_overflow_menu)
|
||||
end
|
||||
|
||||
it(
|
||||
|
|
|
|||
|
|
@ -1,97 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Blob button line permalinks (BlobLinePermalinkUpdater)', :js, feature_category: :source_code_management do
|
||||
include TreeHelper
|
||||
|
||||
let(:project) { create(:project, :public, :repository) }
|
||||
let(:path) { 'CHANGELOG' }
|
||||
let(:sha) { project.repository.commit.sha }
|
||||
|
||||
describe 'On a file(blob)' do
|
||||
def get_absolute_url(path = "")
|
||||
"http://#{page.server.host}:#{page.server.port}#{path}"
|
||||
end
|
||||
|
||||
def visit_blob(fragment = nil)
|
||||
visit project_blob_path(project, tree_join('master', path), anchor: fragment)
|
||||
end
|
||||
|
||||
describe 'Click "Permalink" button' do
|
||||
before do
|
||||
stub_feature_flags(blob_overflow_menu: false)
|
||||
end
|
||||
|
||||
it 'works with no initial line number fragment hash' do
|
||||
visit_blob
|
||||
|
||||
expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path))))
|
||||
end
|
||||
|
||||
it 'maintains initial fragment hash' do
|
||||
fragment = "L3"
|
||||
|
||||
visit_blob(fragment)
|
||||
|
||||
expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: fragment)))
|
||||
end
|
||||
|
||||
it 'changes fragment hash if line number clicked' do
|
||||
visit_blob
|
||||
|
||||
find('#L3').click
|
||||
find("#L5").click
|
||||
|
||||
expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: "L5")))
|
||||
end
|
||||
|
||||
it 'with initial fragment hash, changes fragment hash if line number clicked' do
|
||||
fragment = "L1"
|
||||
|
||||
visit_blob(fragment)
|
||||
|
||||
find('#L3').click
|
||||
find("#L5").click
|
||||
|
||||
expect(find('.js-data-file-blob-permalink-url')['href']).to eq(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: "L5")))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Click "Blame" button' do
|
||||
it 'works with no initial line number fragment hash' do
|
||||
visit_blob
|
||||
|
||||
expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path))))
|
||||
end
|
||||
|
||||
it 'maintains initial fragment hash' do
|
||||
fragment = "L3"
|
||||
|
||||
visit_blob(fragment)
|
||||
|
||||
expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path), anchor: fragment)))
|
||||
end
|
||||
|
||||
it 'changes fragment hash if line number clicked' do
|
||||
visit_blob
|
||||
|
||||
find('#L3').click
|
||||
find("#L5").click
|
||||
|
||||
expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path), anchor: "L5")))
|
||||
end
|
||||
|
||||
it 'with initial fragment hash, changes fragment hash if line number clicked' do
|
||||
fragment = "L1"
|
||||
|
||||
visit_blob(fragment)
|
||||
|
||||
find('#L3').click
|
||||
find("#L5").click
|
||||
|
||||
expect(find('.js-blob-blame-link')['href']).to eq(get_absolute_url(project_blame_path(project, tree_join('master', path), anchor: "L5")))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Blob shortcuts', :js, feature_category: :groups_and_projects do
|
||||
include TreeHelper
|
||||
let(:project) { create(:project, :public, :repository) }
|
||||
let(:path) { project.repository.ls_files(project.repository.root_ref)[0] }
|
||||
let(:sha) { project.repository.commit.sha }
|
||||
|
||||
describe 'On a file(blob)', :js do
|
||||
def get_absolute_url(path = "")
|
||||
"http://#{page.server.host}:#{page.server.port}#{path}"
|
||||
end
|
||||
|
||||
def visit_blob(fragment = nil)
|
||||
visit project_blob_path(project, tree_join('master', path), anchor: fragment)
|
||||
end
|
||||
|
||||
describe 'pressing "y"' do
|
||||
before do
|
||||
stub_feature_flags(blob_overflow_menu: false)
|
||||
end
|
||||
|
||||
it 'redirects to permalink with commit sha' do
|
||||
visit_blob
|
||||
wait_for_requests
|
||||
|
||||
find('body').native.send_key('y')
|
||||
|
||||
expect(page).to have_current_path(get_absolute_url(project_blob_path(project, tree_join(sha, path))), url: true)
|
||||
end
|
||||
|
||||
it 'redirects to permalink of a currently viewed file' do
|
||||
visit project_path(project)
|
||||
wait_for_requests
|
||||
click_link 'VERSION'
|
||||
wait_for_requests
|
||||
page.driver.go_back
|
||||
click_link path
|
||||
wait_for_requests
|
||||
|
||||
find('body').native.send_key('y')
|
||||
|
||||
expect(page).to have_current_path(get_absolute_url(project_blob_path(project, tree_join(sha, path))), url: true)
|
||||
end
|
||||
|
||||
it 'maintains fragment hash when redirecting' do
|
||||
fragment = "L1"
|
||||
visit_blob(fragment)
|
||||
wait_for_requests
|
||||
|
||||
find('body').native.send_key('y')
|
||||
|
||||
expect(page).to have_current_path(get_absolute_url(project_blob_path(project, tree_join(sha, path), anchor: fragment)), url: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -24,8 +24,6 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :so
|
|||
let_it_be(:project_with_crlf) { create(:project, :custom_repo, name: 'Project with crlf', files: { 'crlf_file.txt' => crlf_text }) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(blob_overflow_menu: false)
|
||||
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
|
|
@ -40,8 +38,11 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :so
|
|||
expect(page).not_to have_text('Edit')
|
||||
expect(page).not_to have_text('Web IDE')
|
||||
|
||||
expect(page).not_to have_text('Replace')
|
||||
expect(page).not_to have_text('Delete')
|
||||
within_testid('blob-controls') do
|
||||
click_button 'File actions'
|
||||
expect(page).not_to have_text('Replace')
|
||||
expect(page).not_to have_text('Delete')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -172,8 +173,8 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :so
|
|||
expect(page).to have_selector(:link_or_button, 'Fork')
|
||||
expect(page).to have_selector(:link_or_button, 'Cancel')
|
||||
expect(page).to have_content(
|
||||
"You can’t edit files directly in this project. "\
|
||||
"Fork this project and submit a merge request with your changes."
|
||||
"You're not allowed to make changes to this project directly. "\
|
||||
"Create a fork to make changes and submit a merge request."
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -194,6 +195,8 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :so
|
|||
|
||||
expect_fork_status
|
||||
|
||||
edit_in_single_file_editor
|
||||
|
||||
find('.file-editor', match: :first)
|
||||
|
||||
editor_set_value('*.rbca')
|
||||
|
|
@ -210,9 +213,21 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :so
|
|||
|
||||
click_link_or_button('Fork')
|
||||
|
||||
within_web_ide do
|
||||
expect(page).to have_text(project2.path.upcase)
|
||||
expect(page).to have_text('.gitignore')
|
||||
expect_fork_status
|
||||
|
||||
click_button 'Edit'
|
||||
|
||||
new_tab = window_opened_by { click_link_or_button 'Web IDE' }
|
||||
|
||||
switch_to_window new_tab
|
||||
|
||||
wait_for_requests
|
||||
|
||||
within_window new_tab do
|
||||
within_web_ide do
|
||||
expect(page).to have_text(project2.path.upcase)
|
||||
expect(page).to have_text('.gitignore')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -223,6 +238,8 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :so
|
|||
expect_fork_prompt
|
||||
click_link_or_button('Fork')
|
||||
|
||||
edit_in_single_file_editor
|
||||
|
||||
find('.file-editor', match: :first)
|
||||
|
||||
editor_set_value('*.rbca')
|
||||
|
|
|
|||
|
|
@ -13,9 +13,12 @@ RSpec.describe 'Public Snippets', :js, feature_category: :source_code_management
|
|||
wait_for_requests
|
||||
|
||||
expect(page).to have_content(content)
|
||||
click_button('Code')
|
||||
expect(page).to have_field('Embed', readonly: true, with: "<script src=\"#{url}.js\"></script>")
|
||||
expect(page).to have_field('Share', readonly: true, with: url)
|
||||
|
||||
within_testid('snippet-header') do
|
||||
click_button('Code')
|
||||
expect(page).to have_field('Embed', readonly: true, with: "<script src=\"#{url}.js\"></script>")
|
||||
expect(page).to have_field('Share', readonly: true, with: url)
|
||||
end
|
||||
end
|
||||
|
||||
it 'unauthenticated user should see raw public snippets' do
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob';
|
||||
import { PROJECT_FILES_GO_TO_PERMALINK } from '~/behaviors/shortcuts/keybindings';
|
||||
import { moveToFilePermalink } from '~/blob/utils';
|
||||
|
||||
describe('ShortcutsBlob', () => {
|
||||
const shortcuts = {
|
||||
add: jest.fn(),
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
return new ShortcutsBlob(shortcuts);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
shortcuts.add.mockClear();
|
||||
window.gon = {};
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
describe('when shortcuts should be added', () => {
|
||||
it('adds the permalink shortcut when gon.features is undefined', () => {
|
||||
init();
|
||||
|
||||
expect(shortcuts.add).toHaveBeenCalledWith(
|
||||
PROJECT_FILES_GO_TO_PERMALINK,
|
||||
moveToFilePermalink,
|
||||
);
|
||||
});
|
||||
|
||||
it('adds shortcuts when blobOverflowMenu is false', () => {
|
||||
window.gon.features = {
|
||||
blobOverflowMenu: false,
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
expect(shortcuts.add).toHaveBeenCalledWith(
|
||||
PROJECT_FILES_GO_TO_PERMALINK,
|
||||
moveToFilePermalink,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when shortcuts should not be added', () => {
|
||||
it('does not add shortcuts when blobOverflowMenu is true', () => {
|
||||
window.gon.features = {
|
||||
blobOverflowMenu: true,
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
expect(shortcuts.add).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -19,9 +19,6 @@ describe('Blob Header Default Actions', () => {
|
|||
wrapper = shallowMountExtended(BlobHeaderActions, {
|
||||
provide: {
|
||||
blobHash,
|
||||
glFeatures: {
|
||||
blobOverflowMenu: false,
|
||||
},
|
||||
...provided,
|
||||
},
|
||||
propsData: {
|
||||
|
|
@ -136,9 +133,9 @@ describe('Blob Header Default Actions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when blob_overflow_menu is enabled', () => {
|
||||
describe('default actions layout', () => {
|
||||
it('hides default actions for mobile layout', () => {
|
||||
createComponent({}, { glFeatures: { blobOverflowMenu: true } });
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.findComponent(GlButtonGroup).attributes('class')).toBe(
|
||||
'gl-hidden sm:gl-inline-flex',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import BlobHeader from '~/blob/components/blob_header.vue';
|
||||
import DefaultActions from '~/blob/components/blob_header_default_actions.vue';
|
||||
|
|
@ -7,21 +5,15 @@ import BlobFilepath from '~/blob/components/blob_header_filepath.vue';
|
|||
import ViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue';
|
||||
import { SIMPLE_BLOB_VIEWER } from '~/blob/components/constants';
|
||||
import TableContents from '~/blob/components/table_contents.vue';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import WebIdeLink from 'ee_else_ce/vue_shared/components/web_ide_link.vue';
|
||||
import userInfoQuery from '~/blob/queries/user_info.query.graphql';
|
||||
import applicationInfoQuery from '~/blob/queries/application_info.query.graphql';
|
||||
import { Blob, userInfoMock, applicationInfoMock } from './mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
import { Blob } from './mock_data';
|
||||
|
||||
describe('Blob Header Default Actions', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultProvide = {
|
||||
blobHash: 'foo-bar',
|
||||
glFeatures: { blobOverflowMenu: true },
|
||||
};
|
||||
|
||||
const findDefaultActions = () => wrapper.findComponent(DefaultActions);
|
||||
|
|
@ -30,7 +22,6 @@ describe('Blob Header Default Actions', () => {
|
|||
const findBlobFilePath = () => wrapper.findComponent(BlobFilepath);
|
||||
const findRichTextEditorBtn = () => wrapper.findByTestId('rich-blob-viewer-button');
|
||||
const findSimpleTextEditorBtn = () => wrapper.findByTestId('simple-blob-viewer-button');
|
||||
const findWebIdeLink = () => wrapper.findComponent(WebIdeLink);
|
||||
const findDuoWorkflowActionSlot = () => wrapper.findByTestId('ee-duo-workflow-action');
|
||||
|
||||
async function createComponent({
|
||||
|
|
@ -39,21 +30,7 @@ describe('Blob Header Default Actions', () => {
|
|||
propsData = {},
|
||||
mountFn = shallowMountExtended,
|
||||
} = {}) {
|
||||
const userInfoMockResolver = jest.fn().mockResolvedValue({
|
||||
data: { ...userInfoMock },
|
||||
});
|
||||
|
||||
const applicationInfoMockResolver = jest.fn().mockResolvedValue({
|
||||
data: { ...applicationInfoMock },
|
||||
});
|
||||
|
||||
const fakeApollo = createMockApollo([
|
||||
[userInfoQuery, userInfoMockResolver],
|
||||
[applicationInfoQuery, applicationInfoMockResolver],
|
||||
]);
|
||||
|
||||
wrapper = mountFn(BlobHeader, {
|
||||
apolloProvider: fakeApollo,
|
||||
provide: {
|
||||
...defaultProvide,
|
||||
},
|
||||
|
|
@ -75,87 +52,6 @@ describe('Blob Header Default Actions', () => {
|
|||
createComponent();
|
||||
});
|
||||
|
||||
describe('WebIdeLink component', () => {
|
||||
it('does not render WebIdeLink component', () => {
|
||||
expect(findWebIdeLink().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when blob_overflow_menu feature flag is false', () => {
|
||||
it('renders the WebIdeLink component with the correct props', async () => {
|
||||
const { ideEditPath, editBlobPath, gitpodBlobUrl, pipelineEditorPath } = Blob;
|
||||
const showForkSuggestion = false;
|
||||
const showWebIdeForkSuggestion = false;
|
||||
await createComponent({
|
||||
options: {
|
||||
provide: {
|
||||
glFeatures: { blobOverflowMenu: false },
|
||||
},
|
||||
},
|
||||
propsData: { showForkSuggestion, showWebIdeForkSuggestion },
|
||||
});
|
||||
|
||||
expect(findWebIdeLink().props()).toMatchObject({
|
||||
showEditButton: true,
|
||||
buttonVariant: 'confirm',
|
||||
editUrl: editBlobPath,
|
||||
webIdeUrl: ideEditPath,
|
||||
needsToFork: showForkSuggestion,
|
||||
needsToForkWithWebIde: showWebIdeForkSuggestion,
|
||||
showPipelineEditorButton: Boolean(pipelineEditorPath),
|
||||
pipelineEditorUrl: pipelineEditorPath,
|
||||
gitpodUrl: gitpodBlobUrl,
|
||||
isGitpodEnabledForInstance: applicationInfoMock.gitpodEnabled,
|
||||
isGitpodEnabledForUser: userInfoMock.currentUser.gitpodEnabled,
|
||||
disabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('disables the WebIdeLink component when file is LFS', async () => {
|
||||
await createComponent({
|
||||
options: {
|
||||
provide: {
|
||||
glFeatures: { blobOverflowMenu: false },
|
||||
},
|
||||
},
|
||||
propsData: { isUsingLfs: true },
|
||||
});
|
||||
|
||||
expect(findWebIdeLink().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('passes the edit button variant down to the WebIdeLink', () => {
|
||||
const editButtonVariant = 'danger';
|
||||
|
||||
createComponent({
|
||||
options: {
|
||||
provide: {
|
||||
glFeatures: { blobOverflowMenu: false },
|
||||
},
|
||||
},
|
||||
propsData: { editButtonVariant },
|
||||
});
|
||||
|
||||
expect(findWebIdeLink().props('buttonVariant')).toBe(editButtonVariant);
|
||||
});
|
||||
|
||||
it.each([[{ archived: true }], [{ editBlobPath: null }]])(
|
||||
'does not render the WebIdeLink component when blob is archived or does not have an edit path',
|
||||
(blobProps) => {
|
||||
createComponent({
|
||||
blobProps,
|
||||
options: {
|
||||
provide: {
|
||||
glFeatures: { blobOverflowMenu: false },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(findWebIdeLink().exists()).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default render', () => {
|
||||
it.each`
|
||||
findComponent | componentName
|
||||
|
|
@ -275,23 +171,5 @@ describe('Blob Header Default Actions', () => {
|
|||
|
||||
expect(wrapper.emitted('viewer-changed')).toBeDefined();
|
||||
});
|
||||
|
||||
it('sets different icons depending on the blob file type', async () => {
|
||||
factory();
|
||||
|
||||
expect(findViewSwitcher().props('docIcon')).toBe('document');
|
||||
|
||||
await wrapper.setProps({
|
||||
blob: {
|
||||
...Blob,
|
||||
richViewer: {
|
||||
...Blob.richViewer,
|
||||
fileType: 'csv',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(findViewSwitcher().props('docIcon')).toBe('table');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,12 +14,9 @@ describe('Blob Header Viewer Switcher', () => {
|
|||
let wrapper;
|
||||
let trackingSpy;
|
||||
|
||||
function createComponent(propsData = { showViewerToggles: true }, featureFlag = false) {
|
||||
function createComponent(propsData = { showViewerToggles: true }) {
|
||||
wrapper = mountExtended(BlobHeaderViewerSwitcher, {
|
||||
propsData,
|
||||
provide: {
|
||||
glFeatures: { blobOverflowMenu: featureFlag },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -126,22 +123,10 @@ describe('Blob Header Viewer Switcher', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('with blobOverflowMenu feature flag', () => {
|
||||
it('renders icon toggles, when flag is disabled', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findSimpleViewerButton().props('icon')).toBe('code');
|
||||
expect(findSimpleViewerButton().text()).toBe('');
|
||||
expect(findRichViewerButton().props('icon')).toBe('document');
|
||||
expect(findRichViewerButton().text()).toBe('');
|
||||
});
|
||||
|
||||
it('renders text toggles, when flag is enabled', () => {
|
||||
createComponent({ showViewerToggles: true }, true);
|
||||
|
||||
expect(findSimpleViewerButton().props('icon')).toBe('');
|
||||
describe('viewer toggles', () => {
|
||||
it('renders text toggles', () => {
|
||||
createComponent({ showViewerToggles: true });
|
||||
expect(findSimpleViewerButton().text()).toBe('Code');
|
||||
expect(findRichViewerButton().props('icon')).toBe('');
|
||||
expect(findRichViewerButton().text()).toBe('Preview');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -83,5 +83,3 @@ export const userInfoMock = {
|
|||
profileEnableGitpodPath: '/-/profile?user%5Bgitpod_enabled%5D=true',
|
||||
},
|
||||
};
|
||||
|
||||
export const applicationInfoMock = { gitpodEnabled: true };
|
||||
|
|
|
|||
|
|
@ -1,160 +0,0 @@
|
|||
import { GlButton } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
|
||||
import CommitChangesModal from '~/repository/components/commit_changes_modal.vue';
|
||||
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
name: 'some name',
|
||||
path: 'some/path',
|
||||
canPushCode: true,
|
||||
canPushToBranch: true,
|
||||
replacePath: 'some/replace/path',
|
||||
deletePath: 'some/delete/path',
|
||||
emptyRepo: false,
|
||||
projectPath: 'some/project/path',
|
||||
isLocked: false,
|
||||
canLock: true,
|
||||
showForkSuggestion: false,
|
||||
isUsingLfs: true,
|
||||
};
|
||||
|
||||
const DEFAULT_INJECT = {
|
||||
targetBranch: 'master',
|
||||
originalBranch: 'master',
|
||||
};
|
||||
|
||||
describe('BlobButtonGroup component', () => {
|
||||
let wrapper;
|
||||
|
||||
let showUploadBlobModalMock;
|
||||
let showDeleteBlobModalMock;
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
showUploadBlobModalMock = jest.fn();
|
||||
showDeleteBlobModalMock = jest.fn();
|
||||
|
||||
const UploadBlobModalStub = stubComponent(UploadBlobModal, {
|
||||
methods: {
|
||||
show: showUploadBlobModalMock,
|
||||
},
|
||||
});
|
||||
const DeleteBlobModalStub = stubComponent(CommitChangesModal, {
|
||||
methods: {
|
||||
show: showDeleteBlobModalMock,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper = mountExtended(BlobButtonGroup, {
|
||||
propsData: {
|
||||
...DEFAULT_PROPS,
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
...DEFAULT_INJECT,
|
||||
},
|
||||
stubs: {
|
||||
UploadBlobModal: UploadBlobModalStub,
|
||||
CommitChangesModal: DeleteBlobModalStub,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findDeleteBlobModal = () => wrapper.findComponent(CommitChangesModal);
|
||||
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
|
||||
const findDeleteButton = () => wrapper.findByTestId('delete');
|
||||
const findReplaceButton = () => wrapper.findByTestId('replace');
|
||||
|
||||
it('renders component', () => {
|
||||
createComponent();
|
||||
|
||||
const { name, path } = DEFAULT_PROPS;
|
||||
|
||||
expect(wrapper.props()).toMatchObject({
|
||||
name,
|
||||
path,
|
||||
});
|
||||
});
|
||||
|
||||
describe('buttons', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders both the replace and delete button', () => {
|
||||
expect(wrapper.findAllComponents(GlButton)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('renders the buttons in the correct order', () => {
|
||||
expect(wrapper.findAllComponents(GlButton).at(0).text()).toBe('Replace');
|
||||
expect(wrapper.findAllComponents(GlButton).at(1).text()).toBe('Delete');
|
||||
});
|
||||
|
||||
it('triggers the UploadBlobModal from the replace button', () => {
|
||||
findReplaceButton().vm.$emit('click');
|
||||
|
||||
expect(showUploadBlobModalMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('triggers the CommitChangesModal from the delete button', () => {
|
||||
findDeleteButton().vm.$emit('click');
|
||||
|
||||
expect(showDeleteBlobModalMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('showForkSuggestion set to true', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ showForkSuggestion: true });
|
||||
});
|
||||
|
||||
it('does not trigger the UploadBlobModal from the replace button', () => {
|
||||
findReplaceButton().vm.$emit('click');
|
||||
|
||||
expect(showUploadBlobModalMock).not.toHaveBeenCalled();
|
||||
expect(wrapper.emitted().fork).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('does not trigger the DeleteBlobModal from the delete button', () => {
|
||||
findDeleteButton().vm.$emit('click');
|
||||
|
||||
expect(showDeleteBlobModalMock).not.toHaveBeenCalled();
|
||||
expect(wrapper.emitted().fork).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders UploadBlobModal', () => {
|
||||
createComponent();
|
||||
|
||||
const { targetBranch, originalBranch } = DEFAULT_INJECT;
|
||||
const { name, path, canPushCode, replacePath } = DEFAULT_PROPS;
|
||||
const title = `Replace ${name}`;
|
||||
|
||||
expect(findUploadBlobModal().props()).toMatchObject({
|
||||
commitMessage: title,
|
||||
targetBranch,
|
||||
originalBranch,
|
||||
canPushCode,
|
||||
path,
|
||||
replacePath,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders CommitChangesModal for delete', () => {
|
||||
createComponent();
|
||||
|
||||
const { targetBranch, originalBranch } = DEFAULT_INJECT;
|
||||
const { name, canPushCode, emptyRepo, isUsingLfs } = DEFAULT_PROPS;
|
||||
const title = `Delete ${name}`;
|
||||
|
||||
expect(findDeleteBlobModal().props()).toMatchObject({
|
||||
commitMessage: title,
|
||||
targetBranch,
|
||||
originalBranch,
|
||||
canPushCode,
|
||||
emptyRepo,
|
||||
isUsingLfs,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -11,9 +11,7 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import { createAlert } from '~/alert';
|
||||
import BlobContent from '~/blob/components/blob_content.vue';
|
||||
import BlobHeader from 'ee_else_ce/blob/components/blob_header.vue';
|
||||
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
|
||||
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
|
||||
import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
|
||||
import { loadViewer } from '~/repository/components/blob_viewers';
|
||||
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
|
||||
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
|
||||
|
|
@ -150,8 +148,6 @@ describe('Blob content viewer component', () => {
|
|||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findBlobHeader = () => wrapper.findComponent(BlobHeader);
|
||||
const findBlobContent = () => wrapper.findComponent(BlobContent);
|
||||
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
|
||||
const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
|
||||
const findCodeIntelligence = () => wrapper.findComponent(CodeIntelligence);
|
||||
const findSourceViewer = () => wrapper.findComponent(SourceViewer);
|
||||
|
||||
|
|
@ -178,9 +174,7 @@ describe('Blob content viewer component', () => {
|
|||
expect(findBlobHeader().props('hasRenderError')).toEqual(false);
|
||||
expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(false);
|
||||
expect(findBlobHeader().props('blob')).toEqual(simpleViewerMock);
|
||||
expect(findBlobHeader().props('showForkSuggestion')).toEqual(false);
|
||||
expect(findBlobHeader().props('showBlameToggle')).toEqual(true);
|
||||
expect(findBlobHeader().props('projectPath')).toEqual(propsMock.projectPath);
|
||||
expect(findBlobHeader().props('projectId')).toEqual(projectMock.id);
|
||||
expect(mockRouterPush).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -503,40 +497,6 @@ describe('Blob content viewer component', () => {
|
|||
expect(findBlobHeader().props('isBinary')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BlobButtonGroup', () => {
|
||||
const { name, path, replacePath, webPath } = simpleViewerMock;
|
||||
const {
|
||||
userPermissions: { pushCode, downloadCode },
|
||||
repository: { empty },
|
||||
} = projectMock;
|
||||
|
||||
it('renders component', async () => {
|
||||
window.gon.current_user_id = 'gid://gitlab/User/1';
|
||||
window.gon.current_username = 'root';
|
||||
|
||||
await createComponent({ pushCode, downloadCode, empty }, mount);
|
||||
|
||||
expect(findBlobButtonGroup().props()).toMatchObject({
|
||||
name,
|
||||
path,
|
||||
replacePath,
|
||||
deletePath: webPath,
|
||||
canPushCode: pushCode,
|
||||
canLock: true,
|
||||
isLocked: false,
|
||||
emptyRepo: empty,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render if not logged in', async () => {
|
||||
isLoggedIn.mockReturnValueOnce(false);
|
||||
|
||||
await createComponent();
|
||||
|
||||
expect(findBlobButtonGroup().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('blob info query', () => {
|
||||
|
|
@ -581,76 +541,6 @@ describe('Blob content viewer component', () => {
|
|||
findBlobHeader().vm.$emit('edit', 'ide');
|
||||
expect(urlUtility.visitUrl).toHaveBeenCalledWith(simpleViewerMock.ideEditPath);
|
||||
});
|
||||
|
||||
it.each`
|
||||
loggedIn | canModifyBlob | isUsingLfs | createMergeRequestIn | forkProject | showSingleFileEditorForkSuggestion
|
||||
${true} | ${true} | ${false} | ${true} | ${true} | ${false}
|
||||
${true} | ${false} | ${false} | ${true} | ${true} | ${true}
|
||||
${false} | ${false} | ${false} | ${true} | ${true} | ${false}
|
||||
${true} | ${false} | ${false} | ${false} | ${true} | ${false}
|
||||
${true} | ${false} | ${false} | ${true} | ${false} | ${false}
|
||||
${true} | ${false} | ${true} | ${true} | ${true} | ${false}
|
||||
`(
|
||||
'shows/hides a fork suggestion according to a set of conditions',
|
||||
async ({
|
||||
loggedIn,
|
||||
canModifyBlob,
|
||||
isUsingLfs,
|
||||
createMergeRequestIn,
|
||||
forkProject,
|
||||
showSingleFileEditorForkSuggestion,
|
||||
}) => {
|
||||
isLoggedIn.mockReturnValueOnce(loggedIn);
|
||||
await createComponent(
|
||||
{
|
||||
blob: { ...simpleViewerMock, canModifyBlob, storedExternally: isUsingLfs },
|
||||
createMergeRequestIn,
|
||||
forkProject,
|
||||
},
|
||||
mount,
|
||||
);
|
||||
|
||||
findBlobHeader().vm.$emit('edit', 'simple');
|
||||
await nextTick();
|
||||
|
||||
expect(findForkSuggestion().exists()).toBe(showSingleFileEditorForkSuggestion);
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
loggedIn | canModifyBlobWithWebIde | isUsingLfs | createMergeRequestIn | forkProject | showWebIdeForkSuggestion
|
||||
${true} | ${true} | ${false} | ${true} | ${true} | ${false}
|
||||
${true} | ${false} | ${false} | ${true} | ${true} | ${true}
|
||||
${false} | ${false} | ${false} | ${true} | ${true} | ${false}
|
||||
${true} | ${false} | ${false} | ${false} | ${true} | ${false}
|
||||
${true} | ${false} | ${false} | ${true} | ${false} | ${false}
|
||||
${true} | ${false} | ${true} | ${true} | ${true} | ${false}
|
||||
`(
|
||||
'shows/hides a fork suggestion for WebIDE according to a set of conditions',
|
||||
async ({
|
||||
loggedIn,
|
||||
canModifyBlobWithWebIde,
|
||||
isUsingLfs,
|
||||
createMergeRequestIn,
|
||||
forkProject,
|
||||
showWebIdeForkSuggestion,
|
||||
}) => {
|
||||
isLoggedIn.mockReturnValueOnce(loggedIn);
|
||||
await createComponent(
|
||||
{
|
||||
blob: { ...simpleViewerMock, canModifyBlobWithWebIde, storedExternally: isUsingLfs },
|
||||
createMergeRequestIn,
|
||||
forkProject,
|
||||
},
|
||||
mount,
|
||||
);
|
||||
|
||||
findBlobHeader().vm.$emit('edit', 'ide');
|
||||
await nextTick();
|
||||
|
||||
expect(findForkSuggestion().exists()).toBe(showWebIdeForkSuggestion);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('active viewer based on plain attribute', () => {
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
|
||||
|
||||
const DEFAULT_PROPS = { forkPath: 'some_file.js/fork' };
|
||||
|
||||
describe('ForkSuggestion component', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMountExtended(ForkSuggestion, {
|
||||
propsData: { ...DEFAULT_PROPS },
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => createComponent());
|
||||
|
||||
const { i18n } = ForkSuggestion;
|
||||
const findMessage = () => wrapper.findByTestId('message');
|
||||
const findForkButton = () => wrapper.findByTestId('fork');
|
||||
const findCancelButton = () => wrapper.findByTestId('cancel');
|
||||
|
||||
it('renders a message', () => {
|
||||
expect(findMessage().text()).toBe(i18n.message);
|
||||
});
|
||||
|
||||
it('renders a Fork button', () => {
|
||||
const forkButton = findForkButton();
|
||||
|
||||
expect(forkButton.text()).toBe(i18n.fork);
|
||||
expect(forkButton.attributes('href')).toBe(DEFAULT_PROPS.forkPath);
|
||||
});
|
||||
|
||||
it('renders a Cancel button', () => {
|
||||
expect(findCancelButton().text()).toBe(i18n.cancel);
|
||||
});
|
||||
|
||||
it('emits a cancel event when Cancel button is clicked', () => {
|
||||
findCancelButton().vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('cancel')).toEqual([[]]);
|
||||
});
|
||||
});
|
||||
|
|
@ -13,14 +13,12 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
|||
import { InternalEvents } from '~/tracking';
|
||||
import WebIdeLink from 'ee_else_ce/vue_shared/components/web_ide_link.vue';
|
||||
import { resetShortcutsForTests } from '~/behaviors/shortcuts';
|
||||
import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob';
|
||||
import Shortcuts from '~/behaviors/shortcuts/shortcuts';
|
||||
import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater';
|
||||
import OverflowMenu from 'ee_else_ce/repository/components/header_area/blob_overflow_menu.vue';
|
||||
import BlobControls from '~/repository/components/header_area/blob_controls.vue';
|
||||
import blobControlsQuery from '~/repository/queries/blob_controls.query.graphql';
|
||||
import userGitpodInfo from '~/repository/queries/user_gitpod_info.query.graphql';
|
||||
import applicationInfoQuery from '~/blob/queries/application_info.query.graphql';
|
||||
import applicationInfoQuery from '~/repository/queries/application_info.query.graphql';
|
||||
import createRouter from '~/repository/router';
|
||||
import OpenMrBadge from '~/repository/components/header_area/open_mr_badge.vue';
|
||||
import ForkSuggestionModal from '~/repository/components/header_area/fork_suggestion_modal.vue';
|
||||
|
|
@ -33,8 +31,6 @@ import {
|
|||
|
||||
Vue.use(VueApollo);
|
||||
jest.mock('~/repository/utils/dom');
|
||||
jest.mock('~/behaviors/shortcuts/shortcuts_blob');
|
||||
jest.mock('~/blob/blob_line_permalink_updater');
|
||||
jest.mock('~/alert');
|
||||
jest.mock('~/lib/logger');
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
|
|
@ -89,7 +85,7 @@ describe('Blob controls component', () => {
|
|||
blobControlsResolver = blobControlsSuccessResolver,
|
||||
currentUserResolver = currentUserSuccessResolver,
|
||||
applicationInfoResolver = applicationInfoSuccessResolver,
|
||||
glFeatures = { blobOverflowMenu: false },
|
||||
glFeatures = {},
|
||||
} = {}) => {
|
||||
const projectPath = 'some/project';
|
||||
router = createRouter(projectPath, refMock);
|
||||
|
|
@ -133,7 +129,6 @@ describe('Blob controls component', () => {
|
|||
const findOpenMrBadge = () => wrapper.findComponent(OpenMrBadge);
|
||||
const findFindButton = () => wrapper.findByTestId('find');
|
||||
const findBlameButton = () => wrapper.findByTestId('blame');
|
||||
const findPermalinkButton = () => wrapper.findByTestId('permalink');
|
||||
const findWebIdeLink = () => wrapper.findComponent(WebIdeLink);
|
||||
const findForkSuggestionModal = () => wrapper.findComponent(ForkSuggestionModal);
|
||||
const findOverflowMenu = () => wrapper.findComponent(OverflowMenu);
|
||||
|
|
@ -141,41 +136,13 @@ describe('Blob controls component', () => {
|
|||
const { bindInternalEventDocument } = useMockInternalEventsTracking();
|
||||
|
||||
beforeEach(async () => {
|
||||
createAlert.mockClear();
|
||||
await createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fakeApollo = null;
|
||||
});
|
||||
|
||||
it('loads the ShortcutsBlob', () => {
|
||||
expect(ShortcutsBlob).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('loads the BlobLinePermalinkUpdater', () => {
|
||||
expect(BlobLinePermalinkUpdater).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it.each`
|
||||
scenario | resolverParam | loggedError
|
||||
${'blobControls query'} | ${{ blobControlsResolver: blobControlsErrorResolver }} | ${'Failed to fetch blob controls. See exception details for more information.'}
|
||||
${'currentUser query'} | ${{ currentUserResolver: currentUserErrorResolver }} | ${'Failed to fetch current user. See exception details for more information.'}
|
||||
${'applicationInfo query'} | ${{ applicationInfoResolver: applicationInfoErrorResolver }} | ${'Failed to fetch application info. See exception details for more information.'}
|
||||
`(
|
||||
'renders an alert and logs the error if the $scenario fails',
|
||||
async ({ resolverParam, loggedError }) => {
|
||||
const mockError = new Error('Request failed');
|
||||
await createComponent(resolverParam);
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
message: 'An error occurred while loading the blob controls.',
|
||||
});
|
||||
expect(logError).toHaveBeenCalledWith(loggedError, mockError);
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(mockError);
|
||||
},
|
||||
);
|
||||
Sentry.captureException.mockRestore();
|
||||
createAlert.mockClear();
|
||||
});
|
||||
|
||||
describe('MR badge', () => {
|
||||
|
|
@ -192,11 +159,15 @@ describe('Blob controls component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('FindFile button', () => {
|
||||
it('renders FindFile button', () => {
|
||||
describe('Find file button', () => {
|
||||
it('renders by default', () => {
|
||||
expect(findFindButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render on mobile layout', () => {
|
||||
expect(findFindButton().classes()).toContain('gl-hidden', 'sm:gl-inline-flex');
|
||||
});
|
||||
|
||||
it('triggers a `focusSearchFile` shortcut when the findFile button is clicked', () => {
|
||||
jest.spyOn(Shortcuts, 'focusSearchFile').mockResolvedValue();
|
||||
|
||||
|
|
@ -224,6 +195,10 @@ describe('Blob controls component', () => {
|
|||
expect(findBlameButton().attributes('href')).toBe('blame/file.js');
|
||||
});
|
||||
|
||||
it('does not render on mobile layout', () => {
|
||||
expect(findBlameButton().classes()).toContain('gl-hidden', 'sm:gl-inline-flex');
|
||||
});
|
||||
|
||||
it('does not render blame button when blobInfo.storedExternally is true', async () => {
|
||||
const blobOverwriteResolver = overrideBlobControlsResolver({ storedExternally: true });
|
||||
await createComponent({ blobControlsResolver: blobOverwriteResolver });
|
||||
|
|
@ -260,216 +235,199 @@ describe('Blob controls component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('renders a permalink button with the correct href', () => {
|
||||
expect(findPermalinkButton().attributes('href')).toBe('permalink/file.js');
|
||||
});
|
||||
|
||||
it('does not render WebIdeLink component', () => {
|
||||
expect(findWebIdeLink().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when blobOverflowMenu feature flag is true', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponent({ glFeatures: { blobOverflowMenu: true } });
|
||||
});
|
||||
|
||||
describe('Find file button', () => {
|
||||
it('does not render on mobile layout', () => {
|
||||
expect(findFindButton().classes()).toContain('gl-hidden', 'sm:gl-inline-flex');
|
||||
describe('WebIdeLink component', () => {
|
||||
it('renders the WebIdeLink component with the correct props', () => {
|
||||
expect(findWebIdeLink().props()).toMatchObject({
|
||||
showEditButton: false,
|
||||
editUrl: 'https://edit/blob/path/file.js',
|
||||
webIdeUrl: 'https://ide/blob/path/file.js',
|
||||
needsToFork: false,
|
||||
needsToForkWithWebIde: false,
|
||||
showPipelineEditorButton: true,
|
||||
pipelineEditorUrl: 'pipeline/editor/path/file.yml',
|
||||
gitpodUrl: 'gitpod/blob/url/file.js',
|
||||
isGitpodEnabledForInstance: true,
|
||||
isGitpodEnabledForUser: true,
|
||||
disabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('Blame button', () => {
|
||||
it('does not render on mobile layout', () => {
|
||||
expect(findBlameButton().classes()).toContain('gl-hidden', 'sm:gl-inline-flex');
|
||||
it('disables the WebIdeLink component when file is LFS', async () => {
|
||||
const blobOverwriteResolver = overrideBlobControlsResolver({
|
||||
storedExternally: true,
|
||||
externalStorage: 'lfs',
|
||||
});
|
||||
await createComponent({
|
||||
blobControlsResolver: blobOverwriteResolver,
|
||||
});
|
||||
expect(findWebIdeLink().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render WebIdeLink component if file is archived', async () => {
|
||||
const blobOverwriteResolver = overrideBlobControlsResolver({
|
||||
...blobControlsDataMock.repository.blobs.nodes[0],
|
||||
archived: true,
|
||||
});
|
||||
await createComponent({
|
||||
blobControlsResolver: blobOverwriteResolver,
|
||||
});
|
||||
|
||||
expect(findWebIdeLink().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render WebIdeLink component if file is not editable', async () => {
|
||||
const blobOverwriteResolver = overrideBlobControlsResolver({
|
||||
...blobControlsDataMock.repository.blobs.nodes[0],
|
||||
editBlobPath: '',
|
||||
});
|
||||
await createComponent({
|
||||
blobControlsResolver: blobOverwriteResolver,
|
||||
});
|
||||
|
||||
expect(findWebIdeLink().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when can modify blob', () => {
|
||||
it('redirects to WebIDE to edit the file', async () => {
|
||||
findWebIdeLink().vm.$emit('edit', 'ide');
|
||||
await nextTick();
|
||||
|
||||
expect(visitUrl).toHaveBeenCalledWith('https://ide/blob/path/file.js');
|
||||
expect(findForkSuggestionModal().props('visible')).toBe(false);
|
||||
});
|
||||
|
||||
it('redirects to single file editor to edit the file', async () => {
|
||||
findWebIdeLink().vm.$emit('edit', 'simple');
|
||||
await nextTick();
|
||||
|
||||
expect(visitUrl).toHaveBeenCalledWith('https://edit/blob/path/file.js');
|
||||
expect(findForkSuggestionModal().props('visible')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WebIdeLink component', () => {
|
||||
it('renders the WebIdeLink component with the correct props', () => {
|
||||
expect(findWebIdeLink().props()).toMatchObject({
|
||||
showEditButton: false,
|
||||
editUrl: 'https://edit/blob/path/file.js',
|
||||
webIdeUrl: 'https://ide/blob/path/file.js',
|
||||
needsToFork: false,
|
||||
needsToForkWithWebIde: false,
|
||||
showPipelineEditorButton: true,
|
||||
pipelineEditorUrl: 'pipeline/editor/path/file.yml',
|
||||
gitpodUrl: 'gitpod/blob/url/file.js',
|
||||
isGitpodEnabledForInstance: true,
|
||||
isGitpodEnabledForUser: true,
|
||||
disabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('disables the WebIdeLink component when file is LFS', async () => {
|
||||
const blobOverwriteResolver = overrideBlobControlsResolver({
|
||||
storedExternally: true,
|
||||
externalStorage: 'lfs',
|
||||
});
|
||||
await createComponent({
|
||||
blobControlsResolver: blobOverwriteResolver,
|
||||
glFeatures: { blobOverflowMenu: true },
|
||||
});
|
||||
expect(findWebIdeLink().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render WebIdeLink component if file is archived', async () => {
|
||||
const blobOverwriteResolver = overrideBlobControlsResolver({
|
||||
...blobControlsDataMock.repository.blobs.nodes[0],
|
||||
archived: true,
|
||||
});
|
||||
await createComponent({
|
||||
blobControlsResolver: blobOverwriteResolver,
|
||||
glFeatures: { blobOverflowMenu: true },
|
||||
});
|
||||
|
||||
expect(findWebIdeLink().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render WebIdeLink component if file is not editable', async () => {
|
||||
const blobOverwriteResolver = overrideBlobControlsResolver({
|
||||
...blobControlsDataMock.repository.blobs.nodes[0],
|
||||
editBlobPath: '',
|
||||
});
|
||||
await createComponent({
|
||||
blobControlsResolver: blobOverwriteResolver,
|
||||
glFeatures: { blobOverflowMenu: true },
|
||||
});
|
||||
|
||||
expect(findWebIdeLink().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when can modify blob', () => {
|
||||
it('redirects to WebIDE to edit the file', async () => {
|
||||
findWebIdeLink().vm.$emit('edit', 'ide');
|
||||
await nextTick();
|
||||
|
||||
expect(visitUrl).toHaveBeenCalledWith('https://ide/blob/path/file.js');
|
||||
expect(findForkSuggestionModal().props('visible')).toBe(false);
|
||||
});
|
||||
|
||||
it('redirects to single file editor to edit the file', async () => {
|
||||
findWebIdeLink().vm.$emit('edit', 'simple');
|
||||
await nextTick();
|
||||
|
||||
expect(visitUrl).toHaveBeenCalledWith('https://edit/blob/path/file.js');
|
||||
expect(findForkSuggestionModal().props('visible')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user cannot modify blob', () => {
|
||||
it('changes ForkSuggestionModal visibility', async () => {
|
||||
const blobControlsForForkResolver = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
project: {
|
||||
...blobControlsDataMock,
|
||||
userPermissions: {
|
||||
...blobControlsDataMock.userPermissions,
|
||||
pushCode: false,
|
||||
createMergeRequestIn: true,
|
||||
},
|
||||
repository: {
|
||||
...blobControlsDataMock.repository,
|
||||
blobs: {
|
||||
...blobControlsDataMock.repository.blobs,
|
||||
nodes: [
|
||||
{
|
||||
...blobControlsDataMock.repository.blobs.nodes[0],
|
||||
canModifyBlob: false,
|
||||
canModifyBlobWithWebIde: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
describe('when user cannot modify blob', () => {
|
||||
it('changes ForkSuggestionModal visibility', async () => {
|
||||
const blobControlsForForkResolver = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
project: {
|
||||
...blobControlsDataMock,
|
||||
userPermissions: {
|
||||
...blobControlsDataMock.userPermissions,
|
||||
pushCode: false,
|
||||
createMergeRequestIn: true,
|
||||
},
|
||||
repository: {
|
||||
...blobControlsDataMock.repository,
|
||||
blobs: {
|
||||
...blobControlsDataMock.repository.blobs,
|
||||
nodes: [
|
||||
{
|
||||
...blobControlsDataMock.repository.blobs.nodes[0],
|
||||
canModifyBlob: false,
|
||||
canModifyBlobWithWebIde: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await createComponent({
|
||||
blobControlsResolver: blobControlsForForkResolver,
|
||||
glFeatures: { blobOverflowMenu: true },
|
||||
});
|
||||
|
||||
findWebIdeLink().vm.$emit('edit', 'simple');
|
||||
await nextTick();
|
||||
|
||||
expect(findForkSuggestionModal().props('visible')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ForkSuggestionModal component', () => {
|
||||
it('renders ForkSuggestionModal', () => {
|
||||
expect(findForkSuggestionModal().exists()).toBe(true);
|
||||
expect(findForkSuggestionModal().props()).toMatchObject({
|
||||
visible: false,
|
||||
forkPath: 'fork/view/path',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('BlobOverflow dropdown', () => {
|
||||
it('renders a spinner for BlobOverflowMenu when loading repository blob data', async () => {
|
||||
const loadingBlobControlsResolver = jest.fn().mockResolvedValue(new Promise(() => {}));
|
||||
await createComponent({
|
||||
glFeatures: { blobOverflowMenu: true },
|
||||
blobControlsResolver: loadingBlobControlsResolver,
|
||||
});
|
||||
expect(findOverflowMenuLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render a spinner for BlobOverflowMenu when not loading repository blob data', () => {
|
||||
expect(findOverflowMenuLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders BlobOverflow component with correct props', () => {
|
||||
expect(findOverflowMenu().exists()).toBe(true);
|
||||
expect(findOverflowMenu().props()).toEqual({
|
||||
projectPath: 'some/project',
|
||||
isBinaryFileType: true,
|
||||
overrideCopy: true,
|
||||
isEmptyRepository: false,
|
||||
isUsingLfs: false,
|
||||
eeCanCreateLock: undefined,
|
||||
eeCanDestroyLock: undefined,
|
||||
eeCanModifyFile: undefined,
|
||||
eeIsLocked: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('passes the correct isBinary value to BlobOverflow when viewing a binary file', async () => {
|
||||
await createComponent({
|
||||
props: {
|
||||
isBinary: true,
|
||||
},
|
||||
glFeatures: {
|
||||
blobOverflowMenu: true,
|
||||
},
|
||||
});
|
||||
await createComponent({
|
||||
blobControlsResolver: blobControlsForForkResolver,
|
||||
});
|
||||
|
||||
expect(findOverflowMenu().props('isBinaryFileType')).toBe(true);
|
||||
});
|
||||
|
||||
it('copies to clipboard raw blob text, when receives copy event', () => {
|
||||
jest.spyOn(navigator.clipboard, 'writeText');
|
||||
findOverflowMenu().vm.$emit('copy');
|
||||
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('Example raw text content');
|
||||
});
|
||||
|
||||
it('changes ForkSuggestionModal visibility when receives showForkSuggestion event', async () => {
|
||||
findOverflowMenu().vm.$emit('showForkSuggestion');
|
||||
findWebIdeLink().vm.$emit('edit', 'simple');
|
||||
await nextTick();
|
||||
|
||||
expect(findForkSuggestionModal().props('visible')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('proxy locked-file event', async () => {
|
||||
findOverflowMenu().vm.$emit('lockedFile', true);
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.emitted('lockedFile')).toEqual([[true]]);
|
||||
describe('ForkSuggestionModal component', () => {
|
||||
it('renders ForkSuggestionModal', () => {
|
||||
expect(findForkSuggestionModal().exists()).toBe(true);
|
||||
expect(findForkSuggestionModal().props()).toMatchObject({
|
||||
visible: false,
|
||||
forkPath: 'fork/view/path',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('BlobOverflow dropdown', () => {
|
||||
it('renders a spinner for BlobOverflowMenu when loading repository blob data', async () => {
|
||||
const loadingBlobControlsResolver = jest.fn().mockResolvedValue(new Promise(() => {}));
|
||||
await createComponent({
|
||||
blobControlsResolver: loadingBlobControlsResolver,
|
||||
});
|
||||
expect(findOverflowMenuLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render a spinner for BlobOverflowMenu when not loading repository blob data', () => {
|
||||
expect(findOverflowMenuLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders BlobOverflow component with correct props', () => {
|
||||
expect(findOverflowMenu().exists()).toBe(true);
|
||||
expect(findOverflowMenu().props()).toMatchObject({
|
||||
projectPath: 'some/project',
|
||||
isBinaryFileType: true,
|
||||
overrideCopy: true,
|
||||
isEmptyRepository: false,
|
||||
isUsingLfs: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('passes the correct isBinary value to BlobOverflow when viewing a binary file', async () => {
|
||||
await createComponent({
|
||||
props: {
|
||||
isBinary: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findOverflowMenu().props('isBinaryFileType')).toBe(true);
|
||||
});
|
||||
|
||||
it('copies to clipboard raw blob text, when receives copy event', () => {
|
||||
jest.spyOn(navigator.clipboard, 'writeText');
|
||||
findOverflowMenu().vm.$emit('copy');
|
||||
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('Example raw text content');
|
||||
});
|
||||
|
||||
it('changes ForkSuggestionModal visibility when receives showForkSuggestion event', async () => {
|
||||
findOverflowMenu().vm.$emit('showForkSuggestion');
|
||||
await nextTick();
|
||||
|
||||
expect(findForkSuggestionModal().props('visible')).toBe(true);
|
||||
});
|
||||
|
||||
it('proxy locked-file event', async () => {
|
||||
findOverflowMenu().vm.$emit('lockedFile', true);
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.emitted('lockedFile')).toEqual([[true]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error handling', () => {
|
||||
it.each`
|
||||
scenario | resolverParam | loggedError
|
||||
${'blobControls query'} | ${{ blobControlsResolver: blobControlsErrorResolver }} | ${'Failed to fetch blob controls. See exception details for more information.'}
|
||||
${'currentUser query'} | ${{ currentUserResolver: currentUserErrorResolver }} | ${'Failed to fetch current user. See exception details for more information.'}
|
||||
${'applicationInfo query'} | ${{ applicationInfoResolver: applicationInfoErrorResolver }} | ${'Failed to fetch application info. See exception details for more information.'}
|
||||
`(
|
||||
'renders an alert and logs the error if the $scenario fails',
|
||||
async ({ resolverParam, loggedError }) => {
|
||||
const mockError = new Error('Request failed');
|
||||
await createComponent(resolverParam);
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
message: 'An error occurred while loading the blob controls.',
|
||||
});
|
||||
expect(logError).toHaveBeenCalledWith(loggedError, mockError);
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(mockError);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -56,6 +56,9 @@ describe('Blob Overflow Menu', () => {
|
|||
blobInfo: blobControlsDataMock.repository.blobs.nodes[0],
|
||||
currentRef: refMock,
|
||||
rootRef: 'main',
|
||||
glFeatures: {
|
||||
fileLocks: false,
|
||||
},
|
||||
...provide,
|
||||
},
|
||||
propsData: {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { GlDisclosureDropdownItem } from '@gitlab/ui';
|
||||
import PermalinkDropdownItem from '~/repository/components/header_area/permalink_dropdown_item.vue';
|
||||
import { keysFor, PROJECT_FILES_GO_TO_PERMALINK } from '~/behaviors/shortcuts/keybindings';
|
||||
import { keysFor, PROJECT_FILES_COPY_FILE_PERMALINK } from '~/behaviors/shortcuts/keybindings';
|
||||
import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
|
||||
import { Mousetrap } from '~/lib/mousetrap';
|
||||
import { hashState, updateHash } from '~/blob/state';
|
||||
|
|
@ -169,12 +169,12 @@ describe('PermalinkDropdownItem', () => {
|
|||
|
||||
createComponent();
|
||||
expect(bindSpy).toHaveBeenCalledWith(
|
||||
keysFor(PROJECT_FILES_GO_TO_PERMALINK),
|
||||
keysFor(PROJECT_FILES_COPY_FILE_PERMALINK),
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
wrapper.destroy();
|
||||
expect(unbindSpy).toHaveBeenCalledWith(keysFor(PROJECT_FILES_GO_TO_PERMALINK));
|
||||
expect(unbindSpy).toHaveBeenCalledWith(keysFor(PROJECT_FILES_COPY_FILE_PERMALINK));
|
||||
});
|
||||
|
||||
it('add and remove event listener for hashChange event', () => {
|
||||
|
|
@ -193,7 +193,7 @@ describe('PermalinkDropdownItem', () => {
|
|||
shouldDisableShortcuts.mockReturnValue(false);
|
||||
createComponent();
|
||||
expect(wrapper.find('kbd').exists()).toBe(true);
|
||||
expect(wrapper.find('kbd').text()).toBe(keysFor(PROJECT_FILES_GO_TO_PERMALINK)[0]);
|
||||
expect(wrapper.find('kbd').text()).toBe(keysFor(PROJECT_FILES_COPY_FILE_PERMALINK)[0]);
|
||||
});
|
||||
|
||||
it('does not display the shortcut key when shortcuts are disabled', () => {
|
||||
|
|
|
|||
|
|
@ -123,26 +123,26 @@ describe('WorkItemChildrenWrapper', () => {
|
|||
expect(workItemLinkChildren).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('emits `show-modal` on `click` event', () => {
|
||||
it('emits `show-modal` on `toggleDrawer` event', () => {
|
||||
createComponent();
|
||||
const event = {
|
||||
childItem: 'gid://gitlab/WorkItem/2',
|
||||
stopPropagation: jest.fn(),
|
||||
};
|
||||
|
||||
findFirstWorkItemLinkChildItem().vm.$emit('click', event);
|
||||
findFirstWorkItemLinkChildItem().vm.$emit('toggleDrawer', event);
|
||||
|
||||
expect(wrapper.emitted('show-modal')).toEqual([[{ event, child: event.childItem }]]);
|
||||
});
|
||||
|
||||
it('emits `click` event when clicking on nested child', () => {
|
||||
it('emits `toggleDrawer` event when clicking on nested child', () => {
|
||||
createComponent({ isTopLevel: false });
|
||||
const event = {
|
||||
childItem: 'gid://gitlab/WorkItem/2',
|
||||
stopPropagation: jest.fn(),
|
||||
};
|
||||
|
||||
findFirstWorkItemLinkChildItem().vm.$emit('click', event);
|
||||
findFirstWorkItemLinkChildItem().vm.$emit('toggleDrawer', event);
|
||||
|
||||
expect(wrapper.emitted('click')).toEqual([[event]]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ describe('WorkItemLinkChild', () => {
|
|||
|
||||
findTreeChildren().vm.$emit('click', 'event');
|
||||
|
||||
expect(wrapper.emitted('click')).toEqual([['event']]);
|
||||
expect(wrapper.emitted('toggleDrawer')).toEqual([['event']]);
|
||||
});
|
||||
|
||||
it('emits event on removing child item', () => {
|
||||
|
|
|
|||
|
|
@ -2,18 +2,11 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::BackfillArchivedAndTraversalIdsToVulnerabilityStatistics, migration: :gitlab_sec, feature_category: :vulnerability_management do
|
||||
before(:all) do
|
||||
# Some spec in this file currently fails when a sec database is configured. We plan to ensure it all functions
|
||||
# and passes prior to the sec db rollout.
|
||||
# Consult https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180764 for more info.
|
||||
skip_if_multiple_databases_are_setup(:sec)
|
||||
end
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::BackfillArchivedAndTraversalIdsToVulnerabilityStatistics, feature_category: :vulnerability_management do
|
||||
let(:organizations) { table(:organizations) }
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:projects) { table(:projects) }
|
||||
let(:vulnerability_statistics) { table(:vulnerability_statistics) }
|
||||
let(:vulnerability_statistics) { table(:vulnerability_statistics, database: :sec) }
|
||||
let(:args) do
|
||||
min, max = vulnerability_statistics.pick('MIN(id)', 'MAX(id)')
|
||||
{
|
||||
|
|
|
|||
|
|
@ -123,9 +123,10 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillDetectedAtFromCreatedAtColum
|
|||
end
|
||||
|
||||
before(:all) do
|
||||
# This test shares the db connection to establish it's fixtures, resulting in
|
||||
# incorrect connection usage, so we're skipping it.
|
||||
# Consult https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180764 for more info.
|
||||
# This spec was disabled before the confidence column was dropped from
|
||||
# vulnerabilities. https://gitlab.com/gitlab-org/gitlab/-/merge_requests/182637/diffs
|
||||
# As a result, the BG migration can't work and the spec can't pass either, so no
|
||||
# point fixing it. We'll wait for it to be cleaned up with the migration.
|
||||
skip_if_multiple_databases_are_setup(:sec)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
RSpec.describe Gitlab::BackgroundMigration::BackfillPlaceholderUsersDetailsFromSourceUsers, feature_category: :importers do
|
||||
let(:migration_attrs) do
|
||||
{
|
||||
start_id: import_source_users_table.minimum(:id),
|
||||
end_id: import_source_users_table.maximum(:id),
|
||||
batch_table: :import_source_users,
|
||||
batch_column: :id,
|
||||
sub_batch_size: 2,
|
||||
pause_ms: 0,
|
||||
connection: connection
|
||||
}
|
||||
end
|
||||
|
||||
let(:migration) { described_class.new(**migration_attrs) }
|
||||
let(:connection) { ApplicationRecord.connection }
|
||||
|
||||
let(:import_source_users_table) { table(:import_source_users) }
|
||||
let(:import_placeholder_user_details_table) { table(:import_placeholder_user_details) }
|
||||
let(:users_table) { table(:users) }
|
||||
let(:namespaces_table) { table(:namespaces) }
|
||||
let(:organizations_table) { table(:organizations) }
|
||||
|
||||
let(:organization) { create_organization }
|
||||
let(:namespace) { create_namespace(organization_id: organization.id) }
|
||||
let(:user) { create_user(email: "test1@example.com", username: "test1") }
|
||||
|
||||
let(:source_user_without_placeholder) { create_source_user(placeholder_user_id: nil, namespace_id: namespace.id) }
|
||||
let(:existing_user) { create_user(email: "test2@example.com", username: "test2") }
|
||||
let(:source_user_with_existing_detail) do
|
||||
create_source_user(placeholder_user_id: existing_user.id, namespace_id: namespace.id)
|
||||
end
|
||||
|
||||
let!(:existing_detail) do
|
||||
create_placeholder_user_detail(placeholder_user_id: existing_user.id, namespace_id: namespace.id,
|
||||
organization_id: organization.id)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
it 'creates placeholder user details for source users without them', :aggregate_failures do
|
||||
create_source_user(placeholder_user_id: user.id, namespace_id: namespace.id)
|
||||
|
||||
expect do
|
||||
migration.perform
|
||||
end.to change { count_placeholder_user_details }.by(1)
|
||||
|
||||
detail = find_placeholder_user_detail(user.id)
|
||||
expect(detail).to be_present
|
||||
expect(detail['namespace_id']).to eq(namespace.id)
|
||||
expect(detail['organization_id']).to eq(organization.id)
|
||||
end
|
||||
|
||||
it 'skips source users without placeholder_user_id' do
|
||||
create_source_user(placeholder_user_id: nil, namespace_id: namespace.id)
|
||||
|
||||
expect do
|
||||
migration.perform
|
||||
end.not_to change { count_placeholder_user_details }
|
||||
end
|
||||
|
||||
it 'skips source users that already have placeholder user details' do
|
||||
create_source_user(placeholder_user_id: existing_user.id, namespace_id: namespace.id)
|
||||
|
||||
expect do
|
||||
migration.perform
|
||||
end.not_to change { count_placeholder_user_details(existing_user.id) }
|
||||
end
|
||||
|
||||
it 'logs error and re-raises exception when bulk insert fails', :aggregate_failures do
|
||||
create_source_user(placeholder_user_id: user.id, namespace_id: namespace.id)
|
||||
|
||||
logger = instance_double(Gitlab::BackgroundMigration::Logger)
|
||||
allow(Gitlab::BackgroundMigration::Logger).to receive(:build).and_return(logger)
|
||||
|
||||
exception = StandardError.new("Database connection error")
|
||||
allow(Gitlab::BackgroundMigration::BackfillPlaceholderUsersDetailsFromSourceUsers::ImportPlaceholderUserDetail)
|
||||
.to receive(:upsert_all).and_raise(exception)
|
||||
|
||||
expect(logger).to receive(:error).with(
|
||||
hash_including(
|
||||
message: "Error bulk creating placeholder user details: Database connection error",
|
||||
count: 1,
|
||||
placeholder_user_ids: [user.id]
|
||||
)
|
||||
)
|
||||
expect { migration.perform }.to raise_error(StandardError, "Database connection error")
|
||||
end
|
||||
|
||||
it 'processes multiple batches correctly', :aggregate_failures do
|
||||
user3 = create_user(email: "test3@example.com", username: "test3")
|
||||
user4 = create_user(email: "test4@example.com", username: "test4")
|
||||
user5 = create_user(email: "test5@example.com", username: "test5")
|
||||
|
||||
create_source_user(placeholder_user_id: user.id, namespace_id: namespace.id)
|
||||
create_source_user(placeholder_user_id: user3.id, namespace_id: namespace.id)
|
||||
create_source_user(placeholder_user_id: user4.id, namespace_id: namespace.id)
|
||||
create_source_user(placeholder_user_id: user5.id, namespace_id: namespace.id)
|
||||
|
||||
create_placeholder_user_detail(placeholder_user_id: user4.id, namespace_id: namespace.id,
|
||||
organization_id: organization.id)
|
||||
create_source_user(placeholder_user_id: nil, namespace_id: namespace.id)
|
||||
|
||||
expect do
|
||||
migration.perform
|
||||
end.to change { count_placeholder_user_details }.by(3)
|
||||
end
|
||||
end
|
||||
|
||||
def create_organization
|
||||
organizations_table.create!(
|
||||
name: 'GitLab Enterprise',
|
||||
path: 'gitlab-enterprise'
|
||||
)
|
||||
end
|
||||
|
||||
def create_namespace(organization_id:)
|
||||
namespaces_table.create!(
|
||||
name: 'Engineering Department',
|
||||
path: 'engineering-dept',
|
||||
organization_id: organization_id
|
||||
)
|
||||
end
|
||||
|
||||
def create_user(overrides = {})
|
||||
attrs = {
|
||||
email: "test@example.com",
|
||||
notification_email: "test@example.com",
|
||||
name: "test",
|
||||
username: "test",
|
||||
state: "active",
|
||||
projects_limit: 10
|
||||
}.merge(overrides)
|
||||
|
||||
users_table.create!(attrs)
|
||||
end
|
||||
|
||||
def create_source_user(placeholder_user_id:, namespace_id:)
|
||||
import_source_users_table.create!(
|
||||
placeholder_user_id: placeholder_user_id,
|
||||
namespace_id: namespace_id,
|
||||
source_user_identifier: SecureRandom.uuid,
|
||||
source_hostname: 'https://gitlab.com',
|
||||
source_name: 'test-user',
|
||||
source_username: 'test-user1',
|
||||
import_type: 'gitlab_migration'
|
||||
)
|
||||
end
|
||||
|
||||
def create_placeholder_user_detail(placeholder_user_id:, namespace_id:, organization_id:)
|
||||
import_placeholder_user_details_table.create!(
|
||||
placeholder_user_id: placeholder_user_id,
|
||||
namespace_id: namespace_id,
|
||||
organization_id: organization_id
|
||||
)
|
||||
end
|
||||
|
||||
def count_placeholder_user_details(placeholder_user_id = nil)
|
||||
query = import_placeholder_user_details_table
|
||||
query = query.where(placeholder_user_id: placeholder_user_id) if placeholder_user_id
|
||||
query.count
|
||||
end
|
||||
|
||||
def find_placeholder_user_detail(placeholder_user_id)
|
||||
Import::PlaceholderUserDetail.find_by(placeholder_user_id: placeholder_user_id)
|
||||
end
|
||||
end
|
||||
|
|
@ -28,9 +28,11 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillProjectIdToDependencyListExp
|
|||
subject(:perform_migration) { described_class.new(**args).perform }
|
||||
|
||||
before do
|
||||
# This test shares the db connection to establish it's fixtures, resulting in
|
||||
# incorrect connection usage, so we're skipping it.
|
||||
# Consult https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180764 for more info.
|
||||
# With the introduction of:
|
||||
# CONSTRAINT check_67a9c23e79 CHECK ((num_nonnulls(group_id, organization_id, project_id) > 0))
|
||||
# the table now has a sharding key. As this bbm was finalised in
|
||||
# migration 20241106181154, making the spec pass isn't worht the effort.
|
||||
|
||||
skip_if_multiple_databases_are_setup(:sec)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -28,9 +28,10 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillProjectIdToSecurityScans, fe
|
|||
subject(:perform_migration) { described_class.new(**args).perform }
|
||||
|
||||
before do
|
||||
# This test shares the db connection to establish it's fixtures, resulting in
|
||||
# incorrect connection usage, so we're skipping it.
|
||||
# Consult https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180764 for more info.
|
||||
# With the introduction of CONSTRAINT check_2d56d882f6 CHECK ((project_id IS NOT NULL))
|
||||
# it's no longer possible for project_id to be null, meaning this spec is now inapplicable
|
||||
# and waiting for the cleanup of this BG migration.
|
||||
|
||||
skip_if_multiple_databases_are_setup(:sec)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,8 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Gitlab::BackgroundMigration::BackfillVulnerabilityExternalIssueLinksProjectId,
|
||||
feature_category: :vulnerability_management,
|
||||
schema: 20240624135059 do
|
||||
before do
|
||||
# This test shares the db connection to establish it's fixtures, resulting in
|
||||
# incorrect connection usage, so we're skipping it.
|
||||
# Consult https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180764 for more info.
|
||||
skip_if_multiple_databases_are_setup(:sec)
|
||||
end
|
||||
|
||||
schema: 20240624135059,
|
||||
migration: :gitlab_sec do
|
||||
include_examples 'desired sharding key backfill job' do
|
||||
let(:batch_table) { :vulnerability_external_issue_links }
|
||||
let(:backfill_column) { :project_id }
|
||||
|
|
|
|||
|
|
@ -4,14 +4,8 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Gitlab::BackgroundMigration::BackfillVulnerabilityOccurrenceIdentifiersProjectId,
|
||||
feature_category: :vulnerability_management,
|
||||
schema: 20240730171958 do
|
||||
before do
|
||||
# This test shares the db connection to establish it's fixtures, resulting in
|
||||
# incorrect connection usage, so we're skipping it.
|
||||
# Consult https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180764 for more info.
|
||||
skip_if_multiple_databases_are_setup(:sec)
|
||||
end
|
||||
|
||||
schema: 20240730171958,
|
||||
migration: :gitlab_sec do
|
||||
include_examples 'desired sharding key backfill job' do
|
||||
let(:batch_table) { :vulnerability_occurrence_identifiers }
|
||||
let(:backfill_column) { :project_id }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::RemoveNamespaceFromOsTypeSbomComponents, schema: 20240909204952, feature_category: :software_composition_analysis do
|
||||
let(:components) { table(:sbom_components) }
|
||||
let(:components) { table(:sbom_components, database: :sec) }
|
||||
let(:expected) do
|
||||
(0...os_prefix_to_purl_type_mapping.size).map { |n| "package-#{n}" }
|
||||
end
|
||||
|
|
@ -33,11 +33,6 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveNamespaceFromOsTypeSbomCompone
|
|||
end
|
||||
|
||||
before do
|
||||
# This test shares the db connection to establish it's fixtures, resulting in
|
||||
# incorrect connection usage, so we're skipping it.
|
||||
# Consult https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180764 for more info.
|
||||
skip_if_multiple_databases_are_setup(:sec)
|
||||
|
||||
os_prefix_to_purl_type_mapping.each.with_index do |(namespace, purl_type), index|
|
||||
components.create!(name: "#{namespace}/package-#{index}", purl_type: purl_type, component_type: 0)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Gitlab::BackgroundMigration::ResyncHasVulnerabilities, feature_category: :vulnerability_management do
|
||||
let(:project_settings) { table(:project_settings) }
|
||||
let(:findings) { table(:vulnerability_occurrences) }
|
||||
let(:vulnerabilities) { table(:vulnerabilities) }
|
||||
let(:identifiers) { table(:vulnerability_identifiers) }
|
||||
let(:scanners) { table(:vulnerability_scanners) }
|
||||
let(:findings) { table(:vulnerability_occurrences, database: :sec) }
|
||||
let(:vulnerabilities) { table(:vulnerabilities, database: :sec) }
|
||||
let(:identifiers) { table(:vulnerability_identifiers, database: :sec) }
|
||||
let(:scanners) { table(:vulnerability_scanners, database: :sec) }
|
||||
let!(:user) { table(:users).create!(email: 'author@example.com', username: 'author', projects_limit: 10) }
|
||||
let(:organizations) { table(:organizations) }
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
|
|
@ -68,13 +68,6 @@ RSpec.describe Gitlab::BackgroundMigration::ResyncHasVulnerabilities, feature_ca
|
|||
|
||||
subject(:perform_migration) { described_class.new(**args).perform }
|
||||
|
||||
before do
|
||||
# This test shares the db connection to establish it's fixtures, resulting in
|
||||
# incorrect connection usage, so we're skipping it.
|
||||
# Consult https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180764 for more info.
|
||||
skip_if_multiple_databases_are_setup(:sec)
|
||||
end
|
||||
|
||||
def create_project_setting(
|
||||
name,
|
||||
has_vulnerabilities_setting:,
|
||||
|
|
@ -115,7 +108,6 @@ RSpec.describe Gitlab::BackgroundMigration::ResyncHasVulnerabilities, feature_ca
|
|||
finding = findings.create!(
|
||||
scanner_id: scanner.id,
|
||||
primary_identifier_id: primary_identifier.id,
|
||||
project_fingerprint: SecureRandom.hex(20),
|
||||
location_fingerprint: SecureRandom.hex(20),
|
||||
uuid: SecureRandom.uuid,
|
||||
name: 'name',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe QueueBackfillPlaceholderUsersDetailsFromSourceUsers, migration: :gitlab_main, feature_category: :importers do
|
||||
let(:batched_migration) { described_class::MIGRATION }
|
||||
|
||||
it 'schedules a new batched migration' do
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(batched_migration).to have_scheduled_batched_migration(
|
||||
gitlab_schema: :gitlab_main,
|
||||
table_name: :import_source_users,
|
||||
column_name: :id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe RemoveCustomerRelationContactsForEpicType, migration: :gitlab_main, feature_category: :service_desk do
|
||||
let(:organizations) { table(:organizations) }
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:projects) { table(:projects) }
|
||||
let(:issues) { table(:issues) }
|
||||
let(:work_item_types) { table(:work_item_types) }
|
||||
let(:customer_relations_contacts) { table(:customer_relations_contacts) }
|
||||
let(:issue_customer_relations_contacts) { table(:issue_customer_relations_contacts) }
|
||||
|
||||
let(:epic_work_item_type_id) { described_class::EPIC_WORK_ITEM_TYPE_ID }
|
||||
|
||||
let!(:organization) { organizations.create!(name: 'Test Org', path: 'test-org') }
|
||||
let!(:namespace) { namespaces.create!(name: 'Test Group', path: 'test-group', organization_id: organization.id) }
|
||||
let!(:project) do
|
||||
projects.create!(name: 'Test Project', path: 'test-project', namespace_id: namespace.id,
|
||||
project_namespace_id: namespace.id, organization_id: organization.id)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const("#{described_class}::BATCH_SIZE", 2)
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
let!(:epic_issue) do
|
||||
issues.create!(
|
||||
title: 'Epic Issue',
|
||||
project_id: project.id,
|
||||
namespace_id: namespace.id,
|
||||
work_item_type_id: epic_work_item_type_id
|
||||
)
|
||||
end
|
||||
|
||||
let!(:regular_issue) do
|
||||
issues.create!(
|
||||
title: 'Regular Issue',
|
||||
project_id: project.id,
|
||||
namespace_id: namespace.id,
|
||||
work_item_type_id: 1
|
||||
)
|
||||
end
|
||||
|
||||
let!(:epic_contacts) do
|
||||
[
|
||||
customer_relations_contacts.create!(group_id: namespace.id, first_name: 'Epic', last_name: 'Contact1',
|
||||
email: 'epic1@example.com'),
|
||||
customer_relations_contacts.create!(group_id: namespace.id, first_name: 'Epic', last_name: 'Contact2',
|
||||
email: 'epic2@example.com'),
|
||||
customer_relations_contacts.create!(group_id: namespace.id, first_name: 'Epic', last_name: 'Contact3',
|
||||
email: 'epic3@example.com')
|
||||
]
|
||||
end
|
||||
|
||||
let!(:regular_contact) do
|
||||
customer_relations_contacts.create!(group_id: namespace.id, first_name: 'Regular', last_name: 'Contact',
|
||||
email: 'regular@example.com')
|
||||
end
|
||||
|
||||
before do
|
||||
epic_contacts.each do |contact|
|
||||
issue_customer_relations_contacts.create!(
|
||||
issue_id: epic_issue.id,
|
||||
contact_id: contact.id,
|
||||
namespace_id: namespace.id
|
||||
)
|
||||
end
|
||||
|
||||
issue_customer_relations_contacts.create!(
|
||||
issue_id: regular_issue.id,
|
||||
contact_id: regular_contact.id,
|
||||
namespace_id: namespace.id
|
||||
)
|
||||
end
|
||||
|
||||
it 'removes issue_customer_relations_contacts associated with epic work item type in batches' do
|
||||
expect do
|
||||
migrate!
|
||||
end.to make_queries_matching(
|
||||
/DELETE FROM issue_customer_relations_contacts/,
|
||||
2 # Should run 2 batches (3 epic contacts + 1 regular contact, batch size = 2)
|
||||
)
|
||||
end
|
||||
|
||||
it 'removes only issue_customer_relations_contacts associated with epic work item type' do
|
||||
expect { migrate! }.to change { issue_customer_relations_contacts.count }.from(4).to(1)
|
||||
.and not_change { customer_relations_contacts.count }
|
||||
end
|
||||
|
||||
it 'keeps issue_customer_relations_contacts for non-epic work item types' do
|
||||
expect { migrate! }.to change {
|
||||
issue_customer_relations_contacts.where(issue_id: epic_issue.id).count
|
||||
}.from(3).to(0)
|
||||
.and not_change { issue_customer_relations_contacts.where(issue_id: regular_issue.id).count }
|
||||
end
|
||||
|
||||
context 'when there are no epic-related contacts' do
|
||||
before do
|
||||
customer_relations_contacts.delete_all
|
||||
issue_customer_relations_contacts.delete_all
|
||||
|
||||
regular_contact_new = customer_relations_contacts.create!(
|
||||
group_id: namespace.id,
|
||||
first_name: 'Regular',
|
||||
last_name: 'Contact',
|
||||
email: 'regular@example.com'
|
||||
)
|
||||
|
||||
issue_customer_relations_contacts.create!(
|
||||
issue_id: regular_issue.id,
|
||||
contact_id: regular_contact_new.id,
|
||||
namespace_id: namespace.id
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not remove any issue_customer_relations_contacts' do
|
||||
expect { migrate! }.not_to change { issue_customer_relations_contacts.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'is a no-op' do
|
||||
expect { schema_migrate_down! }.not_to change { issue_customer_relations_contacts.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3038,10 +3038,8 @@
|
|||
- './spec/features/projects/badges/list_spec.rb'
|
||||
- './spec/features/projects/badges/pipeline_badge_spec.rb'
|
||||
- './spec/features/projects/blobs/blame_spec.rb'
|
||||
- './spec/features/projects/blobs/blob_line_permalink_updater_spec.rb'
|
||||
- './spec/features/projects/blobs/blob_show_spec.rb'
|
||||
- './spec/features/projects/blobs/edit_spec.rb'
|
||||
- './spec/features/projects/blobs/shortcuts_blob_spec.rb'
|
||||
- './spec/features/projects/blobs/user_views_pipeline_editor_button_spec.rb'
|
||||
- './spec/features/projects/branches/download_buttons_spec.rb'
|
||||
- './spec/features/projects/branches/new_branch_ref_dropdown_spec.rb'
|
||||
|
|
|
|||
Loading…
Reference in New Issue