Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
69d7f138e4
commit
0ff07f6124
|
|
@ -128,6 +128,7 @@ function mountBoardApp(el) {
|
|||
hasIssuableHealthStatusFeature: parseBoolean(el.dataset.healthStatusFeatureAvailable),
|
||||
hasSubepicsFeature: parseBoolean(el.dataset.subEpicsFeatureAvailable),
|
||||
hasLinkedItemsEpicsFeature: parseBoolean(el.dataset.hasLinkedItemsEpicsFeature),
|
||||
hasOkrsFeature: parseBoolean(el.dataset.hasOkrsFeature),
|
||||
},
|
||||
render: (createComponent) => createComponent(BoardApp),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export default {
|
|||
},
|
||||
{
|
||||
orderBy: LIST_KEY_CREATED_AT,
|
||||
label: s__('MlExperimentTracking|Created at'),
|
||||
label: s__('MlExperimentTracking|Created'),
|
||||
},
|
||||
],
|
||||
emptyState: {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
|
||||
TEST_ID_DELETE_ACTION,
|
||||
TEST_ID_PROMOTE_ACTION,
|
||||
TEST_ID_CHANGE_TYPE_ACTION,
|
||||
TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
|
||||
TEST_ID_COPY_REFERENCE_ACTION,
|
||||
TEST_ID_TOGGLE_ACTION,
|
||||
|
|
@ -47,6 +48,7 @@ import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql
|
|||
import updateWorkItemNotificationsMutation from '../graphql/update_work_item_notifications.mutation.graphql';
|
||||
import convertWorkItemMutation from '../graphql/work_item_convert.mutation.graphql';
|
||||
import namespaceWorkItemTypesQuery from '../graphql/namespace_work_item_types.query.graphql';
|
||||
import WorkItemChangeTypeModal from './work_item_change_type_modal.vue';
|
||||
import WorkItemStateToggle from './work_item_state_toggle.vue';
|
||||
import CreateWorkItemModal from './create_work_item_modal.vue';
|
||||
|
||||
|
|
@ -67,6 +69,7 @@ export default {
|
|||
emailAddressCopied: __('Email address copied'),
|
||||
moreActions: __('More actions'),
|
||||
reportAbuse: __('Report abuse'),
|
||||
changeWorkItemType: s__('WorkItem|Change type'),
|
||||
},
|
||||
WORK_ITEM_TYPE_ENUM_EPIC,
|
||||
components: {
|
||||
|
|
@ -78,6 +81,7 @@ export default {
|
|||
GlToggle,
|
||||
WorkItemStateToggle,
|
||||
CreateWorkItemModal,
|
||||
WorkItemChangeTypeModal,
|
||||
},
|
||||
directives: {
|
||||
GlModal: GlModalDirective,
|
||||
|
|
@ -91,6 +95,7 @@ export default {
|
|||
copyCreateNoteEmailTestId: TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
|
||||
deleteActionTestId: TEST_ID_DELETE_ACTION,
|
||||
promoteActionTestId: TEST_ID_PROMOTE_ACTION,
|
||||
changeTypeTestId: TEST_ID_CHANGE_TYPE_ACTION,
|
||||
lockDiscussionTestId: TEST_ID_LOCK_ACTION,
|
||||
stateToggleTestId: TEST_ID_TOGGLE_ACTION,
|
||||
reportAbuseActionTestId: TEST_ID_REPORT_ABUSE,
|
||||
|
|
@ -170,6 +175,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isDrawer: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
hideSubscribe: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
@ -180,6 +190,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
hasParent: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
workItemAuthorId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
|
|
@ -194,6 +209,16 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
widgets: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
allowedChildTypes: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -287,6 +312,9 @@ export default {
|
|||
? this.$options.i18n.confidentialityDisabled
|
||||
: this.$options.i18n.confidentialityEnabled;
|
||||
},
|
||||
showChangeType() {
|
||||
return !(this.isEpic || this.isDrawer) && this.glFeatures.workItemsBeta;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(text, message) {
|
||||
|
|
@ -420,6 +448,9 @@ export default {
|
|||
this.$emit('toggleReportAbuseModal', true);
|
||||
this.closeDropdown();
|
||||
},
|
||||
showChangeTypeModal() {
|
||||
this.$refs.workItemsChangeTypeModal.show();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -487,6 +518,14 @@ export default {
|
|||
<template #list-item>{{ __('Promote to objective') }}</template>
|
||||
</gl-disclosure-dropdown-item>
|
||||
|
||||
<gl-disclosure-dropdown-item
|
||||
v-if="showChangeType"
|
||||
:data-testid="$options.changeTypeTestId"
|
||||
@action="showChangeTypeModal"
|
||||
>
|
||||
<template #list-item>{{ $options.i18n.changeWorkItemType }}</template>
|
||||
</gl-disclosure-dropdown-item>
|
||||
|
||||
<gl-disclosure-dropdown-item
|
||||
v-if="canLockWorkItem"
|
||||
:data-testid="$options.lockDiscussionTestId"
|
||||
|
|
@ -568,5 +607,17 @@ export default {
|
|||
@workItemCreated="$emit('workItemCreated')"
|
||||
@hideModal="isCreateWorkItemModalVisible = false"
|
||||
/>
|
||||
<work-item-change-type-modal
|
||||
v-if="showChangeType"
|
||||
ref="workItemsChangeTypeModal"
|
||||
:work-item-id="workItemId"
|
||||
:work-item-type="workItemType"
|
||||
:full-path="fullPath"
|
||||
:has-children="hasChildren"
|
||||
:has-parent="hasParent"
|
||||
:widgets="widgets"
|
||||
:allowed-child-types="allowedChildTypes"
|
||||
@workItemTypeChanged="$emit('workItemTypeChanged')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,384 @@
|
|||
<script>
|
||||
import { GlModal, GlFormGroup, GlFormSelect, GlAlert, GlButton } from '@gitlab/ui';
|
||||
import { differenceBy } from 'lodash';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import { findDesignWidget } from '~/work_items/utils';
|
||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||
|
||||
import {
|
||||
WIDGET_TYPE_HIERARCHY,
|
||||
WORK_ITEMS_TYPE_MAP,
|
||||
WORK_ITEM_ALLOWED_CHANGE_TYPE_MAP,
|
||||
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
|
||||
WORK_ITEM_TYPE_ENUM_KEY_RESULT,
|
||||
sprintfWorkItem,
|
||||
I18N_WORK_ITEM_CHANGE_TYPE_PARENT_ERROR,
|
||||
I18N_WORK_ITEM_CHANGE_TYPE_CHILD_ERROR,
|
||||
I18N_WORK_ITEM_CHANGE_TYPE_MISSING_FIELDS_ERROR,
|
||||
WORK_ITEM_WIDGETS_NAME_MAP,
|
||||
WIDGET_TYPE_DESIGNS,
|
||||
} from '../constants';
|
||||
|
||||
import namespaceWorkItemTypesQuery from '../graphql/namespace_work_item_types.query.graphql';
|
||||
import convertWorkItemMutation from '../graphql/work_item_convert.mutation.graphql';
|
||||
import getWorkItemDesignListQuery from './design_management/graphql/design_collection.query.graphql';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
type: __('Type'),
|
||||
subText: s__('WorkItem|Select which type you would like to change this item to.'),
|
||||
},
|
||||
components: {
|
||||
GlModal,
|
||||
GlFormGroup,
|
||||
GlFormSelect,
|
||||
GlAlert,
|
||||
GlButton,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: ['hasOkrsFeature'],
|
||||
props: {
|
||||
workItemId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
workItemType: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
fullPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
hasChildren: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
hasParent: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
widgets: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
allowedChildTypes: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedWorkItemType: null,
|
||||
workItemTypes: [],
|
||||
warningMessage: '',
|
||||
errorMessage: '',
|
||||
changeTypeDisabled: true,
|
||||
hasDesigns: false,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
workItemTypes: {
|
||||
query: namespaceWorkItemTypesQuery,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return data.workspace?.workItemTypes?.nodes;
|
||||
},
|
||||
error(e) {
|
||||
this.showErrorMessage(e);
|
||||
},
|
||||
},
|
||||
hasDesigns: {
|
||||
query: getWorkItemDesignListQuery,
|
||||
variables() {
|
||||
return {
|
||||
id: this.workItemId,
|
||||
atVersion: null,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return findDesignWidget(data.workItem.widgets)?.designCollection?.designs.nodes?.length > 0;
|
||||
},
|
||||
skip() {
|
||||
return !this.workItemId;
|
||||
},
|
||||
error(e) {
|
||||
this.showErrorMessage(e);
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
allowedConversionWorkItemTypes() {
|
||||
// The logic will be simplified once we implement
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/498656
|
||||
return [
|
||||
{ text: __('Select type'), value: null },
|
||||
...Object.entries(WORK_ITEMS_TYPE_MAP)
|
||||
.map(([key, value]) => ({
|
||||
text: value.value,
|
||||
value: key,
|
||||
}))
|
||||
.filter((item) => {
|
||||
if (item.text === this.workItemType) {
|
||||
return false;
|
||||
}
|
||||
// Keeping this separate for readability
|
||||
if (
|
||||
item.value === WORK_ITEM_TYPE_ENUM_OBJECTIVE ||
|
||||
item.value === WORK_ITEM_TYPE_ENUM_KEY_RESULT
|
||||
) {
|
||||
return this.isOkrsEnabled;
|
||||
}
|
||||
return WORK_ITEM_ALLOWED_CHANGE_TYPE_MAP.includes(item.value);
|
||||
}),
|
||||
];
|
||||
},
|
||||
isOkrsEnabled() {
|
||||
return this.hasOkrsFeature && this.glFeatures.okrsMvc;
|
||||
},
|
||||
selectedWorkItemTypeWidgetDefinitions() {
|
||||
return this.getWidgetDefinitions(this.selectedWorkItemType?.text);
|
||||
},
|
||||
currentWorkItemTypeWidgetDefinitions() {
|
||||
return this.getWidgetDefinitions(this.workItemType);
|
||||
},
|
||||
widgetDifference() {
|
||||
return differenceBy(
|
||||
this.currentWorkItemTypeWidgetDefinitions,
|
||||
this.selectedWorkItemTypeWidgetDefinitions,
|
||||
'type',
|
||||
);
|
||||
},
|
||||
widgetsWithExistingData() {
|
||||
// Filter the widgets based on the presence or absence of data
|
||||
const widgetsWithExistingDataList = this.widgetDifference.filter((item) => {
|
||||
// Find the widget object
|
||||
const widgetObject = this.widgets?.find((widget) => widget.type === item.type);
|
||||
|
||||
// return false if the widget data is not found
|
||||
if (!widgetObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip the type and __typename fields
|
||||
// It will either have the actual widget object or none
|
||||
const fieldName = Object.keys(widgetObject).find(
|
||||
(key) => key !== 'type' && key !== '__typename',
|
||||
);
|
||||
|
||||
// return false if the field name is undefined
|
||||
if (!fieldName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the object has non-empty nodes array or
|
||||
// non-empty object
|
||||
return widgetObject[fieldName]?.nodes !== undefined
|
||||
? widgetObject[fieldName]?.nodes?.length > 0
|
||||
: Boolean(widgetObject[fieldName]);
|
||||
});
|
||||
|
||||
if (this.hasDesigns) {
|
||||
widgetsWithExistingDataList.push({
|
||||
name: WORK_ITEM_WIDGETS_NAME_MAP[WIDGET_TYPE_DESIGNS],
|
||||
type: WIDGET_TYPE_DESIGNS,
|
||||
});
|
||||
}
|
||||
|
||||
return widgetsWithExistingDataList.map((item) => ({
|
||||
...item,
|
||||
name: WORK_ITEM_WIDGETS_NAME_MAP[item.type],
|
||||
}));
|
||||
},
|
||||
hasWidgetDifference() {
|
||||
return this.widgetsWithExistingData.length > 0;
|
||||
},
|
||||
parentWorkItem() {
|
||||
return this.widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)?.parent;
|
||||
},
|
||||
parentWorkItemType() {
|
||||
return this.parentWorkItem?.workItemType?.name;
|
||||
},
|
||||
workItemTypeId() {
|
||||
return this.workItemTypes.find((type) => type.name === this.selectedWorkItemType.text).id;
|
||||
},
|
||||
selectedWorkItemTypeValue() {
|
||||
return this.selectedWorkItemType?.value || null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async changeType() {
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
workItemConvert: { errors },
|
||||
},
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: convertWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.workItemId,
|
||||
workItemTypeId: this.workItemTypeId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (errors.length > 0) {
|
||||
this.showErrorMessage(errors[0]);
|
||||
return;
|
||||
}
|
||||
this.$toast.show(s__('WorkItem|Type changed.'));
|
||||
this.$emit('workItemTypeChanged');
|
||||
this.hide();
|
||||
} catch (error) {
|
||||
this.showErrorMessage(error);
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
},
|
||||
getWidgetDefinitions(type) {
|
||||
if (!type) {
|
||||
return [];
|
||||
}
|
||||
return this.workItemTypes.find((widget) => widget.name === type)?.widgetDefinitions;
|
||||
},
|
||||
validateWorkItemType(value) {
|
||||
this.changeTypeDisabled = false;
|
||||
this.warningMessage = '';
|
||||
if (!value) {
|
||||
this.resetModal();
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedWorkItemType = this.allowedConversionWorkItemTypes.find(
|
||||
(item) => item.value === value,
|
||||
);
|
||||
|
||||
if (this.hasParent) {
|
||||
this.showWarningMessage(
|
||||
sprintfWorkItem(
|
||||
I18N_WORK_ITEM_CHANGE_TYPE_PARENT_ERROR,
|
||||
this.selectedWorkItemType.text,
|
||||
this.parentWorkItemType,
|
||||
),
|
||||
);
|
||||
this.changeTypeDisabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hasChildren) {
|
||||
const msg = sprintf(I18N_WORK_ITEM_CHANGE_TYPE_CHILD_ERROR, {
|
||||
workItemType: capitalizeFirstCharacter(
|
||||
this.selectedWorkItemType.text.toLocaleLowerCase(),
|
||||
),
|
||||
childItemType: this.allowedChildTypes?.[0]?.name?.toLocaleLowerCase(),
|
||||
});
|
||||
|
||||
this.showWarningMessage(msg);
|
||||
this.changeTypeDisabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Compare the widget definitions of both types
|
||||
if (this.hasWidgetDifference) {
|
||||
this.warningMessage = sprintfWorkItem(
|
||||
I18N_WORK_ITEM_CHANGE_TYPE_MISSING_FIELDS_ERROR,
|
||||
this.selectedWorkItemType.text,
|
||||
);
|
||||
}
|
||||
},
|
||||
showWarningMessage(message) {
|
||||
this.warningMessage = message;
|
||||
},
|
||||
showErrorMessage(message) {
|
||||
this.errorMessage = message;
|
||||
},
|
||||
show() {
|
||||
this.resetModal();
|
||||
this.changeTypeDisabled = true;
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
hide() {
|
||||
this.resetModal();
|
||||
this.$refs.modal.hide();
|
||||
},
|
||||
resetModal() {
|
||||
this.warningMessage = '';
|
||||
this.errorMessage = '';
|
||||
this.showDifferenceMessage = false;
|
||||
this.selectedWorkItemType = null;
|
||||
this.changeTypeDisabled = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-modal
|
||||
v-if="workItemId"
|
||||
ref="modal"
|
||||
modal-id="work-item-change-type"
|
||||
:title="s__('WorkItem|Change type')"
|
||||
size="sm"
|
||||
>
|
||||
<gl-alert
|
||||
v-if="errorMessage"
|
||||
data-testid="change-type-error-message"
|
||||
class="gl-mb-3"
|
||||
variant="danger"
|
||||
@dismiss="errorMessage = undefined"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</gl-alert>
|
||||
<div>
|
||||
<div class="gl-mb-4">{{ $options.i18n.subText }}</div>
|
||||
<gl-form-group :label="$options.i18n.type" label-for="work-item-type-select">
|
||||
<gl-form-select
|
||||
id="work-item-type-select"
|
||||
:value="selectedWorkItemTypeValue"
|
||||
width="md"
|
||||
:options="allowedConversionWorkItemTypes"
|
||||
@change="validateWorkItemType"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-alert
|
||||
v-if="warningMessage"
|
||||
data-testid="change-type-warning-message"
|
||||
variant="warning"
|
||||
:dismissible="false"
|
||||
>
|
||||
{{ warningMessage }}
|
||||
<ul v-if="hasWidgetDifference" class="gl-mb-0">
|
||||
<li v-for="widget in widgetsWithExistingData" :key="widget.type">
|
||||
{{ widget.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</gl-alert>
|
||||
</div>
|
||||
<template #modal-footer>
|
||||
<div class="gl-m-0 gl-flex gl-flex-row gl-flex-wrap gl-justify-end">
|
||||
<gl-button @click="hide">
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
<div class="gl-mr-3"></div>
|
||||
<gl-button
|
||||
:disabled="changeTypeDisabled"
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
data-testid="change-type-confirmation-button"
|
||||
@click="changeType"
|
||||
>{{ s__('WorkItem|Change type') }}</gl-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</gl-modal>
|
||||
</template>
|
||||
|
|
@ -311,7 +311,7 @@ export default {
|
|||
const { workItemType, parentWorkItem, hasSubepicsFeature } = this;
|
||||
|
||||
if (workItemType === WORK_ITEM_TYPE_VALUE_EPIC) {
|
||||
return hasSubepicsFeature && parentWorkItem;
|
||||
return Boolean(hasSubepicsFeature && parentWorkItem);
|
||||
}
|
||||
|
||||
return Boolean(parentWorkItem);
|
||||
|
|
@ -333,6 +333,9 @@ export default {
|
|||
parentWorkItemConfidentiality() {
|
||||
return this.parentWorkItem?.confidential;
|
||||
},
|
||||
parentWorkItemType() {
|
||||
return this.parentWorkItem?.workItemType?.name;
|
||||
},
|
||||
workItemIconName() {
|
||||
return this.workItem.workItemType?.iconName;
|
||||
},
|
||||
|
|
@ -433,6 +436,9 @@ export default {
|
|||
iid() {
|
||||
return this.workItemIid || this.workItem.iid;
|
||||
},
|
||||
widgets() {
|
||||
return this.workItem.widgets;
|
||||
},
|
||||
isItemSelected() {
|
||||
return !isEmpty(this.activeChildItem);
|
||||
},
|
||||
|
|
@ -448,7 +454,7 @@ export default {
|
|||
this.editMode = true;
|
||||
},
|
||||
isWidgetPresent(type) {
|
||||
return this.workItem.widgets?.find((widget) => widget.type === type);
|
||||
return this.widgets?.find((widget) => widget.type === type);
|
||||
},
|
||||
toggleConfidentiality(confidentialStatus) {
|
||||
this.updateInProgress = true;
|
||||
|
|
@ -652,6 +658,9 @@ export default {
|
|||
});
|
||||
cache.gc();
|
||||
},
|
||||
workItemTypeChanged() {
|
||||
this.$apollo.queries.workItem.refetch();
|
||||
},
|
||||
},
|
||||
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
|
||||
WORKSPACE_PROJECT,
|
||||
|
|
@ -674,12 +683,14 @@ export default {
|
|||
:work-item-notifications-subscribed="workItemNotificationsSubscribed"
|
||||
:work-item-author-id="workItemAuthorId"
|
||||
:is-group="isGroupWorkItem"
|
||||
:allowed-child-types="allowedChildTypes"
|
||||
@hideStickyHeader="hideStickyHeader"
|
||||
@showStickyHeader="showStickyHeader"
|
||||
@deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
|
||||
@toggleWorkItemConfidentiality="toggleConfidentiality"
|
||||
@error="updateError = $event"
|
||||
@promotedToObjective="$emit('promotedToObjective', iid)"
|
||||
@workItemTypeChanged="workItemTypeChanged"
|
||||
@toggleEditMode="enableEditMode"
|
||||
@workItemStateUpdated="$emit('workItemStateUpdated')"
|
||||
@toggleReportAbuseModal="toggleReportAbuseModal"
|
||||
|
|
@ -767,16 +778,21 @@ export default {
|
|||
:work-item-reference="workItem.reference"
|
||||
:work-item-create-note-email="workItem.createNoteEmail"
|
||||
:is-modal="isModal"
|
||||
:is-drawer="isDrawer"
|
||||
:work-item-state="workItem.state"
|
||||
:has-children="hasChildren"
|
||||
:has-parent="shouldShowAncestors"
|
||||
:work-item-author-id="workItemAuthorId"
|
||||
:can-create-related-item="workItemLinkedItems !== undefined"
|
||||
:is-group="isGroupWorkItem"
|
||||
:widgets="widgets"
|
||||
:allowed-child-types="allowedChildTypes"
|
||||
@deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
|
||||
@toggleWorkItemConfidentiality="toggleConfidentiality"
|
||||
@error="updateError = $event"
|
||||
@promotedToObjective="$emit('promotedToObjective', iid)"
|
||||
@workItemStateUpdated="$emit('workItemStateUpdated')"
|
||||
@workItemTypeChanged="workItemTypeChanged"
|
||||
@toggleReportAbuseModal="toggleReportAbuseModal"
|
||||
@workItemCreated="handleWorkItemCreated"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -75,6 +75,11 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
allowedChildTypes: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
canUpdate() {
|
||||
|
|
@ -101,6 +106,9 @@ export default {
|
|||
newTodoAndNotificationsEnabled() {
|
||||
return this.glFeatures.notificationsTodosButtons;
|
||||
},
|
||||
widgets() {
|
||||
return this.workItem.widgets;
|
||||
},
|
||||
},
|
||||
WORKSPACE_PROJECT,
|
||||
};
|
||||
|
|
@ -180,12 +188,15 @@ export default {
|
|||
:is-modal="isModal"
|
||||
:work-item-author-id="workItemAuthorId"
|
||||
:is-group="isGroup"
|
||||
:widgets="widgets"
|
||||
:allowed-child-types="allowedChildTypes"
|
||||
@deleteWorkItem="$emit('deleteWorkItem')"
|
||||
@toggleWorkItemConfidentiality="
|
||||
$emit('toggleWorkItemConfidentiality', !workItem.confidential)
|
||||
"
|
||||
@error="$emit('error')"
|
||||
@promotedToObjective="$emit('promotedToObjective')"
|
||||
@workItemTypeChanged="$emit('workItemTypeChanged')"
|
||||
@workItemStateUpdated="$emit('workItemStateUpdated')"
|
||||
@toggleReportAbuseModal="$emit('toggleReportAbuseModal', true)"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -143,6 +143,18 @@ export const I18N_MAX_WORK_ITEMS_NOTE_LABEL = sprintf(
|
|||
{ MAX_WORK_ITEMS },
|
||||
);
|
||||
|
||||
export const I18N_WORK_ITEM_CHANGE_TYPE_PARENT_ERROR = s__(
|
||||
'WorkItem|Parent item type %{parentWorkItemType} is not supported on %{workItemType}. Remove the parent item to change type.',
|
||||
);
|
||||
|
||||
export const I18N_WORK_ITEM_CHANGE_TYPE_CHILD_ERROR = s__(
|
||||
'WorkItem|%{workItemType} does not support the %{childItemType} child item types. Remove child items to change type.',
|
||||
);
|
||||
|
||||
export const I18N_WORK_ITEM_CHANGE_TYPE_MISSING_FIELDS_ERROR = s__(
|
||||
'WorkItem|Some fields are not present in %{workItemType}. If you change type now, this information will be lost.',
|
||||
);
|
||||
|
||||
export const sprintfWorkItem = (msg, workItemTypeArg, parentWorkItemType = '') => {
|
||||
const workItemType = workItemTypeArg || s__('WorkItem|item');
|
||||
return capitalizeFirstCharacter(
|
||||
|
|
@ -268,6 +280,7 @@ export const TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION = 'confidentiality-toggle-act
|
|||
export const TEST_ID_NOTIFICATIONS_TOGGLE_FORM = 'notifications-toggle-form';
|
||||
export const TEST_ID_DELETE_ACTION = 'delete-action';
|
||||
export const TEST_ID_PROMOTE_ACTION = 'promote-action';
|
||||
export const TEST_ID_CHANGE_TYPE_ACTION = 'change-type-action';
|
||||
export const TEST_ID_LOCK_ACTION = 'lock-action';
|
||||
export const TEST_ID_COPY_REFERENCE_ACTION = 'copy-reference-action';
|
||||
export const TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION = 'copy-create-note-email-action';
|
||||
|
|
@ -386,3 +399,35 @@ export const INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION = Symbol(
|
|||
|
||||
export const WORK_ITEM_CREATE_ENTITY_MODAL_TARGET_SOURCE = 'source';
|
||||
export const WORK_ITEM_CREATE_ENTITY_MODAL_TARGET_BRANCH = 'branch';
|
||||
|
||||
export const WORK_ITEM_ALLOWED_CHANGE_TYPE_MAP = [
|
||||
WORK_ITEM_TYPE_ENUM_KEY_RESULT,
|
||||
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
|
||||
WORK_ITEM_TYPE_ENUM_TASK,
|
||||
WORK_ITEM_TYPE_ENUM_ISSUE,
|
||||
];
|
||||
|
||||
export const WORK_ITEM_WIDGETS_NAME_MAP = {
|
||||
[WIDGET_TYPE_ASSIGNEES]: s__('WorkItem|Assignees'),
|
||||
[WIDGET_TYPE_DESCRIPTION]: s__('WorkItem|Description'),
|
||||
[WIDGET_TYPE_AWARD_EMOJI]: s__('WorkItem|Emoji reactions'),
|
||||
[WIDGET_TYPE_NOTIFICATIONS]: s__('WorkItem|Notifications'),
|
||||
[WIDGET_TYPE_CURRENT_USER_TODOS]: s__('WorkItem|To-do item'),
|
||||
[WIDGET_TYPE_LABELS]: s__('WorkItem|Labels'),
|
||||
[WIDGET_TYPE_START_AND_DUE_DATE]: s__('WorkItem|Dates'),
|
||||
[WIDGET_TYPE_TIME_TRACKING]: s__('WorkItem|Time tracking'),
|
||||
[WIDGET_TYPE_WEIGHT]: s__('WorkItem|Weight'),
|
||||
[WIDGET_TYPE_PARTICIPANTS]: s__('WorkItem|Participants'),
|
||||
[WIDGET_TYPE_EMAIL_PARTICIPANTS]: s__('WorkItem|Email participants'),
|
||||
[WIDGET_TYPE_PROGRESS]: s__('WorkItem|Progress'),
|
||||
[WIDGET_TYPE_HIERARCHY]: s__('WorkItem|Child items'),
|
||||
[WIDGET_TYPE_MILESTONE]: s__('WorkItem|Milestone'),
|
||||
[WIDGET_TYPE_ITERATION]: s__('WorkItem|Iteration'),
|
||||
[WIDGET_TYPE_NOTES]: s__('WorkItem|Comments and threads'),
|
||||
[WIDGET_TYPE_HEALTH_STATUS]: s__('WorkItem|Health status'),
|
||||
[WIDGET_TYPE_LINKED_ITEMS]: s__('WorkItem|Linked items'),
|
||||
[WIDGET_TYPE_COLOR]: s__('WorkItem|Color'),
|
||||
[WIDGET_TYPE_DESIGNS]: s__('WorkItem|Designs'),
|
||||
[WIDGET_TYPE_DEVELOPMENT]: s__('WorkItem|Development'),
|
||||
[WIDGET_TYPE_CRM_CONTACTS]: s__('WorkItem|Contacts'),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,12 +16,7 @@ module Packages
|
|||
def execute
|
||||
return ::Packages::Package.none unless params[:package_name].present?
|
||||
|
||||
packages = if Feature.enabled?(:npm_extract_npm_package_model, Feature.current_request)
|
||||
base.installable
|
||||
else
|
||||
base.npm.installable
|
||||
end
|
||||
|
||||
packages = base.installable
|
||||
packages = filter_by_exact_package_name(packages)
|
||||
filter_by_package_version(packages)
|
||||
end
|
||||
|
|
@ -36,30 +31,16 @@ module Packages
|
|||
elsif namespace
|
||||
packages_for_namespace
|
||||
else
|
||||
packages_class.none
|
||||
::Packages::Npm::Package.none
|
||||
end
|
||||
end
|
||||
|
||||
def packages_for_project
|
||||
if Feature.enabled?(:npm_extract_npm_package_model, Feature.current_request)
|
||||
::Packages::Npm::Package.for_projects(project)
|
||||
else
|
||||
project.packages
|
||||
end
|
||||
::Packages::Npm::Package.for_projects(project)
|
||||
end
|
||||
|
||||
def packages_for_namespace
|
||||
packages_class.for_projects(namespace.all_projects)
|
||||
end
|
||||
|
||||
# TODO: Use the class directly with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
def packages_class
|
||||
if Feature.enabled?(:npm_extract_npm_package_model, Feature.current_request)
|
||||
::Packages::Npm::Package
|
||||
else
|
||||
::Packages::Package
|
||||
end
|
||||
::Packages::Npm::Package.for_projects(namespace.all_projects)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,12 +12,7 @@ module Packages
|
|||
private
|
||||
|
||||
def packages
|
||||
if Feature.enabled?(:npm_extract_npm_package_model, Feature.current_request)
|
||||
base.with_name(@params[:package_name])
|
||||
else
|
||||
base.npm
|
||||
.with_name(@params[:package_name])
|
||||
end
|
||||
base.with_name(@params[:package_name])
|
||||
end
|
||||
|
||||
override :group_packages
|
||||
|
|
@ -27,11 +22,7 @@ module Packages
|
|||
|
||||
override :packages_class
|
||||
def packages_class
|
||||
if Feature.enabled?(:npm_extract_npm_package_model, Feature.current_request)
|
||||
::Packages::Npm::Package
|
||||
else
|
||||
super
|
||||
end
|
||||
::Packages::Npm::Package
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,40 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Packages::Npm::Metadatum < ApplicationRecord
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
MAX_PACKAGE_JSON_SIZE = 20_000
|
||||
MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING = 5_000
|
||||
NUM_FIELDS_FOR_ERROR_TRACKING = 5
|
||||
|
||||
belongs_to :package, class_name: 'Packages::Npm::Package', inverse_of: :npm_metadatum
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
belongs_to :legacy_package, -> {
|
||||
where(package_type: :npm)
|
||||
}, inverse_of: :npm_metadatum, class_name: 'Packages::Package', foreign_key: :package_id
|
||||
validates :package, presence: true
|
||||
|
||||
validates :package, presence: true, if: -> { npm_extract_npm_package_model_enabled? }
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
validates :legacy_package, presence: true, unless: -> { npm_extract_npm_package_model_enabled? }
|
||||
# From https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
|
||||
validates :package_json, json_schema: { filename: "npm_package_json" }
|
||||
validate :ensure_npm_package_type, unless: -> { npm_extract_npm_package_model_enabled? }
|
||||
validate :ensure_package_json_size
|
||||
|
||||
scope :package_id_in, ->(package_ids) { where(package_id: package_ids) }
|
||||
|
||||
private
|
||||
|
||||
def ensure_npm_package_type
|
||||
return if legacy_package&.npm?
|
||||
|
||||
errors.add(:base, _('Package type must be NPM'))
|
||||
end
|
||||
|
||||
def ensure_package_json_size
|
||||
return if package_json.to_s.size < MAX_PACKAGE_JSON_SIZE
|
||||
|
||||
|
|
@ -45,9 +27,4 @@ class Packages::Npm::Metadatum < ApplicationRecord
|
|||
)
|
||||
)
|
||||
end
|
||||
|
||||
def npm_extract_npm_package_model_enabled?
|
||||
Feature.enabled?(:npm_extract_npm_package_model, Feature.current_request)
|
||||
end
|
||||
strong_memoize_attr :npm_extract_npm_package_model_enabled?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -44,9 +44,6 @@ class Packages::Package < ApplicationRecord
|
|||
has_many :tags, inverse_of: :package, class_name: 'Packages::Tag'
|
||||
|
||||
has_one :maven_metadatum, inverse_of: :package, class_name: 'Packages::Maven::Metadatum'
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
has_one :npm_metadatum, inverse_of: :package, class_name: 'Packages::Npm::Metadatum'
|
||||
|
||||
has_many :build_infos, inverse_of: :package
|
||||
has_many :pipelines, through: :build_infos, disable_joins: true
|
||||
|
|
@ -67,21 +64,8 @@ class Packages::Package < ApplicationRecord
|
|||
},
|
||||
unless: -> { pending_destruction? || conan? }
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
validate :npm_package_already_taken, if: :npm?
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
validates :name, format: { with: Gitlab::Regex.npm_package_name_regex, message: Gitlab::Regex.npm_package_name_regex_message }, if: :npm?
|
||||
|
||||
validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? }
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
validates :version, format: { with: Gitlab::Regex.semver_regex, message: Gitlab::Regex.semver_regex_message },
|
||||
if: -> { npm? }
|
||||
|
||||
scope :for_projects, ->(project_ids) { where(project_id: project_ids) }
|
||||
scope :with_name, ->(name) { where(name: name) }
|
||||
scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) }
|
||||
|
|
@ -104,17 +88,6 @@ class Packages::Package < ApplicationRecord
|
|||
scope :including_project_namespace_route, -> { includes(project: { namespace: :route }) }
|
||||
scope :including_tags, -> { includes(:tags) }
|
||||
scope :including_dependency_links, -> { includes(dependency_links: :dependency) }
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
scope :preload_npm_metadatum, -> { preload(:npm_metadatum) }
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
scope :with_npm_scope, ->(scope) do
|
||||
npm.where("position('/' in packages_packages.name) > 0 AND split_part(packages_packages.name, '/', 1) = :package_scope", package_scope: "@#{sanitize_sql_like(scope)}")
|
||||
end
|
||||
|
||||
scope :has_version, -> { where.not(version: nil) }
|
||||
scope :preload_files, -> { preload(:installable_package_files) }
|
||||
scope :preload_pipelines, -> { preload(pipelines: :user) }
|
||||
|
|
@ -158,7 +131,7 @@ class Packages::Package < ApplicationRecord
|
|||
def self.inheritance_column = 'package_type'
|
||||
|
||||
def self.inheritance_column_to_class_map
|
||||
hash = {
|
||||
{
|
||||
ml_model: 'Packages::MlModel::Package',
|
||||
golang: 'Packages::Go::Package',
|
||||
rubygems: 'Packages::Rubygems::Package',
|
||||
|
|
@ -170,14 +143,9 @@ class Packages::Package < ApplicationRecord
|
|||
generic: 'Packages::Generic::Package',
|
||||
pypi: 'Packages::Pypi::Package',
|
||||
terraform_module: 'Packages::TerraformModule::Package',
|
||||
nuget: 'Packages::Nuget::Package'
|
||||
}
|
||||
|
||||
if Feature.enabled?(:npm_extract_npm_package_model, Feature.current_request)
|
||||
hash[:npm] = 'Packages::Npm::Package'
|
||||
end
|
||||
|
||||
hash
|
||||
nuget: 'Packages::Nuget::Package',
|
||||
npm: 'Packages::Npm::Package'
|
||||
}.freeze
|
||||
end
|
||||
|
||||
def self.only_maven_packages_with_path(path, use_cte: false)
|
||||
|
|
@ -278,14 +246,6 @@ class Packages::Package < ApplicationRecord
|
|||
::Packages::Maven::Metadata::SyncWorker.perform_async(user.id, project_id, name)
|
||||
end
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
def sync_npm_metadata_cache
|
||||
return unless npm?
|
||||
|
||||
::Packages::Npm::CreateMetadataCacheWorker.perform_async(project_id, name)
|
||||
end
|
||||
|
||||
def create_build_infos!(build)
|
||||
return unless build&.pipeline
|
||||
|
||||
|
|
@ -328,24 +288,4 @@ class Packages::Package < ApplicationRecord
|
|||
|
||||
connection.execute("SELECT pg_advisory_xact_lock(#{lock_expression})")
|
||||
end
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
def npm_package_already_taken
|
||||
return unless project
|
||||
return unless follows_npm_naming_convention?
|
||||
|
||||
if project.package_already_taken?(name, version, package_type: :npm)
|
||||
errors.add(:base, _('Package already exists'))
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
# https://docs.gitlab.com/ee/user/packages/npm_registry/#package-naming-convention
|
||||
def follows_npm_naming_convention?
|
||||
return false unless project&.root_namespace&.path
|
||||
|
||||
project.root_namespace.path == ::Packages::Npm.scope_of(name)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3112,14 +3112,6 @@ class Project < ApplicationRecord
|
|||
).exists?
|
||||
end
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
def has_namespaced_npm_packages?
|
||||
packages.with_npm_scope(root_namespace.path)
|
||||
.not_pending_destruction
|
||||
.exists?
|
||||
end
|
||||
|
||||
def default_branch_or_main
|
||||
return default_branch if default_branch
|
||||
|
||||
|
|
|
|||
|
|
@ -170,22 +170,22 @@ class WikiPage
|
|||
end
|
||||
|
||||
def insert_slugs(strings, is_new, canonical_slug)
|
||||
creation = Time.current.utc
|
||||
created_at = Time.current.utc
|
||||
|
||||
slug_attrs = strings.map do |slug|
|
||||
slug_attributes(slug, canonical_slug, is_new, creation)
|
||||
slug_attributes(slug, canonical_slug, is_new, created_at)
|
||||
end
|
||||
slugs.insert_all(slug_attrs) unless !is_new && slug_attrs.size == 1
|
||||
|
||||
@canonical_slug = canonical_slug if is_new || strings.size == 1
|
||||
end
|
||||
|
||||
def slug_attributes(slug, canonical_slug, is_new, creation)
|
||||
def slug_attributes(slug, canonical_slug, is_new, created_at)
|
||||
{
|
||||
slug: slug,
|
||||
canonical: is_new && slug == canonical_slug,
|
||||
created_at: creation,
|
||||
updated_at: creation
|
||||
created_at: created_at,
|
||||
updated_at: created_at
|
||||
}.merge(slug_meta_attributes)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ module Git
|
|||
push_changes.take(MAX_CHANGES).each do |change| # rubocop:disable CodeReuse/ActiveRecord
|
||||
next unless change.page.present?
|
||||
|
||||
response = create_event_for(change)
|
||||
wiki_page_meta = WikiPage::Meta.find_or_create(change.last_known_slug, change.page)
|
||||
response = create_event_for(change, wiki_page_meta)
|
||||
log_error(response.message) if response.error?
|
||||
end
|
||||
end
|
||||
|
|
@ -50,10 +51,9 @@ module Git
|
|||
wiki.repository.raw.raw_changes_between(change[:oldrev], change[:newrev])
|
||||
end
|
||||
|
||||
def create_event_for(change)
|
||||
def create_event_for(change, wiki_page_meta)
|
||||
event_service.execute(
|
||||
change.last_known_slug,
|
||||
change.page,
|
||||
wiki_page_meta,
|
||||
change.event_action,
|
||||
change.sha
|
||||
)
|
||||
|
|
|
|||
|
|
@ -123,15 +123,9 @@ module Groups
|
|||
def group_with_namespaced_npm_packages?
|
||||
return false unless group.packages_feature_enabled?
|
||||
|
||||
npm_packages = if Feature.enabled?(:npm_extract_npm_package_model, Feature.current_request)
|
||||
::Packages::GroupPackagesFinder
|
||||
.new(current_user, group, packages_class: ::Packages::Npm::Package, preload_pipelines: false)
|
||||
.execute
|
||||
else
|
||||
::Packages::GroupPackagesFinder
|
||||
.new(current_user, group, package_type: :npm, preload_pipelines: false)
|
||||
.execute
|
||||
end
|
||||
npm_packages = ::Packages::GroupPackagesFinder
|
||||
.new(current_user, group, packages_class: ::Packages::Npm::Package, preload_pipelines: false)
|
||||
.execute
|
||||
|
||||
npm_packages = npm_packages.with_npm_scope(group.root_ancestor.path)
|
||||
|
||||
|
|
|
|||
|
|
@ -78,17 +78,10 @@ module Groups
|
|||
|
||||
# we have a path change on a root group:
|
||||
# check that we don't have any npm package with a scope set to the group path
|
||||
npm_packages = if Feature.enabled?(:npm_extract_npm_package_model, Feature.current_request)
|
||||
::Packages::GroupPackagesFinder
|
||||
.new(current_user, group, packages_class: ::Packages::Npm::Package, preload_pipelines: false)
|
||||
.execute
|
||||
.with_npm_scope(group.path)
|
||||
else
|
||||
::Packages::GroupPackagesFinder
|
||||
.new(current_user, group, package_type: :npm, preload_pipelines: false)
|
||||
.execute
|
||||
.with_npm_scope(group.path)
|
||||
end
|
||||
npm_packages = ::Packages::GroupPackagesFinder
|
||||
.new(current_user, group, packages_class: ::Packages::Npm::Package, preload_pipelines: false)
|
||||
.execute
|
||||
.with_npm_scope(group.path)
|
||||
|
||||
return true unless npm_packages.exists?
|
||||
|
||||
|
|
|
|||
|
|
@ -51,16 +51,16 @@ module LooseForeignKeys
|
|||
handle_over_limit
|
||||
break
|
||||
end
|
||||
|
||||
break if modification_tracker.over_limit?
|
||||
|
||||
# At this point, all associations are cleaned up, we can update the status of the parent records
|
||||
update_count = Gitlab::Database::SharedModel.using_connection(connection) do
|
||||
LooseForeignKeys::DeletedRecord.mark_records_processed(deleted_parent_records)
|
||||
end
|
||||
|
||||
deleted_records_counter.increment({ table: parent_table, db_config_name: db_config_name }, update_count)
|
||||
end
|
||||
|
||||
return if modification_tracker.over_limit?
|
||||
|
||||
# At this point, all associations are cleaned up, we can update the status of the parent records
|
||||
update_count = Gitlab::Database::SharedModel.using_connection(connection) do
|
||||
LooseForeignKeys::DeletedRecord.mark_records_processed(deleted_parent_records)
|
||||
end
|
||||
|
||||
deleted_records_counter.increment({ table: parent_table, db_config_name: db_config_name }, update_count)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -67,20 +67,11 @@ module Packages
|
|||
end
|
||||
|
||||
def current_package_exists?
|
||||
if Feature.enabled?(:npm_extract_npm_package_model, Feature.current_request)
|
||||
::Packages::Npm::Package.for_projects(project)
|
||||
.with_name(name)
|
||||
.with_version(version)
|
||||
.not_pending_destruction
|
||||
.exists?
|
||||
else
|
||||
project.packages
|
||||
.npm
|
||||
.with_name(name)
|
||||
.with_version(version)
|
||||
.not_pending_destruction
|
||||
.exists?
|
||||
end
|
||||
::Packages::Npm::Package.for_projects(project)
|
||||
.with_name(name)
|
||||
.with_version(version)
|
||||
.not_pending_destruction
|
||||
.exists?
|
||||
end
|
||||
|
||||
def current_package_protected?
|
||||
|
|
|
|||
|
|
@ -34,10 +34,8 @@ module Packages
|
|||
|
||||
attr_reader :package_file
|
||||
|
||||
# TODO: Remove `package&.npm?` with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
def valid_package_file?
|
||||
package_file && !package_file.file.empty_size? && package&.npm? && package&.processing?
|
||||
package_file && !package_file.file.empty_size? && package&.processing?
|
||||
end
|
||||
|
||||
def with_package_json_entry
|
||||
|
|
|
|||
|
|
@ -333,14 +333,10 @@ module Projects
|
|||
end
|
||||
|
||||
def project_has_namespaced_npm_packages?
|
||||
if Feature.enabled?(:npm_extract_npm_package_model, Feature.current_request)
|
||||
::Packages::Npm::Package.for_projects(project)
|
||||
.with_npm_scope(project.root_namespace.path)
|
||||
.not_pending_destruction
|
||||
.exists?
|
||||
else
|
||||
project.has_namespaced_npm_packages?
|
||||
end
|
||||
::Packages::Npm::Package.for_projects(project)
|
||||
.with_npm_scope(project.root_namespace.path)
|
||||
.not_pending_destruction
|
||||
.exists?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ module WikiPages
|
|||
container.execute_hooks(page_data, :wiki_page_hooks)
|
||||
container.execute_integrations(page_data, :wiki_page_hooks)
|
||||
increment_usage(page)
|
||||
create_wiki_event(page)
|
||||
wiki_page_meta = wiki_page_meta_for(page)
|
||||
create_wiki_event(wiki_page_meta, page)
|
||||
end
|
||||
|
||||
# Passed to web-hooks, and send to external consumers.
|
||||
|
|
@ -57,10 +58,14 @@ module WikiPages
|
|||
)
|
||||
end
|
||||
|
||||
def create_wiki_event(page)
|
||||
def wiki_page_meta_for(page)
|
||||
WikiPage::Meta.find_or_create(slug_for_page(page), page)
|
||||
end
|
||||
|
||||
def create_wiki_event(wiki_page_meta, page)
|
||||
response = WikiPages::EventCreateService
|
||||
.new(current_user)
|
||||
.execute(slug_for_page(page), page, event_action, fingerprint(page))
|
||||
.execute(wiki_page_meta, event_action, fingerprint(page))
|
||||
|
||||
log_error(response.message) if response.error?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,9 +9,7 @@ module WikiPages
|
|||
@author = author
|
||||
end
|
||||
|
||||
def execute(slug, page, action, event_fingerprint)
|
||||
wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)
|
||||
|
||||
def execute(wiki_page_meta, action, event_fingerprint)
|
||||
event = ::EventCreateService.new.wiki_event(wiki_page_meta, author, action, event_fingerprint)
|
||||
|
||||
ServiceResponse.success(payload: { event: event })
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: npm_extract_npm_package_model
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/435823
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171285
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
milestone: '17.6'
|
||||
group: group::package registry
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -163,11 +163,12 @@ The code flow information is shown the **Code flow** tab and includes:
|
|||
The code flow view is integrated into each view where vulnerability details are shown.
|
||||
On GitLab self-managed, you can activate the view by [enabling the required feature flags](../../../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags) starting in the minimum version shown.
|
||||
|
||||
| Location | Availability on GitLab.com | Availability on GitLab self-managed | Feature flags required |
|
||||
|-----------------------------------------------------------------|-----------------------------------|-----------------------------------------------------------------------|-----------------------------------------------------------------------|
|
||||
| [Vulnerability Report](../vulnerability_report/index.md) | Enabled by default in GitLab 17.3 | Enabled by default in GitLab 17.6. Available in GitLab 17.3 or later. | `vulnerability_code_flow` |
|
||||
| [Merge request widget](index.md#merge-request-widget) | Enabled by default in GitLab 17.6 | Enabled by default in GitLab 17.6. Available in GitLab 17.5 or later. | Both `vulnerability_code_flow` and `pipeline_vulnerability_code_flow` |
|
||||
| [Pipeline security report](../vulnerability_report/pipeline.md) | Enabled by default in GitLab 17.6 | Enabled by default in GitLab 17.6. Available in GitLab 17.5 or later. | Both `vulnerability_code_flow` and `pipeline_vulnerability_code_flow` |
|
||||
| Location | Availability on GitLab.com | Availability on GitLab self-managed | Feature flags required |
|
||||
|-------------------------------------------------------------------|-----------------------------------|-----------------------------------------------------------------------|-----------------------------------------------------------------------|
|
||||
| [Vulnerability Report](../vulnerability_report/index.md) | Enabled by default in GitLab 17.3 | Enabled by default in GitLab 17.6. Available in GitLab 17.3 or later. | `vulnerability_code_flow` |
|
||||
| [Merge request widget](index.md#merge-request-widget) | Enabled by default in GitLab 17.6 | Enabled by default in GitLab 17.6. Available in GitLab 17.5 or later. | Both `vulnerability_code_flow` and `pipeline_vulnerability_code_flow` |
|
||||
| [Pipeline security report](../vulnerability_report/pipeline.md) | Enabled by default in GitLab 17.6 | Enabled by default in GitLab 17.6. Available in GitLab 17.5 or later. | Both `vulnerability_code_flow` and `pipeline_vulnerability_code_flow` |
|
||||
| [Merge request changes view](index.md#merge-request-changes-view) | Enabled by default in GitLab 17.7 | Enabled by default in GitLab 17.7. Available in GitLab 17.7 or later. | Both `vulnerability_code_flow` and `mr_vulnerability_code_flow` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
|
|||
|
|
@ -64,14 +64,9 @@ module API
|
|||
get '*package_name/-/*file_name', format: false do
|
||||
authorize_read_package!(project)
|
||||
|
||||
package = if Feature.enabled?(:npm_extract_npm_package_model, Feature.current_request)
|
||||
::Packages::Npm::Package
|
||||
.for_projects(project)
|
||||
.by_name_and_file_name(params[:package_name], params[:file_name])
|
||||
else
|
||||
project.packages.npm
|
||||
.by_name_and_file_name(params[:package_name], params[:file_name])
|
||||
end
|
||||
package = ::Packages::Npm::Package
|
||||
.for_projects(project)
|
||||
.by_name_and_file_name(params[:package_name], params[:file_name])
|
||||
|
||||
not_found!('Package') unless package
|
||||
|
||||
|
|
|
|||
|
|
@ -39194,9 +39194,6 @@ msgstr ""
|
|||
msgid "Package type must be Maven"
|
||||
msgstr ""
|
||||
|
||||
msgid "Package type must be NPM"
|
||||
msgstr ""
|
||||
|
||||
msgid "Package type must be NuGet"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63139,6 +63136,9 @@ msgstr ""
|
|||
msgid "WorkItem|%{workItemType} deleted"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|%{workItemType} does not support the %{childItemType} child item types. Remove child items to change type."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|%{workItemType} has 1 linked item"
|
||||
msgid_plural "WorkItem|%{workItemType} has %{itemCount} linked items"
|
||||
msgstr[0] ""
|
||||
|
|
@ -63211,6 +63211,9 @@ msgstr ""
|
|||
msgid "WorkItem|Are you sure you want to delete the %{workItemType}? This action cannot be reversed."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Assignees"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Blocked by"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63223,6 +63226,9 @@ msgstr ""
|
|||
msgid "WorkItem|Cancel"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Change type"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Child items"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63250,12 +63256,18 @@ msgstr ""
|
|||
msgid "WorkItem|Coffee"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Color"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Comment & close %{workItemType}"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Comment & reopen %{workItemType}"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Comments and threads"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Comments only"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63268,6 +63280,9 @@ msgstr ""
|
|||
msgid "WorkItem|Configure work items such as epics, issues, and tasks to represent how your team works."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Contacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Convert to child item"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63304,6 +63319,12 @@ msgstr ""
|
|||
msgid "WorkItem|Delete this %{workItemType} and release all child items? This action cannot be reversed."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Description"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Designs"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Development"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63313,6 +63334,12 @@ msgstr ""
|
|||
msgid "WorkItem|Due"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Email participants"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Emoji reactions"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Epic"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63364,6 +63391,9 @@ msgstr ""
|
|||
msgid "WorkItem|Key result"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Labels"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Last updated %{timeago}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63481,6 +63511,12 @@ msgstr ""
|
|||
msgid "WorkItem|Parent"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Parent item type %{parentWorkItemType} is not supported on %{workItemType}. Remove the parent item to change type."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Participants"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Pink"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63532,6 +63568,9 @@ msgstr ""
|
|||
msgid "WorkItem|Select type"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Select which type you would like to change this item to."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Show all ancestors"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63544,6 +63583,9 @@ msgstr ""
|
|||
msgid "WorkItem|Single select"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Some fields are not present in %{workItemType}. If you change type now, this information will be lost."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Someone edited the description at the same time you did. If you save it will overwrite their changes. Please confirm you'd like to save your edits."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63679,6 +63721,12 @@ msgstr ""
|
|||
msgid "WorkItem|This work item is not available. It either doesn't exist or you don't have permission to view it."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Time tracking"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|To-do item"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Toggle details"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63691,6 +63739,9 @@ msgstr ""
|
|||
msgid "WorkItem|Type"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Type changed."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Undo"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -63706,6 +63757,9 @@ msgstr ""
|
|||
msgid "WorkItem|View on a roadmap"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Weight"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Work item"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,10 @@ FactoryBot.define do
|
|||
factory :npm_metadatum, class: 'Packages::Npm::Metadatum' do
|
||||
package { association(:npm_package) }
|
||||
|
||||
# TODO: Remove `legacy_package) with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
package_json do
|
||||
{
|
||||
name: (package || legacy_package).name,
|
||||
version: (package || legacy_package).version,
|
||||
name: package.name,
|
||||
version: package.version,
|
||||
dist: {
|
||||
tarball: 'http://localhost/tarball.tgz',
|
||||
shasum: '1234567890'
|
||||
|
|
|
|||
|
|
@ -117,43 +117,6 @@ RSpec.describe ::Packages::Npm::PackageFinder, feature_category: :package_regist
|
|||
it_behaves_like 'avoids N+1 database queries in the package registry'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
let_it_be_with_refind(:package) do
|
||||
create(:npm_package_legacy, project: project, name: "#{FFaker::Lorem.word}-#{SecureRandom.hex(4)}")
|
||||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
context 'with a project' do
|
||||
let(:finder) { described_class.new(project: project, params: params) }
|
||||
|
||||
it_behaves_like 'finding packages by name'
|
||||
it_behaves_like 'avoids N+1 database queries in the package registry', :npm_package_legacy
|
||||
|
||||
context 'set to nil' do
|
||||
let(:project) { nil }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a namespace' do
|
||||
let(:finder) { described_class.new(namespace: namespace, params: params) }
|
||||
|
||||
it_behaves_like 'accepting a namespace for', 'finding packages by name', :npm_package_legacy
|
||||
|
||||
context 'set to nil' do
|
||||
let_it_be(:namespace) { nil }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
|
||||
it_behaves_like 'avoids N+1 database queries in the package registry', :npm_package_legacy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#last' do
|
||||
|
|
@ -191,42 +154,5 @@ RSpec.describe ::Packages::Npm::PackageFinder, feature_category: :package_regist
|
|||
it { is_expected.to eq(package2) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
let_it_be_with_refind(:package) do
|
||||
create(:npm_package_legacy, project: project, name: "#{FFaker::Lorem.word}-#{SecureRandom.hex(4)}")
|
||||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
context 'with a project' do
|
||||
let(:finder) { described_class.new(project: project, params: params) }
|
||||
|
||||
it_behaves_like 'finding package by last'
|
||||
end
|
||||
|
||||
context 'with a namespace' do
|
||||
let(:finder) { described_class.new(namespace: namespace, params: params) }
|
||||
|
||||
it_behaves_like 'accepting a namespace for', 'finding package by last', :npm_package_legacy
|
||||
|
||||
context 'with duplicate packages' do
|
||||
let_it_be(:namespace) { create(:group) }
|
||||
let_it_be(:subgroup1) { create(:group, parent: namespace) }
|
||||
let_it_be(:subgroup2) { create(:group, parent: namespace) }
|
||||
let_it_be(:project2) { create(:project, namespace: subgroup2) }
|
||||
let_it_be(:package2) { create(:npm_package_legacy, name: package.name, project: project2) }
|
||||
|
||||
before do
|
||||
project.update!(namespace: subgroup1)
|
||||
end
|
||||
|
||||
# the most recent one is returned
|
||||
it { is_expected.to eq(package2) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -57,56 +57,5 @@ RSpec.describe ::Packages::Npm::PackagesForUserFinder, feature_category: :packag
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
let_it_be(:package_name) { "#{FFaker::Lorem.word}-#{SecureRandom.hex(4)}" }
|
||||
let_it_be(:package) { create(:npm_package_legacy, project: project, name: package_name) }
|
||||
|
||||
let_it_be(:package_with_diff_name) do
|
||||
create(:npm_package_legacy, project: project, name: "#{FFaker::Lorem.word}-#{SecureRandom.hex(4)}")
|
||||
end
|
||||
|
||||
let_it_be(:package_with_diff_project) { create(:npm_package_legacy, name: package_name, project: project2) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
context 'with a project' do
|
||||
let(:project_or_group) { project }
|
||||
|
||||
it_behaves_like 'searches for packages'
|
||||
it_behaves_like 'avoids N+1 database queries in the package registry', :npm_package_legacy
|
||||
end
|
||||
|
||||
context 'with a group' do
|
||||
let(:project_or_group) { group }
|
||||
|
||||
before_all do
|
||||
project.add_reporter(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'searches for packages'
|
||||
it_behaves_like 'avoids N+1 database queries in the package registry', :npm_package_legacy
|
||||
|
||||
context 'when an user is a reporter of both projects' do
|
||||
before_all do
|
||||
project2.add_reporter(user)
|
||||
end
|
||||
|
||||
it { is_expected.to contain_exactly(package, package_with_diff_project) }
|
||||
|
||||
context 'when the second project has the package registry disabled' do
|
||||
before_all do
|
||||
project.reload.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
|
||||
project2.reload.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC,
|
||||
package_registry_access_level: 'disabled', packages_enabled: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'searches for packages'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -79,7 +79,19 @@ describeSkipVue3(skipReason, () => {
|
|||
});
|
||||
|
||||
it('search is hidden', () => {
|
||||
expect(findSearchableTable().props('showSearch')).toBe(false);
|
||||
expect(findSearchableTable().props()).toMatchObject({
|
||||
showSearch: false,
|
||||
sortableFields: [
|
||||
{
|
||||
label: 'Version',
|
||||
orderBy: 'version',
|
||||
},
|
||||
{
|
||||
label: 'Created',
|
||||
orderBy: 'created_at',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import WorkItemActions from '~/work_items/components/work_item_actions.vue';
|
|||
import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue';
|
||||
import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
|
||||
import CreateWorkItemModal from '~/work_items/components/create_work_item_modal.vue';
|
||||
import WorkItemChangeTypeModal from '~/work_items/components/work_item_change_type_modal.vue';
|
||||
import {
|
||||
STATE_OPEN,
|
||||
TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
|
||||
|
|
@ -25,6 +26,7 @@ import {
|
|||
TEST_ID_LOCK_ACTION,
|
||||
TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
|
||||
TEST_ID_PROMOTE_ACTION,
|
||||
TEST_ID_CHANGE_TYPE_ACTION,
|
||||
TEST_ID_TOGGLE_ACTION,
|
||||
TEST_ID_REPORT_ABUSE,
|
||||
TEST_ID_NEW_RELATED_WORK_ITEM,
|
||||
|
|
@ -64,8 +66,10 @@ describe('WorkItemActions component', () => {
|
|||
wrapper.findByTestId(TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION);
|
||||
const findReportAbuseButton = () => wrapper.findByTestId(TEST_ID_REPORT_ABUSE);
|
||||
const findNewRelatedItemButton = () => wrapper.findByTestId(TEST_ID_NEW_RELATED_WORK_ITEM);
|
||||
const findChangeTypeButton = () => wrapper.findByTestId(TEST_ID_CHANGE_TYPE_ACTION);
|
||||
const findReportAbuseModal = () => wrapper.findComponent(WorkItemAbuseModal);
|
||||
const findCreateWorkItemModal = () => wrapper.findComponent(CreateWorkItemModal);
|
||||
const findWorkItemChangeTypeModal = () => wrapper.findComponent(WorkItemChangeTypeModal);
|
||||
const findMoreDropdown = () => wrapper.findByTestId('work-item-actions-dropdown');
|
||||
const findMoreDropdownTooltip = () => getBinding(findMoreDropdown().element, 'gl-tooltip');
|
||||
const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *');
|
||||
|
|
@ -124,6 +128,8 @@ describe('WorkItemActions component', () => {
|
|||
hideSubscribe = undefined,
|
||||
hasChildren = false,
|
||||
canCreateRelatedItem = true,
|
||||
workItemsBeta = true,
|
||||
isDrawer = false,
|
||||
} = {}) => {
|
||||
wrapper = shallowMountExtended(WorkItemActions, {
|
||||
isLoggedIn: isLoggedIn(),
|
||||
|
|
@ -154,12 +160,16 @@ describe('WorkItemActions component', () => {
|
|||
hideSubscribe,
|
||||
hasChildren,
|
||||
canCreateRelatedItem,
|
||||
isDrawer,
|
||||
},
|
||||
mocks: {
|
||||
$toast,
|
||||
},
|
||||
provide: {
|
||||
fullPath: 'gitlab-org/gitlab-test',
|
||||
glFeatures: {
|
||||
workItemsBeta,
|
||||
},
|
||||
},
|
||||
stubs: {
|
||||
GlModal: stubComponent(GlModal, {
|
||||
|
|
@ -173,6 +183,11 @@ describe('WorkItemActions component', () => {
|
|||
close: modalShowSpy,
|
||||
},
|
||||
}),
|
||||
WorkItemChangeTypeModal: stubComponent(WorkItemChangeTypeModal, {
|
||||
methods: {
|
||||
show: jest.fn(),
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -206,6 +221,10 @@ describe('WorkItemActions component', () => {
|
|||
testId: TEST_ID_NEW_RELATED_WORK_ITEM,
|
||||
text: 'New related task',
|
||||
},
|
||||
{
|
||||
testId: TEST_ID_CHANGE_TYPE_ACTION,
|
||||
text: 'Change type',
|
||||
},
|
||||
{
|
||||
testId: TEST_ID_LOCK_ACTION,
|
||||
text: 'Lock discussion',
|
||||
|
|
@ -583,4 +602,32 @@ describe('WorkItemActions component', () => {
|
|||
expect(wrapper.emitted('workItemCreated')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change type action', () => {
|
||||
it('opens the change type modal', () => {
|
||||
createComponent({ workItemType: 'Task' });
|
||||
|
||||
findChangeTypeButton().vm.$emit('action');
|
||||
|
||||
expect(findWorkItemChangeTypeModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('hides the action in case of `workItemBeta` is disabled', () => {
|
||||
createComponent({ workItemType: 'Task', workItemsBeta: false });
|
||||
|
||||
expect(findChangeTypeButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('hides the action in case of Epic type', () => {
|
||||
createComponent({ workItemType: 'Epic' });
|
||||
|
||||
expect(findChangeTypeButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('hides the action in case of drawer', () => {
|
||||
createComponent({ isDrawer: true });
|
||||
|
||||
expect(findChangeTypeButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,248 @@
|
|||
import { GlModal, GlFormSelect } from '@gitlab/ui';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
|
||||
import namespaceWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/namespace_work_item_types.query.graphql.json';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
|
||||
import WorkItemChangeTypeModal from '~/work_items/components/work_item_change_type_modal.vue';
|
||||
import namespaceWorkItemTypesQuery from '~/work_items/graphql/namespace_work_item_types.query.graphql';
|
||||
import convertWorkItemMutation from '~/work_items/graphql/work_item_convert.mutation.graphql';
|
||||
import getWorkItemDesignListQuery from '~/work_items/components/design_management/graphql/design_collection.query.graphql';
|
||||
import {
|
||||
WORK_ITEM_TYPE_ENUM_KEY_RESULT,
|
||||
WORK_ITEM_TYPE_VALUE_KEY_RESULT,
|
||||
WORK_ITEM_TYPE_VALUE_TASK,
|
||||
WORK_ITEM_TYPE_VALUE_ISSUE,
|
||||
WORK_ITEM_WIDGETS_NAME_MAP,
|
||||
} from '~/work_items/constants';
|
||||
|
||||
import {
|
||||
convertWorkItemMutationResponse,
|
||||
workItemChangeTypeWidgets,
|
||||
workItemQueryResponse,
|
||||
} from '../mock_data';
|
||||
import { designCollectionResponse, mockDesign } from './design_management/mock_data';
|
||||
|
||||
describe('WorkItemChangeTypeModal component', () => {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
let wrapper;
|
||||
|
||||
const typesQuerySuccessHandler = jest.fn().mockResolvedValue(namespaceWorkItemTypesQueryResponse);
|
||||
const keyResultTypeId =
|
||||
namespaceWorkItemTypesQueryResponse.data.workspace.workItemTypes.nodes.find(
|
||||
(type) => type.name === WORK_ITEM_TYPE_VALUE_KEY_RESULT,
|
||||
).id;
|
||||
|
||||
const convertWorkItemMutationSuccessHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(convertWorkItemMutationResponse);
|
||||
const graphqlError = 'GraphQL error';
|
||||
const convertWorkItemMutationErrorResponse = {
|
||||
errors: [
|
||||
{
|
||||
message: graphqlError,
|
||||
},
|
||||
],
|
||||
data: {
|
||||
workItemConvert: null,
|
||||
},
|
||||
};
|
||||
const noDesignQueryHandler = jest.fn().mockResolvedValue(designCollectionResponse([]));
|
||||
const oneDesignQueryHandler = jest.fn().mockResolvedValue(designCollectionResponse([mockDesign]));
|
||||
|
||||
const createComponent = ({
|
||||
hasOkrsFeature = true,
|
||||
okrsMvc = true,
|
||||
hasParent = false,
|
||||
hasChildren = false,
|
||||
widgets = [],
|
||||
workItemType = WORK_ITEM_TYPE_VALUE_TASK,
|
||||
convertWorkItemMutationHandler = convertWorkItemMutationSuccessHandler,
|
||||
designQueryHandler = noDesignQueryHandler,
|
||||
} = {}) => {
|
||||
wrapper = mountExtended(WorkItemChangeTypeModal, {
|
||||
apolloProvider: createMockApollo([
|
||||
[namespaceWorkItemTypesQuery, typesQuerySuccessHandler],
|
||||
[convertWorkItemMutation, convertWorkItemMutationHandler],
|
||||
[getWorkItemDesignListQuery, designQueryHandler],
|
||||
]),
|
||||
propsData: {
|
||||
workItemId: 'gid://gitlab/WorkItem/1',
|
||||
fullPath: 'gitlab-org/gitlab-test',
|
||||
hasParent,
|
||||
hasChildren,
|
||||
widgets,
|
||||
workItemType,
|
||||
allowedChildTypes: [{ name: WORK_ITEM_TYPE_VALUE_TASK }],
|
||||
},
|
||||
provide: {
|
||||
hasOkrsFeature,
|
||||
glFeatures: {
|
||||
okrsMvc,
|
||||
},
|
||||
},
|
||||
stubs: {
|
||||
GlModal: stubComponent(GlModal, {
|
||||
template:
|
||||
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findChangeTypeModal = () => wrapper.findComponent(GlModal);
|
||||
const findGlFormSelect = () => wrapper.findComponent(GlFormSelect);
|
||||
const findWarningAlert = () => wrapper.findByTestId('change-type-warning-message');
|
||||
const findErrorAlert = () => wrapper.findByTestId('change-type-error-message');
|
||||
const findConfirmationButton = () => wrapper.findByTestId('change-type-confirmation-button');
|
||||
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('renders change type modal with the select', () => {
|
||||
expect(findChangeTypeModal().exists()).toBe(true);
|
||||
expect(findGlFormSelect().exists()).toBe(true);
|
||||
expect(findConfirmationButton().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('calls the `namespaceWorkItemTypesQuery` to get the work item types', () => {
|
||||
expect(typesQuerySuccessHandler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders all types as select options', () => {
|
||||
expect(findGlFormSelect().findAll('option')).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('does not render objective and key result if `okrsMvc` is disabled', () => {
|
||||
createComponent({ okrsMvc: false });
|
||||
|
||||
expect(findGlFormSelect().findAll('option')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('does not allow to change type and disables `Change type` button when the work item has a parent', async () => {
|
||||
createComponent({ hasParent: true, widgets: workItemQueryResponse.data.workItem.widgets });
|
||||
|
||||
findGlFormSelect().vm.$emit('change', WORK_ITEM_TYPE_ENUM_KEY_RESULT);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findWarningAlert().text()).toBe(
|
||||
'Parent item type issue is not supported on key result. Remove the parent item to change type.',
|
||||
);
|
||||
|
||||
expect(findConfirmationButton().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not allow to change type and disables `Change type` button when the work item has child items', async () => {
|
||||
createComponent({ workItemType: WORK_ITEM_TYPE_VALUE_ISSUE, hasChildren: true });
|
||||
|
||||
findGlFormSelect().vm.$emit('change', WORK_ITEM_TYPE_ENUM_KEY_RESULT);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findWarningAlert().text()).toBe(
|
||||
'Key result does not support the task child item types. Remove child items to change type.',
|
||||
);
|
||||
expect(findConfirmationButton().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
describe('when widget data has difference', () => {
|
||||
it('shows warning message in case of designs', async () => {
|
||||
createComponent({
|
||||
workItemType: WORK_ITEM_TYPE_VALUE_ISSUE,
|
||||
designQueryHandler: oneDesignQueryHandler,
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
findGlFormSelect().vm.$emit('change', WORK_ITEM_TYPE_ENUM_KEY_RESULT);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findWarningAlert().text()).toContain('Design');
|
||||
expect(findConfirmationButton().props('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
// These are all possible use cases of conflicts among project level work items
|
||||
// Other widgets are shared between all the work item types
|
||||
it.each`
|
||||
widgetType | widgetData | workItemType | typeTobeConverted | expectedString
|
||||
${WORK_ITEM_WIDGETS_NAME_MAP.MILESTONE} | ${workItemChangeTypeWidgets.MILESTONE} | ${WORK_ITEM_TYPE_VALUE_TASK} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT} | ${'Milestone'}
|
||||
${WORK_ITEM_WIDGETS_NAME_MAP.DEVELOPMENT} | ${workItemChangeTypeWidgets.DEVELOPMENT} | ${WORK_ITEM_TYPE_VALUE_ISSUE} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT} | ${'Development'}
|
||||
${WORK_ITEM_WIDGETS_NAME_MAP.CRM_CONTACTS} | ${workItemChangeTypeWidgets.CRM_CONTACTS} | ${WORK_ITEM_TYPE_VALUE_ISSUE} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT} | ${'Contacts'}
|
||||
${WORK_ITEM_WIDGETS_NAME_MAP.TIME_TRACKING} | ${workItemChangeTypeWidgets.TIME_TRACKING} | ${WORK_ITEM_TYPE_VALUE_ISSUE} | ${WORK_ITEM_TYPE_ENUM_KEY_RESULT} | ${'Time tracking'}
|
||||
`(
|
||||
'shows warning message in case of $widgetType widget',
|
||||
async ({ workItemType, widgetData, typeTobeConverted, expectedString }) => {
|
||||
createComponent({
|
||||
workItemType,
|
||||
widgets: [widgetData],
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
findGlFormSelect().vm.$emit('change', typeTobeConverted);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findWarningAlert().text()).toContain(expectedString);
|
||||
expect(findConfirmationButton().props('disabled')).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('convert work item mutation', () => {
|
||||
it('successfully changes a work item type when conditions are met', async () => {
|
||||
createComponent();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
findGlFormSelect().vm.$emit('change', WORK_ITEM_TYPE_ENUM_KEY_RESULT);
|
||||
|
||||
await nextTick();
|
||||
|
||||
findConfirmationButton().vm.$emit('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(convertWorkItemMutationSuccessHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
id: 'gid://gitlab/WorkItem/1',
|
||||
workItemTypeId: keyResultTypeId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each`
|
||||
errorType | expectedErrorMessage | failureHandler
|
||||
${'graphql error'} | ${graphqlError} | ${jest.fn().mockResolvedValue(convertWorkItemMutationErrorResponse)}
|
||||
${'network error'} | ${'Error: Network error'} | ${jest.fn().mockRejectedValue(new Error('Network error'))}
|
||||
`(
|
||||
'emits an error when there is a $errorType',
|
||||
async ({ expectedErrorMessage, failureHandler }) => {
|
||||
createComponent({
|
||||
convertWorkItemMutationHandler: failureHandler,
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
findGlFormSelect().vm.$emit('change', WORK_ITEM_TYPE_ENUM_KEY_RESULT);
|
||||
|
||||
await nextTick();
|
||||
|
||||
findConfirmationButton().vm.$emit('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findErrorAlert().text()).toContain(expectedErrorMessage);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -796,6 +796,20 @@ describe('WorkItemDetail component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('work item change type', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('should call work item query on type change', async () => {
|
||||
findWorkItemActions().vm.$emit('workItemTypeChanged');
|
||||
await nextTick();
|
||||
|
||||
expect(successHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('todos widget', () => {
|
||||
beforeEach(async () => {
|
||||
isLoggedIn.mockReturnValue(false);
|
||||
|
|
|
|||
|
|
@ -1836,6 +1836,72 @@ export const workItemObjectiveMetadataWidgets = {
|
|||
},
|
||||
};
|
||||
|
||||
export const workItemChangeTypeWidgets = {
|
||||
MILESTONE: {
|
||||
type: 'MILESTONE',
|
||||
__typename: 'WorkItemWidgetMilestone',
|
||||
milestone: mockMilestone,
|
||||
},
|
||||
ITERATION: {
|
||||
type: 'ITERATION',
|
||||
iteration: {
|
||||
id: 'gid://gitlab/Iteration/86312',
|
||||
__typename: 'Iteration',
|
||||
},
|
||||
__typename: 'WorkItemWidgetIteration',
|
||||
},
|
||||
DEVELOPMENT: {
|
||||
type: 'DEVELOPMENT',
|
||||
relatedBranches: {
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
__typename: 'WorkItemRelatedBranchConnection',
|
||||
},
|
||||
},
|
||||
WEIGHT: {
|
||||
type: 'WEIGHT',
|
||||
weight: 1,
|
||||
__typename: 'WorkItemWidgetWeight',
|
||||
},
|
||||
CRM_CONTACTS: {
|
||||
type: 'CRM_CONTACTS',
|
||||
contacts: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/CustomerRelations::Contact/50',
|
||||
__typename: 'CustomerRelationsContact',
|
||||
},
|
||||
],
|
||||
__typename: 'CustomerRelationsContactConnection',
|
||||
},
|
||||
__typename: 'WorkItemWidgetCrmContacts',
|
||||
},
|
||||
TIME_TRACKING: {
|
||||
type: 'TIME_TRACKING',
|
||||
timeEstimate: 10,
|
||||
timelogs: {
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'WorkItemTimelog',
|
||||
id: 'gid://gitlab/Timelog/2',
|
||||
},
|
||||
],
|
||||
__typename: 'WorkItemTimelogConnection',
|
||||
},
|
||||
totalTimeSpent: 10800,
|
||||
__typename: 'WorkItemWidgetTimeTracking',
|
||||
},
|
||||
PROGRESS: {
|
||||
type: 'PROGRESS',
|
||||
progress: 33,
|
||||
updatedAt: '2024-12-05T16:24:56Z',
|
||||
__typename: 'WorkItemWidgetProgress',
|
||||
},
|
||||
};
|
||||
|
||||
export const confidentialWorkItemTask = {
|
||||
id: 'gid://gitlab/WorkItem/2',
|
||||
iid: '2',
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ describe('Work items router', () => {
|
|||
WorkItemAncestors: true,
|
||||
WorkItemCreateBranchMergeRequestModal: true,
|
||||
WorkItemDevelopment: true,
|
||||
WorkItemChangeTypeModal: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,175 +2,6 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.shared_examples "a measurable object" do
|
||||
let(:part_type) { :range }
|
||||
|
||||
context 'when the table is not allowed' do
|
||||
let(:source_table) { :_test_this_table_is_not_allowed }
|
||||
|
||||
it 'raises an error' do
|
||||
expect(migration).to receive(:assert_table_is_allowed).with(source_table).and_call_original
|
||||
|
||||
expect do
|
||||
subject
|
||||
end.to raise_error(/#{source_table} is not allowed for use/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when run inside a transaction block' do
|
||||
it 'raises an error' do
|
||||
expect(migration).to receive(:transaction_open?).and_return(true)
|
||||
|
||||
expect do
|
||||
subject
|
||||
end.to raise_error(/can not be run inside a transaction/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the given table does not have a primary key' do
|
||||
it 'raises an error' do
|
||||
migration.execute(<<~SQL)
|
||||
ALTER TABLE #{source_table}
|
||||
DROP CONSTRAINT #{source_table}_pkey
|
||||
SQL
|
||||
|
||||
expect do
|
||||
subject
|
||||
end.to raise_error(/primary key not defined for #{source_table}/)
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates the partitioned table with the same non-key columns' do
|
||||
subject
|
||||
|
||||
copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key)
|
||||
original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key)
|
||||
|
||||
expect(copied_columns).to match_array(original_columns)
|
||||
end
|
||||
|
||||
it 'removes the default from the primary key column' do
|
||||
subject
|
||||
|
||||
pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
|
||||
|
||||
expect(pk_column.default_function).to be_nil
|
||||
end
|
||||
|
||||
describe 'constructing the partitioned table' do
|
||||
it 'creates a table partitioned by the proper column' do
|
||||
subject
|
||||
|
||||
expect(connection.table_exists?(partitioned_table)).to be(true)
|
||||
expect(connection.primary_key(partitioned_table)).to eq(new_primary_key)
|
||||
|
||||
expect_table_partitioned_by(partitioned_table, [partition_column_name], part_type: part_type)
|
||||
end
|
||||
|
||||
it 'requires the migration helper to be run in DDL mode' do
|
||||
expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_ddl_mode!)
|
||||
|
||||
subject
|
||||
|
||||
expect(connection.table_exists?(partitioned_table)).to be(true)
|
||||
expect(connection.primary_key(partitioned_table)).to eq(new_primary_key)
|
||||
|
||||
expect_table_partitioned_by(partitioned_table, [partition_column_name], part_type: part_type)
|
||||
end
|
||||
|
||||
it 'changes the primary key datatype to bigint' do
|
||||
subject
|
||||
|
||||
pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
|
||||
|
||||
expect(pk_column.sql_type).to eq('bigint')
|
||||
end
|
||||
|
||||
it 'removes the default from the primary key column' do
|
||||
subject
|
||||
|
||||
pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
|
||||
|
||||
expect(pk_column.default_function).to be_nil
|
||||
end
|
||||
|
||||
it 'creates the partitioned table with the same non-key columns' do
|
||||
subject
|
||||
|
||||
copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key)
|
||||
original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key)
|
||||
|
||||
expect(copied_columns).to match_array(original_columns)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'keeping data in sync with the partitioned table' do
|
||||
before do
|
||||
partitioned_model.primary_key = :id
|
||||
partitioned_model.table_name = partitioned_table
|
||||
end
|
||||
|
||||
it 'creates a trigger function on the original table' do
|
||||
expect_function_not_to_exist(function_name)
|
||||
expect_trigger_not_to_exist(source_table, trigger_name)
|
||||
|
||||
subject
|
||||
|
||||
expect_function_to_exist(function_name)
|
||||
expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update])
|
||||
end
|
||||
|
||||
it 'syncs inserts to the partitioned tables' do
|
||||
subject
|
||||
|
||||
expect(partitioned_model.count).to eq(0)
|
||||
|
||||
first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1, updated_at: timestamp)
|
||||
second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2, updated_at: timestamp)
|
||||
|
||||
expect(partitioned_model.count).to eq(2)
|
||||
expect(partitioned_model.find(first_record.id).attributes).to eq(first_record.attributes)
|
||||
expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes)
|
||||
end
|
||||
|
||||
it 'syncs updates to the partitioned tables' do
|
||||
subject
|
||||
|
||||
first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1, updated_at: timestamp)
|
||||
second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2, updated_at: timestamp)
|
||||
|
||||
expect(partitioned_model.count).to eq(2)
|
||||
|
||||
first_copy = partitioned_model.find(first_record.id)
|
||||
second_copy = partitioned_model.find(second_record.id)
|
||||
|
||||
expect(first_copy.attributes).to eq(first_record.attributes)
|
||||
expect(second_copy.attributes).to eq(second_record.attributes)
|
||||
|
||||
first_record.update!(age: 21, updated_at: timestamp + 1.hour, external_id: 3)
|
||||
|
||||
expect(partitioned_model.count).to eq(2)
|
||||
expect(first_copy.reload.attributes).to eq(first_record.attributes)
|
||||
expect(second_copy.reload.attributes).to eq(second_record.attributes)
|
||||
end
|
||||
|
||||
it 'syncs deletes to the partitioned tables' do
|
||||
subject
|
||||
|
||||
first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1, updated_at: timestamp)
|
||||
second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2, updated_at: timestamp)
|
||||
|
||||
expect(partitioned_model.count).to eq(2)
|
||||
|
||||
first_record.destroy!
|
||||
|
||||
expect(partitioned_model.count).to eq(1)
|
||||
expect(partitioned_model.find_by_id(first_record.id)).to be_nil
|
||||
expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHelpers, feature_category: :database do
|
||||
include Database::PartitioningHelpers
|
||||
include Database::TriggerHelpers
|
||||
|
|
|
|||
|
|
@ -5,32 +5,12 @@ require 'spec_helper'
|
|||
RSpec.describe Packages::Npm::Metadatum, type: :model, feature_category: :package_registry do
|
||||
describe 'relationships' do
|
||||
it { is_expected.to belong_to(:package).class_name('Packages::Npm::Package').inverse_of(:npm_metadatum) }
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
it 'belongs to `legacy_package`' do
|
||||
is_expected.to belong_to(:legacy_package).conditions(package_type: :npm).class_name('Packages::Package')
|
||||
.inverse_of(:npm_metadatum).with_foreign_key(:package_id)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
describe 'package', :aggregate_failures do
|
||||
it { is_expected.to validate_presence_of(:package) }
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
it { is_expected.not_to validate_presence_of(:legacy_package) }
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it { is_expected.to validate_presence_of(:legacy_package) }
|
||||
it { is_expected.not_to validate_presence_of(:package) }
|
||||
end
|
||||
|
||||
describe '#ensure_npm_package_type', :aggregate_failures do
|
||||
subject(:npm_metadatum) { build(:npm_metadatum) }
|
||||
|
||||
|
|
@ -45,19 +25,6 @@ RSpec.describe Packages::Npm::Metadatum, type: :model, feature_category: :packag
|
|||
it 'raises the error' do
|
||||
expect { build(:npm_metadatum, package: package) }.to raise_error(ActiveRecord::AssociationTypeMismatch)
|
||||
end
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it 'adds the validation error' do
|
||||
npm_metadatum = build(:npm_metadatum, legacy_package: package, package: nil)
|
||||
|
||||
expect(npm_metadatum).not_to be_valid
|
||||
expect(npm_metadatum.errors.to_a).to include('Package type must be NPM')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,9 +18,6 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
|
|||
it { is_expected.to have_many(:tags).inverse_of(:package) }
|
||||
it { is_expected.to have_many(:build_infos).inverse_of(:package) }
|
||||
it { is_expected.to have_one(:maven_metadatum).inverse_of(:package) }
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
it { is_expected.to have_one(:npm_metadatum).inverse_of(:package) }
|
||||
end
|
||||
|
||||
describe '.sort_by_attribute' do
|
||||
|
|
@ -108,20 +105,6 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
|
|||
it { is_expected.to allow_value("my/domain/com/my-app").for(:name) }
|
||||
it { is_expected.to allow_value("my.app-11.07.2018").for(:name) }
|
||||
it { is_expected.not_to allow_value("my(dom$$$ain)com.my-app").for(:name) }
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
context 'npm package' do
|
||||
subject { build_stubbed(:npm_package_legacy) }
|
||||
|
||||
it { is_expected.to allow_value("@group-1/package").for(:name) }
|
||||
it { is_expected.to allow_value("@any-scope/package").for(:name) }
|
||||
it { is_expected.to allow_value("unscoped-package").for(:name) }
|
||||
it { is_expected.not_to allow_value("@inv@lid-scope/package").for(:name) }
|
||||
it { is_expected.not_to allow_value("@scope/../../package").for(:name) }
|
||||
it { is_expected.not_to allow_value("@scope%2e%2e%fpackage").for(:name) }
|
||||
it { is_expected.not_to allow_value("@scope/sub/package").for(:name) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#version' do
|
||||
|
|
@ -152,195 +135,6 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
|
|||
it { is_expected.not_to allow_value('../../../../../1.2.3').for(:version) }
|
||||
it { is_expected.not_to allow_value('%2e%2e%2f1.2.3').for(:version) }
|
||||
end
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
it_behaves_like 'validating version to be SemVer compliant for', :npm_package_legacy
|
||||
end
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
describe '#npm_package_already_taken' do
|
||||
context 'maven package' do
|
||||
let!(:package) { create(:maven_package) }
|
||||
|
||||
it 'will allow a package of the same name' do
|
||||
new_package = build(:maven_package, name: package.name)
|
||||
|
||||
expect(new_package).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context 'npm package' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, namespace: group) }
|
||||
let_it_be(:second_project) { create(:project, namespace: group) }
|
||||
|
||||
let(:package) { build(:npm_package_legacy, project: project, name: name) }
|
||||
|
||||
shared_examples 'validating the first package' do
|
||||
it 'validates the first package' do
|
||||
expect(package).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'validating the second package' do
|
||||
it 'validates the second package' do
|
||||
package.save!
|
||||
|
||||
expect(second_package).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'not validating the second package' do |field_with_error:|
|
||||
it 'does not validate the second package' do
|
||||
package.save!
|
||||
|
||||
expect(second_package).not_to be_valid
|
||||
case field_with_error
|
||||
when :base
|
||||
expect(second_package.errors.messages[:base]).to eq ['Package already exists']
|
||||
when :name
|
||||
expect(second_package.errors.messages[:name]).to eq ['has already been taken']
|
||||
else
|
||||
raise ArgumentError, "field #{field_with_error} not expected"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'validating both if the first package is pending destruction' do
|
||||
before do
|
||||
package.status = :pending_destruction
|
||||
end
|
||||
|
||||
it_behaves_like 'validating the first package'
|
||||
it_behaves_like 'validating the second package'
|
||||
end
|
||||
|
||||
context 'following the naming convention' do
|
||||
let(:name) { "@#{group.path}/test" }
|
||||
|
||||
context 'with the second package in the project of the first package' do
|
||||
let(:second_package) { build(:npm_package_legacy, project: project, name: second_package_name, version: second_package_version) }
|
||||
|
||||
context 'with no duplicated name' do
|
||||
let(:second_package_name) { "@#{group.path}/test2" }
|
||||
let(:second_package_version) { '5.0.0' }
|
||||
|
||||
it_behaves_like 'validating the first package'
|
||||
it_behaves_like 'validating the second package'
|
||||
end
|
||||
|
||||
context 'with duplicated name' do
|
||||
let(:second_package_name) { package.name }
|
||||
let(:second_package_version) { '5.0.0' }
|
||||
|
||||
it_behaves_like 'validating the first package'
|
||||
it_behaves_like 'validating the second package'
|
||||
end
|
||||
|
||||
context 'with duplicate name and duplicated version' do
|
||||
let(:second_package_name) { package.name }
|
||||
let(:second_package_version) { package.version }
|
||||
|
||||
it_behaves_like 'validating the first package'
|
||||
it_behaves_like 'not validating the second package', field_with_error: :name
|
||||
it_behaves_like 'validating both if the first package is pending destruction'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the second package in a different project than the first package' do
|
||||
let(:second_package) { build(:npm_package_legacy, project: second_project, name: second_package_name, version: second_package_version) }
|
||||
|
||||
context 'with no duplicated name' do
|
||||
let(:second_package_name) { "@#{group.path}/test2" }
|
||||
let(:second_package_version) { '5.0.0' }
|
||||
|
||||
it_behaves_like 'validating the first package'
|
||||
it_behaves_like 'validating the second package'
|
||||
end
|
||||
|
||||
context 'with duplicated name' do
|
||||
let(:second_package_name) { package.name }
|
||||
let(:second_package_version) { '5.0.0' }
|
||||
|
||||
it_behaves_like 'validating the first package'
|
||||
it_behaves_like 'validating the second package'
|
||||
end
|
||||
|
||||
context 'with duplicate name and duplicated version' do
|
||||
let(:second_package_name) { package.name }
|
||||
let(:second_package_version) { package.version }
|
||||
|
||||
it_behaves_like 'validating the first package'
|
||||
it_behaves_like 'not validating the second package', field_with_error: :base
|
||||
it_behaves_like 'validating both if the first package is pending destruction'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'not following the naming convention' do
|
||||
let(:name) { '@foobar/test' }
|
||||
|
||||
context 'with the second package in the project of the first package' do
|
||||
let(:second_package) { build(:npm_package_legacy, project: project, name: second_package_name, version: second_package_version) }
|
||||
|
||||
context 'with no duplicated name' do
|
||||
let(:second_package_name) { "@foobar/test2" }
|
||||
let(:second_package_version) { '5.0.0' }
|
||||
|
||||
it_behaves_like 'validating the first package'
|
||||
it_behaves_like 'validating the second package'
|
||||
end
|
||||
|
||||
context 'with duplicated name' do
|
||||
let(:second_package_name) { package.name }
|
||||
let(:second_package_version) { '5.0.0' }
|
||||
|
||||
it_behaves_like 'validating the first package'
|
||||
it_behaves_like 'validating the second package'
|
||||
end
|
||||
|
||||
context 'with duplicate name and duplicated version' do
|
||||
let(:second_package_name) { package.name }
|
||||
let(:second_package_version) { package.version }
|
||||
|
||||
it_behaves_like 'validating the first package'
|
||||
it_behaves_like 'not validating the second package', field_with_error: :name
|
||||
it_behaves_like 'validating both if the first package is pending destruction'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the second package in a different project than the first package' do
|
||||
let(:second_package) { build(:npm_package_legacy, project: second_project, name: second_package_name, version: second_package_version) }
|
||||
|
||||
context 'with no duplicated name' do
|
||||
let(:second_package_name) { "@foobar/test2" }
|
||||
let(:second_package_version) { '5.0.0' }
|
||||
|
||||
it_behaves_like 'validating the first package'
|
||||
it_behaves_like 'validating the second package'
|
||||
end
|
||||
|
||||
context 'with duplicated name' do
|
||||
let(:second_package_name) { package.name }
|
||||
let(:second_package_version) { '5.0.0' }
|
||||
|
||||
it_behaves_like 'validating the first package'
|
||||
it_behaves_like 'validating the second package'
|
||||
end
|
||||
|
||||
context 'with duplicate name and duplicated version' do
|
||||
let(:second_package_name) { package.name }
|
||||
let(:second_package_version) { package.version }
|
||||
|
||||
it_behaves_like 'validating the first package'
|
||||
it_behaves_like 'validating the second package'
|
||||
it_behaves_like 'validating both if the first package is pending destruction'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#prevent_concurrent_inserts' do
|
||||
|
|
@ -465,18 +259,6 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
|
|||
end
|
||||
end
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
describe '.with_npm_scope' do
|
||||
let_it_be(:package1) { create(:npm_package, name: '@test/foobar') }
|
||||
let_it_be(:package2) { create(:npm_package, name: '@test2/foobar') }
|
||||
let_it_be(:package3) { create(:npm_package, name: 'foobar') }
|
||||
|
||||
subject { described_class.with_npm_scope('test') }
|
||||
|
||||
it { is_expected.to contain_exactly(package1) }
|
||||
end
|
||||
|
||||
describe '.limit_recent' do
|
||||
let!(:package1) { create(:nuget_package) }
|
||||
let!(:package2) { create(:nuget_package) }
|
||||
|
|
@ -828,32 +610,6 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
|
|||
end
|
||||
end
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
describe '#sync_npm_metadata_cache' do
|
||||
let_it_be(:package) { create(:npm_package) }
|
||||
|
||||
subject { package.sync_npm_metadata_cache }
|
||||
|
||||
it 'enqueues a sync worker job' do
|
||||
expect(::Packages::Npm::CreateMetadataCacheWorker)
|
||||
.to receive(:perform_async).with(package.project_id, package.name)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
context 'with a non npm package' do
|
||||
let_it_be(:package) { create(:maven_package) }
|
||||
|
||||
it 'does not enqueue a sync worker job' do
|
||||
expect(::Packages::Npm::CreateMetadataCacheWorker)
|
||||
.not_to receive(:perform_async)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#mark_package_files_for_destruction' do
|
||||
let_it_be(:package) { create(:npm_package, :pending_destruction) }
|
||||
|
||||
|
|
@ -983,19 +739,5 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
context 'for package format npm' do
|
||||
let(:format) { :npm }
|
||||
|
||||
it 'maps to Packages::Package' do
|
||||
is_expected.to eq(described_class)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8262,31 +8262,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
|
|||
end
|
||||
end
|
||||
|
||||
# TODO: Remove with the rollout of the FF npm_extract_npm_package_model
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/501469
|
||||
describe '#has_namespaced_npm_packages?' do
|
||||
let_it_be(:namespace) { create(:namespace, path: 'test') }
|
||||
let_it_be(:project) { create(:project, :public, namespace: namespace) }
|
||||
|
||||
subject { project.has_namespaced_npm_packages? }
|
||||
|
||||
context 'with scope of the namespace path' do
|
||||
let_it_be(:package) { create(:npm_package, project: project, name: "@#{namespace.path}/foo") }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'without scope of the namespace path' do
|
||||
let_it_be(:package) { create(:npm_package, project: project, name: "@someotherscope/foo") }
|
||||
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
context 'without packages' do
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#package_already_taken?' do
|
||||
let_it_be(:namespace) { create(:namespace, path: 'test') }
|
||||
let_it_be(:project) { create(:project, :public, namespace: namespace) }
|
||||
|
|
|
|||
|
|
@ -194,16 +194,6 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
|
|||
|
||||
it_behaves_like 'successfully downloads the file'
|
||||
end
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
let_it_be(:package, reload: true) { create(:npm_package_legacy, project: project, name: FFaker::Lorem.word, version: '1.2.3') }
|
||||
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'successfully downloads the file'
|
||||
end
|
||||
end
|
||||
|
||||
context 'private project' do
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ RSpec.describe Git::WikiPushService, :services, feature_category: :wiki do
|
|||
message = 'something went very very wrong'
|
||||
allow_next_instance_of(WikiPages::EventCreateService, current_user) do |service|
|
||||
allow(service).to receive(:execute)
|
||||
.with(String, WikiPage, Symbol, String)
|
||||
.with(WikiPage::Meta, Symbol, String)
|
||||
.and_return(ServiceResponse.error(message: message))
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -95,29 +95,6 @@ RSpec.describe Groups::TransferService, :sidekiq_inline, feature_category: :grou
|
|||
|
||||
it_behaves_like 'transfer allowed'
|
||||
end
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it 'does not allow transfer' do
|
||||
transfer_service.execute(new_group)
|
||||
|
||||
expect(transfer_service.error).to eq('Transfer failed: Group contains projects with NPM packages scoped to the current root level group.')
|
||||
expect(group.parent).not_to eq(new_group)
|
||||
end
|
||||
|
||||
context 'namespaced package is pending destruction' do
|
||||
let!(:group) { create(:group) }
|
||||
|
||||
before do
|
||||
package.pending_destruction!
|
||||
end
|
||||
|
||||
it_behaves_like 'transfer allowed'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when transferring a group into a root group' do
|
||||
|
|
|
|||
|
|
@ -334,15 +334,6 @@ RSpec.describe Groups::UpdateService, feature_category: :groups_and_projects do
|
|||
|
||||
it_behaves_like 'not allowing a path update'
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'not allowing a path update'
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
end
|
||||
end
|
||||
|
||||
context 'updating the subgroup' do
|
||||
|
|
@ -361,15 +352,6 @@ RSpec.describe Groups::UpdateService, feature_category: :groups_and_projects do
|
|||
|
||||
it_behaves_like 'allowing an update', on: :path
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'allowing an update', on: :path
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
end
|
||||
end
|
||||
|
||||
context 'updating the subgroup' do
|
||||
|
|
@ -388,15 +370,6 @@ RSpec.describe Groups::UpdateService, feature_category: :groups_and_projects do
|
|||
|
||||
it_behaves_like 'allowing an update', on: :path
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'allowing an update', on: :path
|
||||
it_behaves_like 'allowing an update', on: :name
|
||||
end
|
||||
end
|
||||
|
||||
context 'updating the subgroup' do
|
||||
|
|
|
|||
|
|
@ -213,16 +213,6 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r
|
|||
it { is_expected.to have_attributes reason: :package_already_exists }
|
||||
end
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'returning an error service response', message: 'Package already exists.' do
|
||||
it { is_expected.to have_attributes reason: :package_already_exists }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when marked as pending_destruction' do
|
||||
before do
|
||||
existing_package.pending_destruction!
|
||||
|
|
|
|||
|
|
@ -39,14 +39,6 @@ RSpec.describe ::Packages::Npm::ProcessPackageFileService, feature_category: :pa
|
|||
it_behaves_like 'raising an error', 'invalid package file'
|
||||
end
|
||||
|
||||
context 'when linked to a non npm package' do
|
||||
before do
|
||||
allow(package).to receive(:npm?).and_return(false)
|
||||
end
|
||||
|
||||
it_behaves_like 'raising an error', 'invalid package file'
|
||||
end
|
||||
|
||||
context 'with a 0 byte package file' do
|
||||
before do
|
||||
allow(package_file.file).to receive(:size).and_return(0)
|
||||
|
|
|
|||
|
|
@ -37,16 +37,6 @@ RSpec.describe Projects::TransferService, feature_category: :groups_and_projects
|
|||
it_behaves_like 'allow the transfer' do
|
||||
let(:namespace) { group }
|
||||
end
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'allow the transfer' do
|
||||
let(:namespace) { group }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pending destruction package' do
|
||||
|
|
@ -57,16 +47,6 @@ RSpec.describe Projects::TransferService, feature_category: :groups_and_projects
|
|||
it_behaves_like 'allow the transfer' do
|
||||
let(:namespace) { group }
|
||||
end
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'allow the transfer' do
|
||||
let(:namespace) { group }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with namespaced packages present' do
|
||||
|
|
@ -76,17 +56,6 @@ RSpec.describe Projects::TransferService, feature_category: :groups_and_projects
|
|||
expect(transfer_service.execute(group)).to be false
|
||||
expect(project.errors[:new_namespace]).to include("Root namespace can't be updated if the project has NPM packages scoped to the current root level namespace.")
|
||||
end
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it 'does not allow the transfer' do
|
||||
expect(transfer_service.execute(group)).to be false
|
||||
expect(project.errors[:new_namespace]).to include("Root namespace can't be updated if the project has NPM packages scoped to the current root level namespace.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a root namespace change' do
|
||||
|
|
@ -102,16 +71,6 @@ RSpec.describe Projects::TransferService, feature_category: :groups_and_projects
|
|||
it_behaves_like 'allow the transfer' do
|
||||
let(:namespace) { other_group }
|
||||
end
|
||||
|
||||
context 'when npm_extract_npm_package_model is disabled' do
|
||||
before do
|
||||
stub_feature_flags(npm_extract_npm_package_model: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'allow the transfer' do
|
||||
let(:namespace) { other_group }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ RSpec.describe WikiPages::EventCreateService, feature_category: :wiki do
|
|||
describe '#execute' do
|
||||
let_it_be(:page) { create(:wiki_page, project: project) }
|
||||
|
||||
let(:slug) { generate(:sluggified_title) }
|
||||
let(:action) { :created }
|
||||
let(:fingerprint) { page.sha }
|
||||
let(:response) { subject.execute(slug, page, action, fingerprint) }
|
||||
let(:wiki_page_meta) { create(:wiki_page_meta) }
|
||||
let(:response) { subject.execute(wiki_page_meta, action, fingerprint) }
|
||||
|
||||
context 'the user is nil' do
|
||||
subject { described_class.new(nil) }
|
||||
|
|
@ -40,18 +40,6 @@ RSpec.describe WikiPages::EventCreateService, feature_category: :wiki do
|
|||
expect(response).to be_success
|
||||
end
|
||||
|
||||
context 'the action is a deletion' do
|
||||
let(:action) { :destroyed }
|
||||
|
||||
it 'does not synchronize the wiki metadata timestamps with the git commit' do
|
||||
expect_next_instance_of(WikiPage::Meta) do |instance|
|
||||
expect(instance).not_to receive(:synch_times_with_page)
|
||||
end
|
||||
|
||||
response
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates a wiki page event' do
|
||||
expect { response }.to change(Event, :count).by(1)
|
||||
end
|
||||
|
|
@ -59,12 +47,5 @@ RSpec.describe WikiPages::EventCreateService, feature_category: :wiki do
|
|||
it 'returns an event in the payload' do
|
||||
expect(response.payload).to include(event: have_attributes(author: user, wiki_page?: true, action: 'created'))
|
||||
end
|
||||
|
||||
it 'records the slug for the page' do
|
||||
response
|
||||
meta = WikiPage::Meta.find_or_create(page.slug, page)
|
||||
|
||||
expect(meta.slugs.pluck(:slug)).to include(slug)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples "a measurable object" do
|
||||
let(:part_type) { :range }
|
||||
|
||||
context 'when the table is not allowed' do
|
||||
let(:source_table) { :_test_this_table_is_not_allowed }
|
||||
|
||||
it 'raises an error' do
|
||||
expect(migration).to receive(:assert_table_is_allowed).with(source_table).and_call_original
|
||||
|
||||
expect do
|
||||
subject
|
||||
end.to raise_error(/#{source_table} is not allowed for use/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when run inside a transaction block' do
|
||||
it 'raises an error' do
|
||||
expect(migration).to receive(:transaction_open?).and_return(true)
|
||||
|
||||
expect do
|
||||
subject
|
||||
end.to raise_error(/can not be run inside a transaction/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the given table does not have a primary key' do
|
||||
it 'raises an error' do
|
||||
migration.execute(<<~SQL)
|
||||
ALTER TABLE #{source_table}
|
||||
DROP CONSTRAINT #{source_table}_pkey
|
||||
SQL
|
||||
|
||||
expect do
|
||||
subject
|
||||
end.to raise_error(/primary key not defined for #{source_table}/)
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates the partitioned table with the same non-key columns' do
|
||||
subject
|
||||
|
||||
copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key)
|
||||
original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key)
|
||||
|
||||
expect(copied_columns).to match_array(original_columns)
|
||||
end
|
||||
|
||||
it 'removes the default from the primary key column' do
|
||||
subject
|
||||
|
||||
pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
|
||||
|
||||
expect(pk_column.default_function).to be_nil
|
||||
end
|
||||
|
||||
describe 'constructing the partitioned table' do
|
||||
it 'creates a table partitioned by the proper column' do
|
||||
subject
|
||||
|
||||
expect(connection.table_exists?(partitioned_table)).to be(true)
|
||||
expect(connection.primary_key(partitioned_table)).to eq(new_primary_key)
|
||||
|
||||
expect_table_partitioned_by(partitioned_table, [partition_column_name], part_type: part_type)
|
||||
end
|
||||
|
||||
it 'requires the migration helper to be run in DDL mode' do
|
||||
expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_ddl_mode!)
|
||||
|
||||
subject
|
||||
|
||||
expect(connection.table_exists?(partitioned_table)).to be(true)
|
||||
expect(connection.primary_key(partitioned_table)).to eq(new_primary_key)
|
||||
|
||||
expect_table_partitioned_by(partitioned_table, [partition_column_name], part_type: part_type)
|
||||
end
|
||||
|
||||
it 'changes the primary key datatype to bigint' do
|
||||
subject
|
||||
|
||||
pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
|
||||
|
||||
expect(pk_column.sql_type).to eq('bigint')
|
||||
end
|
||||
|
||||
it 'removes the default from the primary key column' do
|
||||
subject
|
||||
|
||||
pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key }
|
||||
|
||||
expect(pk_column.default_function).to be_nil
|
||||
end
|
||||
|
||||
it 'creates the partitioned table with the same non-key columns' do
|
||||
subject
|
||||
|
||||
copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key)
|
||||
original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key)
|
||||
|
||||
expect(copied_columns).to match_array(original_columns)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'keeping data in sync with the partitioned table' do
|
||||
before do
|
||||
partitioned_model.primary_key = :id
|
||||
partitioned_model.table_name = partitioned_table
|
||||
end
|
||||
|
||||
it 'creates a trigger function on the original table' do
|
||||
expect_function_not_to_exist(function_name)
|
||||
expect_trigger_not_to_exist(source_table, trigger_name)
|
||||
|
||||
subject
|
||||
|
||||
expect_function_to_exist(function_name)
|
||||
expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update])
|
||||
end
|
||||
|
||||
it 'syncs inserts to the partitioned tables' do
|
||||
subject
|
||||
|
||||
expect(partitioned_model.count).to eq(0)
|
||||
|
||||
first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1,
|
||||
updated_at: timestamp)
|
||||
second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2,
|
||||
updated_at: timestamp)
|
||||
|
||||
expect(partitioned_model.count).to eq(2)
|
||||
expect(partitioned_model.find(first_record.id).attributes).to eq(first_record.attributes)
|
||||
expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes)
|
||||
end
|
||||
|
||||
it 'syncs updates to the partitioned tables' do
|
||||
subject
|
||||
|
||||
first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1,
|
||||
updated_at: timestamp)
|
||||
second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2,
|
||||
updated_at: timestamp)
|
||||
|
||||
expect(partitioned_model.count).to eq(2)
|
||||
|
||||
first_copy = partitioned_model.find(first_record.id)
|
||||
second_copy = partitioned_model.find(second_record.id)
|
||||
|
||||
expect(first_copy.attributes).to eq(first_record.attributes)
|
||||
expect(second_copy.attributes).to eq(second_record.attributes)
|
||||
|
||||
first_record.update!(age: 21, updated_at: timestamp + 1.hour, external_id: 3)
|
||||
|
||||
expect(partitioned_model.count).to eq(2)
|
||||
expect(first_copy.reload.attributes).to eq(first_record.attributes)
|
||||
expect(second_copy.reload.attributes).to eq(second_record.attributes)
|
||||
end
|
||||
|
||||
it 'syncs deletes to the partitioned tables' do
|
||||
subject
|
||||
|
||||
first_record = source_model.create!(name: 'Bob', age: 20, created_at: timestamp, external_id: 1,
|
||||
updated_at: timestamp)
|
||||
second_record = source_model.create!(name: 'Alice', age: 30, created_at: timestamp, external_id: 2,
|
||||
updated_at: timestamp)
|
||||
|
||||
expect(partitioned_model.count).to eq(2)
|
||||
|
||||
first_record.destroy!
|
||||
|
||||
expect(partitioned_model.count).to eq(1)
|
||||
expect(partitioned_model.find_by_id(first_record.id)).to be_nil
|
||||
expect(partitioned_model.find(second_record.id).attributes).to eq(second_record.attributes)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue