Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-08-29 15:07:22 +00:00
parent 9d2ebf5c7a
commit ef53b9b911
92 changed files with 1306 additions and 507 deletions

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@ fragment MergeRequestDashboardFragment on MergeRequest {
...CiIcon
}
}
userDiscussionsCount
userNotesCount
createdAt
updatedAt
...MergeRequestApprovalFragment

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -96,9 +96,3 @@
margin-left: $gl-spacing-scale-5;
}
}
// Timeline avatars
.timeline-avatar {
@apply gl-bg-default;
}

View File

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

View File

@ -60,6 +60,7 @@
}
.dark-mode-override {
// stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
background-color: $white;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -57,6 +57,7 @@
&.development {
background-color: $perf-bar-development;
// stylelint-disable-next-line gitlab/no-gl-class
.gl-dark & {
background-color: $red-950;
}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
description: "Selec stream from dropdown"
description: "Select stream from dropdown"
category: default
action: click_dropdown
extra_properties:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
b19bdab41864682ccef9b4cbde289e3c4bd5a924286bd8282df5559686ac414b

View File

@ -0,0 +1 @@
ad1958f4ade8d13c64d19894bd0115a357d7322443fb95acbcfc0c582611afc5

View File

@ -0,0 +1 @@
4bebe3172ceb8fcb39dbbf722bc8eb841eb73ca6e409979f325acc54e6672cf4

View File

@ -0,0 +1 @@
42f405277c2e1b5c4ea935ddcfe20d550a135bf9bbfc5ef6174e531b3b04e540

View File

@ -0,0 +1 @@
df9e7c4f161cdc7bb359d9cfa36bf3117c28d950481f69be90c80ffdf1f3c96d

View File

@ -0,0 +1 @@
4ca6cc8ccecbdec8dae4af9af5099e782a69a3b2e92e806e04c7fad8e3a26e70

View File

@ -0,0 +1 @@
02b4b6ab225fc477382c66434dba8ffda9deede17f610361f7d34ab5c135622e

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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` | &bull;&nbsp;`MergeRequest::ChangedEvent` <br> &bull;&nbsp;`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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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', () => {

View File

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

View File

@ -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', () => {

View File

@ -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(/&#x000A;/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]);
});

View File

@ -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', () => {

View File

@ -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', () => {

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ export function createMockMergeRequest(mergeRequest = {}) {
nodes: [],
},
headPipeline: null,
userDiscussionsCount: 0,
userNotesCount: 0,
createdAt: '',
updatedAt: '',
approved: false,

View File

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

View File

@ -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", () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ const ruleFunction = (primary) => {
if (!validOptions) return;
root.walkRules(/\.gl-(?!dark)/, (ruleNode) => {
root.walkRules(/\.gl-/, (ruleNode) => {
report({
result,
ruleName,