summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-13 15:08:09 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-13 15:08:09 +0000
commitaadb3204eaf8b5912e262cd19fed34fc70789e95 (patch)
treeab19fa23b4e03cfd370b96627be10e0f2aefa648
parent67cddd762d819d320ea905ed068f816b354d14bc (diff)
downloadgitlab-ce-aadb3204eaf8b5912e262cd19fed34fc70789e95.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/rake/require.yml1
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue6
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue52
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue26
-rw-r--r--app/assets/javascripts/issues/constants.js2
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue6
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue167
-rw-r--r--app/assets/javascripts/issues/show/components/task_list_item_actions.vue11
-rw-r--r--app/assets/javascripts/issues/show/index.js9
-rw-r--r--app/assets/javascripts/issues/show/utils.js74
-rw-r--r--app/models/integrations/base_chat_notification.rb21
-rw-r--r--app/policies/group_policy.rb9
-rw-r--r--app/views/admin/application_settings/_email.html.haml7
-rw-r--r--db/migrate/20230202153926_add_scan_result_policy_id_to_approval_rules.rb10
-rw-r--r--db/migrate/20230206172702_add_match_on_inclusion_to_scan_result_policy.rb9
-rw-r--r--db/post_migrate/20230203122602_schedule_vulnerabilities_feedback_migration3.rb27
-rw-r--r--db/post_migrate/20230209103650_add_unique_software_license_policies_index_on_project_and_scan_result_policy.rb18
-rw-r--r--db/post_migrate/20230209103714_add_fk_to_approval_rules_on_scan_result_policy_id.rb23
-rw-r--r--db/post_migrate/20230209123006_remove_unique_software_license_policies_index_on_project.rb15
-rw-r--r--db/post_migrate/20230209171547_schedule_vulnerabilities_feedback_migration4.rb43
-rw-r--r--db/post_migrate/20230210113924_prepare_index_approval_rules_on_scan_result_policy_id.rb17
-rw-r--r--db/schema_migrations/202302021539261
-rw-r--r--db/schema_migrations/202302061727021
-rw-r--r--db/schema_migrations/202302091036501
-rw-r--r--db/schema_migrations/202302091037141
-rw-r--r--db/schema_migrations/202302091230061
-rw-r--r--db/schema_migrations/202302091715471
-rw-r--r--db/schema_migrations/202302101139241
-rw-r--r--db/structure.sql17
-rw-r--r--doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md25
-rw-r--r--doc/architecture/blueprints/runner_tokens/index.md35
-rw-r--r--doc/user/project/integrations/mattermost.md2
-rw-r--r--doc/user/project/integrations/slack.md2
-rw-r--r--doc/user/project/issues/managing_issues.md16
-rw-r--r--doc/user/tasks.md16
-rw-r--r--lib/gitlab/ci/status/bridge/common.rb2
-rw-r--r--locale/gitlab.pot21
-rw-r--r--package.json4
-rw-r--r--spec/db/schema_spec.rb5
-rw-r--r--spec/features/admin/admin_settings_spec.rb27
-rw-r--r--spec/features/groups_spec.rb27
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb2
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js2
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js41
-rw-r--r--spec/frontend/issues/show/components/description_spec.js115
-rw-r--r--spec/frontend/issues/show/components/task_list_item_actions_spec.js15
-rw-r--r--spec/frontend/issues/show/utils_spec.js94
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js40
-rw-r--r--spec/frontend/work_items/mock_data.js32
-rw-r--r--spec/lib/gitlab/bare_repository_import/importer_spec.rb5
-rw-r--r--spec/lib/gitlab/bitbucket_import/project_creator_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/bridge/common_spec.rb33
-rw-r--r--spec/lib/gitlab/ci/status/bridge/factory_spec.rb4
-rw-r--r--spec/lib/gitlab/gitlab_import/project_creator_spec.rb4
-rw-r--r--spec/lib/gitlab/legacy_github_import/project_creator_spec.rb2
-rw-r--r--spec/migrations/20230203122602_schedule_vulnerabilities_feedback_migration4_spec.rb (renamed from spec/migrations/20230203122602_schedule_vulnerabilities_feedback_migration3_spec.rb)6
-rw-r--r--spec/models/integrations/base_chat_notification_spec.rb30
-rw-r--r--spec/policies/group_policy_spec.rb36
-rw-r--r--yarn.lock16
59 files changed, 941 insertions, 299 deletions
diff --git a/.rubocop_todo/rake/require.yml b/.rubocop_todo/rake/require.yml
index 07138d6b622..60f40f20a6c 100644
--- a/.rubocop_todo/rake/require.yml
+++ b/.rubocop_todo/rake/require.yml
@@ -2,7 +2,6 @@
Rake/Require:
Details: grace period
Exclude:
- - 'ee/lib/tasks/gitlab/spdx.rake'
- 'lib/tasks/gitlab/artifacts/migrate.rake'
- 'lib/tasks/gitlab/assets.rake'
- 'lib/tasks/gitlab/backup.rake'
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
index 8d72942447c..ed7c9e7abe9 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
@@ -46,7 +46,7 @@ export default {
<template>
<span class="gl-white-space-nowrap gl-inline-flex gl-align-items-center">
<gl-dropdown
- v-if="isProjectsImportEnabled && isAvailableForImport"
+ v-if="isProjectsImportEnabled && (isAvailableForImport || isFinished)"
:text="isFinished ? __('Re-import with projects') : __('Import with projects')"
:disabled="isInvalid"
variant="confirm"
@@ -60,7 +60,7 @@ export default {
}}</gl-dropdown-item>
</gl-dropdown>
<gl-button
- v-else-if="isAvailableForImport"
+ v-else-if="isAvailableForImport || isFinished"
:disabled="isInvalid"
variant="confirm"
category="secondary"
@@ -70,7 +70,7 @@ export default {
{{ isFinished ? __('Re-import') : __('Import') }}
</gl-button>
<gl-icon
- v-if="isAvailableForImport && isFinished"
+ v-if="isFinished"
v-gl-tooltip
:size="16"
name="information-o"
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index d686522c748..7d2ddd2176b 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -103,6 +103,7 @@ export default {
perPage: DEFAULT_PAGE_SIZE,
selectedGroupsIds: [],
pendingGroupsIds: [],
+ reimportRequests: [],
importTargets: {},
unavailableFeaturesAlertVisible: true,
helpUrl: helpPagePath('ee/user/group/import', {
@@ -181,9 +182,14 @@ export default {
const importTarget = this.importTargets[group.id];
const status = this.getStatus(group);
+ const isGroupAvailableForImport = isFinished(group)
+ ? this.reimportRequests.includes(group.id)
+ : isAvailableForImport(group) && status !== STATUSES.SCHEDULING;
+
const flags = {
isInvalid: (importTarget.validationErrors ?? []).filter((e) => !e.nonBlocking).length > 0,
- isAvailableForImport: isAvailableForImport(group) && status !== STATUSES.SCHEDULING,
+ isAvailableForImport: isGroupAvailableForImport,
+ isAllowedForReimport: false,
isFinished: isFinished(group),
};
@@ -359,13 +365,9 @@ export default {
this.validateImportTarget(newImportTarget);
},
- async importGroups(importRequests) {
+ async requestGroupsImport(importRequests) {
const newPendingGroupsIds = importRequests.map((request) => request.sourceGroupId);
newPendingGroupsIds.forEach((id) => {
- this.importTargets[id].validationErrors = [
- { field: NEW_NAME_FIELD, message: i18n.ERROR_IMPORT_COMPLETED },
- ];
-
if (!this.pendingGroupsIds.includes(id)) {
this.pendingGroupsIds.push(id);
}
@@ -397,6 +399,26 @@ export default {
}
},
+ importGroup({ group, extraArgs, index }) {
+ if (group.flags.isFinished && !this.reimportRequests.includes(group.id)) {
+ this.validateImportTarget(group.importTarget);
+ this.reimportRequests.push(group.id);
+ this.$nextTick(() => {
+ this.$refs[`importTargetCell-${index}`].focusNewName();
+ });
+ } else {
+ this.reimportRequests = this.reimportRequests.filter((id) => id !== group.id);
+ this.requestGroupsImport([
+ {
+ sourceGroupId: group.id,
+ targetNamespace: group.importTarget.targetNamespace.fullPath,
+ newName: group.importTarget.newName,
+ ...extraArgs,
+ },
+ ]);
+ }
+ },
+
importSelectedGroups(extraArgs = {}) {
const importRequests = this.groupsTableData
.filter((group) => this.selectedGroupsIds.includes(group.id))
@@ -407,7 +429,7 @@ export default {
...extraArgs,
}));
- this.importGroups(importRequests);
+ this.requestGroupsImport(importRequests);
},
setPageSize(size) {
@@ -768,8 +790,9 @@ export default {
<template #cell(webUrl)="{ item: group }">
<import-source-cell :group="group" />
</template>
- <template #cell(importTarget)="{ item: group }">
+ <template #cell(importTarget)="{ item: group, index }">
<import-target-cell
+ :ref="`importTargetCell-${index}`"
:group="group"
:group-path-regex="groupPathRegex"
@update-target-namespace="updateImportTarget(group, { targetNamespace: $event })"
@@ -779,22 +802,13 @@ export default {
<template #cell(progress)="{ item: group }">
<import-status-cell :status="group.visibleStatus" class="gl-line-height-32" />
</template>
- <template #cell(actions)="{ item: group }">
+ <template #cell(actions)="{ item: group, index }">
<import-actions-cell
:is-projects-import-enabled="isProjectsImportEnabled"
:is-finished="group.flags.isFinished"
:is-available-for-import="group.flags.isAvailableForImport"
:is-invalid="group.flags.isInvalid"
- @import-group="
- importGroups([
- {
- sourceGroupId: group.id,
- targetNamespace: group.importTarget.targetNamespace.fullPath,
- newName: group.importTarget.newName,
- ...$event,
- },
- ])
- "
+ @import-group="importGroup({ group, extraArgs: $event, index })"
/>
</template>
</gl-table>
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
index 04a90d9c20c..807b084fefb 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
@@ -38,6 +38,15 @@ export default {
// this will highlight field in green like "passed validation"
return this.group.flags.isInvalid && this.group.flags.isAvailableForImport ? false : null;
},
+ isPathSelectionAvailable() {
+ return this.group.flags.isAvailableForImport;
+ },
+ },
+
+ methods: {
+ focusNewName() {
+ this.$refs.newName.$el.focus();
+ },
},
};
</script>
@@ -48,7 +57,7 @@ export default {
<import-group-dropdown
#default="{ namespaces }"
:text="fullPath"
- :disabled="!group.flags.isAvailableForImport"
+ :disabled="!isPathSelectionAvailable"
toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
class="gl-h-7 gl-flex-grow-1"
data-qa-selector="target_namespace_selector_dropdown"
@@ -76,23 +85,22 @@ export default {
<div
class="gl-h-7 gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1 gl-bg-gray-10"
:class="{
- 'gl-text-gray-400 gl-border-gray-100': !group.flags.isAvailableForImport,
- 'gl-border-gray-200': group.flags.isAvailableForImport,
+ 'gl-text-gray-400 gl-border-gray-100': !isPathSelectionAvailable,
+ 'gl-border-gray-200': isPathSelectionAvailable,
}"
>
/
</div>
<div class="gl-flex-grow-1">
<gl-form-input
+ ref="newName"
class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
:class="{
- 'gl-inset-border-1-gray-200!':
- group.flags.isAvailableForImport && !group.flags.isInvalid,
- 'gl-inset-border-1-gray-100!':
- !group.flags.isAvailableForImport && !group.flags.isInvalid,
+ 'gl-inset-border-1-gray-200!': isPathSelectionAvailable,
+ 'gl-inset-border-1-gray-100!': !isPathSelectionAvailable,
}"
debounce="500"
- :disabled="!group.flags.isAvailableForImport"
+ :disabled="!isPathSelectionAvailable"
:value="group.importTarget.newName"
:aria-label="__('New name')"
:state="validNameState"
@@ -101,7 +109,7 @@ export default {
</div>
</div>
<div
- v-if="group.flags.isAvailableForImport && (group.flags.isInvalid || validationMessage)"
+ v-if="isPathSelectionAvailable && (group.flags.isInvalid || validationMessage)"
class="gl-text-red-500 gl-m-0 gl-mt-2"
role="alert"
>
diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js
index 7213a4edf89..f8d7b0adf3b 100644
--- a/app/assets/javascripts/issues/constants.js
+++ b/app/assets/javascripts/issues/constants.js
@@ -4,6 +4,8 @@ export const STATUS_CLOSED = 'closed';
export const STATUS_OPEN = 'opened';
export const STATUS_REOPENED = 'reopened';
+export const TITLE_LENGTH_MAX = 255;
+
export const IssuableStatusText = {
[STATUS_CLOSED]: __('Closed'),
[STATUS_OPEN]: __('Open'),
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index 7a79202e007..ffded421f74 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -185,6 +185,11 @@ export default {
required: false,
default: null,
},
+ issueIid: {
+ type: Number,
+ required: false,
+ default: null,
+ },
},
data() {
const store = new Store({
@@ -559,6 +564,7 @@ export default {
<component
:is="descriptionComponent"
:issue-id="issueId"
+ :issue-iid="issueIid"
:can-update="canUpdate"
:description-html="state.descriptionHtml"
:description-text="state.descriptionText"
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index ad2aa4a8f1e..05edc51171f 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -4,6 +4,7 @@ import $ from 'jquery';
import { uniqueId } from 'lodash';
import Sortable from 'sortablejs';
import Vue from 'vue';
+import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_WORK_ITEM } from '~/graphql_shared/constants';
@@ -16,22 +17,29 @@ import { __, s__, sprintf } from '~/locale';
import { getSortableDefaultOptions, isDragging } from '~/sortable/utils';
import TaskList from '~/task_list';
import Tracking from '~/tracking';
+import addHierarchyChildMutation from '~/work_items/graphql/add_hierarchy_child.mutation.graphql';
+import removeHierarchyChildMutation from '~/work_items/graphql/remove_hierarchy_child.mutation.graphql';
+import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
+import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
-import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_CREATING,
+ I18N_WORK_ITEM_ERROR_DELETING,
TRACKING_CATEGORY_SHOW,
TASK_TYPE_NAME,
- WIDGET_TYPE_DESCRIPTION,
} from '~/work_items/constants';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import eventHub from '../event_hub';
import animateMixin from '../mixins/animate';
-import { convertDescriptionWithDeletedTaskListItem, convertDescriptionWithNewSort } from '../utils';
+import {
+ deleteTaskListItem,
+ convertDescriptionWithNewSort,
+ extractTaskTitleAndDescription,
+} from '../utils';
import TaskListItemActions from './task_list_item_actions.vue';
Vue.use(GlToast);
@@ -49,7 +57,7 @@ export default {
WorkItemDetailModal,
},
mixins: [animateMixin, glFeatureFlagMixin(), Tracking.mixin()],
- inject: ['fullPath'],
+ inject: ['fullPath', 'hasIterationsFeature'],
props: {
canUpdate: {
type: Boolean,
@@ -89,6 +97,11 @@ export default {
required: false,
default: null,
},
+ issueIid: {
+ type: Number,
+ required: false,
+ default: null,
+ },
isUpdating: {
type: Boolean,
required: false,
@@ -103,6 +116,7 @@ export default {
preAnimation: false,
pulseAnimation: false,
initialUpdate: true,
+ issueDetails: {},
activeTask: {},
workItemId: isPositiveInteger(workItemId)
? convertToGraphQLId(TYPENAME_WORK_ITEM, workItemId)
@@ -111,6 +125,16 @@ export default {
};
},
apollo: {
+ issueDetails: {
+ query: getIssueDetailsQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ iid: String(this.issueIid),
+ };
+ },
+ update: (data) => data.workspace?.issuable,
+ },
workItem: {
query: workItemQuery,
variables() {
@@ -165,6 +189,7 @@ export default {
},
},
mounted() {
+ eventHub.$on('convert-task-list-item', this.convertTaskListItem);
eventHub.$on('delete-task-list-item', this.deleteTaskListItem);
this.renderGFM();
@@ -178,6 +203,7 @@ export default {
}
},
beforeDestroy() {
+ eventHub.$off('convert-task-list-item', this.convertTaskListItem);
eventHub.$off('delete-task-list-item', this.deleteTaskListItem);
this.removeAllPointerEventListeners();
@@ -319,19 +345,26 @@ export default {
$tasksShort.text('');
}
},
- createTaskListItemActions(toggleClass) {
+ createTaskListItemActions(provide) {
const app = new Vue({
el: document.createElement('div'),
- provide: { toggleClass },
+ provide,
render: (createElement) => createElement(TaskListItemActions),
});
return app.$el;
},
- deleteTaskListItem(sourcepos) {
- this.$emit(
- 'saveDescription',
- convertDescriptionWithDeletedTaskListItem(this.descriptionText, sourcepos),
+ convertTaskListItem(sourcepos) {
+ const oldDescription = this.descriptionText;
+ const { newDescription, taskDescription, taskTitle } = deleteTaskListItem(
+ oldDescription,
+ sourcepos,
);
+ this.$emit('saveDescription', newDescription);
+ this.createTask({ taskTitle, taskDescription, oldDescription });
+ },
+ deleteTaskListItem(sourcepos) {
+ const { newDescription } = deleteTaskListItem(this.descriptionText, sourcepos);
+ this.$emit('saveDescription', newDescription);
},
renderTaskListItemActions() {
if (!this.$el?.querySelectorAll) {
@@ -368,8 +401,9 @@ export default {
}
const toggleClass = uniqueId('task-list-item-actions-');
+ const dropdown = this.createTaskListItemActions({ canUpdate: this.canUpdate, toggleClass });
this.addPointerEventListeners(item, `.${toggleClass}`);
- this.insertNextToTaskListItemText(this.createTaskListItemActions(toggleClass), item);
+ this.insertNextToTaskListItemText(dropdown, item);
this.hasTaskListItemActions = true;
});
},
@@ -423,55 +457,90 @@ export default {
this.workItemId = undefined;
this.updateWorkItemIdUrlQuery(undefined);
},
- async handleCreateTask(el) {
- this.setActiveTask(el);
+ async createTask({ taskTitle, taskDescription, oldDescription }) {
try {
- const { data } = await this.$apollo.mutate({
- mutation: createWorkItemFromTaskMutation,
- variables: {
- input: {
- id: this.issueGid,
- workItemData: {
- lockVersion: this.lockVersion,
- title: this.activeTask.title,
- lineNumberStart: Number(this.activeTask.lineNumberStart),
- lineNumberEnd: Number(this.activeTask.lineNumberEnd),
- workItemTypeId: this.taskWorkItemType,
- },
- },
+ const { title, description } = extractTaskTitleAndDescription(taskTitle, taskDescription);
+ const iterationInput = {
+ iterationWidget: {
+ iterationId: this.issueDetails.iteration?.id ?? null,
},
- update(store, { data: { workItemCreateFromTask } }) {
- const { newWorkItem } = workItemCreateFromTask;
-
- store.writeQuery({
- query: workItemQuery,
- variables: {
- id: newWorkItem.id,
- },
- data: {
- workItem: newWorkItem,
- },
- });
+ };
+ const input = {
+ confidential: this.issueDetails.confidential,
+ description,
+ hierarchyWidget: {
+ parentId: this.issueGid,
+ },
+ ...(this.hasIterationsFeature && iterationInput),
+ milestoneWidget: {
+ milestoneId: this.issueDetails.milestone?.id ?? null,
},
+ projectPath: this.fullPath,
+ title,
+ workItemTypeId: this.taskWorkItemType,
+ };
+
+ const { data } = await this.$apollo.mutate({
+ mutation: createWorkItemMutation,
+ variables: { input },
});
- const { workItem, newWorkItem } = data.workItemCreateFromTask;
+ const { workItem, errors } = data.workItemCreate;
+
+ if (errors?.length) {
+ throw new Error(errors);
+ }
- const updatedDescription = workItem?.widgets?.find(
- (widget) => widget.type === WIDGET_TYPE_DESCRIPTION,
- )?.descriptionHtml;
+ await this.$apollo.mutate({
+ mutation: addHierarchyChildMutation,
+ variables: { id: this.issueGid, workItem },
+ });
- this.$emit('updateDescription', updatedDescription);
- this.workItemId = newWorkItem.id;
- this.openWorkItemDetailModal(el);
+ this.$toast.show(s__('WorkItem|Converted to task'), {
+ action: {
+ text: s__('WorkItem|Undo'),
+ onClick: (_, toast) => {
+ this.undoCreateTask(oldDescription, workItem.id);
+ toast.hide();
+ },
+ },
+ });
} catch (error) {
- createAlert({
- message: sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, workItemTypes.TASK),
- error,
- captureError: true,
+ this.showAlert(I18N_WORK_ITEM_ERROR_CREATING, error);
+ }
+ },
+ async undoCreateTask(oldDescription, id) {
+ this.$emit('saveDescription', oldDescription);
+
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: deleteWorkItemMutation,
+ variables: { input: { id } },
+ });
+
+ const { errors } = data.workItemDelete;
+
+ if (errors?.length) {
+ throw new Error(errors);
+ }
+
+ await this.$apollo.mutate({
+ mutation: removeHierarchyChildMutation,
+ variables: { id: this.issueGid, workItem: { id } },
});
+
+ this.$toast.show(s__('WorkItem|Task reverted'));
+ } catch (error) {
+ this.showAlert(I18N_WORK_ITEM_ERROR_DELETING, error);
}
},
+ showAlert(message, error) {
+ createAlert({
+ message: sprintfWorkItem(message, workItemTypes.TASK),
+ error,
+ captureError: true,
+ });
+ },
handleDeleteTask(description) {
this.$emit('updateDescription', description);
this.$toast.show(s__('WorkItem|Task deleted'));
diff --git a/app/assets/javascripts/issues/show/components/task_list_item_actions.vue b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
index 084cd6062d5..d0beb0f39b3 100644
--- a/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
+++ b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
@@ -5,6 +5,7 @@ import eventHub from '../event_hub';
export default {
i18n: {
+ convertToTask: s__('WorkItem|Convert to task'),
delete: __('Delete'),
taskActions: s__('WorkItem|Task actions'),
},
@@ -12,8 +13,11 @@ export default {
GlDropdown,
GlDropdownItem,
},
- inject: ['toggleClass'],
+ inject: ['canUpdate', 'toggleClass'],
methods: {
+ convertToTask() {
+ eventHub.$emit('convert-task-list-item', this.$el.closest('li').dataset.sourcepos);
+ },
deleteTaskListItem() {
eventHub.$emit('delete-task-list-item', this.$el.closest('li').dataset.sourcepos);
},
@@ -33,7 +37,10 @@ export default {
text-sr-only
:toggle-class="`task-list-item-actions gl-opacity-0 gl-p-2! ${toggleClass}`"
>
- <gl-dropdown-item variant="danger" @click="deleteTaskListItem">
+ <gl-dropdown-item v-if="canUpdate" @click="convertToTask">
+ {{ $options.i18n.convertToTask }}
+ </gl-dropdown-item>
+ <gl-dropdown-item v-if="canUpdate" variant="danger" @click="deleteTaskListItem">
{{ $options.i18n.delete }}
</gl-dropdown-item>
</gl-dropdown>
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index fc0145672ed..1793ce66ad4 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -94,7 +94,12 @@ export function initIssueApp(issueData, store) {
bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
- const { canCreateIncident, hasIssueWeightsFeature, ...issueProps } = issueData;
+ const {
+ canCreateIncident,
+ hasIssueWeightsFeature,
+ hasIterationsFeature,
+ ...issueProps
+ } = issueData;
return new Vue({
el,
@@ -107,6 +112,7 @@ export function initIssueApp(issueData, store) {
registerPath,
signInPath,
hasIssueWeightsFeature,
+ hasIterationsFeature,
},
computed: {
...mapGetters(['getNoteableData']),
@@ -119,6 +125,7 @@ export function initIssueApp(issueData, store) {
isLocked: this.getNoteableData?.discussion_locked,
issuableStatus: this.getNoteableData?.state,
issueId: this.getNoteableData?.id,
+ issueIid: this.getNoteableData?.iid,
},
});
},
diff --git a/app/assets/javascripts/issues/show/utils.js b/app/assets/javascripts/issues/show/utils.js
index 26ce5d03c2f..7742a015836 100644
--- a/app/assets/javascripts/issues/show/utils.js
+++ b/app/assets/javascripts/issues/show/utils.js
@@ -1,4 +1,6 @@
+import { TITLE_LENGTH_MAX } from '~/issues/constants';
import { COLON, HYPHEN, NEWLINE } from '~/lib/utils/text_utility';
+import { __ } from '~/locale';
/**
* Returns the start and end `sourcepos` rows, converted to zero-based numbering.
@@ -94,8 +96,10 @@ export const convertDescriptionWithNewSort = (description, list) => {
return descriptionLines.join(NEWLINE);
};
-const bulletTaskListItemRegex = /^\s*[-*]\s+\[.]/;
-const numericalTaskListItemRegex = /^\s*[0-9]\.\s+\[.]/;
+const bulletTaskListItemRegex = /^\s*[-*]\s+\[.]\s+/;
+const numericalTaskListItemRegex = /^\s*[0-9]\.\s+\[.]\s+/;
+const codeMarkdownRegex = /^\s*`.*`\s*$/;
+const imageOrLinkMarkdownRegex = /^\s*!?\[.*\)\s*$/;
/**
* Checks whether the line of markdown contains a task list item,
@@ -139,12 +143,24 @@ const containsTaskListItem = (line) =>
*
* @param {String} description Description in markdown format
* @param {String} sourcepos Source position in format `23:3-23:14`
- * @returns {String} Markdown with the deleted task list item
+ * @returns {{newDescription: String, taskDescription: String, taskTitle: String}} Object with:
+ *
+ * - `newDescription` property that contains markdown with the deleted task list item omitted
+ * - `taskDescription` property that contains the description of the deleted task list item
+ * - `taskTitle` property that contains the title of the deleted task list item
*/
-export const convertDescriptionWithDeletedTaskListItem = (description, sourcepos) => {
+export const deleteTaskListItem = (description, sourcepos) => {
const descriptionLines = description.split(NEWLINE);
const [startIndex, endIndex] = getSourceposRows(sourcepos);
+ const firstLine = descriptionLines[startIndex];
+ const firstLineIndentation = firstLine.length - firstLine.trimStart().length;
+
+ const taskTitle = firstLine
+ .replace(bulletTaskListItemRegex, '')
+ .replace(numericalTaskListItemRegex, '');
+ const taskDescription = [];
+
let indentation = 0;
let linesToDelete = 1;
let reduceIndentation = false;
@@ -154,17 +170,61 @@ export const convertDescriptionWithDeletedTaskListItem = (description, sourcepos
descriptionLines[i] = descriptionLines[i].slice(indentation);
} else if (containsTaskListItem(descriptionLines[i])) {
reduceIndentation = true;
- const firstLine = descriptionLines[startIndex];
const currentLine = descriptionLines[i];
- const firstLineIndentation = firstLine.length - firstLine.trimStart().length;
const currentLineIndentation = currentLine.length - currentLine.trimStart().length;
indentation = currentLineIndentation - firstLineIndentation;
descriptionLines[i] = descriptionLines[i].slice(indentation);
} else {
+ taskDescription.push(descriptionLines[i].trimStart());
linesToDelete += 1;
}
}
descriptionLines.splice(startIndex, linesToDelete);
- return descriptionLines.join(NEWLINE);
+
+ return {
+ newDescription: descriptionLines.join(NEWLINE),
+ taskDescription: taskDescription.join(NEWLINE) || undefined,
+ taskTitle,
+ };
+};
+
+/**
+ * Given a title and description for a task:
+ *
+ * - Moves characters beyond the 255 character limit from the title to the description
+ * - Moves a pure markdown title to the description and gives the title the value `Untitled`
+ *
+ * @param {String} taskTitle The task title
+ * @param {String} taskDescription The task description
+ * @returns {{description: String, title: String}} An object with the formatted task title and description
+ */
+export const extractTaskTitleAndDescription = (taskTitle, taskDescription) => {
+ const isTitleOnlyMarkdown =
+ codeMarkdownRegex.test(taskTitle) || imageOrLinkMarkdownRegex.test(taskTitle);
+
+ if (isTitleOnlyMarkdown) {
+ return {
+ title: __('Untitled'),
+ description: taskDescription
+ ? taskTitle.concat(NEWLINE, NEWLINE, taskDescription)
+ : taskTitle,
+ };
+ }
+
+ const isTitleTooLong = taskTitle.length > TITLE_LENGTH_MAX;
+
+ if (isTitleTooLong) {
+ return {
+ title: taskTitle.slice(0, TITLE_LENGTH_MAX),
+ description: taskDescription
+ ? taskTitle.slice(TITLE_LENGTH_MAX).concat(NEWLINE, NEWLINE, taskDescription)
+ : taskTitle.slice(TITLE_LENGTH_MAX),
+ };
+ }
+
+ return {
+ title: taskTitle,
+ description: taskDescription,
+ };
};
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index fa5ce098379..963ba918089 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -23,6 +23,7 @@ module Integrations
].freeze
SECRET_MASK = '************'
+ CHANNEL_LIMIT_PER_EVENT = 10
attribute :category, default: 'chat'
@@ -37,7 +38,8 @@ module Integrations
presence: true,
public_url: true,
if: -> (integration) { integration.activated? && integration.requires_webhook? }
- validates :labels_to_be_notified_behavior, inclusion: { in: LABEL_NOTIFICATION_BEHAVIOURS }, allow_blank: true
+ validates :labels_to_be_notified_behavior, inclusion: { in: LABEL_NOTIFICATION_BEHAVIOURS }, allow_blank: true, if: :activated?
+ validate :validate_channel_limit, if: :activated?
def initialize_properties
super
@@ -300,7 +302,7 @@ module Integrations
channel_names = event_channel_value(event).presence || channel.presence
return [] unless channel_names
- channel_names.split(',').map(&:strip)
+ channel_names.split(',').map(&:strip).uniq
end
def unique_channels
@@ -308,6 +310,21 @@ module Integrations
channels_for_event(event)
end.uniq
end
+
+ def validate_channel_limit
+ supported_events.each do |event|
+ count = channels_for_event(event).count
+ next unless count > CHANNEL_LIMIT_PER_EVENT
+
+ errors.add(
+ event_channel_name(event).to_sym,
+ format(
+ s_('SlackIntegration|cannot have more than %{limit} channels'),
+ limit: CHANNEL_LIMIT_PER_EVENT
+ )
+ )
+ end
+ end
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 9568998e27d..abb3616c58f 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -36,14 +36,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
condition(:request_access_enabled) { @subject.request_access_enabled }
condition(:create_projects_disabled, scope: :subject) do
- next true if @user.nil?
-
- visibility_evaluation_result = Gitlab::VisibilityLevelChecker
- .new(user, Project.new(namespace_id: @subject.id))
- .level_restricted?
-
- @subject.project_creation_level == ::Gitlab::Access::NO_ONE_PROJECT_ACCESS ||
- visibility_evaluation_result.restricted?
+ @subject.project_creation_level == ::Gitlab::Access::NO_ONE_PROJECT_ACCESS
end
condition(:developer_maintainer_access, scope: :subject) do
diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml
index e0ff1f4be43..80a7d3607ef 100644
--- a/app/views/admin/application_settings/_email.html.haml
+++ b/app/views/admin/application_settings/_email.html.haml
@@ -21,4 +21,11 @@
.form-group
= f.gitlab_ui_checkbox_component :user_deactivation_emails_enabled, _('Enable user deactivation emails'), help_text: _('Send emails to users upon account deactivation.')
+ - if Feature.enabled?(:deactivation_email_additional_text)
+ .form-group
+ = f.label :deactivation_email_additional_text, _('Additional text for deactivation email')
+ = f.text_area :deactivation_email_additional_text, class: 'form-control gl-form-input', rows: 4
+ .form-text.text-muted
+ = _('Text added to the body of user deactivation email messages. 1000 character limit.')
+
= f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
diff --git a/db/migrate/20230202153926_add_scan_result_policy_id_to_approval_rules.rb b/db/migrate/20230202153926_add_scan_result_policy_id_to_approval_rules.rb
new file mode 100644
index 00000000000..b72405ccf30
--- /dev/null
+++ b/db/migrate/20230202153926_add_scan_result_policy_id_to_approval_rules.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddScanResultPolicyIdToApprovalRules < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ def change
+ add_column :approval_project_rules, :scan_result_policy_id, :bigint
+ add_column :approval_merge_request_rules, :scan_result_policy_id, :bigint
+ end
+end
diff --git a/db/migrate/20230206172702_add_match_on_inclusion_to_scan_result_policy.rb b/db/migrate/20230206172702_add_match_on_inclusion_to_scan_result_policy.rb
new file mode 100644
index 00000000000..bec3dfeca76
--- /dev/null
+++ b/db/migrate/20230206172702_add_match_on_inclusion_to_scan_result_policy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddMatchOnInclusionToScanResultPolicy < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ def change
+ add_column :scan_result_policies, :match_on_inclusion, :boolean
+ end
+end
diff --git a/db/post_migrate/20230203122602_schedule_vulnerabilities_feedback_migration3.rb b/db/post_migrate/20230203122602_schedule_vulnerabilities_feedback_migration3.rb
index 1d960c22b72..7fed726823b 100644
--- a/db/post_migrate/20230203122602_schedule_vulnerabilities_feedback_migration3.rb
+++ b/db/post_migrate/20230203122602_schedule_vulnerabilities_feedback_migration3.rb
@@ -14,32 +14,11 @@ class ScheduleVulnerabilitiesFeedbackMigration3 < Gitlab::Database::Migration[2.
restrict_gitlab_migration gitlab_schema: :gitlab_main
def up
- # Delete the previous jobs
- delete_batched_background_migration(
- MIGRATION,
- TABLE_NAME,
- BATCH_COLUMN,
- []
- )
-
- # Reschedule the migration
- queue_batched_background_migration(
- MIGRATION,
- TABLE_NAME,
- BATCH_COLUMN,
- job_interval: DELAY_INTERVAL,
- batch_size: BATCH_SIZE,
- max_batch_size: MAX_BATCH_SIZE,
- sub_batch_size: SUB_BATCH_SIZE
- )
+ # replaced by db/post_migrate/20230209171547_schedule_vulnerabilities_feedback_migration4.rb
+ # no-op
end
def down
- delete_batched_background_migration(
- MIGRATION,
- TABLE_NAME,
- BATCH_COLUMN,
- []
- )
+ # no-op
end
end
diff --git a/db/post_migrate/20230209103650_add_unique_software_license_policies_index_on_project_and_scan_result_policy.rb b/db/post_migrate/20230209103650_add_unique_software_license_policies_index_on_project_and_scan_result_policy.rb
new file mode 100644
index 00000000000..b3e6c42370a
--- /dev/null
+++ b/db/post_migrate/20230209103650_add_unique_software_license_policies_index_on_project_and_scan_result_policy.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddUniqueSoftwareLicensePoliciesIndexOnProjectAndScanResultPolicy < Gitlab::Database::Migration[2.1]
+ INDEX_NAME = 'idx_software_license_policies_unique_on_project_and_scan_policy'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :software_license_policies,
+ [:project_id, :software_license_id, :scan_result_policy_id],
+ unique: true,
+ name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :software_license_policies, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20230209103714_add_fk_to_approval_rules_on_scan_result_policy_id.rb b/db/post_migrate/20230209103714_add_fk_to_approval_rules_on_scan_result_policy_id.rb
new file mode 100644
index 00000000000..cfc2c33b32a
--- /dev/null
+++ b/db/post_migrate/20230209103714_add_fk_to_approval_rules_on_scan_result_policy_id.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class AddFkToApprovalRulesOnScanResultPolicyId < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :approval_project_rules,
+ :scan_result_policies,
+ column: :scan_result_policy_id,
+ on_delete: :cascade,
+ reverse_lock_order: true
+ add_concurrent_foreign_key :approval_merge_request_rules,
+ :scan_result_policies,
+ column: :scan_result_policy_id,
+ on_delete: :cascade,
+ reverse_lock_order: true
+ end
+
+ def down
+ remove_foreign_key_if_exists :approval_project_rules, column: :scan_result_policy_id
+ remove_foreign_key_if_exists :approval_merge_request_rules, column: :scan_result_policy_id
+ end
+end
diff --git a/db/post_migrate/20230209123006_remove_unique_software_license_policies_index_on_project.rb b/db/post_migrate/20230209123006_remove_unique_software_license_policies_index_on_project.rb
new file mode 100644
index 00000000000..5c69fc6d9fe
--- /dev/null
+++ b/db/post_migrate/20230209123006_remove_unique_software_license_policies_index_on_project.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class RemoveUniqueSoftwareLicensePoliciesIndexOnProject < Gitlab::Database::Migration[2.1]
+ INDEX_NAME = 'index_software_license_policies_unique_per_project'
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index_by_name :software_license_policies, INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index :software_license_policies, [:project_id, :software_license_id], unique: true, name: INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20230209171547_schedule_vulnerabilities_feedback_migration4.rb b/db/post_migrate/20230209171547_schedule_vulnerabilities_feedback_migration4.rb
new file mode 100644
index 00000000000..fb2f8fd65cc
--- /dev/null
+++ b/db/post_migrate/20230209171547_schedule_vulnerabilities_feedback_migration4.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+class ScheduleVulnerabilitiesFeedbackMigration4 < Gitlab::Database::Migration[2.1]
+ MIGRATION = 'MigrateVulnerabilitiesFeedbackToVulnerabilitiesStateTransition'
+ TABLE_NAME = :vulnerability_feedback
+ BATCH_COLUMN = :id
+ JOB_INTERVAL = 2.minutes
+ BATCH_SIZE = 250
+ SUB_BATCH_SIZE = 5
+
+ disable_ddl_transaction!
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ # Delete the previous jobs
+ delete_batched_background_migration(
+ MIGRATION,
+ TABLE_NAME,
+ BATCH_COLUMN,
+ []
+ )
+
+ # Reschedule the migration
+ queue_batched_background_migration(
+ MIGRATION,
+ TABLE_NAME,
+ BATCH_COLUMN,
+ job_interval: JOB_INTERVAL,
+ batch_size: BATCH_SIZE,
+ sub_batch_size: SUB_BATCH_SIZE
+ )
+ end
+
+ def down
+ delete_batched_background_migration(
+ MIGRATION,
+ TABLE_NAME,
+ BATCH_COLUMN,
+ []
+ )
+ end
+end
diff --git a/db/post_migrate/20230210113924_prepare_index_approval_rules_on_scan_result_policy_id.rb b/db/post_migrate/20230210113924_prepare_index_approval_rules_on_scan_result_policy_id.rb
new file mode 100644
index 00000000000..5776d6f737e
--- /dev/null
+++ b/db/post_migrate/20230210113924_prepare_index_approval_rules_on_scan_result_policy_id.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class PrepareIndexApprovalRulesOnScanResultPolicyId < Gitlab::Database::Migration[2.1]
+ PROJECT_INDEX_NAME = 'idx_approval_project_rules_on_scan_result_policy_id'
+ MERGE_REQUEST_INDEX_NAME = 'idx_approval_merge_request_rules_on_scan_result_policy_id'
+
+ # TODO: Index to be created synchronously in https://gitlab.com/gitlab-org/gitlab/-/issues/391312
+ def up
+ prepare_async_index :approval_project_rules, :scan_result_policy_id, name: PROJECT_INDEX_NAME
+ prepare_async_index :approval_merge_request_rules, :scan_result_policy_id, name: MERGE_REQUEST_INDEX_NAME
+ end
+
+ def down
+ unprepare_async_index :approval_project_rules, :scan_result_policy_id, name: PROJECT_INDEX_NAME
+ unprepare_async_index :approval_merge_request_rules, :scan_result_policy_id, name: MERGE_REQUEST_INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20230202153926 b/db/schema_migrations/20230202153926
new file mode 100644
index 00000000000..7be855b6d22
--- /dev/null
+++ b/db/schema_migrations/20230202153926
@@ -0,0 +1 @@
+b446c818b57801c3afa26fd4e2c633f04b7956d80f709947cc1be9f87a520fc2 \ No newline at end of file
diff --git a/db/schema_migrations/20230206172702 b/db/schema_migrations/20230206172702
new file mode 100644
index 00000000000..686eaf82767
--- /dev/null
+++ b/db/schema_migrations/20230206172702
@@ -0,0 +1 @@
+779501ae368409cfe42bf03151309a07f043834c37d742dc52a062727a9cb9de \ No newline at end of file
diff --git a/db/schema_migrations/20230209103650 b/db/schema_migrations/20230209103650
new file mode 100644
index 00000000000..c4e01b8f49e
--- /dev/null
+++ b/db/schema_migrations/20230209103650
@@ -0,0 +1 @@
+f56cd57c85a852f129099357ae72e94cbed7bc08c3099273842708dc40bc4411 \ No newline at end of file
diff --git a/db/schema_migrations/20230209103714 b/db/schema_migrations/20230209103714
new file mode 100644
index 00000000000..1609e8df370
--- /dev/null
+++ b/db/schema_migrations/20230209103714
@@ -0,0 +1 @@
+bcfb07f384564295b4fc359ced37d5fdcde5689a589ab32953fb1d276de692e8 \ No newline at end of file
diff --git a/db/schema_migrations/20230209123006 b/db/schema_migrations/20230209123006
new file mode 100644
index 00000000000..a4f8e624eeb
--- /dev/null
+++ b/db/schema_migrations/20230209123006
@@ -0,0 +1 @@
+daaba8ca5c6b9e5eb4ca06d4194208452cb1cf91da8abd80ea228b3887a30c0c \ No newline at end of file
diff --git a/db/schema_migrations/20230209171547 b/db/schema_migrations/20230209171547
new file mode 100644
index 00000000000..1c3b3b8d8f4
--- /dev/null
+++ b/db/schema_migrations/20230209171547
@@ -0,0 +1 @@
+ec7d8a7d00e5c6a80efa6c859df8de31e8615df4ba586d6b014fee60e0da6644 \ No newline at end of file
diff --git a/db/schema_migrations/20230210113924 b/db/schema_migrations/20230210113924
new file mode 100644
index 00000000000..b9787cddadb
--- /dev/null
+++ b/db/schema_migrations/20230210113924
@@ -0,0 +1 @@
+00998ed2ff2e1300d4af7f2b1f3817aad6cc3dcec37887704ebc0571963c461d \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 6628492e8e8..0f351f2db5e 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -11840,6 +11840,7 @@ CREATE TABLE approval_merge_request_rules (
severity_levels text[] DEFAULT '{}'::text[] NOT NULL,
vulnerability_states text[] DEFAULT '{newly_detected}'::text[] NOT NULL,
security_orchestration_policy_configuration_id bigint,
+ scan_result_policy_id bigint,
CONSTRAINT check_6fca5928b2 CHECK ((char_length(section) <= 255))
);
@@ -11912,7 +11913,8 @@ CREATE TABLE approval_project_rules (
vulnerability_states text[] DEFAULT '{newly_detected}'::text[] NOT NULL,
orchestration_policy_idx smallint,
applies_to_all_protected_branches boolean DEFAULT false NOT NULL,
- security_orchestration_policy_configuration_id bigint
+ security_orchestration_policy_configuration_id bigint,
+ scan_result_policy_id bigint
);
CREATE TABLE approval_project_rules_groups (
@@ -21726,7 +21728,8 @@ CREATE TABLE scan_result_policies (
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
orchestration_policy_idx smallint NOT NULL,
- license_states text[] DEFAULT '{}'::text[]
+ license_states text[] DEFAULT '{}'::text[],
+ match_on_inclusion boolean
);
CREATE SEQUENCE scan_result_policies_id_seq
@@ -28806,6 +28809,8 @@ CREATE INDEX idx_security_scans_on_scan_type ON security_scans USING btree (scan
CREATE UNIQUE INDEX idx_serverless_domain_cluster_on_clusters_applications_knative ON serverless_domain_cluster USING btree (clusters_applications_knative_id);
+CREATE UNIQUE INDEX idx_software_license_policies_unique_on_project_and_scan_policy ON software_license_policies USING btree (project_id, software_license_id, scan_result_policy_id);
+
CREATE INDEX idx_streaming_headers_on_external_audit_event_destination_id ON audit_events_streaming_headers USING btree (external_audit_event_destination_id);
CREATE INDEX idx_test_reports_on_issue_id_created_at_and_id ON requirements_management_test_reports USING btree (issue_id, created_at, id);
@@ -31578,8 +31583,6 @@ CREATE INDEX index_software_license_policies_on_scan_result_policy_id ON softwar
CREATE INDEX index_software_license_policies_on_software_license_id ON software_license_policies USING btree (software_license_id);
-CREATE UNIQUE INDEX index_software_license_policies_unique_per_project ON software_license_policies USING btree (project_id, software_license_id);
-
CREATE INDEX index_software_licenses_on_spdx_identifier ON software_licenses USING btree (spdx_identifier);
CREATE UNIQUE INDEX index_software_licenses_on_unique_name ON software_licenses USING btree (name);
@@ -34494,6 +34497,9 @@ ALTER TABLE ONLY protected_branches
ALTER TABLE ONLY issues
ADD CONSTRAINT fk_df75a7c8b8 FOREIGN KEY (promoted_to_epic_id) REFERENCES epics(id) ON DELETE SET NULL;
+ALTER TABLE ONLY approval_project_rules
+ ADD CONSTRAINT fk_e1372c912e FOREIGN KEY (scan_result_policy_id) REFERENCES scan_result_policies(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY ci_resources
ADD CONSTRAINT fk_e169a8e3d5_p FOREIGN KEY (partition_id, build_id) REFERENCES ci_builds(partition_id, id) ON UPDATE CASCADE ON DELETE SET NULL;
@@ -34578,6 +34584,9 @@ ALTER TABLE ONLY boards_epic_list_user_preferences
ALTER TABLE ONLY user_project_callouts
ADD CONSTRAINT fk_f62dd11a33 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+ALTER TABLE ONLY approval_merge_request_rules
+ ADD CONSTRAINT fk_f726c79756 FOREIGN KEY (scan_result_policy_id) REFERENCES scan_result_policies(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY cluster_agents
ADD CONSTRAINT fk_f7d43dee13 FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL;
diff --git a/doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md b/doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md
index e7e031fb82a..ecbafc1f32e 100644
--- a/doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md
+++ b/doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md
@@ -758,26 +758,25 @@ gantt
section Phase 0
Build data partitioning strategy :done, 0_1, 2022-06-01, 90d
section Phase 1
- Partition biggest CI tables :1_1, after 0_1, 140d
- Biggest table partitioned :milestone, metadata, 2022-12-01, 1min
+ Partition biggest CI tables :1_1, after 0_1, 200d
+ Biggest table partitioned :milestone, metadata, 2023-03-01, 1min
Tables larger than 100GB partitioned :milestone, 100gb, after 1_1, 1min
section Phase 2
- Add paritioning keys to SQL queries :2_1, after 1_1, 120d
+ Add paritioning keys to SQL queries :2_1, 2023-01-01, 120d
Emergency partition detachment possible :milestone, detachment, 2023-04-01, 1min
All SQL queries are routed to partitions :milestone, routing, after 2_1, 1min
section Phase 3
- Build new data access patterns :3_1, 2023-03-01, 120d
- New API endpoint created for inactive data :milestone, api1, 2023-05-01, 1min
- Filtering added to existing API endpoints :milestone, api2, 2023-07-01, 1min
+ Build new data access patterns :3_1, 2023-05-01, 120d
+ New API endpoint created for inactive data :milestone, api1, 2023-07-01, 1min
+ Filtering added to existing API endpoints :milestone, api2, 2023-09-01, 1min
section Phase 4
- Introduce time-decay mechanisms :4_1, 2023-06-01, 120d
- Inactive partitions are not being read :milestone, part1, 2023-08-01, 1min
- Performance of the database cluster improves :milestone, part2, 2023-09-01, 1min
+ Introduce time-decay mechanisms :4_1, 2023-08-01, 120d
+ Inactive partitions are not being read :milestone, part1, 2023-10-01, 1min
+ Performance of the database cluster improves :milestone, part2, 2023-11-01, 1min
section Phase 5
- Introduce auto-partitioning mechanisms :5_1, 2023-07-01, 120d
- New partitions are being created automatically :milestone, part3, 2023-10-01, 1min
- Partitioning is made available on self-managed :milestone, part4, 2023-11-01, 1min
-```
+ Introduce auto-partitioning mechanisms :5_1, 2023-09-01, 120d
+ New partitions are being created automatically :milestone, part3, 2023-12-01, 1min
+ Partitioning is made available on self-managed :milestone, part4, 2024-01-01, 1min
## Conclusions
diff --git a/doc/architecture/blueprints/runner_tokens/index.md b/doc/architecture/blueprints/runner_tokens/index.md
index a8cc4aad648..9709d9ff058 100644
--- a/doc/architecture/blueprints/runner_tokens/index.md
+++ b/doc/architecture/blueprints/runner_tokens/index.md
@@ -46,11 +46,38 @@ runner in supported environments using the existing `gitlab-runner register` com
The remaining concerns become non-issues due to the elimination of the registration token.
+### Comparison of current and new runner registration flow
+
+```mermaid
+graph TD
+ subgraph new[<b>New registration flow</b>]
+ A[<b>GitLab</b>: User creates a runner in GitLab UI and adds the runner configuration] -->|<b>GitLab</b>: creates ci_runners record and returns<br/>new 'glrt-' prefixed authentication token| B
+ B(<b>Runner</b>: User runs 'gitlab-runner register' command with</br>authentication token to register new runner machine with<br/>the GitLab instance) --> C{<b>Runner</b>: Does a .runner_system_id file exist in<br/>the gitlab-runner configuration directory?}
+ C -->|Yes| D[<b>Runner</b>: Reads existing system ID] --> F
+ C -->|No| E[<b>Runner</b>: Generates and persists unique system ID] --> F
+ F[<b>Runner</b>: Issues 'POST /runner/verify' request<br/>to verify authentication token validity] --> G{<b>GitLab</b>: Is the authentication token valid?}
+ G -->|Yes| H[<b>GitLab</b>: Creates ci_runner_machine database record if missing] --> J[<b>Runner</b>: Store authentication token in .config.toml]
+ G -->|No| I(<b>GitLab</b>: Returns '403 Forbidden' error) --> K(gitlab-runner register command fails)
+ J --> Z(Runner and runner machine are ready for use)
+ end
+
+ subgraph current[<b>Current registration flow</b>]
+ A'[<b>GitLab</b>: User retrieves runner registration token in GitLab UI] --> B'
+ B'[<b>Runner</b>: User runs 'gitlab-runner register' command<br/>with registration token to register new runner] -->|<b>Runner</b>: Issues 'POST /runner request' to create<br/>new runner and obtain authentication token| C'{<b>GitLab</b>: Is the registration token valid?}
+ C' -->|Yes| D'[<b>GitLab</b>: Create ci_runners database record] --> F'
+ C' -->|No| E'(<b>GitLab</b>: Return '403 Forbidden' error) --> K'(gitlab-runner register command fails)
+ F'[<b>Runner</b>: Store authentication token<br/>from response in .config.toml] --> Z'(Runner is ready for use)
+ end
+
+ style new fill:#f2ffe6
+```
+
### Using the authentication token in place of the registration token
<!-- vale gitlab.Spelling = NO -->
-In this proposal, runners created in the GitLab UI are assigned authentication tokens prefixed with
-`glrt-` (**G**it**L**ab **R**unner **T**oken).
+In this proposal, runners created in the GitLab UI are assigned
+[authentication tokens](../../../security/token_overview.md#runner-authentication-tokens-also-called-runner-tokens)
+prefixed with `glrt-` (**G**it**L**ab **R**unner **T**oken).
<!-- vale gitlab.Spelling = YES -->
The prefix allows the existing `register` command to use the authentication token _in lieu_
of the current registration token (`--registration-token`), requiring minimal adjustments in
@@ -68,8 +95,8 @@ token in the `--registration-token` argument:
| Token type | Behavior |
| ---------- | -------- |
-| Registration token | Leverages the `POST /api/v4/runners` REST endpoint to create a new runner, creating a new entry in `config.toml`. |
-| Authentication token | Leverages the `POST /api/v4/runners/verify` REST endpoint to ensure the validity of the authentication token. Creates an entry in `config.toml` file and a `system_id` value in a sidecar file if missing (`.runner_system_id`). |
+| [Registration token](../../../security/token_overview.md#runner-authentication-tokens-also-called-runner-tokens) | Leverages the `POST /api/v4/runners` REST endpoint to create a new runner, creating a new entry in `config.toml`. |
+| [Authentication token](../../../security/token_overview.md#runner-authentication-tokens-also-called-runner-tokens) | Leverages the `POST /api/v4/runners/verify` REST endpoint to ensure the validity of the authentication token. Creates an entry in `config.toml` file and a `system_id` value in a sidecar file if missing (`.runner_system_id`). |
### Transition period
diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md
index 39b89cd87a9..3782d3acd0c 100644
--- a/doc/user/project/integrations/mattermost.md
+++ b/doc/user/project/integrations/mattermost.md
@@ -36,6 +36,8 @@ Display name override is not enabled by default, you need to ask your administra
## Configure GitLab to send notifications to Mattermost
+> [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106760) in GitLab 15.9 to limit Mattermost channels to 10 per event.
+
After the Mattermost instance has an incoming webhook set up, you can set up GitLab
to send the notifications:
diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md
index 9932e782ff4..d14401a5c9d 100644
--- a/doc/user/project/integrations/slack.md
+++ b/doc/user/project/integrations/slack.md
@@ -28,6 +28,8 @@ to control GitLab from Slack. Slash commands are configured separately.
## Configure GitLab
+> [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106760) in GitLab 15.9 to limit Slack channels to 10 per event.
+
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Settings > Integrations**.
1. Select **Slack notifications**.
diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md
index 75102cf1e6c..a76557a93e6 100644
--- a/doc/user/project/issues/managing_issues.md
+++ b/doc/user/project/issues/managing_issues.md
@@ -22,6 +22,22 @@ To edit an issue:
1. Edit the available fields.
1. Select **Save changes**.
+### Remove a task list item
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/377307) in GitLab 15.9 [with a flag](../../../administration/feature_flags.md) named `work_items_mvc_2`. Disabled by default.
+
+Prerequisites:
+
+- You must have at least the Reporter role for the project, or be the author or assignee of the issue.
+
+In an issue description with task list items:
+
+1. Hover over a task list item and select the options menu (**{ellipsis_v}**).
+1. Select **Delete**.
+
+The task list item is removed from the issue description.
+Any nested task list items are moved up a nested level.
+
## Bulk edit issues from a project
> - Assigning epic [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210470) in GitLab 13.2.
diff --git a/doc/user/tasks.md b/doc/user/tasks.md
index aad53f4165c..ff18abfab5b 100644
--- a/doc/user/tasks.md
+++ b/doc/user/tasks.md
@@ -58,6 +58,22 @@ To create a task:
1. Enter the task title.
1. Select **Create task**.
+### Create a task from a task list item
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/377307) in GitLab 15.9 [with a flag](../administration/feature_flags.md) named `work_items_mvc_2`. Disabled by default.
+
+Prerequisites:
+
+- You must have at least the Reporter role for the project.
+
+In an issue description with task list items:
+
+1. Hover over a task list item and select the options menu (**{ellipsis_v}**).
+1. Select **Convert to task**.
+
+The task list item is removed from the issue description and a task is created in the tasks widget from its contents.
+Any nested task list items are moved up a nested level.
+
## Add existing tasks to an issue
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/381868) in GitLab 15.6.
diff --git a/lib/gitlab/ci/status/bridge/common.rb b/lib/gitlab/ci/status/bridge/common.rb
index d66d4b20bba..8e9eda560a7 100644
--- a/lib/gitlab/ci/status/bridge/common.rb
+++ b/lib/gitlab/ci/status/bridge/common.rb
@@ -6,7 +6,7 @@ module Gitlab
module Bridge
module Common
def label
- subject.description
+ subject.description.presence || super
end
def has_details?
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 13ae7fc419e..3e2a6a6376b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2515,6 +2515,9 @@ msgstr ""
msgid "Additional text"
msgstr ""
+msgid "Additional text for deactivation email"
+msgstr ""
+
msgid "Additional text for the sign-in and Help page."
msgstr ""
@@ -40071,6 +40074,9 @@ msgstr ""
msgid "SlackIntegration|You may need to reinstall the GitLab for Slack app when we %{linkStart}make updates or change permissions%{linkEnd}."
msgstr ""
+msgid "SlackIntegration|cannot have more than %{limit} channels"
+msgstr ""
+
msgid "SlackModal|Are you sure you want to change the project?"
msgstr ""
@@ -42439,6 +42445,9 @@ msgstr ""
msgid "Text added to the body of all email messages. %{character_limit} character limit"
msgstr ""
+msgid "Text added to the body of user deactivation email messages. 1000 character limit."
+msgstr ""
+
msgid "Text style"
msgstr ""
@@ -45545,6 +45554,9 @@ msgstr ""
msgid "Unsupported todo type passed. Supported todo types are: %{todo_types}"
msgstr ""
+msgid "Untitled"
+msgstr ""
+
msgid "Unused"
msgstr ""
@@ -48298,6 +48310,12 @@ msgstr ""
msgid "WorkItem|Closed"
msgstr ""
+msgid "WorkItem|Convert to task"
+msgstr ""
+
+msgid "WorkItem|Converted to task"
+msgstr ""
+
msgid "WorkItem|Create %{workItemType}"
msgstr ""
@@ -48445,6 +48463,9 @@ msgstr ""
msgid "WorkItem|Task deleted"
msgstr ""
+msgid "WorkItem|Task reverted"
+msgstr ""
+
msgid "WorkItem|Tasks"
msgstr ""
diff --git a/package.json b/package.json
index b2624d45395..39939ea702f 100644
--- a/package.json
+++ b/package.json
@@ -57,9 +57,9 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.2.0",
"@gitlab/svgs": "3.20.0",
- "@gitlab/ui": "55.1.0",
+ "@gitlab/ui": "55.2.0",
"@gitlab/visual-review-tools": "1.7.3",
- "@gitlab/web-ide": "0.0.1-dev-20230120231236",
+ "@gitlab/web-ide": "0.0.1-dev-20230210211358",
"@rails/actioncable": "6.1.4-7",
"@rails/ujs": "6.1.4-7",
"@sourcegraph/code-host-integration": "0.0.84",
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 5a02d712117..6019f10eeeb 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -10,7 +10,10 @@ RSpec.describe 'Database schema', feature_category: :database do
let(:columns_name_with_jsonb) { retrieve_columns_name_with_jsonb }
IGNORED_INDEXES_ON_FKS = {
- slack_integrations_scopes: %w[slack_api_scope_id]
+ slack_integrations_scopes: %w[slack_api_scope_id],
+ # Will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/391312
+ approval_project_rules: %w[scan_result_policy_id],
+ approval_merge_request_rules: %w[scan_result_policy_id]
}.with_indifferent_access.freeze
TABLE_PARTITIONS = %w[ci_builds_metadata].freeze
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index da206086663..26efed85513 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -829,9 +829,36 @@ RSpec.describe 'Admin updates settings', feature_category: :not_owned do
context 'Preferences page' do
before do
+ stub_feature_flags(deactivation_email_additional_text: deactivation_email_additional_text_feature_flag)
visit preferences_admin_application_settings_path
end
+ let(:deactivation_email_additional_text_feature_flag) { true }
+
+ describe 'Email page' do
+ context 'when deactivation email additional text feature flag is enabled' do
+ it 'shows deactivation email additional text field' do
+ expect(page).to have_field 'Additional text for deactivation email'
+
+ page.within('.as-email') do
+ fill_in 'Additional text for deactivation email', with: 'So long and thanks for all the fish!'
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content 'Application settings saved successfully'
+ expect(current_settings.deactivation_email_additional_text).to eq('So long and thanks for all the fish!')
+ end
+ end
+
+ context 'when deactivation email additional text feature flag is disabled' do
+ let(:deactivation_email_additional_text_feature_flag) { false }
+
+ it 'does not show deactivation email additional text field' do
+ expect(page).not_to have_field 'Additional text for deactivation email'
+ end
+ end
+ end
+
it 'change Help page' do
new_support_url = 'http://example.com/help'
new_documentation_url = 'https://docs.gitlab.com'
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index e3ec28f9c65..8806d1c2219 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -510,33 +510,6 @@ RSpec.describe 'Group', feature_category: :subgroups do
end
end
end
-
- context 'when in a private group' do
- before do
- group.update!(
- visibility_level: Gitlab::VisibilityLevel::PRIVATE,
- project_creation_level: Gitlab::Access::MAINTAINER_PROJECT_ACCESS
- )
- end
-
- context 'when visibility levels have been restricted to private only by an administrator' do
- before do
- stub_application_setting(
- restricted_visibility_levels: [
- Gitlab::VisibilityLevel::PRIVATE
- ]
- )
- end
-
- it 'does not display the "New project" button' do
- visit group_path(group)
-
- page.within '[data-testid="group-buttons"]' do
- expect(page).not_to have_link('New project')
- end
- end
- end
- end
end
def remove_with_confirm(button_text, confirm_with)
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index 19b2633969d..a8a747e188e 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -433,7 +433,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
issue.save!
end
- it 'shows milestone text' do
+ it 'shows milestone text', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/389287' do
sign_out(:user)
sign_in(guest)
diff --git a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
index da7fb4e060d..163a60bae36 100644
--- a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
@@ -39,7 +39,7 @@ describe('import actions cell', () => {
describe('when group is finished', () => {
beforeEach(() => {
- createComponent({ isAvailableForImport: true, isFinished: true });
+ createComponent({ isAvailableForImport: false, isFinished: true });
});
it('renders re-import button', () => {
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index 480f1bad8d4..00b69948bfb 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -38,6 +38,7 @@ describe('import table', () => {
const FAKE_GROUPS = [
generateFakeEntry({ id: 1, status: STATUSES.NONE }),
generateFakeEntry({ id: 2, status: STATUSES.FINISHED }),
+ generateFakeEntry({ id: 3, status: STATUSES.NONE }),
];
const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 };
const FAKE_VERSION_VALIDATION = {
@@ -65,8 +66,8 @@ describe('import table', () => {
const triggerSelectAllCheckbox = (checked = true) =>
wrapper.find('thead input[type=checkbox]').setChecked(checked);
- const selectRow = (idx) =>
- wrapper.findAll('tbody td input[type=checkbox]').at(idx).setChecked(true);
+ const findRowCheckbox = (idx) => wrapper.findAll('tbody td input[type=checkbox]').at(idx);
+ const selectRow = (idx) => findRowCheckbox(idx).setChecked(true);
const createComponent = ({
bulkImportSourceGroups,
@@ -115,7 +116,7 @@ describe('import table', () => {
beforeEach(() => {
axiosMock = new MockAdapter(axios);
- axiosMock.onGet(/.*\/exists$/, () => []).reply(HTTP_STATUS_OK);
+ axiosMock.onGet(/.*\/exists$/, () => []).reply(HTTP_STATUS_OK, { exists: false });
});
afterEach(() => {
@@ -609,6 +610,40 @@ describe('import table', () => {
expect(tooltip.value).toBe('Path of the new group.');
});
+ describe('re-import', () => {
+ it('renders finished row as disabled by default', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: [generateFakeEntry({ id: 5, status: STATUSES.FINISHED })],
+ pageInfo: FAKE_PAGE_INFO,
+ versionValidation: FAKE_VERSION_VALIDATION,
+ }),
+ });
+ await waitForPromises();
+
+ expect(findRowCheckbox(0).attributes('disabled')).toBeDefined();
+ });
+
+ it('enables row after clicking re-import', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: [generateFakeEntry({ id: 5, status: STATUSES.FINISHED })],
+ pageInfo: FAKE_PAGE_INFO,
+ versionValidation: FAKE_VERSION_VALIDATION,
+ }),
+ });
+ await waitForPromises();
+
+ const reimportButton = wrapper
+ .findAll('tbody td button')
+ .wrappers.find((w) => w.text().includes('Re-import'));
+
+ await reimportButton.trigger('click');
+
+ expect(findRowCheckbox(0).attributes('disabled')).toBeUndefined();
+ });
+ });
+
describe('unavailable features warning', () => {
it('renders alert when there are unavailable features', async () => {
createComponent({
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index 6345238e0e8..1fb290b5b19 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlModal } from '@gitlab/ui';
+import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import setWindowLocation from 'helpers/set_window_location_helper';
import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
@@ -9,19 +10,22 @@ import { mockTracking } from 'helpers/tracking_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createAlert } from '~/flash';
import Description from '~/issues/show/components/description.vue';
import eventHub from '~/issues/show/event_hub';
import { updateHistory } from '~/lib/utils/url_utility';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import workItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
-import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql';
import TaskList from '~/task_list';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import {
+ createWorkItemMutationErrorResponse,
+ createWorkItemMutationResponse,
+ getIssueDetailsResponse,
projectWorkItemTypesQueryResponse,
- createWorkItemFromTaskMutationResponse,
} from 'jest/work_items/mock_data';
import {
descriptionProps as initialProps,
@@ -30,6 +34,7 @@ import {
descriptionHtmlWithTask,
} from '../mock_data/mock_data';
+jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
updateHistory: jest.fn(),
@@ -45,6 +50,7 @@ const $toast = {
show: jest.fn(),
};
+const issueDetailsResponse = getIssueDetailsResponse();
const workItemQueryResponse = {
data: {
workItem: null,
@@ -53,9 +59,6 @@ const workItemQueryResponse = {
const queryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
const workItemTypesQueryHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
-const createWorkItemFromTaskSuccessHandler = jest
- .fn()
- .mockResolvedValue(createWorkItemFromTaskMutationResponse);
describe('Description component', () => {
let wrapper;
@@ -74,23 +77,27 @@ describe('Description component', () => {
function createComponent({
props = {},
provide,
- createWorkItemFromTaskHandler = createWorkItemFromTaskSuccessHandler,
+ issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse),
+ createWorkItemMutationHandler,
...options
} = {}) {
wrapper = shallowMountExtended(Description, {
propsData: {
issueId: 1,
+ issueIid: 1,
...initialProps,
...props,
},
provide: {
fullPath: 'gitlab-org/gitlab-test',
+ hasIterationsFeature: true,
...provide,
},
apolloProvider: createMockApollo([
[workItemQuery, queryHandler],
[workItemTypesQuery, workItemTypesQueryHandler],
- [createWorkItemFromTaskMutation, createWorkItemFromTaskHandler],
+ [getIssueDetailsQuery, issueDetailsQueryHandler],
+ [createWorkItemMutation, createWorkItemMutationHandler],
]),
mocks: {
$toast,
@@ -357,6 +364,100 @@ describe('Description component', () => {
});
describe('task list item actions', () => {
+ describe('converting the task list item to a task', () => {
+ describe('when successful', () => {
+ let createWorkItemMutationHandler;
+
+ beforeEach(async () => {
+ createWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(createWorkItemMutationResponse);
+ const descriptionText = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 2
+
+ paragraph text
+
+ 1. [ ] item 3
+ 1. [ ] item 4;`;
+ createComponent({
+ props: { descriptionText },
+ provide: { glFeatures: { workItemsMvc2: true } },
+ createWorkItemMutationHandler,
+ });
+ await waitForPromises();
+
+ eventHub.$emit('convert-task-list-item', '4:4-8:19');
+ await waitForPromises();
+ });
+
+ it('emits an event to update the description with the deleted task list item omitted', () => {
+ const newDescriptionText = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 3
+ 1. [ ] item 4;`;
+
+ expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
+ });
+
+ it('calls a mutation to create a task', () => {
+ const {
+ confidential,
+ iteration,
+ milestone,
+ } = issueDetailsResponse.data.workspace.issuable;
+ expect(createWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ confidential,
+ description: '\nparagraph text\n',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ iterationWidget: {
+ iterationId: IS_EE ? iteration.id : null,
+ },
+ milestoneWidget: {
+ milestoneId: milestone.id,
+ },
+ projectPath: 'gitlab-org/gitlab-test',
+ title: 'item 2',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ },
+ });
+ });
+
+ it('shows a toast to confirm the creation of the task', () => {
+ expect($toast.show).toHaveBeenCalledWith('Converted to task', expect.any(Object));
+ });
+ });
+
+ describe('when unsuccessful', () => {
+ beforeEach(async () => {
+ createComponent({
+ props: { descriptionText: 'description' },
+ provide: { glFeatures: { workItemsMvc2: true } },
+ createWorkItemMutationHandler: jest
+ .fn()
+ .mockResolvedValue(createWorkItemMutationErrorResponse),
+ });
+ await waitForPromises();
+
+ eventHub.$emit('convert-task-list-item', '1:1-1:11');
+ await waitForPromises();
+ });
+
+ it('shows an alert with an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Something went wrong when creating task. Please try again.',
+ error: new Error('an error'),
+ captureError: true,
+ });
+ });
+ });
+ });
+
describe('deleting the task list item', () => {
it('emits an event to update the description with the deleted task list item', () => {
const descriptionText = `Tasks
diff --git a/spec/frontend/issues/show/components/task_list_item_actions_spec.js b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
index d1879510d59..d52f9d57453 100644
--- a/spec/frontend/issues/show/components/task_list_item_actions_spec.js
+++ b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
@@ -7,7 +7,8 @@ describe('TaskListItemActions component', () => {
let wrapper;
const findGlDropdown = () => wrapper.findComponent(GlDropdown);
- const findGlDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findConvertToTaskItem = () => wrapper.findAllComponents(GlDropdownItem).at(0);
+ const findDeleteItem = () => wrapper.findAllComponents(GlDropdownItem).at(1);
const mountComponent = () => {
const li = document.createElement('li');
@@ -16,7 +17,7 @@ describe('TaskListItemActions component', () => {
document.body.appendChild(li);
wrapper = shallowMount(TaskListItemActions, {
- provide: { toggleClass: 'task-list-item-actions' },
+ provide: { canUpdate: true, toggleClass: 'task-list-item-actions' },
attachTo: document.querySelector('div'),
});
};
@@ -35,10 +36,18 @@ describe('TaskListItemActions component', () => {
});
});
+ it('emits event when `Convert to task` dropdown item is clicked', () => {
+ jest.spyOn(eventHub, '$emit');
+
+ findConvertToTaskItem().vm.$emit('click');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10');
+ });
+
it('emits event when `Delete` dropdown item is clicked', () => {
jest.spyOn(eventHub, '$emit');
- findGlDropdownItem().vm.$emit('click');
+ findDeleteItem().vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10');
});
diff --git a/spec/frontend/issues/show/utils_spec.js b/spec/frontend/issues/show/utils_spec.js
index 58966d51be2..e5041dd559b 100644
--- a/spec/frontend/issues/show/utils_spec.js
+++ b/spec/frontend/issues/show/utils_spec.js
@@ -1,6 +1,7 @@
import {
- convertDescriptionWithDeletedTaskListItem,
+ deleteTaskListItem,
convertDescriptionWithNewSort,
+ extractTaskTitleAndDescription,
} from '~/issues/show/utils';
describe('app/assets/javascripts/issues/show/utils.js', () => {
@@ -141,7 +142,7 @@ describe('app/assets/javascripts/issues/show/utils.js', () => {
});
});
- describe('convertDescriptionWithDeletedTaskListItem', () => {
+ describe('deleteTaskListItem', () => {
const description = `Tasks
1. [ ] item 1
@@ -226,9 +227,10 @@ describe('app/assets/javascripts/issues/show/utils.js', () => {
1. [ ] item 9
1. [ ] item 10`;
- expect(convertDescriptionWithDeletedTaskListItem(description, sourcepos)).toBe(
+ expect(deleteTaskListItem(description, sourcepos)).toEqual({
newDescription,
- );
+ taskTitle: 'item 2',
+ });
});
it('deletes deeply nested item with no children', () => {
@@ -254,9 +256,10 @@ describe('app/assets/javascripts/issues/show/utils.js', () => {
1. [ ] item 9
1. [ ] item 10`;
- expect(convertDescriptionWithDeletedTaskListItem(description, sourcepos)).toBe(
+ expect(deleteTaskListItem(description, sourcepos)).toEqual({
newDescription,
- );
+ taskTitle: 'item 4',
+ });
});
it('deletes item with children and moves sub-tasks up a level', () => {
@@ -282,9 +285,10 @@ describe('app/assets/javascripts/issues/show/utils.js', () => {
1. [ ] item 9
1. [ ] item 10`;
- expect(convertDescriptionWithDeletedTaskListItem(description, sourcepos)).toBe(
+ expect(deleteTaskListItem(description, sourcepos)).toEqual({
newDescription,
- );
+ taskTitle: 'item 3',
+ });
});
it('deletes item with associated paragraph text', () => {
@@ -306,10 +310,15 @@ describe('app/assets/javascripts/issues/show/utils.js', () => {
1. [ ] item 9
1. [ ] item 10`;
+ const taskDescription = `
+paragraph text
+`;
- expect(convertDescriptionWithDeletedTaskListItem(description, sourcepos)).toBe(
+ expect(deleteTaskListItem(description, sourcepos)).toEqual({
newDescription,
- );
+ taskDescription,
+ taskTitle: 'item 6',
+ });
});
it('deletes item with associated paragraph text and moves sub-tasks up a level', () => {
@@ -331,10 +340,71 @@ describe('app/assets/javascripts/issues/show/utils.js', () => {
1. [ ] item 9
1. [ ] item 10`;
+ const taskDescription = `
+paragraph text
+`;
- expect(convertDescriptionWithDeletedTaskListItem(description, sourcepos)).toBe(
+ expect(deleteTaskListItem(description, sourcepos)).toEqual({
newDescription,
- );
+ taskDescription,
+ taskTitle: 'item 7',
+ });
+ });
+ });
+
+ describe('extractTaskTitleAndDescription', () => {
+ const description = `A multi-line
+description`;
+
+ describe('when title is pure code block', () => {
+ const title = '`code block`';
+
+ it('moves the title to the description', () => {
+ expect(extractTaskTitleAndDescription(title)).toEqual({
+ title: 'Untitled',
+ description: title,
+ });
+ });
+
+ it('moves the title to the description and appends the description to it', () => {
+ expect(extractTaskTitleAndDescription(title, description)).toEqual({
+ title: 'Untitled',
+ description: `${title}\n\n${description}`,
+ });
+ });
+ });
+
+ describe('when title is too long', () => {
+ const title =
+ 'Deleniti id facere numquam cum consectetur sint ipsum consequatur. Odit nihil harum consequuntur est nemo adipisci. Incidunt suscipit voluptatem et culpa at voluptatem consequuntur. Rerum aliquam earum quia consequatur ipsam quae ut. Quod molestias ducimus quia ratione nostrum ut adipisci.';
+ const expectedTitle =
+ 'Deleniti id facere numquam cum consectetur sint ipsum consequatur. Odit nihil harum consequuntur est nemo adipisci. Incidunt suscipit voluptatem et culpa at voluptatem consequuntur. Rerum aliquam earum quia consequatur ipsam quae ut. Quod molestias ducimu';
+
+ it('moves the title beyond the character limit to the description', () => {
+ expect(extractTaskTitleAndDescription(title)).toEqual({
+ title: expectedTitle,
+ description: 's quia ratione nostrum ut adipisci.',
+ });
+ });
+
+ it('moves the title beyond the character limit to the description and appends the description to it', () => {
+ expect(extractTaskTitleAndDescription(title, description)).toEqual({
+ title: expectedTitle,
+ description: `s quia ratione nostrum ut adipisci.\n\n${description}`,
+ });
+ });
+ });
+
+ describe('when title is fine', () => {
+ const title = 'A fine title';
+
+ it('uses the title with no modifications', () => {
+ expect(extractTaskTitleAndDescription(title)).toEqual({ title });
+ });
+
+ it('uses the title and description with no modifications', () => {
+ expect(extractTaskTitleAndDescription(title, description)).toEqual({ title, description });
+ });
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index 0c98ac794f7..ec51f92b578 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -18,6 +18,7 @@ import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.
import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
+ getIssueDetailsResponse,
workItemHierarchyResponse,
workItemHierarchyEmptyResponse,
workItemHierarchyNoUpdatePermissionResponse,
@@ -28,39 +29,6 @@ import {
Vue.use(VueApollo);
-const issueDetailsResponse = (confidential = false) => ({
- data: {
- workspace: {
- id: 'gid://gitlab/Project/1',
- issuable: {
- id: 'gid://gitlab/Issue/4',
- confidential,
- iteration: {
- id: 'gid://gitlab/Iteration/1124',
- title: null,
- startDate: '2022-06-22',
- dueDate: '2022-07-19',
- webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/-/iterations/1124',
- iterationCadence: {
- id: 'gid://gitlab/Iterations::Cadence/1101',
- title: 'Quod voluptates quidem ea eaque eligendi ex corporis.',
- __typename: 'IterationCadence',
- },
- __typename: 'Iteration',
- },
- milestone: {
- dueDate: null,
- expired: false,
- id: 'gid://gitlab/Milestone/28',
- title: 'v2.0',
- __typename: 'Milestone',
- },
- __typename: 'Issue',
- },
- __typename: 'Project',
- },
- },
-});
const showModal = jest.fn();
describe('WorkItemLinks', () => {
@@ -84,7 +52,7 @@ describe('WorkItemLinks', () => {
data = {},
fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse),
mutationHandler = mutationChangeParentHandler,
- issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse()),
+ issueDetailsQueryHandler = jest.fn().mockResolvedValue(getIssueDetailsResponse()),
hasIterationsFeature = false,
fetchByIid = false,
} = {}) => {
@@ -295,7 +263,9 @@ describe('WorkItemLinks', () => {
describe('when parent item is confidential', () => {
it('passes correct confidentiality status to form', async () => {
await createComponent({
- issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)),
+ issueDetailsQueryHandler: jest
+ .fn()
+ .mockResolvedValue(getIssueDetailsResponse({ confidential: true })),
});
findToggleFormDropdown().vm.$emit('click');
findToggleAddFormButton().vm.$emit('click');
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index d6b2b5a1981..a74e01db0da 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -471,6 +471,28 @@ export const workItemResponseFactory = ({
},
});
+export const getIssueDetailsResponse = ({ confidential = false } = {}) => ({
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/1',
+ issuable: {
+ id: 'gid://gitlab/Issue/4',
+ confidential,
+ iteration: {
+ id: 'gid://gitlab/Iteration/1124',
+ __typename: 'Iteration',
+ },
+ milestone: {
+ id: 'gid://gitlab/Milestone/28',
+ __typename: 'Milestone',
+ },
+ __typename: 'Issue',
+ },
+ __typename: 'Project',
+ },
+ },
+});
+
export const projectWorkItemTypesQueryResponse = {
data: {
workspace: {
@@ -528,6 +550,16 @@ export const createWorkItemMutationResponse = {
},
};
+export const createWorkItemMutationErrorResponse = {
+ data: {
+ workItemCreate: {
+ __typename: 'WorkItemCreatePayload',
+ workItem: null,
+ errors: ['an error'],
+ },
+ },
+};
+
export const createWorkItemFromTaskMutationResponse = {
data: {
workItemCreateFromTask: {
diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
index 1fb442a74fb..3a885d70eb4 100644
--- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
@@ -73,8 +73,9 @@ RSpec.describe Gitlab::BareRepositoryImport::Importer do
end
it 'does not schedule an import' do
- project = Project.find_by_full_path(project_path)
- expect(project).not_to receive(:import_schedule)
+ expect_next_instance_of(Project) do |instance|
+ expect(instance).not_to receive(:import_schedule)
+ end
importer.create_project_if_needed
end
diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
index 95b1661ac99..236e04a041b 100644
--- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
@@ -27,8 +27,8 @@ RSpec.describe Gitlab::BitbucketImport::ProjectCreator do
end
it 'creates project' do
- allow_next_instances_of(Project, 2) do |project|
- allow(project).to receive(:add_import_job)
+ expect_next_instance_of(Project) do |project|
+ expect(project).to receive(:add_import_job)
end
project_creator = described_class.new(repo, 'vim', namespace, user, access_params)
diff --git a/spec/lib/gitlab/ci/status/bridge/common_spec.rb b/spec/lib/gitlab/ci/status/bridge/common_spec.rb
index 37524afc83d..fef97c73a91 100644
--- a/spec/lib/gitlab/ci/status/bridge/common_spec.rb
+++ b/spec/lib/gitlab/ci/status/bridge/common_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Status::Bridge::Common do
+RSpec.describe Gitlab::Ci::Status::Bridge::Common, feature_category: :continuous_integration do
let_it_be(:user) { create(:user) }
let_it_be(:bridge) { create(:ci_bridge) }
let_it_be(:downstream_pipeline) { create(:ci_pipeline) }
@@ -37,4 +37,35 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Common do
it { expect(subject.details_path).to be_nil }
end
end
+
+ describe '#label' do
+ let(:description) { 'my description' }
+ let(:bridge) { create(:ci_bridge, description: description) }
+
+ subject do
+ Gitlab::Ci::Status::Created
+ .new(bridge, user)
+ .extend(described_class)
+ end
+
+ it 'returns description' do
+ expect(subject.label).to eq description
+ end
+
+ context 'when description is nil' do
+ let(:description) { nil }
+
+ it 'returns core status label' do
+ expect(subject.label).to eq('created')
+ end
+ end
+
+ context 'when description is empty string' do
+ let(:description) { '' }
+
+ it 'returns core status label' do
+ expect(subject.label).to eq('created')
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
index eac17cf08fb..552c64d81ce 100644
--- a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory, feature_category: :continuou
expect(status.text).to eq s_('CiStatusText|created')
expect(status.icon).to eq 'status_created'
expect(status.favicon).to eq 'favicon_status_created'
- expect(status.label).to be_nil
+ expect(status.label).to eq 'created'
expect(status).not_to have_details
expect(status).not_to have_action
end
@@ -52,7 +52,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory, feature_category: :continuou
expect(status.text).to eq s_('CiStatusText|failed')
expect(status.icon).to eq 'status_failed'
expect(status.favicon).to eq 'favicon_status_failed'
- expect(status.label).to be_nil
+ expect(status.label).to eq 'failed'
expect(status.status_tooltip).to eq "#{s_('CiStatusText|failed')} - (unknown failure)"
expect(status).not_to have_details
expect(status).to have_action
diff --git a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
index 59a98987f7d..53bf1db3438 100644
--- a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
@@ -24,8 +24,8 @@ RSpec.describe Gitlab::GitlabImport::ProjectCreator do
end
it 'creates project' do
- allow_next_instance_of(Project) do |project|
- allow(project).to receive(:add_import_job)
+ expect_next_instance_of(Project) do |project|
+ expect(project).to receive(:add_import_job)
end
project_creator = described_class.new(repo, namespace, user, access_params)
diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
index 5df44bfb83c..17ecd183ac9 100644
--- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Gitlab::LegacyGithubImport::ProjectCreator do
before do
namespace.add_owner(user)
- allow_next_instances_of(Project, 2) do |project|
+ expect_next_instance_of(Project) do |project|
allow(project).to receive(:add_import_job)
end
end
diff --git a/spec/migrations/20230203122602_schedule_vulnerabilities_feedback_migration3_spec.rb b/spec/migrations/20230203122602_schedule_vulnerabilities_feedback_migration4_spec.rb
index 91604b1f57c..26c63e6deb2 100644
--- a/spec/migrations/20230203122602_schedule_vulnerabilities_feedback_migration3_spec.rb
+++ b/spec/migrations/20230203122602_schedule_vulnerabilities_feedback_migration4_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-RSpec.describe ScheduleVulnerabilitiesFeedbackMigration3, feature_category: :vulnerability_management do
+RSpec.describe ScheduleVulnerabilitiesFeedbackMigration4, feature_category: :vulnerability_management do
let(:migration) { described_class::MIGRATION }
describe '#up' do
@@ -13,9 +13,9 @@ RSpec.describe ScheduleVulnerabilitiesFeedbackMigration3, feature_category: :vul
expect(migration).to have_scheduled_batched_migration(
table_name: :vulnerability_feedback,
column_name: :id,
- interval: described_class::DELAY_INTERVAL,
+ interval: described_class::JOB_INTERVAL,
batch_size: described_class::BATCH_SIZE,
- max_batch_size: described_class::MAX_BATCH_SIZE
+ sub_batch_size: described_class::SUB_BATCH_SIZE
)
end
end
diff --git a/spec/models/integrations/base_chat_notification_spec.rb b/spec/models/integrations/base_chat_notification_spec.rb
index 1527ffd7278..13dd9e03ab1 100644
--- a/spec/models/integrations/base_chat_notification_spec.rb
+++ b/spec/models/integrations/base_chat_notification_spec.rb
@@ -9,13 +9,33 @@ RSpec.describe Integrations::BaseChatNotification, feature_category: :integratio
describe 'validations' do
before do
- allow(subject).to receive(:activated?).and_return(true)
+ subject.active = active
+
allow(subject).to receive(:default_channel_placeholder).and_return('placeholder')
allow(subject).to receive(:webhook_help).and_return('help')
end
- it { is_expected.to validate_presence_of :webhook }
- it { is_expected.to validate_inclusion_of(:labels_to_be_notified_behavior).in_array(%w[match_any match_all]).allow_blank }
+ def build_channel_list(count)
+ (1..count).map { |i| "##{i}" }.join(',')
+ end
+
+ context 'when active' do
+ let(:active) { true }
+
+ it { is_expected.to validate_presence_of :webhook }
+ it { is_expected.to validate_inclusion_of(:labels_to_be_notified_behavior).in_array(%w[match_any match_all]).allow_blank }
+ it { is_expected.to allow_value(build_channel_list(10)).for(:push_channel) }
+ it { is_expected.not_to allow_value(build_channel_list(11)).for(:push_channel) }
+ end
+
+ context 'when inactive' do
+ let(:active) { false }
+
+ it { is_expected.not_to validate_presence_of :webhook }
+ it { is_expected.not_to validate_inclusion_of(:labels_to_be_notified_behavior).in_array(%w[match_any match_all]).allow_blank }
+ it { is_expected.to allow_value(build_channel_list(10)).for(:push_channel) }
+ it { is_expected.to allow_value(build_channel_list(11)).for(:push_channel) }
+ end
end
describe '#execute' do
@@ -309,6 +329,10 @@ RSpec.describe Integrations::BaseChatNotification, feature_category: :integratio
context 'with multiple channel names with spaces specified' do
it_behaves_like 'with channel specified', 'slack-integration, #slack-test, @UDLP91W0A', ['slack-integration', '#slack-test', '@UDLP91W0A']
end
+
+ context 'with duplicate channel names' do
+ it_behaves_like 'with channel specified', '#slack-test,#slack-test,#slack-test-2', ['#slack-test', '#slack-test-2']
+ end
end
describe '#default_channel_placeholder' do
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 668b3aa8236..451db9eaf9c 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -667,42 +667,6 @@ RSpec.describe GroupPolicy, feature_category: :authentication_and_authorization
it { is_expected.to be_allowed(:create_projects) }
end
-
- context 'when there are no available visibility levels because they have been restricted by an administrator' do
- before do
- stub_application_setting(
- restricted_visibility_levels: [
- Gitlab::VisibilityLevel::PUBLIC,
- Gitlab::VisibilityLevel::INTERNAL,
- Gitlab::VisibilityLevel::PRIVATE
- ]
- )
- end
-
- context 'reporter' do
- let(:current_user) { reporter }
-
- it { is_expected.to be_disallowed(:create_projects) }
- end
-
- context 'developer' do
- let(:current_user) { developer }
-
- it { is_expected.to be_disallowed(:create_projects) }
- end
-
- context 'maintainer' do
- let(:current_user) { maintainer }
-
- it { is_expected.to be_disallowed(:create_projects) }
- end
-
- context 'owner' do
- let(:current_user) { owner }
-
- it { is_expected.to be_disallowed(:create_projects) }
- end
- end
end
end
diff --git a/yarn.lock b/yarn.lock
index 59e654c52e7..2735a71c5d6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1347,10 +1347,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.20.0.tgz#4ee4f2f24304d13ccce58f82c2ecd87e556f35b4"
integrity sha512-nYTF4j5kon4XbBr/sAzuubgxjIne9+RTZLmSrSaL9FL4eyuv9aa7YMCcOrlIbYX5jlSYlcD+ck2F2M1sqXXOBA==
-"@gitlab/ui@55.1.0":
- version "55.1.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-55.1.0.tgz#a2ea6951365c495df7acdd1351b1660771607b67"
- integrity sha512-0E+l76jNsK3BPqQmbuTKAvC4RfjQfpaLvmlShe8wxrnMrS0IKsse43RST0ttV+mhkOVfac0me8pDhN4ijSm7Tw==
+"@gitlab/ui@55.2.0":
+ version "55.2.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-55.2.0.tgz#0cd24310a4ebbd08c1fcf4281b8cc60709fde0da"
+ integrity sha512-iEbW3OvgyLcT7c0Sd2LcB3eo4kuxIRoqkM4xCZwgIxVLOeGsQfbaBHMCSG9Ekt/OYoesq7B7pX6AqrV9UBuwKw==
dependencies:
"@popperjs/core" "^2.11.2"
bootstrap-vue "2.20.1"
@@ -1366,10 +1366,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.3.tgz#9ea641146436da388ffbad25d7f2abe0df52c235"
integrity sha512-NMV++7Ew1FSBDN1xiZaauU9tfeSfgDHcOLpn+8bGpP+O5orUPm2Eu66R5eC5gkjBPaXosNAxNWtriee+aFk4+g==
-"@gitlab/web-ide@0.0.1-dev-20230120231236":
- version "0.0.1-dev-20230120231236"
- resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20230120231236.tgz#ab80a527b002c3ed4bbb719c8f624b4af4011352"
- integrity sha512-1RDZ3h94YjFiPKuoKAV3TMEdaxHQvNTH0RH7AUVb1e5KXR82jXPYAfWyh3QGdfQ0XFu2QVdkRI0BE3zfhL0FIQ==
+"@gitlab/web-ide@0.0.1-dev-20230210211358":
+ version "0.0.1-dev-20230210211358"
+ resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20230210211358.tgz#1417d4beec86879aec4e6c13a4ba2ffbd3cb8874"
+ integrity sha512-U5Q9Dmb/rkfWqzb0TOpxzSzs1BJ1v2/IOj+8AwL+16CWaQE3Lh/d5XizWULwk29McLtm6H8Em2OZwjOqWZvouA==
"@graphql-eslint/eslint-plugin@3.12.0":
version "3.12.0"