Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
67cddd762d
commit
aadb3204ea
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 { title, description } = extractTaskTitleAndDescription(taskTitle, taskDescription);
|
||||
const iterationInput = {
|
||||
iterationWidget: {
|
||||
iterationId: this.issueDetails.iteration?.id ?? null,
|
||||
},
|
||||
};
|
||||
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: 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,
|
||||
},
|
||||
mutation: createWorkItemMutation,
|
||||
variables: { input },
|
||||
});
|
||||
|
||||
const { workItem, errors } = data.workItemCreate;
|
||||
|
||||
if (errors?.length) {
|
||||
throw new Error(errors);
|
||||
}
|
||||
|
||||
await this.$apollo.mutate({
|
||||
mutation: addHierarchyChildMutation,
|
||||
variables: { id: this.issueGid, workItem },
|
||||
});
|
||||
|
||||
this.$toast.show(s__('WorkItem|Converted to task'), {
|
||||
action: {
|
||||
text: s__('WorkItem|Undo'),
|
||||
onClick: (_, toast) => {
|
||||
this.undoCreateTask(oldDescription, workItem.id);
|
||||
toast.hide();
|
||||
},
|
||||
},
|
||||
update(store, { data: { workItemCreateFromTask } }) {
|
||||
const { newWorkItem } = workItemCreateFromTask;
|
||||
|
||||
store.writeQuery({
|
||||
query: workItemQuery,
|
||||
variables: {
|
||||
id: newWorkItem.id,
|
||||
},
|
||||
data: {
|
||||
workItem: newWorkItem,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { workItem, newWorkItem } = data.workItemCreateFromTask;
|
||||
|
||||
const updatedDescription = workItem?.widgets?.find(
|
||||
(widget) => widget.type === WIDGET_TYPE_DESCRIPTION,
|
||||
)?.descriptionHtml;
|
||||
|
||||
this.$emit('updateDescription', updatedDescription);
|
||||
this.workItemId = newWorkItem.id;
|
||||
this.openWorkItemDetailModal(el);
|
||||
} 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'));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
b446c818b57801c3afa26fd4e2c633f04b7956d80f709947cc1be9f87a520fc2
|
||||
|
|
@ -0,0 +1 @@
|
|||
779501ae368409cfe42bf03151309a07f043834c37d742dc52a062727a9cb9de
|
||||
|
|
@ -0,0 +1 @@
|
|||
f56cd57c85a852f129099357ae72e94cbed7bc08c3099273842708dc40bc4411
|
||||
|
|
@ -0,0 +1 @@
|
|||
bcfb07f384564295b4fc359ced37d5fdcde5689a589ab32953fb1d276de692e8
|
||||
|
|
@ -0,0 +1 @@
|
|||
daaba8ca5c6b9e5eb4ca06d4194208452cb1cf91da8abd80ea228b3887a30c0c
|
||||
|
|
@ -0,0 +1 @@
|
|||
ec7d8a7d00e5c6a80efa6c859df8de31e8615df4ba586d6b014fee60e0da6644
|
||||
|
|
@ -0,0 +1 @@
|
|||
00998ed2ff2e1300d4af7f2b1f3817aad6cc3dcec37887704ebc0571963c461d
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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**.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ module Gitlab
|
|||
module Bridge
|
||||
module Common
|
||||
def label
|
||||
subject.description
|
||||
subject.description.presence || super
|
||||
end
|
||||
|
||||
def has_details?
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
16
yarn.lock
16
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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue