Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
dcaa8f80fb
commit
08c811d7ce
|
|
@ -330,6 +330,9 @@ That's all of the required database changes.
|
|||
```
|
||||
|
||||
- [ ] Create `ee/app/replicators/geo/cool_widget_replicator.rb`. Implement the `#repository` method which should return a `<Repository>` instance, and implement the class method `.model` to return the `CoolWidget` class:
|
||||
- Implement the `replicable_title` and `replicable_title_plural` methods to
|
||||
return the human-readable singular and pluralized title of the replicable,
|
||||
which will be displayed in the UI and Rails console
|
||||
|
||||
```ruby
|
||||
# frozen_string_literal: true
|
||||
|
|
@ -343,6 +346,16 @@ That's all of the required database changes.
|
|||
::CoolWidget
|
||||
end
|
||||
|
||||
# @return [String] human-readable title.
|
||||
def self.replicable_title
|
||||
s_('Geo|Cool Widget')
|
||||
end
|
||||
|
||||
# @return [String] pluralized human-readable title.
|
||||
def self.replicable_title_plural
|
||||
s_('Geo|Cool Widgets')
|
||||
end
|
||||
|
||||
override :verification_feature_flag_enabled?
|
||||
def self.verification_feature_flag_enabled?
|
||||
# We are adding verification at the same time as replication, so we
|
||||
|
|
|
|||
|
|
@ -327,6 +327,9 @@ That's all of the required database changes.
|
|||
```
|
||||
|
||||
- [ ] Create `ee/app/replicators/geo/cool_widget_replicator.rb`. Implement the `#carrierwave_uploader` method which should return a `CarrierWave::Uploader`, and implement the class method `.model` to return the `CoolWidget` class:
|
||||
- Implement the `replicable_title` and `replicable_title_plural` methods to
|
||||
return the human-readable singular and pluralized title of the replicable,
|
||||
which will be displayed in the UI and Rails console
|
||||
|
||||
```ruby
|
||||
# frozen_string_literal: true
|
||||
|
|
@ -340,6 +343,16 @@ That's all of the required database changes.
|
|||
::CoolWidget
|
||||
end
|
||||
|
||||
# @return [String] human-readable title.
|
||||
def self.replicable_title
|
||||
s_('Geo|Cool Widget')
|
||||
end
|
||||
|
||||
# @return [String] pluralized human-readable title.
|
||||
def self.replicable_title_plural
|
||||
s_('Geo|Cool Widgets')
|
||||
end
|
||||
|
||||
def carrierwave_uploader
|
||||
model_record.file
|
||||
end
|
||||
|
|
|
|||
|
|
@ -182,7 +182,6 @@ Gitlab/BoundedContexts:
|
|||
- 'app/graphql/mutations/incident_management/timeline_event_tag/base.rb'
|
||||
- 'app/graphql/mutations/incident_management/timeline_event_tag/create.rb'
|
||||
- 'app/graphql/mutations/issues/base.rb'
|
||||
- 'app/graphql/mutations/issues/bulk_update.rb'
|
||||
- 'app/graphql/mutations/issues/common_mutation_arguments.rb'
|
||||
- 'app/graphql/mutations/issues/create.rb'
|
||||
- 'app/graphql/mutations/issues/link_alerts.rb'
|
||||
|
|
@ -845,7 +844,6 @@ Gitlab/BoundedContexts:
|
|||
- 'app/models/concerns/issuable.rb'
|
||||
- 'app/models/concerns/issuable_link.rb'
|
||||
- 'app/models/concerns/issue_available_features.rb'
|
||||
- 'app/models/concerns/issue_parent.rb'
|
||||
- 'app/models/concerns/issue_resource_event.rb'
|
||||
- 'app/models/concerns/label_eventable.rb'
|
||||
- 'app/models/concerns/legacy_bulk_insert.rb'
|
||||
|
|
@ -2155,7 +2153,6 @@ Gitlab/BoundedContexts:
|
|||
- 'ee/app/graphql/ee/mutations/alert_management/http_integration/http_integration_base.rb'
|
||||
- 'ee/app/graphql/ee/mutations/alert_management/http_integration/update.rb'
|
||||
- 'ee/app/graphql/ee/mutations/groups/update.rb'
|
||||
- 'ee/app/graphql/ee/mutations/issues/bulk_update.rb'
|
||||
- 'ee/app/graphql/ee/mutations/issues/create.rb'
|
||||
- 'ee/app/graphql/ee/mutations/issues/update.rb'
|
||||
- 'ee/app/graphql/ee/mutations/resolves_issuable.rb'
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ Gitlab/FeatureFlagWithoutActor:
|
|||
- 'app/finders/merge_requests_finder.rb'
|
||||
- 'app/finders/notes_finder.rb'
|
||||
- 'app/finders/snippets_finder.rb'
|
||||
- 'app/graphql/mutations/issues/bulk_update.rb'
|
||||
- 'app/graphql/types/namespace_type.rb'
|
||||
- 'app/graphql/types/project_type.rb'
|
||||
- 'app/helpers/auto_devops_helper.rb'
|
||||
|
|
|
|||
|
|
@ -58,29 +58,6 @@ Layout/LineBreakAfterFinalMixin:
|
|||
- 'ee/app/workers/sync_seat_link_worker.rb'
|
||||
- 'ee/app/workers/vulnerabilities/historical_statistics/deletion_worker.rb'
|
||||
- 'ee/app/workers/vulnerabilities/statistics/schedule_worker.rb'
|
||||
- 'ee/lib/api/dependency_proxy/packages/maven.rb'
|
||||
- 'ee/lib/api/resource_iteration_events.rb'
|
||||
- 'ee/lib/api/resource_weight_events.rb'
|
||||
- 'ee/lib/ee/api/entities/project_push_rule.rb'
|
||||
- 'ee/lib/gitlab/auth/group_saml/identity_linker.rb'
|
||||
- 'ee/lib/gitlab/llm/chain/tools/explain_code/prompts/anthropic.rb'
|
||||
- 'ee/lib/gitlab/search/zoekt/client.rb'
|
||||
- 'ee/lib/gitlab/status_page/storage/s3_multipart_upload.rb'
|
||||
- 'ee/lib/search/zoekt/query.rb'
|
||||
- 'lib/api/discussions.rb'
|
||||
- 'lib/api/events.rb'
|
||||
- 'lib/api/group_labels.rb'
|
||||
- 'lib/api/issues.rb'
|
||||
- 'lib/api/labels.rb'
|
||||
- 'lib/api/notes.rb'
|
||||
- 'lib/api/project_events.rb'
|
||||
- 'lib/api/remote_mirrors.rb'
|
||||
- 'lib/api/resource_label_events.rb'
|
||||
- 'lib/api/resource_milestone_events.rb'
|
||||
- 'lib/api/resource_state_events.rb'
|
||||
- 'lib/api/rubygem_packages.rb'
|
||||
- 'lib/api/terraform/modules/v1/namespace_packages.rb'
|
||||
- 'lib/api/terraform/modules/v1/project_packages.rb'
|
||||
- 'lib/banzai/filter/references/design_reference_filter.rb'
|
||||
- 'lib/feature.rb'
|
||||
- 'lib/gitlab/auth/current_user_mode.rb'
|
||||
|
|
|
|||
|
|
@ -249,7 +249,6 @@ Layout/LineEndStringConcatenationIndentation:
|
|||
- 'ee/spec/requests/api/graphql/mutations/dast_site_profiles/update_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/epics/create_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/epics/update_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/issues/promote_to_epic_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/issues/set_epic_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/issues/set_weight_spec.rb'
|
||||
|
|
@ -531,7 +530,6 @@ Layout/LineEndStringConcatenationIndentation:
|
|||
- 'spec/requests/api/graphql/mutations/achievements/update_user_achievement_priorities_spec.rb'
|
||||
- 'spec/requests/api/graphql/mutations/ci/runner/create_spec.rb'
|
||||
- 'spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb'
|
||||
- 'spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb'
|
||||
- 'spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb'
|
||||
- 'spec/requests/api/graphql/mutations/ml/models/delete_spec.rb'
|
||||
- 'spec/requests/api/graphql/mutations/uploads/delete_spec.rb'
|
||||
|
|
|
|||
|
|
@ -2255,7 +2255,6 @@ Style/InlineDisableAnnotation:
|
|||
- 'spec/requests/api/ci/runner/jobs_artifacts_spec.rb'
|
||||
- 'spec/requests/api/graphql/groups_query_spec.rb'
|
||||
- 'spec/requests/api/graphql/issues_spec.rb'
|
||||
- 'spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb'
|
||||
- 'spec/requests/api/group_import_spec.rb'
|
||||
- 'spec/requests/api/internal/base_spec.rb'
|
||||
- 'spec/requests/api/ml_model_packages_spec.rb'
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
dad1421c02c5d89c3d667ed0abd41da2336d6418
|
||||
124a10130bac885023b44616e773ffd4229ee592
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'
|
|||
import EmailParticipantsWarning from './email_participants_warning.vue';
|
||||
|
||||
const ATTACHMENT_REGEXP = /!?\[.*?\]\(\/uploads\/[0-9a-f]{32}\/.*?\)/;
|
||||
const DEFAULT_NOTEABLE_TYPE = 'Issue';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
|
|
@ -34,11 +33,6 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
noteableType: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: DEFAULT_NOTEABLE_TYPE,
|
||||
},
|
||||
withAlertContainer: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
@ -92,9 +86,6 @@ export default {
|
|||
class="-gl-mb-3 gl-rounded-lg gl-rounded-b-none gl-pb-5 gl-pt-4"
|
||||
:is-locked="isLocked"
|
||||
:is-confidential="isConfidential"
|
||||
:noteable-type="noteableType"
|
||||
:locked-noteable-docs-path="noteableData.locked_discussion_docs_path"
|
||||
:confidential-noteable-docs-path="noteableData.confidential_issues_docs_path"
|
||||
/>
|
||||
<slot></slot>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -399,7 +399,6 @@ export default {
|
|||
:is-internal-note="noteIsInternal"
|
||||
:note="note"
|
||||
:noteable-data="getNoteableData"
|
||||
:noteable-type="noteableType"
|
||||
>
|
||||
<markdown-editor
|
||||
ref="markdownEditor"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapGetters } from 'vuex';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
|
|
@ -11,7 +12,9 @@ export default {
|
|||
return this.getNoteableDataByProp('archived_project_docs_path');
|
||||
},
|
||||
lockedIssueDocsPath() {
|
||||
return this.getNoteableDataByProp('locked_discussion_docs_path');
|
||||
return helpPagePath('user/discussions/_index.md', {
|
||||
anchor: 'prevent-comments-by-locking-the-discussion',
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -93,8 +93,6 @@ export default {
|
|||
discussion_locked: false,
|
||||
confidential: false,
|
||||
issue_email_participants: [],
|
||||
locked_discussion_docs_path: '',
|
||||
confidential_issues_docs_path: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
@ -266,7 +264,6 @@ export default {
|
|||
:is-internal-note="noteIsInternal"
|
||||
:note="note"
|
||||
:noteable-data="noteableData"
|
||||
:noteable-type="noteableType"
|
||||
>
|
||||
<markdown-editor
|
||||
ref="markdownEditor"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import getStandardContext from './get_standard_context';
|
||||
import { validateEvent } from './utils';
|
||||
import { isEventEligible, validateEvent } from './utils';
|
||||
|
||||
export function dispatchSnowplowEvent(
|
||||
category = document.body.dataset.page,
|
||||
|
|
@ -12,6 +12,10 @@ export function dispatchSnowplowEvent(
|
|||
throw new Error('Tracking: no category provided for tracking.');
|
||||
}
|
||||
|
||||
if (!isEventEligible(action)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
validateEvent(action);
|
||||
|
||||
const { label, property, extra = {} } = data;
|
||||
|
|
|
|||
|
|
@ -38,35 +38,36 @@ export function initDefaultTrackers() {
|
|||
return;
|
||||
}
|
||||
|
||||
const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions };
|
||||
|
||||
// must be before initializing the trackers
|
||||
Tracking.setAnonymousUrls();
|
||||
|
||||
window.snowplow('enableActivityTracking', {
|
||||
minimumVisitLength: 30,
|
||||
heartbeatDelay: 30,
|
||||
});
|
||||
// must be after enableActivityTracking
|
||||
const standardContext = getStandardContext();
|
||||
const experimentContexts = getAllExperimentContexts();
|
||||
// To not expose personal identifying information, the page title is hardcoded as `GitLab`
|
||||
// See: https://gitlab.com/gitlab-org/gitlab/-/issues/345243
|
||||
window.snowplow('trackPageView', {
|
||||
title: 'GitLab',
|
||||
context: [standardContext, ...experimentContexts],
|
||||
});
|
||||
window.snowplow('setDocumentTitle', 'GitLab');
|
||||
|
||||
if (window.snowplowOptions.formTracking) {
|
||||
Tracking.enableFormTracking(opts.formTrackingConfig);
|
||||
}
|
||||
|
||||
if (window.snowplowOptions.linkClickTracking) {
|
||||
window.snowplow('enableLinkClickTracking', {
|
||||
pseudoClicks: true,
|
||||
if (!window.gl?.onlySendDuoEvents) {
|
||||
window.snowplow('enableActivityTracking', {
|
||||
minimumVisitLength: 30,
|
||||
heartbeatDelay: 30,
|
||||
});
|
||||
// must be after enableActivityTracking
|
||||
const standardContext = getStandardContext();
|
||||
const experimentContexts = getAllExperimentContexts();
|
||||
// To not expose personal identifying information, the page title is hardcoded as `GitLab`
|
||||
// See: https://gitlab.com/gitlab-org/gitlab/-/issues/345243
|
||||
window.snowplow('trackPageView', {
|
||||
title: 'GitLab',
|
||||
context: [standardContext, ...experimentContexts],
|
||||
});
|
||||
window.snowplow('setDocumentTitle', 'GitLab');
|
||||
|
||||
if (window.snowplowOptions.formTracking) {
|
||||
const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions };
|
||||
Tracking.enableFormTracking(opts.formTrackingConfig);
|
||||
}
|
||||
|
||||
if (window.snowplowOptions.linkClickTracking) {
|
||||
window.snowplow('enableLinkClickTracking', {
|
||||
pseudoClicks: true,
|
||||
context: [standardContext, ...experimentContexts],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Tracking.flushPendingEvents();
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
validateAdditionalProperties,
|
||||
getBaseAdditionalProperties,
|
||||
getCustomAdditionalProperties,
|
||||
isEventEligible,
|
||||
} from './utils';
|
||||
|
||||
const elementsWithBinding = new WeakMap();
|
||||
|
|
@ -25,6 +26,10 @@ const InternalEvents = {
|
|||
*
|
||||
*/
|
||||
trackEvent(event, additionalProperties = {}, category = undefined) {
|
||||
if (!isEventEligible(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
validateAdditionalProperties(additionalProperties);
|
||||
|
||||
const baseProperties = getBaseAdditionalProperties(additionalProperties);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
<script>
|
||||
import { GlLink, GlIcon, GlSprintf } from '@gitlab/ui';
|
||||
import { __, sprintf } from '~/locale';
|
||||
|
||||
const noteableTypeText = {
|
||||
Issue: __('issue'),
|
||||
Epic: __('epic'),
|
||||
MergeRequest: __('merge request'),
|
||||
Task: __('task'),
|
||||
KeyResult: __('key result'),
|
||||
Objective: __('objective'),
|
||||
};
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
|
||||
export default {
|
||||
docsLinks: {
|
||||
locked: helpPagePath('user/discussions/_index', {
|
||||
anchor: 'prevent-comments-by-locking-the-discussion',
|
||||
}),
|
||||
confidential: helpPagePath('user/discussions/_index', {
|
||||
anchor: 'comments-on-confidential-items',
|
||||
}),
|
||||
},
|
||||
components: {
|
||||
GlIcon,
|
||||
GlLink,
|
||||
|
|
@ -28,22 +27,6 @@ export default {
|
|||
default: false,
|
||||
required: false,
|
||||
},
|
||||
noteableType: {
|
||||
type: String,
|
||||
required: false,
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
default: 'Issue',
|
||||
},
|
||||
lockedNoteableDocsPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
confidentialNoteableDocsPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
warningIcon() {
|
||||
|
|
@ -55,19 +38,6 @@ export default {
|
|||
isLockedAndConfidential() {
|
||||
return this.isConfidential && this.isLocked;
|
||||
},
|
||||
noteableTypeText() {
|
||||
return noteableTypeText[this.noteableType];
|
||||
},
|
||||
confidentialContextText() {
|
||||
return sprintf(__('This is a confidential %{noteableTypeText}.'), {
|
||||
noteableTypeText: this.noteableTypeText,
|
||||
});
|
||||
},
|
||||
lockedContextText() {
|
||||
return sprintf(__('The discussion in this %{noteableTypeText} is locked.'), {
|
||||
noteableTypeText: this.noteableTypeText,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -85,36 +55,44 @@ export default {
|
|||
<gl-sprintf
|
||||
:message="
|
||||
__(
|
||||
'This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and its %{lockedLinkStart}discussion is locked%{lockedLinkEnd}.',
|
||||
'Marked as %{confidentialLinkStart}confidential%{confidentialLinkEnd} and discussion is %{lockedLinkStart}locked%{lockedLinkEnd}. People without permission will never get a notification and won\'t be able to comment.',
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #noteableTypeText>{{ noteableTypeText }}</template>
|
||||
<template #confidentialLink="{ content }">
|
||||
<gl-link :href="confidentialNoteableDocsPath" target="_blank">{{ content }}</gl-link>
|
||||
<gl-link :href="$options.docsLinks.confidential" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
<template #lockedLink="{ content }">
|
||||
<gl-link :href="lockedNoteableDocsPath" target="_blank">{{ content }}</gl-link>
|
||||
<gl-link :href="$options.docsLinks.locked" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
{{
|
||||
__("People without permission will never get a notification and won't be able to comment.")
|
||||
}}
|
||||
</span>
|
||||
|
||||
<span v-else-if="isConfidential" ref="confidential">
|
||||
{{ confidentialContextText }}
|
||||
{{ __('People without permission will never get a notification.') }}
|
||||
<gl-link :href="confidentialNoteableDocsPath" target="_blank">{{
|
||||
__('Learn more.')
|
||||
}}</gl-link>
|
||||
<gl-sprintf
|
||||
:message="
|
||||
__(
|
||||
'Marked as %{confidentialLinkStart}confidential%{confidentialLinkEnd}. People without permission will never get a notification.',
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #confidentialLink="{ content }">
|
||||
<gl-link :href="$options.docsLinks.confidential" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
|
||||
<span v-else-if="isLocked" ref="locked">
|
||||
{{ lockedContextText }}
|
||||
{{ __('Only project members can comment.') }}
|
||||
<gl-link :href="lockedNoteableDocsPath" target="_blank">{{ __('Learn more.') }}</gl-link>
|
||||
<gl-sprintf
|
||||
:message="
|
||||
__('Discussion is %{lockedLinkStart}locked%{lockedLinkEnd}. Only members can comment.')
|
||||
"
|
||||
>
|
||||
<template #lockedLink="{ content }">
|
||||
<gl-link :href="$options.docsLinks.locked" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import {
|
|||
import { TYPENAME_MERGE_REQUEST, TYPENAME_VULNERABILITY } from '~/graphql_shared/constants';
|
||||
import {
|
||||
I18N_WORK_ITEM_ERROR_CREATING,
|
||||
sprintfWorkItem,
|
||||
i18n,
|
||||
NAME_TO_TEXT_LOWERCASE_MAP,
|
||||
NAME_TO_TEXT_MAP,
|
||||
|
|
@ -396,21 +395,27 @@ export default {
|
|||
return options;
|
||||
},
|
||||
createErrorText() {
|
||||
return sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, this.selectedWorkItemTypeName);
|
||||
return sprintf(I18N_WORK_ITEM_ERROR_CREATING, {
|
||||
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.selectedWorkItemTypeName],
|
||||
});
|
||||
},
|
||||
createWorkItemText() {
|
||||
return sprintfWorkItem(s__('WorkItem|Create %{workItemType}'), this.selectedWorkItemTypeName);
|
||||
return sprintf(s__('WorkItem|Create %{workItemType}'), {
|
||||
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.selectedWorkItemTypeName],
|
||||
});
|
||||
},
|
||||
makeConfidentialText() {
|
||||
return sprintfWorkItem(
|
||||
return sprintf(
|
||||
s__(
|
||||
'WorkItem|This %{workItemType} is confidential and should only be visible to users having at least the Planner role',
|
||||
),
|
||||
this.selectedWorkItemTypeName,
|
||||
{ workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.selectedWorkItemTypeName] },
|
||||
);
|
||||
},
|
||||
titleText() {
|
||||
return sprintfWorkItem(s__('WorkItem|New %{workItemType}'), this.selectedWorkItemTypeName);
|
||||
return sprintf(s__('WorkItem|New %{workItemType}'), {
|
||||
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.selectedWorkItemTypeName],
|
||||
});
|
||||
},
|
||||
canUpdate() {
|
||||
return this.workItem?.userPermissions?.updateWorkItem;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { helpPagePath } from '~/helpers/help_page_helper';
|
|||
import { s__, __ } from '~/locale';
|
||||
import { detectAndConfirmSensitiveTokens } from '~/lib/utils/secret_detection';
|
||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||
import { STATE_OPEN, WORK_ITEM_TYPE_NAME_TASK, i18n } from '~/work_items/constants';
|
||||
import { STATE_OPEN, i18n } from '~/work_items/constants';
|
||||
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
|
||||
import gfmEventHub from '~/vue_shared/components/markdown/eventhub';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
|
|
@ -17,19 +17,6 @@ import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
|
|||
import workItemEmailParticipantsByIidQuery from '../../graphql/notes/work_item_email_participants_by_iid.query.graphql';
|
||||
import { findEmailParticipantsWidget } from '../../utils';
|
||||
|
||||
const DOCS_WORK_ITEM_LOCKED_TASKS_PATH = helpPagePath('user/tasks.html', {
|
||||
anchor: 'lock-discussion',
|
||||
});
|
||||
const DOCS_WORK_ITEM_CONFIDENTIAL_TASKS_PATH = helpPagePath('user/tasks.html', {
|
||||
anchor: 'confidential-tasks',
|
||||
});
|
||||
const DOCS_WORK_ITEM_LOCKED_OKRS_PATH = helpPagePath('user/okrs.html', {
|
||||
anchor: 'lock-discussion',
|
||||
});
|
||||
const DOCS_WORK_ITEM_CONFIDENTIAL_OKRS_PATH = helpPagePath('user/okrs.html', {
|
||||
anchor: 'confidential-okrs',
|
||||
});
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
internal: s__('Notes|Make this an internal note'),
|
||||
|
|
@ -201,23 +188,11 @@ export default {
|
|||
commentButtonTextComputed() {
|
||||
return this.isNoteInternal ? this.$options.i18n.addInternalNote : this.commentButtonText;
|
||||
},
|
||||
docsLinks() {
|
||||
return this.workItemType === WORK_ITEM_TYPE_NAME_TASK
|
||||
? {
|
||||
confidential_issues_docs_path: DOCS_WORK_ITEM_CONFIDENTIAL_TASKS_PATH,
|
||||
locked_discussion_docs_path: DOCS_WORK_ITEM_LOCKED_TASKS_PATH,
|
||||
}
|
||||
: {
|
||||
confidential_issues_docs_path: DOCS_WORK_ITEM_CONFIDENTIAL_OKRS_PATH,
|
||||
locked_discussion_docs_path: DOCS_WORK_ITEM_LOCKED_OKRS_PATH,
|
||||
};
|
||||
},
|
||||
getWorkItemData() {
|
||||
return {
|
||||
confidential: this.isWorkItemConfidential,
|
||||
discussion_locked: this.isDiscussionLocked,
|
||||
issue_email_participants: this.emailParticipants,
|
||||
...this.docsLinks,
|
||||
};
|
||||
},
|
||||
workItemTypeKey() {
|
||||
|
|
@ -369,7 +344,6 @@ export default {
|
|||
:is-internal-note="isDiscussionInternal || isNoteInternal"
|
||||
:note="commentText"
|
||||
:noteable-data="getWorkItemData"
|
||||
:noteable-type="workItemTypeKey"
|
||||
>
|
||||
<markdown-editor
|
||||
ref="markdownEditor"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
NAME_TO_TEXT_LOWERCASE_MAP,
|
||||
NAME_TO_TEXT_MAP,
|
||||
ALLOWED_CONVERSION_TYPES,
|
||||
sprintfWorkItem,
|
||||
WIDGET_TYPE_DESIGNS,
|
||||
WIDGET_TYPE_HIERARCHY,
|
||||
WIDGET_TYPE_MILESTONE,
|
||||
|
|
@ -357,12 +356,14 @@ export default {
|
|||
const isEpicWithSubepicsFeature =
|
||||
this.parentWorkItemType === WORK_ITEM_TYPE_NAME_EPIC && this.hasSubepicsFeature;
|
||||
if (this.hasParent && !isEpicWithSubepicsFeature) {
|
||||
this.warningMessage = sprintfWorkItem(
|
||||
this.warningMessage = sprintf(
|
||||
s__(
|
||||
'WorkItem|Parent item type %{parentWorkItemType} is not supported on %{workItemType}. Remove the parent item to change type.',
|
||||
),
|
||||
this.selectedWorkItemType.name,
|
||||
this.parentWorkItemType,
|
||||
{
|
||||
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.selectedWorkItemType.name],
|
||||
parentWorkItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.parentWorkItemType],
|
||||
},
|
||||
);
|
||||
|
||||
this.changeTypeDisabled = true;
|
||||
|
|
@ -395,11 +396,11 @@ export default {
|
|||
|
||||
// Compare the widget definitions of both types
|
||||
if (this.hasWidgetDifference) {
|
||||
this.warningMessage = sprintfWorkItem(
|
||||
this.warningMessage = sprintf(
|
||||
s__(
|
||||
'WorkItem|Some fields are not present in %{workItemType}. If you change type now, this information will be lost.',
|
||||
),
|
||||
this.selectedWorkItemType.name,
|
||||
{ workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.selectedWorkItemType.name] },
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
import { GlDatepicker, GlFormGroup } from '@gitlab/ui';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { findStartAndDueDateWidget, newWorkItemId } from '~/work_items/utils';
|
||||
import { s__ } from '~/locale';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import { formatDate, newDate, toISODateFormat } from '~/lib/utils/datetime_utility';
|
||||
import {
|
||||
I18N_WORK_ITEM_ERROR_UPDATING,
|
||||
sprintfWorkItem,
|
||||
NAME_TO_TEXT_LOWERCASE_MAP,
|
||||
TRACKING_CATEGORY_SHOW,
|
||||
WIDGET_TYPE_START_AND_DUE_DATE,
|
||||
} from '../constants';
|
||||
|
|
@ -215,7 +215,9 @@ export default {
|
|||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
|
||||
const message = sprintf(I18N_WORK_ITEM_ERROR_UPDATING, {
|
||||
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType],
|
||||
});
|
||||
this.$emit('error', message);
|
||||
Sentry.captureException(error);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { debounce } from 'lodash';
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
import { createAlert } from '~/alert';
|
||||
import {
|
||||
sprintfWorkItem,
|
||||
NAME_TO_TEXT_LOWERCASE_MAP,
|
||||
WORK_ITEM_CREATE_ENTITY_MODAL_TARGET_SOURCE,
|
||||
WORK_ITEM_CREATE_ENTITY_MODAL_TARGET_BRANCH,
|
||||
} from '~/work_items/constants';
|
||||
|
|
@ -19,7 +19,7 @@ import {
|
|||
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
|
||||
|
||||
import getProjectRootRef from '~/work_items/graphql/get_project_root_ref.query.graphql';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { s__, __, sprintf } from '~/locale';
|
||||
import confidentialMergeRequestState from '~/confidential_merge_request/state';
|
||||
import ProjectFormGroup from '~/confidential_merge_request/components/project_form_group.vue';
|
||||
|
||||
|
|
@ -250,9 +250,9 @@ export default {
|
|||
this.$emit('hideModal');
|
||||
} catch {
|
||||
createAlert({
|
||||
message: sprintfWorkItem(
|
||||
message: sprintf(
|
||||
s__('WorkItem|Failed to create a branch for this %{workItemType}. Please try again.'),
|
||||
this.workItemType?.toLowerCase(),
|
||||
{ workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType] },
|
||||
),
|
||||
});
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -2,11 +2,15 @@
|
|||
import { GlIcon, GlAlert, GlTooltipDirective } from '@gitlab/ui';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { ERROR_POLICY_ALL } from '~/lib/graphql';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { s__, __, sprintf } from '~/locale';
|
||||
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
|
||||
import workItemDevelopmentQuery from '~/work_items/graphql/work_item_development.query.graphql';
|
||||
import workItemDevelopmentUpdatedSubscription from '~/work_items/graphql/work_item_development.subscription.graphql';
|
||||
import { sprintfWorkItem, STATE_OPEN, DEVELOPMENT_ITEMS_ANCHOR } from '~/work_items/constants';
|
||||
import {
|
||||
DEVELOPMENT_ITEMS_ANCHOR,
|
||||
NAME_TO_TEXT_LOWERCASE_MAP,
|
||||
STATE_OPEN,
|
||||
} from '~/work_items/constants';
|
||||
import { findDevelopmentWidget } from '~/work_items/utils';
|
||||
import CrudComponent from '~/vue_shared/components/crud_component.vue';
|
||||
import WorkItemActionsSplitButton from '~/work_items/components/work_item_links/work_item_actions_split_button.vue';
|
||||
|
|
@ -109,21 +113,21 @@ export default {
|
|||
},
|
||||
openStateText() {
|
||||
return this.closingMergeRequests.length > 1
|
||||
? sprintfWorkItem(
|
||||
? sprintf(
|
||||
s__(
|
||||
'WorkItem|This %{workItemType} will be closed when any of the following is merged.',
|
||||
),
|
||||
this.workItemType,
|
||||
{ workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType] },
|
||||
)
|
||||
: sprintfWorkItem(
|
||||
: sprintf(
|
||||
s__('WorkItem|This %{workItemType} will be closed when the following is merged.'),
|
||||
this.workItemType,
|
||||
{ workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType] },
|
||||
);
|
||||
},
|
||||
closedStateText() {
|
||||
return sprintfWorkItem(
|
||||
return sprintf(
|
||||
s__('WorkItem|The %{workItemType} was closed automatically when a branch was merged.'),
|
||||
this.workItemType,
|
||||
{ workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType] },
|
||||
);
|
||||
},
|
||||
tooltipText() {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
<script>
|
||||
import { GlDisclosureDropdown, GlPopover, GlLink, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { s__ } from '~/locale';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import HelpIcon from '~/vue_shared/components/help_icon/help_icon.vue';
|
||||
import {
|
||||
NAME_TO_TEXT_LOWERCASE_MAP,
|
||||
WORK_ITEM_TYPE_NAME_EPIC,
|
||||
WORK_ITEM_TYPE_NAME_OBJECTIVE,
|
||||
sprintfWorkItem,
|
||||
} from '../../constants';
|
||||
|
||||
export default {
|
||||
|
|
@ -42,11 +42,11 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
getPopoverText(workItemType) {
|
||||
return sprintfWorkItem(
|
||||
return sprintf(
|
||||
s__(
|
||||
'WorkItem|You cannot add another child %{workItemType}. You’ve reached the maximum number of nested levels.',
|
||||
),
|
||||
workItemType,
|
||||
{ workItemType: NAME_TO_TEXT_LOWERCASE_MAP[workItemType] },
|
||||
);
|
||||
},
|
||||
getPopoverLink(workItemType) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
|||
import Tracking from '~/tracking';
|
||||
import { newWorkItemId } from '~/work_items/utils';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { s__, __, sprintf } from '~/locale';
|
||||
import { MILESTONE_STATE } from '~/sidebar/constants';
|
||||
import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
|
||||
import { ISSUE_MR_CHANGE_MILESTONE } from '~/behaviors/shortcuts/keybindings';
|
||||
|
|
@ -14,7 +14,7 @@ import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutati
|
|||
import updateNewWorkItemMutation from '~/work_items/graphql/update_new_work_item.mutation.graphql';
|
||||
import {
|
||||
I18N_WORK_ITEM_ERROR_UPDATING,
|
||||
sprintfWorkItem,
|
||||
NAME_TO_TEXT_LOWERCASE_MAP,
|
||||
TRACKING_CATEGORY_SHOW,
|
||||
} from '../constants';
|
||||
|
||||
|
|
@ -218,7 +218,9 @@ export default {
|
|||
})
|
||||
.catch((error) => {
|
||||
this.localMilestone = this.workItemMilestone;
|
||||
const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
|
||||
const msg = sprintf(I18N_WORK_ITEM_ERROR_UPDATING, {
|
||||
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType],
|
||||
});
|
||||
this.$emit('error', msg);
|
||||
Sentry.captureException(error);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { GlIcon, GlLink, GlPopover } from '@gitlab/ui';
|
||||
import { uniqueId } from 'lodash';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { s__ } from '~/locale';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
|
||||
import updateParentMutation from '~/work_items/graphql/update_parent.mutation.graphql';
|
||||
import { isValidURL } from '~/lib/utils/url_utility';
|
||||
|
|
@ -22,8 +22,8 @@ import workItemAllowedParentTypesQuery from '../graphql/work_item_allowed_parent
|
|||
import {
|
||||
I18N_WORK_ITEM_ERROR_UPDATING,
|
||||
NAME_TO_ENUM_MAP,
|
||||
NAME_TO_TEXT_LOWERCASE_MAP,
|
||||
NO_WORK_ITEM_IID,
|
||||
sprintfWorkItem,
|
||||
WORK_ITEM_TYPE_ENUM_EPIC,
|
||||
WORK_ITEM_TYPE_NAME_EPIC,
|
||||
WORK_ITEM_TYPE_NAME_ISSUE,
|
||||
|
|
@ -249,7 +249,9 @@ export default {
|
|||
.catch((error) => {
|
||||
this.$emit(
|
||||
'error',
|
||||
sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType),
|
||||
sprintf(I18N_WORK_ITEM_ERROR_UPDATING, {
|
||||
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType],
|
||||
}),
|
||||
);
|
||||
Sentry.captureException(error);
|
||||
})
|
||||
|
|
@ -289,7 +291,12 @@ export default {
|
|||
this.localSelectedItem = this.parent?.id || NO_WORK_ITEM_IID;
|
||||
}
|
||||
} catch (error) {
|
||||
this.$emit('error', sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType));
|
||||
this.$emit(
|
||||
'error',
|
||||
sprintf(I18N_WORK_ITEM_ERROR_UPDATING, {
|
||||
workItemType: NAME_TO_TEXT_LOWERCASE_MAP[this.workItemType],
|
||||
}),
|
||||
);
|
||||
Sentry.captureException(error);
|
||||
} finally {
|
||||
this.searchStarted = false;
|
||||
|
|
|
|||
|
|
@ -1,125 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Issues
|
||||
class BulkUpdate < BaseMutation
|
||||
graphql_name 'IssuesBulkUpdate'
|
||||
|
||||
include Gitlab::Graphql::Authorize::AuthorizeResource
|
||||
|
||||
MAX_ISSUES = 100
|
||||
|
||||
description 'Allows updating several properties for a set of issues. ' \
|
||||
'Does nothing if the `bulk_update_issues_mutation` feature flag is disabled.'
|
||||
|
||||
argument :parent_id, ::Types::GlobalIDType[::IssueParent],
|
||||
required: true,
|
||||
description: 'Global ID of the parent to which the bulk update will be scoped. ' \
|
||||
'The parent can be a project. The parent can also be a group (Premium and Ultimate only). ' \
|
||||
'Example `IssueParentID` are `"gid://gitlab/Project/1"` and `"gid://gitlab/Group/1"`.'
|
||||
|
||||
argument :ids, [::Types::GlobalIDType[::Issue]],
|
||||
required: true,
|
||||
description: 'Global ID array of the issues that will be updated. ' \
|
||||
"IDs that the user can\'t update will be ignored. A max of #{MAX_ISSUES} can be provided."
|
||||
|
||||
argument :assignee_ids, [::Types::GlobalIDType[::User]],
|
||||
required: false,
|
||||
description: 'Global ID array of the users that will be assigned to the given issues. ' \
|
||||
'Existing assignees will be replaced with the ones on this list.'
|
||||
|
||||
argument :milestone_id, ::Types::GlobalIDType[::Milestone],
|
||||
required: false,
|
||||
description: 'Global ID of the milestone that will be assigned to the issues.'
|
||||
|
||||
argument :state_event, Types::IssueStateEventEnum,
|
||||
description: 'Close or reopen an issue.',
|
||||
required: false
|
||||
|
||||
argument :add_label_ids, [::Types::GlobalIDType[::Label]],
|
||||
description: 'Global ID array of the labels that will be added to the issues. ',
|
||||
required: false
|
||||
|
||||
argument :remove_label_ids, [::Types::GlobalIDType[::Label]],
|
||||
description: 'Global ID array of the labels that will be removed from the issues. ',
|
||||
required: false
|
||||
|
||||
argument :subscription_event, Types::IssuableSubscriptionEventEnum,
|
||||
description: 'Subscribe to or unsubscribe from issue notifications.',
|
||||
required: false
|
||||
|
||||
field :updated_issue_count, GraphQL::Types::Int,
|
||||
null: true,
|
||||
description: 'Number of issues that were successfully updated.'
|
||||
|
||||
def ready?(**args)
|
||||
if Feature.disabled?(:bulk_update_issues_mutation)
|
||||
raise_resource_not_available_error! '`bulk_update_issues_mutation` feature flag is disabled.'
|
||||
end
|
||||
|
||||
if args[:ids].size > MAX_ISSUES
|
||||
raise Gitlab::Graphql::Errors::ArgumentError,
|
||||
format(_('No more than %{max_issues} issues can be updated at the same time'), max_issues: MAX_ISSUES)
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def resolve(ids:, parent_id:, **attributes)
|
||||
parent = find_parent!(parent_id)
|
||||
|
||||
result = Issuable::BulkUpdateService.new(
|
||||
parent,
|
||||
current_user,
|
||||
prepared_params(attributes, ids)
|
||||
).execute('issue')
|
||||
|
||||
if result.success?
|
||||
{ updated_issue_count: result.payload[:count], errors: [] }
|
||||
else
|
||||
{ errors: result.errors }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_parent!(parent_id)
|
||||
parent = GitlabSchema.find_by_gid(parent_id).sync
|
||||
raise_resource_not_available_error! unless current_user.can?("read_#{parent.to_ability_name}", parent)
|
||||
|
||||
parent
|
||||
end
|
||||
|
||||
def prepared_params(attributes, ids)
|
||||
prepared = attributes.except(*global_id_arguments).merge(issuable_ids: model_ids_from(ids).uniq)
|
||||
|
||||
global_id_arguments.each do |argument|
|
||||
next unless attributes.key?(argument)
|
||||
|
||||
prepared[argument] = model_ids_from(attributes[argument])
|
||||
end
|
||||
|
||||
# Ruby 3+: This can be simplified to:
|
||||
# prepared.transform_keys(param_mappings)
|
||||
prepared.transform_keys { |key| param_mappings[key] || key }
|
||||
end
|
||||
|
||||
def param_mappings
|
||||
{}
|
||||
end
|
||||
|
||||
def global_id_arguments
|
||||
%i[assignee_ids milestone_id add_label_ids remove_label_ids]
|
||||
end
|
||||
|
||||
def model_ids_from(attributes)
|
||||
return if attributes.nil?
|
||||
return attributes.map(&:model_id) if attributes.is_a?(Array)
|
||||
|
||||
attributes.model_id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Mutations::Issues::BulkUpdate.prepend_mod
|
||||
|
|
@ -9,8 +9,7 @@ module Mutations
|
|||
|
||||
MAX_WORK_ITEMS = 100
|
||||
|
||||
description 'Allows updating several properties for a set of issues. ' \
|
||||
'Does nothing if the `bulk_update_issues_mutation` feature flag is disabled.'
|
||||
description 'Allows updating several properties for a set of work items. '
|
||||
|
||||
argument :ids, [::Types::GlobalIDType[::WorkItem]],
|
||||
required: true,
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ module Types
|
|||
mount_mutation Mutations::Issues::Move
|
||||
mount_mutation Mutations::Issues::LinkAlerts
|
||||
mount_mutation Mutations::Issues::UnlinkAlert
|
||||
mount_mutation Mutations::Issues::BulkUpdate, experiment: { milestone: '15.9' }
|
||||
mount_mutation Mutations::Labels::Create
|
||||
mount_mutation Mutations::Members::Groups::BulkUpdate
|
||||
mount_mutation Mutations::Members::Projects::BulkUpdate
|
||||
|
|
|
|||
|
|
@ -97,13 +97,13 @@ class ApplicationSetting < ApplicationRecord
|
|||
|
||||
serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :domain_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :domain_denylist, Array # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :disabled_oauth_sign_in_sources, type: Array # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :domain_allowlist, type: Array # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :domain_denylist, type: Array # rubocop:disable Cop/ActiveRecordSerialize
|
||||
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/300916
|
||||
serialize :asset_proxy_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :asset_proxy_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :asset_proxy_allowlist, type: Array # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :asset_proxy_whitelist, type: Array # rubocop:disable Cop/ActiveRecordSerialize
|
||||
|
||||
cache_markdown_field :help_page_text
|
||||
cache_markdown_field :shared_runners_text, pipeline: :plain_markdown
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class AuditEvent < ApplicationRecord
|
|||
|
||||
partitioned_by :created_at, strategy: :monthly
|
||||
|
||||
serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :details, type: Hash # rubocop:disable Cop/ActiveRecordSerialize
|
||||
|
||||
belongs_to :user, foreign_key: :author_id, inverse_of: :audit_events
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ module Ci
|
|||
|
||||
# rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :options
|
||||
serialize :yaml_variables, ::Gitlab::Serializer::Ci::Variables
|
||||
serialize :yaml_variables, coder: ::Gitlab::Serializer::Ci::Variables
|
||||
# rubocop:enable Cop/ActiveRecordSerialize
|
||||
|
||||
state_machine :status do
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ module Ci
|
|||
delegate :enable_debug_trace!, to: :metadata
|
||||
|
||||
serialize :options # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :yaml_variables, Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :yaml_variables, coder: Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiveRecordSerialize
|
||||
|
||||
delegate :name, to: :project, prefix: true
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ module AuditEvents
|
|||
|
||||
partitioned_by :created_at, strategy: :monthly
|
||||
|
||||
serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize -- We need this to serialize details stored in audit event.
|
||||
serialize :details, type: Hash # rubocop:disable Cop/ActiveRecordSerialize -- We need this to serialize details stored in audit event.
|
||||
|
||||
belongs_to :user, foreign_key: :author_id, inverse_of: :audit_events
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ module DiffPositionableNote
|
|||
before_validation :set_original_position, on: :create
|
||||
before_validation :update_position, on: :create, if: :should_update_position?, unless: :importing?
|
||||
|
||||
serialize :original_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :change_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :original_position, type: Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :position, type: Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :change_position, type: Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
|
||||
|
||||
validate :diff_refs_match_commit, if: :for_commit?
|
||||
validates :position, json_schema: { filename: "position", hash_conversion: true }
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# == IssuParent
|
||||
#
|
||||
# Used as a common ancestor for Group and Project so we can allow a polymorphic
|
||||
# Types::GlobalIDType[::IssueParent] in the GraphQL API
|
||||
#
|
||||
# Used by Project, Group
|
||||
#
|
||||
module IssueParent
|
||||
end
|
||||
|
|
@ -17,9 +17,9 @@ class WebHookLog < ApplicationRecord
|
|||
|
||||
belongs_to :web_hook
|
||||
|
||||
serialize :request_headers, Hash # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :request_data, Hash # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :response_headers, Hash # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :request_headers, type: Hash # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :request_data, type: Hash # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :response_headers, type: Hash # rubocop:disable Cop/ActiveRecordSerialize
|
||||
|
||||
validates :web_hook, presence: true
|
||||
|
||||
|
|
|
|||
|
|
@ -233,13 +233,9 @@ class Issue < ApplicationRecord
|
|||
scope :confidential_only, -> { where(confidential: true) }
|
||||
|
||||
scope :without_hidden, -> {
|
||||
if Feature.enabled?(:optimize_issues_banned_users_query, :instance)
|
||||
# We add `+ 0` to the author_id to make the query planner use a nested loop and prevent
|
||||
# loading of all banned user IDs for certain queries
|
||||
where_not_exists(Users::BannedUser.where('banned_users.user_id = (issues.author_id + 0)'))
|
||||
else
|
||||
where_not_exists(Users::BannedUser.where('banned_users.user_id = issues.author_id'))
|
||||
end
|
||||
# We add `+ 0` to the author_id to make the query planner use a nested loop and prevent
|
||||
# loading of all banned user IDs for certain queries
|
||||
where_not_exists(Users::BannedUser.where('banned_users.user_id = (issues.author_id + 0)'))
|
||||
}
|
||||
|
||||
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ class MergeRequest < ApplicationRecord
|
|||
:sha,
|
||||
:skip_ci
|
||||
].freeze
|
||||
serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :merge_params, type: Hash # rubocop:disable Cop/ActiveRecordSerialize
|
||||
|
||||
before_validation :set_draft_status
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class PersonalAccessToken < ApplicationRecord
|
|||
MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS_BUFFERED = 400
|
||||
MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS = 365
|
||||
|
||||
serialize :scopes, Array # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :scopes, type: Array # rubocop:disable Cop/ActiveRecordSerialize
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :organization, class_name: 'Organizations::Organization'
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ class Project < ApplicationRecord
|
|||
include RunnerTokenExpirationInterval
|
||||
include BlocksUnsafeSerialization
|
||||
include Subquery
|
||||
include IssueParent
|
||||
include WorkItems::Parent
|
||||
include UpdatedAtFilterable
|
||||
include CrossDatabaseIgnoredTables
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class ProjectImportData < ApplicationRecord
|
|||
# because the credentials are necessary for a successful import.
|
||||
# This is safe because the serialization is only going between rails
|
||||
# and the database, never to any end users.
|
||||
serialize :data, Serializers::UnsafeJson # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :data, coder: Serializers::UnsafeJson # rubocop:disable Cop/ActiveRecordSerialize
|
||||
|
||||
validates :project, presence: true
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Projects
|
||||
class DeletionSchedule < ApplicationRecord
|
||||
self.table_name = 'project_deletion_schedules'
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :deleting_user, foreign_key: 'user_id', class_name: 'User', inverse_of: :project_deletion_schedules
|
||||
|
||||
validates :marked_for_deletion_at, presence: true
|
||||
end
|
||||
end
|
||||
|
|
@ -112,7 +112,7 @@ class User < ApplicationRecord
|
|||
|
||||
devise :two_factor_backupable, otp_number_of_backup_codes: 10
|
||||
devise :two_factor_backupable_pbkdf2
|
||||
serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :otp_backup_codes, coder: JSON # rubocop:disable Cop/ActiveRecordSerialize
|
||||
|
||||
devise :lockable, :recoverable, :rememberable, :trackable,
|
||||
:validatable, :omniauthable, :confirmable, :registerable
|
||||
|
|
@ -228,8 +228,9 @@ class User < ApplicationRecord
|
|||
has_many :groups_projects, through: :groups, source: :projects
|
||||
has_many :personal_projects, through: :namespace, source: :projects
|
||||
has_many :project_members, -> { where(requested_at: nil) }
|
||||
has_many :projects, through: :project_members
|
||||
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project', dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :projects, through: :project_members
|
||||
has_many :project_deletion_schedules, class_name: '::Projects::DeletionSchedule', inverse_of: :deleting_user
|
||||
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project', dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :created_namespace_details, foreign_key: :creator_id, class_name: 'Namespace::Detail'
|
||||
has_many :projects_with_active_memberships, -> { where(members: { state: ::Member::STATE_ACTIVE }) }, through: :project_members, source: :project
|
||||
has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
|
|
|
|||
|
|
@ -268,7 +268,17 @@ module Projects
|
|||
::Ci::DestroySecureFileService.new(project, current_user).execute(secure_file)
|
||||
end
|
||||
|
||||
deleted_count = ::CommitStatus.for_project(project).delete_all
|
||||
delete_commit_statuses
|
||||
end
|
||||
|
||||
def delete_commit_statuses
|
||||
deleted_count = 0
|
||||
|
||||
loop do
|
||||
deleted_rows = ::CommitStatus.for_project(project).limit(BATCH_SIZE).delete_all
|
||||
deleted_count += deleted_rows
|
||||
break if deleted_rows < BATCH_SIZE
|
||||
end
|
||||
|
||||
Gitlab::AppLogger.info(
|
||||
class: self.class.name,
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ module Gitlab
|
|||
require_dependency Rails.root.join('lib/gitlab/patch/database_config')
|
||||
require_dependency Rails.root.join('lib/gitlab/patch/redis_cache_store')
|
||||
require_dependency Rails.root.join('lib/gitlab/patch/old_redis_cache_store')
|
||||
require_dependency Rails.root.join('lib/gitlab/pdf')
|
||||
require_dependency Rails.root.join('lib/gitlab/exceptions_app')
|
||||
|
||||
unless ::Gitlab.next_rails?
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: prompt_migration_explain_vulnerability
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/475046
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/164210
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/480824
|
||||
milestone: '17.4'
|
||||
group: group::custom models
|
||||
type: beta
|
||||
default_enabled: true
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: prompt_migration_merge_request_reader
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/499593
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169942
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/issues/595
|
||||
milestone: '17.6'
|
||||
group: group::custom models
|
||||
type: beta
|
||||
default_enabled: true
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: prompt_migration_summarize_comments
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/475044
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/163772
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/480550
|
||||
milestone: '17.4'
|
||||
group: group::custom models
|
||||
type: beta
|
||||
default_enabled: true
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: prompt_migration_troubleshoot_job
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/475045
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169944
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/524191
|
||||
milestone: '17.6'
|
||||
group: group::custom models
|
||||
type: beta
|
||||
default_enabled: true
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: bulk_update_issues_mutation
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108505
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388472
|
||||
milestone: '15.9'
|
||||
type: development
|
||||
group: group::project management
|
||||
default_enabled: false
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
name: optimize_issues_banned_users_query
|
||||
description: Optimize banned user clause for issue list queries
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/394980
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/189141
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/537923
|
||||
milestone: '18.0'
|
||||
group: group::project management
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
name: rapid_diffs_on_commit_show
|
||||
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/11559
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/187955
|
||||
rollout_issue_url:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/539577
|
||||
milestone: '18.0'
|
||||
group: group::code review
|
||||
type: wip
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name: rapid_diffs_on_compare_show
|
||||
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/11559
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/187955
|
||||
rollout_issue_url:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/539580
|
||||
milestone: '18.0'
|
||||
group: group::code review
|
||||
type: wip
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name: rapid_diffs_on_mr_creation
|
||||
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/11559
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/187955
|
||||
rollout_issue_url:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/539576
|
||||
milestone: '18.0'
|
||||
group: group::code review
|
||||
type: wip
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name: rapid_diffs_on_mr_show
|
||||
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/11559
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/187955
|
||||
rollout_issue_url:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/539581
|
||||
milestone: '18.0'
|
||||
group: group::code review
|
||||
type: wip
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ silenced = Rails.env.production? && !Gitlab::Utils.to_boolean(ENV['GITLAB_LOG_DE
|
|||
deprecators.silenced = silenced
|
||||
|
||||
ignored_warnings = [
|
||||
/Your `secret_key_base` is configured in `Rails.application.secrets`, which is deprecated in favor of/,
|
||||
/Please pass the (coder|class) as a keyword argument/,
|
||||
/Support for `config.active_support.cache_format_version/
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -8,14 +8,6 @@ description: Email addresses for non-GitLab users added to issues as participant
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42943
|
||||
milestone: '13.5'
|
||||
gitlab_schema: gitlab_main_cell
|
||||
desired_sharding_key:
|
||||
namespace_id:
|
||||
references: namespaces
|
||||
backfill_via:
|
||||
parent:
|
||||
foreign_key: issue_id
|
||||
table: issues
|
||||
sharding_key: namespace_id
|
||||
belongs_to: issue
|
||||
sharding_key:
|
||||
namespace_id: namespaces
|
||||
table_size: small
|
||||
desired_sharding_key_migration_job_name: BackfillIssueEmailParticipantsNamespaceId
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddExternalControlNameToComplianceRequirementsControls < Gitlab::Database::Migration[2.3]
|
||||
disable_ddl_transaction!
|
||||
milestone '18.0'
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
add_column :compliance_requirements_controls, :external_control_name, :text, null: true
|
||||
end
|
||||
|
||||
add_text_limit :compliance_requirements_controls, :external_control_name, 255
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_column :compliance_requirements_controls, :external_control_name, if_exists: true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIssueEmailParticipantsNamespaceIdNotNull < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.0'
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_not_null_constraint :issue_email_participants, :namespace_id
|
||||
end
|
||||
|
||||
def down
|
||||
remove_not_null_constraint :issue_email_participants, :namespace_id
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class RemoveRedundantUuidIndexFromVulnerabilityOccurrences < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.0'
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'index_vulnerability_occurrences_on_uuid_1'
|
||||
|
||||
def up
|
||||
remove_concurrent_index_by_name :vulnerability_occurrences, INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :vulnerability_occurrences, :uuid, unique: true, name: INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexOnExternalControlNameScopedToRequirement < Gitlab::Database::Migration[2.3]
|
||||
disable_ddl_transaction!
|
||||
milestone '18.0'
|
||||
|
||||
TABLE_NAME = :compliance_requirements_controls
|
||||
INDEX_NAME = :i_unique_external_control_name_per_requirement
|
||||
|
||||
def up
|
||||
add_concurrent_index TABLE_NAME,
|
||||
[:compliance_requirement_id, :external_control_name],
|
||||
unique: true,
|
||||
where: "external_control_name IS NOT NULL",
|
||||
name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
ec7459c400ccc8721e3a007ae48676df04803d8373cc807988d8d6da4623f541
|
||||
|
|
@ -0,0 +1 @@
|
|||
d64f957096ec66a174af25b6e3e985f3230ab73f5993d3df1665c2ab6fbd01e3
|
||||
|
|
@ -0,0 +1 @@
|
|||
13829ef575973386ba3ea01df90ee1c56d024ae10fb9f935a12b1df9fc19ce47
|
||||
|
|
@ -0,0 +1 @@
|
|||
62f68a3e9319c6f36ad7bab652003316637bc9296d3d739bafaf6ecece3cba34
|
||||
|
|
@ -12519,8 +12519,10 @@ CREATE TABLE compliance_requirements_controls (
|
|||
encrypted_secret_token bytea,
|
||||
encrypted_secret_token_iv bytea,
|
||||
external_url text,
|
||||
external_control_name text,
|
||||
CONSTRAINT check_110c87ed8d CHECK ((char_length(expression) <= 255)),
|
||||
CONSTRAINT check_5020dd6745 CHECK ((char_length(external_url) <= 1024))
|
||||
CONSTRAINT check_5020dd6745 CHECK ((char_length(external_url) <= 1024)),
|
||||
CONSTRAINT check_e3c26a3c02 CHECK ((char_length(external_control_name) <= 255))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE compliance_requirements_controls_id_seq
|
||||
|
|
@ -16065,7 +16067,8 @@ CREATE TABLE issue_email_participants (
|
|||
updated_at timestamp with time zone NOT NULL,
|
||||
email text NOT NULL,
|
||||
namespace_id bigint,
|
||||
CONSTRAINT check_2c321d408d CHECK ((char_length(email) <= 255))
|
||||
CONSTRAINT check_2c321d408d CHECK ((char_length(email) <= 255)),
|
||||
CONSTRAINT check_9d8a1ecc85 CHECK ((namespace_id IS NOT NULL))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE issue_email_participants_id_seq
|
||||
|
|
@ -32939,6 +32942,8 @@ CREATE UNIQUE INDEX i_sbom_occurrences_vulnerabilities_on_occ_id_and_vuln_id ON
|
|||
|
||||
CREATE INDEX i_software_license_policies_on_custom_software_license_id ON software_license_policies USING btree (custom_software_license_id);
|
||||
|
||||
CREATE UNIQUE INDEX i_unique_external_control_name_per_requirement ON compliance_requirements_controls USING btree (compliance_requirement_id, external_control_name) WHERE (external_control_name IS NOT NULL);
|
||||
|
||||
CREATE INDEX i_vuln_occurrences_on_proj_report_loc_dep_pkg_ver_file_img ON vulnerability_occurrences USING btree (project_id, report_type, ((((location -> 'dependency'::text) -> 'package'::text) ->> 'name'::text)), (((location -> 'dependency'::text) ->> 'version'::text)), COALESCE((location ->> 'file'::text), (location ->> 'image'::text))) WHERE (report_type = ANY (ARRAY[2, 1]));
|
||||
|
||||
CREATE INDEX idx_abuse_reports_user_id_status_and_category ON abuse_reports USING btree (user_id, status, category);
|
||||
|
|
@ -37817,8 +37822,6 @@ CREATE INDEX index_vulnerability_occurrences_on_location_k8s_cluster_id ON vulne
|
|||
|
||||
CREATE INDEX index_vulnerability_occurrences_on_scanner_id ON vulnerability_occurrences USING btree (scanner_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_vulnerability_occurrences_on_uuid_1 ON vulnerability_occurrences USING btree (uuid);
|
||||
|
||||
CREATE INDEX index_vulnerability_occurrences_on_vulnerability_id ON vulnerability_occurrences USING btree (vulnerability_id);
|
||||
|
||||
CREATE INDEX index_vulnerability_occurrences_prim_iden_id_and_vuln_id ON vulnerability_occurrences USING btree (primary_identifier_id, vulnerability_id);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ You can move all repositories managed by GitLab to another file system or anothe
|
|||
The GitLab API is the recommended way to move Git repositories:
|
||||
|
||||
- Between servers.
|
||||
- Between different storage.
|
||||
- Between different storages.
|
||||
- From single-node Gitaly to Gitaly Cluster.
|
||||
|
||||
For more information, see:
|
||||
|
|
|
|||
|
|
@ -7409,42 +7409,6 @@ Input type: `IssueUnlinkAlertInput`
|
|||
| <a id="mutationissueunlinkalerterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationissueunlinkalertissue"></a>`issue` | [`Issue`](#issue) | Issue after mutation. |
|
||||
|
||||
### `Mutation.issuesBulkUpdate`
|
||||
|
||||
Allows updating several properties for a set of issues. Does nothing if the `bulk_update_issues_mutation` feature flag is disabled.
|
||||
|
||||
{{< details >}}
|
||||
**Introduced** in GitLab 15.9.
|
||||
**Status**: Experiment.
|
||||
{{< /details >}}
|
||||
|
||||
Input type: `IssuesBulkUpdateInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationissuesbulkupdateaddlabelids"></a>`addLabelIds` | [`[LabelID!]`](#labelid) | Global ID array of the labels that will be added to the issues. |
|
||||
| <a id="mutationissuesbulkupdateassigneeids"></a>`assigneeIds` | [`[UserID!]`](#userid) | Global ID array of the users that will be assigned to the given issues. Existing assignees will be replaced with the ones on this list. |
|
||||
| <a id="mutationissuesbulkupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationissuesbulkupdateepicid"></a>`epicId` {{< icon name="warning-solid" >}} | [`EpicID`](#epicid) | **Deprecated:** This will be replaced by WorkItem hierarchyWidget. Deprecated in GitLab 17.5. |
|
||||
| <a id="mutationissuesbulkupdatehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Health status that will be assigned to the issues. |
|
||||
| <a id="mutationissuesbulkupdateids"></a>`ids` | [`[IssueID!]!`](#issueid) | Global ID array of the issues that will be updated. IDs that the user can't update will be ignored. A max of 100 can be provided. |
|
||||
| <a id="mutationissuesbulkupdateiterationid"></a>`iterationId` | [`IterationID`](#iterationid) | Global ID of the iteration that will be assigned to the issues. |
|
||||
| <a id="mutationissuesbulkupdatemilestoneid"></a>`milestoneId` | [`MilestoneID`](#milestoneid) | Global ID of the milestone that will be assigned to the issues. |
|
||||
| <a id="mutationissuesbulkupdateparentid"></a>`parentId` | [`IssueParentID!`](#issueparentid) | Global ID of the parent to which the bulk update will be scoped. The parent can be a project. The parent can also be a group (Premium and Ultimate only). Example `IssueParentID` are `"gid://gitlab/Project/1"` and `"gid://gitlab/Group/1"`. |
|
||||
| <a id="mutationissuesbulkupdateremovelabelids"></a>`removeLabelIds` | [`[LabelID!]`](#labelid) | Global ID array of the labels that will be removed from the issues. |
|
||||
| <a id="mutationissuesbulkupdatestateevent"></a>`stateEvent` | [`IssueStateEvent`](#issuestateevent) | Close or reopen an issue. |
|
||||
| <a id="mutationissuesbulkupdatesubscriptionevent"></a>`subscriptionEvent` | [`IssuableSubscriptionEvent`](#issuablesubscriptionevent) | Subscribe to or unsubscribe from issue notifications. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationissuesbulkupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationissuesbulkupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationissuesbulkupdateupdatedissuecount"></a>`updatedIssueCount` | [`Int`](#int) | Number of issues that were successfully updated. |
|
||||
|
||||
### `Mutation.iterationCadenceCreate`
|
||||
|
||||
Input type: `IterationCadenceCreateInput`
|
||||
|
|
@ -12584,7 +12548,7 @@ Input type: `WorkItemAddLinkedItemsInput`
|
|||
|
||||
### `Mutation.workItemBulkUpdate`
|
||||
|
||||
Allows updating several properties for a set of issues. Does nothing if the `bulk_update_issues_mutation` feature flag is disabled.
|
||||
Allows updating several properties for a set of work items.
|
||||
|
||||
{{< details >}}
|
||||
**Introduced** in GitLab 17.4.
|
||||
|
|
@ -43922,15 +43886,6 @@ State of a GitLab issue or merge request.
|
|||
| <a id="issuablestatelocked"></a>`locked` | Discussion has been locked. |
|
||||
| <a id="issuablestateopened"></a>`opened` | In open state. |
|
||||
|
||||
### `IssuableSubscriptionEvent`
|
||||
|
||||
Values for subscribing and unsubscribing from issuables.
|
||||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="issuablesubscriptioneventsubscribe"></a>`SUBSCRIBE` | Subscribe to an issuable. |
|
||||
| <a id="issuablesubscriptioneventunsubscribe"></a>`UNSUBSCRIBE` | Unsubscribe from an issuable. |
|
||||
|
||||
### `IssueCreationIterationWildcardId`
|
||||
|
||||
Iteration ID wildcard values for issue creation.
|
||||
|
|
@ -46791,12 +46746,6 @@ A `IssueID` is a global ID. It is encoded as a string.
|
|||
|
||||
An example `IssueID` is: `"gid://gitlab/Issue/1"`.
|
||||
|
||||
### `IssueParentID`
|
||||
|
||||
A `IssueParentID` is a global ID. It is encoded as a string.
|
||||
|
||||
An example `IssueParentID` is: `"gid://gitlab/IssueParent/1"`.
|
||||
|
||||
### `IterationID`
|
||||
|
||||
A `IterationID` is a global ID. It is encoded as a string.
|
||||
|
|
|
|||
|
|
@ -197,6 +197,17 @@ GitLab adds a system note to the page details.
|
|||
You must unlock all locked discussions in closed issues or merge requests before you can
|
||||
reopen the issue or merge request.
|
||||
|
||||
## Comments on confidential items
|
||||
|
||||
Only users with permission to access a confidential item receive notifications for comments on the item.
|
||||
If the item was not previously confidential, users without access may appear as participants. These users do not receive notifications while the item is confidential.
|
||||
|
||||
Who can be notified:
|
||||
|
||||
- Users assigned to the item, regardless of role.
|
||||
- Users who authored the item, if they have at least the Guest role.
|
||||
- Users with at least a Planner role in the group or project the item belongs to.
|
||||
|
||||
## Add an internal note
|
||||
|
||||
{{< history >}}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
module API
|
||||
class Discussions < ::API::Base
|
||||
include PaginationParams
|
||||
|
||||
helpers ::API::Helpers::NotesHelpers
|
||||
helpers ::RendersNotes
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ module API
|
|||
class Events < ::API::Base
|
||||
include PaginationParams
|
||||
include APIGuard
|
||||
|
||||
helpers ::API::Helpers::EventsHelpers
|
||||
|
||||
allow_access_with_scope :read_user, if: ->(request) { request.get? || request.head? }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
module API
|
||||
class GroupLabels < ::API::Base
|
||||
include PaginationParams
|
||||
|
||||
helpers ::API::Helpers::LabelHelpers
|
||||
|
||||
before { authenticate! }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ module API
|
|||
class Issues < ::API::Base
|
||||
include PaginationParams
|
||||
include APIGuard
|
||||
|
||||
helpers Helpers::IssuesHelpers
|
||||
helpers SpammableActions::CaptchaCheck::RestApiActionsSupport
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
module API
|
||||
class Labels < ::API::Base
|
||||
include PaginationParams
|
||||
|
||||
helpers ::API::Helpers::LabelHelpers
|
||||
|
||||
before { authenticate! }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ module API
|
|||
class Notes < ::API::Base
|
||||
include PaginationParams
|
||||
include APIGuard
|
||||
|
||||
helpers ::API::Helpers::NotesHelpers
|
||||
|
||||
before { authenticate! }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ module API
|
|||
class ProjectEvents < ::API::Base
|
||||
include PaginationParams
|
||||
include APIGuard
|
||||
|
||||
helpers ::API::Helpers::EventsHelpers
|
||||
|
||||
feature_category :user_profile
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
module API
|
||||
class RemoteMirrors < ::API::Base
|
||||
include PaginationParams
|
||||
|
||||
helpers Helpers::RemoteMirrorsHelpers
|
||||
|
||||
feature_category :source_code_management
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
module API
|
||||
class ResourceLabelEvents < ::API::Base
|
||||
include PaginationParams
|
||||
|
||||
helpers ::API::Helpers::NotesHelpers
|
||||
|
||||
before { authenticate! }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
module API
|
||||
class ResourceMilestoneEvents < ::API::Base
|
||||
include PaginationParams
|
||||
|
||||
helpers ::API::Helpers::NotesHelpers
|
||||
|
||||
resource_milestone_events_tags = %w[resource_milestone_events]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
module API
|
||||
class ResourceStateEvents < ::API::Base
|
||||
include PaginationParams
|
||||
|
||||
helpers ::API::Helpers::NotesHelpers
|
||||
|
||||
before { authenticate! }
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
module API
|
||||
class RubygemPackages < ::API::Base
|
||||
include ::API::Helpers::Authentication
|
||||
|
||||
helpers ::API::Helpers::PackagesHelpers
|
||||
|
||||
feature_category :package_registry
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ module API
|
|||
module V1
|
||||
class NamespacePackages < ::API::Base
|
||||
include ::API::Helpers::Authentication
|
||||
|
||||
helpers ::API::Helpers::PackagesHelpers
|
||||
helpers ::API::Helpers::Packages::BasicAuthHelpers
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ module API
|
|||
module V1
|
||||
class ProjectPackages < ::API::Base
|
||||
include ::API::Helpers::Authentication
|
||||
|
||||
helpers ::API::Helpers::PackagesHelpers
|
||||
helpers ::API::Helpers::Packages::BasicAuthHelpers
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "prawn"
|
||||
require "prawn-svg"
|
||||
|
||||
module Gitlab
|
||||
module PDF
|
||||
autoload :Header, 'gitlab/pdf/header'
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "prawn"
|
||||
require "prawn-svg"
|
||||
|
||||
module Gitlab
|
||||
module PDF
|
||||
class Header
|
||||
include Prawn::View
|
||||
|
||||
def self.render(pdf, page: 0, height: 50)
|
||||
new(pdf, page, height).render
|
||||
end
|
||||
|
||||
def initialize(pdf, page, height)
|
||||
@pdf = pdf
|
||||
@page = page
|
||||
@height = height
|
||||
@halfway_point = pdf.bounds.width / 2
|
||||
end
|
||||
|
||||
def render
|
||||
y = @pdf.bounds.top
|
||||
@pdf.bounding_box([0, y], width: @pdf.bounds.right, height: @height) do
|
||||
# The logo and GitLab text
|
||||
@pdf.bounding_box([0, @pdf.bounds.top], width: @halfway_point, height: @height) do
|
||||
logo_path = Rails.root.join('app/assets/images/gitlab_logo.png')
|
||||
|
||||
@pdf.image(
|
||||
logo_path,
|
||||
width: 21,
|
||||
height: 21,
|
||||
position: :left,
|
||||
vposition: 6
|
||||
)
|
||||
|
||||
@pdf.text_box(
|
||||
"GitLab",
|
||||
at: [25, @pdf.bounds.top],
|
||||
width: 100,
|
||||
height: 30,
|
||||
valign: :center,
|
||||
size: 24,
|
||||
style: :bold
|
||||
)
|
||||
end
|
||||
|
||||
# Title (right side)
|
||||
@pdf.bounding_box([@halfway_point, @pdf.bounds.top], width: @halfway_point, height: @height) do
|
||||
@pdf.text_box(
|
||||
"Vulnerability Summary | #{Date.current.strftime('%B %-d, %Y')} | #{@page}",
|
||||
align: :right,
|
||||
valign: :center,
|
||||
size: 10
|
||||
)
|
||||
end
|
||||
|
||||
# Gradient bar using SVG
|
||||
gradient_svg = <<~SVG
|
||||
<svg width="#{@pdf.bounds.width}" height="10">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#d2afed;stop-opacity:1" />
|
||||
<stop offset="25%" style="stop-color:#fa8bca;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#ff76a4;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#fd6c30;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="100%" height="10" fill="url(#grad)"/>
|
||||
</svg>
|
||||
SVG
|
||||
|
||||
# Position the gradient bar just below the header content
|
||||
@pdf.svg gradient_svg, at: [0, @pdf.bounds.top - 45]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -22595,6 +22595,9 @@ msgstr ""
|
|||
msgid "Discuss a specific suggestion or question."
|
||||
msgstr ""
|
||||
|
||||
msgid "Discussion is %{lockedLinkStart}locked%{lockedLinkEnd}. Only members can comment."
|
||||
msgstr ""
|
||||
|
||||
msgid "Discussion locked."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -27101,6 +27104,12 @@ msgstr ""
|
|||
msgid "Geo|Choose specific groups or storage shards"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Ci Secure File"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Ci Secure Files"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Comma-separated, e.g. '1.1.1.1, 2.2.2.0/24'"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -27122,6 +27131,12 @@ msgstr ""
|
|||
msgid "Geo|Consult Geo troubleshooting information"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Container Repositories"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Container Repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Container repositories synchronization concurrency limit"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -27140,6 +27155,24 @@ msgstr ""
|
|||
msgid "Geo|Database Replication status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Dependency Proxy Blob"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Dependency Proxy Blobs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Dependency Proxy Manifest"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Dependency Proxy Manifests"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Design Management Repositories"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Design Management Repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Disabled"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -27206,6 +27239,12 @@ msgstr ""
|
|||
msgid "Geo|GraphQL ID: %{id}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Group Wiki Repositories"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Group Wiki Repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Groups to synchronize"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -27230,6 +27269,12 @@ msgstr ""
|
|||
msgid "Geo|Internal URL (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Job Artifact"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Job Artifacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Last event ID"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -27254,12 +27299,24 @@ msgstr ""
|
|||
msgid "Geo|Learn more about Geo site statuses"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Lfs Object"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Lfs Objects"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Limit the number of concurrent operations this secondary site can run in the background."
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Merge Request Diff"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Merge Request Diffs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Minimum interval in days"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -27305,15 +27362,45 @@ msgstr ""
|
|||
msgid "Geo|Offline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Package File"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Package Files"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Pages Deployment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Pages Deployments"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Pending"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Pipeline Artifact"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Pipeline Artifacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Primary"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Primary site"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Project Repositories"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Project Repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Project Wiki Repositories"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Project Wiki Repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Projects in certain groups"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -27440,6 +27527,12 @@ msgstr ""
|
|||
msgid "Geo|Site's status was updated %{timeAgo}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Snippet Repositories"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Snippet Repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Started"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -27467,6 +27560,12 @@ msgstr ""
|
|||
msgid "Geo|Synchronization status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Terraform State Version"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Terraform State Versions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|The URL of the primary site that is used internally by the secondary sites."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -27539,6 +27638,12 @@ msgstr ""
|
|||
msgid "Geo|Updated %{timeAgo}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Upload"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Uploads"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Verification"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -36489,6 +36594,12 @@ msgstr ""
|
|||
msgid "Marked For Deletion At - %{deletion_time}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Marked as %{confidentialLinkStart}confidential%{confidentialLinkEnd} and discussion is %{lockedLinkStart}locked%{lockedLinkEnd}. People without permission will never get a notification and won't be able to comment."
|
||||
msgstr ""
|
||||
|
||||
msgid "Marked as %{confidentialLinkStart}confidential%{confidentialLinkEnd}. People without permission will never get a notification."
|
||||
msgstr ""
|
||||
|
||||
msgid "Marked as draft. Can only be merged when marked as ready."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -40241,9 +40352,6 @@ msgstr ""
|
|||
msgid "No more than %{max_frameworks} compliance frameworks can be updated at the same time."
|
||||
msgstr ""
|
||||
|
||||
msgid "No more than %{max_issues} issues can be updated at the same time"
|
||||
msgstr ""
|
||||
|
||||
msgid "No more than %{max_work_items} work items can be modified at the same time."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -43767,12 +43875,6 @@ msgstr ""
|
|||
msgid "PendingMembers|There are no pending members left to approve. High five!"
|
||||
msgstr ""
|
||||
|
||||
msgid "People without permission will never get a notification and won't be able to comment."
|
||||
msgstr ""
|
||||
|
||||
msgid "People without permission will never get a notification."
|
||||
msgstr ""
|
||||
|
||||
msgid "Percent rollout must be an integer number between 0 and 100"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -61457,9 +61559,6 @@ msgstr ""
|
|||
msgid "This %{issuable} would exceed the maximum number of linked %{issuables} (%{limit})."
|
||||
msgstr ""
|
||||
|
||||
msgid "This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and its %{lockedLinkStart}discussion is locked%{lockedLinkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "This Cron pattern is invalid"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -61745,9 +61844,6 @@ msgstr ""
|
|||
msgid "This is a child pipeline within the parent pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "This is a confidential %{noteableTypeText}."
|
||||
msgstr ""
|
||||
|
||||
msgid "This is a delayed job to run in %{remainingTime}"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :project_deletion_schedule, class: 'Projects::DeletionSchedule' do
|
||||
association :project, factory: :project
|
||||
association :deleting_user, factory: :user
|
||||
marked_for_deletion_at { Time.current }
|
||||
end
|
||||
end
|
||||
|
|
@ -7,8 +7,6 @@ import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'
|
|||
describe('Comment Field Layout Component', () => {
|
||||
let wrapper;
|
||||
|
||||
const lockedDiscussionDocsPath = 'docs/locked/path';
|
||||
const confidentialIssuesDocsPath = 'docs/confidential/path';
|
||||
const noteWithAttachment =
|
||||
'Have a look at this! ';
|
||||
const attachmentMessage =
|
||||
|
|
@ -19,8 +17,6 @@ describe('Comment Field Layout Component', () => {
|
|||
const noteableDataMock = {
|
||||
confidential: false,
|
||||
discussion_locked: false,
|
||||
locked_discussion_docs_path: lockedDiscussionDocsPath,
|
||||
confidential_issues_docs_path: confidentialIssuesDocsPath,
|
||||
};
|
||||
|
||||
const findIssuableNoteWarning = () => wrapper.findComponent(NoteableWarning);
|
||||
|
|
@ -75,8 +71,6 @@ describe('Comment Field Layout Component', () => {
|
|||
expect(findIssuableNoteWarning().props()).toMatchObject({
|
||||
isLocked: false,
|
||||
isConfidential: true,
|
||||
lockedNoteableDocsPath: lockedDiscussionDocsPath,
|
||||
confidentialNoteableDocsPath: confidentialIssuesDocsPath,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -96,8 +90,6 @@ describe('Comment Field Layout Component', () => {
|
|||
expect(findIssuableNoteWarning().props()).toMatchObject({
|
||||
isConfidential: false,
|
||||
isLocked: true,
|
||||
lockedNoteableDocsPath: lockedDiscussionDocsPath,
|
||||
confidentialNoteableDocsPath: confidentialIssuesDocsPath,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,10 +2,16 @@ import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
|||
|
||||
import { dispatchSnowplowEvent } from '~/tracking/dispatch_snowplow_event';
|
||||
import getStandardContext from '~/tracking/get_standard_context';
|
||||
import { isEventEligible } from '~/tracking/utils';
|
||||
import { extraContext, servicePingContext } from './mock_data';
|
||||
|
||||
jest.mock('~/sentry/sentry_browser_wrapper');
|
||||
jest.mock('~/tracking/get_standard_context');
|
||||
jest.mock('~/tracking/utils', () => ({
|
||||
...jest.requireActual('~/tracking/utils'),
|
||||
isEventEligible: jest.fn(),
|
||||
validateEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
const category = 'Incident Management';
|
||||
const action = 'view_incident_details';
|
||||
|
|
@ -20,6 +26,7 @@ describe('dispatchSnowplowEvent', () => {
|
|||
beforeEach(() => {
|
||||
snowplowMock.mockClear();
|
||||
Sentry.captureException.mockClear();
|
||||
isEventEligible.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('calls snowplow trackStructEvent with correct arguments', () => {
|
||||
|
|
@ -73,4 +80,24 @@ describe('dispatchSnowplowEvent', () => {
|
|||
|
||||
expect(Sentry.captureException).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('returns false when event is not eligible', () => {
|
||||
isEventEligible.mockReturnValue(false);
|
||||
|
||||
const result = dispatchSnowplowEvent(category, action, {});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(snowplowMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns true and tracks event when event is eligible', () => {
|
||||
isEventEligible.mockReturnValue(true);
|
||||
|
||||
snowplowMock.mockImplementation(() => {});
|
||||
|
||||
const result = dispatchSnowplowEvent(category, action, {});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(snowplowMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ jest.mock('~/api', () => ({
|
|||
jest.mock('~/tracking/utils', () => ({
|
||||
...jest.requireActual('~/tracking/utils'),
|
||||
getInternalEventHandlers: jest.fn(),
|
||||
isEventEligible: jest.fn(),
|
||||
}));
|
||||
|
||||
Tracker.enabled = jest.fn();
|
||||
|
|
@ -43,6 +44,23 @@ describe('InternalEvents', () => {
|
|||
describe('trackEvent', () => {
|
||||
const category = 'TestCategory';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(utils, 'isEventEligible').mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('does not track anything if event is not eligible', () => {
|
||||
jest.spyOn(utils, 'isEventEligible').mockReturnValue(false);
|
||||
jest.spyOn(InternalEvents, 'trackBrowserSDK').mockImplementation(() => {});
|
||||
jest.spyOn(Tracking, 'event').mockImplementation(() => {});
|
||||
|
||||
InternalEvents.trackEvent(event, allowedAdditionalProps, category);
|
||||
|
||||
expect(utils.isEventEligible).toHaveBeenCalledWith(event);
|
||||
expect(Tracking.event).not.toHaveBeenCalled();
|
||||
expect(API.trackInternalEvent).not.toHaveBeenCalled();
|
||||
expect(InternalEvents.trackBrowserSDK).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('trackEvent calls API.trackInternalEvent with correct arguments', () => {
|
||||
InternalEvents.trackEvent(event, {}, category);
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ describe('Tracking', () => {
|
|||
|
||||
describe('initDefaultTrackers', () => {
|
||||
beforeEach(() => {
|
||||
window.gl.onlySendDuoEvents = false;
|
||||
bindDocumentSpy = jest.spyOn(Tracking, 'bindDocument').mockImplementation(() => null);
|
||||
trackLoadEventsSpy = jest.spyOn(Tracking, 'trackLoadEvents').mockImplementation(() => null);
|
||||
enableFormTracking = jest
|
||||
|
|
@ -234,6 +235,24 @@ describe('Tracking', () => {
|
|||
snowplowSpy.mock.invocationCallOrder[0],
|
||||
);
|
||||
});
|
||||
|
||||
it('does not initialize snowplow default trackers if `onlySendDuoEvents` is true', () => {
|
||||
window.gl.onlySendDuoEvents = true;
|
||||
|
||||
initDefaultTrackers();
|
||||
|
||||
expect(snowplowSpy).not.toHaveBeenCalledWith('enableActivityTracking', expect.anything());
|
||||
expect(snowplowSpy).not.toHaveBeenCalledWith('trackPageView', expect.anything());
|
||||
expect(snowplowSpy).not.toHaveBeenCalledWith('setDocumentTitle', expect.anything());
|
||||
expect(enableFormTracking).not.toHaveBeenCalled();
|
||||
expect(snowplowSpy).not.toHaveBeenCalledWith('enableLinkClickTracking', expect.anything());
|
||||
|
||||
expect(bindDocumentSpy).toHaveBeenCalled();
|
||||
expect(trackLoadEventsSpy).toHaveBeenCalled();
|
||||
expect(bindInternalEventDocumentSpy).toHaveBeenCalled();
|
||||
expect(trackInternalLoadEventsSpy).toHaveBeenCalled();
|
||||
expect(initBrowserSDKSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are experiment contexts', () => {
|
||||
|
|
@ -249,6 +268,7 @@ describe('Tracking', () => {
|
|||
];
|
||||
|
||||
beforeEach(() => {
|
||||
window.gl.onlySendDuoEvents = false;
|
||||
getAllExperimentContexts.mockReturnValue(experimentContexts);
|
||||
window.snowplowOptions = {
|
||||
...window.snowplowOptions,
|
||||
|
|
|
|||
|
|
@ -1,48 +1,49 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Issue Warning Component when issue is locked but not confidential renders information about locked issue 1`] = `
|
||||
exports[`Noteable Warning Component when item is locked but not confidential renders information about locked item 1`] = `
|
||||
<span>
|
||||
The discussion in this issue is locked. Only project members can comment.
|
||||
Discussion is
|
||||
<gl-link-stub
|
||||
href="locked-path"
|
||||
href="/help/user/discussions/_index#prevent-comments-by-locking-the-discussion"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more.
|
||||
locked
|
||||
</gl-link-stub>
|
||||
. Only members can comment.
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`Issue Warning Component when noteable is confidential but not locked renders information about confidential issue 1`] = `
|
||||
exports[`Noteable Warning Component when noteable is confidential but not locked renders information about confidential item 1`] = `
|
||||
<span>
|
||||
This is a confidential issue. People without permission will never get a notification.
|
||||
Marked as
|
||||
<gl-link-stub
|
||||
href="confidential-path"
|
||||
href="/help/user/discussions/_index#comments-on-confidential-items"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more.
|
||||
confidential
|
||||
</gl-link-stub>
|
||||
. People without permission will never get a notification.
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`Issue Warning Component when noteable is locked and confidential renders information about locked and confidential noteable 1`] = `
|
||||
exports[`Noteable Warning Component when noteable is locked and confidential renders information about locked and confidential noteable 1`] = `
|
||||
<span>
|
||||
<span>
|
||||
This issue is
|
||||
Marked as
|
||||
<gl-link-stub
|
||||
href=""
|
||||
href="/help/user/discussions/_index#comments-on-confidential-items"
|
||||
target="_blank"
|
||||
>
|
||||
confidential
|
||||
</gl-link-stub>
|
||||
and its
|
||||
and discussion is
|
||||
<gl-link-stub
|
||||
href=""
|
||||
href="/help/user/discussions/_index#prevent-comments-by-locking-the-discussion"
|
||||
target="_blank"
|
||||
>
|
||||
discussion is locked
|
||||
locked
|
||||
</gl-link-stub>
|
||||
.
|
||||
. People without permission will never get a notification and won't be able to comment.
|
||||
</span>
|
||||
People without permission will never get a notification and won't be able to comment.
|
||||
</span>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { shallowMount, mount } from '@vue/test-utils';
|
|||
import { nextTick } from 'vue';
|
||||
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
|
||||
|
||||
describe('Issue Warning Component', () => {
|
||||
describe('Noteable Warning Component', () => {
|
||||
let wrapper;
|
||||
|
||||
const findIcon = (w = wrapper) => w.findComponent(GlIcon);
|
||||
|
|
@ -22,16 +22,15 @@ describe('Issue Warning Component', () => {
|
|||
},
|
||||
});
|
||||
|
||||
describe('when issue is locked but not confidential', () => {
|
||||
describe('when item is locked but not confidential', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({
|
||||
isLocked: true,
|
||||
lockedNoteableDocsPath: 'locked-path',
|
||||
isConfidential: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders information about locked issue', () => {
|
||||
it('renders information about locked item', () => {
|
||||
expect(findLockedBlock().exists()).toBe(true);
|
||||
expect(findLockedBlock().element).toMatchSnapshot();
|
||||
});
|
||||
|
|
@ -40,11 +39,11 @@ describe('Issue Warning Component', () => {
|
|||
expect(findIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render information about locked and confidential issue', () => {
|
||||
it('does not render information about locked and confidential item', () => {
|
||||
expect(findLockedAndConfidentialBlock().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render information about confidential issue', () => {
|
||||
it('does not render information about confidential item', () => {
|
||||
expect(findConfidentialBlock().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -54,16 +53,15 @@ describe('Issue Warning Component', () => {
|
|||
wrapper = createComponent({
|
||||
isLocked: false,
|
||||
isConfidential: true,
|
||||
confidentialNoteableDocsPath: 'confidential-path',
|
||||
});
|
||||
});
|
||||
|
||||
it('renders information about confidential issue', async () => {
|
||||
it('renders information about confidential item', async () => {
|
||||
expect(findConfidentialBlock().exists()).toBe(true);
|
||||
expect(findConfidentialBlock().element).toMatchSnapshot();
|
||||
|
||||
await nextTick();
|
||||
expect(findConfidentialBlock(wrapper).text()).toContain('This is a confidential issue.');
|
||||
expect(findConfidentialBlock(wrapper).text()).toContain('Marked as confidential.');
|
||||
});
|
||||
|
||||
it('renders warning icon', () => {
|
||||
|
|
@ -134,70 +132,13 @@ describe('Issue Warning Component', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('renders confidential & locked messages with noteable "issue"', () => {
|
||||
expect(findLockedBlock(wrapperLocked).text()).toContain(
|
||||
'The discussion in this issue is locked.',
|
||||
);
|
||||
it('renders confidential & locked messages with noteable', () => {
|
||||
expect(findLockedBlock(wrapperLocked).text()).toContain('Discussion is locked.');
|
||||
expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
|
||||
'This is a confidential issue.',
|
||||
'Marked as confidential.',
|
||||
);
|
||||
expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
|
||||
'This issue is confidential and its discussion is locked.',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders confidential & locked messages with noteable "epic"', async () => {
|
||||
wrapperLocked.setProps({
|
||||
noteableType: 'Epic',
|
||||
});
|
||||
wrapperConfidential.setProps({
|
||||
noteableType: 'Epic',
|
||||
});
|
||||
wrapperLockedAndConfidential.setProps({
|
||||
noteableType: 'Epic',
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findLockedBlock(wrapperLocked).text()).toContain(
|
||||
'The discussion in this epic is locked.',
|
||||
);
|
||||
|
||||
await nextTick();
|
||||
expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
|
||||
'This is a confidential epic.',
|
||||
);
|
||||
|
||||
await nextTick();
|
||||
expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
|
||||
'This epic is confidential and its discussion is locked.',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders confidential & locked messages with noteable "merge request"', async () => {
|
||||
wrapperLocked.setProps({
|
||||
noteableType: 'MergeRequest',
|
||||
});
|
||||
wrapperConfidential.setProps({
|
||||
noteableType: 'MergeRequest',
|
||||
});
|
||||
wrapperLockedAndConfidential.setProps({
|
||||
noteableType: 'MergeRequest',
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
expect(findLockedBlock(wrapperLocked).text()).toContain(
|
||||
'The discussion in this merge request is locked.',
|
||||
);
|
||||
|
||||
await nextTick();
|
||||
expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
|
||||
'This is a confidential merge request.',
|
||||
);
|
||||
|
||||
await nextTick();
|
||||
expect(findLockedAndConfidentialBlock(wrapperLockedAndConfidential).text()).toContain(
|
||||
'This merge request is confidential and its discussion is locked.',
|
||||
'Marked as confidential and discussion is locked.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -166,12 +166,9 @@ describe('Work item comment form component', () => {
|
|||
withAlertContainer: false,
|
||||
noteableData: {
|
||||
confidential: false,
|
||||
confidential_issues_docs_path: '/help/user/tasks.html#confidential-tasks',
|
||||
discussion_locked: false,
|
||||
locked_discussion_docs_path: '/help/user/tasks.html#lock-discussion',
|
||||
issue_email_participants: [],
|
||||
},
|
||||
noteableType: 'Task',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::PDF::Header, feature_category: :vulnerability_management do
|
||||
let(:pdf) { Prawn::Document.new }
|
||||
|
||||
let(:page_number) { '12345' }
|
||||
let(:logo) { Rails.root.join('app/assets/images/gitlab_logo.png') }
|
||||
|
||||
describe '.render' do
|
||||
subject(:render) { described_class.render(pdf, page: page_number, height: 123) }
|
||||
|
||||
let(:mock_instance) { instance_double(described_class) }
|
||||
|
||||
before do
|
||||
allow(mock_instance).to receive(:render)
|
||||
allow(described_class).to receive(:new).and_return(mock_instance)
|
||||
end
|
||||
|
||||
it 'creates a new instance and calls render on it' do
|
||||
render
|
||||
|
||||
expect(described_class).to have_received(:new).with(pdf, page_number, 123).once
|
||||
expect(mock_instance).to have_received(:render).exactly(:once)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#render' do
|
||||
subject(:render_header) { described_class.render(pdf, page: page_number) }
|
||||
|
||||
before do
|
||||
allow(pdf).to receive(:image).and_call_original
|
||||
allow(pdf).to receive(:text_box).and_call_original
|
||||
allow(pdf).to receive(:svg).and_call_original
|
||||
end
|
||||
|
||||
it 'includes the gitlab logo in the header' do
|
||||
render_header
|
||||
|
||||
expect(pdf).to have_received(:image).with(logo, any_args).once
|
||||
end
|
||||
|
||||
it 'includes the gitlab name in the header' do
|
||||
render_header
|
||||
|
||||
expect(pdf).to have_received(:text_box).with('GitLab', any_args).once
|
||||
end
|
||||
|
||||
it 'includes the page number in the header' do
|
||||
render_header
|
||||
|
||||
expect(pdf).to have_received(:text_box).with(/^.*#{page_number}/, any_args).once
|
||||
end
|
||||
|
||||
it 'includes the svg divider in the header' do
|
||||
render_header
|
||||
|
||||
expect(pdf).to have_received(:svg).with(%r{<svg.*</svg>}m, any_args).once
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::DeletionSchedule, type: :model, feature_category: :groups_and_projects do
|
||||
subject(:project_deletion_schedule) { build(:project_deletion_schedule) }
|
||||
|
||||
describe 'validations' do
|
||||
it { is_expected.to validate_presence_of(:marked_for_deletion_at) }
|
||||
end
|
||||
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:project) }
|
||||
|
||||
specify do
|
||||
expect(project_deletion_schedule).to belong_to(:deleting_user).class_name('User').with_foreign_key('user_id')
|
||||
.inverse_of(:project_deletion_schedules)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -231,6 +231,7 @@ RSpec.describe User, feature_category: :user_profile do
|
|||
it { is_expected.to have_many(:merge_request_assignment_events).class_name('ResourceEvents::MergeRequestAssignmentEvent') }
|
||||
it { is_expected.to have_many(:admin_abuse_report_assignees).class_name('Admin::AbuseReportAssignee') }
|
||||
it { is_expected.to have_many(:early_access_program_tracking_events).class_name('EarlyAccessProgram::TrackingEvent') }
|
||||
it { is_expected.to have_many(:project_deletion_schedules).class_name('::Projects::DeletionSchedule').inverse_of(:deleting_user) }
|
||||
|
||||
describe '#triggers' do
|
||||
let(:user) { create(:user) }
|
||||
|
|
|
|||
|
|
@ -1,179 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Bulk update issues', feature_category: :team_planning do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:developer) { create(:user) }
|
||||
let_it_be(:group) { create(:group, developers: developer) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:label1) { create(:group_label, group: group) }
|
||||
let_it_be(:label2) { create(:group_label, group: group) }
|
||||
let_it_be(:updatable_issues, reload: true) { create_list(:issue, 2, project: project, label_ids: [label1.id]) }
|
||||
let_it_be(:milestone) { create(:milestone, group: group) }
|
||||
|
||||
let(:parent) { project }
|
||||
let(:max_issues) { Mutations::Issues::BulkUpdate::MAX_ISSUES }
|
||||
let(:mutation) { graphql_mutation(:issues_bulk_update, base_arguments.merge(additional_arguments)) }
|
||||
let(:mutation_response) { graphql_mutation_response(:issues_bulk_update) }
|
||||
let(:current_user) { developer }
|
||||
let(:base_arguments) { { parent_id: parent.to_gid.to_s, ids: updatable_issues.map { |i| i.to_gid.to_s } } }
|
||||
|
||||
let(:additional_arguments) do
|
||||
{
|
||||
assignee_ids: [current_user.to_gid.to_s],
|
||||
milestone_id: milestone.to_gid.to_s,
|
||||
state_event: :CLOSE,
|
||||
add_label_ids: [label2.to_gid.to_s],
|
||||
remove_label_ids: [label1.to_gid.to_s],
|
||||
subscription_event: :UNSUBSCRIBE
|
||||
}
|
||||
end
|
||||
|
||||
before_all do
|
||||
updatable_issues.each { |i| i.subscribe(developer, project) }
|
||||
end
|
||||
|
||||
context 'when Gitlab is FOSS only' do
|
||||
unless Gitlab.ee?
|
||||
context 'when parent is a group' do
|
||||
let(:parent) { group }
|
||||
|
||||
it 'does not allow bulk updating issues at the group level' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(graphql_errors).to contain_exactly(
|
||||
hash_including(
|
||||
'message' => match(/does not represent an instance of IssueParent/)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the `bulk_update_issues_mutation` feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(bulk_update_issues_mutation: false)
|
||||
end
|
||||
|
||||
it 'returns a resource not available error' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(graphql_errors).to contain_exactly(
|
||||
hash_including(
|
||||
'message' => '`bulk_update_issues_mutation` feature flag is disabled.'
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user can not update all issues' do
|
||||
let_it_be(:forbidden_issue) { create(:issue) }
|
||||
|
||||
it 'updates only issues that the user can update' do
|
||||
expect do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
updatable_issues.each(&:reset)
|
||||
forbidden_issue.reset
|
||||
end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2).and(
|
||||
not_change(forbidden_issue, :assignee_ids).from([])
|
||||
)
|
||||
|
||||
expect(mutation_response).to include(
|
||||
'updatedIssueCount' => updatable_issues.count
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user can update all issues' do
|
||||
it 'updates all issues' do
|
||||
expect do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
updatable_issues.each(&:reload)
|
||||
end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2)
|
||||
.and(change { updatable_issues.map(&:milestone_id) }.from([nil] * 2).to([milestone.id] * 2))
|
||||
.and(change { updatable_issues.map(&:state) }.from(['opened'] * 2).to(['closed'] * 2))
|
||||
.and(change { updatable_issues.flat_map(&:label_ids) }.from([label1.id] * 2).to([label2.id] * 2))
|
||||
.and(
|
||||
change { updatable_issues.map { |i| i.subscribed?(developer, project) } }.from([true] * 2).to([false] * 2)
|
||||
)
|
||||
|
||||
expect(mutation_response).to include(
|
||||
'updatedIssueCount' => updatable_issues.count
|
||||
)
|
||||
end
|
||||
|
||||
context 'when current user cannot read the specified project' do
|
||||
let_it_be(:parent) { create(:project, :private) }
|
||||
|
||||
it 'returns a resource not found error' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(graphql_errors).to contain_exactly(
|
||||
hash_including(
|
||||
'message' => "The resource that you are attempting to access does not exist or you don't have " \
|
||||
'permission to perform this action'
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when setting arguments to null or none' do
|
||||
let(:additional_arguments) { { assignee_ids: [], milestone_id: nil } }
|
||||
|
||||
before do
|
||||
updatable_issues.each do |issue|
|
||||
issue.update!(assignees: [current_user], milestone: milestone)
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates all issues' do
|
||||
expect do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
updatable_issues.each(&:reload)
|
||||
end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([current_user.id] * 2).to([])
|
||||
.and(change { updatable_issues.map(&:milestone_id) }.from([milestone.id] * 2).to([nil] * 2))
|
||||
|
||||
expect(mutation_response).to include(
|
||||
'updatedIssueCount' => updatable_issues.count
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when update service returns an error' do
|
||||
before do
|
||||
allow_next_instance_of(Issuable::BulkUpdateService) do |update_service|
|
||||
allow(update_service).to receive(:execute).and_return(
|
||||
ServiceResponse.error(message: 'update error', http_status: 422) # rubocop:disable Gitlab/ServiceResponse
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns an error message' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(graphql_data.dig('issuesBulkUpdate', 'errors')).to contain_exactly('update error')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when trying to update more than the max allowed' do
|
||||
before do
|
||||
stub_const('Mutations::Issues::BulkUpdate::MAX_ISSUES', updatable_issues.count - 1)
|
||||
end
|
||||
|
||||
it "restricts updating more than #{Mutations::Issues::BulkUpdate::MAX_ISSUES} issues at the same time" do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(graphql_errors).to contain_exactly(
|
||||
hash_including(
|
||||
'message' =>
|
||||
format(_('No more than %{max_issues} issues can be updated at the same time'), max_issues: max_issues)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -868,6 +868,61 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
|
|||
end
|
||||
end
|
||||
|
||||
describe '#delete_commit_statuses' do
|
||||
let(:service) { described_class.new(project, user, {}) }
|
||||
let(:batch_size) { described_class::BATCH_SIZE }
|
||||
|
||||
context 'when there are no commit statuses' do
|
||||
it 'does not delete anything and logs zero count' do
|
||||
expect(Gitlab::AppLogger).to receive(:info).with(
|
||||
class: described_class.name,
|
||||
project_id: project.id,
|
||||
message: 'leftover commit statuses',
|
||||
orphaned_commit_status_count: 0
|
||||
)
|
||||
|
||||
expect { service.send(:delete_commit_statuses) }.not_to change(::CommitStatus, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are fewer commit statuses than the batch size' do
|
||||
before do
|
||||
create_list(:commit_status, 2, project: project)
|
||||
end
|
||||
|
||||
it 'deletes all statuses in a single batch and logs the count' do
|
||||
expect(Gitlab::AppLogger).to receive(:info).with(
|
||||
class: described_class.name,
|
||||
project_id: project.id,
|
||||
message: 'leftover commit statuses',
|
||||
orphaned_commit_status_count: 2
|
||||
)
|
||||
|
||||
expect { service.send(:delete_commit_statuses) }
|
||||
.to change { CommitStatus.for_project(project).count }.from(2).to(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are more commit statuses than the batch size' do
|
||||
before do
|
||||
stub_const("#{described_class}::BATCH_SIZE", 2)
|
||||
create_list(:commit_status, 3, project: project)
|
||||
end
|
||||
|
||||
it 'deletes statuses in multiple batches and logs the total count' do
|
||||
expect(Gitlab::AppLogger).to receive(:info).with(
|
||||
class: described_class.name,
|
||||
project_id: project.id,
|
||||
message: 'leftover commit statuses',
|
||||
orphaned_commit_status_count: 3
|
||||
)
|
||||
|
||||
expect { service.send(:delete_commit_statuses) }
|
||||
.to change { CommitStatus.for_project(project).count }.from(3).to(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy_project(project, user, params = {})
|
||||
described_class.new(project, user, params).public_send(async ? :async_execute : :execute)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -439,18 +439,18 @@ RSpec.shared_examples 'work items notifications' do
|
|||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'work items lock discussion' do |type|
|
||||
RSpec.shared_examples 'work items lock discussion' do
|
||||
it 'locks and unlocks discussion', :aggregate_failures do
|
||||
click_button _('More actions'), match: :first
|
||||
click_button 'Lock discussion'
|
||||
click_button _('More actions'), match: :first # click again to close the dropdown
|
||||
|
||||
expect(page).to have_text "The discussion in this #{type} is locked. Only project members can comment."
|
||||
expect(page).to have_text "Discussion is locked. Only members can comment."
|
||||
|
||||
click_button _('More actions'), match: :first
|
||||
click_button 'Unlock discussion'
|
||||
|
||||
expect(page).not_to have_text "The discussion in this #{type} is locked. Only project members can comment."
|
||||
expect(page).not_to have_text "Discussion is locked. Only members can comment."
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue