Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6495a4fdc3
commit
9bef5f3d98
|
|
@ -162,9 +162,7 @@ retrieve-frontend-fixtures:
|
|||
echoinfo "INFO: Reusing frontend fixtures from 'retrieve-frontend-fixtures'."
|
||||
exit 0
|
||||
fi
|
||||
- run_timed_command "gem install knapsack --no-document"
|
||||
- section_start "gitaly-test-spawn" "Spawning Gitaly"; scripts/gitaly-test-spawn; section_end "gitaly-test-spawn"; # Do not use 'bundle exec' here
|
||||
- source ./scripts/rspec_helpers.sh
|
||||
- !reference [.base-script, script]
|
||||
- rspec_parallelized_job
|
||||
artifacts:
|
||||
name: frontend-fixtures
|
||||
|
|
|
|||
|
|
@ -240,12 +240,14 @@
|
|||
- ".gitlab/ci/review-apps/qa.gitlab-ci.yml"
|
||||
- ".gitlab/ci/review-apps/rules.gitlab-ci.yml"
|
||||
- ".gitlab/ci/test-on-gdk/*.yml"
|
||||
- ".gitlab/ci/version.yml"
|
||||
|
||||
.gitaly-patterns: &gitaly-patterns
|
||||
- "GITALY_SERVER_VERSION"
|
||||
- "lib/gitlab/setup_helper.rb"
|
||||
|
||||
.workhorse-patterns: &workhorse-patterns
|
||||
- ".gitlab/ci/version.yml"
|
||||
- ".gitlab/ci/workhorse.gitlab-ci.yml"
|
||||
- "GITLAB_WORKHORSE_VERSION"
|
||||
- "workhorse/**/*"
|
||||
|
|
@ -713,7 +715,8 @@
|
|||
- "ee/{lib/,spec/}tasks/gitlab/custom_roles/*"
|
||||
|
||||
.cng-orchestrator-patterns: &cng-orchestrator-patterns
|
||||
- qa/gems/gitlab-cng/**/*.rb
|
||||
- "qa/gems/gitlab-cng/**/*.rb"
|
||||
- "qa/gems/gitlab-cng/{Gemfile,Gemfile.lock}"
|
||||
|
||||
##################
|
||||
# Conditions set #
|
||||
|
|
|
|||
2
Gemfile
2
Gemfile
|
|
@ -228,7 +228,7 @@ gem 'elasticsearch-rails', '~> 7.2', require: 'elasticsearch/rails/instrumentati
|
|||
gem 'elasticsearch-api', '7.17.11', feature_category: :global_search
|
||||
gem 'aws-sdk-core', '~> 3.201.0' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'aws-sdk-cloudformation', '~> 1' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'aws-sdk-s3', '~> 1.156.0' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'aws-sdk-s3', '~> 1.157.0' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
gem 'faraday-typhoeus', '~> 1.1', feature_category: :global_search
|
||||
gem 'faraday_middleware-aws-sigv4', '~> 1.0.1', feature_category: :global_search
|
||||
# Used with Elasticsearch to support http keep-alive connections
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
{"name":"aws-sdk-cloudformation","version":"1.41.0","platform":"ruby","checksum":"31e47539719734413671edf9b1a31f8673fbf9688549f50c41affabbcb1c6b26"},
|
||||
{"name":"aws-sdk-core","version":"3.201.3","platform":"ruby","checksum":"c045a7ff37b4a6f1de5742e64def0841bdf70d215cb17d3875b2c5bdd9e99d52"},
|
||||
{"name":"aws-sdk-kms","version":"1.76.0","platform":"ruby","checksum":"e7f75013cba9ba357144f66bbc600631c192e2cda9dd572794be239654e2cf49"},
|
||||
{"name":"aws-sdk-s3","version":"1.156.0","platform":"ruby","checksum":"9302da1d1a70363308854d5065035f6c72cf8b8af895d8789487cd5c6b076a46"},
|
||||
{"name":"aws-sdk-s3","version":"1.157.0","platform":"ruby","checksum":"e1e0c7a268e710a7ccf4a0f9d2c33e3ca685b06968c3048d907e3a792580e990"},
|
||||
{"name":"aws-sigv4","version":"1.8.0","platform":"ruby","checksum":"84dd99768b91b93b63d1d8e53ee837cfd06ab402812772a7899a78f9f9117cbc"},
|
||||
{"name":"axe-core-api","version":"4.9.1","platform":"ruby","checksum":"9ea7ac16bfee1cb3545345d210878aa8cccfb41b493e00fe1faab79af4d9fed8"},
|
||||
{"name":"axe-core-rspec","version":"4.9.1","platform":"ruby","checksum":"31ef067bee36d6efb3f156a83aa2fb6ac721270a53fb9473f0268e325a3e6efd"},
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ GEM
|
|||
aws-sdk-kms (1.76.0)
|
||||
aws-sdk-core (~> 3, >= 3.188.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.156.0)
|
||||
aws-sdk-s3 (1.157.0)
|
||||
aws-sdk-core (~> 3, >= 3.201.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
|
|
@ -1968,7 +1968,7 @@ DEPENDENCIES
|
|||
awesome_print
|
||||
aws-sdk-cloudformation (~> 1)
|
||||
aws-sdk-core (~> 3.201.0)
|
||||
aws-sdk-s3 (~> 1.156.0)
|
||||
aws-sdk-s3 (~> 1.157.0)
|
||||
axe-core-rspec (~> 4.9.0)
|
||||
babosa (~> 2.0)
|
||||
base32 (~> 0.3.0)
|
||||
|
|
|
|||
|
|
@ -1,41 +1,38 @@
|
|||
<script>
|
||||
import { GlButton, GlSprintf, GlCollapsibleListbox, GlIcon } from '@gitlab/ui';
|
||||
import { GlButton, GlSprintf, GlDisclosureDropdown } from '@gitlab/ui';
|
||||
import GITLAB_LOGO_SVG_URL from '@gitlab/svgs/dist/illustrations/gitlab_logo.svg?url';
|
||||
import { s__ } from '~/locale';
|
||||
import { logError } from '~/lib/logger';
|
||||
import { joinPaths, stripRelativeUrlRootFromPath } from '~/lib/utils/url_utility';
|
||||
|
||||
export default {
|
||||
name: 'OAuthDomainMismatchError',
|
||||
components: {
|
||||
GlButton,
|
||||
GlSprintf,
|
||||
GlCollapsibleListbox,
|
||||
GlIcon,
|
||||
GlDisclosureDropdown,
|
||||
},
|
||||
props: {
|
||||
callbackUrlOrigins: {
|
||||
expectedCallbackUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
callbackUrls: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
dropdownItems() {
|
||||
return this.callbackUrlOrigins.map((domain) => {
|
||||
return {
|
||||
value: domain,
|
||||
text: domain,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reloadPage(urlDomain) {
|
||||
try {
|
||||
const current = new URL(urlDomain + window.location.pathname);
|
||||
window.location.replace(current.toString());
|
||||
} catch (e) {
|
||||
logError(s__('IDE|Error reloading page'), e);
|
||||
}
|
||||
const currentOrigin = window.location.origin;
|
||||
|
||||
return this.callbackUrls
|
||||
.filter(({ base }) => new URL(base).origin !== currentOrigin)
|
||||
.map(({ base }) => {
|
||||
return {
|
||||
href: joinPaths(base, stripRelativeUrlRootFromPath(window.location.pathname)),
|
||||
text: base,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
gitlabLogo: GITLAB_LOGO_SVG_URL,
|
||||
|
|
@ -50,6 +47,7 @@ export default {
|
|||
description: s__(
|
||||
"IDE|The URL you're using to access the Web IDE and the configured OAuth callback URL do not match. This issue often occurs when you're using a proxy.",
|
||||
),
|
||||
expected: s__('IDE|Could not find a callback URL entry for %{expectedCallbackUrl}.'),
|
||||
contact: s__(
|
||||
'IDE|Contact your administrator or try to open the Web IDE again with another domain.',
|
||||
),
|
||||
|
|
@ -64,27 +62,28 @@ export default {
|
|||
<p>
|
||||
{{ $options.i18n.description }}
|
||||
</p>
|
||||
<gl-sprintf :message="$options.i18n.expected">
|
||||
<template #expectedCallbackUrl>
|
||||
<code>{{ expectedCallbackUrl }}</code>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
<p>
|
||||
{{ $options.i18n.contact }}
|
||||
</p>
|
||||
<div class="gl-mt-6">
|
||||
<gl-collapsible-listbox
|
||||
v-if="callbackUrlOrigins.length > 1"
|
||||
<gl-disclosure-dropdown
|
||||
v-if="dropdownItems.length > 1"
|
||||
:items="dropdownItems"
|
||||
:header-text="$options.i18n.dropdownHeader"
|
||||
@select="reloadPage"
|
||||
:toggle-text="$options.i18n.buttonText.domains"
|
||||
/>
|
||||
<gl-button
|
||||
v-else-if="dropdownItems.length === 1"
|
||||
variant="confirm"
|
||||
:href="dropdownItems[0].href"
|
||||
>
|
||||
<template #toggle>
|
||||
<gl-button variant="confirm" class="self-center">
|
||||
{{ $options.i18n.buttonText.domains }}
|
||||
<gl-icon class="dropdown-chevron gl-ml-2" name="chevron-down" />
|
||||
</gl-button>
|
||||
</template>
|
||||
</gl-collapsible-listbox>
|
||||
<gl-button v-else variant="confirm" @click="reloadPage(callbackUrlOrigins[0])">
|
||||
<gl-sprintf :message="$options.i18n.buttonText.singleDomain">
|
||||
<template #domain>
|
||||
{{ callbackUrlOrigins[0] }}
|
||||
{{ dropdownItems[0].text }}
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-button>
|
||||
|
|
|
|||
|
|
@ -112,3 +112,7 @@ export const DEFAULT_BRANCH = 'main';
|
|||
export const GITLAB_WEB_IDE_FEEDBACK_ISSUE = 'https://gitlab.com/gitlab-org/gitlab/-/issues/377367';
|
||||
|
||||
export const IDE_ELEMENT_ID = 'ide';
|
||||
|
||||
// note: This path comes from `config/routes.rb`
|
||||
export const IDE_PATH = '/-/ide';
|
||||
export const WEB_IDE_OAUTH_CALLBACK_URL_PATH = '/-/ide/oauth_redirect';
|
||||
|
|
|
|||
|
|
@ -24,24 +24,20 @@ export async function startIde(options) {
|
|||
return;
|
||||
}
|
||||
|
||||
const oAuthCallbackDomainMismatchApp = new OAuthCallbackDomainMismatchErrorApp(
|
||||
ideElement,
|
||||
ideElement.dataset.callbackUrls,
|
||||
);
|
||||
|
||||
if (oAuthCallbackDomainMismatchApp.isVisitingFromNonRegisteredOrigin()) {
|
||||
oAuthCallbackDomainMismatchApp.renderError();
|
||||
return;
|
||||
}
|
||||
|
||||
const useNewWebIde = parseBoolean(ideElement.dataset.useNewWebIde);
|
||||
|
||||
if (useNewWebIde) {
|
||||
const { initGitlabWebIDE } = await import('./init_gitlab_web_ide');
|
||||
initGitlabWebIDE(ideElement);
|
||||
} else {
|
||||
if (!useNewWebIde) {
|
||||
resetServiceWorkersPublicPath();
|
||||
const { initLegacyWebIDE } = await import('./init_legacy_web_ide');
|
||||
initLegacyWebIDE(ideElement, options);
|
||||
}
|
||||
|
||||
const oAuthCallbackDomainMismatchApp = new OAuthCallbackDomainMismatchErrorApp(ideElement);
|
||||
|
||||
if (oAuthCallbackDomainMismatchApp.shouldRenderError()) {
|
||||
oAuthCallbackDomainMismatchApp.renderError();
|
||||
return;
|
||||
}
|
||||
const { initGitlabWebIDE } = await import('./init_gitlab_web_ide');
|
||||
initGitlabWebIDE(ideElement);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ const getGitLabUrl = (gitlabPath = '') => {
|
|||
const path = joinPaths('/', window.gon.relative_url_root || '', gitlabPath);
|
||||
const baseUrlObj = new URL(path, window.location.origin);
|
||||
|
||||
return cleanEndingSeparator(baseUrlObj.href);
|
||||
return baseUrlObj.href;
|
||||
};
|
||||
|
||||
export const getBaseConfig = () => ({
|
||||
// baseUrl - The URL which hosts the Web IDE static web assets
|
||||
baseUrl: getGitLabUrl(process.env.GITLAB_WEB_IDE_PUBLIC_PATH),
|
||||
// baseUrl - The URL for the GitLab instance
|
||||
gitlabUrl: getGitLabUrl(''),
|
||||
baseUrl: cleanEndingSeparator(getGitLabUrl(process.env.GITLAB_WEB_IDE_PUBLIC_PATH)),
|
||||
// gitlabUrl - The URL for the GitLab instance. End with trailing slash so URL's are built properly in relative_url_root.
|
||||
gitlabUrl: getGitLabUrl('/'),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const WEB_IDE_OAUTH_CALLBACK_URL_PATH = '/-/ide/oauth_redirect';
|
||||
import { getOAuthCallbackUrl } from './oauth_callback_urls';
|
||||
|
||||
export const getOAuthConfig = ({ clientId }) => {
|
||||
if (!clientId) {
|
||||
|
|
@ -8,7 +8,7 @@ export const getOAuthConfig = ({ clientId }) => {
|
|||
return {
|
||||
type: 'oauth',
|
||||
clientId,
|
||||
callbackUrl: new URL(WEB_IDE_OAUTH_CALLBACK_URL_PATH, window.location.origin).toString(),
|
||||
callbackUrl: getOAuthCallbackUrl(),
|
||||
protectRefreshToken: true,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
import { logError } from '~/lib/logger';
|
||||
import { WEB_IDE_OAUTH_CALLBACK_URL_PATH, IDE_PATH } from '../../constants';
|
||||
|
||||
/**
|
||||
* @returns callback URL constructed from current window url
|
||||
*/
|
||||
export function getOAuthCallbackUrl() {
|
||||
const url = window.location.href;
|
||||
|
||||
// We don't rely on `gon.gitlab_url` and `gon.relative_url_root` here because these may not be configured correctly
|
||||
// or we're visiting the instance through a proxy.
|
||||
// Instead, we split on the `/-/ide` in the `href` and use the first part as the base URL.
|
||||
const baseUrl = url.split(IDE_PATH, 2)[0];
|
||||
const callbackUrl = joinPaths(baseUrl, WEB_IDE_OAUTH_CALLBACK_URL_PATH);
|
||||
|
||||
return callbackUrl;
|
||||
}
|
||||
|
||||
const parseCallbackUrl = (urlStr) => {
|
||||
let callbackUrl;
|
||||
|
||||
try {
|
||||
callbackUrl = new URL(urlStr);
|
||||
} catch {
|
||||
// Not a valid URL. Nothing to do here.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// If we're an unexpected callback URL
|
||||
if (!callbackUrl.pathname.endsWith(WEB_IDE_OAUTH_CALLBACK_URL_PATH)) {
|
||||
return {
|
||||
base: joinPaths(callbackUrl.origin, '/'),
|
||||
url: urlStr,
|
||||
};
|
||||
}
|
||||
|
||||
// Else, trim the expected bit to get the origin + relative_url_root
|
||||
const callbackRelativePath = callbackUrl.pathname.substring(
|
||||
0,
|
||||
callbackUrl.pathname.length - WEB_IDE_OAUTH_CALLBACK_URL_PATH.length,
|
||||
);
|
||||
const baseUrl = new URL(callbackUrl);
|
||||
baseUrl.pathname = callbackRelativePath;
|
||||
baseUrl.hash = '';
|
||||
baseUrl.search = '';
|
||||
|
||||
return {
|
||||
base: joinPaths(baseUrl.toString(), '/'),
|
||||
url: urlStr,
|
||||
};
|
||||
};
|
||||
|
||||
export const parseCallbackUrls = (callbackUrlsJson) => {
|
||||
if (!callbackUrlsJson) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let urls;
|
||||
|
||||
try {
|
||||
urls = JSON.parse(callbackUrlsJson);
|
||||
} catch {
|
||||
// why: We dont want to translate console errors
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
logError('Failed to parse callback URLs JSON');
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!urls || !Array.isArray(urls)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return urls.map(parseCallbackUrl).filter(Boolean);
|
||||
};
|
||||
|
|
@ -1,48 +1,43 @@
|
|||
import Vue from 'vue';
|
||||
import OAuthDomainMismatchError from './components/oauth_domain_mismatch_error.vue';
|
||||
import { parseCallbackUrls, getOAuthCallbackUrl } from './lib/gitlab_web_ide/oauth_callback_urls';
|
||||
|
||||
export class OAuthCallbackDomainMismatchErrorApp {
|
||||
#el;
|
||||
#callbackUrlOrigins;
|
||||
#callbackUrls;
|
||||
#expectedCallbackUrl;
|
||||
|
||||
constructor(el, callbackUrls) {
|
||||
constructor(el) {
|
||||
this.#el = el;
|
||||
this.#callbackUrlOrigins =
|
||||
OAuthCallbackDomainMismatchErrorApp.#getCallbackUrlOrigins(callbackUrls);
|
||||
this.#callbackUrls = parseCallbackUrls(el.dataset.callbackUrls);
|
||||
this.#expectedCallbackUrl = getOAuthCallbackUrl();
|
||||
}
|
||||
|
||||
isVisitingFromNonRegisteredOrigin() {
|
||||
return (
|
||||
this.#callbackUrlOrigins.length && !this.#callbackUrlOrigins.includes(window.location.origin)
|
||||
);
|
||||
shouldRenderError() {
|
||||
if (!this.#callbackUrls.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.#callbackUrls.every(({ url }) => url !== this.#expectedCallbackUrl);
|
||||
}
|
||||
|
||||
renderError() {
|
||||
const callbackUrlOrigins = this.#callbackUrlOrigins;
|
||||
const callbackUrls = this.#callbackUrls;
|
||||
const expectedCallbackUrl = this.#expectedCallbackUrl;
|
||||
const el = this.#el;
|
||||
|
||||
if (!el) return null;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
data() {
|
||||
return {
|
||||
callbackUrlOrigins,
|
||||
};
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(OAuthDomainMismatchError, {
|
||||
props: {
|
||||
callbackUrlOrigins,
|
||||
expectedCallbackUrl,
|
||||
callbackUrls,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static #getCallbackUrlOrigins(callbackUrls) {
|
||||
if (!callbackUrls) return [];
|
||||
|
||||
return JSON.parse(callbackUrls).map((url) => new URL(url).origin);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -790,3 +790,14 @@ export function buildURLwithRefType({ base = window.location.origin, path, refTy
|
|||
}
|
||||
return url.pathname + url.search;
|
||||
}
|
||||
|
||||
export function stripRelativeUrlRootFromPath(path) {
|
||||
const relativeUrlRoot = joinPaths(window.gon.relative_url_root, '/');
|
||||
|
||||
// If we have no relative url root or path doesn't start with it, just return the path
|
||||
if (relativeUrlRoot === '/' || !path.startsWith(relativeUrlRoot)) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return joinPaths('/', path.substring(relativeUrlRoot.length));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
<script>
|
||||
import BlobChunks from '~/search/results/components/blob_chunks.vue';
|
||||
import { DEFAULT_SHOW_CHUNKS } from '~/search/results/constants';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
name: 'ZoektBlobResultsChunks',
|
||||
components: {
|
||||
BlobChunks,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showMore: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
projectPathAndFilePath() {
|
||||
return `${this.file.projectPath}:${this.file.path}`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
eventHub.$on('showMore', this.toggleShowMore);
|
||||
},
|
||||
destroyed() {
|
||||
eventHub.$off('showMore', this.toggleShowMore);
|
||||
},
|
||||
methods: {
|
||||
toggleShowMore({ id, state }) {
|
||||
if (id === this.projectPathAndFilePath) {
|
||||
this.showMore = state;
|
||||
}
|
||||
},
|
||||
chunksToShow(file) {
|
||||
if (this.showMore) {
|
||||
return file.chunks;
|
||||
}
|
||||
return file.chunks.slice(0, DEFAULT_SHOW_CHUNKS);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-for="(chunk, index) in chunksToShow(file)"
|
||||
:key="`chunk${index}`"
|
||||
class="chunks-block gl-border-slate-400 gl-border-b last:gl-border-0"
|
||||
>
|
||||
<blob-chunks :chunk="chunk" :blame-link="file.blameUrl" :file-url="file.fileUrl" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<script>
|
||||
import { GlTooltipDirective, GlIcon, GlLink } from '@gitlab/ui';
|
||||
import GlSafeHtmlDirective from '~/vue_shared/directives/safe_html';
|
||||
|
||||
export default {
|
||||
name: 'BlobChunks',
|
||||
components: {
|
||||
GlIcon,
|
||||
GlLink,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
SafeHtml: GlSafeHtmlDirective,
|
||||
},
|
||||
props: {
|
||||
chunk: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
blameLink: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
fileUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
codeTheme() {
|
||||
return gon.user_color_scheme || 'white';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
highlightedRichText(richText) {
|
||||
return richText.replace('<b>', '<b class="hll">');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="search-blob-content" class="file-content code" :class="codeTheme">
|
||||
<div class="blob-content" data-blob-id="" data-path="" data-highlight-line="">
|
||||
<div
|
||||
v-for="line in chunk.lines"
|
||||
:key="line.lineNumber"
|
||||
class="line_holder code-search-line gl-display-flex"
|
||||
data-testid="search-blob-line"
|
||||
>
|
||||
<div class="line-numbers" data-testid="search-blob-line-numbers">
|
||||
<div class="gl-display-flex">
|
||||
<span class="diff-line-num gl-pl-3">
|
||||
<gl-link
|
||||
v-gl-tooltip
|
||||
:href="`${blameLink}#L${line.lineNumber}`"
|
||||
:title="__('View blame')"
|
||||
class="js-navigation-open"
|
||||
><gl-icon name="git"
|
||||
/></gl-link>
|
||||
</span>
|
||||
<span class="diff-line-num flex-grow-1 gl-pr-3">
|
||||
<gl-link
|
||||
v-gl-tooltip
|
||||
:href="`${fileUrl}#L${line.lineNumber}`"
|
||||
:title="__('View Line in repository')"
|
||||
class="gl-display-flex! gl-align-items-center gl-justify-content-end"
|
||||
>{{ line.lineNumber }}</gl-link
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="code highlight flex-grow-1" data-testid="search-blob-line-code">
|
||||
<span v-safe-html="highlightedRichText(line.richText)"></span>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
<script>
|
||||
import { GlSprintf, GlButton, GlLink } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import { DEFAULT_FETCH_CHUNKS, DEFAULT_SHOW_CHUNKS } from '~/search/results/constants';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
name: 'BlobFooter',
|
||||
components: {
|
||||
GlSprintf,
|
||||
GlButton,
|
||||
GlLink,
|
||||
},
|
||||
i18n: {
|
||||
showMore: s__('GlobalSearch|Show %{matches} more matches'),
|
||||
showLess: s__('GlobalSearch|Show less'),
|
||||
showMoreInFile: s__(
|
||||
'GlobalSearch|%{lessButtonStart}Show less%{lessButtonEnd} - Too many matches found. Showing %{showingMatches} chunks out of %{fileMatches} results. %{fileLinkStart}Open the file to view all.%{fileLinkEnd}',
|
||||
),
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showMore: false,
|
||||
filePath: this.file?.path,
|
||||
projectPath: this.file?.projectPath,
|
||||
fileLink: this.file.url,
|
||||
fileMatchCountTotal: this.file.matchCountTotal,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
howMuchMore() {
|
||||
if (this.matchesTotal <= this.showingMatches) {
|
||||
return 0;
|
||||
}
|
||||
return this.matchesTotal - this.showingMatches;
|
||||
},
|
||||
chunksTotal() {
|
||||
return this.file.chunks.length;
|
||||
},
|
||||
showingChunks() {
|
||||
const maxChunksToShow =
|
||||
this.chunksTotal < DEFAULT_FETCH_CHUNKS ? this.chunksTotal : DEFAULT_FETCH_CHUNKS;
|
||||
return this.showMore ? maxChunksToShow : DEFAULT_SHOW_CHUNKS;
|
||||
},
|
||||
showingMatches() {
|
||||
return this.file.chunks
|
||||
.slice(0, this.showingChunks)
|
||||
.reduce((acc, chunk) => acc + chunk.matchCountInChunk, 0);
|
||||
},
|
||||
matchesTotal() {
|
||||
return this.file.chunks.reduce((acc, chunk) => acc + chunk.matchCountInChunk, 0);
|
||||
},
|
||||
hasMoreWeCanShow() {
|
||||
return (
|
||||
this.showingChunks >= DEFAULT_FETCH_CHUNKS && this.fileMatchCountTotal > this.matchesTotal
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleShowMore() {
|
||||
eventHub.$emit('showMore', {
|
||||
id: `${this.projectPath}:${this.filePath}`,
|
||||
state: (this.showMore = !this.showMore),
|
||||
});
|
||||
},
|
||||
},
|
||||
DEFAULT_FETCH_CHUNKS,
|
||||
DEFAULT_SHOW_CHUNKS,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!showMore" data-testid="showing-less">
|
||||
<gl-button category="tertiary" size="medium" @click="toggleShowMore">
|
||||
<gl-sprintf :message="$options.i18n.showMore">
|
||||
<template #matches>
|
||||
<span>{{ howMuchMore }}</span>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-button>
|
||||
</div>
|
||||
<div v-else-if="hasMoreWeCanShow" data-testid="has-more-we-show">
|
||||
<gl-sprintf :message="$options.i18n.showMoreInFile">
|
||||
<template #fileMatches
|
||||
><span>{{ fileMatchCountTotal }}</span></template
|
||||
>
|
||||
<template #showingMatches>{{ showingMatches }}</template>
|
||||
<template #fileLink="{ content }"
|
||||
><gl-link :href="fileLink" target="_blank" data-testid="file-link">{{
|
||||
content
|
||||
}}</gl-link></template
|
||||
>
|
||||
<template #lessButton="{ content }"
|
||||
><gl-button category="tertiary" size="medium" @click="toggleShowMore">{{
|
||||
content
|
||||
}}</gl-button>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
<div v-else data-testid="showing-all">
|
||||
<gl-button category="tertiary" size="medium" @click="toggleShowMore">
|
||||
{{ $options.i18n.showLess }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<script>
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'BlobHeader',
|
||||
components: {
|
||||
FileIcon,
|
||||
ClipboardButton,
|
||||
},
|
||||
props: {
|
||||
filePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
fileUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
fileLink: s__('GlobalSearch|Open file in repository'),
|
||||
},
|
||||
computed: {
|
||||
gfmCopyText() {
|
||||
return `\`${this.filePath}\``;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="file-header-content gl-flex gl-align-items-center gl-leading-1">
|
||||
<file-icon :file-name="filePath" :size="16" aria-hidden="true" css-classes="gl-mr-3" />
|
||||
|
||||
<a :href="fileUrl" :title="$options.i18n.fileLink">
|
||||
<template v-if="projectPath">
|
||||
<strong class="project-path-content" data-testid="project-path-content"
|
||||
>{{ projectPath }}:
|
||||
</strong>
|
||||
</template>
|
||||
|
||||
<strong class="file-name-content" data-testid="file-name-content">{{ filePath }}</strong>
|
||||
</a>
|
||||
<clipboard-button
|
||||
:text="filePath"
|
||||
:gfm="gfmCopyText"
|
||||
:title="__('Copy file path')"
|
||||
category="tertiary"
|
||||
css-class="gl-mr-2"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
<script>
|
||||
import { GlEmptyState, GlSprintf } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import emptySearchSVG from '@gitlab/svgs/dist/illustrations/empty-state/empty-search-md.svg';
|
||||
import { s__ } from '~/locale';
|
||||
import { SCOPE_NAVIGATION_MAP } from '~/search/store/constants';
|
||||
|
||||
export default {
|
||||
name: 'GlobalSearchResultsEmpty',
|
||||
i18n: {
|
||||
title: s__('GlobalSearch|No results found'),
|
||||
descriptionProject: s__(
|
||||
"GlobalSearch|We couldn't find any %{scope} matching %{term} in project %{project}",
|
||||
),
|
||||
descriptionGroup: s__(
|
||||
"GlobalSearch|We couldn't find any %{scope} matching %{term} in group %{group}",
|
||||
),
|
||||
descriptionSimple: s__("GlobalSearch|We couldn't find any %{scope} matching %{term}"),
|
||||
},
|
||||
components: {
|
||||
GlEmptyState,
|
||||
GlSprintf,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['query', 'groupInitialJson', 'projectInitialJson']),
|
||||
...mapGetters(['currentScope']),
|
||||
},
|
||||
emptySearchSVG,
|
||||
SCOPE_NAVIGATION_MAP,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-empty-state
|
||||
:title="$options.i18n.title"
|
||||
:svg-path="$options.emptySearchSVG"
|
||||
description="No results found"
|
||||
>
|
||||
<template #description>
|
||||
<gl-sprintf v-if="query.project_id" :message="$options.i18n.descriptionProject">
|
||||
<template #scope>
|
||||
<strong>{{ $options.SCOPE_NAVIGATION_MAP[currentScope] }}</strong>
|
||||
</template>
|
||||
<template #term>
|
||||
<strong>{{ query.search }}</strong>
|
||||
</template>
|
||||
<template #project>
|
||||
<strong>{{ projectInitialJson.name }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
<gl-sprintf v-else-if="query.group_id" :message="$options.i18n.descriptionGroup">
|
||||
<template #scope>
|
||||
<strong>{{ $options.SCOPE_NAVIGATION_MAP[currentScope] }}</strong>
|
||||
</template>
|
||||
<template #term>
|
||||
<strong>{{ query.search }}</strong>
|
||||
</template>
|
||||
<template #group>
|
||||
<strong>{{ groupInitialJson.name }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
<gl-sprintf v-else :message="$options.i18n.descriptionSimple">
|
||||
<template #scope>
|
||||
<strong>{{ $options.SCOPE_NAVIGATION_MAP[currentScope] }}</strong>
|
||||
</template>
|
||||
<template #term>
|
||||
<strong>{{ query.search }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
</gl-empty-state>
|
||||
</template>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlCard, GlPagination } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState } from 'vuex';
|
||||
import { createAlert } from '~/alert';
|
||||
|
|
@ -12,12 +12,23 @@ import {
|
|||
PROJECT_GRAPHQL_ID_TYPE,
|
||||
GROUP_GRAPHQL_ID_TYPE,
|
||||
SEARCH_RESULTS_DEBOUNCE,
|
||||
DEFAULT_SHOW_CHUNKS,
|
||||
} from '~/search/results/constants';
|
||||
import BlobHeader from '~/search/results/components/blob_header.vue';
|
||||
import BlobFooter from '~/search/results/components/blob_footer.vue';
|
||||
import BlobBody from '~/search/results/components/blob_body.vue';
|
||||
import EmptyResult from '~/search/results/components/result_empty.vue';
|
||||
|
||||
export default {
|
||||
name: 'ZoektBlobResults',
|
||||
components: {
|
||||
GlLoadingIcon,
|
||||
GlCard,
|
||||
BlobHeader,
|
||||
BlobFooter,
|
||||
BlobBody,
|
||||
GlPagination,
|
||||
EmptyResult,
|
||||
},
|
||||
i18n: {
|
||||
headerText: __('Search results'),
|
||||
|
|
@ -69,6 +80,25 @@ export default {
|
|||
isLoading() {
|
||||
return this.$apollo.queries.blobSearch.loading;
|
||||
},
|
||||
hasResults() {
|
||||
return this.blobSearch?.files?.length > 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hasMore(file) {
|
||||
const showingMatches = file.chunks
|
||||
.slice(0, DEFAULT_SHOW_CHUNKS)
|
||||
.reduce((acc, chunk) => acc + chunk.matchCountInChunk, 0);
|
||||
const matchesTotal = file.chunks.reduce((acc, chunk) => acc + chunk.matchCountInChunk, 0);
|
||||
|
||||
return file.matchCount !== 0 && matchesTotal > showingMatches;
|
||||
},
|
||||
hasCode(file) {
|
||||
return file?.chunks.length > 0;
|
||||
},
|
||||
projectPathAndFilePath({ projectPath = '', path = '' }) {
|
||||
return `${projectPath}:${path}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -76,5 +106,41 @@ export default {
|
|||
<template>
|
||||
<div class="gl-flex gl-justify-center gl-flex-col">
|
||||
<gl-loading-icon v-if="isLoading" size="sm" />
|
||||
<div v-if="hasResults && !isLoading && !hasError" class="gl-relative">
|
||||
<gl-card
|
||||
v-for="file in blobSearch.files"
|
||||
:key="projectPathAndFilePath(file)"
|
||||
class="file-result-holder gl-my-5 file-holder"
|
||||
:header-class="{
|
||||
'gl-border-b-0!': !hasCode(file),
|
||||
'gl-new-card-header file-title': true,
|
||||
}"
|
||||
footer-class="gl-new-card-footer"
|
||||
body-class="gl-p-0"
|
||||
>
|
||||
<template #header>
|
||||
<blob-header
|
||||
:file-path="file.path"
|
||||
:project-path="file.projectPath"
|
||||
:file-url="file.fileUrl"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<blob-body v-if="hasCode(file)" :file="file" />
|
||||
|
||||
<template v-if="hasMore(file)" #footer>
|
||||
<blob-footer :file="file" />
|
||||
</template>
|
||||
</gl-card>
|
||||
</div>
|
||||
<empty-result v-else-if="!hasResults && !isLoading" />
|
||||
<template v-if="hasResults && !isLoading && !hasError">
|
||||
<gl-pagination
|
||||
v-model="query.page"
|
||||
class="gl-mx-auto"
|
||||
:per-page="blobSearch.perPage"
|
||||
:total-items="blobSearch.fileCount"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ export const DEFAULT_FETCH_CHUNKS = 50;
|
|||
export const PROJECT_GRAPHQL_ID_TYPE = 'Project';
|
||||
export const GROUP_GRAPHQL_ID_TYPE = 'Group';
|
||||
export const SEARCH_RESULTS_DEBOUNCE = 500;
|
||||
export const DEFAULT_SHOW_CHUNKS = 3;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
export default createEventHub();
|
||||
|
|
@ -4,6 +4,7 @@ import { languageFilterData } from '~/search/sidebar/components/language_filter/
|
|||
import { LABEL_FILTER_PARAM } from '~/search/sidebar/components/label_filter/data';
|
||||
import { archivedFilterData } from '~/search/sidebar/components/archived_filter/data';
|
||||
import { INCLUDE_FORKED_FILTER_PARAM } from '~/search/sidebar/components/forks_filter/index.vue';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export const MAX_FREQUENT_ITEMS = 5;
|
||||
|
||||
|
|
@ -40,6 +41,20 @@ export const ICON_MAP = {
|
|||
snippet_titles: 'snippet',
|
||||
};
|
||||
|
||||
export const SCOPE_NAVIGATION_MAP = {
|
||||
blobs: s__(`GlobalSearch|Code`),
|
||||
issues: s__(`GlobalSearch|Issues`),
|
||||
epics: s__(`GlobalSearch|'Epics`),
|
||||
merge_requests: s__(`GlobalSearch|Merge request`),
|
||||
commits: s__(`GlobalSearch|Commits`),
|
||||
notes: s__(`GlobalSearch|Comments`),
|
||||
milestones: s__(`GlobalSearch|Milestones`),
|
||||
users: s__(`GlobalSearch|Users`),
|
||||
projects: s__(`GlobalSearch|Projects`),
|
||||
wiki_blobs: s__(`GlobalSearch|Wiki`),
|
||||
snippet_titles: s__(`GlobalSearch|Snippets`),
|
||||
};
|
||||
|
||||
export const ZOEKT_SEARCH_TYPE = 'zoekt';
|
||||
export const ADVANCED_SEARCH_TYPE = 'advanced';
|
||||
export const BASIC_SEARCH_TYPE = 'basic';
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@ export const FAILURE_REASONS = {
|
|||
requested_changes: __('The change requests must be completed or resolved.'),
|
||||
approvals_syncing: __('The merge request approvals are currently syncing.'),
|
||||
locked_lfs_files: __('All LFS files must be unlocked.'),
|
||||
security_policy_evaluation: __('All security policies must be evaluated.'),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
<script>
|
||||
import { GlForm, GlFormGroup, GlFormRadioGroup, GlModal } from '@gitlab/ui';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { s__, __ } from '~/locale';
|
||||
import csrf from '~/lib/utils/csrf';
|
||||
import { CATEGORY_OPTIONS } from '~/abuse_reports/components/constants';
|
||||
|
||||
export default {
|
||||
name: 'WorkItemAbuseModal',
|
||||
csrf,
|
||||
i18n: {
|
||||
title: __('Report abuse to administrator'),
|
||||
label: s__('ReportAbuse|Why are you reporting this user?'),
|
||||
},
|
||||
modal: {
|
||||
id: uniqueId('work-item-abuse-modal-'),
|
||||
actionPrimary: {
|
||||
text: __('Next'),
|
||||
attributes: {
|
||||
variant: 'confirm',
|
||||
},
|
||||
},
|
||||
actionSecondary: {
|
||||
text: __('Cancel'),
|
||||
attributes: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
},
|
||||
CATEGORY_OPTIONS,
|
||||
components: {
|
||||
GlForm,
|
||||
GlFormGroup,
|
||||
GlFormRadioGroup,
|
||||
GlModal,
|
||||
},
|
||||
inject: {
|
||||
reportAbusePath: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
props: {
|
||||
reportedUserId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
reportedFromUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
showModal: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedOption: CATEGORY_OPTIONS[0].value,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
closeModal() {
|
||||
this.$emit('close-modal');
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs.form.$el.submit();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-modal
|
||||
size="sm"
|
||||
:visible="showModal"
|
||||
:modal-id="$options.modal.id"
|
||||
:title="$options.i18n.title"
|
||||
:action-primary="$options.modal.actionPrimary"
|
||||
:action-secondary="$options.modal.actionSecondary"
|
||||
@primary="submitForm"
|
||||
@hide="closeModal"
|
||||
>
|
||||
<gl-form ref="form" :action="reportAbusePath" method="post">
|
||||
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
|
||||
|
||||
<input type="hidden" name="user_id" :value="reportedUserId" data-testid="input-user-id" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="abuse_report[reported_from_url]"
|
||||
:value="reportedFromUrl"
|
||||
data-testid="input-referer"
|
||||
/>
|
||||
|
||||
<gl-form-group :label="$options.i18n.label">
|
||||
<gl-form-radio-group
|
||||
v-model="selectedOption"
|
||||
:options="$options.CATEGORY_OPTIONS"
|
||||
name="abuse_report[category]"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</gl-form>
|
||||
</gl-modal>
|
||||
</template>
|
||||
|
|
@ -37,6 +37,7 @@ import {
|
|||
I18N_WORK_ITEM_ERROR_COPY_REFERENCE,
|
||||
I18N_WORK_ITEM_ERROR_COPY_EMAIL,
|
||||
TEST_ID_LOCK_ACTION,
|
||||
TEST_ID_REPORT_ABUSE,
|
||||
} from '../constants';
|
||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||
import updateWorkItemNotificationsMutation from '../graphql/update_work_item_notifications.mutation.graphql';
|
||||
|
|
@ -55,6 +56,7 @@ export default {
|
|||
referenceCopied: __('Reference copied'),
|
||||
emailAddressCopied: __('Email address copied'),
|
||||
moreActions: __('More actions'),
|
||||
reportAbuse: __('Report abuse'),
|
||||
},
|
||||
components: {
|
||||
GlDisclosureDropdown,
|
||||
|
|
@ -79,6 +81,7 @@ export default {
|
|||
promoteActionTestId: TEST_ID_PROMOTE_ACTION,
|
||||
lockDiscussionTestId: TEST_ID_LOCK_ACTION,
|
||||
stateToggleTestId: TEST_ID_TOGGLE_ACTION,
|
||||
reportAbuseActionTestId: TEST_ID_REPORT_ABUSE,
|
||||
props: {
|
||||
fullPath: {
|
||||
type: String,
|
||||
|
|
@ -164,6 +167,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
workItemAuthorId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -228,6 +236,9 @@ export default {
|
|||
showDropdownTooltip() {
|
||||
return !this.isDropdownVisible ? this.$options.i18n.moreActions : '';
|
||||
},
|
||||
isAuthor() {
|
||||
return this.workItemAuthorId === window.gon.current_user_id;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(text, message) {
|
||||
|
|
@ -356,6 +367,10 @@ export default {
|
|||
emitStateToggleError(error) {
|
||||
this.$emit('error', error);
|
||||
},
|
||||
handleToggleReportAbuseModal() {
|
||||
this.$emit('toggleReportAbuseModal', true);
|
||||
this.closeDropdown();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -452,8 +467,16 @@ export default {
|
|||
<template #list-item>{{ i18n.copyCreateNoteEmail }}</template>
|
||||
</gl-disclosure-dropdown-item>
|
||||
|
||||
<gl-dropdown-divider />
|
||||
<gl-disclosure-dropdown-item
|
||||
v-if="!isAuthor"
|
||||
:data-testid="$options.reportAbuseActionTestId"
|
||||
@action="handleToggleReportAbuseModal"
|
||||
>
|
||||
<template #list-item>{{ $options.i18n.reportAbuse }}</template>
|
||||
</gl-disclosure-dropdown-item>
|
||||
|
||||
<template v-if="canDelete">
|
||||
<gl-dropdown-divider />
|
||||
<gl-disclosure-dropdown-item
|
||||
:data-testid="$options.deleteActionTestId"
|
||||
variant="danger"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { getParameterByName, updateHistory, setUrlParams } from '~/lib/utils/url
|
|||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { isLoggedIn } from '~/lib/utils/common_utils';
|
||||
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
|
||||
import { WORKSPACE_PROJECT } from '~/issues/constants';
|
||||
import {
|
||||
i18n,
|
||||
|
|
@ -50,6 +49,7 @@ import WorkItemStickyHeader from './work_item_sticky_header.vue';
|
|||
import WorkItemAncestors from './work_item_ancestors/work_item_ancestors.vue';
|
||||
import WorkItemTitle from './work_item_title.vue';
|
||||
import WorkItemLoading from './work_item_loading.vue';
|
||||
import WorkItemAbuseModal from './work_item_abuse_modal.vue';
|
||||
import DesignWidget from './design_management/design_management_widget.vue';
|
||||
|
||||
export default {
|
||||
|
|
@ -74,12 +74,12 @@ export default {
|
|||
WorkItemTree,
|
||||
WorkItemNotes,
|
||||
WorkItemDetailModal,
|
||||
AbuseCategorySelector,
|
||||
WorkItemRelationships,
|
||||
WorkItemStickyHeader,
|
||||
WorkItemAncestors,
|
||||
WorkItemTitle,
|
||||
WorkItemLoading,
|
||||
WorkItemAbuseModal,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: ['fullPath', 'reportAbusePath', 'groupPath', 'hasSubepicsFeature'],
|
||||
|
|
@ -114,7 +114,7 @@ export default {
|
|||
modalWorkItemId: undefined,
|
||||
modalWorkItemIid: getParameterByName('work_item_iid'),
|
||||
modalWorkItemNamespaceFullPath: '',
|
||||
isReportDrawerOpen: false,
|
||||
isReportModalOpen: false,
|
||||
reportedUrl: '',
|
||||
reportedUserId: 0,
|
||||
isStickyHeaderShowing: false,
|
||||
|
|
@ -201,6 +201,9 @@ export default {
|
|||
workItemTypeId() {
|
||||
return this.workItem.workItemType?.id;
|
||||
},
|
||||
workItemAuthorId() {
|
||||
return getIdFromGraphQLId(this.workItem.author?.id);
|
||||
},
|
||||
canUpdate() {
|
||||
return this.workItem.userPermissions?.updateWorkItem;
|
||||
},
|
||||
|
|
@ -422,17 +425,17 @@ export default {
|
|||
);
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
openReportAbuseDrawer(reply) {
|
||||
openReportAbuseModal(reply) {
|
||||
if (this.isModal) {
|
||||
this.$emit('openReportAbuse', reply);
|
||||
} else {
|
||||
this.toggleReportAbuseDrawer(true, reply);
|
||||
this.toggleReportAbuseModal(true, reply);
|
||||
}
|
||||
},
|
||||
toggleReportAbuseDrawer(isOpen, reply = {}) {
|
||||
this.isReportDrawerOpen = isOpen;
|
||||
this.reportedUrl = reply.url || {};
|
||||
this.reportedUserId = reply.author ? getIdFromGraphQLId(reply.author.id) : 0;
|
||||
toggleReportAbuseModal(isOpen, workItem = this.workItem) {
|
||||
this.isReportModalOpen = isOpen;
|
||||
this.reportedUrl = workItem.webUrl || workItem.url || {};
|
||||
this.reportedUserId = workItem.author ? getIdFromGraphQLId(workItem.author.id) : 0;
|
||||
},
|
||||
hideStickyHeader() {
|
||||
this.isStickyHeaderShowing = false;
|
||||
|
|
@ -499,6 +502,7 @@ export default {
|
|||
:work-item="workItem"
|
||||
:is-sticky-header-showing="isStickyHeaderShowing"
|
||||
:work-item-notifications-subscribed="workItemNotificationsSubscribed"
|
||||
:work-item-author-id="workItemAuthorId"
|
||||
@hideStickyHeader="hideStickyHeader"
|
||||
@showStickyHeader="showStickyHeader"
|
||||
@deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
|
||||
|
|
@ -507,6 +511,7 @@ export default {
|
|||
@promotedToObjective="$emit('promotedToObjective', workItemIid)"
|
||||
@toggleEditMode="enableEditMode"
|
||||
@workItemStateUpdated="$emit('workItemStateUpdated')"
|
||||
@toggleReportAbuseModal="toggleReportAbuseModal"
|
||||
/>
|
||||
<section class="work-item-view">
|
||||
<section v-if="updateError" class="flash-container flash-container-page sticky">
|
||||
|
|
@ -592,11 +597,13 @@ export default {
|
|||
:is-modal="isModal"
|
||||
:work-item-state="workItem.state"
|
||||
:has-children="hasChildren"
|
||||
:work-item-author-id="workItemAuthorId"
|
||||
@deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
|
||||
@toggleWorkItemConfidentiality="toggleConfidentiality"
|
||||
@error="updateError = $event"
|
||||
@promotedToObjective="$emit('promotedToObjective', workItemIid)"
|
||||
@workItemStateUpdated="$emit('workItemStateUpdated')"
|
||||
@toggleReportAbuseModal="toggleReportAbuseModal"
|
||||
/>
|
||||
</div>
|
||||
<gl-button
|
||||
|
|
@ -714,7 +721,7 @@ export default {
|
|||
:use-h2="!isModal"
|
||||
@error="updateError = $event"
|
||||
@has-notes="updateHasNotes"
|
||||
@openReportAbuse="openReportAbuseDrawer"
|
||||
@openReportAbuse="openReportAbuseModal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -728,14 +735,14 @@ export default {
|
|||
:work-item-full-path="modalWorkItemNamespaceFullPath"
|
||||
:show="true"
|
||||
@close="updateUrl"
|
||||
@openReportAbuse="toggleReportAbuseDrawer(true, $event)"
|
||||
@openReportAbuse="toggleReportAbuseModal(true, $event)"
|
||||
/>
|
||||
<abuse-category-selector
|
||||
v-if="isReportDrawerOpen"
|
||||
<work-item-abuse-modal
|
||||
v-if="isReportModalOpen"
|
||||
:show-modal="isReportModalOpen"
|
||||
:reported-user-id="reportedUserId"
|
||||
:reported-from-url="reportedUrl"
|
||||
:show-drawer="true"
|
||||
@close-drawer="toggleReportAbuseDrawer(false)"
|
||||
@close-modal="toggleReportAbuseModal(false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -97,8 +97,7 @@ export default {
|
|||
updateHasNotes() {
|
||||
this.hasNotes = true;
|
||||
},
|
||||
openReportAbuseDrawer(reply) {
|
||||
this.hide();
|
||||
openReportAbuseModal(reply) {
|
||||
this.$emit('openReportAbuse', reply);
|
||||
},
|
||||
},
|
||||
|
|
@ -132,7 +131,7 @@ export default {
|
|||
@deleteWorkItem="deleteWorkItem"
|
||||
@update-modal="updateModal"
|
||||
@has-notes="updateHasNotes"
|
||||
@openReportAbuse="openReportAbuseDrawer"
|
||||
@openReportAbuse="openReportAbuseModal"
|
||||
/>
|
||||
</gl-modal>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
|
|||
import { TYPENAME_ISSUE, TYPENAME_WORK_ITEM } from '~/graphql_shared/constants';
|
||||
import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
|
||||
import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
|
||||
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
|
||||
|
||||
import {
|
||||
FORM_TYPES,
|
||||
|
|
@ -30,6 +29,7 @@ import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql';
|
|||
import WorkItemChildrenLoadMore from '../shared/work_item_children_load_more.vue';
|
||||
import WidgetWrapper from '../widget_wrapper.vue';
|
||||
import WorkItemDetailModal from '../work_item_detail_modal.vue';
|
||||
import WorkItemAbuseModal from '../work_item_abuse_modal.vue';
|
||||
import WorkItemLinksForm from './work_item_links_form.vue';
|
||||
import WorkItemChildrenWrapper from './work_item_children_wrapper.vue';
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ export default {
|
|||
WidgetWrapper,
|
||||
WorkItemLinksForm,
|
||||
WorkItemDetailModal,
|
||||
AbuseCategorySelector,
|
||||
WorkItemAbuseModal,
|
||||
WorkItemChildrenWrapper,
|
||||
WorkItemChildrenLoadMore,
|
||||
GlToggle,
|
||||
|
|
@ -109,7 +109,7 @@ export default {
|
|||
parentIssue: null,
|
||||
formType: null,
|
||||
workItem: null,
|
||||
isReportDrawerOpen: false,
|
||||
isReportModalOpen: false,
|
||||
reportedUserId: 0,
|
||||
reportedUrl: '',
|
||||
widgetName: TASKS_ANCHOR,
|
||||
|
|
@ -206,13 +206,13 @@ export default {
|
|||
updateWorkItemIdUrlQuery({ iid } = {}) {
|
||||
updateHistory({ url: setUrlParams({ work_item_iid: iid }), replace: true });
|
||||
},
|
||||
toggleReportAbuseDrawer(isOpen, reply = {}) {
|
||||
this.isReportDrawerOpen = isOpen;
|
||||
toggleReportAbuseModal(isOpen, reply = {}) {
|
||||
this.isReportModalOpen = isOpen;
|
||||
this.reportedUrl = reply.url;
|
||||
this.reportedUserId = reply.author ? getIdFromGraphQLId(reply.author.id) : 0;
|
||||
},
|
||||
openReportAbuseDrawer(reply) {
|
||||
this.toggleReportAbuseDrawer(true, reply);
|
||||
openReportAbuseModal(reply) {
|
||||
this.toggleReportAbuseModal(true, reply);
|
||||
},
|
||||
async fetchNextPage() {
|
||||
if (this.hasNextPage && !this.fetchNextPageInProgress) {
|
||||
|
|
@ -355,14 +355,14 @@ export default {
|
|||
:work-item-full-path="activeChildNamespaceFullPath"
|
||||
@close="closeModal"
|
||||
@workItemDeleted="handleWorkItemDeleted(activeChild)"
|
||||
@openReportAbuse="openReportAbuseDrawer"
|
||||
@openReportAbuse="openReportAbuseModal"
|
||||
/>
|
||||
<abuse-category-selector
|
||||
v-if="isReportDrawerOpen && reportAbusePath"
|
||||
<work-item-abuse-modal
|
||||
v-if="isReportModalOpen && reportAbusePath"
|
||||
:show-modal="isReportModalOpen"
|
||||
:reported-user-id="reportedUserId"
|
||||
:reported-from-url="reportedUrl"
|
||||
:show-drawer="isReportDrawerOpen"
|
||||
@close-drawer="toggleReportAbuseDrawer(false)"
|
||||
@close-modal="toggleReportAbuseModal(false)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -62,6 +62,11 @@ export default {
|
|||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
workItemAuthorId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
canUpdate() {
|
||||
|
|
@ -153,6 +158,7 @@ export default {
|
|||
:work-item-create-note-email="workItem.createNoteEmail"
|
||||
:work-item-state="workItem.state"
|
||||
:is-modal="isModal"
|
||||
:work-item-author-id="workItemAuthorId"
|
||||
@deleteWorkItem="$emit('deleteWorkItem')"
|
||||
@toggleWorkItemConfidentiality="
|
||||
$emit('toggleWorkItemConfidentiality', !workItem.confidential)
|
||||
|
|
@ -160,6 +166,7 @@ export default {
|
|||
@error="$emit('error')"
|
||||
@promotedToObjective="$emit('promotedToObjective')"
|
||||
@workItemStateUpdated="$emit('workItemStateUpdated')"
|
||||
@toggleReportAbuseModal="$emit('toggleReportAbuseModal', true)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -277,6 +277,7 @@ export const TEST_ID_LOCK_ACTION = 'lock-action';
|
|||
export const TEST_ID_COPY_REFERENCE_ACTION = 'copy-reference-action';
|
||||
export const TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION = 'copy-create-note-email-action';
|
||||
export const TEST_ID_TOGGLE_ACTION = 'state-toggle-action';
|
||||
export const TEST_ID_REPORT_ABUSE = 'report-abuse-action';
|
||||
|
||||
export const TODO_ADD_ICON = 'todo-add';
|
||||
export const TODO_DONE_ICON = 'todo-done';
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ module Types
|
|||
value 'LOCKED_LFS_FILES',
|
||||
value: :locked_lfs_files,
|
||||
description: 'Merge request includes locked LFS files.'
|
||||
value 'SECURITY_POLICIES_EVALUATING',
|
||||
value: :security_policy_evaluation,
|
||||
description: 'All security policies must be evaluated.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1274,7 +1274,8 @@ class MergeRequest < ApplicationRecord
|
|||
skip_external_status_check: merge_when_checks_pass_strat,
|
||||
skip_requested_changes_check: merge_when_checks_pass_strat,
|
||||
skip_jira_check: merge_when_checks_pass_strat,
|
||||
skip_locked_lfs_files_check: merge_when_checks_pass_strat
|
||||
skip_locked_lfs_files_check: merge_when_checks_pass_strat,
|
||||
skip_security_policy_check: merge_when_checks_pass_strat
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ ActiveSupport::Inflector.inflections do |inflect|
|
|||
dependency_proxy_blob_registry
|
||||
design_management_repository_registry
|
||||
dependency_proxy_manifest_registry
|
||||
duo_enterprise
|
||||
duo_pro
|
||||
event_log
|
||||
file_registry
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ InitializerConnections.raise_if_new_database_connection do
|
|||
scope :ide, as: :ide, format: false do
|
||||
get '/', to: 'ide#index'
|
||||
get '/project', to: 'ide#index'
|
||||
# note: This path has a hardcoded reference in the FE `app/assets/javascripts/ide/constants.js`
|
||||
get '/oauth_redirect', to: 'ide#oauth_redirect'
|
||||
|
||||
scope path: 'project/:project_id', as: :project, constraints: { project_id: Gitlab::PathRegex.full_namespace_route_regex } do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../../tooling/danger/settings_sections'
|
||||
|
||||
module Danger
|
||||
class SettingsSections < ::Danger::Plugin
|
||||
include Tooling::Danger::SettingsSections
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
settings_sections.check!
|
||||
|
|
@ -8,4 +8,4 @@ description: CI/CD variables available to all projects and groups in an instance
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30156
|
||||
milestone: '13.0'
|
||||
gitlab_schema: gitlab_ci
|
||||
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/459039
|
||||
exempt_from_sharding: true # table not used in .com nor Cells.
|
||||
|
|
@ -35344,6 +35344,7 @@ Detailed representation of whether a GitLab merge request can be merged.
|
|||
| <a id="detailedmergestatusnot_open"></a>`NOT_OPEN` | Merge request must be open before merging. |
|
||||
| <a id="detailedmergestatuspreparing"></a>`PREPARING` | Merge request diff is being created. |
|
||||
| <a id="detailedmergestatusrequested_changes"></a>`REQUESTED_CHANGES` | Indicates a reviewer has requested changes. |
|
||||
| <a id="detailedmergestatussecurity_policies_evaluating"></a>`SECURITY_POLICIES_EVALUATING` | All security policies must be evaluated. |
|
||||
| <a id="detailedmergestatusunchecked"></a>`UNCHECKED` | Merge status has not been checked. |
|
||||
|
||||
### `DiffPositionType`
|
||||
|
|
@ -36119,6 +36120,7 @@ Representation of mergeability check identifier.
|
|||
| <a id="mergeabilitycheckidentifiernot_approved"></a>`NOT_APPROVED` | Checks whether the merge request is approved. |
|
||||
| <a id="mergeabilitycheckidentifiernot_open"></a>`NOT_OPEN` | Checks whether the merge request is open. |
|
||||
| <a id="mergeabilitycheckidentifierrequested_changes"></a>`REQUESTED_CHANGES` | Checks whether the merge request has changes requested. |
|
||||
| <a id="mergeabilitycheckidentifiersecurity_policy_evaluation"></a>`SECURITY_POLICY_EVALUATION` | Checks whether the security policies are evaluated. |
|
||||
| <a id="mergeabilitycheckidentifierstatus_checks_must_pass"></a>`STATUS_CHECKS_MUST_PASS` | Checks whether the external status checks pass. |
|
||||
|
||||
### `MergeabilityCheckStatus`
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ group: unassigned
|
|||
description: 'Guidelines for deprecations and page removals'
|
||||
---
|
||||
|
||||
## Deprecations and removals
|
||||
# Deprecations and removals
|
||||
|
||||
When GitLab deprecates or removes a feature, use the following process to update the documentation.
|
||||
This process requires temporarily changing content to be "deprecated" or "removed" before it's deleted.
|
||||
|
|
@ -16,7 +16,7 @@ NOTE:
|
|||
A separate process exists for [GraphQL docs](../../api_graphql_styleguide.md#deprecating-schema-items)
|
||||
and [REST API docs](../restful_api_styleguide.md#deprecations).
|
||||
|
||||
### Deprecate a page or topic
|
||||
## Deprecate a page or topic
|
||||
|
||||
To deprecate a page or topic:
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ To deprecate a page or topic:
|
|||
|
||||
1. Open a merge request to add the word `(deprecated)` to the left nav, after the page title.
|
||||
|
||||
### Remove a page
|
||||
## Remove a page
|
||||
|
||||
Mark content as removed during the release the feature was removed.
|
||||
The title and a removed indicator remains until three months after the removal.
|
||||
|
|
@ -107,7 +107,7 @@ To remove a page:
|
|||
This content is removed from the documentation as part of the Technical Writing team's
|
||||
[regularly scheduled tasks](https://handbook.gitlab.com/handbook/product/ux/technical-writing/#regularly-scheduled-tasks).
|
||||
|
||||
### Remove a topic
|
||||
## Remove a topic
|
||||
|
||||
To remove a topic:
|
||||
|
||||
|
|
@ -136,7 +136,7 @@ To remove a topic:
|
|||
This content is removed from the documentation as part of the Technical Writing team's
|
||||
[regularly scheduled tasks](https://handbook.gitlab.com/handbook/product/ux/technical-writing/#regularly-scheduled-tasks).
|
||||
|
||||
### Removing version-specific upgrade pages
|
||||
## Removing version-specific upgrade pages
|
||||
|
||||
Version-specific upgrade pages are in the `doc/update/versions/` directory.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ group: Project Management
|
|||
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
|
||||
---
|
||||
|
||||
## Work items development
|
||||
# Work items development
|
||||
|
||||
- Work item lists are only available at group level `http://gdk.test:3000/groups/flightjs/-/work_items`,
|
||||
they are enabled with feature flags: `namespace_level_work_items` and `work_item_epics_rollout`.
|
||||
|
|
|
|||
|
|
@ -95,12 +95,23 @@ Prerequisites:
|
|||
|
||||
- You must be an administrator.
|
||||
|
||||
In GitLab 15.7 and later, you can [use the application settings API to disable personal access tokens](../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls).
|
||||
Depending on your GitLab version, you can use either the application settings API
|
||||
or the Admin UI to disable personal access tokens.
|
||||
|
||||
### Use the application settings API
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/384201) in GitLab 15.7.
|
||||
|
||||
In GitLab 15.7 and later, you can use the [`disable_personal_access_tokens` attribute in the application settings API](../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls) to disable personal access tokens.
|
||||
|
||||
NOTE:
|
||||
After you have used the API to disable personal access tokens, those tokens cannot be used in subsequent API calls to manage this setting. To re-enable personal access tokens, you must use the [GitLab Rails console](../../administration/operations/rails_console.md). You can also upgrade to GitLab 17.3 or later so you can use the Admin UI instead.
|
||||
|
||||
In GitLab 17.3 and later, you can disable personal access tokens in the Admin UI:
|
||||
### Use the Admin UI
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/436991) in GitLab 17.3.
|
||||
|
||||
In GitLab 17.3 and later, you can use the Admin UI to disable personal access tokens:
|
||||
|
||||
1. On the left sidebar, at the bottom, select **Admin**.
|
||||
1. Select **Settings > General**.
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ When you're ready to start coding:
|
|||
1. Open relevant files, including configuration files, to provide better context.
|
||||
1. Close any files you don't want to be used as context.
|
||||
|
||||
## View Multiple Code Suggestions
|
||||
## View multiple code suggestions
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/issues/1325) in GitLab 17.1.
|
||||
|
||||
|
|
@ -130,10 +130,14 @@ might be available. To view all available suggestions:
|
|||
|
||||
1. Hover over the code completion suggestion.
|
||||
1. Scroll through the alternatives. Either:
|
||||
- Use keyboard shortcuts. Press <kbd>Option</kbd> + <kbd>`]`</kbd> to view the
|
||||
next suggestion, and <kbd>Option</kbd> + <kbd>`[`</kbd> to view the previous
|
||||
suggestions.
|
||||
- Select the right or left arrow to see next or previous options.
|
||||
- Use keyboard shortcuts:
|
||||
- On a Mac, press <kbd>Option</kbd> + <kbd>]</kbd> to view the
|
||||
next suggestion, and <kbd>Option</kbd> + <kbd>[</kbd> to view the previous
|
||||
suggestions.
|
||||
- On Windows, press <kbd>Alt</kbd> + <kbd>]</kbd> to view the
|
||||
next suggestion, and <kbd>Alt</kbd> + <kbd>[</kbd> to view the previous
|
||||
suggestions.
|
||||
- On the dialog that's displayed, select the right or left arrow to see next or previous options.
|
||||
1. Press <kbd>Tab</kbd> to apply the suggestion you prefer.
|
||||
|
||||
## Add additional languages for Code Suggestions
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Search
|
||||
# Generates a list of all available setting sections of a group.
|
||||
# This list is used by the command palette's search functionality.
|
||||
class GroupSettings
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Search
|
||||
# Generates a list of all available setting sections of a project.
|
||||
# This list is used by the command palette's search functionality.
|
||||
class ProjectSettings
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
|
|
|
|||
|
|
@ -1716,9 +1716,6 @@ msgstr ""
|
|||
msgid "- Push code to the repository."
|
||||
msgstr ""
|
||||
|
||||
msgid "- Select -"
|
||||
msgstr ""
|
||||
|
||||
msgid "- User"
|
||||
msgid_plural "- Users"
|
||||
msgstr[0] ""
|
||||
|
|
@ -5449,6 +5446,9 @@ msgstr ""
|
|||
msgid "All required approvals must be given."
|
||||
msgstr ""
|
||||
|
||||
msgid "All security policies must be evaluated."
|
||||
msgstr ""
|
||||
|
||||
msgid "All threads resolved!"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -13375,7 +13375,7 @@ msgstr ""
|
|||
msgid "Company"
|
||||
msgstr ""
|
||||
|
||||
msgid "Company Name"
|
||||
msgid "Company name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Compare"
|
||||
|
|
@ -15525,7 +15525,7 @@ msgstr ""
|
|||
msgid "Couldn't reorder child due to an internal error."
|
||||
msgstr ""
|
||||
|
||||
msgid "Country / Region"
|
||||
msgid "Country or region"
|
||||
msgstr ""
|
||||
|
||||
msgid "Counts"
|
||||
|
|
@ -19665,6 +19665,18 @@ msgstr ""
|
|||
msgid "DuoCodeReview|I have encountered some issues while I was reviewing. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
msgid "DuoEnterpriseTrial|Start your free Duo Enterprise trial"
|
||||
msgstr ""
|
||||
|
||||
msgid "DuoEnterpriseTrial|Start your free GitLab Duo Enterprise trial"
|
||||
msgstr ""
|
||||
|
||||
msgid "DuoEnterpriseTrial|Start your free GitLab Duo Enterprise trial on %{group_name}"
|
||||
msgstr ""
|
||||
|
||||
msgid "DuoEnterpriseTrial|We just need some additional information to activate your trial."
|
||||
msgstr ""
|
||||
|
||||
msgid "DuoProDiscover|Accelerate your path to market"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -22826,9 +22838,6 @@ msgstr ""
|
|||
msgid "Finished"
|
||||
msgstr ""
|
||||
|
||||
msgid "First Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "First Seen"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -24392,12 +24401,18 @@ msgstr ""
|
|||
msgid "GlobalSearch|%{count} default results provided. Use the up and down arrow keys to navigate search results list."
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|%{lessButtonStart}Show less%{lessButtonEnd} - Too many matches found. Showing %{showingMatches} chunks out of %{fileMatches} results. %{fileLinkStart}Open the file to view all.%{fileLinkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|%{linkStart}Exact code search (powered by Zoekt)%{linkEnd} is disabled since %{ref_elem} is not the default branch. %{docs_link}"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|%{linkStart}Exact code search (powered by Zoekt)%{linkEnd} is enabled."
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|'Epics"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Aggregations load error."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -24410,12 +24425,21 @@ msgstr ""
|
|||
msgid "GlobalSearch|Change context %{kbdStart}↵%{kbdEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Code"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Command palette"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Commands %{superKey} %{link2Start}k%{link2End}"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Comments"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Commits"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Could not load search results. Please refresh the page to try again."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -24500,6 +24524,9 @@ msgstr ""
|
|||
msgid "GlobalSearch|Merge Requests"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Merge request"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Merge requests I've created"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -24509,9 +24536,15 @@ msgstr ""
|
|||
msgid "GlobalSearch|Merge requests that I'm a reviewer"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Milestones"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|No labels found"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|No results found"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|No results found. Edit your search and try again."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -24521,6 +24554,9 @@ msgstr ""
|
|||
msgid "GlobalSearch|Only first %{max_shown} of not indexed projects is shown"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Open file in repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Pages or actions"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -24587,12 +24623,21 @@ msgstr ""
|
|||
msgid "GlobalSearch|Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Show %{matches} more matches"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Show less"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Show more"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Showing top %{maxItems}"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Snippets"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|The search term must be at least 3 characters long."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -24629,9 +24674,21 @@ msgstr ""
|
|||
msgid "GlobalSearch|View syntax options."
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|We couldn't find any %{scope} matching %{term}"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|We couldn't find any %{scope} matching %{term} in group %{group}"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|We couldn't find any %{scope} matching %{term} in project %{project}"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|What are you searching for?"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Your work"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26750,15 +26807,15 @@ msgstr ""
|
|||
msgid "IDE|Contact your administrator or try to open the Web IDE again with another domain."
|
||||
msgstr ""
|
||||
|
||||
msgid "IDE|Could not find a callback URL entry for %{expectedCallbackUrl}."
|
||||
msgstr ""
|
||||
|
||||
msgid "IDE|Edit"
|
||||
msgstr ""
|
||||
|
||||
msgid "IDE|Editing this application might affect the functionality of the Web IDE. Ensure the configuration meets the following conditions:"
|
||||
msgstr ""
|
||||
|
||||
msgid "IDE|Error reloading page"
|
||||
msgstr ""
|
||||
|
||||
msgid "IDE|GitLab logo"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -30876,9 +30933,6 @@ msgstr ""
|
|||
msgid "Last GitLab activity"
|
||||
msgstr ""
|
||||
|
||||
msgid "Last Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Last Seen"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -39888,9 +39942,6 @@ msgstr ""
|
|||
msgid "Please select a Jira project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please select a country / region"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please select a group"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -49240,6 +49291,9 @@ msgstr ""
|
|||
msgid "Select a comment template"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select a country or region"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -56302,12 +56356,15 @@ msgstr ""
|
|||
msgid "Trials|Your trial ends on %{boldStart}%{trialEndDate}%{boldEnd}. We hope you’re enjoying the features of GitLab %{planName}. To keep those features after your trial ends, you’ll need to buy a subscription. (You can also choose GitLab Premium if it meets your needs.)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial| By selecting Continue or registering through a third party, you accept the %{gitlabSubscriptionAgreement} and acknowledge the %{privacyStatement} and %{cookiePolicy}"
|
||||
msgid "Trial|Activate my trial"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Allowed characters: +, 0-9, -, and spaces."
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|By clicking \"%{buttonText}\" you accept the %{gitlabSubscriptionAgreement} and acknowledge the %{privacyStatement} and %{cookiePolicy}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Continue"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -56317,16 +56374,19 @@ msgstr ""
|
|||
msgid "Trial|GitLab Subscription Agreement"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Please select"
|
||||
msgid "Trial|Privacy Statement"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Privacy Statement"
|
||||
msgid "Trial|Select number of employees"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Select state or province"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Start free GitLab Ultimate trial"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|State/Province"
|
||||
msgid "Trial|State or province"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|To activate your trial, we need additional details from you."
|
||||
|
|
@ -58668,6 +58728,9 @@ msgstr ""
|
|||
msgid "View File Metadata"
|
||||
msgstr ""
|
||||
|
||||
msgid "View Line in repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "View Stage: %{title}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -59527,6 +59590,9 @@ msgstr ""
|
|||
msgid "We're experiencing difficulties and this tab content is currently unavailable."
|
||||
msgstr ""
|
||||
|
||||
msgid "We're sorry, your GitLab Duo Enterprise trial could not be created because our system did not respond successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "We're sorry, your GitLab Duo Pro trial could not be created because our system did not respond successfully."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@
|
|||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/fonts": "^1.3.0",
|
||||
"@gitlab/svgs": "3.109.0",
|
||||
"@gitlab/ui": "87.8.0",
|
||||
"@gitlab/ui": "88.0.0",
|
||||
"@gitlab/web-ide": "^0.0.1-dev-20240613133550",
|
||||
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
|
||||
"@rails/actioncable": "7.0.8-4",
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ module QA
|
|||
runner.remove_via_api!
|
||||
end
|
||||
|
||||
it 'runs in project pipeline with correct inputs', :aggregate_failures,
|
||||
it 'runs in project pipeline with correct inputs', :blocking, :aggregate_failures,
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/451582' do
|
||||
Flow::Pipeline.visit_latest_pipeline
|
||||
|
||||
|
|
|
|||
|
|
@ -1,87 +1,111 @@
|
|||
import { GlButton, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
|
||||
import { GlButton, GlDisclosureDropdown } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import OAuthDomainMismatchError from '~/ide/components/oauth_domain_mismatch_error.vue';
|
||||
|
||||
const MOCK_CALLBACK_URL_ORIGIN = 'https://example1.com';
|
||||
const MOCK_PATH_NAME = '/path/to/ide';
|
||||
const MOCK_CALLBACK_URLS = [
|
||||
{
|
||||
base: 'https://example1.com/',
|
||||
},
|
||||
{
|
||||
base: 'https://example2.com/',
|
||||
},
|
||||
{
|
||||
base: 'https://example3.com/relative-path/',
|
||||
},
|
||||
];
|
||||
const MOCK_CALLBACK_URL = 'https://example.com';
|
||||
const MOCK_PATH_NAME = 'path/to/ide';
|
||||
|
||||
const EXPECTED_DROPDOWN_ITEMS = MOCK_CALLBACK_URLS.map(({ base }) => ({
|
||||
text: base,
|
||||
href: `${base}${MOCK_PATH_NAME}`,
|
||||
}));
|
||||
|
||||
describe('OAuthDomainMismatchError', () => {
|
||||
useMockLocationHelper();
|
||||
|
||||
let wrapper;
|
||||
let originalLocation;
|
||||
|
||||
const findButton = () => wrapper.findComponent(GlButton);
|
||||
const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
|
||||
const findDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
|
||||
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
|
||||
|
||||
const createWrapper = (props = {}) => {
|
||||
wrapper = mount(OAuthDomainMismatchError, {
|
||||
propsData: {
|
||||
callbackUrlOrigins: [MOCK_CALLBACK_URL_ORIGIN],
|
||||
expectedCallbackUrl: MOCK_CALLBACK_URL,
|
||||
callbackUrls: MOCK_CALLBACK_URLS,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
originalLocation = window.location;
|
||||
window.location.pathname = MOCK_PATH_NAME;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.location = originalLocation;
|
||||
setWindowLocation(`/${MOCK_PATH_NAME}`);
|
||||
});
|
||||
|
||||
describe('single callback URL domain passed', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper();
|
||||
createWrapper({
|
||||
callbackUrls: MOCK_CALLBACK_URLS.slice(0, 1),
|
||||
});
|
||||
});
|
||||
|
||||
it('renders expected callback URL message', () => {
|
||||
expect(wrapper.text()).toContain(
|
||||
`Could not find a callback URL entry for ${MOCK_CALLBACK_URL}.`,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not render dropdown', () => {
|
||||
expect(findDropdown().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('reloads page with correct url on button click', async () => {
|
||||
findButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1);
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
new URL(MOCK_CALLBACK_URL_ORIGIN + MOCK_PATH_NAME).toString(),
|
||||
);
|
||||
it('renders button with correct attributes', () => {
|
||||
const button = findButton();
|
||||
expect(button.exists()).toBe(true);
|
||||
const baseUrl = MOCK_CALLBACK_URLS[0].base;
|
||||
expect(button.text()).toContain(baseUrl);
|
||||
expect(button.attributes('href')).toBe(`${baseUrl}${MOCK_PATH_NAME}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple callback URL domains passed', () => {
|
||||
const MOCK_CALLBACK_URL_ORIGINS = [MOCK_CALLBACK_URL_ORIGIN, 'https://example2.com'];
|
||||
|
||||
beforeEach(() => {
|
||||
createWrapper({ callbackUrlOrigins: MOCK_CALLBACK_URL_ORIGINS });
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
it('renders dropdown', () => {
|
||||
expect(findDropdown().exists()).toBe(true);
|
||||
it('renders dropdown with correct items', () => {
|
||||
const dropdown = findDropdown();
|
||||
|
||||
expect(dropdown.exists()).toBe(true);
|
||||
expect(dropdown.props('items')).toStrictEqual(EXPECTED_DROPDOWN_ITEMS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with erroneous callback from current origin', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({
|
||||
callbackUrls: MOCK_CALLBACK_URLS.concat({
|
||||
base: `${TEST_HOST}/foo`,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('renders dropdown items', () => {
|
||||
const dropdownItems = findDropdownItems();
|
||||
expect(dropdownItems.length).toBe(MOCK_CALLBACK_URL_ORIGINS.length);
|
||||
expect(dropdownItems.at(0).text()).toBe(MOCK_CALLBACK_URL_ORIGINS[0]);
|
||||
expect(dropdownItems.at(1).text()).toBe(MOCK_CALLBACK_URL_ORIGINS[1]);
|
||||
it('filters out item with current origin', () => {
|
||||
expect(findDropdown().props('items')).toStrictEqual(EXPECTED_DROPDOWN_ITEMS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when no callback URL passed', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper({
|
||||
callbackUrls: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('reloads page with correct url on dropdown item click', async () => {
|
||||
const dropdownItem = findDropdownItems().at(0);
|
||||
dropdownItem.vm.$emit('select', MOCK_CALLBACK_URL_ORIGIN);
|
||||
await nextTick();
|
||||
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1);
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
new URL(MOCK_CALLBACK_URL_ORIGIN + MOCK_PATH_NAME).toString(),
|
||||
);
|
||||
it('does not render dropdown or button', () => {
|
||||
expect(findDropdown().exists()).toBe(false);
|
||||
expect(findButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import * as pathUtils from 'path';
|
||||
import { commitActionTypes } from '~/ide/constants';
|
||||
import { WEB_IDE_OAUTH_CALLBACK_URL_PATH, commitActionTypes } from '~/ide/constants';
|
||||
import { decorateData } from '~/ide/stores/utils';
|
||||
import { WEB_IDE_OAUTH_CALLBACK_URL_PATH } from '~/ide/lib/gitlab_web_ide/get_oauth_config';
|
||||
|
||||
export const file = (name = 'name', id = name, type = '', parent = null) =>
|
||||
decorateData({
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ import { startIde } from '~/ide/index';
|
|||
import { IDE_ELEMENT_ID } from '~/ide/constants';
|
||||
import { OAuthCallbackDomainMismatchErrorApp } from '~/ide/oauth_callback_domain_mismatch_error';
|
||||
import { initGitlabWebIDE } from '~/ide/init_gitlab_web_ide';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
|
||||
jest.mock('~/ide/init_gitlab_web_ide');
|
||||
|
||||
const MOCK_CALLBACK_URL = `${window.location.origin}/ide/redirect`;
|
||||
const MOCK_MISMATCH_CALLBACK_URL = 'https://example.com/ide/redirect';
|
||||
const MOCK_DATA_SET = {
|
||||
callbackUrls: JSON.stringify([MOCK_CALLBACK_URL]),
|
||||
callbackUrls: JSON.stringify([`${TEST_HOST}/-/ide/oauth_redirect`]),
|
||||
useNewWebIde: true,
|
||||
};
|
||||
/**
|
||||
|
|
@ -27,12 +29,20 @@ const setupMockIdeElement = (customData = MOCK_DATA_SET) => {
|
|||
};
|
||||
|
||||
describe('startIde', () => {
|
||||
let renderErrorSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
setWindowLocation(`${TEST_HOST}/-/ide/edit/gitlab-org/gitlab`);
|
||||
renderErrorSpy = jest.spyOn(OAuthCallbackDomainMismatchErrorApp.prototype, 'renderError');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.getElementById(IDE_ELEMENT_ID).remove();
|
||||
document.getElementById(IDE_ELEMENT_ID)?.remove();
|
||||
});
|
||||
|
||||
describe('when useNewWebIde feature flag is true', () => {
|
||||
let ideElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
ideElement = setupMockIdeElement();
|
||||
|
||||
|
|
@ -43,34 +53,14 @@ describe('startIde', () => {
|
|||
expect(initGitlabWebIDE).toHaveBeenCalledTimes(1);
|
||||
expect(initGitlabWebIDE).toHaveBeenCalledWith(ideElement);
|
||||
});
|
||||
|
||||
it('does not render error page', () => {
|
||||
expect(renderErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('OAuth callback origin mismatch check', () => {
|
||||
let renderErrorSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
renderErrorSpy = jest.spyOn(OAuthCallbackDomainMismatchErrorApp.prototype, 'renderError');
|
||||
});
|
||||
|
||||
it('does not render error page if no callbackUrl provided', async () => {
|
||||
setupMockIdeElement({ useNewWebIde: true });
|
||||
await startIde();
|
||||
|
||||
expect(renderErrorSpy).not.toHaveBeenCalled();
|
||||
expect(initGitlabWebIDE).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not call renderOAuthDomainMismatchError if no mismatch detected', async () => {
|
||||
setupMockIdeElement();
|
||||
await startIde();
|
||||
|
||||
expect(renderErrorSpy).not.toHaveBeenCalled();
|
||||
expect(initGitlabWebIDE).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders error page if OAuth callback origin does not match window.location.origin', async () => {
|
||||
const MOCK_MISMATCH_CALLBACK_URL = 'https://example.com/ide/redirect';
|
||||
renderErrorSpy.mockImplementation(() => {});
|
||||
describe('with mismatch callback url', () => {
|
||||
it('renders error page', async () => {
|
||||
setupMockIdeElement({
|
||||
callbackUrls: JSON.stringify([MOCK_MISMATCH_CALLBACK_URL]),
|
||||
useNewWebIde: true,
|
||||
|
|
@ -82,4 +72,17 @@ describe('startIde', () => {
|
|||
expect(initGitlabWebIDE).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with relative URL location and mismatch callback url', () => {
|
||||
it('renders error page', async () => {
|
||||
setWindowLocation(`${TEST_HOST}/relative-path/-/ide/edit/project`);
|
||||
|
||||
setupMockIdeElement();
|
||||
|
||||
await startIde();
|
||||
|
||||
expect(renderErrorSpy).toHaveBeenCalledTimes(1);
|
||||
expect(initGitlabWebIDE).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ describe('ide/init_gitlab_web_ide', () => {
|
|||
mrTargetProject: '',
|
||||
forkInfo: null,
|
||||
username: gon.current_username,
|
||||
gitlabUrl: TEST_HOST,
|
||||
gitlabUrl: `${TEST_HOST}/`,
|
||||
nonce: TEST_NONCE,
|
||||
httpHeaders: {
|
||||
'mock-csrf-header': 'mock-csrf-token',
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ describe('~/ide/lib/gitlab_web_ide/get_base_config', () => {
|
|||
|
||||
expect(actual).toEqual({
|
||||
baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
|
||||
gitlabUrl: TEST_HOST,
|
||||
gitlabUrl: `${TEST_HOST}/`,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ describe('~/ide/lib/gitlab_web_ide/get_base_config', () => {
|
|||
|
||||
expect(actual).toEqual({
|
||||
baseUrl: `${TEST_HOST}${TEST_RELATIVE_URL_ROOT}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
|
||||
gitlabUrl: `${TEST_HOST}${TEST_RELATIVE_URL_ROOT}`,
|
||||
gitlabUrl: `${TEST_HOST}${TEST_RELATIVE_URL_ROOT}/`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
import {
|
||||
parseCallbackUrls,
|
||||
getOAuthCallbackUrl,
|
||||
} from '~/ide/lib/gitlab_web_ide/oauth_callback_urls';
|
||||
import { logError } from '~/lib/logger';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
import { IDE_PATH, WEB_IDE_OAUTH_CALLBACK_URL_PATH } from '~/ide/constants';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
|
||||
jest.mock('~/lib/logger');
|
||||
|
||||
const MOCK_IDE_PATH = joinPaths(IDE_PATH, 'some/path');
|
||||
|
||||
describe('ide/lib/oauth_callback_urls', () => {
|
||||
describe('getOAuthCallbackUrl', () => {
|
||||
const mockPath = MOCK_IDE_PATH;
|
||||
const MOCK_RELATIVE_PATH = 'relative-path';
|
||||
const mockPathWithRelative = joinPaths(MOCK_RELATIVE_PATH, MOCK_IDE_PATH);
|
||||
|
||||
const originalHref = window.location.href;
|
||||
|
||||
afterEach(() => {
|
||||
setWindowLocation(originalHref);
|
||||
});
|
||||
|
||||
const expectedBaseUrlWithRelative = joinPaths(window.location.origin, MOCK_RELATIVE_PATH);
|
||||
|
||||
it.each`
|
||||
path | expectedCallbackBaseUrl
|
||||
${mockPath} | ${window.location.origin}
|
||||
${mockPathWithRelative} | ${expectedBaseUrlWithRelative}
|
||||
`(
|
||||
'retrieves expected callback URL based on window url',
|
||||
({ path, expectedCallbackBaseUrl }) => {
|
||||
setWindowLocation(path);
|
||||
|
||||
const actual = getOAuthCallbackUrl();
|
||||
const expected = joinPaths(expectedCallbackBaseUrl, WEB_IDE_OAUTH_CALLBACK_URL_PATH);
|
||||
expect(actual).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
describe('parseCallbackUrls', () => {
|
||||
it('parses the given JSON URL array and returns some metadata for them', () => {
|
||||
const actual = parseCallbackUrls(
|
||||
JSON.stringify([
|
||||
'https://gitlab.com/-/ide/oauth_redirect',
|
||||
'not a url',
|
||||
'https://gdk.test:3443/-/ide/oauth_redirect/',
|
||||
'https://gdk.test:3443/gitlab/-/ide/oauth_redirect#1234?query=foo',
|
||||
'https://example.com/not-a-real-one-/ide/oauth_redirectz',
|
||||
]),
|
||||
);
|
||||
|
||||
expect(actual).toEqual([
|
||||
{
|
||||
base: 'https://gitlab.com/',
|
||||
url: 'https://gitlab.com/-/ide/oauth_redirect',
|
||||
},
|
||||
{
|
||||
base: 'https://gdk.test:3443/',
|
||||
url: 'https://gdk.test:3443/-/ide/oauth_redirect/',
|
||||
},
|
||||
{
|
||||
base: 'https://gdk.test:3443/gitlab/',
|
||||
url: 'https://gdk.test:3443/gitlab/-/ide/oauth_redirect#1234?query=foo',
|
||||
},
|
||||
{
|
||||
base: 'https://example.com/',
|
||||
url: 'https://example.com/not-a-real-one-/ide/oauth_redirectz',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns empty when given empty', () => {
|
||||
expect(parseCallbackUrls('')).toEqual([]);
|
||||
expect(logError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns empty when not valid JSON', () => {
|
||||
expect(parseCallbackUrls('babar')).toEqual([]);
|
||||
expect(logError).toHaveBeenCalledWith('Failed to parse callback URLs JSON');
|
||||
});
|
||||
|
||||
it('returns empty when not array JSON', () => {
|
||||
expect(parseCallbackUrls('{}')).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -46,7 +46,7 @@ describe('~/ide/mount_oauth_callback', () => {
|
|||
clientId: TEST_OAUTH_CLIENT_ID,
|
||||
protectRefreshToken: true,
|
||||
},
|
||||
gitlabUrl: TEST_HOST,
|
||||
gitlabUrl: `${TEST_HOST}/`,
|
||||
baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
|
||||
username: TEST_USERNAME,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1304,4 +1304,21 @@ describe('URL utility', () => {
|
|||
expect(urlUtils.buildURLwithRefType({ base, path, refType })).toBe(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stripRelativeUrlRootFromPath', () => {
|
||||
it.each`
|
||||
relativeUrlRoot | path | expectation
|
||||
${''} | ${'/foo/bar'} | ${'/foo/bar'}
|
||||
${'/'} | ${'/foo/bar'} | ${'/foo/bar'}
|
||||
${'/foo'} | ${'/foo/bar'} | ${'/bar'}
|
||||
${'/gitlab/'} | ${'/gitlab/-/ide/foo'} | ${'/-/ide/foo'}
|
||||
`(
|
||||
'with relative_url_root="$relativeUrlRoot", "$path" should return "$expectation"',
|
||||
({ relativeUrlRoot, path, expectation }) => {
|
||||
window.gon.relative_url_root = relativeUrlRoot;
|
||||
|
||||
expect(urlUtils.stripRelativeUrlRootFromPath(path)).toBe(expectation);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GlobalSearchResultsEmpty component basics renders all parts of header 1`] = `
|
||||
<gl-empty-state-stub
|
||||
contentclass=""
|
||||
description="No results found"
|
||||
invertindarkmode="true"
|
||||
svgheight="144"
|
||||
svgpath="file-mock"
|
||||
title="No results found"
|
||||
>
|
||||
<gl-sprintf-stub
|
||||
message="We couldn't find any %{scope} matching %{term}"
|
||||
/>
|
||||
</gl-empty-state-stub>
|
||||
`;
|
||||
|
|
@ -3,5 +3,89 @@
|
|||
exports[`ZoektBlobResults when component loads normally renders component properly 1`] = `
|
||||
<div
|
||||
class="gl-flex gl-flex-col gl-justify-center"
|
||||
/>
|
||||
>
|
||||
<div
|
||||
class="gl-relative"
|
||||
>
|
||||
<div
|
||||
class="file-holder file-result-holder gl-card gl-my-5"
|
||||
>
|
||||
<div
|
||||
class="file-title gl-card-header gl-new-card-header"
|
||||
>
|
||||
<blob-header-stub
|
||||
filepath="test/test-main.js"
|
||||
fileurl="http://127.0.0.1:3000/flightjs/Flight/-/blob/master/test/test-main.js"
|
||||
projectpath="flightjs/Flight"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="gl-card-body gl-p-0"
|
||||
>
|
||||
<blob-body-stub
|
||||
file="[object Object]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="file-holder file-result-holder gl-card gl-my-5"
|
||||
>
|
||||
<div
|
||||
class="file-title gl-card-header gl-new-card-header"
|
||||
>
|
||||
<blob-header-stub
|
||||
filepath="test/spec/fn_spec.js"
|
||||
fileurl="http://127.0.0.1:3000/flightjs/Flight/-/blob/master/test/spec/fn_spec.js"
|
||||
projectpath="flightjs/Flight"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="gl-card-body gl-p-0"
|
||||
>
|
||||
<blob-body-stub
|
||||
file="[object Object]"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="gl-card-footer gl-new-card-footer"
|
||||
>
|
||||
<blob-footer-stub
|
||||
file="[object Object]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="file-holder file-result-holder gl-card gl-my-5"
|
||||
>
|
||||
<div
|
||||
class="file-title gl-border-b-0! gl-card-header gl-new-card-header"
|
||||
>
|
||||
<blob-header-stub
|
||||
filepath="test/spec/test_utils_spec.js"
|
||||
fileurl="http://127.0.0.1:3000/flightjs/Flight/-/blob/master/test/spec/utils_spec.js"
|
||||
projectpath="flightjs/Flight"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="gl-card-body gl-p-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<gl-pagination-stub
|
||||
align="left"
|
||||
class="gl-mx-auto"
|
||||
ellipsistext="…"
|
||||
labelfirstpage="Go to first page"
|
||||
labellastpage="Go to last page"
|
||||
labelnextpage="Go to next page"
|
||||
labelpage="Go to page %{page}"
|
||||
labelprevpage="Go to previous page"
|
||||
limits="[object Object]"
|
||||
nexttext="Next"
|
||||
perpage="20"
|
||||
prevtext="Previous"
|
||||
totalitems="3"
|
||||
value="1"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import BlobChunks from '~/search/results/components/blob_chunks.vue';
|
||||
import ZoektBlobResultsChunks from '~/search/results/components/blob_body.vue';
|
||||
import eventHub from '~/search/results/event_hub';
|
||||
import { mockDataForBlobBody } from '../../mock_data';
|
||||
|
||||
describe('BlobChunks', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (file = {}) => {
|
||||
wrapper = shallowMountExtended(ZoektBlobResultsChunks, {
|
||||
propsData: {
|
||||
file,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findBlobChunks = () => wrapper.findAllComponents(BlobChunks);
|
||||
|
||||
describe('component basics', () => {
|
||||
beforeEach(() => {
|
||||
createComponent(mockDataForBlobBody);
|
||||
});
|
||||
|
||||
it(`renders default amount of chunks`, () => {
|
||||
expect(findBlobChunks()).toHaveLength(3);
|
||||
expect(findBlobChunks().at(0).props()).toMatchObject({
|
||||
chunk: {
|
||||
lines: expect.any(Array),
|
||||
matchCountInChunk: expect.any(Number),
|
||||
__typename: expect.any(String),
|
||||
},
|
||||
blameLink: 'blame/test.js',
|
||||
fileUrl: 'https://gitlab.com/file/test.js',
|
||||
});
|
||||
});
|
||||
|
||||
it(`renders all chunks`, async () => {
|
||||
eventHub.$emit('showMore', { id: 'Testjs/Test:file/test.js', state: true });
|
||||
await nextTick();
|
||||
expect(findBlobChunks()).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { GlIcon, GlLink } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import BlobChunks from '~/search/results/components/blob_chunks.vue';
|
||||
|
||||
describe('BlobChunks', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (props) => {
|
||||
wrapper = shallowMountExtended(BlobChunks, {
|
||||
propsData: {
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
GlLink,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findGlIcon = () => wrapper.findAllComponents(GlIcon);
|
||||
const findGlLink = () => wrapper.findAllComponents(GlLink);
|
||||
const findLine = () => wrapper.findAllByTestId('search-blob-line');
|
||||
const findLineNumbers = () => wrapper.findAllByTestId('search-blob-line-numbers');
|
||||
const findLineCode = () => wrapper.findAllByTestId('search-blob-line-code');
|
||||
const findRootElement = () => wrapper.find('#search-blob-content');
|
||||
|
||||
describe('component basics', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
chunk: {
|
||||
lines: [
|
||||
{
|
||||
lineNumber: 1,
|
||||
richText: '',
|
||||
text: '',
|
||||
__typename: 'SearchBlobLine',
|
||||
},
|
||||
{
|
||||
lineNumber: 2,
|
||||
richText: '<b>test1</b>',
|
||||
text: 'test1',
|
||||
__typename: 'SearchBlobLine',
|
||||
},
|
||||
{ lineNumber: 3, richText: '', text: '', __typename: 'SearchBlobLine' },
|
||||
],
|
||||
matchCountInChunk: 1,
|
||||
__typename: 'SearchBlobChunk',
|
||||
},
|
||||
blameLink: 'https://gitlab.com/blame/test.js',
|
||||
fileUrl: 'https://gitlab.com/file/test.js',
|
||||
});
|
||||
});
|
||||
|
||||
it(`renders default state`, () => {
|
||||
expect(findLine()).toHaveLength(3);
|
||||
expect(findLineNumbers()).toHaveLength(3);
|
||||
expect(findLineCode()).toHaveLength(3);
|
||||
expect(findGlLink()).toHaveLength(6);
|
||||
expect(findGlIcon()).toHaveLength(3);
|
||||
});
|
||||
|
||||
it(`renders proper colors`, () => {
|
||||
expect(findRootElement().classes('white')).toBe(true);
|
||||
expect(findLineCode().at(1).find('b').classes('hll')).toBe(true);
|
||||
});
|
||||
|
||||
it(`renders links correctly`, () => {
|
||||
expect(findGlLink().at(0).attributes('href')).toBe('https://gitlab.com/blame/test.js#L1');
|
||||
expect(findGlLink().at(0).attributes('title')).toBe('View blame');
|
||||
expect(findGlLink().at(0).findComponent(GlIcon).exists()).toBe(true);
|
||||
expect(findGlLink().at(0).findComponent(GlIcon).props('name')).toBe('git');
|
||||
|
||||
expect(findGlLink().at(1).attributes('href')).toBe('https://gitlab.com/file/test.js#L1');
|
||||
expect(findGlLink().at(1).attributes('title')).toBe('View Line in repository');
|
||||
expect(findGlLink().at(1).text()).toBe('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { GlSprintf, GlButton, GlLink } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import BlobFooter from '~/search/results/components/blob_footer.vue';
|
||||
import eventHub from '~/search/results/event_hub';
|
||||
import { mockDataForBlobBody } from '../../mock_data';
|
||||
|
||||
describe('BlobFooter', () => {
|
||||
let wrapper;
|
||||
let spy;
|
||||
|
||||
const createComponent = (props) => {
|
||||
wrapper = shallowMountExtended(BlobFooter, {
|
||||
propsData: {
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findGlButton = () => wrapper.findComponent(GlButton);
|
||||
const findGlLink = () => wrapper.findComponent(GlLink);
|
||||
|
||||
describe('component basics', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
file: mockDataForBlobBody,
|
||||
});
|
||||
spy = jest.spyOn(eventHub, '$emit');
|
||||
});
|
||||
|
||||
it(`renders default closed state`, () => {
|
||||
expect(findGlButton().exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain('Show 1 more matches');
|
||||
});
|
||||
|
||||
it(`renders default open state`, async () => {
|
||||
findGlButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
expect(spy).toHaveBeenCalledWith('showMore', {
|
||||
id: 'Testjs/Test:file/test.js',
|
||||
state: true,
|
||||
});
|
||||
expect(wrapper.text()).toContain('Show less');
|
||||
});
|
||||
});
|
||||
|
||||
describe('component with too many results', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
// matchCountTotal: 100,
|
||||
// matchCount: 100,
|
||||
// filePath: 'test/file.js',
|
||||
// projectPath: 'Testjs/Test',
|
||||
// fileLink: 'https://gitlab.com/test/file.js',
|
||||
file: {
|
||||
...mockDataForBlobBody,
|
||||
chunks: [
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
...mockDataForBlobBody.chunks,
|
||||
],
|
||||
matchCountTotal: 200,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it(`renders closed state`, () => {
|
||||
expect(findGlButton().exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain('Show 97 more matches');
|
||||
});
|
||||
|
||||
it(`renders open state`, async () => {
|
||||
findGlButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
expect(findGlLink().exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain(
|
||||
'Show less - Too many matches found. Showing 50 chunks out of 200 results. Open the file to view all.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import BlobHeader from '~/search/results/components/blob_header.vue';
|
||||
|
||||
describe('BlobHeader', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (props) => {
|
||||
wrapper = shallowMountExtended(BlobHeader, {
|
||||
propsData: {
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
|
||||
const findFileIcon = () => wrapper.findComponent(FileIcon);
|
||||
const findProjectPath = () => wrapper.findByTestId('project-path-content');
|
||||
const findProjectName = () => wrapper.findByTestId('file-name-content');
|
||||
|
||||
describe('component basics', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
filePath: 'test/file.js',
|
||||
projectPath: 'Testjs/Test',
|
||||
fileUrl: 'https://gitlab.com/test/file.js',
|
||||
});
|
||||
});
|
||||
|
||||
it(`renders all parts of header`, () => {
|
||||
expect(findClipboardButton().exists()).toBe(true);
|
||||
expect(findFileIcon().exists()).toBe(true);
|
||||
expect(findProjectPath().exists()).toBe(true);
|
||||
expect(findProjectName().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('limited component', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
filePath: 'test/file.js',
|
||||
fileUrl: 'https://gitlab.com/test/file.js',
|
||||
});
|
||||
});
|
||||
|
||||
it(`renders withough projectPath`, () => {
|
||||
expect(findClipboardButton().exists()).toBe(true);
|
||||
expect(findFileIcon().exists()).toBe(true);
|
||||
expect(findProjectPath().exists()).toBe(false);
|
||||
expect(findProjectName().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import Vue from 'vue';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import Vuex from 'vuex';
|
||||
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
|
||||
import GlobalSearchResultsEmpty from '~/search/results/components/result_empty.vue';
|
||||
import { MOCK_QUERY } from '../../mock_data';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
describe('GlobalSearchResultsEmpty', () => {
|
||||
let wrapper;
|
||||
|
||||
const getterSpies = {
|
||||
currentScope: jest.fn(() => 'blobs'),
|
||||
};
|
||||
|
||||
const createComponent = (
|
||||
props,
|
||||
initialState = { query: { scope: 'blobs' }, searchType: 'zoekt' },
|
||||
) => {
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
query: MOCK_QUERY,
|
||||
...initialState,
|
||||
},
|
||||
getters: getterSpies,
|
||||
});
|
||||
|
||||
wrapper = shallowMountExtended(GlobalSearchResultsEmpty, {
|
||||
store,
|
||||
propsData: {
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('component basics', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it(`renders all parts of header`, () => {
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -10,6 +10,7 @@ import ZoektBlobResults from '~/search/results/components/zoekt_blob_results.vue
|
|||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { createAlert } from '~/alert';
|
||||
|
||||
import EmptyResult from '~/search/results/components/result_empty.vue';
|
||||
import { MOCK_QUERY, mockGetBlobSearchQuery } from '../../mock_data';
|
||||
|
||||
jest.mock('~/alert');
|
||||
|
|
@ -26,6 +27,7 @@ describe('ZoektBlobResults', () => {
|
|||
|
||||
const blobSearchHandler = jest.fn().mockResolvedValue(mockGetBlobSearchQuery);
|
||||
const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {}));
|
||||
const mockQueryEmpty = jest.fn().mockReturnValue({});
|
||||
const mockQueryError = jest.fn().mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const createComponent = ({
|
||||
|
|
@ -42,7 +44,7 @@ describe('ZoektBlobResults', () => {
|
|||
},
|
||||
getters: getterSpies,
|
||||
});
|
||||
|
||||
// apolloMock = createMockApollo([[getBlobSearchQuery, blobSearchHandler]]);
|
||||
wrapper = shallowMountExtended(ZoektBlobResults, {
|
||||
apolloProvider,
|
||||
store,
|
||||
|
|
@ -53,6 +55,7 @@ describe('ZoektBlobResults', () => {
|
|||
};
|
||||
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findEmptyResult = () => wrapper.findComponent(EmptyResult);
|
||||
|
||||
describe('when loading results', () => {
|
||||
beforeEach(async () => {
|
||||
|
|
@ -81,6 +84,21 @@ describe('ZoektBlobResults', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when component has no results', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({
|
||||
queryHandler: mockQueryEmpty,
|
||||
});
|
||||
jest.advanceTimersByTime(500);
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it(`renders component properly`, async () => {
|
||||
await nextTick();
|
||||
expect(findEmptyResult().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when component has load error', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({ queryHandler: mockQueryError });
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
import { GlModal, GlForm, GlFormGroup, GlFormRadioGroup } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue';
|
||||
import { CATEGORY_OPTIONS } from '~/abuse_reports/components/constants';
|
||||
|
||||
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
|
||||
|
||||
describe('WorkItemAbuseModal', () => {
|
||||
let wrapper;
|
||||
|
||||
const ACTION_PATH = '/abuse_reports/add_category';
|
||||
const USER_ID = 1;
|
||||
const REPORTED_FROM_URL = 'http://example.com';
|
||||
|
||||
const createComponent = (props) => {
|
||||
wrapper = shallowMountExtended(WorkItemAbuseModal, {
|
||||
propsData: {
|
||||
reportedUserId: USER_ID,
|
||||
reportedFromUrl: REPORTED_FROM_URL,
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
reportAbusePath: ACTION_PATH,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({ showModal: true });
|
||||
});
|
||||
|
||||
const findAbuseModal = () => wrapper.findComponent(GlModal);
|
||||
const findForm = () => wrapper.findComponent(GlForm);
|
||||
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
|
||||
const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
|
||||
|
||||
const findCSRFToken = () => findForm().find('input[name="authenticity_token"]');
|
||||
const findUserId = () => wrapper.findByTestId('input-user-id');
|
||||
const findReferer = () => wrapper.findByTestId('input-referer');
|
||||
|
||||
describe('Modal', () => {
|
||||
it('renders report abuse modal with the form', () => {
|
||||
expect(findAbuseModal().exists()).toBe(true);
|
||||
expect(findForm().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should set the modal title when the `title` prop is set', () => {
|
||||
const title = 'Report abuse to administrator';
|
||||
createComponent({ title, showModal: true });
|
||||
|
||||
expect(findAbuseModal().props().title).toBe(title);
|
||||
});
|
||||
|
||||
it('should set modal size to `sm` by default', () => {
|
||||
expect(findAbuseModal().props('size')).toBe('sm');
|
||||
});
|
||||
|
||||
it('renders radio form group with the first option selected by default', () => {
|
||||
const firstOption = CATEGORY_OPTIONS[0].value;
|
||||
expect(findRadioGroup().attributes('checked')).toBe(firstOption);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Select category form', () => {
|
||||
it('renders POST form with path', () => {
|
||||
expect(findForm().attributes()).toMatchObject({
|
||||
method: 'post',
|
||||
action: ACTION_PATH,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders csrf token', () => {
|
||||
expect(findCSRFToken().attributes('value')).toBe('mock-csrf-token');
|
||||
});
|
||||
|
||||
it('renders label', () => {
|
||||
expect(findFormGroup().attributes('label')).toBe('Why are you reporting this user?');
|
||||
});
|
||||
|
||||
it('renders radio group', () => {
|
||||
expect(findRadioGroup().props('options')).toEqual(CATEGORY_OPTIONS);
|
||||
expect(findRadioGroup().attributes('name')).toBe('abuse_report[category]');
|
||||
});
|
||||
|
||||
it('renders userId as a hidden fields', () => {
|
||||
expect(findUserId().attributes()).toMatchObject({
|
||||
type: 'hidden',
|
||||
name: 'user_id',
|
||||
value: USER_ID.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
it('renders referer as a hidden fields', () => {
|
||||
expect(findReferer().attributes()).toMatchObject({
|
||||
type: 'hidden',
|
||||
name: 'abuse_report[reported_from_url]',
|
||||
value: REPORTED_FROM_URL,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -5,7 +5,7 @@ import {
|
|||
GlToggle,
|
||||
GlDisclosureDropdownItem,
|
||||
} from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
|
||||
import namespaceWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/namespace_work_item_types.query.graphql.json';
|
||||
|
|
@ -19,6 +19,7 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
|||
import { isLoggedIn } from '~/lib/utils/common_utils';
|
||||
import toast from '~/vue_shared/plugins/global_toast';
|
||||
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
|
||||
import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue';
|
||||
import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
|
||||
import {
|
||||
STATE_OPEN,
|
||||
|
|
@ -30,6 +31,7 @@ import {
|
|||
TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
|
||||
TEST_ID_PROMOTE_ACTION,
|
||||
TEST_ID_TOGGLE_ACTION,
|
||||
TEST_ID_REPORT_ABUSE,
|
||||
} from '~/work_items/constants';
|
||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_work_item_notifications.mutation.graphql';
|
||||
|
|
@ -64,6 +66,8 @@ describe('WorkItemActions component', () => {
|
|||
const findWorkItemToggleOption = () => wrapper.findComponent(WorkItemStateToggle);
|
||||
const findCopyCreateNoteEmailButton = () =>
|
||||
wrapper.findByTestId(TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION);
|
||||
const findReportAbuseButton = () => wrapper.findByTestId(TEST_ID_REPORT_ABUSE);
|
||||
const findReportAbuseModal = () => wrapper.findComponent(WorkItemAbuseModal);
|
||||
const findMoreDropdown = () => wrapper.findByTestId('work-item-actions-dropdown');
|
||||
const findMoreDropdownTooltip = () => getBinding(findMoreDropdown().element, 'gl-tooltip');
|
||||
const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *');
|
||||
|
|
@ -217,6 +221,10 @@ describe('WorkItemActions component', () => {
|
|||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
testId: TEST_ID_REPORT_ABUSE,
|
||||
text: 'Report abuse',
|
||||
},
|
||||
{
|
||||
testId: TEST_ID_DELETE_ACTION,
|
||||
text: 'Delete task',
|
||||
|
|
@ -502,4 +510,22 @@ describe('WorkItemActions component', () => {
|
|||
expect(findMoreDropdownTooltip().value).toBe('More actions');
|
||||
});
|
||||
});
|
||||
|
||||
describe('report abuse action', () => {
|
||||
it('renders the report abuse button', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findReportAbuseButton().exists()).toBe(true);
|
||||
expect(findReportAbuseModal().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('opens the report abuse modal', async () => {
|
||||
createComponent();
|
||||
|
||||
findReportAbuseButton().vm.$emit('action');
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.emitted('toggleReportAbuseModal')).toEqual([[true]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
|
|||
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
|
||||
import WorkItemStickyHeader from '~/work_items/components/work_item_sticky_header.vue';
|
||||
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
|
||||
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
|
||||
import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue';
|
||||
import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
|
||||
import DesignWidget from '~/work_items/components/design_management/design_management_widget.vue';
|
||||
import { i18n } from '~/work_items/constants';
|
||||
|
|
@ -83,7 +83,7 @@ describe('WorkItemDetail component', () => {
|
|||
const findWorkItemRelationships = () => wrapper.findComponent(WorkItemRelationships);
|
||||
const findNotesWidget = () => wrapper.findComponent(WorkItemNotes);
|
||||
const findModal = () => wrapper.findComponent(WorkItemDetailModal);
|
||||
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
|
||||
const findWorkItemAbuseModal = () => wrapper.findComponent(WorkItemAbuseModal);
|
||||
const findWorkItemTodos = () => wrapper.findComponent(WorkItemTodos);
|
||||
const findStickyHeader = () => wrapper.findComponent(WorkItemStickyHeader);
|
||||
const findWorkItemTwoColumnViewContainer = () => wrapper.findByTestId('work-item-overview');
|
||||
|
|
@ -696,7 +696,7 @@ describe('WorkItemDetail component', () => {
|
|||
});
|
||||
|
||||
it('should not be visible by default', () => {
|
||||
expect(findAbuseCategorySelector().exists()).toBe(false);
|
||||
expect(findWorkItemAbuseModal().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should be visible when the work item modal emits `openReportAbuse` event', async () => {
|
||||
|
|
@ -704,13 +704,25 @@ describe('WorkItemDetail component', () => {
|
|||
|
||||
await nextTick();
|
||||
|
||||
expect(findAbuseCategorySelector().exists()).toBe(true);
|
||||
expect(findWorkItemAbuseModal().exists()).toBe(true);
|
||||
|
||||
findAbuseCategorySelector().vm.$emit('close-drawer');
|
||||
findWorkItemAbuseModal().vm.$emit('close-modal');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findAbuseCategorySelector().exists()).toBe(false);
|
||||
expect(findWorkItemAbuseModal().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should be visible when the work item actions button emits `toggleReportAbuseModal` event', async () => {
|
||||
findWorkItemActions().vm.$emit('toggleReportAbuseModal', true);
|
||||
await nextTick();
|
||||
|
||||
expect(findWorkItemAbuseModal().exists()).toBe(true);
|
||||
|
||||
findWorkItemAbuseModal().vm.$emit('close-modal');
|
||||
await nextTick();
|
||||
|
||||
expect(findWorkItemAbuseModal().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
|
|||
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
|
||||
import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue';
|
||||
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
|
||||
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
|
||||
import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue';
|
||||
import { FORM_TYPES } from '~/work_items/constants';
|
||||
import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql';
|
||||
|
||||
|
|
@ -94,7 +94,7 @@ describe('WorkItemLinks', () => {
|
|||
const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
|
||||
const findChildrenCount = () => wrapper.findByTestId('children-count');
|
||||
const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
|
||||
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
|
||||
const findAbuseCategoryModal = () => wrapper.findComponent(WorkItemAbuseModal);
|
||||
const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper);
|
||||
const findShowLabelsToggle = () => wrapper.findComponent(GlToggle);
|
||||
|
||||
|
|
@ -237,7 +237,7 @@ describe('WorkItemLinks', () => {
|
|||
});
|
||||
|
||||
it('should not be visible by default', () => {
|
||||
expect(findAbuseCategorySelector().exists()).toBe(false);
|
||||
expect(findAbuseCategoryModal().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should be visible when the work item modal emits `openReportAbuse` event', async () => {
|
||||
|
|
@ -245,13 +245,13 @@ describe('WorkItemLinks', () => {
|
|||
|
||||
await nextTick();
|
||||
|
||||
expect(findAbuseCategorySelector().exists()).toBe(true);
|
||||
expect(findAbuseCategoryModal().exists()).toBe(true);
|
||||
|
||||
findAbuseCategorySelector().vm.$emit('close-drawer');
|
||||
findAbuseCategoryModal().vm.$emit('close-modal');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findAbuseCategorySelector().exists()).toBe(false);
|
||||
expect(findAbuseCategoryModal().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -270,6 +270,9 @@ merge_requests:
|
|||
- scan_result_policy_violations
|
||||
- applicable_post_merge_approval_rules
|
||||
- requested_changes
|
||||
- scan_result_policy_reads_through_violations
|
||||
- scan_result_policy_reads_through_approval_rules
|
||||
- running_scan_result_policy_violations
|
||||
external_pull_requests:
|
||||
- project
|
||||
merge_request_diff:
|
||||
|
|
|
|||
|
|
@ -3946,19 +3946,18 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
|
|||
context 'when auto_merge_requested is true' do
|
||||
let(:options) { { auto_merge_requested: true, auto_merge_strategy: auto_merge_strategy } }
|
||||
|
||||
where(:auto_merge_strategy, :skip_approved_check, :skip_draft_check, :skip_blocked_check,
|
||||
:skip_discussions_check, :skip_external_status_check, :skip_requested_changes_check, :skip_jira_check, :skip_locked_lfs_files_check) do
|
||||
'' | false | false | false | false | false | false | false | false
|
||||
AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS | false | false | false | false | false | false | false | false
|
||||
AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS | true | true | true | true | true | true | true | true
|
||||
where(:auto_merge_strategy, :skip_checks) do
|
||||
'' | false
|
||||
AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS | false
|
||||
AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS | true
|
||||
end
|
||||
|
||||
with_them do
|
||||
it do
|
||||
is_expected.to include(skip_approved_check: skip_approved_check, skip_draft_check: skip_draft_check,
|
||||
skip_blocked_check: skip_blocked_check, skip_discussions_check: skip_discussions_check,
|
||||
skip_external_status_check: skip_external_status_check, skip_requested_changes_check: skip_requested_changes_check,
|
||||
skip_jira_check: skip_jira_check)
|
||||
is_expected.to include(skip_approved_check: skip_checks, skip_draft_check: skip_checks,
|
||||
skip_blocked_check: skip_checks, skip_discussions_check: skip_checks,
|
||||
skip_external_status_check: skip_checks, skip_requested_changes_check: skip_checks,
|
||||
skip_jira_check: skip_checks, skip_security_policy_check: skip_checks)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat
|
|||
# We cannot disable SQL query limiting here, since the transaction does not
|
||||
# begin until we enter the controller.
|
||||
headers = {
|
||||
'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => '205,https://gitlab.com/gitlab-org/gitlab/-/issues/469250'
|
||||
'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => '206,https://gitlab.com/gitlab-org/gitlab/-/issues/469250'
|
||||
}
|
||||
|
||||
post_graphql(query, current_user: current_user, headers: headers)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'gitlab/dangerfiles/spec_helper'
|
||||
require 'fast_spec_helper'
|
||||
|
||||
require_relative '../../../tooling/danger/settings_sections'
|
||||
|
||||
RSpec.describe Tooling::Danger::SettingsSections, feature_category: :tooling do
|
||||
include_context 'with dangerfile'
|
||||
|
||||
subject(:settings_section_check) { fake_danger.new(helper: fake_helper) }
|
||||
|
||||
let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
|
||||
let(:matching_changed_files) { ['app/views/foo/bar.html.haml', 'app/assets/js/foo/bar.vue'] }
|
||||
let(:changed_lines) { ['-render SettingsBlockComponent.new(id: "foo") do', '<settings-block id="foo">'] }
|
||||
let(:stable_branch?) { false }
|
||||
|
||||
before do
|
||||
allow(fake_helper).to receive(:changed_files).and_return(matching_changed_files)
|
||||
allow(fake_helper).to receive(:changed_lines).and_return(changed_lines)
|
||||
allow(fake_helper).to receive(:stable_branch?).and_return(stable_branch?)
|
||||
end
|
||||
|
||||
context 'when on stable branch' do
|
||||
let(:stable_branch?) { true }
|
||||
|
||||
it 'does not write any markdown' do
|
||||
expect(settings_section_check).not_to receive(:markdown)
|
||||
settings_section_check.check!
|
||||
end
|
||||
end
|
||||
|
||||
context 'when none of the changed files are Haml or Vue files' do
|
||||
let(:matching_changed_files) { [] }
|
||||
|
||||
it 'does not write any markdown' do
|
||||
expect(settings_section_check).not_to receive(:markdown)
|
||||
settings_section_check.check!
|
||||
end
|
||||
end
|
||||
|
||||
context 'when none of the changed lines match the pattern' do
|
||||
let(:changed_lines) { ['-foo', '+bar'] }
|
||||
|
||||
it 'does not write any markdown' do
|
||||
expect(settings_section_check).not_to receive(:markdown)
|
||||
settings_section_check.check!
|
||||
end
|
||||
end
|
||||
|
||||
it 'adds a new markdown section listing every matching line' do
|
||||
expect(settings_section_check).to receive(:markdown).with(/Searchable setting sections/)
|
||||
expect(settings_section_check).to receive(:markdown).with(/SettingsBlock/)
|
||||
expect(settings_section_check).to receive(:markdown).with(/settings-block/)
|
||||
settings_section_check.check!
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Tooling
|
||||
module Danger
|
||||
module SettingsSections
|
||||
def check!
|
||||
return if helper.stable_branch?
|
||||
|
||||
changed_code_files = helper.changed_files(/\.(haml|vue)$/)
|
||||
return if changed_code_files.empty?
|
||||
|
||||
vc_regexp = /(SettingsBlockComponent|settings-block)/
|
||||
lines_with_matches = filter_changed_lines(changed_code_files, vc_regexp)
|
||||
return if lines_with_matches.empty?
|
||||
|
||||
markdown(<<~MARKDOWN)
|
||||
## Searchable setting sections
|
||||
|
||||
Looks like you have edited the template of some settings section. Please check that all changed sections are still searchable:
|
||||
|
||||
- If you created a new section, make sure to add it to either `lib/search/project_settings.rb` or `lib/search/group_settings.rb`, or in their counterparts in `ee/` if this section is only available behind a licensed feature.
|
||||
- If you removed a section, make sure to also remove it from the files above.
|
||||
- If you changed a section's id, please update it also in the files above.
|
||||
- If you just moved code around within the same page, there is nothing to do.
|
||||
|
||||
MARKDOWN
|
||||
|
||||
lines_with_matches.each do |file, lines|
|
||||
markdown(<<~MARKDOWN)
|
||||
#### `#{file}`
|
||||
|
||||
```shell
|
||||
#{lines.join("\n")}
|
||||
```
|
||||
|
||||
MARKDOWN
|
||||
end
|
||||
end
|
||||
|
||||
def filter_changed_lines(files, pattern)
|
||||
files_with_lines = {}
|
||||
files.each do |file|
|
||||
next if file.start_with?('spec/', 'ee/spec/', 'qa/')
|
||||
|
||||
matching_changed_lines = helper.changed_lines(file).select { |line| line =~ pattern }
|
||||
next unless matching_changed_lines.any?
|
||||
|
||||
files_with_lines[file] = matching_changed_lines
|
||||
end
|
||||
|
||||
files_with_lines
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1359,10 +1359,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.109.0.tgz#af953d8114768343034f1f02bc8e2d93eb613c65"
|
||||
integrity sha512-MmBTsco2LIh/l16iJQy6R98YDOlE3C++AE0Z1+KCpAX/3+fLAmULx2sWp+JnmM0ws8J0LaeLN6+vWiPaEWA16Q==
|
||||
|
||||
"@gitlab/ui@87.8.0":
|
||||
version "87.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-87.8.0.tgz#94b9e301330b22d466fffddaa4f9838385b68ae0"
|
||||
integrity sha512-oGWyFmI87IbTYb6uYGt79MwV/hkl/vVKqLtMCgx2JLnzYSXWxYAdCKPhmQiO8Fib5RpfYwLzsxZ5qfaazTq4ig==
|
||||
"@gitlab/ui@88.0.0":
|
||||
version "88.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-88.0.0.tgz#aab7b5ef169d02d65e80901680052c1f755eeec3"
|
||||
integrity sha512-O9K5UalOBLboPnskMPezt2nttKL/YXiSlADcDZ/MKTfw9usxRVMm+COuS+zrmqXfOEZfmNMd2lbQnXW9uJlyRQ==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "1.4.3"
|
||||
echarts "^5.3.2"
|
||||
|
|
|
|||
Loading…
Reference in New Issue