Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-06-24 21:21:45 +00:00
parent 311ee28119
commit 2647690038
38 changed files with 799 additions and 442 deletions

View File

@ -2836,7 +2836,6 @@ Gitlab/BoundedContexts:
- 'ee/app/models/board_user_preference.rb'
- 'ee/app/models/burndown.rb'
- 'ee/app/models/click_house_model.rb'
- 'ee/app/models/compliance_management/compliance_framework.rb'
- 'ee/app/models/compliance_management/compliance_framework/project_settings.rb'
- 'ee/app/models/compliance_management/compliance_framework/security_policy.rb'
- 'ee/app/models/compliance_management/framework.rb'

View File

@ -1,5 +1,5 @@
<script>
import { GlButton, GlCollapse, GlIcon, GlBadge, GlLink } from '@gitlab/ui';
import { GlButton, GlCollapse, GlIcon, GlBadge, GlLink, GlSprintf } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql';
import folderQuery from '../graphql/queries/folder.query.graphql';
@ -14,6 +14,7 @@ export default {
GlIcon,
GlBadge,
GlLink,
GlSprintf,
},
props: {
nestedEnvironment: {
@ -50,7 +51,10 @@ export default {
i18n: {
collapse: __('Collapse'),
expand: __('Expand'),
link: s__('Environments|Show all'),
link: s__('Environments|See all environments.'),
message: s__(
'Environments|Showing %{listedEnvironmentsCount} of %{totalEnvironmentsCount} environments in this folder.',
),
},
computed: {
icons() {
@ -61,7 +65,7 @@ export default {
label() {
return this.visible ? this.$options.i18n.collapse : this.$options.i18n.expand;
},
count() {
totalEnvironmentsCount() {
const count = ENVIRONMENT_COUNT_BY_SCOPE[this.scope];
return this.folder?.[count] ?? 0;
},
@ -72,7 +76,13 @@ export default {
return this.nestedEnvironment.latest.folderPath;
},
environments() {
return this.folder?.environments;
return this.folder?.environments ?? [];
},
listedEnvironmentsCount() {
return this.environments.length;
},
isMessageShowing() {
return this.listedEnvironmentsCount < this.totalEnvironmentsCount;
},
},
methods: {
@ -108,8 +118,7 @@ export default {
<div class="gl-mr-2 gl-text-gray-500" :class="folderClass">
{{ nestedEnvironment.name }}
</div>
<gl-badge size="sm" class="gl-mr-auto">{{ count }}</gl-badge>
<gl-link v-if="visible" :href="folderPath">{{ $options.i18n.link }}</gl-link>
<gl-badge size="sm" class="gl-mr-auto">{{ totalEnvironmentsCount }}</gl-badge>
</div>
<gl-collapse :visible="visible">
<environment-item
@ -120,6 +129,17 @@ export default {
class="gl-border-gray-100 gl-border-t-solid gl-border-1 gl-pt-3"
in-folder
/>
<div
v-if="isMessageShowing"
class="gl-border-gray-100 gl-border-t-solid gl-border-1 gl-py-5 gl-bg-gray-10 gl-text-center"
data-testid="environment-folder-message-element"
>
<gl-sprintf :message="$options.i18n.message">
<template #listedEnvironmentsCount>{{ listedEnvironmentsCount }}</template>
<template #totalEnvironmentsCount>{{ totalEnvironmentsCount }}</template>
</gl-sprintf>
<gl-link :href="folderPath">{{ $options.i18n.link }}</gl-link>
</div>
</gl-collapse>
</div>
</template>

View File

@ -6,3 +6,5 @@
// Unicode 6.1
export const unicodeLetters =
'\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0\\u08A2-\\u08AC\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F0\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA697\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA793\\uA7A0-\\uA7AA\\uA7F8-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA80-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC';
export const semverRegex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;

View File

@ -12,6 +12,7 @@ import {
import { __, s__, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { semverRegex } from '~/lib/utils/regexp';
import { uploadModel } from '../services/upload_model';
import createModelVersionMutation from '../graphql/mutations/create_model_version.mutation.graphql';
import { emptyArtifactFile, MODEL_VERSION_CREATION_MODAL_ID } from '../constants';
@ -45,19 +46,51 @@ export default {
errorMessage: null,
selectedFile: emptyArtifactFile,
versionData: null,
submitButtonDisabled: true,
};
},
computed: {
versionDescription() {
if (this.latestVersion) {
return sprintf(s__('MlModelRegistry|Latest version is %{latestVersion}'), {
latestVersion: this.latestVersion,
});
return sprintf(
s__('MlModelRegistry|Enter a semantic version. Latest version is %{latestVersion}'),
{
latestVersion: this.latestVersion,
},
);
}
return s__('MlModelRegistry|Enter a semver version.');
return s__('MlModelRegistry|Enter a semantic version.');
},
actionPrimary() {
return {
text: s__('MlModelRegistry|Create & import'),
attributes: { variant: 'confirm', disabled: this.submitButtonDisabled },
};
},
isSemver() {
return semverRegex.test(this.version);
},
invalidFeedback() {
if (this.version === null) {
this.submitDisabled();
return this.versionDescription;
}
if (!this.isSemver) {
this.submitDisabled();
return this.$options.modal.versionInvalid;
}
this.submitAvailable();
return null;
},
},
methods: {
submitDisabled() {
this.submitButtonDisabled = true;
},
submitAvailable() {
this.submitButtonDisabled = false;
},
async createModelVersion() {
const { data } = await this.$apollo.mutate({
mutation: createModelVersionMutation,
@ -117,14 +150,13 @@ export default {
i18n: {},
modal: {
id: MODEL_VERSION_CREATION_MODAL_ID,
actionPrimary: {
text: s__('MlModelRegistry|Create & import'),
attributes: { variant: 'confirm' },
},
actionSecondary: {
text: __('Cancel'),
},
versionPlaceholder: s__('MlModelRegistry|A semver version like 1.0.0'),
versionDescription: s__('MlModelRegistry|Enter a semantic version.'),
versionValid: s__('MlModelRegistry|Version is valid semantic version.'),
versionInvalid: s__('MlModelRegistry|Version is not a valid semantic version.'),
versionPlaceholder: s__('MlModelRegistry|For example 1.0.0'),
descriptionPlaceholder: s__('MlModelRegistry|Enter some description'),
buttonTitle: s__('MlModelRegistry|Create model version'),
title: s__('MlModelRegistry|Create model version & import artifacts'),
@ -138,7 +170,7 @@ export default {
<gl-modal
:modal-id="$options.modal.id"
:title="$options.modal.title"
:action-primary="$options.modal.actionPrimary"
:action-primary="actionPrimary"
:action-secondary="$options.modal.actionSecondary"
size="sm"
@primary="create"
@ -149,6 +181,9 @@ export default {
data-testid="versionDescriptionId"
label="Version:"
label-for="versionId"
:state="isSemver"
:invalid-feedback="!version ? '' : invalidFeedback"
:valid-feedback="isSemver ? $options.modal.versionValid : ''"
:description="versionDescription"
>
<gl-form-input

View File

@ -18,7 +18,13 @@ export function initRelatedIssues() {
fullPath: el.dataset.fullPath,
hasIssueWeightsFeature: parseBoolean(el.dataset.hasIssueWeightsFeature),
hasIterationsFeature: parseBoolean(el.dataset.hasIterationsFeature),
reportAbusePath: el.dataset.reportAbusePath,
isGroup: parseBoolean(el.dataset.isGroup),
// for work item modal
canAdminLabel: el.dataset.wiCanAdminLabel,
groupPath: el.dataset.wiGroupPath,
issuesListPath: el.dataset.wiIssuesListPath,
labelsManagePath: el.dataset.wiLabelsManagePath,
reportAbusePath: el.dataset.wiReportAbusePath,
},
render: (createElement) =>
createElement(RelatedIssuesRoot, {

View File

@ -77,7 +77,7 @@ export default {
<div class="dropdown-input">
<gl-form-input
v-model.trim="labelTitle"
:placeholder="__('Name new label')"
:placeholder="__('Label name')"
:autofocus="true"
data-testid="label-title"
/>

View File

@ -141,7 +141,7 @@ export default {
</gl-alert>
<gl-form-group
class="gl-my-3"
:label="__('Name new label')"
:label="__('Label name')"
label-for="label-title-input"
label-sr-only
>
@ -149,7 +149,7 @@ export default {
id="label-title-input"
v-model.trim="labelTitle"
autofocus
:placeholder="__('Name new label')"
:placeholder="__('Label name')"
/>
</gl-form-group>
<sidebar-color-picker v-model.trim="selectedColor" :suggested-colors="suggestedColors" />

View File

@ -24,3 +24,21 @@ export const Default = Template.bind({});
Default.args = {
heading: 'Page heading',
};
export const WithHeadingSlot = (args, { argTypes }) => ({
components: { PageHeading },
props: Object.keys(argTypes),
template: `
<page-heading v-bind="$props">
<template #heading>
Heading with <i>custom items</i>
</template>
<template #actions>
Actions go here
</template>
<template #description>
Description goes here
</template>
</page-heading>
`,
});

View File

@ -16,7 +16,8 @@ export default {
class="gl-flex gl-flex-wrap gl-items-center gl-justify-between gl-gap-y-2 gl-gap-x-5 gl-my-5"
>
<h1 class="gl-heading-1 !gl-m-0" data-testid="page-heading">
{{ heading }}
<slot name="heading"></slot>
<template v-if="!$scopedSlots.heading">{{ heading }}</template>
</h1>
<div
v-if="$scopedSlots.actions"

View File

@ -98,6 +98,11 @@ export default {
required: false,
default: true,
},
createdLabelId: {
type: String,
required: false,
default: undefined,
},
},
data() {
return {
@ -131,6 +136,10 @@ export default {
}
},
},
createdLabelId(id) {
this.localSelectedItem.push(id);
this.isDirty = true;
},
},
created() {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
@ -205,41 +214,43 @@ export default {
>{{ $options.i18n.applyButtonLabel }}</gl-button
>
</div>
<gl-collapsible-listbox
:id="inputId"
ref="listbox"
:multiple="multiSelect"
:searchable="searchable"
start-opened
block
is-check-centered
:infinite-scroll="infiniteScroll"
:searching="loading"
:header-text="headerText"
:toggle-text="toggleText"
:no-results-text="$options.i18n.noMatchingResults"
positioning-strategy="fixed"
:items="listItems"
:selected="localSelectedItem"
:reset-button-label="resetButton"
:infinite-scroll-loading="infiniteScrollLoading"
toggle-class="work-item-sidebar-dropdown-toggle"
@reset="unassignValue"
@search="debouncedSearchKeyUpdate"
@select="handleItemClick"
@shown="onListboxShown"
@hidden="onListboxHide"
@bottom-reached="$emit('bottomReached')"
>
<template #list-item="{ item }">
<slot name="list-item" :item="item">{{ item.text }}</slot>
</template>
<template v-if="showFooter" #footer>
<div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-200 gl-p-2!">
<slot name="footer"></slot>
</div>
</template>
</gl-collapsible-listbox>
<slot name="body">
<gl-collapsible-listbox
:id="inputId"
ref="listbox"
:multiple="multiSelect"
:searchable="searchable"
start-opened
block
is-check-centered
:infinite-scroll="infiniteScroll"
:searching="loading"
:header-text="headerText"
:toggle-text="toggleText"
:no-results-text="$options.i18n.noMatchingResults"
positioning-strategy="fixed"
:items="listItems"
:selected="localSelectedItem"
:reset-button-label="resetButton"
:infinite-scroll-loading="infiniteScrollLoading"
toggle-class="work-item-sidebar-dropdown-toggle"
@reset="unassignValue"
@search="debouncedSearchKeyUpdate"
@select="handleItemClick"
@shown="onListboxShown"
@hidden="onListboxHide"
@bottom-reached="$emit('bottomReached')"
>
<template #list-item="{ item }">
<slot name="list-item" :item="item">{{ item.text }}</slot>
</template>
<template v-if="showFooter" #footer>
<div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-200 gl-p-2!">
<slot name="footer"></slot>
</div>
</template>
</gl-collapsible-listbox>
</slot>
</gl-form>
<slot v-else-if="hasValue" name="readonly"></slot>
<slot v-else name="none">

View File

@ -1,9 +1,11 @@
<script>
import { GlLabel } from '@gitlab/ui';
import { GlButton, GlDisclosureDropdown, GlLabel } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { difference } from 'lodash';
import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import { __, n__ } from '~/locale';
import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
import groupLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql';
import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
import { isScopedLabel } from '~/lib/utils/common_utils';
@ -16,18 +18,14 @@ import { isLabelsWidget } from '../utils';
export default {
components: {
WorkItemSidebarDropdownWidget,
DropdownContentsCreateView,
GlButton,
GlDisclosureDropdown,
GlLabel,
WorkItemSidebarDropdownWidget,
},
mixins: [Tracking.mixin()],
inject: {
issuesListPath: {
type: String,
},
isGroup: {
type: Boolean,
},
},
inject: ['canAdminLabel', 'isGroup', 'issuesListPath', 'labelsManagePath'],
props: {
fullPath: {
type: String,
@ -55,7 +53,9 @@ export default {
return {
searchTerm: '',
searchStarted: false,
showLabelForm: false,
updateInProgress: false,
createdLabelId: undefined,
removeLabelIds: [],
addLabelIds: [],
};
@ -110,6 +110,9 @@ export default {
allowsScopedLabels() {
return this.labelsWidget?.allowsScopedLabels;
},
workspaceType() {
return this.isGroup ? WORKSPACE_GROUP : WORKSPACE_PROJECT;
},
},
apollo: {
workItem: {
@ -171,10 +174,9 @@ export default {
this.addLabelIds = difference(labels, this.itemValues);
},
async updateLabels(labels) {
this.searchTerm = '';
this.updateInProgress = true;
if (labels && labels.length === 0) {
if (labels?.length === 0) {
this.removeLabelIds = this.itemValues;
this.addLabelIds = [];
}
@ -198,16 +200,16 @@ export default {
});
if (errors.length > 0) {
this.throwUpdateError();
return;
throw new Error();
}
this.addLabelIds = [];
this.removeLabelIds = [];
this.track('updated_labels');
} catch {
this.throwUpdateError();
this.$emit('error', i18n.updateError);
} finally {
this.searchTerm = '';
this.addLabelIds = [];
this.removeLabelIds = [];
this.updateInProgress = false;
}
},
@ -217,14 +219,14 @@ export default {
isSelected(id) {
return this.itemValues.includes(id) || this.addLabelIds.includes(id);
},
throwUpdateError() {
this.$emit('error', i18n.updateError);
this.addLabelIds = [];
this.removeLabelIds = [];
},
labelFilterUrl(label) {
return `${this.issuesListPath}?label_name[]=${encodeURIComponent(label.title)}`;
},
handleLabelCreated(label) {
this.showLabelForm = false;
this.createdLabelId = label.id;
this.addLabelIds.push(label.id);
},
},
};
</script>
@ -233,6 +235,7 @@ export default {
<work-item-sidebar-dropdown-widget
:dropdown-label="__('Labels')"
:can-update="canUpdate"
:created-label-id="createdLabelId"
dropdown-name="label"
:loading="isLoadingLabels"
:list-items="labelsList"
@ -241,7 +244,8 @@ export default {
:toggle-dropdown-text="dropdownText"
:header-text="__('Select labels')"
:reset-button-label="__('Clear')"
:multi-select="true"
multi-select
show-footer
clear-search-on-item-select
data-testid="work-item-labels"
@dropdownShown="onDropdownShown"
@ -250,17 +254,15 @@ export default {
@updateSelected="updateLabel"
>
<template #list-item="{ item }">
<span>
<span
:style="{ background: item.color }"
:class="{ 'gl-border gl-border-white': isSelected(item.value) }"
class="gl-display-inline-block gl-rounded-base gl-mr-1 gl-w-5 gl-h-3 gl-align-middle -gl-mt-1"
></span>
{{ item.text }}
</span>
<span
:style="{ background: item.color }"
:class="{ 'gl-border gl-border-white': isSelected(item.value) }"
class="gl-inline-block gl-rounded gl-mr-1 gl-w-5 gl-h-3 gl-align-middle -gl-mt-1"
></span>
{{ item.text }}
</template>
<template #readonly>
<div class="gl-display-flex gl-gap-2 gl-flex-wrap gl-mt-1">
<div class="gl-flex gl-gap-2 gl-flex-wrap gl-mt-1">
<gl-label
v-for="label in localLabels"
:key="label.id"
@ -274,5 +276,45 @@ export default {
/>
</div>
</template>
<template #footer>
<gl-button
v-if="canAdminLabel"
class="!gl-justify-start"
block
category="tertiary"
data-testid="create-project-label"
@click="showLabelForm = true"
>
{{ __('Create project label') }}
</gl-button>
<gl-button
class="!gl-justify-start !gl-mt-2"
block
category="tertiary"
:href="labelsManagePath"
data-testid="manage-project-labels"
>
{{ __('Manage project labels') }}
</gl-button>
</template>
<template v-if="showLabelForm" #body>
<gl-disclosure-dropdown block start-opened :toggle-text="dropdownText">
<div
class="gl-text-sm gl-font-bold gl-leading-24 gl-border-b gl-pt-2 gl-pb-3 gl-pl-4 gl-mb-4"
>
{{ __('Create label') }}
</div>
<dropdown-contents-create-view
class="gl-mb-2"
:attr-workspace-path="fullPath"
:full-path="fullPath"
:label-create-type="workspaceType"
:search-key="searchTerm"
:workspace-type="workspaceType"
@hideCreateView="showLabelForm = false"
@labelCreated="handleLabelCreated"
/>
</gl-disclosure-dropdown>
</template>
</work-item-sidebar-dropdown-widget>
</template>

View File

@ -15,13 +15,17 @@ export default function initWorkItemLinks() {
const {
fullPath,
isGroup,
registerPath,
signInPath,
wiCanAdminLabel,
wiGroupPath,
wiHasIssueWeightsFeature,
wiHasIterationsFeature,
wiHasIssuableHealthStatusFeature,
registerPath,
signInPath,
wiIssuesListPath,
wiLabelsManagePath,
wiReportAbusePath,
isGroup,
} = workItemLinksRoot.dataset;
return new Vue({
@ -30,13 +34,18 @@ export default function initWorkItemLinks() {
apolloProvider,
provide: {
fullPath,
isGroup: parseBoolean(isGroup),
registerPath,
signInPath,
// for work item modal
canAdminLabel: wiCanAdminLabel,
groupPath: wiGroupPath,
hasIssueWeightsFeature: wiHasIssueWeightsFeature,
hasIterationsFeature: wiHasIterationsFeature,
hasIssuableHealthStatusFeature: wiHasIssuableHealthStatusFeature,
registerPath,
signInPath,
issuesListPath: wiIssuesListPath,
labelsManagePath: wiLabelsManagePath,
reportAbusePath: wiReportAbusePath,
isGroup: parseBoolean(isGroup),
},
render: (createElement) =>
createElement(WorkItemLinks, {

View File

@ -24,11 +24,13 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType } = {}) => {
addShortcutsExtension(ShortcutsWorkItems);
const {
canAdminLabel,
fullPath,
groupPath,
hasIssueWeightsFeature,
iid,
issuesListPath,
labelsManagePath,
registerPath,
signInPath,
hasIterationsFeature,
@ -47,11 +49,13 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType } = {}) => {
router: createRouter({ fullPath, workItemType, workspaceType, defaultBranch }),
apolloProvider,
provide: {
canAdminLabel,
fullPath,
isGroup,
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasOkrsFeature: parseBoolean(hasOkrsFeature),
issuesListPath,
labelsManagePath,
registerPath,
signInPath,
hasIterationsFeature: parseBoolean(hasIterationsFeature),

View File

@ -5,10 +5,13 @@ module WorkItemsHelper
group = resource_parent.is_a?(Group) ? resource_parent : resource_parent.group
{
can_admin_label: can?(current_user, :admin_label, resource_parent).to_s,
full_path: resource_parent.full_path,
group_path: group&.full_path,
issues_list_path:
resource_parent.is_a?(Group) ? issues_group_path(resource_parent) : project_issues_path(resource_parent),
labels_manage_path:
resource_parent.is_a?(Group) ? group_labels_path(resource_parent) : project_labels_path(resource_parent),
register_path: new_user_registration_path(redirect_to_referer: 'yes'),
sign_in_path: new_session_path(:user, redirect_to_referer: 'yes'),
new_comment_template_paths: new_comment_template_paths(group).to_json,

View File

@ -1,18 +1,25 @@
- page_title _("Groups")
- add_page_specific_style 'page_bundles/search'
.top-area
.gl-mt-3.gl-mb-3
= form_tag admin_groups_path, method: :get, class: 'js-search-form' do |f|
= hidden_field_tag :sort, @sort
.search-holder
.search-field-holder
= search_field_tag :name, params[:name].presence, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name', data: { testid: 'group-search-field' }
= sprite_icon('search', css_class: 'search-icon')
= render "shared/groups/dropdown", options_hash: admin_groups_sort_options_hash
= render Pajamas::ButtonComponent.new(variant: :confirm, href: new_admin_group_path) do
= _('New group')
%ul.content-list
= render @groups
= render ::Layouts::PageHeadingComponent.new(_('Groups')) do |c|
- c.with_actions do
= link_button_to new_admin_group_path, variant: :confirm do
= _('New group')
.md:gl-flex.gl-min-w-0.gl-grow.row-content-block
= form_tag admin_groups_path, method: :get, class: 'js-search-form gl-w-full' do |f|
= hidden_field_tag :sort, @sort
.search-holder
.search-field-holder
= search_field_tag :name, params[:name].presence, class: "form-control search-text-input js-search-input", spellcheck: false, placeholder: 'Search by name', data: { testid: 'group-search-field' }
= sprite_icon('search', css_class: 'search-icon')
= render "shared/groups/dropdown", options_hash: admin_groups_sort_options_hash
- if @groups.any?
%ul.content-list
= render @groups
- else
= render Pajamas::EmptyStateComponent.new(svg_path: 'illustrations/empty-state/empty-groups-md.svg',
title: _('No groups found'))
= paginate @groups, theme: "gitlab"

View File

@ -4,7 +4,8 @@
full_path: @project.full_path,
has_issue_weights_feature: @project.licensed_feature_available?(:issue_weights).to_s,
help_path: help_page_path('user/project/issues/related_issues'),
is_group: 'false',
issuable_type: @issue.issue_type,
show_categorized_issues: @project.licensed_feature_available?(:blocked_issues).to_s,
has_iterations_feature: @project.licensed_feature_available?(:iterations).to_s,
report_abuse_path: add_category_abuse_reports_path } }
wi: work_items_show_data(@project) } }

View File

@ -1,9 +0,0 @@
---
name: ci_expand_nested_resource_group_variables
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/361438
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/155594
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/466168
milestone: '17.1'
group: group::pipeline security
type: gitlab_com_derisk
default_enabled: false

View File

@ -104,8 +104,8 @@ Considering that web traffic is proxied to the primary, the behavior of the seco
site is inaccessible:
- UI and API traffic return the same errors as the primary (or fail if the primary is not accessible at all), since they are proxied.
- For repositories that already exist on the specific secondary site being accessed, Git read operations still work as expected,
including authentication through HTTP(s) or SSH.
- For repositories that are fully up-to-date on the specific secondary site being accessed, Git read operations still work as expected,
including authentication through HTTP(s) or SSH. However, Git reads performed by GitLab Runners will fail.
- Git operations for repositories that are not replicated to the secondary site return the same errors
as the primary site, since they are proxied.
- All Git write operations return the same errors as the primary site, since they are proxied.

View File

@ -51,7 +51,7 @@ the security of NGINX itself:
nginx['ssl_session_timeout'] = "5m"
# Should prevent logjam attack etc
nginx['ssl_dhparam'] = "/etc/gitlab/ssl/dhparams.pem" # changed from nil
nginx['ssl_dhparam'] = "/etc/gitlab/ssl/dhparam.pem" # changed from nil
# Turn off session ticket reuse
nginx['ssl_session_tickets'] = "off"

View File

@ -88,3 +88,199 @@ For example:
```shell
git clone https://<username>:<token>@gitlab.example.com/tanuki/awesome_project.git
```
## Reduce clone size
As Git repositories grow in size, they can become cumbersome to work with
because of:
- The large amount of history that must be downloaded.
- The large amount of disk space they require.
[Partial clone](https://git-scm.com/docs/partial-clone)
is a performance optimization that allows Git to function without having a
complete copy of the repository. The goal of this work is to allow Git better
handle extremely large repositories.
Git 2.22.0 or later is required.
### Filter by file size
Storing large binary files in Git is usually discouraged, because every large
file added is downloaded by everyone who clones or fetches changes
thereafter. These downloads are slow and problematic, especially when working from a slow
or unreliable internet connection.
Using partial clone with a file size filter solves this problem, by excluding
troublesome large files from clones and fetches. When Git encounters a missing
file, it's downloaded on demand.
When cloning a repository, use the `--filter=blob:limit=<size>` argument. For example,
to clone the repository excluding files larger than 1 megabyte:
```shell
git clone --filter=blob:limit=1m git@gitlab.com:gitlab-com/www-gitlab-com.git
```
This would produce the following output:
```shell
Cloning into 'www-gitlab-com'...
remote: Enumerating objects: 832467, done.
remote: Counting objects: 100% (832467/832467), done.
remote: Compressing objects: 100% (207226/207226), done.
remote: Total 832467 (delta 585563), reused 826624 (delta 580099), pack-reused 0
Receiving objects: 100% (832467/832467), 2.34 GiB | 5.05 MiB/s, done.
Resolving deltas: 100% (585563/585563), done.
remote: Enumerating objects: 146, done.
remote: Counting objects: 100% (146/146), done.
remote: Compressing objects: 100% (138/138), done.
remote: Total 146 (delta 8), reused 144 (delta 8), pack-reused 0
Receiving objects: 100% (146/146), 471.45 MiB | 4.60 MiB/s, done.
Resolving deltas: 100% (8/8), done.
Updating files: 100% (13008/13008), done.
Filtering content: 100% (3/3), 131.24 MiB | 4.65 MiB/s, done.
```
The output is longer because Git:
1. Clones the repository excluding files larger than 1 megabyte.
1. Downloads any missing large files needed to check out the default branch.
When changing branches, Git may download more missing files.
### Filter by object type
For repositories with millions of files and a long history, you can exclude all files and use
[`git sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout) to reduce the size of
your working copy.
```shell
# Clone the repo excluding all files
$ git clone --filter=blob:none --sparse git@gitlab.com:gitlab-com/www-gitlab-com.git
Cloning into 'www-gitlab-com'...
remote: Enumerating objects: 678296, done.
remote: Counting objects: 100% (678296/678296), done.
remote: Compressing objects: 100% (165915/165915), done.
remote: Total 678296 (delta 472342), reused 673292 (delta 467476), pack-reused 0
Receiving objects: 100% (678296/678296), 81.06 MiB | 5.74 MiB/s, done.
Resolving deltas: 100% (472342/472342), done.
remote: Enumerating objects: 28, done.
remote: Counting objects: 100% (28/28), done.
remote: Compressing objects: 100% (25/25), done.
remote: Total 28 (delta 0), reused 12 (delta 0), pack-reused 0
Receiving objects: 100% (28/28), 140.29 KiB | 341.00 KiB/s, done.
Updating files: 100% (28/28), done.
$ cd www-gitlab-com
$ git sparse-checkout set data --cone
remote: Enumerating objects: 301, done.
remote: Counting objects: 100% (301/301), done.
remote: Compressing objects: 100% (292/292), done.
remote: Total 301 (delta 16), reused 102 (delta 9), pack-reused 0
Receiving objects: 100% (301/301), 1.15 MiB | 608.00 KiB/s, done.
Resolving deltas: 100% (16/16), done.
Updating files: 100% (302/302), done.
```
For more details, see the Git documentation for
[`sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout).
### Filter by file path
Deeper integration between partial clone and sparse checkout is possible through the
`--filter=sparse:oid=<blob-ish>` filter spec. This mode of filtering uses a format similar to a
`.gitignore` file to specify which files to include when cloning and fetching.
WARNING:
Partial clone using `sparse` filters is still experimental. It might be slow and significantly increase
[Gitaly](../../administration/gitaly/index.md) resource utilization when cloning and fetching.
[Filter all blobs and use sparse-checkout](#filter-by-object-type) instead, because
[`git-sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout) simplifies
this type of partial clone use and overcomes its limitations.
For more details, see the Git documentation for
[`rev-list-options`](https://git-scm.com/docs/git-rev-list#Documentation/git-rev-list.txt---filterltfilter-specgt).
1. Create a filter spec. For example, consider a monolithic repository with many applications,
each in a different subdirectory in the root. Create a file `shiny-app/.filterspec`:
```plaintext
# Only the paths listed in the file will be downloaded when performing a
# partial clone using `--filter=sparse:oid=shiny-app/.gitfilterspec`
# Explicitly include filterspec needed to configure sparse checkout with
# git config --local core.sparsecheckout true
# git show master:snazzy-app/.gitfilterspec >> .git/info/sparse-checkout
shiny-app/.gitfilterspec
# Shiny App
shiny-app/
# Dependencies
shimmery-app/
shared-component-a/
shared-component-b/
```
1. Clone and filter by path. Support for `--filter=sparse:oid` using the
clone command is not fully integrated with sparse checkout.
```shell
# Clone the filtered set of objects using the filterspec stored on the
# server. WARNING: this step may be very slow!
git clone --sparse --filter=sparse:oid=master:shiny-app/.gitfilterspec <url>
# Optional: observe there are missing objects that we have not fetched
git rev-list --all --quiet --objects --missing=print | wc -l
```
WARNING:
Git integrations with `bash`, Zsh, etc and editors that automatically
show Git status information often run `git fetch` which fetches the
entire repository. Disabling or reconfiguring these integrations might be required.
### Remove partial clone filtering
Git repositories with partial clone filtering can have the filtering removed. To
remove filtering:
1. Fetch everything that has been excluded by the filters, to make sure that the
repository is complete. If `git sparse-checkout` was used, use
`git sparse-checkout disable` to disable it. See the
[`disable` documentation](https://git-scm.com/docs/git-sparse-checkout#Documentation/git-sparse-checkout.txt-emdisableem)
for more information.
Then do a regular `fetch` to ensure that the repository is complete. To check if
there are missing objects to fetch, and then fetch them, especially when not using
`git sparse-checkout`, the following commands can be used:
```shell
# Show missing objects
git rev-list --objects --all --missing=print | grep -e '^\?'
# Show missing objects without a '?' character before them (needs GNU grep)
git rev-list --objects --all --missing=print | grep -oP '^\?\K\w+'
# Fetch missing objects
git fetch origin $(git rev-list --objects --all --missing=print | grep -oP '^\?\K\w+')
# Show number of missing objects
git rev-list --objects --all --missing=print | grep -e '^\?' | wc -l
```
1. Repack everything. This can be done using `git repack -a -d`, for example. This
should leave only three files in `.git/objects/pack/`:
- A `pack-<SHA1>.pack` file.
- Its corresponding `pack-<SHA1>.idx` file.
- A `pack-<SHA1>.promisor` file.
1. Delete the `.promisor` file. The above step should have left only one
`pack-<SHA1>.promisor` file, which should be empty and should be deleted.
1. Remove partial clone configuration. The partial clone-related configuration
variables should be removed from Git configuration files. Usually only the following
configuration must be removed:
- `remote.origin.promisor`.
- `remote.origin.partialclonefilter`.

View File

@ -1,201 +1,11 @@
---
stage: Create
group: Source Code
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments"
redirect_to: 'clone.md'
remove_date: '2024-09-25'
---
# Use partial clones to reduce clone size
This document was moved to [another location](clone.md).
As Git repositories grow in size, they can become cumbersome to work with
because of:
- The large amount of history that must be downloaded.
- The large amount of disk space they require.
[Partial clone](https://git-scm.com/docs/partial-clone)
is a performance optimization that allows Git to function without having a
complete copy of the repository. The goal of this work is to allow Git better
handle extremely large repositories.
Git 2.22.0 or later is required.
## Filter by file size
Storing large binary files in Git is usually discouraged, because every large
file added is downloaded by everyone who clones or fetches changes
thereafter. These downloads are slow and problematic, especially when working from a slow
or unreliable internet connection.
Using partial clone with a file size filter solves this problem, by excluding
troublesome large files from clones and fetches. When Git encounters a missing
file, it's downloaded on demand.
When cloning a repository, use the `--filter=blob:limit=<size>` argument. For example,
to clone the repository excluding files larger than 1 megabyte:
```shell
git clone --filter=blob:limit=1m git@gitlab.com:gitlab-com/www-gitlab-com.git
```
This would produce the following output:
```shell
Cloning into 'www-gitlab-com'...
remote: Enumerating objects: 832467, done.
remote: Counting objects: 100% (832467/832467), done.
remote: Compressing objects: 100% (207226/207226), done.
remote: Total 832467 (delta 585563), reused 826624 (delta 580099), pack-reused 0
Receiving objects: 100% (832467/832467), 2.34 GiB | 5.05 MiB/s, done.
Resolving deltas: 100% (585563/585563), done.
remote: Enumerating objects: 146, done.
remote: Counting objects: 100% (146/146), done.
remote: Compressing objects: 100% (138/138), done.
remote: Total 146 (delta 8), reused 144 (delta 8), pack-reused 0
Receiving objects: 100% (146/146), 471.45 MiB | 4.60 MiB/s, done.
Resolving deltas: 100% (8/8), done.
Updating files: 100% (13008/13008), done.
Filtering content: 100% (3/3), 131.24 MiB | 4.65 MiB/s, done.
```
The output is longer because Git:
1. Clones the repository excluding files larger than 1 megabyte.
1. Downloads any missing large files needed to check out the default branch.
When changing branches, Git may download more missing files.
## Filter by object type
For repositories with millions of files and a long history, you can exclude all files and use
[`git sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout) to reduce the size of
your working copy.
```shell
# Clone the repo excluding all files
$ git clone --filter=blob:none --sparse git@gitlab.com:gitlab-com/www-gitlab-com.git
Cloning into 'www-gitlab-com'...
remote: Enumerating objects: 678296, done.
remote: Counting objects: 100% (678296/678296), done.
remote: Compressing objects: 100% (165915/165915), done.
remote: Total 678296 (delta 472342), reused 673292 (delta 467476), pack-reused 0
Receiving objects: 100% (678296/678296), 81.06 MiB | 5.74 MiB/s, done.
Resolving deltas: 100% (472342/472342), done.
remote: Enumerating objects: 28, done.
remote: Counting objects: 100% (28/28), done.
remote: Compressing objects: 100% (25/25), done.
remote: Total 28 (delta 0), reused 12 (delta 0), pack-reused 0
Receiving objects: 100% (28/28), 140.29 KiB | 341.00 KiB/s, done.
Updating files: 100% (28/28), done.
$ cd www-gitlab-com
$ git sparse-checkout set data --cone
remote: Enumerating objects: 301, done.
remote: Counting objects: 100% (301/301), done.
remote: Compressing objects: 100% (292/292), done.
remote: Total 301 (delta 16), reused 102 (delta 9), pack-reused 0
Receiving objects: 100% (301/301), 1.15 MiB | 608.00 KiB/s, done.
Resolving deltas: 100% (16/16), done.
Updating files: 100% (302/302), done.
```
For more details, see the Git documentation for
[`sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout).
## Filter by file path
Deeper integration between partial clone and sparse checkout is possible through the
`--filter=sparse:oid=<blob-ish>` filter spec. This mode of filtering uses a format similar to a
`.gitignore` file to specify which files to include when cloning and fetching.
WARNING:
Partial clone using `sparse` filters is still experimental. It might be slow and significantly increase
[Gitaly](../../administration/gitaly/index.md) resource utilization when cloning and fetching.
[Filter all blobs and use sparse-checkout](#filter-by-object-type) instead, because
[`git-sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout) simplifies
this type of partial clone use and overcomes its limitations.
For more details, see the Git documentation for
[`rev-list-options`](https://git-scm.com/docs/git-rev-list#Documentation/git-rev-list.txt---filterltfilter-specgt).
1. Create a filter spec. For example, consider a monolithic repository with many applications,
each in a different subdirectory in the root. Create a file `shiny-app/.filterspec`:
```plaintext
# Only the paths listed in the file will be downloaded when performing a
# partial clone using `--filter=sparse:oid=shiny-app/.gitfilterspec`
# Explicitly include filterspec needed to configure sparse checkout with
# git config --local core.sparsecheckout true
# git show master:snazzy-app/.gitfilterspec >> .git/info/sparse-checkout
shiny-app/.gitfilterspec
# Shiny App
shiny-app/
# Dependencies
shimmery-app/
shared-component-a/
shared-component-b/
```
1. Clone and filter by path. Support for `--filter=sparse:oid` using the
clone command is not fully integrated with sparse checkout.
```shell
# Clone the filtered set of objects using the filterspec stored on the
# server. WARNING: this step may be very slow!
git clone --sparse --filter=sparse:oid=master:shiny-app/.gitfilterspec <url>
# Optional: observe there are missing objects that we have not fetched
git rev-list --all --quiet --objects --missing=print | wc -l
```
WARNING:
Git integrations with `bash`, Zsh, etc and editors that automatically
show Git status information often run `git fetch` which fetches the
entire repository. Disabling or reconfiguring these integrations might be required.
## Remove partial clone filtering
Git repositories with partial clone filtering can have the filtering removed. To
remove filtering:
1. Fetch everything that has been excluded by the filters, to make sure that the
repository is complete. If `git sparse-checkout` was used, use
`git sparse-checkout disable` to disable it. See the
[`disable` documentation](https://git-scm.com/docs/git-sparse-checkout#Documentation/git-sparse-checkout.txt-emdisableem)
for more information.
Then do a regular `fetch` to ensure that the repository is complete. To check if
there are missing objects to fetch, and then fetch them, especially when not using
`git sparse-checkout`, the following commands can be used:
```shell
# Show missing objects
git rev-list --objects --all --missing=print | grep -e '^\?'
# Show missing objects without a '?' character before them (needs GNU grep)
git rev-list --objects --all --missing=print | grep -oP '^\?\K\w+'
# Fetch missing objects
git fetch origin $(git rev-list --objects --all --missing=print | grep -oP '^\?\K\w+')
# Show number of missing objects
git rev-list --objects --all --missing=print | grep -e '^\?' | wc -l
```
1. Repack everything. This can be done using `git repack -a -d`, for example. This
should leave only three files in `.git/objects/pack/`:
- A `pack-<SHA1>.pack` file.
- Its corresponding `pack-<SHA1>.idx` file.
- A `pack-<SHA1>.promisor` file.
1. Delete the `.promisor` file. The above step should have left only one
`pack-<SHA1>.promisor` file, which should be empty and should be deleted.
1. Remove partial clone configuration. The partial clone-related configuration
variables should be removed from Git configuration files. Usually only the following
configuration must be removed:
- `remote.origin.promisor`.
- `remote.origin.partialclonefilter`.
<!-- This redirect file can be deleted after <2024-09-25>. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->

View File

@ -28,13 +28,7 @@ module Gitlab
def expanded_resource_group_key
strong_memoize(:expanded_resource_group_key) do
variable_set = if Feature.enabled?(:ci_expand_nested_resource_group_variables, processable.project)
variables.sort_and_expand_all
else
variables
end
ExpandVariables.expand(resource_group_key, -> { variable_set })
ExpandVariables.expand(resource_group_key, -> { variables.sort_and_expand_all })
end
end

View File

@ -20563,6 +20563,9 @@ msgstr ""
msgid "Environments|Search by environment name"
msgstr ""
msgid "Environments|See all environments."
msgstr ""
msgid "Environments|Select Flux resource"
msgstr ""
@ -20584,6 +20587,9 @@ msgstr ""
msgid "Environments|Show all"
msgstr ""
msgid "Environments|Showing %{listedEnvironmentsCount} of %{totalEnvironmentsCount} environments in this folder."
msgstr ""
msgid "Environments|Stop"
msgstr ""
@ -30441,6 +30447,9 @@ msgid_plural "Labels added: %{labels}"
msgstr[0] ""
msgstr[1] ""
msgid "Label name"
msgstr ""
msgid "Label priority"
msgstr ""
@ -33464,9 +33473,6 @@ msgid_plural "MlModelRegistry|%d versions"
msgstr[0] ""
msgstr[1] ""
msgid "MlModelRegistry|A semver version like 1.0.0"
msgstr ""
msgid "MlModelRegistry|Add a model"
msgstr ""
@ -33557,7 +33563,10 @@ msgstr ""
msgid "MlModelRegistry|Enter a model description"
msgstr ""
msgid "MlModelRegistry|Enter a semver version."
msgid "MlModelRegistry|Enter a semantic version."
msgstr ""
msgid "MlModelRegistry|Enter a semantic version. Latest version is %{latestVersion}"
msgstr ""
msgid "MlModelRegistry|Enter some description"
@ -33581,6 +33590,9 @@ msgstr ""
msgid "MlModelRegistry|File \"%{name}\" is %{size}. It is larger than max allowed size of %{maxAllowedFileSize}"
msgstr ""
msgid "MlModelRegistry|For example 1.0.0"
msgstr ""
msgid "MlModelRegistry|For example 1.0.0. Must be a semantic version."
msgstr ""
@ -33599,9 +33611,6 @@ msgstr ""
msgid "MlModelRegistry|Latest version"
msgstr ""
msgid "MlModelRegistry|Latest version is %{latestVersion}"
msgstr ""
msgid "MlModelRegistry|Leave empty to skip version creation."
msgstr ""
@ -33707,6 +33716,12 @@ msgstr ""
msgid "MlModelRegistry|Version description"
msgstr ""
msgid "MlModelRegistry|Version is not a valid semantic version."
msgstr ""
msgid "MlModelRegistry|Version is valid semantic version."
msgstr ""
msgid "MlModelRegistry|Versions"
msgstr ""
@ -33961,9 +33976,6 @@ msgstr ""
msgid "Name must start with a letter, digit, emoji, or underscore."
msgstr ""
msgid "Name new label"
msgstr ""
msgid "Name the deployment (must be unique)"
msgstr ""
@ -34790,6 +34802,9 @@ msgstr ""
msgid "No group provided"
msgstr ""
msgid "No groups found"
msgstr ""
msgid "No issues found"
msgstr ""

View File

@ -12,7 +12,7 @@ module QA
Flow::Login.sign_in
end
it 'can delete a page', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347815' do
it 'can delete a page', :blocking, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347815' do
initial_wiki.visit!
Page::Project::Wiki::Show.perform(&:click_edit)

View File

@ -212,7 +212,7 @@ RSpec.describe 'Project issue boards sidebar labels', :js, feature_category: :po
wait_for_requests
click_on 'Create project label'
fill_in 'Name new label', with: 'test label'
fill_in 'Label name', with: 'test label'
first('.suggested-colors a').click
click_button 'Create'
wait_for_requests

View File

@ -575,7 +575,7 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
within_testid 'sidebar-labels' do
click_button _('Create project label')
fill_in _('Name new label'), with: 'test label'
fill_in _('Label name'), with: 'test label'
first('.suggest-colors-dropdown a').click
click_button 'Create'
end

View File

@ -5,7 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubTransition } from 'helpers/stub_transition';
import { __, s__ } from '~/locale';
import { __, s__, sprintf } from '~/locale';
import EnvironmentsFolder from '~/environments/components/environment_folder.vue';
import EnvironmentItem from '~/environments/components/new_environment_item.vue';
import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data';
@ -18,7 +18,17 @@ describe('~/environments/components/environments_folder.vue', () => {
let intervalMock;
let nestedEnvironment;
const findLink = () => wrapper.findByRole('link', { name: s__('Environments|Show all') });
const findLink = () => wrapper.findByText(s__('Environments|See all environments.'));
const findMessage = () =>
wrapper.findByText(
sprintf(
s__(
'Environments|Showing %{listedEnvironmentsCount} of %{totalEnvironmentsCount} environments in this folder.',
),
{ listedEnvironmentsCount: 2, totalEnvironmentsCount: 4 },
),
);
const findFolderMessageElement = () => wrapper.findByTestId('environment-folder-message-element');
const createApolloProvider = () => {
const mockResolvers = { Query: { folder: environmentFolderMock, interval: intervalMock } };
@ -77,13 +87,10 @@ describe('~/environments/components/environments_folder.vue', () => {
});
it('is collapsed by default', () => {
const link = findLink();
expect(collapse.props('visible')).toBe(false);
const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
expect(iconNames).toEqual(['chevron-lg-right', 'folder-o']);
expect(folderName.classes('gl-font-bold')).toBe(false);
expect(link.exists()).toBe(false);
});
it('opens on click and starts polling', async () => {
@ -93,14 +100,11 @@ describe('~/environments/components/environments_folder.vue', () => {
jest.advanceTimersByTime(2000);
await waitForPromises();
const link = findLink();
expect(button.attributes('aria-label')).toBe(__('Collapse'));
expect(collapse.props('visible')).toBe(true);
const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
expect(iconNames).toEqual(['chevron-lg-down', 'folder-open']);
expect(folderName.classes('gl-font-bold')).toBe(true);
expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath);
expect(environmentFolderMock).toHaveBeenCalledTimes(2);
});
@ -129,6 +133,36 @@ describe('~/environments/components/environments_folder.vue', () => {
expect(environmentFolderMock).toHaveBeenCalledTimes(2);
});
describe('when there are more environments to show inside the folder', () => {
it('displays the message with the correct text and a link to navigate to all environments', async () => {
environmentFolderMock.mockReturnValue({ ...resolvedFolder, activeCount: 4 });
wrapper = createWrapper({ scope: 'active', nestedEnvironment }, createApolloProvider());
await nextTick();
await waitForPromises();
const link = findLink();
const message = findMessage();
const element = findFolderMessageElement();
expect(element.exists()).toBe(true);
expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath);
expect(message.exists()).toBe(true);
});
});
describe('when all available environments inside the folder are shown', () => {
it("doesn't display the message and a link", () => {
const link = findLink();
const message = findMessage();
const element = findFolderMessageElement();
expect(element.exists()).toBe(false);
expect(link.exists()).toBe(false);
expect(message.exists()).toBe(false);
});
});
});
});

View File

@ -1,4 +1,4 @@
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert, GlModal } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@ -98,10 +98,14 @@ describe('ModelVersionCreate', () => {
expect(findVersionInput().exists()).toBe(true);
});
it('renders the version input label', () => {
it('renders the version input label for initial state', () => {
expect(wrapper.findByTestId('versionDescriptionId').attributes().description).toBe(
'Enter a semver version.',
'Enter a semantic version.',
);
expect(wrapper.findByTestId('versionDescriptionId').attributes('invalid-feedback')).toBe(
'',
);
expect(wrapper.findByTestId('versionDescriptionId').attributes('valid-feedback')).toBe('');
});
it('renders the description input', () => {
@ -124,9 +128,9 @@ describe('ModelVersionCreate', () => {
});
});
it('renders the create button in the modal', () => {
it('disables the create button in the modal when semver is incorrect', () => {
expect(findGlModal().props('actionPrimary')).toEqual({
attributes: { variant: 'confirm' },
attributes: { variant: 'confirm', disabled: true },
text: 'Create & import',
});
});
@ -147,6 +151,47 @@ describe('ModelVersionCreate', () => {
});
});
describe('It reacts to semantic version input', () => {
beforeEach(() => {
createWrapper();
});
it('renders the version input label for initial state', () => {
expect(wrapper.findByTestId('versionDescriptionId').attributes('invalid-feedback')).toBe('');
expect(findGlModal().props('actionPrimary')).toEqual({
attributes: { variant: 'confirm', disabled: true },
text: 'Create & import',
});
});
it.each(['1.0', '1', 'abc', '1.abc', '1.0.0.0'])(
'renders the version input label for invalid state',
async (version) => {
findVersionInput().vm.$emit('input', version);
await nextTick();
expect(wrapper.findByTestId('versionDescriptionId').attributes('invalid-feedback')).toBe(
'Version is not a valid semantic version.',
);
expect(findGlModal().props('actionPrimary')).toEqual({
attributes: { variant: 'confirm', disabled: true },
text: 'Create & import',
});
},
);
it.each(['1.0.0', '0.0.0-b', '24.99.99-b99'])(
'renders the version input label for valid state',
async (version) => {
findVersionInput().vm.$emit('input', version);
await nextTick();
expect(wrapper.findByTestId('versionDescriptionId').attributes('valid-feedback')).toBe(
'Version is valid semantic version.',
);
expect(findGlModal().props('actionPrimary')).toEqual({
attributes: { variant: 'confirm', disabled: false },
text: 'Create & import',
});
},
);
});
describe('Latest version available', () => {
beforeEach(() => {
createWrapper(undefined, { latestVersion: '1.2.3' });
@ -154,7 +199,7 @@ describe('ModelVersionCreate', () => {
it('renders the version input label', () => {
expect(wrapper.findByTestId('versionDescriptionId').attributes().description).toBe(
'Latest version is 1.2.3',
'Enter a semantic version. Latest version is 1.2.3',
);
});
});

View File

@ -144,7 +144,7 @@ describe('DropdownContentsCreateView', () => {
const titleInputEl = wrapper.find('.dropdown-input').findComponent(GlFormInput);
expect(titleInputEl.exists()).toBe(true);
expect(titleInputEl.attributes('placeholder')).toBe('Name new label');
expect(titleInputEl.attributes('placeholder')).toBe('Label name');
expect(titleInputEl.attributes('autofocus')).toBe('true');
});

View File

@ -14,14 +14,21 @@ describe('Pagination links component', () => {
</template>
`;
const headingTemplate = `
<template #heading>
Heading with custom elements <i>here</i>
</template>
`;
describe('Ordered Layout', () => {
let wrapper;
const createWrapper = () => {
const createWrapper = (scopedSlots = {}) => {
wrapper = shallowMountExtended(PageHeading, {
scopedSlots: {
actions: actionsTemplate,
description: descriptionTemplate,
...scopedSlots,
},
propsData: {
heading: 'Page heading',
@ -52,6 +59,12 @@ describe('Pagination links component', () => {
expect(description().text()).toBe('Description go here');
expect(description().classes()).toEqual(['gl-w-full', 'gl-mt-2', 'gl-text-secondary']);
});
it('renders the heading slot if provided', () => {
createWrapper({ heading: headingTemplate });
expect(heading().text()).toBe('Heading with custom elements here');
});
});
});
});

View File

@ -229,4 +229,30 @@ describe('WorkItemSidebarDropdownWidget component', () => {
expect(findCollapsibleListbox().props('toggleText')).toBe('No iteration');
});
});
describe('watcher', () => {
describe('when createdLabelId prop is updated', () => {
it('appends itself to the selected items list', async () => {
createComponent({
isEditing: true,
itemValue: ['gid://gitlab/Label/11', 'gid://gitlab/Label/22'],
multiSelect: true,
});
await nextTick();
expect(findCollapsibleListbox().props('selected')).toEqual([
'gid://gitlab/Label/11',
'gid://gitlab/Label/22',
]);
await wrapper.setProps({ createdLabelId: 'gid://gitlab/Label/33' });
expect(findCollapsibleListbox().props('selected')).toEqual([
'gid://gitlab/Label/11',
'gid://gitlab/Label/22',
'gid://gitlab/Label/33',
]);
});
});
});
});

View File

@ -1,5 +1,5 @@
import { GlLabel } from '@gitlab/ui';
import Vue from 'vue';
import { GlDisclosureDropdown, GlLabel } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -9,6 +9,7 @@ import {
TRACKING_CATEGORY_SHOW,
I18N_WORK_ITEM_ERROR_FETCHING_LABELS,
} from '~/work_items/constants';
import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
import groupLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql';
import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
@ -32,6 +33,7 @@ Vue.use(VueApollo);
const workItemId = 'gid://gitlab/WorkItem/1';
describe('WorkItemLabels component', () => {
/** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
let wrapper;
const label1Id = mockLabels[0].id;
@ -85,8 +87,10 @@ describe('WorkItemLabels component', () => {
[updateWorkItemMutation, updateWorkItemMutationHandler],
]),
provide: {
canAdminLabel: true,
isGroup,
issuesListPath: 'test-project-path/issues',
labelsManagePath: 'test-project-path/labels',
},
propsData: {
workItemId,
@ -101,8 +105,10 @@ describe('WorkItemLabels component', () => {
const findWorkItemSidebarDropdownWidget = () =>
wrapper.findComponent(WorkItemSidebarDropdownWidget);
const findAllLabels = () => wrapper.findAllComponents(GlLabel);
const findDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findRegularLabel = () => findAllLabels().at(0);
const findLabelWithDescription = () => findAllLabels().at(2);
const findDropdownContentsCreateView = () => wrapper.findComponent(DropdownContentsCreateView);
const showDropdown = () => {
findWorkItemSidebarDropdownWidget().vm.$emit('dropdownShown');
@ -144,6 +150,7 @@ describe('WorkItemLabels component', () => {
headerText: 'Select labels',
resetButtonLabel: 'Clear',
multiSelect: true,
showFooter: true,
itemValue: [],
});
expect(findAllLabels()).toHaveLength(0);
@ -414,4 +421,72 @@ describe('WorkItemLabels component', () => {
expect(groupLabelsQueryHandler).toHaveBeenCalled();
});
});
describe('creating project label', () => {
beforeEach(async () => {
createComponent();
wrapper.findByTestId('create-project-label').vm.$emit('click');
await nextTick();
});
describe('when "Create project label" button is clicked', () => {
it('renders "Create label" dropdown', () => {
expect(findDisclosureDropdown().props()).toMatchObject({
block: true,
startOpened: true,
toggleText: 'No labels',
});
expect(findDropdownContentsCreateView().props()).toEqual({
attrWorkspacePath: 'test-project-path',
fullPath: 'test-project-path',
labelCreateType: 'project',
searchKey: '',
workspaceType: 'project',
});
});
});
describe('when "hideCreateView" event is emitted', () => {
it('hides dropdown', async () => {
expect(findDisclosureDropdown().exists()).toBe(true);
expect(findDropdownContentsCreateView().exists()).toBe(true);
findDropdownContentsCreateView().vm.$emit('hideCreateView');
await nextTick();
expect(findDisclosureDropdown().exists()).toBe(false);
expect(findDropdownContentsCreateView().exists()).toBe(false);
});
});
describe('when "labelCreated" event is emitted', () => {
it('updates "createdLabelId" value and hides dropdown', async () => {
expect(findWorkItemSidebarDropdownWidget().props('createdLabelId')).toBe(undefined);
expect(findDisclosureDropdown().exists()).toBe(true);
expect(findDropdownContentsCreateView().exists()).toBe(true);
findDropdownContentsCreateView().vm.$emit('labelCreated', {
id: 'gid://gitlab/Label/55',
name: 'New label',
});
await nextTick();
expect(findWorkItemSidebarDropdownWidget().props('createdLabelId')).toBe(
'gid://gitlab/Label/55',
);
expect(findDisclosureDropdown().exists()).toBe(false);
expect(findDropdownContentsCreateView().exists()).toBe(false);
});
});
});
it('renders "Manage project labels" link in dropdown', () => {
createComponent();
expect(wrapper.findByTestId('manage-project-labels').text()).toBe('Manage project labels');
expect(wrapper.findByTestId('manage-project-labels').attributes('href')).toBe(
'test-project-path/labels',
);
});
});

View File

@ -42,6 +42,7 @@ describe('Work items router', () => {
apolloProvider: createMockApollo(handlers),
router,
provide: {
canAdminLabel: true,
fullPath: 'full-path',
groupPath: '',
isGroup: false,
@ -50,6 +51,7 @@ describe('Work items router', () => {
hasIterationsFeature: false,
hasOkrsFeature: false,
hasIssuableHealthStatusFeature: false,
labelsManagePath: 'test-project-path/labels',
reportAbusePath: '/report/abuse/path',
},
stubs: {

View File

@ -4,36 +4,55 @@ require "spec_helper"
RSpec.describe WorkItemsHelper, feature_category: :team_planning do
include Devise::Test::ControllerHelpers
describe '#work_items_show_data' do
subject(:work_items_show_data) { helper.work_items_show_data(project) }
describe 'with project context' do
let_it_be(:project) { build(:project) }
let_it_be(:project) { build(:project) }
before do
allow(helper).to receive(:can?).and_return(true)
end
it 'returns the expected data properties' do
expect(work_items_show_data).to include(
{
full_path: project.full_path,
group_path: nil,
issues_list_path: project_issues_path(project),
register_path: new_user_registration_path(redirect_to_referer: 'yes'),
sign_in_path: user_session_path(redirect_to_referer: 'yes'),
new_comment_template_paths:
[{ text: "Your comment templates", href: profile_comment_templates_path }].to_json,
report_abuse_path: add_category_abuse_reports_path
}
)
it 'returns the expected data properties' do
expect(helper.work_items_show_data(project)).to include(
{
can_admin_label: 'true',
full_path: project.full_path,
group_path: nil,
issues_list_path: project_issues_path(project),
labels_manage_path: project_labels_path(project),
register_path: new_user_registration_path(redirect_to_referer: 'yes'),
sign_in_path: user_session_path(redirect_to_referer: 'yes'),
new_comment_template_paths:
[{ text: "Your comment templates", href: profile_comment_templates_path }].to_json,
report_abuse_path: add_category_abuse_reports_path,
default_branch: project.default_branch_or_main
}
)
end
describe 'when project has parent group' do
let_it_be(:group_project) { build(:project, group: build(:group)) }
it 'returns the expected data properties' do
expect(helper.work_items_show_data(group_project)).to include(
{
group_path: group_project.group.full_path
}
)
end
end
end
context 'when project is under a group' do
let(:group) { build(:group) }
let(:group_project) { build(:project, group: group) }
subject(:work_items_show_data) { helper.work_items_show_data(group_project) }
context 'with group context' do
let_it_be(:group) { build(:group) }
it 'returns the expected group_path' do
expect(work_items_show_data).to include(
expect(helper.work_items_show_data(group)).to include(
{
group_path: group_project.group.full_path
issues_list_path: issues_group_path(group),
labels_manage_path: group_labels_path(group),
default_branch: nil
}
)
end
@ -75,10 +94,12 @@ RSpec.describe WorkItemsHelper, feature_category: :team_planning do
subject(:work_items_list_data) { helper.work_items_list_data(group, current_user) }
it 'returns expected data' do
before do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:can?).and_return(true)
end
it 'returns expected data' do
expect(work_items_list_data).to include(
{
full_path: group.full_path,

View File

@ -80,24 +80,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EnsureResourceGroups do
context "when there are nested variables" do
let(:resource_group_key) { '${NESTED_VAR}_GROUP' }
context "when the ci_expand_nested_resource_group_variables feature flag is not true" do
before do
stub_feature_flags(ci_expand_nested_resource_group_variables: false)
end
it 'does not expand nested variables before creating the group' do
expect { subject }.to change { Ci::ResourceGroup.count }.by(1)
expect(project.resource_groups.find_by_key('${VAR}ST_GROUP')).to be_present
expect(job.options[:resource_group_key]).to be_nil
end
end
context "when the ci_expand_nested_resource_group_variables feature flag is true" do
it 'expands all of the nested variables before creating the group' do
expect { subject }.to change { Ci::ResourceGroup.count }.by(1)
expect(project.resource_groups.find_by_key('TEST_GROUP')).to be_present
expect(job.options[:resource_group_key]).to be_nil
end
it 'expands all of the nested variables before creating the group' do
expect { subject }.to change { Ci::ResourceGroup.count }.by(1)
expect(project.resource_groups.find_by_key('TEST_GROUP')).to be_present
expect(job.options[:resource_group_key]).to be_nil
end
end
end

View File

@ -98,7 +98,7 @@ RSpec.shared_examples 'labels sidebar widget' do
it 'creates new label' do
page.within(labels_widget) do
fill_in 'Name new label', with: 'wontfix'
fill_in 'Label name', with: 'wontfix'
click_link 'Magenta-pink'
click_button 'Create'
@ -108,7 +108,7 @@ RSpec.shared_examples 'labels sidebar widget' do
it 'shows error message if label title is taken' do
page.within(labels_widget) do
fill_in 'Name new label', with: development.title
fill_in 'Label name', with: development.title
click_link 'Magenta-pink'
click_button 'Create'

View File

@ -197,63 +197,54 @@ RSpec.shared_examples 'work items assignees' do
end
RSpec.shared_examples 'work items labels' do
let(:label_title_selector) { '[data-testid="labels-title"]' }
let(:labels_input_selector) { '[data-testid="work-item-labels-input"]' }
let(:work_item_labels_selector) { '[data-testid="work-item-labels"]' }
it 'adds and removes a label' do
within_testid 'work-item-labels' do
expect(page).not_to have_css '.gl-label', text: label.title
it 'successfully applies the label by searching' do
expect(work_item.reload.labels).not_to include(label)
click_button 'Edit'
select_listbox_item(label.title)
click_button 'Apply'
find_and_click_edit(work_item_labels_selector)
expect(page).to have_css '.gl-label', text: label.title
select_listbox_item(label.title)
click_button 'Edit'
click_button 'Clear'
find("body").click
wait_for_all_requests
expect(work_item.reload.labels).to include(label)
within(work_item_labels_selector) do
expect(page).to have_link(label.title)
expect(page).not_to have_css '.gl-label', text: label.title
end
end
it 'successfully removes all users on clear all button click' do
expect(work_item.reload.labels).not_to include(label)
find_and_click_edit(work_item_labels_selector)
select_listbox_item(label.title)
find("body").click
wait_for_requests
expect(work_item.reload.labels).to include(label)
find_and_click_edit(work_item_labels_selector)
find_and_click_clear(work_item_labels_selector)
wait_for_all_requests
expect(work_item.reload.labels).not_to include(label)
end
it 'updates the assignee in real-time' do
it 'updates the assigned labels in real-time when another user updates the label' do
using_session :other_session do
visit work_items_path
expect(work_item.reload.labels).not_to include(label)
expect(page).not_to have_css '.gl-label', text: label.title
end
find_and_click_edit(work_item_labels_selector)
within_testid 'work-item-labels' do
click_button 'Edit'
select_listbox_item(label.title)
click_button 'Apply'
select_listbox_item(label.title)
expect(page).to have_css '.gl-label', text: label.title
end
find("body").click
wait_for_all_requests
expect(work_item.reload.labels).to include(label)
expect(page).to have_css '.gl-label', text: label.title
using_session :other_session do
expect(work_item.reload.labels).to include(label)
expect(page).to have_css '.gl-label', text: label.title
end
end
it 'creates, auto-selects, and adds new label' do
within_testid 'work-item-labels' do
click_button 'Edit'
click_button 'Create project label'
send_keys 'Quintessence'
click_button 'Create'
click_button 'Apply'
expect(page).to have_css '.gl-label', text: 'Quintessence'
end
end
end

View File

@ -30,7 +30,7 @@ RSpec.describe 'projects/issues/_related_issues.html.haml', feature_category: :t
render
expect(rendered).to have_selector(
".js-related-issues-root[data-report-abuse-path=\"#{add_category_abuse_reports_path}\"]"
".js-related-issues-root[data-wi-report-abuse-path=\"#{add_category_abuse_reports_path}\"]"
)
end
end