Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ed79d7cc5b
commit
ef69661413
|
|
@ -292,7 +292,6 @@ export default {
|
|||
'app/assets/javascripts/work_items/components/work_item_detail_modal.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_development/work_item_create_branch_merge_request_modal.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_development/work_item_development_mr_item.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_labels.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_links/work_item_groups_listbox.vue',
|
||||
'app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue',
|
||||
|
|
|
|||
4
Gemfile
4
Gemfile
|
|
@ -401,7 +401,7 @@ gem 'sentry-sidekiq', '~> 5.22.0', feature_category: :observability
|
|||
|
||||
# PostgreSQL query parsing
|
||||
#
|
||||
gem 'pg_query', '~> 6.0.0', feature_category: :database
|
||||
gem 'pg_query', '~> 6.1.0', feature_category: :database
|
||||
|
||||
gem 'gitlab-schema-validation', path: 'gems/gitlab-schema-validation', feature_category: :shared
|
||||
gem 'gitlab-http', path: 'gems/gitlab-http', feature_category: :shared
|
||||
|
|
@ -521,7 +521,7 @@ group :development, :test do
|
|||
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
|
||||
gem 'minitest', '~> 5.11.0', feature_category: :shared
|
||||
|
||||
gem 'spring', '~> 4.1.0', feature_category: :shared
|
||||
gem 'spring', '~> 4.3.0', feature_category: :shared
|
||||
gem 'spring-commands-rspec', '~> 1.0.4', feature_category: :shared
|
||||
|
||||
gem 'gitlab-styles', '~> 13.1.0', feature_category: :tooling, require: false
|
||||
|
|
|
|||
|
|
@ -513,7 +513,7 @@
|
|||
{"name":"pg","version":"1.5.9","platform":"x64-mingw-ucrt","checksum":"9d9d6a4fcc25251312065b61f94eb56c5266007c0e747606704641d47b92c5eb"},
|
||||
{"name":"pg","version":"1.5.9","platform":"x64-mingw32","checksum":"02a682056d3db6677e0ed5b233e69383d20641785ba0123cdf56a5eb8286a013"},
|
||||
{"name":"pg","version":"1.5.9","platform":"x86-mingw32","checksum":"f32a3cde1018a16f0b3392f654f763fd7ef3f6af4fee008312ad7d3575b3c0ab"},
|
||||
{"name":"pg_query","version":"6.0.0","platform":"ruby","checksum":"fbf09a4e900cee1d61e2bbfda1fefdbc35bc83c5f1c7ae1be1c6ffc5ae0f5c04"},
|
||||
{"name":"pg_query","version":"6.1.0","platform":"ruby","checksum":"8b005229e209f12c5887c34c60d0eb2a241953b9475b53a9840d24578532481e"},
|
||||
{"name":"plist","version":"3.7.0","platform":"ruby","checksum":"703ca90a7cb00e8263edd03da2266627f6741d280c910abbbac07c95ffb2f073"},
|
||||
{"name":"png_quantizator","version":"0.2.1","platform":"ruby","checksum":"6023d4d064125c3a7e02929c95b7320ed6ac0d7341f9e8de0c9ea6576ef3106b"},
|
||||
{"name":"premailer","version":"1.23.0","platform":"ruby","checksum":"f0d7f6ba299559c96ddf982aa5263f85e5617c86437c8d8ffff120813b2d7efb"},
|
||||
|
|
@ -701,7 +701,7 @@
|
|||
{"name":"solargraph","version":"0.47.2","platform":"ruby","checksum":"87ca4b799b9155c2c31c15954c483e952fdacd800f52d6709b901dd447bcac6a"},
|
||||
{"name":"sorbet-runtime","version":"0.5.11647","platform":"ruby","checksum":"64b65112f2e6a5323310ca9ac0d7d9a6be63aade5a62a6225fe066042ff4fdb6"},
|
||||
{"name":"spamcheck","version":"1.3.3","platform":"ruby","checksum":"3a29ba9dfcd59543d88054d38c657f79e0a6cf44d763df08ad47680abed50ec7"},
|
||||
{"name":"spring","version":"4.1.0","platform":"ruby","checksum":"f17f080fb0df558d663c897a6229ed3d5cc54819ab51876ea6eef49a67f0a3cb"},
|
||||
{"name":"spring","version":"4.3.0","platform":"ruby","checksum":"0aaaf3bcce38e8528275854881d1922660d76cbd19a9a3af4a419d95b7fe7122"},
|
||||
{"name":"spring-commands-rspec","version":"1.0.4","platform":"ruby","checksum":"6202e54fa4767452e3641461a83347645af478bf45dddcca9737b43af0dd1a2c"},
|
||||
{"name":"sprite-factory","version":"1.7.1","platform":"ruby","checksum":"5586524a1aec003241f1abc6852b61433e988aba5ee2b55f906387bf49b01ba2"},
|
||||
{"name":"sprockets","version":"3.7.2","platform":"ruby","checksum":"5ea1d7facd09203c1aa196afd6178208cd25abdbcc2a9978810a2f0754e152a0"},
|
||||
|
|
|
|||
|
|
@ -1451,7 +1451,7 @@ GEM
|
|||
peek (1.1.0)
|
||||
railties (>= 4.0.0)
|
||||
pg (1.5.9)
|
||||
pg_query (6.0.0)
|
||||
pg_query (6.1.0)
|
||||
google-protobuf (>= 3.25.3)
|
||||
plist (3.7.0)
|
||||
png_quantizator (0.2.1)
|
||||
|
|
@ -1806,7 +1806,7 @@ GEM
|
|||
sorbet-runtime (0.5.11647)
|
||||
spamcheck (1.3.3)
|
||||
grpc (~> 1.63)
|
||||
spring (4.1.0)
|
||||
spring (4.3.0)
|
||||
spring-commands-rspec (1.0.4)
|
||||
spring (>= 0.9.1)
|
||||
sprite-factory (1.7.1)
|
||||
|
|
@ -2262,7 +2262,7 @@ DEPENDENCIES
|
|||
parslet (~> 1.8)
|
||||
peek (~> 1.1)
|
||||
pg (~> 1.5.6)
|
||||
pg_query (~> 6.0.0)
|
||||
pg_query (~> 6.1.0)
|
||||
png_quantizator (~> 0.2.1)
|
||||
premailer-rails (~> 1.12.0)
|
||||
prometheus-client-mmap (~> 1.2.8)
|
||||
|
|
@ -2328,7 +2328,7 @@ DEPENDENCIES
|
|||
snowplow-tracker (~> 0.8.0)
|
||||
solargraph (~> 0.47.2)
|
||||
spamcheck (~> 1.3.0)
|
||||
spring (~> 4.1.0)
|
||||
spring (~> 4.3.0)
|
||||
spring-commands-rspec (~> 1.0.4)
|
||||
sprite-factory (~> 1.7)
|
||||
sprockets (~> 3.7.0)
|
||||
|
|
|
|||
|
|
@ -516,7 +516,7 @@
|
|||
{"name":"pg","version":"1.5.9","platform":"x64-mingw-ucrt","checksum":"9d9d6a4fcc25251312065b61f94eb56c5266007c0e747606704641d47b92c5eb"},
|
||||
{"name":"pg","version":"1.5.9","platform":"x64-mingw32","checksum":"02a682056d3db6677e0ed5b233e69383d20641785ba0123cdf56a5eb8286a013"},
|
||||
{"name":"pg","version":"1.5.9","platform":"x86-mingw32","checksum":"f32a3cde1018a16f0b3392f654f763fd7ef3f6af4fee008312ad7d3575b3c0ab"},
|
||||
{"name":"pg_query","version":"6.0.0","platform":"ruby","checksum":"fbf09a4e900cee1d61e2bbfda1fefdbc35bc83c5f1c7ae1be1c6ffc5ae0f5c04"},
|
||||
{"name":"pg_query","version":"6.1.0","platform":"ruby","checksum":"8b005229e209f12c5887c34c60d0eb2a241953b9475b53a9840d24578532481e"},
|
||||
{"name":"plist","version":"3.7.0","platform":"ruby","checksum":"703ca90a7cb00e8263edd03da2266627f6741d280c910abbbac07c95ffb2f073"},
|
||||
{"name":"png_quantizator","version":"0.2.1","platform":"ruby","checksum":"6023d4d064125c3a7e02929c95b7320ed6ac0d7341f9e8de0c9ea6576ef3106b"},
|
||||
{"name":"pp","version":"0.6.2","platform":"ruby","checksum":"947ec3120c6f92195f8ee8aa25a7b2c5297bb106d83b41baa02983686577b6ff"},
|
||||
|
|
@ -712,7 +712,7 @@
|
|||
{"name":"solargraph","version":"0.47.2","platform":"ruby","checksum":"87ca4b799b9155c2c31c15954c483e952fdacd800f52d6709b901dd447bcac6a"},
|
||||
{"name":"sorbet-runtime","version":"0.5.11647","platform":"ruby","checksum":"64b65112f2e6a5323310ca9ac0d7d9a6be63aade5a62a6225fe066042ff4fdb6"},
|
||||
{"name":"spamcheck","version":"1.3.3","platform":"ruby","checksum":"3a29ba9dfcd59543d88054d38c657f79e0a6cf44d763df08ad47680abed50ec7"},
|
||||
{"name":"spring","version":"4.1.0","platform":"ruby","checksum":"f17f080fb0df558d663c897a6229ed3d5cc54819ab51876ea6eef49a67f0a3cb"},
|
||||
{"name":"spring","version":"4.3.0","platform":"ruby","checksum":"0aaaf3bcce38e8528275854881d1922660d76cbd19a9a3af4a419d95b7fe7122"},
|
||||
{"name":"spring-commands-rspec","version":"1.0.4","platform":"ruby","checksum":"6202e54fa4767452e3641461a83347645af478bf45dddcca9737b43af0dd1a2c"},
|
||||
{"name":"sprite-factory","version":"1.7.1","platform":"ruby","checksum":"5586524a1aec003241f1abc6852b61433e988aba5ee2b55f906387bf49b01ba2"},
|
||||
{"name":"sprockets","version":"3.7.2","platform":"ruby","checksum":"5ea1d7facd09203c1aa196afd6178208cd25abdbcc2a9978810a2f0754e152a0"},
|
||||
|
|
|
|||
|
|
@ -1468,7 +1468,7 @@ GEM
|
|||
peek (1.1.0)
|
||||
railties (>= 4.0.0)
|
||||
pg (1.5.9)
|
||||
pg_query (6.0.0)
|
||||
pg_query (6.1.0)
|
||||
google-protobuf (>= 3.25.3)
|
||||
plist (3.7.0)
|
||||
png_quantizator (0.2.1)
|
||||
|
|
@ -1839,7 +1839,7 @@ GEM
|
|||
sorbet-runtime (0.5.11647)
|
||||
spamcheck (1.3.3)
|
||||
grpc (~> 1.63)
|
||||
spring (4.1.0)
|
||||
spring (4.3.0)
|
||||
spring-commands-rspec (1.0.4)
|
||||
spring (>= 0.9.1)
|
||||
sprite-factory (1.7.1)
|
||||
|
|
@ -2296,7 +2296,7 @@ DEPENDENCIES
|
|||
parslet (~> 1.8)
|
||||
peek (~> 1.1)
|
||||
pg (~> 1.5.6)
|
||||
pg_query (~> 6.0.0)
|
||||
pg_query (~> 6.1.0)
|
||||
png_quantizator (~> 0.2.1)
|
||||
premailer-rails (~> 1.12.0)
|
||||
prometheus-client-mmap (~> 1.2.8)
|
||||
|
|
@ -2362,7 +2362,7 @@ DEPENDENCIES
|
|||
snowplow-tracker (~> 0.8.0)
|
||||
solargraph (~> 0.47.2)
|
||||
spamcheck (~> 1.3.0)
|
||||
spring (~> 4.1.0)
|
||||
spring (~> 4.3.0)
|
||||
spring-commands-rspec (~> 1.0.4)
|
||||
sprite-factory (~> 1.7)
|
||||
sprockets (~> 3.7.0)
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ export default {
|
|||
/>
|
||||
<div v-else class="gl-pr-3"><gl-loading-icon size="sm" inline /></div>
|
||||
<div class="gl-flex gl-min-w-0 gl-flex-1 gl-flex-col">
|
||||
<span class="gl-truncate" data-testid="downstream-title-content">
|
||||
<span class="gl-whitespace-normal" data-testid="downstream-title-content">
|
||||
{{ downstreamTitle }}
|
||||
</span>
|
||||
<div class="gl-truncate">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@ const markdownToAst = (markdown) => {
|
|||
return unified().use(remarkParse).parse(markdown);
|
||||
};
|
||||
|
||||
export const transformQuickActions = (markdown) => {
|
||||
// ensure 3 newlines after all quick actions so that
|
||||
// any reference style links after it get correctly parsed
|
||||
return markdown.replace(/^\/(.+?)\n/gm, '/$1\n\n\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts link reference definitions from a markdown string.
|
||||
* This is useful for preserving reference definitions when
|
||||
|
|
@ -44,21 +50,22 @@ export default ({ render }) => {
|
|||
* @returns {{ document: ProseMirror.Node }}
|
||||
*/
|
||||
deserialize: async ({ schema, markdown }) => {
|
||||
const html = markdown ? (await render(markdown)).body : '<p></p>';
|
||||
const transformedMarkdown = transformQuickActions(markdown);
|
||||
const html = markdown ? (await render(transformedMarkdown)).body : '<p></p>';
|
||||
const parser = new DOMParser();
|
||||
const { body } = parser.parseFromString(`<body>${html}</body>`, 'text/html');
|
||||
|
||||
replaceCommentsWith(body, 'comment');
|
||||
|
||||
// append original source as a comment that nodes can access
|
||||
body.append(document.createComment(markdown));
|
||||
body.append(document.createComment(transformedMarkdown));
|
||||
|
||||
const doc = ProseMirror.DOMParser.fromSchema(schema).parse(body);
|
||||
|
||||
if (preserveMarkdown())
|
||||
doc.attrs = {
|
||||
source: markdown,
|
||||
referenceDefinitions: extractReferenceDefinitions(markdown),
|
||||
source: transformedMarkdown,
|
||||
referenceDefinitions: extractReferenceDefinitions(transformedMarkdown),
|
||||
};
|
||||
|
||||
return { document: doc };
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
<script>
|
||||
import { GlIcon, GlBadge, GlFormInput, GlButton, GlLink, GlTooltip, GlSprintf } from '@gitlab/ui';
|
||||
import {
|
||||
GlIcon,
|
||||
GlBadge,
|
||||
GlFormInput,
|
||||
GlButton,
|
||||
GlLink,
|
||||
GlTooltip,
|
||||
GlSprintf,
|
||||
GlModal,
|
||||
} from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import { __ } from '~/locale';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { __, s__ } from '~/locale';
|
||||
import HelpPageLink from '~/vue_shared/components/help_page_link/help_page_link.vue';
|
||||
import HelpPopover from '~/vue_shared/components/help_popover.vue';
|
||||
import ImportTargetDropdown from '../../components/import_target_dropdown.vue';
|
||||
import ImportStatus from '../../components/import_status.vue';
|
||||
|
|
@ -13,6 +22,7 @@ import { isProjectImportable, isImporting, isIncompatible, getImportStatus } fro
|
|||
export default {
|
||||
name: 'ProviderRepoTableRow',
|
||||
components: {
|
||||
HelpPageLink,
|
||||
HelpPopover,
|
||||
ImportStatus,
|
||||
ImportTargetDropdown,
|
||||
|
|
@ -23,6 +33,7 @@ export default {
|
|||
GlLink,
|
||||
GlTooltip,
|
||||
GlSprintf,
|
||||
GlModal,
|
||||
},
|
||||
inject: {
|
||||
userNamespace: {
|
||||
|
|
@ -48,6 +59,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
isSelectedForReimport: false,
|
||||
showMembershipsModal: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -61,7 +73,7 @@ export default {
|
|||
|
||||
showMembershipsWarning() {
|
||||
const userNamespaceSelected = this.importTarget.targetNamespace === this.userNamespace;
|
||||
return this.isImportNotStarted && userNamespaceSelected;
|
||||
return (this.isImportNotStarted || this.isSelectedForReimport) && userNamespaceSelected;
|
||||
},
|
||||
|
||||
isFinished() {
|
||||
|
|
@ -143,15 +155,20 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
onImportClick() {
|
||||
if (this.showMembershipsWarning) {
|
||||
this.showMembershipsModal = true;
|
||||
} else {
|
||||
this.handleImportRepo();
|
||||
}
|
||||
},
|
||||
|
||||
onSelect(value) {
|
||||
this.updateImportTarget({ targetNamespace: value });
|
||||
},
|
||||
},
|
||||
|
||||
helpPath: helpPagePath('/user/project/import/github'),
|
||||
membershipsHelpPath: helpPagePath('user/project/import/_index', {
|
||||
anchor: 'user-contribution-and-membership-mapping',
|
||||
}),
|
||||
actionPrimary: { text: s__('ImportProjects|Continue import') },
|
||||
actionCancel: { text: __('Cancel') },
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -223,7 +240,8 @@ export default {
|
|||
'ImportProjects|Imported files will be kept. You can import this repository again later.',
|
||||
)
|
||||
}}
|
||||
<gl-link :href="$options.helpPath" target="_blank">{{ __('Learn more.') }}</gl-link>
|
||||
<help-page-link href="/user/project/import/github">{{ __('Learn more') }}</help-page-link
|
||||
>.
|
||||
</div>
|
||||
</gl-tooltip>
|
||||
<gl-button
|
||||
|
|
@ -239,10 +257,33 @@ export default {
|
|||
v-if="isImportNotStarted || isFinished"
|
||||
type="button"
|
||||
data-testid="import-button"
|
||||
@click="handleImportRepo()"
|
||||
@click="onImportClick"
|
||||
>
|
||||
{{ importButtonText }}
|
||||
</gl-button>
|
||||
<gl-modal
|
||||
v-if="showMembershipsWarning"
|
||||
v-model="showMembershipsModal"
|
||||
:title="
|
||||
s__('ImportProjects|Are you sure you want to import the project to a personal namespace?')
|
||||
"
|
||||
:action-primary="$options.actionPrimary"
|
||||
:action-cancel="$options.actionCancel"
|
||||
@primary="handleImportRepo"
|
||||
>
|
||||
<p>
|
||||
{{
|
||||
s__(
|
||||
'ImportProjects|Importing a project into a personal namespace results in all contributions being mapped to the same bot user and they cannot be reassigned. To map contributions to actual users, import the project to a group instead.',
|
||||
)
|
||||
}}
|
||||
<help-page-link
|
||||
href="/user/project/import/_index"
|
||||
anchor="user-contribution-and-membership-mapping"
|
||||
>{{ __('Learn more') }}</help-page-link
|
||||
>.
|
||||
</p>
|
||||
</gl-modal>
|
||||
<span class="gl-ml-3 gl-inline-flex gl-gap-3">
|
||||
<help-popover
|
||||
v-show="showMembershipsWarning"
|
||||
|
|
@ -252,12 +293,14 @@ export default {
|
|||
>
|
||||
{{
|
||||
s__(
|
||||
'ImportProjects|Importing a project into a personal namespace results in all contributions being mapped to the same bot user. To map contributions to real users, import projects into a group instead.',
|
||||
'ImportProjects|Importing a project into a personal namespace results in all contributions being mapped to the same bot user and they cannot be reassigned. To map contributions to actual users, import the project to a group instead.',
|
||||
)
|
||||
}}
|
||||
<gl-link :href="$options.membershipsHelpPath" target="_blank">{{
|
||||
__('Learn more.')
|
||||
}}</gl-link>
|
||||
<help-page-link
|
||||
href="/user/project/import/_index"
|
||||
anchor="user-contribution-and-membership-mapping"
|
||||
>{{ __('Learn more') }}</help-page-link
|
||||
>.
|
||||
</help-popover>
|
||||
|
||||
<help-popover v-if="isFinished" icon="information-o">
|
||||
|
|
|
|||
|
|
@ -27,8 +27,6 @@ export default {
|
|||
Suggestions,
|
||||
DuoCodeReviewFeedback: () =>
|
||||
import('ee_component/notes/components/duo_code_review_feedback.vue'),
|
||||
AmazonQFixButton: () =>
|
||||
import('ee_component/merge_requests/components/amazon_q/amazon_q_fix_button.vue'),
|
||||
},
|
||||
directives: {
|
||||
SafeHtml,
|
||||
|
|
@ -246,7 +244,6 @@ export default {
|
|||
@handleFormUpdate="handleFormUpdate"
|
||||
@cancelForm="formCancelHandler"
|
||||
/>
|
||||
<amazon-q-fix-button :note="note" class="gl-mt-3" />
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<textarea
|
||||
v-if="canEdit"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { GlButton, GlDisclosureDropdown, GlLabel } from '@gitlab/ui';
|
||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import { difference, unionBy } from 'lodash';
|
||||
import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
|
||||
import { __, n__, s__ } from '~/locale';
|
||||
|
|
@ -64,27 +63,30 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
searchLabels: [],
|
||||
searchTerm: '',
|
||||
searchStarted: false,
|
||||
showLabelForm: false,
|
||||
updateInProgress: false,
|
||||
workItem: {},
|
||||
createdLabelId: undefined,
|
||||
removeLabelIds: [],
|
||||
addLabelIds: [],
|
||||
labelsCache: [],
|
||||
labelsToShowAtTopOfTheListbox: [],
|
||||
labelsToShowAtTopOfListbox: [],
|
||||
shortcut: ISSUABLE_CHANGE_LABEL,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
createFlow() {
|
||||
isCreateFlow() {
|
||||
return this.workItemId === newWorkItemId(this.workItemType);
|
||||
},
|
||||
workItemFullPath() {
|
||||
return this.createFlow
|
||||
return this.isCreateFlow
|
||||
? newWorkItemFullPath(this.fullPath, this.workItemType)
|
||||
: this.fullPath;
|
||||
},
|
||||
// eslint-disable-next-line vue/no-unused-properties
|
||||
tracking() {
|
||||
return {
|
||||
category: TRACKING_CATEGORY_SHOW,
|
||||
|
|
@ -92,55 +94,38 @@ export default {
|
|||
property: `type_${this.workItemType}`,
|
||||
};
|
||||
},
|
||||
areLabelsSelected() {
|
||||
return this.addLabelIds.length > 0 || this.itemValues.length > 0;
|
||||
},
|
||||
selectedLabelCount() {
|
||||
return this.addLabelIds.length + this.itemValues.length - this.removeLabelIds.length;
|
||||
},
|
||||
dropDownLabelText() {
|
||||
return n__('%d label', '%d labels', this.selectedLabelCount);
|
||||
},
|
||||
dropdownText() {
|
||||
return this.areLabelsSelected ? `${this.dropDownLabelText}` : __('No labels');
|
||||
const selectedLabelsCount =
|
||||
this.addLabelIds.length + this.widgetLabelsIds.length - this.removeLabelIds.length;
|
||||
return this.addLabelIds.length > 0 || this.widgetLabelsIds.length > 0
|
||||
? n__('%d label', '%d labels', selectedLabelsCount)
|
||||
: __('No labels');
|
||||
},
|
||||
isLoadingLabels() {
|
||||
return this.$apollo.queries.searchLabels.loading;
|
||||
},
|
||||
visibleLabels() {
|
||||
if (this.searchTerm) {
|
||||
return fuzzaldrinPlus.filter(this.searchLabels, this.searchTerm, {
|
||||
key: ['title'],
|
||||
});
|
||||
listboxItems() {
|
||||
const formattedSearchLabels = this.searchLabels.map(formatLabelForListbox);
|
||||
|
||||
if (this.searchTerm || this.widgetLabelsIds.length === 0) {
|
||||
return formattedSearchLabels;
|
||||
}
|
||||
|
||||
return this.searchLabels;
|
||||
},
|
||||
labelsList() {
|
||||
const visibleLabels = this.visibleLabels?.map(formatLabelForListbox) || [];
|
||||
|
||||
if (this.searchTerm || this.itemValues.length === 0) {
|
||||
return visibleLabels;
|
||||
}
|
||||
|
||||
const selectedLabels = this.labelsToShowAtTopOfTheListbox.map(formatLabelForListbox) || [];
|
||||
const unselectedLabels = visibleLabels.filter(
|
||||
({ value }) => !this.labelsToShowAtTopOfTheListbox.find((l) => l.id === value),
|
||||
);
|
||||
const selectedLabels = this.labelsToShowAtTopOfListbox.map(formatLabelForListbox);
|
||||
|
||||
return [
|
||||
{ options: selectedLabels, text: __('Selected') },
|
||||
{ options: unselectedLabels, text: __('All'), textSrOnly: true },
|
||||
{ text: __('Selected'), options: selectedLabels },
|
||||
{ text: __('All'), textSrOnly: true, options: formattedSearchLabels },
|
||||
];
|
||||
},
|
||||
labelsWidget() {
|
||||
return findLabelsWidget(this.workItem);
|
||||
},
|
||||
localLabels() {
|
||||
widgetLabels() {
|
||||
return this.labelsWidget?.labels?.nodes || [];
|
||||
},
|
||||
itemValues() {
|
||||
return this.localLabels.map(({ id }) => id);
|
||||
widgetLabelsIds() {
|
||||
return this.widgetLabels.map(({ id }) => id);
|
||||
},
|
||||
allowsScopedLabels() {
|
||||
return this.labelsWidget?.allowsScopedLabels;
|
||||
|
|
@ -158,21 +143,20 @@ export default {
|
|||
watch: {
|
||||
searchTerm(newVal, oldVal) {
|
||||
if (newVal === '' && oldVal !== '') {
|
||||
const selectedIds = [...this.itemValues, ...this.addLabelIds].filter(
|
||||
const selectedIds = [...this.widgetLabelsIds, ...this.addLabelIds].filter(
|
||||
(x) => !this.removeLabelIds.includes(x),
|
||||
);
|
||||
|
||||
this.labelsToShowAtTopOfTheListbox = this.labelsCache.filter(({ id }) =>
|
||||
this.labelsToShowAtTopOfListbox = this.labelsCache.filter(({ id }) =>
|
||||
selectedIds.includes(id),
|
||||
);
|
||||
}
|
||||
},
|
||||
localLabels(newVal) {
|
||||
this.labelsToShowAtTopOfTheListbox = newVal;
|
||||
widgetLabels(newVal) {
|
||||
this.labelsToShowAtTopOfListbox = newVal;
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
// eslint-disable-next-line @gitlab/vue-no-undef-apollo-properties
|
||||
workItem: {
|
||||
query: workItemByIidQuery,
|
||||
variables() {
|
||||
|
|
@ -195,7 +179,6 @@ export default {
|
|||
this.$emit('error', i18n.fetchError);
|
||||
},
|
||||
},
|
||||
// eslint-disable-next-line @gitlab/vue-no-undef-apollo-properties
|
||||
searchLabels: {
|
||||
query() {
|
||||
return this.isGroup ? groupLabelsQuery : projectLabelsQuery;
|
||||
|
|
@ -210,7 +193,7 @@ export default {
|
|||
return !this.searchStarted;
|
||||
},
|
||||
update(data) {
|
||||
return data.workspace?.labels?.nodes;
|
||||
return data.workspace?.labels?.nodes ?? [];
|
||||
},
|
||||
result({ data }) {
|
||||
const labels = data?.workspace?.labels?.nodes || [];
|
||||
|
|
@ -231,30 +214,33 @@ export default {
|
|||
},
|
||||
search(searchTerm) {
|
||||
this.searchTerm = searchTerm;
|
||||
this.searchStarted = true;
|
||||
},
|
||||
removeLabel({ id }) {
|
||||
this.removeLabelIds.push(id);
|
||||
this.updateLabels();
|
||||
},
|
||||
updateLabel(labels) {
|
||||
this.removeLabelIds = difference(this.itemValues, labels);
|
||||
this.addLabelIds = difference(labels, this.itemValues);
|
||||
this.removeLabelIds = difference(this.widgetLabelsIds, labels);
|
||||
this.addLabelIds = difference(labels, this.widgetLabelsIds);
|
||||
},
|
||||
async updateLabels(labels) {
|
||||
this.updateInProgress = true;
|
||||
|
||||
if (labels?.length === 0) {
|
||||
this.removeLabelIds = this.itemValues;
|
||||
this.removeLabelIds = this.widgetLabelsIds;
|
||||
this.addLabelIds = [];
|
||||
}
|
||||
|
||||
if (this.createFlow) {
|
||||
const selectedIds = [...this.itemValues, ...this.addLabelIds].filter(
|
||||
if (!this.addLabelIds.length && !this.removeLabelIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateInProgress = true;
|
||||
|
||||
if (this.isCreateFlow) {
|
||||
const selectedIds = [...this.widgetLabelsIds, ...this.addLabelIds].filter(
|
||||
(x) => !this.removeLabelIds.includes(x),
|
||||
);
|
||||
|
||||
this.$apollo.mutate({
|
||||
await this.$apollo.mutate({
|
||||
mutation: updateNewWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
|
|
@ -307,9 +293,6 @@ export default {
|
|||
scopedLabel(label) {
|
||||
return this.allowsScopedLabels && isScopedLabel(label);
|
||||
},
|
||||
isSelected(id) {
|
||||
return this.itemValues.includes(id) || this.addLabelIds.includes(id);
|
||||
},
|
||||
labelFilterUrl(label) {
|
||||
return `${this.issuesListPath}?label_name[]=${encodeURIComponent(label.title)}`;
|
||||
},
|
||||
|
|
@ -329,8 +312,8 @@ export default {
|
|||
:created-label-id="createdLabelId"
|
||||
dropdown-name="label"
|
||||
:loading="isLoadingLabels"
|
||||
:list-items="labelsList"
|
||||
:item-value="itemValues"
|
||||
:list-items="listboxItems"
|
||||
:item-value="widgetLabelsIds"
|
||||
:update-in-progress="updateInProgress"
|
||||
:toggle-dropdown-text="dropdownText"
|
||||
:header-text="__('Select labels')"
|
||||
|
|
@ -357,7 +340,7 @@ export default {
|
|||
<template #readonly>
|
||||
<div class="gl-mt-1 gl-flex gl-flex-wrap gl-gap-2" data-testid="selected-label-content">
|
||||
<gl-label
|
||||
v-for="label in localLabels"
|
||||
v-for="label in widgetLabels"
|
||||
:key="label.id"
|
||||
:title="label.title"
|
||||
:description="label.description"
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class GroupsController < Groups::ApplicationController
|
|||
# Authorize
|
||||
before_action :authorize_admin_group!, only: [:update, :projects, :transfer, :export, :download_export]
|
||||
before_action :authorize_view_edit_page!, only: :edit
|
||||
before_action :authorize_remove_group!, only: :destroy
|
||||
before_action :authorize_remove_group!, only: [:destroy, :restore]
|
||||
before_action :authorize_create_group!, only: [:new]
|
||||
|
||||
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
|
||||
|
|
@ -55,7 +55,7 @@ class GroupsController < Groups::ApplicationController
|
|||
|
||||
feature_category :groups_and_projects, [
|
||||
:index, :new, :create, :show, :edit, :update,
|
||||
:destroy, :details, :transfer, :activity
|
||||
:destroy, :details, :transfer, :activity, :restore
|
||||
]
|
||||
feature_category :team_planning, [:issues, :issues_calendar, :preview_markdown]
|
||||
feature_category :code_review_workflow, [:merge_requests, :unfoldered_environment_names]
|
||||
|
|
@ -180,18 +180,51 @@ class GroupsController < Groups::ApplicationController
|
|||
end
|
||||
|
||||
def destroy
|
||||
Groups::DestroyService.new(@group, current_user).async_execute
|
||||
message = format(_("Group '%{group_name}' is being deleted."), group_name: @group.full_name)
|
||||
return destroy_immediately unless group.adjourned_deletion?
|
||||
return destroy_immediately if group.marked_for_deletion? && ::Gitlab::Utils.to_boolean(params[:permanently_remove])
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
flash[:toast] = message
|
||||
redirect_to root_path, status: :found
|
||||
end
|
||||
result = ::Groups::MarkForDeletionService.new(group, current_user).execute
|
||||
|
||||
format.json do
|
||||
render json: { message: message }
|
||||
if result[:status] == :success
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to group_path(group), status: :found
|
||||
end
|
||||
|
||||
format.json do
|
||||
render json: {
|
||||
message: format(
|
||||
_("'%{group_name}' has been scheduled for deletion and will be deleted on %{date}."),
|
||||
group_name: group.name,
|
||||
# FIXME: Replace `group.marked_for_deletion_on` with `group` after https://gitlab.com/gitlab-org/gitlab/-/work_items/527085
|
||||
date: helpers.permanent_deletion_date_formatted(group.marked_for_deletion_on)
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to edit_group_path(group), status: :found, alert: result[:message]
|
||||
end
|
||||
|
||||
format.json do
|
||||
render json: { message: result[:message] }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def restore
|
||||
return render_404 unless group.marked_for_deletion?
|
||||
|
||||
result = ::Groups::RestoreService.new(group, current_user).execute
|
||||
|
||||
if result[:status] == :success
|
||||
redirect_to edit_group_path(group),
|
||||
notice: format(_("Group '%{group_name}' has been successfully restored."), group_name: group.full_name)
|
||||
else
|
||||
redirect_to edit_group_path(group), alert: result[:message]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -368,6 +401,22 @@ class GroupsController < Groups::ApplicationController
|
|||
def has_project_list?
|
||||
%w[details show index].include?(action_name)
|
||||
end
|
||||
|
||||
def destroy_immediately
|
||||
Groups::DestroyService.new(@group, current_user).async_execute
|
||||
message = format(_("Group '%{group_name}' is being deleted."), group_name: @group.full_name)
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
flash[:toast] = message
|
||||
redirect_to root_path, status: :found
|
||||
end
|
||||
|
||||
format.json do
|
||||
render json: { message: message }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
GroupsController.prepend_mod
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
module Emails
|
||||
module Projects
|
||||
include NamespacesHelper
|
||||
|
||||
def project_was_moved_email(project_id, user_id, old_path_with_namespace)
|
||||
@current_user = @user = User.find user_id
|
||||
@project = Project.find project_id
|
||||
|
|
@ -30,6 +32,18 @@ module Emails
|
|||
)
|
||||
end
|
||||
|
||||
def project_scheduled_for_deletion(recipient_id, project_id)
|
||||
@project = ::Project.find(project_id)
|
||||
@user = ::User.find(recipient_id)
|
||||
@deletion_due_in_days = ::Gitlab::CurrentSettings.deletion_adjourned_period.days
|
||||
@deletion_date = permanent_deletion_date_formatted(@project.marked_for_deletion_on, format: '%B %-d, %Y')
|
||||
|
||||
email_with_layout(
|
||||
to: @user.email,
|
||||
subject: subject('Project scheduled for deletion')
|
||||
)
|
||||
end
|
||||
|
||||
def repository_cleanup_success_email(project, user)
|
||||
@project = project
|
||||
@user = user
|
||||
|
|
|
|||
|
|
@ -435,6 +435,14 @@ class NotifyPreview < ActionMailer::Preview
|
|||
Notify.repository_rewrite_history_failure_email(project, user, 'Error message')
|
||||
end
|
||||
|
||||
def project_scheduled_for_deletion
|
||||
cleanup do
|
||||
project.update!(marked_for_deletion_at: Time.current)
|
||||
|
||||
::Notify.project_scheduled_for_deletion(user.id, project.id).message
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project
|
||||
|
|
|
|||
|
|
@ -21,5 +21,3 @@ class ProjectNoteEntity < NoteEntity
|
|||
project_note_path(note.project, note)
|
||||
end
|
||||
end
|
||||
|
||||
ProjectNoteEntity.prepend_mod_with('ProjectNoteEntity')
|
||||
|
|
|
|||
|
|
@ -788,6 +788,19 @@ class NotificationService
|
|||
mailer.new_achievement_email(user, achievement)
|
||||
end
|
||||
|
||||
def project_scheduled_for_deletion(project)
|
||||
return if project.emails_disabled?
|
||||
|
||||
recipients = owners_without_invites(project)
|
||||
|
||||
recipients.each do |recipient|
|
||||
mailer.project_scheduled_for_deletion(
|
||||
recipient.id,
|
||||
project.id
|
||||
).deliver_later
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def new_resource_email(target, current_user, method)
|
||||
|
|
@ -869,6 +882,16 @@ class NotificationService
|
|||
|
||||
private
|
||||
|
||||
def owners_without_invites(project)
|
||||
recipients = project.members.active_without_invites_and_requests.owners
|
||||
|
||||
if recipients.empty? && project.group
|
||||
recipients = project.group.members.active_without_invites_and_requests.owners
|
||||
end
|
||||
|
||||
recipients.map(&:user)
|
||||
end
|
||||
|
||||
def log_info(message_text, user)
|
||||
Gitlab::AppLogger.info(
|
||||
message: message_text,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ module Projects
|
|||
|
||||
if result[:status] == :success
|
||||
log_event
|
||||
send_project_deletion_notification
|
||||
|
||||
## Trigger root statistics refresh, to skip project_statistics of
|
||||
## projects marked for deletion
|
||||
|
|
@ -27,6 +28,14 @@ module Projects
|
|||
|
||||
private
|
||||
|
||||
def send_project_deletion_notification
|
||||
return unless ::Feature.enabled?(:project_deletion_notification_email, project) &&
|
||||
project.adjourned_deletion? &&
|
||||
project.marked_for_deletion?
|
||||
|
||||
::NotificationService.new.project_scheduled_for_deletion(project)
|
||||
end
|
||||
|
||||
def log_event
|
||||
log_info("User #{current_user.id} marked project #{project.full_path} for deletion")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
%p
|
||||
= _('Hi %{username}!') % { username: sanitize_name(@user.name) }
|
||||
%p
|
||||
= _('Your project %{project_name} has been marked for deletion and will be removed in %{days}.').html_safe % { project_name: link_to(@project.full_name, project_url(@project)), days: pluralize((@deletion_due_in_days / 1.day).to_i, _('day')) }
|
||||
%p
|
||||
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: inactive_dashboard_projects_url }
|
||||
= _('If this was a mistake, you can %{link_start}retain the project%{link_end} before %{deletion_date}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe, deletion_date: @deletion_date }
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<%= _('Hi %{username}!') % { username: sanitize_name(@user.name) } %>
|
||||
|
||||
<%= _('Your project %{project_name} has been marked for deletion and will be removed in %{days}.') % { project_name: @project.full_name, days: pluralize((@deletion_due_in_days / 1.day).to_i, _('day')) } %>
|
||||
|
||||
<%= _('View your project: %{project_url}') % { project_url: project_url(@project) } %>
|
||||
|
||||
<%= _('If this was a mistake, you can retain the project before %{deletion_date}: %{retention_url}') % { retention_url: inactive_dashboard_projects_url, deletion_date: @deletion_date } %>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: project_deletion_notification_email
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/522883
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/184026
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/525979
|
||||
milestone: '17.11'
|
||||
group: group::authorization
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
||||
|
|
@ -181,7 +181,6 @@ InitializerConnections.raise_if_new_database_connection do
|
|||
draw :gitlab_subscriptions
|
||||
draw :phone_verification
|
||||
draw :arkose
|
||||
draw :amazon_q
|
||||
|
||||
scope '/from_secondary/:geo_node_id' do
|
||||
draw :git_http
|
||||
|
|
|
|||
|
|
@ -182,6 +182,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
|
|||
resources :work_items, only: [:index, :show], param: :iid
|
||||
|
||||
post :preview_markdown
|
||||
|
||||
post '/restore' => '/groups#restore', as: :restore
|
||||
end
|
||||
|
||||
scope(
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ The following Rake tasks are available for use with GitLab:
|
|||
| [Incoming email](incoming_email.md) | Incoming email-related tasks. |
|
||||
| [Integrity checks](check.md) | Check the integrity of repositories, files, LDAP, and more. |
|
||||
| [LDAP maintenance](ldap.md) | [LDAP](../../administration/auth/ldap/_index.md)-related tasks. |
|
||||
| [Password](password.md) | Password management tasks. |
|
||||
| [Praefect Rake tasks](praefect.md) | [Praefect](../../administration/gitaly/praefect.md)-related tasks. |
|
||||
| [Project import/export](project_import_export.md) | Prepare for [project exports and imports](../../user/project/settings/import_export.md). |
|
||||
| [Sidekiq job migration](../sidekiq/sidekiq_job_migration.md) | Migrate Sidekiq jobs scheduled for future dates to a new queue. |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
stage: Systems
|
||||
group: Distribution
|
||||
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
|
||||
title: Maintenance Rake tasks
|
||||
---
|
||||
|
||||
{{< details >}}
|
||||
|
||||
- Tier: Free, Premium, Ultimate
|
||||
- Offering: GitLab Self-Managed
|
||||
|
||||
{{< /details >}}
|
||||
|
||||
GitLab provides Rake tasks for managing passwords.
|
||||
|
||||
## Reset passwords
|
||||
|
||||
To reset a password using a Rake task, see [reset user passwords](../../security/reset_user_password.md#use-a-rake-task).
|
||||
|
||||
## Check password salt length
|
||||
|
||||
Starting with GitLab 17.11, the salts of password hashes on FIPS instances
|
||||
are increased when a user signs in.
|
||||
|
||||
You can check how many users need this migration:
|
||||
|
||||
```shell
|
||||
# omnibus-gitlab
|
||||
sudo gitlab-rake gitlab:password:fips_check_salts:[true]
|
||||
|
||||
# installation from source
|
||||
bundle exec rake gitlab:password:fips_check_salts:[true] RAILS_ENV=production
|
||||
```
|
||||
|
|
@ -6,7 +6,7 @@ title: Foreign keys and associations
|
|||
---
|
||||
|
||||
When adding an association to a model you must also add a foreign key. When
|
||||
adding a foreign key you must always add an [index](#indexes).
|
||||
adding a foreign key you must always add an [index](#indexes) first.
|
||||
|
||||
If the [index must be created async](adding_database_indexes.md#create-indexes-asynchronously)
|
||||
due to duration reasons, you must avoid adding the foreign key until the index
|
||||
|
|
@ -158,10 +158,10 @@ this should be set to `CASCADE`.
|
|||
|
||||
When adding a foreign key in PostgreSQL the column is not indexed automatically,
|
||||
thus you must also add a concurrent index. Indexes are required for all foreign
|
||||
keys and they must be added in the same or earlier migration than the migration
|
||||
adding the foreign key. Conversely, foreign keys must be removed in
|
||||
the same or earlier migration than the migration
|
||||
removing indexes supporting these foreign keys.
|
||||
keys and they must be added before the foreign key. This can mean that they are
|
||||
an earlier step in the same migration or they are added in an earlier migration
|
||||
than the migration adding the foreign key. For the same reasons, foreign keys
|
||||
must be removed before removing indexes supporting these foreign keys.
|
||||
|
||||
Without an index on the foreign key it forces Postgres to do a full table scan
|
||||
every time a record is deleted from the referenced table. In the past this has
|
||||
|
|
|
|||
|
|
@ -413,10 +413,13 @@ For this tool to automatically remove the usages of the feature flag in your cod
|
|||
|
||||
For example you can create a patch file for `config/feature_flags/beta/my_feature_flag.yml` using the following steps:
|
||||
|
||||
1. Edit the code locally to remove the feature flag `my_feature_flag` usage assuming that the feature flag is already enabled and we are rolling forward
|
||||
1. Run `git diff > config/feature_flags/beta/my_feature_flag.patch`
|
||||
1. Undo the changes to the files where you removed the feature flag usage
|
||||
1. Commit this file `config/feature_flags/beta/my_feature_flag.patch` file to the branch where you are adding the feature flag
|
||||
1. Ensure you have a clean Git working directory.
|
||||
1. Delete `config/feature_flags/beta/my_feature_flag.yml`.
|
||||
1. Edit the code locally to remove any usage of `my_feature_flag` as though that the feature flag is already enabled and the feature is moving forward.
|
||||
1. Run `git diff > config/feature_flags/beta/my_feature_flag.patch`. If your feature flag is not a `beta` flag, ensure your patch file in the same directory as the YAML file that defines your feature flag.
|
||||
1. Undo the deletion of `config/feature_flags/beta/my_feature_flag.yml`
|
||||
1. Undo the changes to the files you ended to remove the feature flag usage
|
||||
1. Commit the patch file to the branch where you are adding the feature flag
|
||||
|
||||
Then in future the `gitlab-housekeeper` will automatically clean up your
|
||||
feature flag for you by applying this patch.
|
||||
|
|
|
|||
|
|
@ -269,6 +269,8 @@ Learn how to [opt out of all emails from GitLab](#opt-out-of-all-gitlab-emails).
|
|||
The following table presents the events that generate notifications for issues, merge requests, and
|
||||
epics:
|
||||
|
||||
<!-- For issue due timing source, see 'issue_due_scheduler_worker' in https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/initializers/1_settings.rb -->
|
||||
|
||||
| Type | Event | Sent to |
|
||||
|------|-------|---------|
|
||||
| Epic | Closed | Subscribers and participants. |
|
||||
|
|
@ -276,7 +278,6 @@ epics:
|
|||
| Epic | New note | Participants, Watchers, Subscribers, and Custom notification level with this event selected. Also anyone mentioned by username in the comment, with notification level "Mention" or higher. |
|
||||
| Epic | Reopened | Subscribers and participants. |
|
||||
| Issue | Closed | Subscribers and participants. |
|
||||
<!-- For issue due timing source, see 'issue_due_scheduler_worker' in https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/initializers/1_settings.rb -->
|
||||
| Issue | Due tomorrow. The notification is sent at 00:50 in the server's time zone (for GitLab.com this is UTC) for open issues with a due date of the next calendar day. | Participants and Custom notification level with this event selected. |
|
||||
| Issue | Milestone changed | Subscribers and participants. |
|
||||
| Issue | Milestone removed | Subscribers and participants. |
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ To create a custom field:
|
|||
- In **Use on**, select the work item types where you want this field to be available.
|
||||
- In **Options** (on single-select and multi-select fields), enter the possible select options.
|
||||
A single-select or multi-select field can have at most 50 select options.
|
||||
- Reorder options by dragging the grip icon ({{< icon name="grip" >}}) to the left of each option.
|
||||
1. Select **Save**.
|
||||
|
||||
### Edit a custom field
|
||||
|
|
|
|||
|
|
@ -489,7 +489,8 @@ module Gitlab
|
|||
|
||||
def unavailable_scopes_for_resource(resource)
|
||||
unavailable_ai_features_scopes +
|
||||
unavailable_observability_scopes_for_resource(resource)
|
||||
unavailable_observability_scopes_for_resource(resource) +
|
||||
unavailable_virtual_registry_scopes_for_resource(resource)
|
||||
end
|
||||
|
||||
def unavailable_ai_features_scopes
|
||||
|
|
@ -503,6 +504,12 @@ module Gitlab
|
|||
OBSERVABILITY_SCOPES
|
||||
end
|
||||
|
||||
def unavailable_virtual_registry_scopes_for_resource(resource)
|
||||
return VIRTUAL_REGISTRY_SCOPES if resource.is_a?(Project)
|
||||
|
||||
[]
|
||||
end
|
||||
|
||||
def non_admin_available_scopes
|
||||
API_SCOPES + REPOSITORY_SCOPES + registry_scopes + virtual_registry_scopes + OBSERVABILITY_SCOPES + AI_FEATURES_SCOPES
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28,5 +28,40 @@ namespace :gitlab do
|
|||
|
||||
puts "Password successfully updated for user with username #{username}."
|
||||
end
|
||||
|
||||
desc "GitLab | Password | Check status of password salts on FIPS systems"
|
||||
task :fips_check_salts, [:print_usernames] => :environment do |_, args|
|
||||
abort Rainbow('This command is only available on FIPS instances').red unless Gitlab::FIPS.enabled?
|
||||
|
||||
message = "Active users with unmigrated salts:"
|
||||
batch_size = 50
|
||||
min_salt_len = 64
|
||||
count_total = 0
|
||||
count_unmigrated = 0
|
||||
|
||||
puts Rainbow(message) if args.print_usernames
|
||||
|
||||
User.active.each_batch(of: batch_size) do |user_batch|
|
||||
user_batch.each do |user|
|
||||
count_total += 1
|
||||
|
||||
begin
|
||||
hash = user.encrypted_password
|
||||
salt_len = Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512
|
||||
.split_digest(hash)[:salt].length
|
||||
rescue StandardError => e
|
||||
puts("Error getting salt for user #{user.username}: #{e.message}")
|
||||
next
|
||||
end
|
||||
|
||||
if salt_len < min_salt_len
|
||||
puts user.username if args.print_usernames
|
||||
count_unmigrated += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
puts Rainbow("#{message} #{count_unmigrated} out of #{count_total} total users")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6418,9 +6418,6 @@ msgstr ""
|
|||
msgid "AmazonQ|Amazon Q will be turned off for all groups, subgroups, and projects, even if they have previously enabled it."
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|An error occurred. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|An unexpected error occurred while disconnecting Amazon Q. Please see the browser console log for more details."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -6433,9 +6430,6 @@ msgstr ""
|
|||
msgid "AmazonQ|Are you sure? Removing the ARN will disconnect Amazon Q from GitLab and all related features will stop working."
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|Ask GitLab Duo with Amazon Q to suggest a solution for this issue"
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|Audience"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -6463,9 +6457,6 @@ msgstr ""
|
|||
msgid "AmazonQ|Create an identity provider for this GitLab instance within AWS using the following values. %{helpStart}Learn more%{helpEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|Create fixes for review findings"
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|Create unit tests for selected lines of code in Java or Python files"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -6514,9 +6505,6 @@ msgstr ""
|
|||
msgid "AmazonQ|I'm creating unit tests for this merge request. I'll update this comment when I'm done."
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|I'm generating a fix for this review finding. I'll update this comment when I'm done."
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|I'm generating code for this issue. I'll update this comment and open a merge request when I'm done."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -6586,9 +6574,6 @@ msgstr ""
|
|||
msgid "AmazonQ|Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|Suggest a fix"
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|This field is required"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -6628,9 +6613,6 @@ msgstr ""
|
|||
msgid "AmazonQ|dev"
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|fix"
|
||||
msgstr ""
|
||||
|
||||
msgid "AmazonQ|review"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -31526,6 +31508,9 @@ msgstr ""
|
|||
msgid "ImportProjects|All organizations"
|
||||
msgstr ""
|
||||
|
||||
msgid "ImportProjects|Are you sure you want to import the project to a personal namespace?"
|
||||
msgstr ""
|
||||
|
||||
msgid "ImportProjects|Blocked import URL: %{message}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -31541,6 +31526,9 @@ msgstr ""
|
|||
msgid "ImportProjects|Collaborated"
|
||||
msgstr ""
|
||||
|
||||
msgid "ImportProjects|Continue import"
|
||||
msgstr ""
|
||||
|
||||
msgid "ImportProjects|Error importing repository %{project_safe_import_url} into %{project_full_path} - %{message}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -31550,7 +31538,7 @@ msgstr ""
|
|||
msgid "ImportProjects|Imported files will be kept. You can import this repository again later."
|
||||
msgstr ""
|
||||
|
||||
msgid "ImportProjects|Importing a project into a personal namespace results in all contributions being mapped to the same bot user. To map contributions to real users, import projects into a group instead."
|
||||
msgid "ImportProjects|Importing a project into a personal namespace results in all contributions being mapped to the same bot user and they cannot be reassigned. To map contributions to actual users, import the project to a group instead."
|
||||
msgstr ""
|
||||
|
||||
msgid "ImportProjects|Importing the project failed"
|
||||
|
|
@ -64474,7 +64462,7 @@ msgstr ""
|
|||
msgid "UsageQuota|Code packages and container images."
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuota|Compute units usage is calculated based on instance runners duration with cost factors applied."
|
||||
msgid "UsageQuota|Compute minutes usage displays the hosted runner usage against the total available compute minutes."
|
||||
msgstr ""
|
||||
|
||||
msgid "UsageQuota|Compute usage"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe GroupsController, :with_current_organization, factory_default: :keep, feature_category: :code_review_workflow do
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
include AdminModeHelper
|
||||
include NamespacesHelper
|
||||
|
||||
let_it_be(:group_organization) { current_organization }
|
||||
let_it_be_with_refind(:group) { create_default(:group, :public, organization: group_organization) }
|
||||
|
|
@ -24,7 +25,6 @@ RSpec.describe GroupsController, :with_current_organization, factory_default: :k
|
|||
|
||||
before do
|
||||
enable_admin_mode!(admin_with_admin_mode)
|
||||
stub_feature_flags(downtier_delayed_deletion: false)
|
||||
end
|
||||
|
||||
shared_examples 'member with ability to create subgroups' do
|
||||
|
|
@ -527,42 +527,249 @@ RSpec.describe GroupsController, :with_current_organization, factory_default: :k
|
|||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
context 'as another user' do
|
||||
it 'returns 404' do
|
||||
sign_in(create(:user))
|
||||
let(:format) { :html }
|
||||
let(:params) { {} }
|
||||
|
||||
delete :destroy, params: { id: group.to_param }
|
||||
subject { delete :destroy, format: format, params: { id: group.to_param, **params } }
|
||||
|
||||
context 'when authenticated user can admin the group' do
|
||||
let_it_be(:user) { owner }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'delayed deletion feature is available' do
|
||||
context 'success' do
|
||||
it 'marks the group for delayed deletion' do
|
||||
expect { subject }.to change { group.reload.marked_for_deletion? }.from(false).to(true)
|
||||
end
|
||||
|
||||
it 'does not immediately delete the group' do
|
||||
Sidekiq::Testing.fake! do
|
||||
expect { subject }.not_to change { GroupDestroyWorker.jobs.size }
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a html request' do
|
||||
it 'redirects to group path' do
|
||||
subject
|
||||
|
||||
expect(response).to redirect_to(group_path(group))
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a json request', :freeze_time do
|
||||
let(:format) { :json }
|
||||
|
||||
it 'returns json with message' do
|
||||
subject
|
||||
|
||||
# FIXME: Replace `group.marked_for_deletion_on` with `group` after https://gitlab.com/gitlab-org/gitlab/-/work_items/527085
|
||||
expect(json_response['message'])
|
||||
.to eq(
|
||||
"'#{group.name}' has been scheduled for deletion and will be deleted on " \
|
||||
"#{permanent_deletion_date_formatted(group.marked_for_deletion_on)}.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'failure' do
|
||||
before do
|
||||
allow(::Groups::MarkForDeletionService).to receive_message_chain(:new, :execute).and_return({ status: :error, message: 'error' })
|
||||
end
|
||||
|
||||
it 'does not mark the group for deletion' do
|
||||
expect { subject }.not_to change { group.reload.marked_for_deletion? }.from(false)
|
||||
end
|
||||
|
||||
context 'for a html request' do
|
||||
it 'redirects to group edit page' do
|
||||
subject
|
||||
|
||||
expect(response).to redirect_to(edit_group_path(group))
|
||||
expect(flash[:alert]).to include 'error'
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a json request' do
|
||||
let(:format) { :json }
|
||||
|
||||
it 'returns json with message' do
|
||||
subject
|
||||
|
||||
expect(json_response['message']).to eq("error")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when group is already marked for deletion' do
|
||||
before do
|
||||
create(:group_deletion_schedule, group: group, marked_for_deletion_on: Date.current)
|
||||
end
|
||||
|
||||
context 'when permanently_remove param is set' do
|
||||
let(:params) { { permanently_remove: true } }
|
||||
|
||||
context 'for a html request' do
|
||||
it 'deletes the group immediately and redirects to root path' do
|
||||
expect(GroupDestroyWorker).to receive(:perform_async)
|
||||
|
||||
subject
|
||||
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:toast]).to include "Group '#{group.name}' is being deleted."
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a json request' do
|
||||
let(:format) { :json }
|
||||
|
||||
it 'deletes the group immediately and returns json with message' do
|
||||
expect(GroupDestroyWorker).to receive(:perform_async)
|
||||
|
||||
subject
|
||||
|
||||
expect(json_response['message']).to eq("Group '#{group.name}' is being deleted.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when permanently_remove param is not set' do
|
||||
context 'for a html request' do
|
||||
it 'redirects to edit path with error' do
|
||||
subject
|
||||
|
||||
expect(response).to redirect_to(edit_group_path(group))
|
||||
expect(flash[:alert]).to include "Group has been already marked for deletion"
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a json request' do
|
||||
let(:format) { :json }
|
||||
|
||||
it 'returns json with message' do
|
||||
subject
|
||||
|
||||
expect(json_response['message']).to eq("Group has been already marked for deletion")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'delayed deletion feature is not available', :sidekiq_inline do
|
||||
before do
|
||||
stub_feature_flags(downtier_delayed_deletion: false)
|
||||
end
|
||||
|
||||
context 'for a html request' do
|
||||
it 'immediately schedules a group destroy and redirects to root page with alert about immediate deletion' do
|
||||
Sidekiq::Testing.fake! do
|
||||
expect { subject }.to change { GroupDestroyWorker.jobs.size }.by(1)
|
||||
end
|
||||
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:toast]).to include "Group '#{group.name}' is being deleted."
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a json request' do
|
||||
let(:format) { :json }
|
||||
|
||||
it 'immediately schedules a group destroy and returns json with message' do
|
||||
Sidekiq::Testing.fake! do
|
||||
expect { subject }.to change { GroupDestroyWorker.jobs.size }.by(1)
|
||||
end
|
||||
|
||||
expect(json_response['message']).to eq("Group '#{group.name}' is being deleted.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated user cannot admin the group' do
|
||||
before do
|
||||
sign_in(create(:user))
|
||||
end
|
||||
|
||||
it 'returns 404' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'as the group owner' do
|
||||
let(:user) { create(:user) }
|
||||
let(:group) { create(:group) }
|
||||
describe 'POST #restore' do
|
||||
let_it_be(:group) do
|
||||
create(:group_with_deletion_schedule,
|
||||
marked_for_deletion_on: 1.day.ago,
|
||||
deleting_user: user)
|
||||
end
|
||||
|
||||
subject { post :restore, params: { group_id: group.to_param } }
|
||||
|
||||
context 'when authenticated user can admin the group' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'for a html request' do
|
||||
it 'schedules a group destroy and redirects to the root path' do
|
||||
Sidekiq::Testing.fake! do
|
||||
expect { delete :destroy, params: { id: group.to_param } }.to change(GroupDestroyWorker.jobs, :size).by(1)
|
||||
context 'when the delayed deletion feature is available' do
|
||||
context 'when the restore succeeds' do
|
||||
it 'restores the group' do
|
||||
expect { subject }.to change { group.reload.marked_for_deletion? }.from(true).to(false)
|
||||
end
|
||||
|
||||
it 'renders success notice upon restoring' do
|
||||
subject
|
||||
|
||||
expect(response).to redirect_to(edit_group_path(group))
|
||||
expect(flash[:notice]).to include "Group '#{group.name}' has been successfully restored."
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the restore fails' do
|
||||
before do
|
||||
allow(::Groups::RestoreService).to receive_message_chain(:new, :execute).and_return({ status: :error, message: 'error' })
|
||||
end
|
||||
|
||||
it 'does not restore the group' do
|
||||
expect { subject }.not_to change { group.reload.marked_for_deletion? }.from(true)
|
||||
end
|
||||
|
||||
it 'redirects to group edit page' do
|
||||
subject
|
||||
|
||||
expect(response).to redirect_to(edit_group_path(group))
|
||||
expect(flash[:alert]).to include 'error'
|
||||
end
|
||||
expect(flash[:toast]).to eq(format(_("Group '%{group_name}' is being deleted."), group_name: group.full_name))
|
||||
expect(response).to redirect_to(root_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a json request' do
|
||||
it 'schedules a group destroy and returns message' do
|
||||
Sidekiq::Testing.fake! do
|
||||
expect { delete :destroy, format: :json, params: { id: group.to_param } }.to change(GroupDestroyWorker.jobs, :size).by(1)
|
||||
end
|
||||
expect(Gitlab::Json.parse(response.body)).to eq({ 'message' => "Group '#{group.full_name}' is being deleted." })
|
||||
context 'when delayed deletion feature is not available' do
|
||||
before do
|
||||
stub_feature_flags(downtier_delayed_deletion: false)
|
||||
end
|
||||
|
||||
it 'returns 404' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated user cannot admin the group' do
|
||||
before do
|
||||
sign_in(create(:user))
|
||||
end
|
||||
|
||||
it 'returns 404' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -242,11 +242,10 @@ RSpec.describe 'Project issue boards sidebar labels', :js, feature_category: :po
|
|||
page.within(labels_widget) do
|
||||
click_button 'Edit'
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('.gl-new-dropdown-item-check-icon', count: 2)
|
||||
expect(page).to have_content(development.title)
|
||||
expect(page).to have_content(stretch.title)
|
||||
# Selected labels are shown twice - once in a "Selected" section and once in the "All" section below
|
||||
expect(page).to have_selector('.gl-new-dropdown-item-check-icon', count: 4)
|
||||
expect(page).to have_content(development.title, count: 2)
|
||||
expect(page).to have_content(stretch.title, count: 2)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ RSpec.describe 'Import multiple repositories by uploading a manifest file', :js,
|
|||
page.within(second_row) do
|
||||
click_on 'Import'
|
||||
end
|
||||
click_on 'Continue import'
|
||||
|
||||
wait_for_requests
|
||||
|
||||
|
|
|
|||
|
|
@ -176,12 +176,6 @@
|
|||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"amazon_q_quick_actions_path": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"commands_changes": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import createMarkdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer';
|
||||
import createMarkdownDeserializer, {
|
||||
transformQuickActions,
|
||||
} from '~/content_editor/services/gl_api_markdown_deserializer';
|
||||
import MarkdownSerializer from '~/content_editor/services/markdown_serializer';
|
||||
import { builders, tiptapEditor, doc, text } from '../serialization_utils';
|
||||
|
||||
|
|
@ -22,6 +24,14 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
|
|||
renderMarkdown = jest.fn();
|
||||
});
|
||||
|
||||
describe('transformQuickActions', () => {
|
||||
it('ensures at least 3 newlines after quick actions so that reference style links after the quick action are correctly parsed', () => {
|
||||
expect(
|
||||
transformQuickActions('Link to [GitLab][link]\n/confidential\n[link]: https://gitlab.com'),
|
||||
).toBe('Link to [GitLab][link]\n/confidential\n\n\n[link]: https://gitlab.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when deserializing', () => {
|
||||
let deserializer;
|
||||
let result;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GlBadge, GlButton } from '@gitlab/ui';
|
||||
import { GlBadge, GlButton, GlModal } from '@gitlab/ui';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import Vuex from 'vuex';
|
||||
|
|
@ -45,6 +45,7 @@ describe('ProviderRepoTableRow', () => {
|
|||
const findImportStatus = () => wrapper.findComponent(ImportStatus);
|
||||
const findProviderLink = () => wrapper.findByTestId('provider-link');
|
||||
const findMembershipsWarning = () => wrapper.findByTestId('memberships-warning');
|
||||
const findGlModal = () => wrapper.findComponent(GlModal);
|
||||
|
||||
const findCancelButton = () => {
|
||||
const buttons = wrapper
|
||||
|
|
@ -107,12 +108,44 @@ describe('ProviderRepoTableRow', () => {
|
|||
it('shows memberships warning', () => {
|
||||
expect(findMembershipsWarning().isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows modal with warning message when import button is clicked', async () => {
|
||||
findImportButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
|
||||
const modal = findGlModal();
|
||||
expect(modal.props('title')).toBe(
|
||||
'Are you sure you want to import the project to a personal namespace?',
|
||||
);
|
||||
expect(modal.text()).toContain(
|
||||
'Importing a project into a personal namespace results in all contributions being mapped to the same bot user and they cannot be reassigned. To map contributions to actual users, import the project to a group instead.',
|
||||
);
|
||||
});
|
||||
|
||||
it('triggers import when clicking modal primary button', async () => {
|
||||
findImportButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
|
||||
findGlModal().vm.$emit('primary');
|
||||
|
||||
expect(fetchImport).toHaveBeenCalledWith(expect.anything(), {
|
||||
repoId: repo.importSource.id,
|
||||
optionalStages: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when group namespace is selected as import target', () => {
|
||||
it('does not show memberships warning', () => {
|
||||
expect(findMembershipsWarning().isVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not show modal when import button is clicked', async () => {
|
||||
findImportButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
|
||||
expect(findGlModal().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders import button', () => {
|
||||
|
|
|
|||
|
|
@ -214,22 +214,6 @@ describe('WorkItemLabels component', () => {
|
|||
expect(findWorkItemSidebarDropdownWidget().props('loading')).toBe(false);
|
||||
});
|
||||
|
||||
it('filters search results by title in frontend', async () => {
|
||||
createComponent({
|
||||
searchQueryHandler: jest.fn().mockResolvedValue(getProjectLabelsResponse(mockLabels)),
|
||||
});
|
||||
|
||||
showDropdown();
|
||||
await findWorkItemSidebarDropdownWidget().vm.$emit('searchStarted', mockLabels[0].title);
|
||||
|
||||
expect(findWorkItemSidebarDropdownWidget().props('loading')).toBe(true);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findWorkItemSidebarDropdownWidget().props('listItems')).toHaveLength(1);
|
||||
expect(findWorkItemSidebarDropdownWidget().props('loading')).toBe(false);
|
||||
});
|
||||
|
||||
it('emits error event if search query fails', async () => {
|
||||
createComponent({ searchQueryHandler: errorHandler });
|
||||
showDropdown();
|
||||
|
|
@ -336,25 +320,17 @@ describe('WorkItemLabels component', () => {
|
|||
|
||||
it('shows selected labels at top of list', async () => {
|
||||
const [label1, label2, label3] = mockLabels;
|
||||
const label999 = {
|
||||
__typename: 'Label',
|
||||
id: 'gid://gitlab/Label/999',
|
||||
title: 'Label 999',
|
||||
description: 'Label not in the label query result',
|
||||
color: '#fff',
|
||||
textColor: '#000',
|
||||
};
|
||||
|
||||
createComponent({
|
||||
workItemQueryHandler: workItemQuerySuccess,
|
||||
updateWorkItemMutationHandler: jest.fn().mockResolvedValue(
|
||||
updateWorkItemMutationResponseFactory({
|
||||
labels: [label1, label999],
|
||||
labels: [label1, label3],
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
updateLabels([label1Id, label999.id]);
|
||||
updateLabels([label1Id, label3Id]);
|
||||
|
||||
showDropdown();
|
||||
|
||||
|
|
@ -362,10 +338,11 @@ describe('WorkItemLabels component', () => {
|
|||
|
||||
const selected = [
|
||||
{ color: label1.color, text: label1.title, value: label1.id },
|
||||
{ color: label999.color, text: label999.title, value: label999.id },
|
||||
{ color: label3.color, text: label3.title, value: label3.id },
|
||||
];
|
||||
|
||||
const unselected = [
|
||||
{ color: label1.color, text: label1.title, value: label1.id },
|
||||
{ color: label2.color, text: label2.title, value: label2.id },
|
||||
{ color: label3.color, text: label3.title, value: label3.id },
|
||||
];
|
||||
|
|
@ -376,6 +353,21 @@ describe('WorkItemLabels component', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('does not update labels when no labels were added or removed', async () => {
|
||||
createComponent({
|
||||
workItemQueryHandler: workItemQueryWithLabelsHandler,
|
||||
updateWorkItemMutationHandler: successRemoveAllLabelWorkItemMutationHandler,
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
showDropdown();
|
||||
findWorkItemSidebarDropdownWidget().vm.$emit('updateSelected', [label2Id, label3Id]);
|
||||
findWorkItemSidebarDropdownWidget().vm.$emit('updateSelected', [label1Id, label2Id, label3Id]);
|
||||
findWorkItemSidebarDropdownWidget().vm.$emit('updateValue', [label1Id, label2Id, label3Id]);
|
||||
|
||||
expect(successRemoveAllLabelWorkItemMutationHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('tracking', () => {
|
||||
let trackingSpy;
|
||||
|
||||
|
|
|
|||
|
|
@ -251,12 +251,28 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
|
|||
end
|
||||
|
||||
context 'when dependency proxy is enabled' do
|
||||
let(:virtual_registry_scopes) { %i[read_virtual_registry write_virtual_registry] }
|
||||
|
||||
before do
|
||||
stub_config(dependency_proxy: { enabled: true })
|
||||
end
|
||||
|
||||
it 'contains all virtual registry related scopes' do
|
||||
expect(subject.virtual_registry_scopes).to eq %i[read_virtual_registry write_virtual_registry]
|
||||
expect(subject.virtual_registry_scopes).to eq virtual_registry_scopes
|
||||
end
|
||||
|
||||
context 'for a Project' do
|
||||
it 'does not include virtual registry scopes' do
|
||||
expect(subject.available_scopes_for(build_stubbed(:project))).to not_include(*virtual_registry_scopes)
|
||||
end
|
||||
end
|
||||
|
||||
%i[user group].each do |resource_type|
|
||||
context "for a #{resource_type}" do
|
||||
it 'includes the virtual registry scopes' do
|
||||
expect(subject.available_scopes_for(build_stubbed(resource_type))).to include(*virtual_registry_scopes)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -283,4 +283,29 @@ RSpec.describe Emails::Projects do
|
|||
is_expected.to have_body_text("#{project.name} | Project export error")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#project_scheduled_for_deletion' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:frozen_time) { Time.new(2023, 10, 15, 12, 0, 0) }
|
||||
let_it_be(:project) { create(:project, marked_for_deletion_on: frozen_time) }
|
||||
|
||||
let(:deletion_adjourned_period) { 7 }
|
||||
let(:deletion_date) { (frozen_time.to_date + deletion_adjourned_period.days).strftime('%B %-d, %Y') }
|
||||
|
||||
before do
|
||||
stub_application_setting(deletion_adjourned_period: deletion_adjourned_period)
|
||||
allow_next_instance_of(Project) do |instance|
|
||||
allow(instance).to receive(:marked_for_deletion_on).and_return(frozen_time)
|
||||
end
|
||||
end
|
||||
|
||||
subject { Notify.project_scheduled_for_deletion(user.id, project.id) }
|
||||
|
||||
it 'has expected content', :aggregate_failures do
|
||||
is_expected.to have_subject("#{project.name} | Project scheduled for deletion")
|
||||
is_expected.to have_body_text(project.full_name)
|
||||
is_expected.to have_body_text(deletion_adjourned_period.to_s)
|
||||
is_expected.to have_body_text(deletion_date)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4551,6 +4551,76 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'project scheduled for deletion' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
context 'when project emails are disabled' do
|
||||
before do
|
||||
allow(project).to receive(:emails_disabled?).and_return(true)
|
||||
end
|
||||
|
||||
it 'does not send any emails' do
|
||||
expect(Notify).not_to receive(:project_scheduled_for_deletion)
|
||||
|
||||
subject.project_scheduled_for_deletion(project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project emails are enabled' do
|
||||
before do
|
||||
allow(project).to receive(:emails_disabled?).and_return(false)
|
||||
end
|
||||
|
||||
context 'when user is owner' do
|
||||
it 'sends email' do
|
||||
expect(Notify).to receive(:project_scheduled_for_deletion).with(project.first_owner.id, project.id).and_call_original
|
||||
|
||||
subject.project_scheduled_for_deletion(project)
|
||||
end
|
||||
|
||||
context 'when owner is blocked' do
|
||||
it 'does not send email' do
|
||||
project.owner.block!
|
||||
|
||||
expect(Notify).not_to receive(:project_scheduled_for_deletion)
|
||||
|
||||
subject.project_scheduled_for_deletion(project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project has multiple owners' do
|
||||
it 'sends email to all owners' do
|
||||
project.add_owner(user)
|
||||
|
||||
expect(Notify).to receive(:project_scheduled_for_deletion).with(project.first_owner.id, project.id).and_call_original
|
||||
expect(Notify).to receive(:project_scheduled_for_deletion).with(user.id, project.id).and_call_original
|
||||
|
||||
subject.project_scheduled_for_deletion(project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project has no direct owners but belongs to a group with owners' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:group_owner) { create(:user) }
|
||||
|
||||
before do
|
||||
group.add_owner(group_owner)
|
||||
# Ensure project has no direct owners
|
||||
project.members.owners.delete_all if project.members.owners.any?
|
||||
end
|
||||
|
||||
it 'sends email to group owners' do
|
||||
expect(Notify).to receive(:project_scheduled_for_deletion).with(group_owner.id, project.id).and_call_original
|
||||
|
||||
subject.project_scheduled_for_deletion(project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_team(project)
|
||||
@u_watcher = create_global_setting_for(create(:user), :watch)
|
||||
@u_participating = create_global_setting_for(create(:user), :participating)
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ RSpec.describe Projects::MarkForDeletionService, feature_category: :groups_and_p
|
|||
let(:original_project_path) { project.path }
|
||||
let(:original_project_name) { project.name }
|
||||
let(:licensed) { false }
|
||||
let(:service) { described_class.new(project, user) }
|
||||
|
||||
subject(:result) { described_class.new(project, user).execute(licensed: licensed) }
|
||||
subject(:result) { service.execute(licensed: licensed) }
|
||||
|
||||
context 'with downtier_delayed_deletion feature flag enabled' do
|
||||
context 'when marking project for deletion' do
|
||||
|
|
@ -47,6 +48,12 @@ RSpec.describe Projects::MarkForDeletionService, feature_category: :groups_and_p
|
|||
|
||||
result
|
||||
end
|
||||
|
||||
it 'sends project deletion notification' do
|
||||
expect(service).to receive(:send_project_deletion_notification)
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
context 'when marking project for deletion once again' do
|
||||
|
|
@ -58,6 +65,22 @@ RSpec.describe Projects::MarkForDeletionService, feature_category: :groups_and_p
|
|||
expect(result[:status]).to eq(:success)
|
||||
expect(project.marked_for_deletion_at).to eq(marked_for_deletion_at.to_date)
|
||||
end
|
||||
|
||||
it 'does not send project deletion notification' do
|
||||
project.update!(marked_for_deletion_at: marked_for_deletion_at)
|
||||
|
||||
expect(service).not_to receive(:send_project_deletion_notification)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
it 'does not send notification email' do
|
||||
stub_feature_flags(project_deletion_notification_email: true)
|
||||
|
||||
expect(NotificationService).not_to receive(:new)
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -66,7 +89,8 @@ RSpec.describe Projects::MarkForDeletionService, feature_category: :groups_and_p
|
|||
stub_feature_flags(downtier_delayed_deletion: false)
|
||||
end
|
||||
|
||||
it 'returns an error response' do
|
||||
it 'returns an error response and does not send notification' do
|
||||
expect(service).not_to receive(:send_project_deletion_notification)
|
||||
expect(result).to eq(status: :error, message: 'Cannot mark project for deletion: feature not supported')
|
||||
end
|
||||
|
||||
|
|
@ -76,6 +100,88 @@ RSpec.describe Projects::MarkForDeletionService, feature_category: :groups_and_p
|
|||
it 'is successful' do
|
||||
expect(result[:status]).to eq(:success)
|
||||
end
|
||||
|
||||
it 'sends project deletion notification' do
|
||||
expect(service).to receive(:send_project_deletion_notification)
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#send_project_deletion_notification' do
|
||||
context 'when all conditions are met' do
|
||||
before do
|
||||
stub_feature_flags(project_deletion_notification_email: true)
|
||||
allow(project).to receive_messages(adjourned_deletion?: true, marked_for_deletion?: true)
|
||||
end
|
||||
|
||||
it 'sends a notification email' do
|
||||
expect_next_instance_of(NotificationService) do |service|
|
||||
expect(service).to receive(:project_scheduled_for_deletion).with(project)
|
||||
end
|
||||
|
||||
execute_send_project_deletion_notification
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(project_deletion_notification_email: false)
|
||||
allow(project).to receive_messages(adjourned_deletion?: true, marked_for_deletion?: true)
|
||||
end
|
||||
|
||||
it 'does not send a notification email' do
|
||||
expect(NotificationService).not_to receive(:new)
|
||||
|
||||
execute_send_project_deletion_notification
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature flag is enabled for specific project' do
|
||||
before do
|
||||
stub_feature_flags(project_deletion_notification_email: project)
|
||||
allow(project).to receive_messages(adjourned_deletion?: true, marked_for_deletion?: true)
|
||||
end
|
||||
|
||||
it 'sends a notification email' do
|
||||
expect_next_instance_of(NotificationService) do |service|
|
||||
expect(service).to receive(:project_scheduled_for_deletion).with(project)
|
||||
end
|
||||
|
||||
execute_send_project_deletion_notification
|
||||
end
|
||||
end
|
||||
|
||||
context 'when adjourned deletion is disabled' do
|
||||
before do
|
||||
stub_feature_flags(project_deletion_notification_email: true)
|
||||
allow(project).to receive_messages(adjourned_deletion?: false, marked_for_deletion?: true)
|
||||
end
|
||||
|
||||
it 'does not send a notification email' do
|
||||
expect(NotificationService).not_to receive(:new)
|
||||
|
||||
execute_send_project_deletion_notification
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project is not marked for deletion' do
|
||||
before do
|
||||
stub_feature_flags(project_deletion_notification_email: true)
|
||||
allow(project).to receive_messages(adjourned_deletion?: true, marked_for_deletion?: false)
|
||||
end
|
||||
|
||||
it 'does not send a notification email' do
|
||||
expect(NotificationService).not_to receive(:new)
|
||||
|
||||
execute_send_project_deletion_notification
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def execute_send_project_deletion_notification
|
||||
service = described_class.new(project, user)
|
||||
service.send(:send_project_deletion_notification)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,27 +3,29 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'gitlab:password rake tasks', :silence_stdout do
|
||||
let!(:user_1) { create(:user, username: 'foobar', password: User.random_password, password_automatically_set: true) }
|
||||
let(:password) { User.random_password }
|
||||
|
||||
def stub_username(username)
|
||||
allow(Gitlab::TaskHelpers).to receive(:prompt).with('Enter username: ').and_return(username)
|
||||
end
|
||||
|
||||
def stub_password(password, confirmation = nil)
|
||||
confirmation ||= password
|
||||
allow(Gitlab::TaskHelpers).to receive(:prompt_for_password).and_return(password)
|
||||
allow(Gitlab::TaskHelpers).to receive(:prompt_for_password).with('Confirm password: ').and_return(confirmation)
|
||||
end
|
||||
|
||||
before do
|
||||
before(:all) do
|
||||
Rake.application.rake_require 'tasks/gitlab/password'
|
||||
|
||||
stub_username('foobar')
|
||||
stub_password(password)
|
||||
end
|
||||
|
||||
describe ':reset' do
|
||||
let!(:user_1) { create(:user, username: 'foobar', password: User.random_password, password_automatically_set: true) }
|
||||
let(:password) { User.random_password }
|
||||
|
||||
def stub_username(username)
|
||||
allow(Gitlab::TaskHelpers).to receive(:prompt).with('Enter username: ').and_return(username)
|
||||
end
|
||||
|
||||
def stub_password(password, confirmation = nil)
|
||||
confirmation ||= password
|
||||
allow(Gitlab::TaskHelpers).to receive(:prompt_for_password).and_return(password)
|
||||
allow(Gitlab::TaskHelpers).to receive(:prompt_for_password).with('Confirm password: ').and_return(confirmation)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_username('foobar')
|
||||
stub_password(password)
|
||||
end
|
||||
|
||||
context 'when all inputs are correct' do
|
||||
it 'updates the password properly' do
|
||||
expect(user_1.password_automatically_set?).to eq(true)
|
||||
|
|
@ -80,4 +82,69 @@ RSpec.describe 'gitlab:password rake tasks', :silence_stdout do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe ":fips_check_salts" do
|
||||
subject(:run_rake) { run_rake_task('gitlab:password:fips_check_salts') }
|
||||
|
||||
context 'without fips mode' do
|
||||
it 'aborts' do
|
||||
expect { run_rake }.to abort_execution
|
||||
end
|
||||
end
|
||||
|
||||
context 'in fips mode', :fips_mode do
|
||||
def hash(salt_len)
|
||||
Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.digest(
|
||||
User.random_password, 20_000, Devise.friendly_token(salt_len))
|
||||
end
|
||||
|
||||
let!(:unmigrated_users) do
|
||||
create(:user, username: 'user1', encrypted_password: hash(16))
|
||||
create(:user, username: 'user2', encrypted_password: hash(16))
|
||||
end
|
||||
|
||||
let!(:migrated_users) do
|
||||
create(:user, username: 'user3', encrypted_password: hash(64))
|
||||
create(:user, username: 'user4', encrypted_password: hash(64))
|
||||
end
|
||||
|
||||
context 'with no extra argument' do
|
||||
it 'only prints the user count' do
|
||||
expect { run_rake }
|
||||
.to output("Active users with unmigrated salts: 2 out of 4 total users\n")
|
||||
.to_stdout
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an error while inspecting a salt' do
|
||||
before do
|
||||
allow(Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512)
|
||||
.to receive(:split_digest)
|
||||
.and_raise(StandardError.new('test error'))
|
||||
end
|
||||
|
||||
it 'prints an error message' do
|
||||
expect { run_rake }
|
||||
.to output(/Error getting salt for user user1: test error/)
|
||||
.to_stdout
|
||||
end
|
||||
end
|
||||
|
||||
context 'with user printing enabled' do
|
||||
subject(:run_rake) { run_rake_task('gitlab:password:fips_check_salts', "true") }
|
||||
|
||||
it 'prints the user names and user count' do
|
||||
expect { run_rake }
|
||||
.to output(
|
||||
<<~MSG
|
||||
Active users with unmigrated salts:
|
||||
user1
|
||||
user2
|
||||
Active users with unmigrated salts: 2 out of 4 total users
|
||||
MSG
|
||||
).to_stdout
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue