Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-07-16 06:26:49 +00:00
parent e7d07db84d
commit 5e0b1a97dc
55 changed files with 613 additions and 365 deletions

View File

@ -80,6 +80,15 @@ export const config = {
},
},
},
WorkItemWidgetHierarchy: {
fields: {
// If we add any key args, the children field becomes children({"first":10}) and
// kills any possibility to handle it on the widget level without hardcoding a string.
children: {
keyArgs: false,
},
},
},
WorkItem: {
fields: {
// widgets policy because otherwise the subscriptions invalidate the cache

View File

@ -336,8 +336,7 @@ export default {
update: (cache, { data: { workItemCreate } }) =>
addHierarchyChild({
cache,
fullPath: this.fullPath,
iid: this.issueIid,
id: convertToGraphQLId(TYPENAME_WORK_ITEM, this.issueId),
workItem: workItemCreate.workItem,
}),
});
@ -371,8 +370,7 @@ export default {
update: (cache) =>
removeHierarchyChild({
cache,
fullPath: this.fullPath,
iid: this.issueIid,
id: convertToGraphQLId(TYPENAME_WORK_ITEM, this.issueId),
workItem: { id },
}),
});

View File

@ -1,5 +1,4 @@
<script>
import { __ } from '~/locale';
import SharedDeleteButton from './shared/delete_button.vue';
export default {
@ -11,11 +10,6 @@ export default {
type: String,
required: true,
},
buttonText: {
type: String,
required: false,
default: __('Delete project'),
},
formPath: {
type: String,
required: true,
@ -47,7 +41,6 @@ export default {
<template>
<shared-delete-button
:confirm-phrase="confirmPhrase"
:button-text="buttonText"
:form-path="formPath"
:is-fork="isFork"
:issues-count="issuesCount"

View File

@ -15,11 +15,6 @@ export default {
type: String,
required: true,
},
buttonText: {
type: String,
required: false,
default: __('Delete project'),
},
formPath: {
type: String,
required: true,
@ -63,6 +58,9 @@ export default {
this.isModalVisible = true;
},
},
i18n: {
deleteProject: __('Delete project'),
},
};
</script>
@ -91,7 +89,7 @@ export default {
variant="danger"
data-testid="delete-button"
@click="onButtonClick"
>{{ buttonText }}</gl-button
>{{ $options.i18n.deleteProject }}</gl-button
>
</gl-form>
</template>

View File

@ -9,7 +9,6 @@ export default (selector = '#js-project-delete-button') => {
const {
confirmPhrase,
buttonText,
formPath,
isFork,
issuesCount,
@ -25,7 +24,6 @@ export default (selector = '#js-project-delete-button') => {
return createElement(ProjectDeleteButton, {
props: {
confirmPhrase,
buttonText,
formPath,
isFork: parseBoolean(isFork),
issuesCount: parseInt(issuesCount, 10),

View File

@ -33,7 +33,7 @@ import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql
import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import getAllowedWorkItemChildTypes from '../graphql/work_item_allowed_children.query.graphql';
import { findHierarchyWidgetChildren, findHierarchyWidgetDefinition } from '../utils';
import { findHierarchyWidgetDefinition } from '../utils';
import WorkItemTree from './work_item_links/work_item_tree.vue';
import WorkItemActions from './work_item_actions.vue';
@ -119,6 +119,7 @@ export default {
isStickyHeaderShowing: false,
editMode: false,
draftData: {},
hasChildren: false,
};
},
apollo: {
@ -282,12 +283,6 @@ export default {
workItemNotes() {
return this.isWidgetPresent(WIDGET_TYPE_NOTES);
},
children() {
return this.workItem ? findHierarchyWidgetChildren(this.workItem) : [];
},
hasChildren() {
return !isEmpty(this.children);
},
workItemBodyClass() {
return {
'gl-pt-5': !this.updateError && !this.isModal,
@ -672,13 +667,13 @@ export default {
:parent-work-item-type="workItem.workItemType.name"
:work-item-id="workItem.id"
:work-item-iid="workItemIid"
:children="children"
:can-update="canUpdate"
:can-update-children="canUpdateChildren"
:confidential="workItem.confidential"
:allowed-child-types="allowedChildTypes"
@show-modal="openInModal"
@addChild="$emit('addChild')"
@childrenLoaded="hasChildren = $event"
/>
<work-item-relationships
v-if="workItemLinkedItems"

View File

@ -8,13 +8,14 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import { defaultSortableOptions, DRAG_DELAY } from '~/sortable/constants';
import { WORK_ITEM_TYPE_VALUE_OBJECTIVE } from '../../constants';
import { WORK_ITEM_TYPE_VALUE_OBJECTIVE, DEFAULT_PAGE_SIZE_CHILD_ITEMS } from '../../constants';
import { findHierarchyWidgets } from '../../utils';
import { addHierarchyChild, removeHierarchyChild } from '../../graphql/cache_utils';
import reorderWorkItem from '../../graphql/reorder_work_item.mutation.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql';
import WorkItemLinkChild from './work_item_link_child.vue';
export default {
@ -97,9 +98,7 @@ export default {
update: (cache) =>
removeHierarchyChild({
cache,
fullPath: this.fullPath,
iid: this.workItemIid,
isGroup: this.isGroup,
id: this.workItemId,
workItem: child,
}),
});
@ -130,9 +129,7 @@ export default {
update: (cache) =>
addHierarchyChild({
cache,
fullPath: this.fullPath,
iid: this.workItemIid,
isGroup: this.isGroup,
id: this.workItemId,
workItem: child,
}),
});
@ -228,12 +225,12 @@ export default {
update: (store) => {
store.updateQuery(
{
query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath: this.fullPath, iid: this.workItemIid },
query: getWorkItemTreeQuery,
variables: { id: this.workItemId, pageSize: DEFAULT_PAGE_SIZE_CHILD_ITEMS },
},
(sourceData) =>
produce(sourceData, (draftData) => {
const { widgets } = draftData.workspace.workItem;
const { widgets } = draftData.workItem;
const hierarchyWidget = findHierarchyWidgets(widgets);
hierarchyWidget.children.nodes = updatedChildren;
}),

View File

@ -11,6 +11,7 @@ import {
WIDGET_TYPE_HIERARCHY,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
WORK_ITEM_TYPE_VALUE_TASK,
DEFAULT_PAGE_SIZE_CHILD_ITEMS,
} from '../../constants';
import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql';
import WorkItemLinkChildContents from '../shared/work_item_link_child_contents.vue';
@ -138,6 +139,7 @@ export default {
query: getWorkItemTreeQuery,
variables: {
id: this.childItem.id,
pageSize: DEFAULT_PAGE_SIZE_CHILD_ITEMS,
},
});
this.children = this.getWidgetByType(data?.workItem, WIDGET_TYPE_HIERARCHY).children.nodes;

View File

@ -22,11 +22,11 @@ import {
WORK_ITEM_STATUS_TEXT,
I18N_WORK_ITEM_SHOW_LABELS,
TASKS_ANCHOR,
DEFAULT_PAGE_SIZE_CHILD_ITEMS,
} from '../../constants';
import { findHierarchyWidgetChildren } from '../../utils';
import { removeHierarchyChild } from '../../graphql/cache_utils';
import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql';
import WidgetWrapper from '../widget_wrapper.vue';
import WorkItemDetailModal from '../work_item_detail_modal.vue';
import WorkItemLinksForm from './work_item_links_form.vue';
@ -62,19 +62,19 @@ export default {
apollo: {
workItem: {
query() {
return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
return getWorkItemTreeQuery;
},
variables() {
return {
fullPath: this.fullPath,
iid: this.iid,
id: this.issuableGid,
pageSize: DEFAULT_PAGE_SIZE_CHILD_ITEMS,
};
},
update(data) {
return data.workspace.workItem ?? {};
return data.workItem ?? {};
},
skip() {
return !this.iid;
return !this.issuableId;
},
error(e) {
this.error = e.message || this.$options.i18n.fetchError;

View File

@ -6,7 +6,7 @@ import WorkItemTokenInput from '../shared/work_item_token_input.vue';
import { addHierarchyChild } from '../../graphql/cache_utils';
import groupWorkItemTypesQuery from '../../graphql/group_work_item_types.query.graphql';
import projectWorkItemTypesQuery from '../../graphql/project_work_item_types.query.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import updateWorkItemHierarchyMutation from '../../graphql/update_work_item_hierarchy.mutation.graphql';
import createWorkItemMutation from '../../graphql/create_work_item.mutation.graphql';
import {
FORM_TYPES,
@ -271,7 +271,7 @@ export default {
this.submitInProgress = true;
this.$apollo
.mutate({
mutation: updateWorkItemMutation,
mutation: updateWorkItemHierarchyMutation,
variables: {
input: {
id: this.issuableGid,
@ -311,9 +311,7 @@ export default {
update: (cache, { data }) =>
addHierarchyChild({
cache,
fullPath: this.fullPath,
iid: this.workItemIid,
isGroup: this.isGroup,
id: this.issuableGid,
workItem: data.workItemCreate.workItem,
}),
})

View File

@ -1,5 +1,5 @@
<script>
import { GlToggle } from '@gitlab/ui';
import { GlToggle, GlLoadingIcon } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import {
FORM_TYPES,
@ -12,7 +12,10 @@ import {
WORK_ITEM_TYPE_ENUM_EPIC,
I18N_WORK_ITEM_SHOW_LABELS,
CHILD_ITEMS_ANCHOR,
DEFAULT_PAGE_SIZE_CHILD_ITEMS,
} from '../../constants';
import { findHierarchyWidgets } from '../../utils';
import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql';
import WidgetWrapper from '../widget_wrapper.vue';
import WorkItemActionsSplitButton from './work_item_actions_split_button.vue';
import WorkItemLinksForm from './work_item_links_form.vue';
@ -31,6 +34,7 @@ export default {
WorkItemChildrenWrapper,
WorkItemTreeActions,
GlToggle,
GlLoadingIcon,
},
inject: ['hasSubepicsFeature'],
props: {
@ -61,11 +65,6 @@ export default {
required: false,
default: false,
},
children: {
type: Array,
required: false,
default: () => [],
},
canUpdate: {
type: Boolean,
required: false,
@ -92,6 +91,28 @@ export default {
showLabels: true,
};
},
apollo: {
hierarchyWidget: {
query: getWorkItemTreeQuery,
variables() {
return {
id: this.workItemId,
pageSize: DEFAULT_PAGE_SIZE_CHILD_ITEMS,
};
},
skip() {
return !this.workItemId;
},
update({ workItem = {} }) {
const { children } = findHierarchyWidgets(workItem.widgets);
this.$emit('childrenLoaded', Boolean(children?.count));
return children || {};
},
error() {
this.error = s__('WorkItems|An error occurred while fetching children');
},
},
},
computed: {
childrenIds() {
return this.children.map((c) => c.id);
@ -135,6 +156,15 @@ export default {
canShowActionsMenu() {
return this.workItemType.toUpperCase() === WORK_ITEM_TYPE_ENUM_EPIC && this.workItemIid;
},
children() {
return this.hierarchyWidget?.nodes || [];
},
isLoadingChildren() {
return this.$apollo.queries.hierarchyWidget.loading;
},
showEmptyMessage() {
return !this.isShownAddForm && this.children.length === 0 && !this.isLoadingChildren;
},
},
methods: {
genericActionItems(workItem) {
@ -206,7 +236,7 @@ export default {
</template>
<template #body>
<div class="gl-new-card-content gl-px-0">
<div v-if="!isShownAddForm && children.length === 0" data-testid="tree-empty">
<div v-if="showEmptyMessage" data-testid="tree-empty">
<p class="gl-new-card-empty">
{{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].empty }}
</p>
@ -227,6 +257,7 @@ export default {
@addChild="$emit('addChild')"
/>
<work-item-children-wrapper
v-if="!isLoadingChildren"
:children="children"
:can-update="canUpdateChildren"
:full-path="fullPath"
@ -237,6 +268,7 @@ export default {
@error="error = $event"
@show-modal="showModal"
/>
<gl-loading-icon v-else size="md" />
</div>
</template>
</widget-wrapper>

View File

@ -7,7 +7,7 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import { removeHierarchyChild } from '../graphql/cache_utils';
import { updateParent } from '../graphql/cache_utils';
import groupWorkItemsQuery from '../graphql/group_work_items.query.graphql';
import projectWorkItemsQuery from '../graphql/project_work_items.query.graphql';
import {
@ -174,7 +174,7 @@ export default {
},
},
update: (cache) =>
removeHierarchyChild({
updateParent({
cache,
fullPath: this.fullPath,
iid: this.oldParent?.iid,

View File

@ -245,6 +245,7 @@ export const FORM_TYPES = {
export const DEFAULT_PAGE_SIZE_ASSIGNEES = 10;
export const DEFAULT_PAGE_SIZE_NOTES = 30;
export const DEFAULT_PAGE_SIZE_EMOJIS = 100;
export const DEFAULT_PAGE_SIZE_CHILD_ITEMS = 1000;
export const WORK_ITEM_NOTES_SORT_ORDER_KEY = 'sort_direction_work_item';

View File

@ -31,6 +31,7 @@ import {
} from '../constants';
import groupWorkItemByIidQuery from './group_work_item_by_iid.query.graphql';
import workItemByIidQuery from './work_item_by_iid.query.graphql';
import getWorkItemTreeQuery from './work_item_tree.query.graphql';
const getNotesWidgetFromSourceData = (draftData) =>
draftData?.workspace?.workItem?.widgets.find(isNotesWidget);
@ -154,10 +155,10 @@ export const updateCacheAfterRemovingAwardEmojiFromNote = (currentNotes, note) =
});
};
export const addHierarchyChild = ({ cache, fullPath, iid, isGroup, workItem }) => {
export const addHierarchyChild = ({ cache, id, workItem }) => {
const queryArgs = {
query: isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath, iid },
query: getWorkItemTreeQuery,
variables: { id },
};
const sourceData = cache.readQuery(queryArgs);
@ -168,19 +169,40 @@ export const addHierarchyChild = ({ cache, fullPath, iid, isGroup, workItem }) =
cache.writeQuery({
...queryArgs,
data: produce(sourceData, (draftState) => {
const existingChild = findHierarchyWidgetChildren(draftState.workspace?.workItem).find(
const existingChild = findHierarchyWidgetChildren(draftState?.workItem).find(
(child) => child.id === workItem?.id,
);
if (!existingChild) {
findHierarchyWidgetChildren(draftState.workspace?.workItem).push(workItem);
findHierarchyWidgetChildren(draftState?.workItem).push(workItem);
}
}),
});
};
export const removeHierarchyChild = ({ cache, fullPath, iid, isGroup, workItem }) => {
export const removeHierarchyChild = ({ cache, id, workItem }) => {
const queryArgs = {
query: isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
query: getWorkItemTreeQuery,
variables: { id },
};
const sourceData = cache.readQuery(queryArgs);
if (!sourceData) {
return;
}
cache.writeQuery({
...queryArgs,
data: produce(sourceData, (draftState) => {
const children = findHierarchyWidgetChildren(draftState?.workItem);
const index = children.findIndex((child) => child.id === workItem.id);
if (index >= 0) children.splice(index, 1);
}),
});
};
export const updateParent = ({ cache, query, fullPath, iid, workItem }) => {
const queryArgs = {
query,
variables: { fullPath, iid },
};
const sourceData = cache.readQuery(queryArgs);

View File

@ -0,0 +1,10 @@
#import "./work_item_hierarchy.fragment.graphql"
mutation workItemHierarchyUpdate($input: WorkItemUpdateInput!, $pageSize: Int = 1000) {
workItemUpdate(input: $input) {
workItem {
...WorkItemHierarchy
}
errors
}
}

View File

@ -0,0 +1,60 @@
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
#import "ee_else_ce/work_items/graphql/work_item_metadata_widgets.fragment.graphql"
fragment WorkItemHierarchy on WorkItem {
id
workItemType {
id
name
iconName
}
title
confidential
userPermissions {
deleteWorkItem
updateWorkItem
adminParentLink
setWorkItemMetadata
createNote
adminWorkItemLink
}
widgets {
type
... on WorkItemWidgetHierarchy {
type
parent {
id
}
children(first: $pageSize) {
pageInfo {
...PageInfo
}
count
nodes {
id
iid
confidential
workItemType {
id
name
iconName
}
title
state
createdAt
closedAt
webUrl
reference(full: true)
widgets {
... on WorkItemWidgetHierarchy {
type
hasChildren
}
...WorkItemMetadataWidgets
}
}
}
}
...WorkItemMetadataWidgets
}
}

View File

@ -1,55 +1,7 @@
#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "ee_else_ce/work_items/graphql/work_item_metadata_widgets.fragment.graphql"
#import "./work_item_hierarchy.fragment.graphql"
query workItemTreeQuery($id: WorkItemID!) {
query workItemTreeQuery($id: WorkItemID!, $pageSize: Int = 1000) {
workItem(id: $id) {
id
workItemType {
id
name
iconName
}
title
userPermissions {
deleteWorkItem
updateWorkItem
}
confidential
widgets {
type
... on WorkItemWidgetHierarchy {
type
parent {
id
}
children {
nodes {
id
iid
confidential
workItemType {
id
name
iconName
}
title
state
createdAt
closedAt
webUrl
reference(full: true)
widgets {
... on WorkItemWidgetHierarchy {
type
hasChildren
}
...WorkItemMetadataWidgets
}
}
}
}
...WorkItemMetadataWidgets
}
...WorkItemHierarchy
}
}

View File

@ -69,34 +69,6 @@ fragment WorkItemWidgets on WorkItemWidget {
iconName
}
}
children {
nodes {
id
iid
confidential
workItemType {
id
name
iconName
}
title
state
createdAt
closedAt
webUrl
reference(full: true)
namespace {
fullPath
}
widgets {
... on WorkItemWidgetHierarchy {
type
hasChildren
}
...WorkItemMetadataWidgets
}
}
}
}
... on WorkItemWidgetMilestone {
milestone {

View File

@ -8,6 +8,8 @@
- c.with_body do
= form_tag(group, method: :delete, id: remove_form_id) do
%p
= _('This action will permanently delete this group, including its subgroups and projects.')
= _('Deleting this group also deletes all child projects, including archived projects, and their resources.')
%br
%strong= _('Deleted group can not be restored!')
= render 'groups/settings/remove_button', group: group, remove_form_id: remove_form_id

View File

@ -1,9 +1,8 @@
- remove_form_id = local_assigns.fetch(:remove_form_id, nil)
- button_text = local_assigns.fetch(:button_text, nil)
- if group.prevent_delete?
= render Pajamas::AlertComponent.new(variant: :tip, dismissible: false, alert_options: { class: 'gl-mb-5', data: { testid: 'group-has-linked-subscription-alert' }}) do |c|
- c.with_body do
= html_escape(_("This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/gitlab_com/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
.js-confirm-danger{ data: group_confirm_modal_data(group: group, remove_form_id: remove_form_id, button_text: button_text) }
.js-confirm-danger{ data: group_confirm_modal_data(group: group, remove_form_id: remove_form_id) }

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class DropTmpIndexFromVulnerabilityOccurrences < Gitlab::Database::Migration[2.2]
TABLE_NAME = :vulnerability_occurrences
INITIAL_PIPELINE_INDEX = 'tmp_index_vulnerability_occurrences_id_and_initial_pipline_id'
LATEST_PIPELINE_INDEX = 'tmp_index_vulnerability_occurrences_id_and_latest_pipeline_id'
INITIAL_PIPELINE_COLUMNS = [:id, :initial_pipeline_id]
LATEST_PIPELINE_COLUMNS = [:id, :latest_pipeline_id]
milestone '17.3'
disable_ddl_transaction!
def up
remove_concurrent_index_by_name TABLE_NAME, name: INITIAL_PIPELINE_INDEX
remove_concurrent_index_by_name TABLE_NAME, name: LATEST_PIPELINE_INDEX
end
def down
add_concurrent_index TABLE_NAME, INITIAL_PIPELINE_COLUMNS, name: INITIAL_PIPELINE_INDEX,
where: 'initial_pipeline_id IS NULL'
add_concurrent_index TABLE_NAME, LATEST_PIPELINE_COLUMNS, name: LATEST_PIPELINE_INDEX,
where: 'latest_pipeline_id IS NULL'
end
end

View File

@ -0,0 +1 @@
1ceb75c51414ece44f8d25e14756d9926e7cf6f34b5b730706386fdbc769e1bd

View File

@ -30024,10 +30024,6 @@ CREATE INDEX tmp_index_on_vulnerabilities_non_dismissed ON vulnerabilities USING
CREATE INDEX tmp_index_project_statistics_cont_registry_size ON project_statistics USING btree (project_id) WHERE (container_registry_size = 0);
CREATE INDEX tmp_index_vulnerability_occurrences_id_and_initial_pipline_id ON vulnerability_occurrences USING btree (id, initial_pipeline_id) WHERE (initial_pipeline_id IS NULL);
CREATE INDEX tmp_index_vulnerability_occurrences_id_and_latest_pipeline_id ON vulnerability_occurrences USING btree (id, latest_pipeline_id) WHERE (latest_pipeline_id IS NULL);
CREATE INDEX tmp_index_vulnerability_overlong_title_html ON vulnerabilities USING btree (id) WHERE (length(title_html) > 800);
CREATE UNIQUE INDEX u_project_compliance_standards_adherence_for_reporting ON project_compliance_standards_adherence USING btree (project_id, check_name, standard);

View File

@ -22,7 +22,7 @@ and can no longer be changed:
To enable merge request approval settings for an instance:
1. On the left sidebar, at the bottom, select **Admin area**.
1. Select **Push Rules**.
1. Select **Push rules**.
1. Expand **Merge request approvals**.
1. Choose the required options.
1. Select **Save changes**.

View File

@ -29,9 +29,9 @@ Source Code Management shares ownership of Code Owners with the Code Review grou
- [Approval Rules](../../merge_request_concepts/approval_rules.md)
### Push Rules
### Push rules
- [Push Rules development guidelines](../../push_rules/index.md)
- [Push rules development guidelines](../../push_rules/index.md)
### Protected Branches

View File

@ -52,12 +52,14 @@ in the catalog.
or ask one of the group owners to create an empty project for you.
1. Follow the [standard guide for creating components](../../ci/components/index.md).
1. Add a concise project description that clearly describes the capabilities offered by the component project.
1. Ensure to follow the general guidance to [write a component](../../ci/components/index.md#write-a-component) as well as
[those for the official components](#best-practices-for-official-components).
1. Add a `LICENSE.md` file with the MIT license.
1. Make sure to follow the general guidance given to [write a component](../../ci/components/index.md#write-a-component) as well as
the guidance [for official components](#best-practices-for-official-components).
1. Add a `LICENSE.md` file with the MIT license ([example](https://gitlab.com/components/ruby/-/blob/d8db5288b01947e8a931d8d1a410befed69325a7/LICENSE.md)).
1. The project must have a `.gitlab-ci.yml` file that:
- Validates all the components in the project correctly.
- Contains a `release` job to publish newly released tags to the catalog.
- Validates all the components in the project correctly
([example](https://gitlab.com/components/secret-detection/-/blob/646d0fcbbf3c2a3e4b576f1884543c874041c633/.gitlab-ci.yml#L11-23)).
- Contains a `release` job to publish newly released tags to the catalog
([example](https://gitlab.com/components/secret-detection/-/blob/646d0fcbbf3c2a3e4b576f1884543c874041c633/.gitlab-ci.yml#L50-58)).
1. For official component projects, upload the [official avatar image](img/avatar_component_project.png) to the component project.
### Best practices for official components

View File

@ -73,7 +73,7 @@ batch size may be increased or decreased, based on the performance of the last 2
hide empty description
skinparam ConditionEndStyle hline
left to right direction
rectangle "Batched Background Migration Queue" as migrations {
rectangle "Batched background migration queue" as migrations {
rectangle "Migration N (active)" as migrationn
rectangle "Migration 1 (completed)" as migration1
rectangle "Migration 2 (active)" as migration2
@ -410,7 +410,7 @@ In the example above we need an index on `(type, id)` to support the filters. Se
### Access data for multiple databases
Background Migration contrary to regular migrations does have access to multiple databases
Background migration contrary to regular migrations does have access to multiple databases
and can be used to efficiently access and update data across them. To properly indicate
a database to be used it is desired to create ActiveRecord model inline the migration code.
Such model should use a correct [`ApplicationRecord`](multiple_databases.md#gitlab-schema)

View File

@ -13,9 +13,7 @@ Developers have two options for how set up a development environment for the Git
## Set up with Jira
### Install the app in Jira
The following are required to install and test the app:
The following are required to install the app:
- A Jira Cloud instance. Atlassian provides [free instances for development and testing](https://developer.atlassian.com/platform/marketplace/getting-started/#free-developer-instances-to-build-and-test-your-app).
- A GitLab instance available over the internet. For the app to work, Jira Cloud should
@ -38,6 +36,13 @@ The following are required to install and test the app:
Jira requires all connections to the app host to be over SSL. If you set up
your own environment, remember to enable SSL and an appropriate certificate.
### Setting up GitPod
If you are using [Gitpod](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/howto/gitpod.md)
you must [make port `3000` public](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/howto/gitpod.md#make-the-rails-web-server-publicly-accessible).
### Install the app in Jira
To install the app in Jira:
1. Enable Jira development mode to install apps that are not from the Atlassian
@ -65,17 +70,20 @@ To install the app in Jira:
You can also select **Getting Started** to open the configuration page rendered from your GitLab instance.
_Note that any changes to the app descriptor requires you to uninstall then reinstall the app._
1. You can now [set up the OAuth authentication flow](#set-up-the-gitlab-oauth-authentication-flow).
1. If the _Installed and ready to go!_ dialog opens asking you to **Get started**, do not get started yet
and instead select **Close**.
1. You must now [set up the OAuth authentication flow](#set-up-the-gitlab-oauth-authentication-flow).
### Set up the GitLab OAuth authentication flow
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117648) in GitLab 16.0. Feature flag `jira_connect_oauth` removed.
GitLab for Jira users authenticate with GitLab using GitLab OAuth.
GitLab for Jira users can authenticate with GitLab using GitLab OAuth.
Ensure you have [installed the app in Jira](#install-the-app-in-jira) first before doing these steps,
otherwise the app installation in Jira fails.
The following steps describe setting up an environment to test the GitLab OAuth flow:
1. Start a Gitpod session.
1. Start a [Gitpod session](#setting-up-gitpod).
1. On your GitLab instance, go to **Admin > Applications**.
1. Create a new application with the following settings:
- Name: `GitLab for Jira`
@ -88,12 +96,19 @@ The following steps describe setting up an environment to test the GitLab OAuth
1. Expand **GitLab for Jira App**.
1. Paste the **Application ID** value into **Jira Connect Application ID**.
1. In **Jira Connect Proxy URL**, enter `YOUR_GITPOD_INSTANCE` (for example, `https://xxxx.gitpod.io`).
1. Select **Enable public key storage**.
1. Enable public key storage: **Leave unchecked**.
1. Select **Save changes**.
### Setting up GitPod
### Set up the app in Jira
If you are using [Gitpod](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/howto/gitpod.md) you must [make port `3000` public](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/howto/gitpod.md#make-the-rails-web-server-publicly-accessible).
Ensure you have [set up OAuth first](#set-up-the-gitlab-oauth-authentication-flow) first before doing these steps,
otherwise these steps fail.
1. In Jira, go to **Jira settings > Apps > Manage apps**.
1. Scroll to **User-installed apps**, find your GitLab for Jira app and expand it.
1. Select **Get started**.
You should be able to authenticate with your GitLab instance and begin linking groups.
### Troubleshooting

View File

@ -4,16 +4,16 @@ group: Source Code
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
---
# Push Rules development guidelines
# Push rules development guidelines
This document was created to help contributors understand the code design of
[Push Rules](../../user/project/repository/push_rules.md). You should read this
[push rules](../../user/project/repository/push_rules.md). You should read this
document before making changes to the code for this feature.
This document is intentionally limited to an overview of how the code is
designed, as code can change often. To understand how a specific part of the
feature works, view the code and the specs. The details here explain how the
major components of the Push Rules feature work.
major components of the push rules feature work.
NOTE:
This document should be updated when parts of the codebase referenced in this

View File

@ -359,9 +359,36 @@ reproduction.
#### Hanging specs
If a spec hangs, it might be caused by a [bug in Rails](https://github.com/rails/rails/issues/45994):
If a spec hangs, or times out in CI, it might be caused by a
[LoadInterlockAwareMonitor deadlock bug in Rails](https://github.com/rails/rails/issues/45994).
To diagnose, you can use
[sigdump](https://github.com/fluent/sigdump/blob/master/README.md#usage)
to print the Ruby thread dump :
1. Run the hanging spec locally.
1. Trigger the Ruby thread dump by running this command:
```shell
kill -CONT <pid>
```
1. The thread dump will be saved to the `/tmp/sigdump-<pid>.log` file.
If you see lines with `load_interlock_aware_monitor.rb`, this is likely related:
```shell
/builds/gitlab-org/gitlab/vendor/ruby/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/concurrency/load_interlock_aware_monitor.rb:17:in `mon_enter'
/builds/gitlab-org/gitlab/vendor/ruby/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/concurrency/load_interlock_aware_monitor.rb:22:in `block in synchronize'
/builds/gitlab-org/gitlab/vendor/ruby/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `handle_interrupt'
/builds/gitlab-org/gitlab/vendor/ruby/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `synchronize'
```
See examples where we worked around by creating the factories before making
requests:
- <https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81112>
- <https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158890>
- <https://gitlab.com/gitlab-org/gitlab/-/issues/337039>
### Suggestions

View File

@ -32,7 +32,7 @@ encouraged for communications through system hooks.
## Push rules
1. On the left sidebar, at the bottom, select **Admin area**.
1. Select **Push Rules**.
1. Select **Push rules**.
Ensure that the following items are selected:

View File

@ -490,9 +490,9 @@ To support the following package managers, the GitLab analyzers proceed in two s
</tr>
<tr>
<td>maven</td>
<td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v5.2.14/build/gemnasium-maven/debian/config/.tool-versions#L3">3.8.8</a></td>
<td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v5.3.1/build/gemnasium-maven/debian/config/.tool-versions#L3">3.9.8</a></td>
<td>
<a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v5.2.14/spec/gemnasium-maven_image_spec.rb#L92-94">3.8.8</a><sup><b><a href="#exported-dependency-information-notes-1">1</a></b></sup>
<a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/-/blob/v5.3.1/spec/gemnasium-maven_image_spec.rb#L92-94">3.9.8</a><sup><b><a href="#exported-dependency-information-notes-1">1</a></b></sup>
</td>
</tr>
<tr>

View File

@ -24,13 +24,13 @@ In GitLab 15.4 and later, to configure push rules for a group:
1. On the left sidebar, select **Settings > Repository**.
1. Expand the **Pre-defined push rules** section.
1. Select the settings you want.
1. Select **Save Push Rules**.
1. Select **Save push rules**.
In GitLab 15.3 and earlier, to configure push rules for a group:
1. On the left sidebar, select **Push rules**.
1. Select the settings you want.
1. Select **Save Push Rules**.
1. Select **Save push rules**.
The group's new subgroups have push rules set for them based on either:

View File

@ -54,7 +54,7 @@ To set up a protected branch flow:
You can specify the format and security measures such as requiring SSH key signing for changes
coming into your code base with push rules:
- [Push Rules](../repository/push_rules.md)
- [Push rules](../repository/push_rules.md)
1. To ensure that the code is reviewed and checked by the right people in your team, use:

View File

@ -45,7 +45,7 @@ Prerequisites:
To create global push rules:
1. On the left sidebar, at the bottom, select **Admin area**.
1. Select **Push Rules**.
1. Select **Push rules**.
1. Expand **Push rules**.
1. Set the rule you want.
1. Select **Save push rules**.
@ -319,7 +319,7 @@ read [issue #19185](https://gitlab.com/gitlab-org/gitlab/-/issues/19185).
To update the push rules to be the same for all projects,
you need to use [the rails console](../../../administration/operations/rails_console.md#starting-a-rails-console-session),
or write a script to update each project using the [Push Rules API endpoint](../../../api/projects.md#push-rules).
or write a script to update each project using the [push rules API endpoint](../../../api/projects.md#push-rules).
For example, to enable **Check whether the commit author is a GitLab user** and **Do not allow users to remove Git tags with `git push`** checkboxes,
and create a filter for allowing commits from a specific email domain only through rails console:

View File

@ -125,7 +125,7 @@ module Gitlab
unfinished_count = Gitlab::Database::BackgroundMigration::BatchedMigration.unfinished.count
if unfinished_count > 0
raise MigrateError,
"Found #{unfinished_count} unfinished Background Migration(s). Please wait until they are finished."
"Found #{unfinished_count} unfinished background migration(s). Please wait until they are finished."
end
true

View File

@ -4732,7 +4732,7 @@ msgstr ""
msgid "Admin|Overview"
msgstr ""
msgid "Admin|Push Rules"
msgid "Admin|Push rules"
msgstr ""
msgid "Admin|Quarterly reconciliation will occur on %{qrtlyDate}"
@ -6934,6 +6934,9 @@ msgstr ""
msgid "Archiving the project makes it entirely read-only. It is hidden from the dashboard and doesn't display in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}"
msgstr ""
msgid "Are you ABSOLUTELY SURE you wish to delete this group?"
msgstr ""
msgid "Are you absolutely sure?"
msgstr ""
@ -17462,9 +17465,6 @@ msgstr ""
msgid "Delete group"
msgstr ""
msgid "Delete group immediately"
msgstr ""
msgid "Delete group immediately?"
msgstr ""
@ -17489,9 +17489,6 @@ msgstr ""
msgid "Delete project"
msgstr ""
msgid "Delete project immediately"
msgstr ""
msgid "Delete release"
msgstr ""
@ -17536,6 +17533,9 @@ msgstr ""
msgid "Delete this epic and release all child items?"
msgstr ""
msgid "Delete this project"
msgstr ""
msgid "Delete user list"
msgstr ""
@ -17596,6 +17596,9 @@ msgstr ""
msgid "Deleted commits:"
msgstr ""
msgid "Deleted group can not be restored!"
msgstr ""
msgid "Deleted projects cannot be restored!"
msgstr ""
@ -17617,6 +17620,9 @@ msgstr ""
msgid "Deleting the project will delete its repository and all related resources, including issues and merge requests."
msgstr ""
msgid "Deleting this group also deletes all child projects, including archived projects, and their resources."
msgstr ""
msgid "Deletion pending. This project will be deleted on %{date}. Repository and other project resources are read-only."
msgstr ""
@ -19497,6 +19503,15 @@ msgstr ""
msgid "DuoChat|What is a fork?"
msgstr ""
msgid "DuoCodeReview|Hey :wave: I'm starting to review your merge request and I will let you know when I'm finished."
msgstr ""
msgid "DuoCodeReview|I finished my review and found nothing to comment on. Nice work! :tada:"
msgstr ""
msgid "DuoCodeReview|I have encountered some issues while I was reviewing. Please try again later."
msgstr ""
msgid "DuoProDiscover|Ship software faster and more securely with AI integrated into your entire DevSecOps lifecycle."
msgstr ""
@ -38313,6 +38328,9 @@ msgstr ""
msgid "Permalink"
msgstr ""
msgid "Permanently delete group"
msgstr ""
msgid "Permissions"
msgstr ""
@ -42632,10 +42650,10 @@ msgstr ""
msgid "Promotions|Not now, thanks!"
msgstr ""
msgid "Promotions|Push Rules"
msgid "Promotions|Push rules"
msgstr ""
msgid "Promotions|Push Rules are defined per project so you can have different rules applied to different projects depends on your needs."
msgid "Promotions|Push rules are defined per project so you can have different rules applied to different projects depends on your needs."
msgstr ""
msgid "Promotions|Repository Mirroring"
@ -43248,12 +43266,6 @@ msgstr ""
msgid "Push Rule updated successfully."
msgstr ""
msgid "Push Rules"
msgstr ""
msgid "Push Rules updated successfully."
msgstr ""
msgid "Push an existing Git repository"
msgstr ""
@ -43278,6 +43290,12 @@ msgstr ""
msgid "Push project from command line"
msgstr ""
msgid "Push rules"
msgstr ""
msgid "Push rules updated successfully."
msgstr ""
msgid "Push the source branch up to GitLab."
msgstr ""
@ -54064,16 +54082,16 @@ msgstr ""
msgid "This action cannot be undone, and will permanently delete the %{key} SSH key. All commits signed using this SSH key will be marked as unverified."
msgstr ""
msgid "This action will permanently delete this group, including its subgroups and projects."
msgid "This action deletes %{codeOpen}%{project_path_with_namespace}%{codeClose} and everything this project contains. %{strongOpen}There is no going back.%{strongClose}"
msgstr ""
msgid "This action will permanently delete this project, including all its resources."
msgid "This action deletes %{codeOpen}%{project_path_with_namespace}%{codeClose} on %{date} and everything this project contains."
msgstr ""
msgid "This action will place this group, including its subgroups and projects, in a pending deletion state for %{deletion_delayed_period} days, and delete it permanently on %{date}."
msgid "This action deletes %{codeOpen}%{project_path_with_namespace}%{codeClose} on %{date} and everything this project contains. %{strongOpen}There is no going back.%{strongClose}"
msgstr ""
msgid "This action will place this project, including all its resources, in a pending deletion state for %{deletion_adjourned_period} days, and delete it permanently on %{strongOpen}%{date}%{strongClose}."
msgid "This action will %{strongOpen}permanently remove%{strongClose} %{codeOpen}%{group}%{codeClose} %{strongOpen}immediately%{strongClose}."
msgstr ""
msgid "This also resolves all related threads"
@ -54268,6 +54286,9 @@ msgstr ""
msgid "This group"
msgstr ""
msgid "This group and its subgroups and projects will be placed in a 'pending deletion' state for %{deletion_delayed_period} days, then permanently deleted on %{date}. The group can be fully restored before that date."
msgstr ""
msgid "This group can be restored until %{date}. %{linkStart}Learn more%{linkEnd}."
msgstr ""
@ -54298,9 +54319,6 @@ msgstr ""
msgid "This group is not permitted to create compliance violations"
msgstr ""
msgid "This group is scheduled for deletion on %{date}. This action will permanently delete this group, including its subgroups and projects, %{strong_open}immediately%{strong_close}. This action cannot be undone."
msgstr ""
msgid "This group is scheduled to be deleted on %{date}. You are about to delete this group, including its subgroups and projects, immediately. This action cannot be undone."
msgstr ""
@ -54612,9 +54630,6 @@ msgstr ""
msgid "This project is public. Non-members can guess the Service Desk email address, because it contains the group and project name. %{linkStart}How do I create a custom email address?%{linkEnd}"
msgstr ""
msgid "This project is scheduled for deletion on %{strongOpen}%{date}%{strongClose}. This action will permanently delete this project, including all its resources, %{strongOpen}immediately%{strongClose}. This action cannot be undone."
msgstr ""
msgid "This project manages its dependencies using %{strong_start}%{manager_name}%{strong_end}"
msgstr ""
@ -60154,6 +60169,9 @@ msgstr ""
msgid "Work in progress limit: %{wipLimit}"
msgstr ""
msgid "WorkItems|An error occurred while fetching children"
msgstr ""
msgid "WorkItem|%{count} more assignees"
msgstr ""

View File

@ -14,12 +14,14 @@ module QA
resource.add_name_uuid = false
resource.name = name
resource.path_with_namespace = "#{user.username}/#{name}"
resource.api_client = @api_client
resource.api_client = api_client
end
end
attribute :upstream do
Repository::ProjectPush.fabricate!.project
Resource::Project.fabricate_via_api! do |resource|
resource.initialize_with_readme = true
end
end
attribute :user do
@ -41,8 +43,6 @@ module QA
# Sign out as admin and sign is as the fork user
Flow::Login.sign_in(as: user)
@api_client = Runtime::API::Client.new(:gitlab, is_new_session: false, user: user)
upstream.visit!
Page::Project::Show.perform(&:fork_project)
@ -61,8 +61,6 @@ module QA
def fabricate_via_api!
populate(:upstream, :user)
@api_client = Runtime::API::Client.new(:gitlab, is_new_session: false, user: user)
Runtime::Logger.debug("Forking project #{upstream.name} to namespace #{user.username}...")
super
wait_until_forked
@ -76,6 +74,15 @@ module QA
user.remove_via_api! unless Specs::Helpers::ContextSelector.dot_com?
end
# Public api client method
# By default resources have api_client private. Fork requires operating with 2 users, so it needs to be public
# to correctly fabricate mr from fork
#
# @return [Runtime::API::Client]
def api_client
@api_client ||= Runtime::API::Client.new(:gitlab, is_new_session: false, user: user)
end
def api_get_path
"/projects/#{CGI.escape(path_with_namespace)}"
end

View File

@ -3,23 +3,26 @@
module QA
module Resource
class MergeRequestFromFork < MergeRequest
attr_accessor :fork_branch
attribute :fork do
Fork.fabricate_via_browser_ui!
Fork.fabricate_via_api!
end
attribute :push do
Repository::ProjectPush.fabricate! do |resource|
resource.project = fork.project
resource.branch_name = fork_branch
resource.file_name = "file2-#{SecureRandom.hex(8)}.txt"
resource.user = fork.user
attribute :project do
fork.project
end
attribute :source do
Repository::Commit.fabricate_via_api! do |resource|
resource.project = project
resource.api_client = api_client
resource.commit_message = 'This is a test commit'
resource.add_files([{ file_path: "file-#{SecureRandom.hex(8)}.txt", content: 'MR init' }])
resource.branch = project.default_branch
end
end
def fabricate!
populate(:push)
populate(:source)
fork.project.visit!
@ -37,8 +40,42 @@ module QA
visit(mr_url)
end
def api_post_body
super.merge({
target_project_id: upstream.id,
source_branch: project.default_branch,
target_branch: upstream.default_branch
})
end
def fabricate_via_api!
raise NotImplementedError
populate(:source)
super
end
# Fabricated mr needs to be fetched from upstream project rather than source project
#
# @return [String]
def api_get_path
"/projects/#{upstream.id}/merge_requests/#{iid}"
end
private
def api_client
fork.api_client
end
# Target is upstream, in fork workflow it must not be populated
#
# @return [Boolean]
def create_target?
false
end
def upstream
fork.upstream
end
end
end

View File

@ -3,21 +3,14 @@
module QA
RSpec.describe 'Create' do
describe 'Merge request creation from fork', product_group: :code_review do
let(:merge_request) do
Resource::MergeRequestFromFork.fabricate_via_browser_ui! do |merge_request|
merge_request.fork_branch = 'feature-branch'
end
end
let(:merge_request) { Resource::MergeRequestFromFork.fabricate_via_api! }
before do
Flow::Login.sign_in
end
after do
merge_request.fork.remove_via_api!
end
it 'can merge source branch from fork into upstream repository', :blocking, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347818' do
it 'can merge source branch from fork into upstream repository', :blocking,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347818' do
merge_request.visit!
Page::MergeRequest::Show.perform do |merge_request|

View File

@ -3,7 +3,7 @@
module RuboCop
module Cop
module BackgroundMigration
# Checks for rescuing errors inside Batched Background Migration Job Classes.
# Checks for rescuing errors inside batched background Migration Job Classes.
#
# @example
#

View File

@ -8,7 +8,7 @@ module RuboCop
class BackgroundMigrations < RuboCop::Cop::Base
include MigrationHelpers
MSG = 'Background migrations are deprecated. Please use a Batched Background Migration instead. '\
MSG = 'Background migrations are deprecated. Please use a batched background migration instead. '\
'More info: https://docs.gitlab.com/ee/development/database/batched_background_migrations.html'
def on_send(node)

View File

@ -11,7 +11,8 @@ class SemgrepResultProcessor
ALLOWED_API_URLS = %w[https://gitlab.com/api/v4].freeze
# Remove this when the feature is fully working
MESSAGE_FOOTER = <<-FOOTER
MESSAGE_FOOTER = <<~FOOTER
<small>
This AppSec automation is currently under testing.
@ -19,6 +20,7 @@ class SemgrepResultProcessor
For any detailed feedback, [add a comment here](https://gitlab.com/gitlab-com/gl-security/product-security/appsec/sast-custom-rules/-/issues/38).
</small>
/label ~"appsec-sast::commented"
FOOTER

View File

@ -12,7 +12,6 @@ describe('Project remove modal', () => {
const defaultProps = {
confirmPhrase: 'foo',
buttonText: 'Delete project',
formPath: 'some/path',
isFork: false,
issuesCount: 1,
@ -46,7 +45,6 @@ describe('Project remove modal', () => {
it('passes confirmPhrase and formPath props to the shared delete button', () => {
expect(findSharedDeleteButton().props()).toEqual({
confirmPhrase: defaultProps.confirmPhrase,
buttonText: defaultProps.buttonText,
forksCount: defaultProps.forksCount,
formPath: defaultProps.formPath,
isFork: defaultProps.isFork,

View File

@ -10,7 +10,6 @@ describe('DeleteButton', () => {
const findForm = () => wrapper.findComponent(GlForm);
const findModal = () => wrapper.findComponent(DeleteModal);
const findDeleteButton = () => wrapper.findComponent(GlButton);
const defaultPropsData = {
confirmPhrase: 'foo',
@ -34,18 +33,6 @@ describe('DeleteButton', () => {
});
};
it('renders the correct default title', () => {
createComponent();
expect(findDeleteButton().text()).toBe('Delete project');
});
it('renders a title passed via `buttonText` prop', () => {
createComponent({ buttonText: 'Delete project immediately' });
expect(findDeleteButton().text()).toBe('Delete project immediately');
});
it('renders modal and passes correct props', () => {
createComponent();

View File

@ -545,6 +545,18 @@ describe('WorkItemDetail component', () => {
expect(findHierarchyTree().exists()).toBe(true);
});
it.each([true, false])(
'passes hasChildren %s to WorkItemActions when `WorkItemTree` emits `childrenLoaded` %s',
async (hasChildren) => {
createComponent({ handler: objectiveHandler });
await waitForPromises();
await findHierarchyTree().vm.$emit('childrenLoaded', hasChildren);
expect(findWorkItemActions().props('hasChildren')).toBe(hasChildren);
},
);
it('renders a modal', async () => {
createComponent({ handler: objectiveHandler });
await waitForPromises();

View File

@ -25,7 +25,7 @@ import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query
import groupWorkItemTypesQuery from '~/work_items/graphql/group_work_item_types.query.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemHierarchyMutation from '~/work_items/graphql/update_work_item_hierarchy.mutation.graphql';
import groupProjectsForLinksWidgetQuery from '~/work_items/graphql/group_projects_for_links_widget.query.graphql';
import relatedProjectsForLinksWidgetQuery from '~/work_items/graphql/related_projects_for_links_widget.query.graphql';
import {
@ -88,7 +88,7 @@ describe('WorkItemLinksForm', () => {
[groupWorkItemTypesQuery, groupWorkItemTypesResolver],
[groupProjectsForLinksWidgetQuery, groupProjectsFormLinksWidgetResolver],
[relatedProjectsForLinksWidgetQuery, relatedProjectsForLinksWidgetResolver],
[updateWorkItemMutation, updateMutation],
[updateWorkItemHierarchyMutation, updateMutation],
[createWorkItemMutation, createMutationResolver],
]),
propsData: {

View File

@ -1,12 +1,14 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlToggle } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import setWindowLocation from 'helpers/set_window_location_helper';
import { RENDER_ALL_SLOTS_TEMPLATE, stubComponent } from 'helpers/stub_component';
import issueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import { resolvers } from '~/graphql_shared/issuable_client';
import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
@ -14,13 +16,13 @@ import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/wor
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import { FORM_TYPES } from '~/work_items/constants';
import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql';
import {
getIssueDetailsResponse,
groupWorkItemByIidResponseFactory,
workItemHierarchyResponse,
workItemHierarchyEmptyResponse,
workItemHierarchyTreeResponse,
workItemHierarchyTreeEmptyResponse,
workItemHierarchyNoUpdatePermissionResponse,
workItemByIidResponseFactory,
mockWorkItemCommentNote,
@ -34,7 +36,7 @@ describe('WorkItemLinks', () => {
let wrapper;
let mockApollo;
const responseWithAddChildPermission = jest.fn().mockResolvedValue(workItemHierarchyResponse);
const responseWithAddChildPermission = jest.fn().mockResolvedValue(workItemHierarchyTreeResponse);
const groupResponseWithAddChildPermission = jest
.fn()
.mockResolvedValue(groupWorkItemByIidResponseFactory());
@ -50,8 +52,7 @@ describe('WorkItemLinks', () => {
} = {}) => {
mockApollo = createMockApollo(
[
[workItemByIidQuery, fetchHandler],
[groupWorkItemByIidQuery, groupResponseWithAddChildPermission],
[getWorkItemTreeQuery, fetchHandler],
[issueDetailsQuery, issueDetailsQueryHandler],
],
resolvers,
@ -149,7 +150,7 @@ describe('WorkItemLinks', () => {
describe('when no child links', () => {
beforeEach(async () => {
await createComponent({
fetchHandler: jest.fn().mockResolvedValue(workItemHierarchyEmptyResponse),
fetchHandler: jest.fn().mockResolvedValue(workItemHierarchyTreeEmptyResponse),
});
});
@ -162,7 +163,7 @@ describe('WorkItemLinks', () => {
await createComponent();
expect(findWorkItemLinkChildrenWrapper().exists()).toBe(true);
expect(findWorkItemLinkChildrenWrapper().props().children).toHaveLength(4);
expect(findWorkItemLinkChildrenWrapper().props().children).toHaveLength(1);
});
it('shows an alert when list loading fails', async () => {
@ -178,7 +179,7 @@ describe('WorkItemLinks', () => {
await createComponent();
expect(findChildrenCount().exists()).toBe(true);
expect(findChildrenCount().text()).toContain('4');
expect(findChildrenCount().text()).toContain('1');
});
describe('when no permission to update', () => {
@ -267,20 +268,6 @@ describe('WorkItemLinks', () => {
});
});
describe('when group context', () => {
it('skips calling the project work item query', () => {
createComponent({ isGroup: true });
expect(responseWithAddChildPermission).not.toHaveBeenCalled();
});
it('calls the group work item query', () => {
createComponent({ isGroup: true });
expect(groupResponseWithAddChildPermission).toHaveBeenCalled();
});
});
it.each`
toggleValue
${true}

View File

@ -1,13 +1,16 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlToggle } from '@gitlab/ui';
import { GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue';
import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
import WorkItemActionsSplitButton from '~/work_items/components/work_item_links/work_item_actions_split_button.vue';
import WorkItemTreeActions from '~/work_items/components/work_item_links/work_item_tree_actions.vue';
import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql';
import {
FORM_TYPES,
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
@ -17,14 +20,23 @@ import {
WORK_ITEM_TYPE_VALUE_EPIC,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
} from '~/work_items/constants';
import { childrenWorkItems } from '../../mock_data';
import {
workItemHierarchyTreeResponse,
workItemHierarchyTreeEmptyResponse,
workItemHierarchyNoUpdatePermissionResponse,
} from '../../mock_data';
Vue.use(VueApollo);
describe('WorkItemTree', () => {
let wrapper;
const workItemHierarchyTreeResponseHandler = jest
.fn()
.mockResolvedValue(workItemHierarchyTreeResponse);
const findEmptyState = () => wrapper.findByTestId('tree-empty');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findToggleFormSplitButton = () => wrapper.findComponent(WorkItemActionsSplitButton);
const findForm = () => wrapper.findComponent(WorkItemLinksForm);
const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper);
@ -32,15 +44,15 @@ describe('WorkItemTree', () => {
const findShowLabelsToggle = () => wrapper.findComponent(GlToggle);
const findTreeActions = () => wrapper.findComponent(WorkItemTreeActions);
const createComponent = ({
const createComponent = async ({
workItemType = 'Objective',
workItemIid = '2',
parentWorkItemType = 'Objective',
confidential = false,
children = childrenWorkItems,
canUpdate = true,
canUpdateChildren = true,
hasSubepicsFeature = true,
workItemHierarchyTreeHandler = workItemHierarchyTreeResponseHandler,
} = {}) => {
wrapper = shallowMountExtended(WorkItemTree, {
propsData: {
@ -48,17 +60,18 @@ describe('WorkItemTree', () => {
workItemType,
workItemIid,
parentWorkItemType,
workItemId: 'gid://gitlab/WorkItem/515',
workItemId: 'gid://gitlab/WorkItem/2',
confidential,
children,
canUpdate,
canUpdateChildren,
},
apolloProvider: createMockApollo([[getWorkItemTreeQuery, workItemHierarchyTreeHandler]]),
provide: {
hasSubepicsFeature,
},
stubs: { WidgetWrapper },
});
await waitForPromises();
};
it('displays Add button', () => {
@ -67,17 +80,25 @@ describe('WorkItemTree', () => {
expect(findToggleFormSplitButton().exists()).toBe(true);
});
it('displays empty state if there are no children', () => {
createComponent({ children: [] });
it('displays empty state if there are no children', async () => {
await createComponent({
workItemHierarchyTreeHandler: jest.fn().mockResolvedValue(workItemHierarchyTreeEmptyResponse),
});
expect(findEmptyState().exists()).toBe(true);
});
it('renders hierarchy widget children container', () => {
it('displays loading-icon while children are being loaded', () => {
createComponent();
expect(findLoadingIcon().exists()).toBe(true);
});
it('renders hierarchy widget children container', async () => {
await createComponent();
expect(findWorkItemLinkChildrenWrapper().exists()).toBe(true);
expect(findWorkItemLinkChildrenWrapper().props().children).toHaveLength(4);
expect(findWorkItemLinkChildrenWrapper().props().children).toHaveLength(1);
});
it('does not display form by default', () => {
@ -88,7 +109,7 @@ describe('WorkItemTree', () => {
it('shows an error message on error', async () => {
const errorMessage = 'Some error';
createComponent();
await createComponent();
findWorkItemLinkChildrenWrapper().vm.$emit('error', errorMessage);
await nextTick();
@ -167,10 +188,13 @@ describe('WorkItemTree', () => {
});
describe('when no permission to update', () => {
beforeEach(() => {
createComponent({
beforeEach(async () => {
await createComponent({
canUpdate: false,
canUpdateChildren: false,
workItemHierarchyTreeHandler: jest
.fn()
.mockResolvedValue(workItemHierarchyNoUpdatePermissionResponse),
});
});
@ -200,7 +224,7 @@ describe('WorkItemTree', () => {
`(
'passes showLabels as $toggleValue to child items when toggle is $toggleValue',
async ({ toggleValue }) => {
createComponent();
await createComponent();
findShowLabelsToggle().vm.$emit('change', toggleValue);
@ -217,8 +241,8 @@ describe('WorkItemTree', () => {
${false} | ${WORK_ITEM_TYPE_VALUE_OBJECTIVE}
`(
'When displaying a $workItemType, it is $visible that the action menu is rendered',
({ workItemType, visible }) => {
createComponent({ workItemType });
async ({ workItemType, visible }) => {
await createComponent({ workItemType });
expect(findTreeActions().exists()).toBe(visible);
},

View File

@ -7,7 +7,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import WorkItemParent from '~/work_items/components/work_item_parent.vue';
import { removeHierarchyChild } from '~/work_items/graphql/cache_utils';
import { updateParent } from '~/work_items/graphql/cache_utils';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import groupWorkItemsQuery from '~/work_items/graphql/group_work_items.query.graphql';
import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
@ -23,7 +23,7 @@ import {
jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/work_items/graphql/cache_utils', () => ({
removeHierarchyChild: jest.fn(),
updateParent: jest.fn(),
}));
describe('WorkItemParent component', () => {
@ -337,7 +337,7 @@ describe('WorkItemParent component', () => {
},
});
expect(removeHierarchyChild).toHaveBeenCalledWith({
expect(updateParent).toHaveBeenCalledWith({
cache: expect.anything(Object),
fullPath: mockFullPath,
iid: undefined,
@ -373,7 +373,7 @@ describe('WorkItemParent component', () => {
},
},
});
expect(removeHierarchyChild).toHaveBeenCalledWith({
expect(updateParent).toHaveBeenCalledWith({
cache: expect.anything(Object),
fullPath: mockFullPath,
iid: '1',

View File

@ -1,29 +1,26 @@
import { WIDGET_TYPE_HIERARCHY } from '~/work_items/constants';
import { addHierarchyChild, removeHierarchyChild } from '~/work_items/graphql/cache_utils';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql';
describe('work items graphql cache utils', () => {
const fullPath = 'full/path';
const iid = '10';
const id = 'gid://gitlab/WorkItem/10';
const mockCacheData = {
workspace: {
workItem: {
id: 'gid://gitlab/WorkItem/10',
title: 'Work item',
widgets: [
{
type: WIDGET_TYPE_HIERARCHY,
children: {
nodes: [
{
id: 'gid://gitlab/WorkItem/20',
title: 'Child',
},
],
},
workItem: {
id: 'gid://gitlab/WorkItem/10',
title: 'Work item',
widgets: [
{
type: WIDGET_TYPE_HIERARCHY,
children: {
nodes: [
{
id: 'gid://gitlab/WorkItem/20',
title: 'Child',
},
],
},
],
},
},
],
},
};
@ -39,31 +36,29 @@ describe('work items graphql cache utils', () => {
title: 'New child',
};
addHierarchyChild({ cache: mockCache, fullPath, iid, workItem: child });
addHierarchyChild({ cache: mockCache, id, workItem: child });
expect(mockCache.writeQuery).toHaveBeenCalledWith({
query: workItemByIidQuery,
variables: { fullPath, iid },
query: getWorkItemTreeQuery,
variables: { id },
data: {
workspace: {
workItem: {
id: 'gid://gitlab/WorkItem/10',
title: 'Work item',
widgets: [
{
type: WIDGET_TYPE_HIERARCHY,
children: {
nodes: [
{
id: 'gid://gitlab/WorkItem/20',
title: 'Child',
},
child,
],
},
workItem: {
id: 'gid://gitlab/WorkItem/10',
title: 'Work item',
widgets: [
{
type: WIDGET_TYPE_HIERARCHY,
children: {
nodes: [
{
id: 'gid://gitlab/WorkItem/20',
title: 'Child',
},
child,
],
},
],
},
},
],
},
},
});
@ -80,7 +75,7 @@ describe('work items graphql cache utils', () => {
title: 'New child',
};
addHierarchyChild({ cache: mockCache, fullPath, iid, workItem: child });
addHierarchyChild({ cache: mockCache, id, workItem: child });
expect(mockCache.writeQuery).not.toHaveBeenCalled();
});
@ -98,25 +93,23 @@ describe('work items graphql cache utils', () => {
title: 'Child',
};
removeHierarchyChild({ cache: mockCache, fullPath, iid, workItem: childToRemove });
removeHierarchyChild({ cache: mockCache, id, workItem: childToRemove });
expect(mockCache.writeQuery).toHaveBeenCalledWith({
query: workItemByIidQuery,
variables: { fullPath, iid },
query: getWorkItemTreeQuery,
variables: { id },
data: {
workspace: {
workItem: {
id: 'gid://gitlab/WorkItem/10',
title: 'Work item',
widgets: [
{
type: WIDGET_TYPE_HIERARCHY,
children: {
nodes: [],
},
workItem: {
id: 'gid://gitlab/WorkItem/10',
title: 'Work item',
widgets: [
{
type: WIDGET_TYPE_HIERARCHY,
children: {
nodes: [],
},
],
},
},
],
},
},
});
@ -133,7 +126,7 @@ describe('work items graphql cache utils', () => {
title: 'Child',
};
removeHierarchyChild({ cache: mockCache, fullPath, iid, workItem: childToRemove });
removeHierarchyChild({ cache: mockCache, id, workItem: childToRemove });
expect(mockCache.writeQuery).not.toHaveBeenCalled();
});

View File

@ -1417,6 +1417,14 @@ export const workItemHierarchyEmptyResponse = {
parent: null,
hasChildren: false,
children: {
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
__typename: 'PageInfo',
},
count: 1,
nodes: [],
__typename: 'WorkItemConnection',
},
@ -1471,6 +1479,14 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
parent: null,
hasChildren: true,
children: {
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
__typename: 'PageInfo',
},
count: 1,
nodes: [
{
id: 'gid://gitlab/WorkItem/2',
@ -1767,6 +1783,65 @@ export const workItemObjectiveNoMetadata = {
],
};
export const workItemHierarchyTreeEmptyResponse = {
data: {
workItem: {
id: 'gid://gitlab/WorkItem/2',
iid: '2',
archived: false,
workItemType: {
id: 'gid://gitlab/WorkItems::Type/2411',
name: 'Objective',
iconName: 'issue-type-objective',
__typename: 'WorkItemType',
},
title: 'New title',
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
setWorkItemMetadata: true,
adminParentLink: true,
createNote: true,
adminWorkItemLink: true,
__typename: 'WorkItemPermissions',
},
confidential: false,
reference: 'test-project-path#2',
namespace: {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
name: 'Project name',
},
widgets: [
{
type: 'DESCRIPTION',
__typename: 'WorkItemWidgetDescription',
},
{
type: 'HIERARCHY',
parent: null,
hasChildren: true,
children: {
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
__typename: 'PageInfo',
},
count: 0,
nodes: [],
__typename: 'WorkItemConnection',
},
__typename: 'WorkItemWidgetHierarchy',
},
],
__typename: 'WorkItem',
},
},
};
export const workItemHierarchyTreeResponse = {
data: {
workItem: {
@ -1807,10 +1882,18 @@ export const workItemHierarchyTreeResponse = {
parent: null,
hasChildren: true,
children: {
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
__typename: 'PageInfo',
},
count: 1,
nodes: [
{
id: 'gid://gitlab/WorkItem/13',
iid: '13',
id: 'gid://gitlab/WorkItem/2',
iid: '2',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/2411',
name: 'Objective',

View File

@ -172,7 +172,7 @@ RSpec.describe Gitlab::Database::Decomposition::Migrate, :delete, query_analyzer
it 'raises error' do
expect { process }.to raise_error(
Gitlab::Database::Decomposition::MigrateError,
"Found 1 unfinished Background Migration(s). Please wait until they are finished."
"Found 1 unfinished background migration(s). Please wait until they are finished."
)
end
end

View File

@ -9,7 +9,7 @@ RSpec.describe RuboCop::Cop::Migration::BackgroundMigrations do
expect_offense(<<~RUBY)
def up
queue_background_migration_jobs_by_range_at_intervals('example', 'example', 1, batch_size: 1, track_jobs: true)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Background migrations are deprecated. Please use a Batched Background Migration instead[...]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Background migrations are deprecated. Please use a batched background migration instead[...]
end
RUBY
end
@ -20,7 +20,7 @@ RSpec.describe RuboCop::Cop::Migration::BackgroundMigrations do
expect_offense(<<~RUBY)
def up
requeue_background_migration_jobs_by_range_at_intervals('example', 1)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Background migrations are deprecated. Please use a Batched Background Migration instead[...]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Background migrations are deprecated. Please use a batched background migration instead[...]
end
RUBY
end
@ -31,7 +31,7 @@ RSpec.describe RuboCop::Cop::Migration::BackgroundMigrations do
expect_offense(<<~RUBY)
def up
migrate_in(1, 'example', 1, ['example'])
^^^^^^^^^^ Background migrations are deprecated. Please use a Batched Background Migration instead[...]
^^^^^^^^^^ Background migrations are deprecated. Please use a batched background migration instead[...]
end
RUBY
end

View File

@ -71,7 +71,7 @@ RSpec.describe SemgrepResultProcessor, feature_category: :tooling do
{
"id" => 1933334610,
"type" => "DiffNote",
"body" => "Deserializing user-controlled objects can cause vulnerabilities. \n\n \u003csmall\u003e\n This AppSec automation is currently under testing.\n Use ~\"appsec-sast::helpful\" or ~\"appsec-sast::unhelpful\" for quick feedback.\n For any detailed feedback, [add a comment here](https://gitlab.com/gitlab-com/gl-security/product-security/appsec/sast-custom-rules/-/issues/38).\n \u003c/small\u003e\n\n /label ~\"appsec-sast::commented\"",
"body" => "Deserializing user-controlled objects can cause vulnerabilities.\n\n\n\u003csmall\u003e\nThis AppSec automation is currently under testing.\nUse ~\"appsec-sast::helpful\" or ~\"appsec-sast::unhelpful\" for quick feedback.\nFor any detailed feedback, [add a comment here](https://gitlab.com/gitlab-com/gl-security/product-security/appsec/sast-custom-rules/-/issues/38).\n\u003c/small\u003e\n\n\n/label ~\"appsec-sast::commented\"",
"author" => {
"id" => 21564538
},