Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
311ee28119
commit
2647690038
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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-]+)*))?$/;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) } }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue