Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
9d2ebf5c7a
commit
ef53b9b911
|
|
@ -103,6 +103,7 @@ release-environments-qa:
|
|||
- echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
|
||||
retry:
|
||||
max: 2
|
||||
when: always
|
||||
|
||||
release-environments-notification-failure:
|
||||
stage: finish
|
||||
|
|
|
|||
|
|
@ -1335,7 +1335,7 @@ entry.
|
|||
- [Add UserStarredProjectsResolver sort argument](https://gitlab.com/gitlab-org/gitlab/-/commit/077ca496eaadc0a9383a552ed32294233de2f7e7) by @jzeng88 ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/153651))
|
||||
- [Migrates gl-display-inline-flex to gl-inline-flex](https://gitlab.com/gitlab-org/gitlab/-/commit/3aa4f990bde82a9c6fb59d7c726a02bddc693cea) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154887))
|
||||
- [Multiple frameworks labels](https://gitlab.com/gitlab-org/gitlab/-/commit/ca5a43e01aadde03cf32218f62f7e56eb5709f05) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156570)) **GitLab Enterprise Edition**
|
||||
- [Add permissions checking to AI impact dashboard](https://gitlab.com/gitlab-org/gitlab/-/commit/23bf0938f52424ec382ba745b57375234b769949) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156555)) **GitLab Enterprise Edition**
|
||||
- [Add permissions checking to AI Impact Dashboard](https://gitlab.com/gitlab-org/gitlab/-/commit/23bf0938f52424ec382ba745b57375234b769949) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156555)) **GitLab Enterprise Edition**
|
||||
- [Admin settings: Migrate security settings to use SettingsBlock](https://gitlab.com/gitlab-org/gitlab/-/commit/467df2db45835010a9b4210982fe662f2f30e8b4) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157043))
|
||||
- [Externalize strings on ldap_group_links](https://gitlab.com/gitlab-org/gitlab/-/commit/2fcc3e2fd12ea0c6813e7c88a1548c90cecf24e0) by @MAlvarez32 ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/155271))
|
||||
- [Improve usability of environment folders](https://gitlab.com/gitlab-org/gitlab/-/commit/076d3d3a212c3a93ec60863090c3a0fa185ecd05) by @antonkalmykov ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157104))
|
||||
|
|
@ -2246,7 +2246,7 @@ entry.
|
|||
- [Rate limit project / group notifications per user](gitlab-org/gitlab@ea40bb22bfd028d687faeaaa6cf9734777decde0) ([merge request](gitlab-org/gitlab!153786))
|
||||
- [Allow redirecting wiki directories on rename](gitlab-org/gitlab@8b9d3640355e73b9ed6196aeeafb923d3cb8f3be) ([merge request](gitlab-org/gitlab!153289))
|
||||
- [Add NOT NULL constraint to "path_locks.project_id"](gitlab-org/gitlab@8630330b599fcd0e15cc28680fac9b0c31c0ebea) ([merge request](gitlab-org/gitlab!153090))
|
||||
- [Add metric start date tooltip to AI impact dashboard](gitlab-org/gitlab@8999b334c8612b21a553c2b14d7ef342700854f4) ([merge request](gitlab-org/gitlab!153141)) **GitLab Enterprise Edition**
|
||||
- [Add metric start date tooltip to AI Impact Dashboard](gitlab-org/gitlab@8999b334c8612b21a553c2b14d7ef342700854f4) ([merge request](gitlab-org/gitlab!153141)) **GitLab Enterprise Edition**
|
||||
- [Create parent links for imported epics](gitlab-org/gitlab@d6132daae948ef8daada9fbbb37e0d98f7337040) ([merge request](gitlab-org/gitlab!154445))
|
||||
- [Migrate d-inline-block to gl-inline-block](gitlab-org/gitlab@cb06f5c91a046b7b86dda6793d4c3a6ceff3b6d7) ([merge request](gitlab-org/gitlab!152739))
|
||||
- [Docs(Epic Header): add entry to describe counts](gitlab-org/gitlab@494803b1a5ba348ce7ea43c0d94960e8ca6f68f1) ([merge request](gitlab-org/gitlab!154391)) **GitLab Enterprise Edition**
|
||||
|
|
@ -3175,7 +3175,7 @@ entry.
|
|||
- [Include template in deprecated flafinder-sast job](gitlab-org/gitlab@7bce91fd3639660b11b7669831f9ddc0d13bbe50) ([merge request](gitlab-org/gitlab!151298))
|
||||
- [Add AzureRM support to orphan artifacts cleanup](gitlab-org/gitlab@627eb5411af6f76a03c067131c7846c5c8d9129d) ([merge request](gitlab-org/gitlab!140497))
|
||||
- [Fix work item child status icon color](gitlab-org/gitlab@e5770bc16824362e06c73d56957255c5500f60c7) ([merge request](gitlab-org/gitlab!151094))
|
||||
- [Use locale-specific formatting for numbers in the AI Impact dashboard](gitlab-org/gitlab@07a2c3c576b7d0574c0d649ac7931d14606b5305) ([merge request](gitlab-org/gitlab!150882)) **GitLab Enterprise Edition**
|
||||
- [Use locale-specific formatting for numbers in the AI Impact Dashboard](gitlab-org/gitlab@07a2c3c576b7d0574c0d649ac7931d14606b5305) ([merge request](gitlab-org/gitlab!150882)) **GitLab Enterprise Edition**
|
||||
- [Allows ml_model pending destruction](gitlab-org/gitlab@4b3d7a7eaf03cb799fdd91781347bcafdd9fa040) ([merge request](gitlab-org/gitlab!150808))
|
||||
- [Fixes issue with registry search query params when removed](gitlab-org/gitlab@2f13fba9b1c405de37dc7b618f5472f129859989) ([merge request](gitlab-org/gitlab!150934))
|
||||
- [MR list: Fix overlapping search icon](gitlab-org/gitlab@7421cb36c6481acafb30435cb81695ff97bf6a3c) ([merge request](gitlab-org/gitlab!151045))
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
statsAriaLabel() {
|
||||
const comments = n__('%d comment', '%d comments', this.mergeRequest.userDiscussionsCount);
|
||||
const comments = n__('%d comment', '%d comments', this.mergeRequest.userNotesCount);
|
||||
const fileAdditions = n__(
|
||||
'%d file addition',
|
||||
'%d file additions',
|
||||
|
|
@ -109,7 +109,7 @@ export default {
|
|||
<div class="gl-flex gl-justify-end" :aria-label="statsAriaLabel">
|
||||
<div class="gl-whitespace-nowrap">
|
||||
<gl-icon name="comments" class="!gl-align-middle" />
|
||||
{{ mergeRequest.userDiscussionsCount }}
|
||||
{{ mergeRequest.userNotesCount }}
|
||||
</div>
|
||||
<div class="gl-ml-5 gl-whitespace-nowrap">
|
||||
<gl-icon name="doc-code" />
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ fragment MergeRequestDashboardFragment on MergeRequest {
|
|||
...CiIcon
|
||||
}
|
||||
}
|
||||
userDiscussionsCount
|
||||
userNotesCount
|
||||
createdAt
|
||||
updatedAt
|
||||
...MergeRequestApprovalFragment
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const sidebarInitState = () => {
|
|||
const el = document.getElementById('js-search-sidebar');
|
||||
if (!el) return {};
|
||||
|
||||
const { navigationJson, searchType, searchLevel, groupInitialJson, projectInitialJson } =
|
||||
const { navigationJson, searchType, searchLevel, groupInitialJson, projectInitialJson, ref } =
|
||||
el.dataset;
|
||||
|
||||
const navigationJsonParsed = JSON.parse(navigationJson);
|
||||
|
|
@ -26,6 +26,7 @@ const sidebarInitState = () => {
|
|||
searchLevel,
|
||||
groupInitialJsonParsed,
|
||||
projectInitialJsonParsed,
|
||||
ref,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ export const initSearchApp = () => {
|
|||
searchLevel,
|
||||
groupInitialJsonParsed: groupInitialJson,
|
||||
projectInitialJsonParsed: projectInitialJson,
|
||||
ref,
|
||||
} = sidebarInitState() || {};
|
||||
|
||||
const { defaultBranchName } = topBarInitState() || {};
|
||||
|
|
@ -61,6 +63,7 @@ export const initSearchApp = () => {
|
|||
groupInitialJson,
|
||||
projectInitialJson,
|
||||
defaultBranchName,
|
||||
repositoryRef: ref,
|
||||
});
|
||||
|
||||
initTopbar(store);
|
||||
|
|
|
|||
|
|
@ -1,33 +1,99 @@
|
|||
<script>
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import { __ } from '~/locale';
|
||||
import { __, s__ } from '~/locale';
|
||||
import getBlobSearchQuery from '~/search/graphql/blob_search_zoekt.query.graphql';
|
||||
import { SCOPE_BLOB, SEARCH_TYPE_ZOEKT } from '~/search/sidebar/constants/index';
|
||||
import ZoektBlobResults from '~/search/results/components/zoekt_blob_results.vue';
|
||||
import { DEFAULT_FETCH_CHUNKS } from '../constants';
|
||||
import StatusBar from './status_bar.vue';
|
||||
|
||||
import ZoektBlobResults from './zoekt_blob_results.vue';
|
||||
|
||||
export default {
|
||||
name: 'GlobalSearchResultsApp',
|
||||
i18n: {
|
||||
headerText: __('Search results'),
|
||||
blobDataFetchError: s__(
|
||||
'GlobalSearch|Could not load search results. Refresh the page to try again.',
|
||||
),
|
||||
},
|
||||
components: {
|
||||
ZoektBlobResults,
|
||||
StatusBar,
|
||||
GlAlert,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasError: false,
|
||||
blobSearch: {},
|
||||
hasResults: true,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
blobSearch: {
|
||||
query() {
|
||||
return getBlobSearchQuery;
|
||||
},
|
||||
errorPolicy: 'none',
|
||||
variables() {
|
||||
return {
|
||||
search: this.query.search,
|
||||
groupId: this.query.group_id && `gid://gitlab/Group/${this.query.group_id}`,
|
||||
projectId: this.query.project_id && `gid://gitlab/Project/${this.query.project_id}`,
|
||||
page: this.currentPage,
|
||||
chunkCount: DEFAULT_FETCH_CHUNKS,
|
||||
regex: this.query.regex ? JSON.parse(this.query.regex) : false,
|
||||
};
|
||||
},
|
||||
result({ data }) {
|
||||
this.hasError = false;
|
||||
this.blobSearch = data?.blobSearch;
|
||||
this.hasResults = data?.blobSearch?.files?.length > 0;
|
||||
},
|
||||
debounce: 500,
|
||||
error() {
|
||||
this.hasError = true;
|
||||
this.hasResults = false;
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['searchType']),
|
||||
...mapState(['searchType', 'query']),
|
||||
...mapGetters(['currentScope']),
|
||||
currentPage() {
|
||||
return this.query?.page ? parseInt(this.query?.page, 10) : 1;
|
||||
},
|
||||
isBlobScope() {
|
||||
return this.currentScope === SCOPE_BLOB;
|
||||
},
|
||||
isZoektSearch() {
|
||||
return this.searchType === SEARCH_TYPE_ZOEKT;
|
||||
},
|
||||
isLoading() {
|
||||
return this.$apollo.queries.blobSearch.loading;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clearErrors() {
|
||||
this.hasError = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<zoekt-blob-results v-if="isBlobScope && isZoektSearch" />
|
||||
</section>
|
||||
<div>
|
||||
<gl-alert v-if="hasError" variant="danger" @dismiss="clearErrors">
|
||||
{{ $options.i18n.blobDataFetchError }}
|
||||
</gl-alert>
|
||||
<section v-else-if="isBlobScope && isZoektSearch">
|
||||
<status-bar :blob-search="blobSearch" :has-results="hasResults" :is-loading="isLoading" />
|
||||
<zoekt-blob-results
|
||||
:blob-search="blobSearch"
|
||||
:has-results="hasResults"
|
||||
:is-loading="isLoading"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export default {
|
|||
<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"
|
||||
class="chunks-block gl-border-b gl-border-subtle last:gl-border-0"
|
||||
>
|
||||
<blob-chunks :chunk="chunk" :blame-link="file.blameUrl" :file-url="file.fileUrl" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { GlTooltipDirective, GlIcon, GlLink } from '@gitlab/ui';
|
||||
import GlSafeHtmlDirective from '~/vue_shared/directives/safe_html';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'BlobChunks',
|
||||
|
|
@ -12,6 +13,10 @@ export default {
|
|||
GlTooltip: GlTooltipDirective,
|
||||
SafeHtml: GlSafeHtmlDirective,
|
||||
},
|
||||
i18n: {
|
||||
viewBlame: s__('GlobalSearch|View blame'),
|
||||
viewLine: s__('GlobalSearch|View line in repository'),
|
||||
},
|
||||
props: {
|
||||
chunk: {
|
||||
type: Object,
|
||||
|
|
@ -56,24 +61,26 @@ export default {
|
|||
<gl-link
|
||||
v-gl-tooltip
|
||||
:href="`${blameLink}#L${line.lineNumber}`"
|
||||
:title="__('View blame')"
|
||||
:title="$options.i18n.viewBlame"
|
||||
class="js-navigation-open"
|
||||
><gl-icon name="git"
|
||||
/></gl-link>
|
||||
</span>
|
||||
<span class="diff-line-num flex-grow-1 gl-pr-3">
|
||||
<span class="diff-line-num gl-grow gl-pr-3">
|
||||
<gl-link
|
||||
v-gl-tooltip
|
||||
:href="`${fileUrl}#L${line.lineNumber}`"
|
||||
:title="__('View Line in repository')"
|
||||
:title="$options.i18n.viewLine"
|
||||
class="!gl-flex gl-items-center gl-justify-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 class="code highlight gl-grow" data-testid="search-blob-line-code">
|
||||
<code class="!gl-inline">
|
||||
<span v-safe-html="highlightedRichText(line.richText)" class="line"></span>
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { GlLink } from '@gitlab/ui';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import { s__ } from '~/locale';
|
||||
|
|
@ -8,6 +9,7 @@ export default {
|
|||
components: {
|
||||
FileIcon,
|
||||
ClipboardButton,
|
||||
GlLink,
|
||||
},
|
||||
props: {
|
||||
filePath: {
|
||||
|
|
@ -39,7 +41,7 @@ export default {
|
|||
<div class="file-header-content gl-flex gl-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">
|
||||
<gl-link :href="fileUrl" :title="$options.i18n.fileLink">
|
||||
<template v-if="projectPath">
|
||||
<strong class="project-path-content" data-testid="project-path-content"
|
||||
>{{ projectPath }}:
|
||||
|
|
@ -47,7 +49,7 @@ export default {
|
|||
</template>
|
||||
|
||||
<strong class="file-name-content" data-testid="file-name-content">{{ filePath }}</strong>
|
||||
</a>
|
||||
</gl-link>
|
||||
<clipboard-button
|
||||
:text="filePath"
|
||||
:gfm="gfmCopyText"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
<script>
|
||||
import { GlSprintf, GlLink } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState } from 'vuex';
|
||||
import { n__ } from '~/locale';
|
||||
import RefSelector from '~/ref/components/ref_selector.vue';
|
||||
import { REF_FIELD_NAME } from '~/search/results/constants';
|
||||
import { getBaseURL, setUrlParams, visitUrl } from '~/lib/utils/url_utility';
|
||||
|
||||
export default {
|
||||
name: 'GlobalSearchStatusBar',
|
||||
components: {
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
RefSelector,
|
||||
},
|
||||
props: {
|
||||
blobSearch: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasResults: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['query', 'groupInitialJson', 'projectInitialJson', 'repositoryRef']),
|
||||
currentPage() {
|
||||
return this.query.page ? parseInt(this.query.page, 10) : 1;
|
||||
},
|
||||
filesPerPage() {
|
||||
return this.blobSearch.perPage;
|
||||
},
|
||||
allFilesResults() {
|
||||
return this.blobSearch.fileCount;
|
||||
},
|
||||
showingFilesFrom() {
|
||||
return this.filesPerPage * (this.currentPage - 1) + 1;
|
||||
},
|
||||
showingFilesTo() {
|
||||
return Math.min(this.filesPerPage * this.currentPage, this.allFilesResults);
|
||||
},
|
||||
resultsTotal() {
|
||||
return this.blobSearch?.matchCount;
|
||||
},
|
||||
showBar() {
|
||||
return this.hasResults && !this.hasError && !this.isLoading;
|
||||
},
|
||||
getBaseURL() {
|
||||
return getBaseURL();
|
||||
},
|
||||
resultsSimple() {
|
||||
return n__(
|
||||
'GlobalSearch|Showing 1 code result for %{term}',
|
||||
'GlobalSearch|Showing %{resultsTotal} code results for %{term}',
|
||||
this?.resultsTotal ?? 0,
|
||||
);
|
||||
},
|
||||
statusGroup() {
|
||||
return n__(
|
||||
'GlobalSearch|Showing 1 code result for %{term} in group %{groupNameLink}',
|
||||
'GlobalSearch|Showing %{resultsTotal} code results for %{term} in group %{groupNameLink}',
|
||||
this?.resultsTotal ?? 0,
|
||||
);
|
||||
},
|
||||
statusProject() {
|
||||
return n__(
|
||||
'GlobalSearch|Showing 1 code result for %{term} in %{branchDropdown} of %{ProjectWithGroupPathLink}',
|
||||
'GlobalSearch|Showing %{resultsTotal} code results for %{term} in %{branchDropdown} of %{ProjectWithGroupPathLink}',
|
||||
this?.resultsTotal ?? 0,
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleInput(selected) {
|
||||
visitUrl(setUrlParams({ ...this.query, [REF_FIELD_NAME]: selected }));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="showBar" class="search-results-status gl-my-4">
|
||||
<gl-sprintf v-if="!query.project_id && !query.group_id" :message="resultsSimple">
|
||||
<template #resultsTotal>{{ resultsTotal }}</template>
|
||||
<template #term
|
||||
><code>{{ query.search }}</code></template
|
||||
>
|
||||
</gl-sprintf>
|
||||
|
||||
<gl-sprintf v-if="!query.project_id && query.group_id" :message="statusGroup">
|
||||
<template #resultsTotal>{{ resultsTotal }}</template>
|
||||
<template #term
|
||||
><code>{{ query.search }}</code></template
|
||||
><template #groupNameLink
|
||||
><gl-link :href="`${getBaseURL}/${groupInitialJson.name}`">{{
|
||||
groupInitialJson.full_name
|
||||
}}</gl-link></template
|
||||
>
|
||||
</gl-sprintf>
|
||||
|
||||
<gl-sprintf v-if="query.project_id" :message="statusProject">
|
||||
<template #resultsTotal>{{ resultsTotal }}</template>
|
||||
<template #term
|
||||
><code>{{ query.search }}</code></template
|
||||
>
|
||||
<template #branchDropdown>
|
||||
<ref-selector
|
||||
:project-id="String(projectInitialJson.id)"
|
||||
:value="repositoryRef"
|
||||
class="gl-inline"
|
||||
@input="handleInput"
|
||||
/>
|
||||
</template>
|
||||
<template #ProjectWithGroupPathLink
|
||||
><gl-link :href="`${getBaseURL}/${groupInitialJson.name}/${projectInitialJson.name}`">{{
|
||||
projectInitialJson.name_with_namespace
|
||||
}}</gl-link></template
|
||||
>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,90 +1,55 @@
|
|||
<script>
|
||||
import { GlLoadingIcon, GlCard, GlPagination } from '@gitlab/ui';
|
||||
import { GlCard, GlPagination, GlLoadingIcon } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState } from 'vuex';
|
||||
import { createAlert } from '~/alert';
|
||||
import { __, s__ } from '~/locale';
|
||||
import getBlobSearchQuery from '~/search/graphql/blob_search_zoekt.query.graphql';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
|
||||
import {
|
||||
DEFAULT_FETCH_CHUNKS,
|
||||
PROJECT_GRAPHQL_ID_TYPE,
|
||||
GROUP_GRAPHQL_ID_TYPE,
|
||||
SEARCH_RESULTS_DEBOUNCE,
|
||||
DEFAULT_SHOW_CHUNKS,
|
||||
} from '~/search/results/constants';
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
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';
|
||||
|
||||
import { DEFAULT_SHOW_CHUNKS } from '~/search/results/constants';
|
||||
|
||||
export default {
|
||||
name: 'ZoektBlobResults',
|
||||
components: {
|
||||
GlLoadingIcon,
|
||||
GlCard,
|
||||
BlobHeader,
|
||||
BlobFooter,
|
||||
BlobBody,
|
||||
GlPagination,
|
||||
EmptyResult,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
i18n: {
|
||||
headerText: __('Search results'),
|
||||
blobDataFetchError: s__(
|
||||
'GlobalSearch|Could not load search results. Please refresh the page to try again.',
|
||||
),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasError: false,
|
||||
blobSearch: [],
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
props: {
|
||||
blobSearch: {
|
||||
query() {
|
||||
return getBlobSearchQuery;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
search: this.query.search,
|
||||
groupId:
|
||||
this.query.group_id && convertToGraphQLId(GROUP_GRAPHQL_ID_TYPE, this.query.group_id),
|
||||
projectId:
|
||||
this.query.project_id &&
|
||||
convertToGraphQLId(PROJECT_GRAPHQL_ID_TYPE, this.query.project_id),
|
||||
page: this.query.page,
|
||||
chunkCount: DEFAULT_FETCH_CHUNKS,
|
||||
regex: this.query.regex ? JSON.parse(this.query.regex) : false,
|
||||
};
|
||||
},
|
||||
result({ data }) {
|
||||
this.blobSearch = data?.blobSearch;
|
||||
this.hasError = false;
|
||||
},
|
||||
debounce: SEARCH_RESULTS_DEBOUNCE,
|
||||
error(error) {
|
||||
this.hasError = true;
|
||||
createAlert({
|
||||
message: this.$options.i18n.blobDataFetchError,
|
||||
captureError: true,
|
||||
error,
|
||||
});
|
||||
},
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasResults: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['query']),
|
||||
isLoading() {
|
||||
return this.$apollo.queries.blobSearch.loading;
|
||||
pagination: {
|
||||
get() {
|
||||
return this.currentPage;
|
||||
},
|
||||
set(value) {
|
||||
this.setQuery({ key: 'page', value });
|
||||
},
|
||||
},
|
||||
hasResults() {
|
||||
return this.blobSearch?.files?.length > 0;
|
||||
currentPage() {
|
||||
return this.query.page ? parseInt(this.query.page, 10) : 1;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setQuery']),
|
||||
hasMore(file) {
|
||||
const showingMatches = file.chunks
|
||||
.slice(0, DEFAULT_SHOW_CHUNKS)
|
||||
|
|
@ -104,9 +69,9 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-flex gl-flex-col gl-justify-center">
|
||||
<gl-loading-icon v-if="isLoading" size="sm" />
|
||||
<div v-if="hasResults && !isLoading && !hasError" class="gl-relative">
|
||||
<div class="gl-flex gl-flex-col gl-justify-center" :class="{ 'gl-mt-5': isLoading }">
|
||||
<gl-loading-icon v-if="isLoading" :label="__('Loading')" size="md" variant="spinner" />
|
||||
<div v-if="hasResults && !isLoading" class="gl-relative">
|
||||
<gl-card
|
||||
v-for="file in blobSearch.files"
|
||||
:key="projectPathAndFilePath(file)"
|
||||
|
|
@ -134,9 +99,9 @@ export default {
|
|||
</gl-card>
|
||||
</div>
|
||||
<empty-result v-else-if="!hasResults && !isLoading" />
|
||||
<template v-if="hasResults && !isLoading && !hasError">
|
||||
<template v-if="hasResults && !isLoading">
|
||||
<gl-pagination
|
||||
v-model="query.page"
|
||||
v-model="pagination"
|
||||
class="gl-mx-auto"
|
||||
:per-page="blobSearch.perPage"
|
||||
:total-items="blobSearch.fileCount"
|
||||
|
|
|
|||
|
|
@ -3,3 +3,5 @@ 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;
|
||||
|
||||
export const REF_FIELD_NAME = 'repository_ref';
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export const setQuery = ({ state, commit, getters }, { key, value }) => {
|
|||
getters.currentScope === SCOPE_BLOB &&
|
||||
gon.features.zoektMultimatchFrontend
|
||||
) {
|
||||
const newUrl = setUrlParams({ ...state.query, page: null }, window.location.href, false, true);
|
||||
const newUrl = setUrlParams({ ...state.query }, window.location.href, false, true);
|
||||
updateHistory({ state: state.query, url: newUrl, replace: true });
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const createState = ({
|
|||
groupInitialJson,
|
||||
projectInitialJson,
|
||||
defaultBranchName,
|
||||
repositoryRef,
|
||||
}) => ({
|
||||
urlQuery: cloneDeep(query),
|
||||
query,
|
||||
|
|
@ -33,6 +34,7 @@ const createState = ({
|
|||
groupInitialJson,
|
||||
projectInitialJson,
|
||||
defaultBranchName,
|
||||
repositoryRef,
|
||||
});
|
||||
|
||||
export default createState;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
updateCacheAfterDeletingNote,
|
||||
} from '~/work_items/graphql/cache_utils';
|
||||
import { getLocationHash } from '~/lib/utils/url_utility';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { collapseSystemNotes } from '~/work_items/notes/collapse_utils';
|
||||
import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
|
||||
import WorkItemHistoryOnlyFilterNote from '~/work_items/components/notes/work_item_history_only_filter_note.vue';
|
||||
|
|
@ -284,6 +285,18 @@ export default {
|
|||
reportAbuse(isOpen, reply = {}) {
|
||||
this.$emit('openReportAbuse', reply);
|
||||
},
|
||||
noteId(note) {
|
||||
return getIdFromGraphQLId(note.id);
|
||||
},
|
||||
isHashTargeted(discussion) {
|
||||
return (
|
||||
discussion.notes.nodes.length &&
|
||||
discussion.notes.nodes.some((note) => this.targetNoteHash === `note_${this.noteId(note)}`)
|
||||
);
|
||||
},
|
||||
isDiscussionExpandedOnLoad(discussion) {
|
||||
return !this.isDiscussionResolved(discussion) || this.isHashTargeted(discussion);
|
||||
},
|
||||
isDiscussionResolved(discussion) {
|
||||
return discussion.notes.nodes[0]?.discussion?.resolved;
|
||||
},
|
||||
|
|
@ -390,7 +403,7 @@ export default {
|
|||
:can-set-work-item-metadata="canSetWorkItemMetadata"
|
||||
:is-discussion-locked="isDiscussionLocked"
|
||||
:is-work-item-confidential="isWorkItemConfidential"
|
||||
:is-expanded-on-load="!isDiscussionResolved(discussion)"
|
||||
:is-expanded-on-load="isDiscussionExpandedOnLoad(discussion)"
|
||||
@deleteNote="showDeleteNoteModal($event, discussion)"
|
||||
@reportAbuse="reportAbuse(true, $event)"
|
||||
@error="$emit('error', $event)"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
overflow-y: auto;
|
||||
transition: box-shadow ease-in-out 0.15s;
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
width: calc(100% - 6px);
|
||||
margin: 2px 3px;
|
||||
|
|
@ -304,6 +305,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark .ProseMirror {
|
||||
.suggestion-added-input,
|
||||
.suggestion-deleted {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
.board-swimlanes-headers {
|
||||
background-color: $white;
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
background-color: var(--gray-10);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
.branches-list .branch-item:not(:last-of-type) {
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
border-bottom-color: $gray-800;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
grid-template-columns: auto minmax(0, 1fr) #{$gl-spacing-scale-7};
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark .timeline-event-icon {
|
||||
background-color: $gray-950;
|
||||
color: $gl-text-secondary;
|
||||
|
|
@ -46,6 +47,7 @@
|
|||
height: calc(100% + #{$gl-spacing-scale-5});
|
||||
top: -#{$gl-spacing-scale-5};
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
border-color: var(--gray-50);
|
||||
}
|
||||
|
|
@ -88,6 +90,7 @@
|
|||
width: calc(100% - $gl-spacing-scale-8 - $gl-spacing-scale-5);
|
||||
bottom: $gl-spacing-scale-3;
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
border-color: var(--gray-50);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
|
||||
|
||||
body {
|
||||
&.with-system-header {
|
||||
padding-top: $system-header-height;
|
||||
}
|
||||
|
||||
|
||||
&.with-system-footer {
|
||||
.footer-container {
|
||||
padding-bottom: $system-footer-height;
|
||||
|
|
@ -40,6 +40,7 @@
|
|||
.sm-bg-gray {
|
||||
background-color: $gray-10;
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
background-color: var(--gray-100);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ $comparison-empty-state-height: 62px;
|
|||
border-bottom: 1px solid var(--gl-border-color-default);
|
||||
background-color: $white;
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
background-color: var(--gray-10);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,9 +96,3 @@
|
|||
margin-left: $gl-spacing-scale-5;
|
||||
}
|
||||
}
|
||||
|
||||
// Timeline avatars
|
||||
|
||||
.timeline-avatar {
|
||||
@apply gl-bg-default;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@
|
|||
.ci-job-item-failed {
|
||||
background-color: var(--red-50, $red-50);
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
background-color: var(--gray-200, $gray-200);
|
||||
}
|
||||
|
|
@ -309,6 +310,7 @@
|
|||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-badge.badge-muted {
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
@apply gl-bg-gray-100;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@
|
|||
}
|
||||
|
||||
.dark-mode-override {
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
background-color: $white;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
&.ui-neutral {
|
||||
background-color: $gray-50;
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
background-color: $gray-950;
|
||||
border: solid 1px $border-color;
|
||||
|
|
|
|||
|
|
@ -287,6 +287,7 @@ $disclosure-hierarchy-chevron-dimension: 1.2rem;
|
|||
width: 100%;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, $white 100%);
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
background: linear-gradient(180deg, rgba(31, 30, 36, 0.00) 0%, $gray-950 100%);
|
||||
}
|
||||
|
|
@ -296,6 +297,7 @@ $disclosure-hierarchy-chevron-dimension: 1.2rem;
|
|||
pointer-events: auto;
|
||||
background-color: $white;
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
background-color: $gray-950;
|
||||
}
|
||||
|
|
@ -306,6 +308,7 @@ $disclosure-hierarchy-chevron-dimension: 1.2rem;
|
|||
flex: 1;
|
||||
border-top: 1px solid $gray-50;
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
border-top: 1px solid $gray-900;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@
|
|||
margin: 2px;
|
||||
padding-left: calc(#{$gl-spacing-scale-5} - 2px);
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
width: calc(100% - 6px);
|
||||
margin: 2px 3px;
|
||||
|
|
@ -313,6 +314,7 @@ table {
|
|||
@apply gl-border-b;
|
||||
@apply gl-border-b-subtle;
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
@apply gl-bg-strong;
|
||||
border-bottom-color: var(--gl-background-color-default);
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
|
|||
.main-notes-list::before {
|
||||
background: var(--gray-50, $gray-50);
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark .modal-body & {
|
||||
background: var(--gray-100, $gray-100);
|
||||
}
|
||||
|
|
@ -43,10 +44,12 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
|
|||
.timeline-entry:not(.draft-note):last-child::before {
|
||||
background: var(--white);
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
background: var(--gray-10);
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark .modal-body & {
|
||||
background: var(--gray-50, $gray-50);
|
||||
}
|
||||
|
|
@ -98,6 +101,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
|
|||
border-radius: $gl-border-radius-base;
|
||||
padding: $gl-padding-4 $gl-padding-8;
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
@apply gl-bg-strong;
|
||||
border-color: var(--gl-background-color-default);
|
||||
|
|
@ -136,6 +140,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
|
|||
border-top-right-radius: $gl-border-radius-base;
|
||||
padding: $gl-padding-4 $gl-padding-8;
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
@apply gl-bg-strong;
|
||||
border-bottom-color: var(--gl-background-color-default);
|
||||
|
|
@ -197,6 +202,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
|
|||
@apply gl-border-b;
|
||||
@apply gl-border-b-subtle;
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
@apply gl-bg-strong;
|
||||
border-bottom-color: var(--gl-background-color-default);
|
||||
|
|
@ -585,6 +591,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
|
|||
.discussion-notes .timeline-entry:first-of-type > .timeline-entry-inner {
|
||||
@apply gl-bg-default;
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
@apply gl-bg-strong;
|
||||
border-bottom-color: var(--gl-background-color-default);
|
||||
|
|
@ -766,6 +773,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
|
|||
padding: $gl-padding-8 !important;
|
||||
@include gl-border;
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
@apply gl-bg-strong;
|
||||
@apply gl-border-b;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
&.development {
|
||||
background-color: $perf-bar-development;
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-dark & {
|
||||
background-color: $red-950;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
.gl-w-full.gl-grow
|
||||
= render partial: 'search/results_status'
|
||||
= render partial: 'search/results_status' unless should_show_zoekt_results?(@scope, @search_type)
|
||||
= render partial: 'search/results_list'
|
||||
|
|
|
|||
|
|
@ -18,6 +18,6 @@
|
|||
|
||||
#js-search-topbar{ data: { "default-branch-name": @project&.default_branch } }
|
||||
.results.lg:gl-flex.gl-mt-0
|
||||
#js-search-sidebar{ data: { navigation_json: search_navigation_json, search_type: search_service.search_type, search_level: search_service.level, group_initial_json: group_attributes.to_json, project_initial_json: project_attributes.to_json, } }
|
||||
#js-search-sidebar{ data: { navigation_json: search_navigation_json, search_type: search_service.search_type, search_level: search_service.level, group_initial_json: group_attributes.to_json, project_initial_json: project_attributes.to_json, ref: @project.present? ? repository_ref(@project) : nil } }
|
||||
- if @search_term
|
||||
= render 'search/results'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
description: "Selec stream from dropdown"
|
||||
description: "Select stream from dropdown"
|
||||
category: default
|
||||
action: click_dropdown
|
||||
extra_properties:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
description: A user is created
|
||||
action: create_user
|
||||
identifiers:
|
||||
- user
|
||||
additional_properties:
|
||||
label:
|
||||
description: "'signup' or 'invited'"
|
||||
product_group: acquisition
|
||||
milestone: '15.8'
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108508
|
||||
distributions:
|
||||
- ce
|
||||
- ee
|
||||
tiers:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: ci_conditionals_reduce_gitaly_calls
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/472223
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162741
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/479151
|
||||
milestone: '17.4'
|
||||
group: group::security policies
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -470,6 +470,19 @@ vulnerability_export_parts:
|
|||
- table: organizations
|
||||
column: organization_id
|
||||
on_delete: async_delete
|
||||
vulnerability_exports:
|
||||
- table: organizations
|
||||
column: organization_id
|
||||
on_delete: async_delete
|
||||
- table: namespaces
|
||||
column: group_id
|
||||
on_delete: async_delete
|
||||
- table: projects
|
||||
column: project_id
|
||||
on_delete: async_delete
|
||||
- table: users
|
||||
column: author_id
|
||||
on_delete: async_delete
|
||||
vulnerability_feedback:
|
||||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddCompositeIndexToSubscriptionAddOnPurchases < Gitlab::Database::Migration[2.2]
|
||||
disable_ddl_transaction!
|
||||
milestone '17.4'
|
||||
|
||||
INDEX_NAME = 'index_subscription_add_on_purchases_on_namespace_id_add_on_id'
|
||||
|
||||
def up
|
||||
add_concurrent_index :subscription_add_on_purchases,
|
||||
[:namespace_id, :subscription_add_on_id],
|
||||
name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index :subscription_add_on_purchases,
|
||||
[:namespace_id, :subscription_add_on_id],
|
||||
name: INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveNamespaceIdIndexFromSubscriptionAddOnPurchases < Gitlab::Database::Migration[2.2]
|
||||
disable_ddl_transaction!
|
||||
milestone '17.4'
|
||||
|
||||
INDEX_NAME = 'index_subscription_add_on_purchases_on_namespace_id'
|
||||
|
||||
def up
|
||||
remove_concurrent_index :subscription_add_on_purchases, :namespace_id, name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :subscription_add_on_purchases, :namespace_id, name: INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveSubscriptionAddOnIdIndexFromSubscriptionAddOnPurchases < Gitlab::Database::Migration[2.2]
|
||||
disable_ddl_transaction!
|
||||
milestone '17.4'
|
||||
|
||||
INDEX_NAME = 'index_subscription_add_on_purchases_on_subscription_add_on_id'
|
||||
|
||||
def up
|
||||
remove_concurrent_index :subscription_add_on_purchases, :subscription_add_on_id,
|
||||
name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :subscription_add_on_purchases, :subscription_add_on_id,
|
||||
name: INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveOrganizationsVulnerabilityExportsOrganizationIdFk < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.4'
|
||||
disable_ddl_transaction!
|
||||
|
||||
FOREIGN_KEY_NAME = "fk_90e75ccdf8"
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists(:vulnerability_exports, :organizations,
|
||||
name: FOREIGN_KEY_NAME, reverse_lock_order: true)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_foreign_key(:vulnerability_exports, :organizations,
|
||||
name: FOREIGN_KEY_NAME, column: :organization_id,
|
||||
target_column: :id, on_delete: :cascade)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveNamespacesVulnerabilityExportsGroupIdFk < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.4'
|
||||
disable_ddl_transaction!
|
||||
|
||||
FOREIGN_KEY_NAME = "fk_c3d3cb5d0f"
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists(:vulnerability_exports, :namespaces,
|
||||
name: FOREIGN_KEY_NAME, reverse_lock_order: true)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_foreign_key(:vulnerability_exports, :namespaces,
|
||||
name: FOREIGN_KEY_NAME, column: :group_id,
|
||||
target_column: :id, on_delete: :cascade)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveUsersVulnerabilityExportsAuthorIdFk < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.4'
|
||||
disable_ddl_transaction!
|
||||
|
||||
FOREIGN_KEY_NAME = "fk_rails_1019162882"
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists(:vulnerability_exports, :users,
|
||||
name: FOREIGN_KEY_NAME, reverse_lock_order: true)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_foreign_key(:vulnerability_exports, :users,
|
||||
name: FOREIGN_KEY_NAME, column: :author_id,
|
||||
target_column: :id, on_delete: :cascade)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveProjectsVulnerabilityExportsProjectIdFk < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.4'
|
||||
disable_ddl_transaction!
|
||||
|
||||
FOREIGN_KEY_NAME = "fk_rails_9aff2c3b45"
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists(:vulnerability_exports, :projects,
|
||||
name: FOREIGN_KEY_NAME, reverse_lock_order: true)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_foreign_key(:vulnerability_exports, :projects,
|
||||
name: FOREIGN_KEY_NAME, column: :project_id,
|
||||
target_column: :id, on_delete: :cascade)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
b19bdab41864682ccef9b4cbde289e3c4bd5a924286bd8282df5559686ac414b
|
||||
|
|
@ -0,0 +1 @@
|
|||
ad1958f4ade8d13c64d19894bd0115a357d7322443fb95acbcfc0c582611afc5
|
||||
|
|
@ -0,0 +1 @@
|
|||
4bebe3172ceb8fcb39dbbf722bc8eb841eb73ca6e409979f325acc54e6672cf4
|
||||
|
|
@ -0,0 +1 @@
|
|||
42f405277c2e1b5c4ea935ddcfe20d550a135bf9bbfc5ef6174e531b3b04e540
|
||||
|
|
@ -0,0 +1 @@
|
|||
df9e7c4f161cdc7bb359d9cfa36bf3117c28d950481f69be90c80ffdf1f3c96d
|
||||
|
|
@ -0,0 +1 @@
|
|||
4ca6cc8ccecbdec8dae4af9af5099e782a69a3b2e92e806e04c7fad8e3a26e70
|
||||
|
|
@ -0,0 +1 @@
|
|||
02b4b6ab225fc477382c66434dba8ffda9deede17f610361f7d34ab5c135622e
|
||||
|
|
@ -30202,9 +30202,7 @@ CREATE UNIQUE INDEX index_status_page_published_incidents_on_issue_id ON status_
|
|||
|
||||
CREATE INDEX index_status_page_settings_on_project_id ON status_page_settings USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_subscription_add_on_purchases_on_namespace_id ON subscription_add_on_purchases USING btree (namespace_id);
|
||||
|
||||
CREATE INDEX index_subscription_add_on_purchases_on_subscription_add_on_id ON subscription_add_on_purchases USING btree (subscription_add_on_id);
|
||||
CREATE INDEX index_subscription_add_on_purchases_on_namespace_id_add_on_id ON subscription_add_on_purchases USING btree (namespace_id, subscription_add_on_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_subscription_add_ons_on_name ON subscription_add_ons USING btree (name);
|
||||
|
||||
|
|
@ -33792,9 +33790,6 @@ ALTER TABLE ONLY audit_events_streaming_group_namespace_filters
|
|||
ALTER TABLE ONLY compliance_requirements
|
||||
ADD CONSTRAINT fk_8f5fb77fc7 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY vulnerability_exports
|
||||
ADD CONSTRAINT fk_90e75ccdf8 FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY todos
|
||||
ADD CONSTRAINT fk_91d1f47b13 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
@ -34125,9 +34120,6 @@ ALTER TABLE ONLY issues
|
|||
ALTER TABLE ONLY user_group_callouts
|
||||
ADD CONSTRAINT fk_c366e12ec3 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY vulnerability_exports
|
||||
ADD CONSTRAINT fk_c3d3cb5d0f FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY timelogs
|
||||
ADD CONSTRAINT fk_c49c83dd77 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
@ -34665,9 +34657,6 @@ ALTER TABLE ONLY issue_email_participants
|
|||
ALTER TABLE ONLY merge_request_context_commits
|
||||
ADD CONSTRAINT fk_rails_0fe0039f60 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY vulnerability_exports
|
||||
ADD CONSTRAINT fk_rails_1019162882 FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY prometheus_alert_events
|
||||
ADD CONSTRAINT fk_rails_106f901176 FOREIGN KEY (prometheus_alert_id) REFERENCES prometheus_alerts(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
@ -35709,9 +35698,6 @@ ALTER TABLE ONLY pages_deployments
|
|||
ALTER TABLE ONLY dast_pre_scan_verification_steps
|
||||
ADD CONSTRAINT fk_rails_9990fc2adf FOREIGN KEY (dast_pre_scan_verification_id) REFERENCES dast_pre_scan_verifications(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY vulnerability_exports
|
||||
ADD CONSTRAINT fk_rails_9aff2c3b45 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY users_ops_dashboard_projects
|
||||
ADD CONSTRAINT fk_rails_9b4ebf005b FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ We aim to employ the Chat for all use cases and workflows that can benefit from
|
|||
- Among the latter are tasks where the **AI may not get it right the first time but** where **users can easily course correct** by telling the AI more precisely what they need. For instance, "Explain this code" is a common question that most of the time would result in a satisfying answer, but sometimes the user may have additional questions.
|
||||
- **Tasks that benefit from the history of a conversation**, so neither the user nor the AI need to repeat themselves.
|
||||
|
||||
The chat aims to be context aware and ultimately have access to all the resources in GitLab that the user has access to. Initially, this context was limited to the content of individual issues and epics, as well as GitLab documentation. Since then additional contexts have been added, such as code selection and code files. Currently, work is underway contributing vulnerability context and pipeline job context, so that users can ask questions about these contexts.
|
||||
Chat aims to be context aware and ultimately have access to all the resources in GitLab that the user has access to. Initially, this context was limited to the content of individual issues and epics, as well as GitLab documentation. Since then additional contexts have been added, such as code selection and code files. Currently, work is underway contributing vulnerability context and pipeline job context, so that users can ask questions about these contexts.
|
||||
|
||||
To scale the context awareness and hence to scale creation, ideation, and learning use cases across the entire DevSecOps domain, the Duo Chat team welcomes contributions to the chat platform from other GitLab teams and the wider community. They are the experts for the use cases and workflows to accelerate.
|
||||
To scale the context awareness and hence to scale creation, ideation, and learning use cases across the entire DevSecOps domain, the Duo Chat team welcomes contributions to the Chat platform from other GitLab teams and the wider community. They are the experts for the use cases and workflows to accelerate.
|
||||
|
||||
### Which use cases are better implemented as stand-alone AI features?
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ message writing workflow.
|
|||
|
||||
Using Chat for commit message writing would probably take longer than writing the message oneself. The user would have to switch to the Chat window, type the request and then copy the result into the commit message field.
|
||||
|
||||
That said, it does not mean that Chat can't write commit messages, nor that it would be prevented from doing so. If Chat has the commit context (which may be added at some point for reasons other than commit message writing), the user can certainly ask to do anything with this commit content, including writing a commit message. But users are certainly unlikely to do that with Chat as they would only loose time. Note: the resulting commit messages may be different if created from chat with a prompt written by the user vs. a static prompt behind a purpose-built commit message creation.
|
||||
That said, it does not mean that Chat can't write commit messages, nor that it would be prevented from doing so. If Chat has the commit context (which may be added at some point for reasons other than commit message writing), the user can certainly ask to do anything with this commit content, including writing a commit message. But users are certainly unlikely to do that with Chat as they would only loose time. Note: the resulting commit messages may be different if created from Chat with a prompt written by the user vs. a static prompt behind a purpose-built commit message creation.
|
||||
|
||||
## Set up GitLab Duo Chat
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ you find a solution.
|
|||
| There is no Chat button in the GitLab UI. | Make sure your user is a part of a group with Premium or Ultimate license and enabled Chat. |
|
||||
| Chat replies with "Forbidden by auth provider" error. | Backend can't access LLMs. Make sure your [AI Gateway](index.md#required-install-ai-gateway) is set up correctly. |
|
||||
| Requests take too long to appear in UI | Consider restarting Sidekiq by running `gdk restart rails-background-jobs`. If that doesn't work, try `gdk kill` and then `gdk start`. Alternatively, you can bypass Sidekiq entirely. To do that temporary alter `Llm::CompletionWorker.perform_async` statements with `Llm::CompletionWorker.perform_inline` |
|
||||
| There is no chat button in GitLab UI when GDK is running on non-SaaS mode | You do not have cloud connector access token record or seat assigned. To create cloud connector access record, in rails console put following code: `CloudConnector::Access.new(data: { available_services: [{ name: "duo_chat", serviceStartTime: ":date_in_the_future" }] }).save`. |
|
||||
| There is no Chat button in GitLab UI when GDK is running on non-SaaS mode | You do not have cloud connector access token record or seat assigned. To create cloud connector access record, in rails console put following code: `CloudConnector::Access.new(data: { available_services: [{ name: "duo_chat", serviceStartTime: ":date_in_the_future" }] }).save`. |
|
||||
|
||||
Please, see also the section on [error codes](#interpreting-gitlab-duo-chat-error-codes) where you can read about codes
|
||||
that Chat sends to assist troubleshooting.
|
||||
|
|
@ -266,7 +266,7 @@ It's not available in Production environment.
|
|||
project will be created during request.
|
||||
|
||||
1. Restart GDK.
|
||||
1. Ask any question to chat.
|
||||
1. Ask any question to Chat.
|
||||
1. Observe project in the LangSmith [page](https://smith.langchain.com/) > Projects > \[Project name\]. 'Runs' tab should contain
|
||||
your last requests.
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ to AI that you think could benefit from being in this list, add it!
|
|||
`embeddings` database. The embeddings search is done in Postgres using the
|
||||
`vector` extension. The vertex embeddings database is updated based on the
|
||||
latest version of GitLab documentation on a daily basis by running `Llm::Embedding::GitlabDocumentation::CreateEmbeddingsRecordsWorker` as a cronjob.
|
||||
- **Fine Tuning**: Altering an existing model using a supervised learning process that utilizes a dataset of labeled examples to update the weights of the LLM, improving its output for specific tasks such as code completion or chat.
|
||||
- **Fine Tuning**: Altering an existing model using a supervised learning process that utilizes a dataset of labeled examples to update the weights of the LLM, improving its output for specific tasks such as code completion or Chat.
|
||||
**Foundational Model**: A general purpose LLM trained using a generic objective, typically next token prediction. These models are capable and flexible, and can be adjusted to solved many domain-specific tasks (through finetuning or prompt engineering). This means that these general purpose models are ideal to serve as the foundation of many downstream models. Examples of foundational models are: GPT-4o, Claude 3.5 Sonnet.
|
||||
- **Frozen Model**: A LLM which cannot be fine-tuned (also Frozen LLM).
|
||||
- **GitLab Duo**: AI-assisted features across the GitLab DevSecOps platform. These features aim to help increase velocity and solve key pain points across the software development lifecycle. See also the [GitLab Duo](../../user/ai_features.md) features page.
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ subscription aiCompletionResponse(
|
|||
}
|
||||
```
|
||||
|
||||
The [subscription for chat](duo_chat.md#graphql-subscription) behaves differently.
|
||||
The [subscription for Chat](duo_chat.md#graphql-subscription) behaves differently.
|
||||
|
||||
To not have many concurrent subscriptions, you should also only subscribe to the subscription once the mutation is sent by using [`skip()`](https://apollo.vuejs.org/guide-option/subscriptions.html#skipping-the-subscription).
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ This should enable everyone to see locally any change in an IDE being sent to th
|
|||
1. If you'd like to test that Code Suggestions is working from inside the VS Code Extension, then follow the [steps to set up a personal access token](https://gitlab.com/gitlab-org/gitlab-vscode-extension/#setup) with your GDK inside the new window of VS Code that pops up when you run the "Run and Debug" command.
|
||||
- Once you complete the steps below, to test you are hitting your local `/code_suggestions/completions` endpoint and not production, follow these steps:
|
||||
1. Inside the new window, in the built in terminal select the "Output" tab then "GitLab Language Server" from the drop down menu on the right.
|
||||
1. Open a new file inside of this VS Code window and begin typing to see code suggestions in action.
|
||||
1. Open a new file inside of this VS Code window and begin typing to see Code Suggestions in action.
|
||||
1. You will see completion request URLs being fetched that match the Git remote URL for your GDK.
|
||||
|
||||
1. Main Application (GDK):
|
||||
|
|
@ -35,7 +35,7 @@ This should enable everyone to see locally any change in an IDE being sent to th
|
|||
1. Enable Feature Flag ```ai_duo_code_suggestions_switch```:
|
||||
1. In your terminal, go to your `gitlab-development-kit` > `gitlab` directory.
|
||||
1. Run `gdk rails console` or `bundle exec rails c` to start a Rails console.
|
||||
1. [Enable the Feature Flag](../../administration/feature_flags.md#enable-or-disable-the-feature) for the code suggestions tokens API by calling `Feature.enable(:ai_duo_code_suggestions_switch)` from the console.
|
||||
1. [Enable the Feature Flag](../../administration/feature_flags.md#enable-or-disable-the-feature) for the Code Suggestions tokens API by calling `Feature.enable(:ai_duo_code_suggestions_switch)` from the console.
|
||||
1. [Setup AI Gateway](../ai_features/index.md#required-install-ai-gateway).
|
||||
1. Run your GDK server with `gdk start` if it's not already running.
|
||||
|
||||
|
|
|
|||
|
|
@ -127,9 +127,32 @@ back into Sidekiq to be retried.
|
|||
|
||||
## Define an event
|
||||
|
||||
An `Event` object represents a domain event that occurred in a bounded context.
|
||||
Notify other bounded contexts about something
|
||||
that happened by publishing events, so that they can react to it.
|
||||
An `Event` object represents a domain event that occurred in a [bounded context](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/bounded_contexts.yml).
|
||||
Producers can notify other bounded contexts about something that happened by publishing events, so that they can react to it. An event should be named `<domain_object><action>Event`, where the `action` is in past tense, e.g. `ReviewerAddedEvent` instead of `AddReviewerEvent`. The `domain_object` may be elided when it is obvious based on the bounded context, e.g. `MergeRequest::ApprovedEvent` instead of `MergeRequest::MergeRequestApprovedEvent`.
|
||||
|
||||
### Guidance for good events
|
||||
|
||||
Events are a public interface, just like an API or a UI. Collaborate with your
|
||||
product and design counterparts to ensure new events will address the needs of
|
||||
subscribers. Whenever possible, new events should strive to meet the following
|
||||
principles:
|
||||
|
||||
- **Semantic**: Events should describe what occurred within the bounded context, _not_ the intended
|
||||
action for subscribers.
|
||||
- **Specific**: Events should be narrowly defined without being overly precise. This minimizes the
|
||||
amount of event filtering that subscribers have to perform, as well as the number of unique events
|
||||
to which they need to subscribe. Consider using properties to communicate additional information.
|
||||
- **Scoped**: Events should be scoped to their bounded context. Avoid publishing events about domain objects that are not contained by your bounded context.
|
||||
|
||||
#### Examples
|
||||
|
||||
| Principle | Good | Bad |
|
||||
| --- | --- | --- |
|
||||
| Semantic | `MergeRequest::ApprovedEvent` | `MergeRequest::NotifyAuthorEvent` |
|
||||
| Specific | `MergeRequest::ReviewerAddedEvent` | • `MergeRequest::ChangedEvent` <br> • `MergeRequest::CodeownerAddedAsReviewerEvent` |
|
||||
| Scoped | `MergeRequest::CreatedEvent` | `Project::MergeRequestCreatedEvent` |
|
||||
|
||||
### Creating the event schema
|
||||
|
||||
Define new event classes under `app/events/<namespace>/` with a name representing something that happened in the past:
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ This extension provides these custom commands:
|
|||
|
||||
| Command name | Default keyboard shortcut | Feature |
|
||||
|--------------------------------|---------------------------|---------|
|
||||
| `GitLab.ToggleCodeSuggestions` | not applicable | Enable or disable automated code suggestions. |
|
||||
| `GitLab.ToggleCodeSuggestions` | not applicable | Enable or disable automated Code Suggestions. |
|
||||
|
||||
You can access the extension's custom commands with keyboard shortcuts, which you can customize:
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ DETAILS:
|
|||
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://youtu.be/ds7SG1wgcVM)
|
||||
- [View documentation](../project/repository/code_suggestions/index.md).
|
||||
|
||||
### Code explanation
|
||||
### Code Explanation
|
||||
|
||||
DETAILS:
|
||||
**Tier: GitLab.com and Self-managed:** For a limited time, Premium or Ultimate. In the future, Premium with GitLab Duo Pro or Ultimate with [GitLab Duo Pro or Enterprise](../../subscriptions/subscription-add-ons.md). **GitLab Dedicated:** GitLab Duo Pro or Enterprise.
|
||||
|
|
@ -121,7 +121,7 @@ DETAILS:
|
|||
- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=MMVFvGrmMzw&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW)
|
||||
- [View documentation](../application_security/vulnerabilities/index.md#explaining-a-vulnerability).
|
||||
|
||||
### AI Impact dashboard
|
||||
### AI Impact Dashboard
|
||||
|
||||
DETAILS:
|
||||
**Tier:** For a limited time, Ultimate. In the future, Ultimate with [GitLab Duo Enterprise](../../subscriptions/subscription-add-ons.md).
|
||||
|
|
@ -173,7 +173,7 @@ DETAILS:
|
|||
|
||||
## Experimental features
|
||||
|
||||
### Issue description generation
|
||||
### Issue Description Generation
|
||||
|
||||
DETAILS:
|
||||
**Tier:** For a limited time, Ultimate. In the future, Ultimate with [GitLab Duo Enterprise](../../subscriptions/subscription-add-ons.md).
|
||||
|
|
@ -184,7 +184,7 @@ DETAILS:
|
|||
- LLM: Anthropic [Claude Instant 1.2](https://docs.anthropic.com/en/docs/about-claude/models#legacy-models)
|
||||
- [View documentation](experiments.md#populate-an-issue-with-issue-description-generation).
|
||||
|
||||
### Code review summary
|
||||
### Code Review Summary
|
||||
|
||||
DETAILS:
|
||||
**Tier:** For a limited time, Ultimate. In the future, Ultimate with [GitLab Duo Enterprise](../../subscriptions/subscription-add-ons.md).
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ Explain labels in GitLab. Provide an example for efficient usage with issue boar
|
|||
|
||||
## Reset when needed
|
||||
|
||||
Use `/reset` if chat gets stuck on a wrong track. Start fresh.
|
||||
Use `/reset` if Chat gets stuck on a wrong track. Start fresh.
|
||||
|
||||
## Refine slash command prompts
|
||||
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ Project permissions for [merge requests](project/merge_requests/index.md):
|
|||
|--------------------------------------------------------------------------------------------------------------|:-----:|:--------:|:---------:|:----------:|:-----:|-------|
|
||||
| [View](project/merge_requests/index.md#view-merge-requests) a merge request | ✓ | ✓ | ✓ | ✓ | ✓ | On self-managed GitLab instances, users with the Guest role are able to perform this action only on public and internal projects (not on private projects). [External users](../administration/external_users.md) must be given explicit access (at least the **Reporter** role) even if the project is internal. Users with the Guest role on GitLab.com are only able to perform this action on public projects because internal visibility is not available. |
|
||||
| [Create](project/merge_requests/creating_merge_requests.md) a merge request | | | ✓ | ✓ | ✓ | In projects that accept contributions from external members, users can create, edit, and close their own merge requests. For **private** projects, this excludes the Guest role as those users [cannot clone private projects](public_access.md#private-projects-and-groups). For **internal** projects, includes users with read-only access to the project, as [they can clone internal projects](public_access.md#internal-projects-and-groups). |
|
||||
| Update a merge request including assign, review, code suggestions, approve, labels, lock and resolve threads | | | ✓ | ✓ | ✓ | For information on eligible approvers for merge requests, see [Eligible approvers](project/merge_requests/approvals/rules.md#eligible-approvers). |
|
||||
| Update a merge request including assign, review, Code Suggestions, approve, labels, lock and resolve threads | | | ✓ | ✓ | ✓ | For information on eligible approvers for merge requests, see [Eligible approvers](project/merge_requests/approvals/rules.md#eligible-approvers). |
|
||||
| Manage [merge request settings](project/merge_requests/approvals/settings.md) | | | | ✓ | ✓ | |
|
||||
| Manage [merge request approval rules](project/merge_requests/approvals/rules.md) | | | | ✓ | ✓ | |
|
||||
| Delete merge request | | | | | ✓ | |
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ DETAILS:
|
|||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10466) in GitLab 16.0 as an [experiment](../../../policy/experiment-beta-support.md#experiment).
|
||||
|
||||
When you've completed your review of a merge request and are ready to [submit your review](reviews/index.md#submit-a-review), generate a GitLab Duo Code review summary:
|
||||
When you've completed your review of a merge request and are ready to [submit your review](reviews/index.md#submit-a-review), generate a GitLab Duo Code Review Summary:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Select **Code > Merge requests** and find the merge request you want to review.
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ Prerequisites:
|
|||
what you want to build. Code Generation treats your code comments like chat. Your code comments
|
||||
update the `user_instruction`, and then improve the next results you receive.
|
||||
|
||||
As you work, GitLab Duo provides code suggestions that use your other open files
|
||||
As you work, GitLab Duo provides Code Suggestions that use your other open files
|
||||
(within [truncation limits](#truncation-of-file-content))
|
||||
as extra context.
|
||||
|
||||
|
|
|
|||
|
|
@ -32,12 +32,16 @@ module Gitlab
|
|||
end
|
||||
|
||||
def top_level_worktree_paths
|
||||
return pipeline.top_level_worktree_paths if reduce_gitaly_calls?
|
||||
|
||||
strong_memoize(:top_level_worktree_paths) do
|
||||
project.repository.tree(sha).blobs.map(&:path)
|
||||
end
|
||||
end
|
||||
|
||||
def all_worktree_paths
|
||||
return pipeline.all_worktree_paths if reduce_gitaly_calls?
|
||||
|
||||
strong_memoize(:all_worktree_paths) do
|
||||
project.repository.ls_files(sha)
|
||||
end
|
||||
|
|
@ -56,6 +60,10 @@ module Gitlab
|
|||
protected: pipeline.protected_ref?
|
||||
}
|
||||
end
|
||||
|
||||
def reduce_gitaly_calls?
|
||||
Feature.enabled?(:ci_conditionals_reduce_gitaly_calls, project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24828,7 +24828,7 @@ msgstr ""
|
|||
msgid "GlobalSearch|Commits"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Could not load search results. Please refresh the page to try again."
|
||||
msgid "GlobalSearch|Could not load search results. Refresh the page to try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Explore"
|
||||
|
|
@ -25020,6 +25020,21 @@ msgstr ""
|
|||
msgid "GlobalSearch|Show more"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Showing 1 code result for %{term}"
|
||||
msgid_plural "GlobalSearch|Showing %{resultsTotal} code results for %{term}"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "GlobalSearch|Showing 1 code result for %{term} in %{branchDropdown} of %{ProjectWithGroupPathLink}"
|
||||
msgid_plural "GlobalSearch|Showing %{resultsTotal} code results for %{term} in %{branchDropdown} of %{ProjectWithGroupPathLink}"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "GlobalSearch|Showing 1 code result for %{term} in group %{groupNameLink}"
|
||||
msgid_plural "GlobalSearch|Showing %{resultsTotal} code results for %{term} in group %{groupNameLink}"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "GlobalSearch|Showing top %{maxItems}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -25059,6 +25074,12 @@ msgstr ""
|
|||
msgid "GlobalSearch|Users"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|View blame"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|View line in repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|View syntax options."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -59391,9 +59412,6 @@ msgstr ""
|
|||
msgid "View File Metadata"
|
||||
msgstr ""
|
||||
|
||||
msgid "View Line in repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "View Stage: %{title}"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ RSpec.describe 'Database schema', feature_category: :database do
|
|||
search_namespace_index_assignments: [%w[search_index_id index_type]],
|
||||
slack_integrations_scopes: [%w[slack_api_scope_id]],
|
||||
snippets: %w[organization_id], # this index is added in an async manner, hence it needs to be ignored in the first phase.
|
||||
users: [%w[accepted_term_id]]
|
||||
users: [%w[accepted_term_id]],
|
||||
subscription_add_on_purchases: [["subscription_add_on_id"]] # index handled via composite index with namespace_id
|
||||
}.with_indifferent_access.freeze
|
||||
|
||||
# If splitting FK and table removal into two MRs as suggested in the docs, use this constant in the initial FK removal MR.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import createState from '~/badges/store/state';
|
|||
import mutations from '~/badges/store/mutations';
|
||||
import actions from '~/badges/store/actions';
|
||||
import BadgeList from '~/badges/components/badge_list.vue';
|
||||
import Badge from '~/badges/components/badge.vue';
|
||||
import { createDummyBadge } from '../dummy_badge';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
|
@ -27,6 +28,7 @@ describe('BadgeList component', () => {
|
|||
const findButtons = () => wrapper.findByTestId('badge-actions').findAllComponents(GlButton);
|
||||
const findEditButton = () => wrapper.findByTestId('edit-badge-button');
|
||||
const findDeleteButton = () => wrapper.findByTestId('delete-badge');
|
||||
const findBadge = () => wrapper.findComponent(Badge);
|
||||
|
||||
const createComponent = (customState) => {
|
||||
mockedActions = Object.fromEntries(Object.keys(actions).map((name) => [name, jest.fn()]));
|
||||
|
|
@ -100,10 +102,7 @@ describe('BadgeList component', () => {
|
|||
});
|
||||
|
||||
it('renders the badge', () => {
|
||||
const badgeImage = wrapper.find('.project-badge');
|
||||
|
||||
expect(badgeImage.exists()).toBe(true);
|
||||
expect(badgeImage.attributes('src')).toBe(badges[0].renderedImageUrl);
|
||||
expect(findBadge().props('imageUrl')).toBe(badges[0].renderedImageUrl);
|
||||
});
|
||||
|
||||
it('renders the badge name', () => {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
|
|||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
|
||||
import BoardCardInner from '~/boards/components/board_card_inner.vue';
|
||||
import isShowingLabelsQuery from '~/graphql_shared/client/is_showing_labels.query.graphql';
|
||||
|
|
@ -48,6 +49,7 @@ describe('Board card component', () => {
|
|||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findHiddenIssueIcon = () => wrapper.findByTestId('hidden-icon');
|
||||
const findWorkItemIcon = () => wrapper.findComponent(WorkItemTypeIcon);
|
||||
const findUserAvatar = () => wrapper.findComponent(UserAvatarLink);
|
||||
|
||||
const mockApollo = createMockApollo();
|
||||
|
||||
|
|
@ -250,9 +252,6 @@ describe('Board card component', () => {
|
|||
item: {
|
||||
...wrapper.props('item'),
|
||||
assignees: [user],
|
||||
updateData(newData) {
|
||||
Object.assign(this, newData);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -274,25 +273,25 @@ describe('Board card component', () => {
|
|||
expect(wrapper.find('.board-card-assignee img').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the avatar using avatarUrl property', async () => {
|
||||
wrapper.props('item').updateData({
|
||||
...wrapper.props('item'),
|
||||
assignees: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'test',
|
||||
state: 'active',
|
||||
username: 'test_name',
|
||||
avatarUrl: 'test_image_from_avatar_url',
|
||||
it('renders the avatar using avatarUrl property', () => {
|
||||
createWrapper({
|
||||
props: {
|
||||
item: {
|
||||
...wrapper.props('item'),
|
||||
assignees: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'test',
|
||||
state: 'active',
|
||||
username: 'test_name',
|
||||
avatarUrl: 'test_image_from_avatar_url',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe(
|
||||
'test_image_from_avatar_url?width=48',
|
||||
);
|
||||
expect(findUserAvatar().props('imgSrc')).toBe('test_image_from_avatar_url');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -317,10 +316,7 @@ describe('Board card component', () => {
|
|||
});
|
||||
|
||||
it('displays defaults avatar if users avatar is null', () => {
|
||||
expect(wrapper.find('.board-card-assignee img').exists()).toBe(true);
|
||||
expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe(
|
||||
'default_avatar?width=48',
|
||||
);
|
||||
expect(findUserAvatar().props('imgSrc')).toBe('default_avatar');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import PipelinesCiTemplates from '~/ci/pipelines_page/components/empty_state/pip
|
|||
describe('Pipelines Empty State', () => {
|
||||
let wrapper;
|
||||
|
||||
const findIllustration = () => wrapper.find('img');
|
||||
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
|
||||
const findButton = () => wrapper.find('a');
|
||||
const pipelinesCiTemplates = () => wrapper.findComponent(PipelinesCiTemplates);
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ describe('Pipelines Empty State', () => {
|
|||
});
|
||||
|
||||
it('should render empty state SVG', () => {
|
||||
expect(findIllustration().attributes('src')).toBe('foo.svg');
|
||||
expect(findEmptyState().props('svgPath')).toBe('foo.svg');
|
||||
});
|
||||
|
||||
it('should render empty state header', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { GlFormCheckbox } from '@gitlab/ui';
|
||||
import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
|
||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
import Component from '~/diffs/components/commit_item.vue';
|
||||
|
|
@ -21,15 +22,15 @@ describe('diffs/components/commit_item', () => {
|
|||
const timeago = getTimeago();
|
||||
const { commit } = getDiffWithCommit;
|
||||
|
||||
const getTitleElement = () => wrapper.find('.commit-row-message.item-title');
|
||||
const getDescElement = () => wrapper.find('pre.commit-row-description');
|
||||
const getDescExpandElement = () => wrapper.find('.commit-content .js-toggle-button');
|
||||
const getShaElement = () => wrapper.find('[data-testid="commit-sha-group"]');
|
||||
const getAvatarElement = () => wrapper.find('.user-avatar-link');
|
||||
const getCommitterElement = () => wrapper.find('.committer');
|
||||
const getCommitActionsElement = () => wrapper.find('.commit-actions');
|
||||
const getCommitPipelineStatus = () => wrapper.findComponent(CommitPipelineStatus);
|
||||
const getCommitCheckbox = () => wrapper.findComponent(GlFormCheckbox);
|
||||
const findTitleElement = () => wrapper.find('.commit-row-message.item-title');
|
||||
const findDescElement = () => wrapper.find('pre.commit-row-description');
|
||||
const findDescExpandElement = () => wrapper.find('.commit-content .js-toggle-button');
|
||||
const findShaElement = () => wrapper.find('[data-testid="commit-sha-group"]');
|
||||
const findUserAvatar = () => wrapper.findComponent(UserAvatarLink);
|
||||
const findCommitterElement = () => wrapper.find('.committer');
|
||||
const findCommitActionsElement = () => wrapper.find('.commit-actions');
|
||||
const findCommitPipelineStatus = () => wrapper.findComponent(CommitPipelineStatus);
|
||||
const findCommitCheckbox = () => wrapper.findComponent(GlFormCheckbox);
|
||||
|
||||
const mountComponent = (propsData) => {
|
||||
wrapper = mount(Component, {
|
||||
|
|
@ -49,15 +50,15 @@ describe('diffs/components/commit_item', () => {
|
|||
});
|
||||
|
||||
it('renders commit title', () => {
|
||||
const titleElement = getTitleElement();
|
||||
const titleElement = findTitleElement();
|
||||
|
||||
expect(titleElement.attributes('href')).toBe(commit.commit_url);
|
||||
expect(titleElement.text()).toBe(commit.title_html);
|
||||
});
|
||||
|
||||
it('renders commit description', () => {
|
||||
const descElement = getDescElement();
|
||||
const descExpandElement = getDescExpandElement();
|
||||
const descElement = findDescElement();
|
||||
const descExpandElement = findDescExpandElement();
|
||||
|
||||
const expected = commit.description_html.replace(/
/g, '');
|
||||
|
||||
|
|
@ -66,7 +67,7 @@ describe('diffs/components/commit_item', () => {
|
|||
});
|
||||
|
||||
it('renders commit sha', () => {
|
||||
const shaElement = getShaElement();
|
||||
const shaElement = findShaElement();
|
||||
const labelElement = shaElement.find('[data-testid="commit-sha-short-id"]');
|
||||
const buttonElement = shaElement.find('button.input-group-text');
|
||||
|
||||
|
|
@ -75,17 +76,16 @@ describe('diffs/components/commit_item', () => {
|
|||
});
|
||||
|
||||
it('renders author avatar', () => {
|
||||
const avatarElement = getAvatarElement();
|
||||
const imgElement = avatarElement.find('img');
|
||||
|
||||
expect(avatarElement.attributes('href')).toBe(commit.author.web_url);
|
||||
expect(imgElement.classes()).toContain('gl-avatar-s32');
|
||||
expect(imgElement.attributes('alt')).toBe(commit.author.name);
|
||||
expect(imgElement.attributes('src')).toBe(commit.author.avatar_url);
|
||||
expect(findUserAvatar().props()).toMatchObject({
|
||||
linkHref: commit.author.web_url,
|
||||
imgSrc: commit.author.avatar_url,
|
||||
imgAlt: commit.author.name,
|
||||
imgSize: 32,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders committer text', () => {
|
||||
const committerElement = getCommitterElement();
|
||||
const committerElement = findCommitterElement();
|
||||
const nameElement = committerElement.find('a');
|
||||
|
||||
const expectTimeText = timeago.format(commit.authored_date);
|
||||
|
|
@ -105,8 +105,8 @@ describe('diffs/components/commit_item', () => {
|
|||
});
|
||||
|
||||
it('hides description', () => {
|
||||
const descElement = getDescElement();
|
||||
const descExpandElement = getDescExpandElement();
|
||||
const descElement = findDescElement();
|
||||
const descExpandElement = findDescExpandElement();
|
||||
|
||||
expect(descElement.exists()).toBe(false);
|
||||
expect(descExpandElement.exists()).toBe(false);
|
||||
|
|
@ -127,16 +127,16 @@ describe('diffs/components/commit_item', () => {
|
|||
});
|
||||
|
||||
it('renders author avatar', () => {
|
||||
const avatarElement = getAvatarElement();
|
||||
const imgElement = avatarElement.find('img');
|
||||
|
||||
expect(avatarElement.attributes('href')).toBe(`mailto:${TEST_AUTHOR_EMAIL}`);
|
||||
expect(imgElement.attributes('alt')).toBe(TEST_AUTHOR_NAME);
|
||||
expect(imgElement.attributes('src')).toBe(TEST_AUTHOR_GRAVATAR);
|
||||
expect(findUserAvatar().props()).toMatchObject({
|
||||
linkHref: `mailto:${TEST_AUTHOR_EMAIL}`,
|
||||
imgSrc: TEST_AUTHOR_GRAVATAR,
|
||||
imgAlt: TEST_AUTHOR_NAME,
|
||||
imgSize: 32,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders committer text', () => {
|
||||
const committerElement = getCommitterElement();
|
||||
const committerElement = findCommitterElement();
|
||||
const nameElement = committerElement.find('a');
|
||||
|
||||
expect(nameElement.attributes('href')).toBe(`mailto:${TEST_AUTHOR_EMAIL}`);
|
||||
|
|
@ -152,7 +152,7 @@ describe('diffs/components/commit_item', () => {
|
|||
});
|
||||
|
||||
it('renders signature html', () => {
|
||||
const actionsElement = getCommitActionsElement();
|
||||
const actionsElement = findCommitActionsElement();
|
||||
const signatureElement = actionsElement.find('.signature-badge');
|
||||
|
||||
expect(signatureElement.html()).toBe(TEST_SIGNATURE_HTML);
|
||||
|
|
@ -167,7 +167,7 @@ describe('diffs/components/commit_item', () => {
|
|||
});
|
||||
|
||||
it('renders pipeline status', () => {
|
||||
expect(getCommitPipelineStatus().exists()).toBe(true);
|
||||
expect(findCommitPipelineStatus().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -180,12 +180,12 @@ describe('diffs/components/commit_item', () => {
|
|||
});
|
||||
|
||||
it('renders checkbox', () => {
|
||||
expect(getCommitCheckbox().exists()).toBe(true);
|
||||
expect(findCommitCheckbox().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('emits "handleCheckboxChange" event on change', () => {
|
||||
expect(wrapper.emitted('handleCheckboxChange')).toBeUndefined();
|
||||
getCommitCheckbox().vm.$emit('change');
|
||||
findCommitCheckbox().vm.$emit('change');
|
||||
|
||||
expect(wrapper.emitted('handleCheckboxChange')[0]).toEqual([true]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { GlAvatar } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import Commit from '~/environments/components/commit.vue';
|
||||
import { resolvedEnvironment } from './graphql/mock_data';
|
||||
|
|
@ -6,6 +7,8 @@ describe('~/environments/components/commit.vue', () => {
|
|||
let commit;
|
||||
let wrapper;
|
||||
|
||||
const findAvatar = () => wrapper.findComponent(GlAvatar);
|
||||
|
||||
beforeEach(() => {
|
||||
commit = resolvedEnvironment.lastDeployment.commit;
|
||||
});
|
||||
|
|
@ -33,8 +36,7 @@ describe('~/environments/components/commit.vue', () => {
|
|||
});
|
||||
|
||||
it('displays the user avatar', () => {
|
||||
const avatar = wrapper.findByRole('img', { name: 'avatar' });
|
||||
expect(avatar.attributes('src')).toBe(commit.author.avatarUrl);
|
||||
expect(findAvatar().props('src')).toBe(commit.author.avatarUrl);
|
||||
});
|
||||
|
||||
it('links the commit title to the commit', () => {
|
||||
|
|
@ -59,8 +61,7 @@ describe('~/environments/components/commit.vue', () => {
|
|||
});
|
||||
|
||||
it('displays the user avatar', () => {
|
||||
const avatar = wrapper.findByRole('img', { name: 'avatar' });
|
||||
expect(avatar.attributes('src')).toBe(commit.authorGravatarUrl);
|
||||
expect(findAvatar().props('src')).toBe(commit.authorGravatarUrl);
|
||||
});
|
||||
|
||||
it('displays the commit title', () => {
|
||||
|
|
@ -82,8 +83,7 @@ describe('~/environments/components/commit.vue', () => {
|
|||
});
|
||||
|
||||
it('displays the user avatar', () => {
|
||||
const avatar = wrapper.findByRole('img', { name: 'avatar' });
|
||||
expect(avatar.attributes('src')).toBe(commit.author.avatarUrl);
|
||||
expect(findAvatar().props('src')).toBe(commit.author.avatarUrl);
|
||||
});
|
||||
|
||||
it('links the commit title to the commit', () => {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ describe('MemberList', () => {
|
|||
it("renders group's avatar", () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.find('img').attributes('src')).toBe(group.avatarUrl);
|
||||
expect(wrapper.findComponent(GlAvatarLabeled).attributes('src')).toBe(group.avatarUrl);
|
||||
});
|
||||
|
||||
describe('when group is private', () => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import { getByText as getByTextHelper } from '@testing-library/dom';
|
||||
import { mount, createWrapper } from '@vue/test-utils';
|
||||
import { GlAvatarLabeled } from '@gitlab/ui';
|
||||
import InviteAvatar from '~/members/components/avatars/invite_avatar.vue';
|
||||
import { invite as member } from '../../mock_data';
|
||||
|
||||
describe('MemberList', () => {
|
||||
let wrapper;
|
||||
|
||||
const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
|
||||
|
||||
const { invite } = member;
|
||||
|
||||
const createComponent = (propsData = {}) => {
|
||||
|
|
@ -29,6 +32,6 @@ describe('MemberList', () => {
|
|||
});
|
||||
|
||||
it('renders avatar', () => {
|
||||
expect(wrapper.find('img').attributes('src')).toBe(invite.avatarUrl);
|
||||
expect(findAvatarLabeled().attributes('src')).toBe(invite.avatarUrl);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GlAvatarLink, GlBadge } from '@gitlab/ui';
|
||||
import { GlAvatarLink, GlAvatarLabeled, GlBadge } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import UserAvatar from '~/members/components/avatars/user_avatar.vue';
|
||||
import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
|
||||
|
|
@ -8,6 +8,8 @@ import { member as memberMock, member2faEnabled, orphanedMember } from '../../mo
|
|||
describe('UserAvatar', () => {
|
||||
let wrapper;
|
||||
|
||||
const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
|
||||
|
||||
const { user } = memberMock;
|
||||
|
||||
const createComponent = (propsData = {}, provide = {}) => {
|
||||
|
|
@ -55,7 +57,7 @@ describe('UserAvatar', () => {
|
|||
it("renders user's avatar", () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.find('img').attributes('src')).toBe(
|
||||
expect(findAvatarLabeled().attributes('src')).toBe(
|
||||
'https://www.gravatar.com/avatar/4816142ef496f956a277bedf1a40607b?s=80&d=identicon&width=96',
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ describe('Merge request dashboard merge request component', () => {
|
|||
},
|
||||
],
|
||||
},
|
||||
userDiscussionsCount: 5,
|
||||
userNotesCount: 5,
|
||||
createdAt: '2024-04-22T10:13:09Z',
|
||||
updatedAt: '2024-04-19T14:34:42Z',
|
||||
diffStatsSummary: {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export function createMockMergeRequest(mergeRequest = {}) {
|
|||
nodes: [],
|
||||
},
|
||||
headPipeline: null,
|
||||
userDiscussionsCount: 0,
|
||||
userNotesCount: 0,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
approved: false,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlBadge } from '@gitlab/ui';
|
||||
import { GlBadge, GlAvatar } from '@gitlab/ui';
|
||||
import getUserAchievementsEmptyResponse from 'test_fixtures/graphql/get_user_achievements_empty_response.json';
|
||||
import getUserAchievementsLongResponse from 'test_fixtures/graphql/get_user_achievements_long_response.json';
|
||||
import getUserAchievementsResponse from 'test_fixtures/graphql/get_user_achievements_with_avatar_and_description_response.json';
|
||||
|
|
@ -24,7 +24,8 @@ describe('UserAchievements', () => {
|
|||
let wrapper;
|
||||
|
||||
const getUserAchievementsQueryHandler = jest.fn().mockResolvedValue(getUserAchievementsResponse);
|
||||
const achievement = () => wrapper.findByTestId('user-achievement');
|
||||
const findUserAchievement = () => wrapper.findByTestId('user-achievement');
|
||||
const findAvatar = () => wrapper.findComponent(GlAvatar);
|
||||
|
||||
const createComponent = ({ queryHandler = getUserAchievementsQueryHandler } = {}) => {
|
||||
const fakeApollo = createMockApollo([[getUserAchievements, queryHandler]]);
|
||||
|
|
@ -61,7 +62,7 @@ describe('UserAchievements', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
expect(achievement().findComponent(GlBadge).text()).toBe('2x');
|
||||
expect(findUserAchievement().findComponent(GlBadge).text()).toBe('2x');
|
||||
});
|
||||
|
||||
it('renders correctly if the achievement is from a private namespace', async () => {
|
||||
|
|
@ -74,8 +75,8 @@ describe('UserAchievements', () => {
|
|||
const userAchievement =
|
||||
getUserAchievementsPrivateGroupResponse.data.user.userAchievements.nodes[0];
|
||||
|
||||
expect(achievement().text()).toContain(userAchievement.achievement.name);
|
||||
expect(achievement().text()).toContain(
|
||||
expect(findUserAchievement().text()).toContain(userAchievement.achievement.name);
|
||||
expect(findUserAchievement().text()).toContain(
|
||||
`Awarded ${getTimeago().format(
|
||||
userAchievement.createdAt,
|
||||
timeagoLanguageCode,
|
||||
|
|
@ -88,15 +89,13 @@ describe('UserAchievements', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
expect(achievement().text()).toContain(userAchievement1.achievement.name);
|
||||
expect(achievement().text()).toContain(
|
||||
expect(findUserAchievement().text()).toContain(userAchievement1.achievement.name);
|
||||
expect(findUserAchievement().text()).toContain(
|
||||
`Awarded ${getTimeago().format(userAchievement1.createdAt, timeagoLanguageCode)} by`,
|
||||
);
|
||||
expect(achievement().text()).toContain(userAchievement1.achievement.namespace.fullPath);
|
||||
expect(achievement().text()).toContain(userAchievement1.achievement.description);
|
||||
expect(achievement().find('img').attributes('src')).toBe(
|
||||
userAchievement1.achievement.avatarUrl,
|
||||
);
|
||||
expect(findUserAchievement().text()).toContain(userAchievement1.achievement.namespace.fullPath);
|
||||
expect(findUserAchievement().text()).toContain(userAchievement1.achievement.description);
|
||||
expect(findAvatar().props('src')).toBe(userAchievement1.achievement.avatarUrl);
|
||||
});
|
||||
|
||||
it('renders a placeholder when no avatar is present', async () => {
|
||||
|
|
@ -107,7 +106,7 @@ describe('UserAchievements', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
expect(achievement().find('img').attributes('src')).toBe(PLACEHOLDER_URL);
|
||||
expect(findAvatar().props('src')).toBe(PLACEHOLDER_URL);
|
||||
});
|
||||
|
||||
it('does not render a description when none is present', async () => {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import originalOneReleaseQueryResponse from 'test_fixtures/graphql/releases/grap
|
|||
import { convertOneReleaseGraphQLResponse } from '~/releases/util';
|
||||
import { RELEASED_AT_ASC, RELEASED_AT_DESC, CREATED_ASC, CREATED_DESC } from '~/releases/constants';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
|
||||
|
||||
// TODO: Encapsulate date helpers https://gitlab.com/gitlab-org/gitlab/-/issues/320883
|
||||
|
|
@ -38,6 +39,7 @@ describe('Release block footer', () => {
|
|||
const tagInfoSection = () => wrapper.find('.js-tag-info');
|
||||
const tagInfoSectionLink = () => tagInfoSection().findComponent(GlLink);
|
||||
const authorDateInfoSection = () => wrapper.find('.js-author-date-info');
|
||||
const findUserAvatar = () => wrapper.findComponent(UserAvatarLink);
|
||||
|
||||
describe.each`
|
||||
sortFlag | expectedInfoString
|
||||
|
|
@ -88,10 +90,10 @@ describe('Release block footer', () => {
|
|||
});
|
||||
if (authorFlag) {
|
||||
it("renders the author's avatar image", () => {
|
||||
const avatarImg = authorDateInfoSection().find('img');
|
||||
const avatarImg = findUserAvatar();
|
||||
|
||||
expect(avatarImg.exists()).toBe(true);
|
||||
expect(avatarImg.attributes('src')).toBe(release.author.avatarUrl);
|
||||
expect(avatarImg.props('imgSrc')).toBe(release.author.avatarUrl);
|
||||
});
|
||||
|
||||
it("renders a link to the author's profile", () => {
|
||||
|
|
|
|||
|
|
@ -906,6 +906,19 @@ export const defaultProvide = {
|
|||
},
|
||||
};
|
||||
|
||||
export const mockGetBlobSearchQueryEmpty = {
|
||||
data: {
|
||||
blobSearch: {
|
||||
fileCount: 0,
|
||||
files: [],
|
||||
matchCount: 0,
|
||||
perPage: 0,
|
||||
searchLevel: 'PROJECT',
|
||||
searchType: 'ZOEKT',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockGetBlobSearchQuery = {
|
||||
data: {
|
||||
blobSearch: {
|
||||
|
|
|
|||
|
|
@ -2,65 +2,124 @@ import Vue from 'vue';
|
|||
// eslint-disable-next-line no-restricted-imports
|
||||
import Vuex from 'vuex';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import getBlobSearchQuery from '~/search/graphql/blob_search_zoekt.query.graphql';
|
||||
import GlobalSearchResultsApp from '~/search/results/components/app.vue';
|
||||
import ZoektBlobResults from '~/search/results/components/zoekt_blob_results.vue';
|
||||
import { MOCK_QUERY, mockGetBlobSearchQuery } from '../../mock_data';
|
||||
import StatusBar from '~/search/results/components/status_bar.vue';
|
||||
import { MOCK_QUERY, mockGetBlobSearchQuery, mockGetBlobSearchQueryEmpty } from '../../mock_data';
|
||||
|
||||
Vue.use(Vuex);
|
||||
Vue.use(VueApollo);
|
||||
|
||||
jest.mock('~/alert');
|
||||
|
||||
describe('GlobalSearchResultsApp', () => {
|
||||
let wrapper;
|
||||
let apolloMock;
|
||||
|
||||
const getterSpies = {
|
||||
currentScope: jest.fn(() => 'blobs'),
|
||||
};
|
||||
|
||||
const blobSearchHandler = jest.fn().mockResolvedValue(mockGetBlobSearchQuery);
|
||||
const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {}));
|
||||
const mockQueryEmpty = jest.fn().mockReturnValue(mockGetBlobSearchQueryEmpty);
|
||||
const mockQueryError = jest.fn().mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const createComponent = ({
|
||||
initialState = { query: { scope: 'blobs' }, searchType: 'zoekt' },
|
||||
queryHandler = blobSearchHandler,
|
||||
} = {}) => {
|
||||
const requestHandlers = [[getBlobSearchQuery, queryHandler]];
|
||||
const apolloProvider = createMockApollo(requestHandlers);
|
||||
|
||||
const createComponent = (initialState = {}) => {
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
urlQuery: MOCK_QUERY,
|
||||
query: MOCK_QUERY,
|
||||
...initialState,
|
||||
},
|
||||
getters: getterSpies,
|
||||
});
|
||||
apolloMock = createMockApollo([[getBlobSearchQuery, blobSearchHandler]]);
|
||||
wrapper = shallowMountExtended(GlobalSearchResultsApp, {
|
||||
apolloProvider: apolloMock,
|
||||
apolloProvider,
|
||||
store,
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
apolloMock = null;
|
||||
const findZoektBlobResults = () => wrapper.findComponent(ZoektBlobResults);
|
||||
const findStatusBar = () => wrapper.findComponent(StatusBar);
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
|
||||
describe('when loading results', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({
|
||||
initialState: { query: { scope: 'blobs' }, searchType: 'zoekt' },
|
||||
queryHandler: mockQueryLoading,
|
||||
});
|
||||
jest.advanceTimersByTime(500);
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('renders loading icon', () => {
|
||||
expect(findZoektBlobResults().props('isLoading')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
const findZoektBlobResults = () => wrapper.findComponent(ZoektBlobResults);
|
||||
describe('when component has load error', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({
|
||||
initialState: { query: { scope: 'blobs' }, searchType: 'zoekt' },
|
||||
queryHandler: mockQueryError,
|
||||
});
|
||||
jest.runOnlyPendingTimers();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
it('renders alert', () => {
|
||||
expect(findAlert().text()).toBe(
|
||||
'Could not load search results. Refresh the page to try again.',
|
||||
);
|
||||
expect(findZoektBlobResults().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when component has no results', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({
|
||||
initialState: { query: { scope: 'blobs' }, searchType: 'zoekt' },
|
||||
queryHandler: mockQueryEmpty,
|
||||
});
|
||||
jest.runOnlyPendingTimers();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it(`renders component properly`, async () => {
|
||||
await waitForPromises();
|
||||
expect(findZoektBlobResults().props('hasResults')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when we have results', () => {
|
||||
describe.each`
|
||||
scope | searchType | isRendered
|
||||
${'blobs'} | ${'zoekt'} | ${true}
|
||||
${'issues'} | ${'zoekt'} | ${false}
|
||||
${'blobs'} | ${'advanced'} | ${false}
|
||||
${'issues'} | ${'basic'} | ${false}
|
||||
`('template', ({ scope, searchType, isRendered }) => {
|
||||
`(`has scope: $scope, searchType: $searchType`, ({ scope, searchType, isRendered }) => {
|
||||
beforeEach(async () => {
|
||||
getterSpies.currentScope = jest.fn(() => scope);
|
||||
createComponent({ query: { scope }, searchType });
|
||||
createComponent({ initialState: { query: { scope }, searchType } });
|
||||
jest.advanceTimersByTime(500);
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it(`renders component based on conditions`, () => {
|
||||
it(`correctly renders components`, () => {
|
||||
expect(findZoektBlobResults().exists()).toBe(isRendered);
|
||||
expect(findStatusBar().exists()).toBe(isRendered);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ describe('BlobChunks', () => {
|
|||
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).attributes('title')).toBe('View line in repository');
|
||||
expect(findGlLink().at(1).text()).toBe('1');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -51,11 +51,6 @@ describe('BlobFooter', () => {
|
|||
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: [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,228 @@
|
|||
import { GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import Vuex from 'vuex';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import GlobalSearchStatusBar from '~/search/results/components/status_bar.vue';
|
||||
import { MOCK_QUERY } from '../../mock_data';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
describe('GlobalSearchStatusBar', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultProps = {
|
||||
blobSearch: {
|
||||
perPage: 20,
|
||||
fileCount: 1074,
|
||||
matchCount: 3000,
|
||||
},
|
||||
hasError: false,
|
||||
hasResults: true,
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const defaultState = {
|
||||
query: {
|
||||
...MOCK_QUERY,
|
||||
group_id: null,
|
||||
project_id: null,
|
||||
search: 'test',
|
||||
},
|
||||
projectInitialJson: {},
|
||||
groupInitialJson: {},
|
||||
repositoryRef: 'main',
|
||||
};
|
||||
|
||||
const groupInitialJson = {
|
||||
id: 1,
|
||||
name: 'group-name',
|
||||
full_name: 'Group Full Name',
|
||||
};
|
||||
|
||||
const projectInitialJson = {
|
||||
id: 1,
|
||||
name: 'project-name',
|
||||
name_with_namespace: 'Project with Namespace',
|
||||
};
|
||||
|
||||
const createComponent = ({ propsData = {}, initialState = {} } = {}) => {
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
...defaultState,
|
||||
...initialState,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper = shallowMountExtended(GlobalSearchStatusBar, {
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...propsData,
|
||||
},
|
||||
store,
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('simple status message', () => {
|
||||
describe('multiple results', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders the status bar', () => {
|
||||
expect(wrapper.text()).toContain('Showing 3000 code results for test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('one result status message', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
propsData: {
|
||||
blobSearch: {
|
||||
perPage: 20,
|
||||
fileCount: 1,
|
||||
matchCount: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the status bar', () => {
|
||||
expect(wrapper.text()).toContain('Showing 1 code result for test');
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('group status message', () => {
|
||||
describe('multiple results', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
initialState: {
|
||||
query: {
|
||||
...MOCK_QUERY,
|
||||
group_id: 1,
|
||||
project_id: null,
|
||||
search: 'test',
|
||||
},
|
||||
groupInitialJson,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the status bar', () => {
|
||||
expect(wrapper.text()).toContain(
|
||||
'Showing 3000 code results for test in group Group Full Name',
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('single result', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
propsData: {
|
||||
blobSearch: {
|
||||
perPage: 20,
|
||||
fileCount: 1,
|
||||
matchCount: 1,
|
||||
},
|
||||
},
|
||||
initialState: {
|
||||
query: {
|
||||
...MOCK_QUERY,
|
||||
group_id: 1,
|
||||
project_id: null,
|
||||
search: 'test',
|
||||
},
|
||||
groupInitialJson,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the status bar', () => {
|
||||
expect(wrapper.text()).toContain('Showing 1 code result for test in group Group Full Name');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('project status message', () => {
|
||||
describe('multiple results', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
initialState: {
|
||||
query: {
|
||||
...MOCK_QUERY,
|
||||
group_id: null,
|
||||
project_id: 1,
|
||||
search: 'test',
|
||||
},
|
||||
projectInitialJson,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the status bar', () => {
|
||||
expect(wrapper.text()).toContain(
|
||||
'Showing 3000 code results for test in of Project with Namespace',
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('single result', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
propsData: {
|
||||
blobSearch: {
|
||||
perPage: 20,
|
||||
fileCount: 1,
|
||||
matchCount: 1,
|
||||
},
|
||||
},
|
||||
initialState: {
|
||||
query: {
|
||||
...MOCK_QUERY,
|
||||
group_id: null,
|
||||
project_id: 1,
|
||||
search: 'test',
|
||||
},
|
||||
projectInitialJson,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the status bar', () => {
|
||||
expect(wrapper.text()).toContain(
|
||||
'Showing 1 code result for test in of Project with Namespace',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are no results', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
propsData: {
|
||||
hasResults: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render the status bar', () => {
|
||||
expect(wrapper.text()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when loading', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
propsData: {
|
||||
isLoading: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render the status bar', () => {
|
||||
expect(wrapper.text()).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,21 +1,16 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import Vuex from 'vuex';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlLoadingIcon, GlCard } from '@gitlab/ui';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import getBlobSearchQuery from '~/search/graphql/blob_search_zoekt.query.graphql';
|
||||
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');
|
||||
|
||||
Vue.use(VueApollo);
|
||||
Vue.use(Vuex);
|
||||
|
||||
describe('ZoektBlobResults', () => {
|
||||
|
|
@ -25,28 +20,22 @@ describe('ZoektBlobResults', () => {
|
|||
currentScope: jest.fn(() => 'blobs'),
|
||||
};
|
||||
|
||||
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 = ({
|
||||
initialState = { query: { scope: 'blobs' }, searchType: 'zoekt' },
|
||||
queryHandler = blobSearchHandler,
|
||||
} = {}) => {
|
||||
const requestHandlers = [[getBlobSearchQuery, queryHandler]];
|
||||
const apolloProvider = createMockApollo(requestHandlers);
|
||||
const defaultState = { ...MOCK_QUERY, query: { scope: 'blobs' }, searchType: 'zoekt' };
|
||||
const defaultProps = { hasResults: true, isLoading: false, blobSearch: {} };
|
||||
|
||||
const createComponent = ({ initialState = {}, propsData = {} } = {}) => {
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
query: MOCK_QUERY,
|
||||
...defaultState,
|
||||
...initialState,
|
||||
},
|
||||
getters: getterSpies,
|
||||
});
|
||||
// apolloMock = createMockApollo([[getBlobSearchQuery, blobSearchHandler]]);
|
||||
wrapper = shallowMountExtended(ZoektBlobResults, {
|
||||
apolloProvider,
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...propsData,
|
||||
},
|
||||
store,
|
||||
stubs: {
|
||||
GlCard,
|
||||
|
|
@ -60,7 +49,7 @@ describe('ZoektBlobResults', () => {
|
|||
describe('when loading results', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({
|
||||
queryHandler: mockQueryLoading,
|
||||
propsData: { isLoading: true },
|
||||
});
|
||||
jest.advanceTimersByTime(500);
|
||||
await waitForPromises();
|
||||
|
|
@ -73,7 +62,11 @@ describe('ZoektBlobResults', () => {
|
|||
|
||||
describe('when component loads normally', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
createComponent({
|
||||
propsData: {
|
||||
blobSearch: mockGetBlobSearchQuery.data.blobSearch,
|
||||
},
|
||||
});
|
||||
jest.advanceTimersByTime(500);
|
||||
await waitForPromises();
|
||||
});
|
||||
|
|
@ -87,7 +80,7 @@ describe('ZoektBlobResults', () => {
|
|||
describe('when component has no results', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({
|
||||
queryHandler: mockQueryEmpty,
|
||||
propsData: { hasResults: false },
|
||||
});
|
||||
jest.advanceTimersByTime(500);
|
||||
await waitForPromises();
|
||||
|
|
@ -98,20 +91,4 @@ describe('ZoektBlobResults', () => {
|
|||
expect(findEmptyResult().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when component has load error', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({ queryHandler: mockQueryError });
|
||||
jest.runOnlyPendingTimers();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('calls createAlert', () => {
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
message: 'Could not load search results. Please refresh the page to try again.',
|
||||
captureError: true,
|
||||
error: expect.any(Error),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
import { WIDGET_TYPE_NOTES } from '~/work_items/constants';
|
||||
|
||||
const mockWorkItemNotesWidgetResponseWithComments =
|
||||
mockWorkItemNotesResponseWithComments.data.workspace.workItem.widgets.find(
|
||||
mockWorkItemNotesResponseWithComments().data.workspace.workItem.widgets.find(
|
||||
(widget) => widget.type === WIDGET_TYPE_NOTES,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ Vue.use(VueApollo);
|
|||
|
||||
describe('Work Item Note Awards List', () => {
|
||||
let wrapper;
|
||||
const { workItem } = mockWorkItemNotesResponseWithComments.data.workspace;
|
||||
const { workItem } = mockWorkItemNotesResponseWithComments().data.workspace;
|
||||
const firstNote = workItem.widgets.find((w) => w.type === 'NOTES').discussions.nodes[0].notes
|
||||
.nodes[0];
|
||||
const fullPath = 'test-project-path';
|
||||
|
|
@ -57,7 +57,7 @@ describe('Work Item Note Awards List', () => {
|
|||
apolloProvider.clients.defaultClient.writeQuery({
|
||||
query,
|
||||
variables: { fullPath, iid: workItemIid },
|
||||
...mockWorkItemNotesResponseWithComments,
|
||||
...mockWorkItemNotesResponseWithComments(),
|
||||
});
|
||||
|
||||
wrapper = shallowMount(WorkItemNoteAwardsList, {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { GlModal } from '@gitlab/ui';
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
import { setHTMLFixture } from 'helpers/fixtures';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
|
@ -42,7 +44,7 @@ const mockMoreNotesWidgetResponse =
|
|||
);
|
||||
|
||||
const mockWorkItemNotesWidgetResponseWithComments =
|
||||
mockWorkItemNotesResponseWithComments.data.workspace.workItem.widgets.find(
|
||||
mockWorkItemNotesResponseWithComments().data.workspace.workItem.widgets.find(
|
||||
(widget) => widget.type === WIDGET_TYPE_NOTES,
|
||||
);
|
||||
|
||||
|
|
@ -71,7 +73,7 @@ describe('WorkItemNotes component', () => {
|
|||
const workItemMoreNotesQueryHandler = jest.fn().mockResolvedValue(mockMoreWorkItemNotesResponse);
|
||||
const workItemNotesWithCommentsQueryHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockWorkItemNotesResponseWithComments);
|
||||
.mockResolvedValue(mockWorkItemNotesResponseWithComments());
|
||||
const deleteWorkItemNoteMutationSuccessHandler = jest.fn().mockResolvedValue({
|
||||
data: { destroyNote: { note: null, __typename: 'DestroyNote' } },
|
||||
});
|
||||
|
|
@ -122,6 +124,7 @@ describe('WorkItemNotes component', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
setHTMLFixture('<div id="content-body"></div>');
|
||||
createComponent();
|
||||
});
|
||||
|
||||
|
|
@ -381,6 +384,41 @@ describe('WorkItemNotes component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('discussions expanded status', () => {
|
||||
it('should be expanded when the discussion is not resolved', async () => {
|
||||
createComponent({
|
||||
defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
|
||||
});
|
||||
await waitForPromises();
|
||||
expect(findAllWorkItemCommentNotes().at(0).props('isExpandedOnLoad')).toBe(true);
|
||||
});
|
||||
|
||||
it('should be collapsed when the discussion is resolved', async () => {
|
||||
createComponent({
|
||||
defaultWorkItemNotesQueryHandler: jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockWorkItemNotesResponseWithComments(true)),
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
expect(findAllWorkItemCommentNotes().at(0).props('isExpandedOnLoad')).toBe(false);
|
||||
});
|
||||
|
||||
it('should be expanded when the notes are resolved but the target note hash has note id', async () => {
|
||||
setWindowLocation('#note_174');
|
||||
|
||||
createComponent({
|
||||
defaultWorkItemNotesQueryHandler: jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockWorkItemNotesResponseWithComments(true)),
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
await nextTick();
|
||||
expect(findAllWorkItemCommentNotes().at(0).props('isExpandedOnLoad')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when group context', () => {
|
||||
it('should pass the correct `autoCompleteDataSources` to group work item comment note', async () => {
|
||||
const groupWorkItemNotes = {
|
||||
|
|
@ -388,7 +426,7 @@ describe('WorkItemNotes component', () => {
|
|||
workspace: {
|
||||
id: 'gid://gitlab/Group/24',
|
||||
workItem: {
|
||||
...mockWorkItemNotesResponseWithComments.data.workspace.workItem,
|
||||
...mockWorkItemNotesResponseWithComments().data.workspace.workItem,
|
||||
namespace: {
|
||||
id: 'gid://gitlab/Group/24',
|
||||
__typename: 'Namespace',
|
||||
|
|
|
|||
|
|
@ -3372,216 +3372,218 @@ export const mockWorkItemCommentByMaintainer = {
|
|||
maxAccessLevelOfAuthor: 'Maintainer',
|
||||
};
|
||||
|
||||
export const mockWorkItemNotesResponseWithComments = {
|
||||
data: {
|
||||
workspace: {
|
||||
id: 'gid://gitlab/Project/6',
|
||||
workItem: {
|
||||
id: 'gid://gitlab/WorkItem/600',
|
||||
iid: '60',
|
||||
namespace: {
|
||||
id: 'gid://gitlab/Namespaces::ProjectNamespace/34',
|
||||
__typename: 'Namespace',
|
||||
},
|
||||
widgets: [
|
||||
{
|
||||
__typename: 'WorkItemWidgetIteration',
|
||||
export const mockWorkItemNotesResponseWithComments = (resolved = false) => {
|
||||
return {
|
||||
data: {
|
||||
workspace: {
|
||||
id: 'gid://gitlab/Project/6',
|
||||
workItem: {
|
||||
id: 'gid://gitlab/WorkItem/600',
|
||||
iid: '60',
|
||||
namespace: {
|
||||
id: 'gid://gitlab/Namespaces::ProjectNamespace/34',
|
||||
__typename: 'Namespace',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetWeight',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetAssignees',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetLabels',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetDescription',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetHierarchy',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetStartAndDueDate',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetMilestone',
|
||||
},
|
||||
{
|
||||
type: 'NOTES',
|
||||
discussions: {
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
endCursor: null,
|
||||
__typename: 'PageInfo',
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
|
||||
notes: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/DiscussionNote/174',
|
||||
body: 'Separate thread',
|
||||
bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Separate thread</p>',
|
||||
system: false,
|
||||
internal: false,
|
||||
systemNoteIconName: null,
|
||||
createdAt: '2023-01-12T07:47:40Z',
|
||||
lastEditedAt: null,
|
||||
url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
|
||||
lastEditedBy: null,
|
||||
maxAccessLevelOfAuthor: 'Owner',
|
||||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
|
||||
resolved: false,
|
||||
resolvable: true,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
author: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: 'http://127.0.0.1:3000/root',
|
||||
webPath: '/root',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
systemNoteMetadata: null,
|
||||
userPermissions: {
|
||||
adminNote: true,
|
||||
awardEmoji: true,
|
||||
readNote: true,
|
||||
createNote: true,
|
||||
resolveNote: true,
|
||||
repositionNote: true,
|
||||
__typename: 'NotePermissions',
|
||||
},
|
||||
awardEmoji: {
|
||||
nodes: [mockAwardEmojiThumbsDown],
|
||||
},
|
||||
__typename: 'Note',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/DiscussionNote/235',
|
||||
body: 'Thread comment',
|
||||
bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Thread comment</p>',
|
||||
system: false,
|
||||
internal: false,
|
||||
systemNoteIconName: null,
|
||||
createdAt: '2023-01-18T09:09:54Z',
|
||||
lastEditedAt: null,
|
||||
url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
|
||||
lastEditedBy: null,
|
||||
maxAccessLevelOfAuthor: 'Owner',
|
||||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
|
||||
resolved: false,
|
||||
resolvable: true,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
author: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: 'http://127.0.0.1:3000/root',
|
||||
webPath: '/root',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
systemNoteMetadata: null,
|
||||
userPermissions: {
|
||||
adminNote: true,
|
||||
awardEmoji: true,
|
||||
readNote: true,
|
||||
createNote: true,
|
||||
resolveNote: true,
|
||||
repositionNote: true,
|
||||
__typename: 'NotePermissions',
|
||||
},
|
||||
awardEmoji: {
|
||||
nodes: [],
|
||||
},
|
||||
__typename: 'Note',
|
||||
},
|
||||
],
|
||||
__typename: 'NoteConnection',
|
||||
},
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
|
||||
notes: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9883864',
|
||||
body: 'Main thread 2',
|
||||
bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Main thread 2</p>',
|
||||
systemNoteIconName: 'weight',
|
||||
createdAt: '2022-11-25T07:16:20Z',
|
||||
lastEditedAt: null,
|
||||
url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
|
||||
lastEditedBy: null,
|
||||
system: false,
|
||||
internal: false,
|
||||
maxAccessLevelOfAuthor: 'Owner',
|
||||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
|
||||
resolved: false,
|
||||
resolvable: true,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
userPermissions: {
|
||||
adminNote: false,
|
||||
awardEmoji: true,
|
||||
readNote: true,
|
||||
createNote: true,
|
||||
resolveNote: true,
|
||||
repositionNote: true,
|
||||
__typename: 'NotePermissions',
|
||||
},
|
||||
systemNoteMetadata: null,
|
||||
author: {
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
id: 'gid://gitlab/User/1',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: 'http://127.0.0.1:3000/root',
|
||||
webPath: '/root',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
awardEmoji: {
|
||||
nodes: [],
|
||||
},
|
||||
__typename: 'Note',
|
||||
},
|
||||
],
|
||||
__typename: 'NoteConnection',
|
||||
},
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
],
|
||||
__typename: 'DiscussionConnection',
|
||||
widgets: [
|
||||
{
|
||||
__typename: 'WorkItemWidgetIteration',
|
||||
},
|
||||
__typename: 'WorkItemWidgetNotes',
|
||||
},
|
||||
],
|
||||
__typename: 'WorkItem',
|
||||
{
|
||||
__typename: 'WorkItemWidgetWeight',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetAssignees',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetLabels',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetDescription',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetHierarchy',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetStartAndDueDate',
|
||||
},
|
||||
{
|
||||
__typename: 'WorkItemWidgetMilestone',
|
||||
},
|
||||
{
|
||||
type: 'NOTES',
|
||||
discussions: {
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
endCursor: null,
|
||||
__typename: 'PageInfo',
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
|
||||
notes: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/DiscussionNote/174',
|
||||
body: 'Separate thread',
|
||||
bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Separate thread</p>',
|
||||
system: false,
|
||||
internal: false,
|
||||
systemNoteIconName: null,
|
||||
createdAt: '2023-01-12T07:47:40Z',
|
||||
lastEditedAt: null,
|
||||
url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
|
||||
lastEditedBy: null,
|
||||
maxAccessLevelOfAuthor: 'Owner',
|
||||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
|
||||
resolved,
|
||||
resolvable: true,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
author: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: 'http://127.0.0.1:3000/root',
|
||||
webPath: '/root',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
systemNoteMetadata: null,
|
||||
userPermissions: {
|
||||
adminNote: true,
|
||||
awardEmoji: true,
|
||||
readNote: true,
|
||||
createNote: true,
|
||||
resolveNote: true,
|
||||
repositionNote: true,
|
||||
__typename: 'NotePermissions',
|
||||
},
|
||||
awardEmoji: {
|
||||
nodes: [mockAwardEmojiThumbsDown],
|
||||
},
|
||||
__typename: 'Note',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/DiscussionNote/235',
|
||||
body: 'Thread comment',
|
||||
bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Thread comment</p>',
|
||||
system: false,
|
||||
internal: false,
|
||||
systemNoteIconName: null,
|
||||
createdAt: '2023-01-18T09:09:54Z',
|
||||
lastEditedAt: null,
|
||||
url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
|
||||
lastEditedBy: null,
|
||||
maxAccessLevelOfAuthor: 'Owner',
|
||||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
|
||||
resolved,
|
||||
resolvable: true,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
author: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: 'http://127.0.0.1:3000/root',
|
||||
webPath: '/root',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
systemNoteMetadata: null,
|
||||
userPermissions: {
|
||||
adminNote: true,
|
||||
awardEmoji: true,
|
||||
readNote: true,
|
||||
createNote: true,
|
||||
resolveNote: true,
|
||||
repositionNote: true,
|
||||
__typename: 'NotePermissions',
|
||||
},
|
||||
awardEmoji: {
|
||||
nodes: [],
|
||||
},
|
||||
__typename: 'Note',
|
||||
},
|
||||
],
|
||||
__typename: 'NoteConnection',
|
||||
},
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
|
||||
notes: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9883864',
|
||||
body: 'Main thread 2',
|
||||
bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Main thread 2</p>',
|
||||
systemNoteIconName: 'weight',
|
||||
createdAt: '2022-11-25T07:16:20Z',
|
||||
lastEditedAt: null,
|
||||
url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
|
||||
lastEditedBy: null,
|
||||
system: false,
|
||||
internal: false,
|
||||
maxAccessLevelOfAuthor: 'Owner',
|
||||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
|
||||
resolved,
|
||||
resolvable: true,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
userPermissions: {
|
||||
adminNote: false,
|
||||
awardEmoji: true,
|
||||
readNote: true,
|
||||
createNote: true,
|
||||
resolveNote: true,
|
||||
repositionNote: true,
|
||||
__typename: 'NotePermissions',
|
||||
},
|
||||
systemNoteMetadata: null,
|
||||
author: {
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
id: 'gid://gitlab/User/1',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: 'http://127.0.0.1:3000/root',
|
||||
webPath: '/root',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
awardEmoji: {
|
||||
nodes: [],
|
||||
},
|
||||
__typename: 'Note',
|
||||
},
|
||||
],
|
||||
__typename: 'NoteConnection',
|
||||
},
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
],
|
||||
__typename: 'DiscussionConnection',
|
||||
},
|
||||
__typename: 'WorkItemWidgetNotes',
|
||||
},
|
||||
],
|
||||
__typename: 'WorkItem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const workItemNotesCreateSubscriptionResponse = {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function getFirstNote(workItem) {
|
|||
}
|
||||
|
||||
describe('Work item note award utils', () => {
|
||||
const workItem = getWorkItem(mockWorkItemNotesResponseWithComments.data);
|
||||
const workItem = getWorkItem(mockWorkItemNotesResponseWithComments().data);
|
||||
const firstNote = getFirstNote(workItem);
|
||||
const fullPath = 'test-project-path';
|
||||
const workItemIid = workItem.iid;
|
||||
|
|
@ -60,7 +60,7 @@ describe('Work item note award utils', () => {
|
|||
apolloProvider.clients.defaultClient.writeQuery({
|
||||
query: workItemNotesByIidQuery,
|
||||
variables: { fullPath, iid: workItemIid },
|
||||
...mockWorkItemNotesResponseWithComments,
|
||||
...mockWorkItemNotesResponseWithComments(),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,14 @@ RSpec.describe Mutations::Todos::Create do
|
|||
target = create(:milestone)
|
||||
input = { target_id: global_id_of(target).to_s }
|
||||
mutation = graphql_mutation(described_class, input)
|
||||
headers = { "Referer" => "foobar", "User-Agent" => "user-agent" }
|
||||
request = instance_double(ActionDispatch::Request, headers: headers, env: nil)
|
||||
|
||||
response = GitlabSchema.execute(mutation.query, context: query_context, variables: mutation.variables).to_h
|
||||
response = GitlabSchema.execute(
|
||||
mutation.query,
|
||||
context: query_context(request: request),
|
||||
variables: mutation.variables
|
||||
).to_h
|
||||
|
||||
expect(response).to include(
|
||||
'errors' => contain_exactly(
|
||||
|
|
|
|||
|
|
@ -1189,8 +1189,12 @@ RSpec.describe SearchHelper, feature_category: :global_search do
|
|||
end
|
||||
|
||||
describe '#should_show_zoekt_results?' do
|
||||
before do
|
||||
allow(self).to receive(:current_user).and_return(nil)
|
||||
end
|
||||
|
||||
it 'returns false for any scope and search type' do
|
||||
expect(should_show_zoekt_results?(:any_scope, :any_type)).to be false
|
||||
expect(should_show_zoekt_results?(:some_scope, :some_type)).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -37,4 +37,50 @@ RSpec.describe Gitlab::Ci::Build::Context::Global, feature_category: :pipeline_c
|
|||
|
||||
it_behaves_like 'variables collection'
|
||||
end
|
||||
|
||||
describe '#top_level_worktree_paths' do
|
||||
subject(:top_level_worktree_paths) { context.top_level_worktree_paths }
|
||||
|
||||
it 'delegates to pipeline' do
|
||||
expect(pipeline).to receive(:top_level_worktree_paths)
|
||||
|
||||
top_level_worktree_paths
|
||||
end
|
||||
|
||||
context 'with feature disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_conditionals_reduce_gitaly_calls: false)
|
||||
end
|
||||
|
||||
it 'accesses repository' do
|
||||
expect(pipeline).not_to receive(:top_level_worktree_paths)
|
||||
expect(context.project.repository).to receive(:tree).and_return(instance_double('Tree', blobs: []))
|
||||
|
||||
top_level_worktree_paths
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#all_worktree_paths' do
|
||||
subject(:all_worktree_paths) { context.all_worktree_paths }
|
||||
|
||||
it 'delegates to pipeline' do
|
||||
expect(pipeline).to receive(:all_worktree_paths)
|
||||
|
||||
all_worktree_paths
|
||||
end
|
||||
|
||||
context 'with feature disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_conditionals_reduce_gitaly_calls: false)
|
||||
end
|
||||
|
||||
it 'accesses repository' do
|
||||
expect(context.project.repository).to receive(:ls_files).with(instance_of(String))
|
||||
expect(pipeline).not_to receive(:all_worktree_paths)
|
||||
|
||||
all_worktree_paths
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -169,9 +169,9 @@ module GraphqlHelpers
|
|||
end
|
||||
|
||||
# create a valid query context object
|
||||
def query_context(user: current_user)
|
||||
def query_context(user: current_user, request: {})
|
||||
query = GraphQL::Query.new(empty_schema, document: nil, context: {}, variables: {})
|
||||
GraphQL::Query::Context.new(query: query, values: { current_user: user })
|
||||
GraphQL::Query::Context.new(query: query, values: { current_user: user, request: request })
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/ParameterLists
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const ruleFunction = (primary) => {
|
|||
|
||||
if (!validOptions) return;
|
||||
|
||||
root.walkRules(/\.gl-(?!dark)/, (ruleNode) => {
|
||||
root.walkRules(/\.gl-/, (ruleNode) => {
|
||||
report({
|
||||
result,
|
||||
ruleName,
|
||||
|
|
|
|||
Loading…
Reference in New Issue