Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-10-04 12:17:55 +00:00
parent b68afda329
commit 49d36ce6e3
109 changed files with 1149 additions and 839 deletions

View File

@ -160,7 +160,6 @@ RSpec/ContextWording:
- 'ee/spec/features/projects/settings/push_rules_settings_spec.rb'
- 'ee/spec/features/promotion_spec.rb'
- 'ee/spec/features/protected_branches_spec.rb'
- 'ee/spec/features/signup_spec.rb'
- 'ee/spec/features/users/login_spec.rb'
- 'ee/spec/features/users/signup_spec.rb'
- 'ee/spec/finders/approval_rules/group_finder_spec.rb'

View File

@ -12,7 +12,6 @@ RSpec/ExpectInHook:
- 'ee/spec/features/projects/feature_flags/user_creates_feature_flag_spec.rb'
- 'ee/spec/features/projects/feature_flags/user_deletes_feature_flag_spec.rb'
- 'ee/spec/features/projects/settings/ee/service_desk_setting_spec.rb'
- 'ee/spec/features/signup_spec.rb'
- 'ee/spec/finders/license_template_finder_spec.rb'
- 'ee/spec/finders/projects/integrations/jira/issues_finder_spec.rb'
- 'ee/spec/finders/template_finder_spec.rb'

View File

@ -56,7 +56,7 @@ export default {
if (window.location.hash) {
const hash = getLocationHash();
const lineToMatch = `L${line.lineNumber + 1}`;
const lineToMatch = `L${line.lineNumber}`;
if (hash === lineToMatch) {
applyHashHighlight = true;

View File

@ -46,7 +46,7 @@ export default {
},
mounted() {
const hash = getLocationHash();
const lineToMatch = `L${this.line.lineNumber + 1}`;
const lineToMatch = `L${this.line.lineNumber}`;
if (hash === lineToMatch) {
this.applyHashHighlight = true;

View File

@ -14,8 +14,7 @@ export default {
render(h, { props }) {
const { lineNumber, path } = props;
const parsedLineNumber = lineNumber + 1;
const lineId = `L${parsedLineNumber}`;
const lineId = `L${lineNumber}`;
const lineHref = `${path}#${lineId}`;
return h(
@ -27,7 +26,7 @@ export default {
href: lineHref,
},
},
parsedLineNumber,
lineNumber,
);
},
};

View File

@ -19,20 +19,17 @@ export const parseLine = (line = {}, lineNumber) => ({
* @param Number lineNumber
*/
export const parseHeaderLine = (line = {}, lineNumber, hash) => {
let isClosed = parseBoolean(line.section_options?.collapsed);
// if a hash is present in the URL then we ensure
// all sections are visible so we can scroll to the hash
// in the DOM
if (hash) {
return {
isClosed: false,
isHeader: true,
line: parseLine(line, lineNumber),
lines: [],
};
isClosed = false;
}
return {
isClosed: parseBoolean(line.section_options?.collapsed),
isClosed,
isHeader: true,
line: parseLine(line, lineNumber),
lines: [],
@ -80,27 +77,28 @@ export const isCollapsibleSection = (acc = [], last = {}, section = {}) =>
section.section === last.line.section;
/**
* Returns the lineNumber of the last line in
* a parsed log
* Returns the next line number in the parsed log
*
* @param Array acc
* @returns Number
*/
export const getIncrementalLineNumber = (acc) => {
let lineNumberValue;
const lastIndex = acc.length - 1;
const lastElement = acc[lastIndex];
export const getNextLineNumber = (acc) => {
if (!acc?.length) {
return 1;
}
const lastElement = acc[acc.length - 1];
const nestedLines = lastElement.lines;
if (lastElement.isHeader && !nestedLines.length && lastElement.line) {
lineNumberValue = lastElement.line.lineNumber;
} else if (lastElement.isHeader && nestedLines.length) {
lineNumberValue = nestedLines[nestedLines.length - 1].lineNumber;
} else {
lineNumberValue = lastElement.lineNumber;
return lastElement.line.lineNumber + 1;
}
return lineNumberValue === 0 ? 1 : lineNumberValue + 1;
if (lastElement.isHeader && nestedLines.length) {
return nestedLines[nestedLines.length - 1].lineNumber + 1;
}
return lastElement.lineNumber + 1;
};
/**
@ -119,31 +117,28 @@ export const getIncrementalLineNumber = (acc) => {
* @returns Array parsed log lines
*/
export const logLinesParser = (lines = [], prevLogLines = [], hash = '') =>
lines.reduce(
(acc, line, index) => {
const lineNumber = acc.length > 0 ? getIncrementalLineNumber(acc) : index;
lines.reduce((acc, line) => {
const lineNumber = getNextLineNumber(acc);
const last = acc[acc.length - 1];
const last = acc[acc.length - 1];
// If the object is an header, we parse it into another structure
if (line.section_header) {
acc.push(parseHeaderLine(line, lineNumber, hash));
} else if (isCollapsibleSection(acc, last, line)) {
// if the object belongs to a nested section, we append it to the new `lines` array of the
// previously formatted header
last.lines.push(parseLine(line, lineNumber));
} else if (line.section_duration) {
// if the line has section_duration, we look for the correct header to add it
addDurationToHeader(acc, line);
} else {
// otherwise it's a regular line
acc.push(parseLine(line, lineNumber));
}
// If the object is an header, we parse it into another structure
if (line.section_header) {
acc.push(parseHeaderLine(line, lineNumber, hash));
} else if (isCollapsibleSection(acc, last, line)) {
// if the object belongs to a nested section, we append it to the new `lines` array of the
// previously formatted header
last.lines.push(parseLine(line, lineNumber));
} else if (line.section_duration) {
// if the line has section_duration, we look for the correct header to add it
addDurationToHeader(acc, line);
} else {
// otherwise it's a regular line
acc.push(parseLine(line, lineNumber));
}
return acc;
},
[...prevLogLines],
);
return acc;
}, prevLogLines);
/**
* Finds the repeated offset, removes the old one

View File

@ -362,7 +362,12 @@ export default {
},
},
update: (cache, { data: { workItemCreate } }) =>
addHierarchyChild(cache, this.fullPath, String(this.issueIid), workItemCreate.workItem),
addHierarchyChild({
cache,
fullPath: this.fullPath,
iid: String(this.issueIid),
workItem: workItemCreate.workItem,
}),
});
const { workItem, errors } = data.workItemCreate;
@ -392,7 +397,12 @@ export default {
mutation: deleteWorkItemMutation,
variables: { input: { id } },
update: (cache) =>
removeHierarchyChild(cache, this.fullPath, String(this.issueIid), { id }),
removeHierarchyChild({
cache,
fullPath: this.fullPath,
iid: String(this.issueIid),
workItem: { id },
}),
});
if (data.workItemDelete.errors?.length) {

View File

@ -37,11 +37,11 @@ export const I18N_OAUTH_FAILED_MESSAGE = s__(
export const INTEGRATIONS_DOC_LINK = helpPagePath('integration/jira/development_panel', {
anchor: 'use-the-integration',
});
export const OAUTH_SELF_MANAGED_DOC_LINK = helpPagePath('integration/jira/connect-app', {
anchor: 'connect-the-gitlab-for-jira-cloud-app-for-self-managed-instances',
export const OAUTH_SELF_MANAGED_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
anchor: 'set-up-oauth-authentication',
});
export const FAILED_TO_UPDATE_DOC_LINK = helpPagePath('integration/jira/connect-app', {
anchor: 'failed-to-update-the-gitlab-instance-for-self-managed-instances',
export const FAILED_TO_UPDATE_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
anchor: 'failed-to-update-the-gitlab-instance',
});
export const GITLAB_COM_BASE_PATH = 'https://gitlab.com';

View File

@ -77,7 +77,7 @@ export default {
<template>
<gl-disclosure-dropdown-item
data-qa-selector="delete_member_dropdown_item"
data-testid="delete-member-dropdown-item"
@action="showRemoveMemberModal(modalData)"
>
<template #list-item>

View File

@ -109,7 +109,6 @@ export default {
no-caret
placement="right"
data-testid="user-action-dropdown"
data-qa-selector="user_action_dropdown"
>
<disable-two-factor-dropdown-item
v-if="permissions.canDisableTwoFactor"

View File

@ -0,0 +1,4 @@
import { WORKSPACE_GROUP } from '~/issues/constants';
import { initWorkItemsRoot } from '~/work_items';
initWorkItemsRoot(WORKSPACE_GROUP);

View File

@ -1,3 +1,3 @@
import { initWorkItemsRoot } from '~/work_items/index';
import { initWorkItemsRoot } from '~/work_items';
initWorkItemsRoot();

View File

@ -118,7 +118,7 @@ export default {
class="table tree-table"
:class="{ 'gl-table-layout-fixed': !showParentRow }"
aria-live="polite"
data-qa-selector="file_tree_table"
data-testid="file-tree-table"
>
<table-header v-once />
<tbody>

View File

@ -219,7 +219,7 @@ export default {
'is-submodule': isSubmodule,
}"
class="tree-item-link str-truncated"
data-qa-selector="file_name_link"
data-testid="file-name-link"
>
<file-icon
:file-name="fullPath"

View File

@ -41,7 +41,7 @@ export default {
item.extraAttrs = {
...USER_MENU_TRACKING_DEFAULTS,
'data-track-label': 'user_profile',
'data-testid': 'user_profile_link',
'data-testid': 'user-profile-link',
};
}

View File

@ -5,6 +5,7 @@ import { ASC } from '~/notes/constants';
import { __ } from '~/locale';
import { clearDraft } from '~/lib/utils/autosave';
import createNoteMutation from '../../graphql/notes/create_work_item_note.mutation.graphql';
import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import { TRACKING_CATEGORY_SHOW, i18n } from '../../constants';
import WorkItemNoteSignedOut from './work_item_note_signed_out.vue';
@ -21,7 +22,7 @@ export default {
WorkItemCommentForm,
},
mixins: [Tracking.mixin()],
inject: ['fullPath'],
inject: ['fullPath', 'isGroup'],
props: {
workItemId: {
type: String,
@ -90,7 +91,9 @@ export default {
},
apollo: {
workItem: {
query: workItemByIidQuery,
query() {
return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
},
variables() {
return {
fullPath: this.fullPath,

View File

@ -3,7 +3,6 @@ import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import Tracking from '~/tracking';
import { updateDraft, clearDraft } from '~/lib/utils/autosave';
import { renderMarkdown } from '~/notes/utils';
@ -11,15 +10,17 @@ import { getLocationHash } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import EditedAt from '~/issues/show/components/edited.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
import NoteHeader from '~/notes/components/note_header.vue';
import NoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import { i18n, TRACKING_CATEGORY_SHOW } from '../../constants';
import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import updateWorkItemNoteMutation from '../../graphql/notes/update_work_item_note.mutation.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import { isAssigneesWidget } from '../../utils';
import WorkItemCommentForm from './work_item_comment_form.vue';
import NoteActions from './work_item_note_actions.vue';
import WorkItemNoteAwardsList from './work_item_note_awards_list.vue';
import NoteBody from './work_item_note_body.vue';
export default {
name: 'WorkItemNoteThread',
@ -35,7 +36,7 @@ export default {
EditedAt,
},
mixins: [Tracking.mixin()],
inject: ['fullPath'],
inject: ['fullPath', 'isGroup'],
props: {
workItemId: {
type: String,
@ -169,7 +170,9 @@ export default {
},
apollo: {
workItem: {
query: workItemByIidQuery,
query() {
return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
},
variables() {
return {
fullPath: this.fullPath,

View File

@ -15,7 +15,8 @@ import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import toast from '~/vue_shared/plugins/global_toast';
import { isLoggedIn } from '~/lib/utils/common_utils';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import {
sprintfWorkItem,
@ -70,7 +71,7 @@ export default {
copyCreateNoteEmailTestId: TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
deleteActionTestId: TEST_ID_DELETE_ACTION,
promoteActionTestId: TEST_ID_PROMOTE_ACTION,
inject: ['fullPath'],
inject: ['fullPath', 'isGroup'],
props: {
workItemId: {
type: String,
@ -256,7 +257,7 @@ export default {
},
updateWorkItemNotificationsWidgetCache({ cache, issue }) {
const query = {
query: workItemByIidQuery,
query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath: this.fullPath, iid: this.workItemIid },
};
// Read the work item object

View File

@ -3,10 +3,11 @@ import { GlAvatarLink, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { WORKSPACE_PROJECT } from '~/issues/constants';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import WorkItemStateBadge from '~/work_items/components/work_item_state_badge.vue';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import WorkItemStateBadge from './work_item_state_badge.vue';
import WorkItemTypeIcon from './work_item_type_icon.vue';
export default {
components: {
@ -18,7 +19,7 @@ export default {
ConfidentialityBadge,
GlLoadingIcon,
},
inject: ['fullPath'],
inject: ['fullPath', 'isGroup'],
props: {
workItemIid: {
type: String,
@ -59,7 +60,9 @@ export default {
},
apollo: {
workItem: {
query: workItemByIidQuery,
query() {
return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
},
variables() {
return {
fullPath: this.fullPath,

View File

@ -10,6 +10,7 @@ import Tracking from '~/tracking';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import { autocompleteDataSources, markdownPreviewPath } from '../utils';
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 { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_DESCRIPTION } from '../constants';
import WorkItemDescriptionRendered from './work_item_description_rendered.vue';
@ -25,7 +26,7 @@ export default {
WorkItemDescriptionRendered,
},
mixins: [Tracking.mixin()],
inject: ['fullPath'],
inject: ['fullPath', 'isGroup'],
props: {
workItemId: {
type: String,
@ -55,7 +56,9 @@ export default {
},
apollo: {
workItem: {
query: workItemByIidQuery,
query() {
return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
},
variables() {
return {
fullPath: this.fullPath,

View File

@ -16,7 +16,6 @@ import { getParameterByName, updateHistory, setUrlParams } from '~/lib/utils/url
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isLoggedIn } from '~/lib/utils/common_utils';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import { WORKSPACE_PROJECT } from '~/issues/constants';
@ -37,6 +36,7 @@ import {
import workItemUpdatedSubscription from '../graphql/work_item_updated.subscription.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import { findHierarchyWidgetChildren } from '../utils';
@ -52,6 +52,7 @@ import WorkItemDetailModal from './work_item_detail_modal.vue';
import WorkItemAwardEmoji from './work_item_award_emoji.vue';
import WorkItemStateToggleButton from './work_item_state_toggle_button.vue';
import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue';
import WorkItemTypeIcon from './work_item_type_icon.vue';
export default {
i18n,
@ -84,7 +85,7 @@ export default {
WorkItemRelationships,
},
mixins: [glFeatureFlagMixin()],
inject: ['fullPath', 'reportAbusePath'],
inject: ['fullPath', 'isGroup', 'reportAbusePath'],
props: {
isModal: {
type: Boolean,
@ -118,7 +119,9 @@ export default {
},
apollo: {
workItem: {
query: workItemByIidQuery,
query() {
return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
},
variables() {
return {
fullPath: this.fullPath,
@ -189,8 +192,8 @@ export default {
canAssignUnassignUser() {
return this.workItemAssignees && this.canSetWorkItemMetadata;
},
fullPath() {
return this.workItem?.project.fullPath;
projectFullPath() {
return this.workItem?.project?.fullPath;
},
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
@ -460,7 +463,7 @@ export default {
v-if="showWorkItemCurrentUserTodos"
:work-item-id="workItem.id"
:work-item-iid="workItemIid"
:work-item-fullpath="workItem.project.fullPath"
:work-item-fullpath="projectFullPath"
:current-user-todos="currentUserTodos"
@error="updateError = $event"
/>
@ -535,7 +538,7 @@ export default {
v-if="showWorkItemCurrentUserTodos"
:work-item-id="workItem.id"
:work-item-iid="workItemIid"
:work-item-fullpath="workItem.project.fullPath"
:work-item-fullpath="projectFullPath"
:current-user-todos="currentUserTodos"
@error="updateError = $event"
/>
@ -585,7 +588,7 @@ export default {
<work-item-award-emoji
v-if="workItemAwardEmoji"
:work-item-id="workItem.id"
:work-item-fullpath="workItem.project.fullPath"
:work-item-fullpath="projectFullPath"
:award-emoji="workItemAwardEmoji.awardEmoji"
:work-item-iid="workItemIid"
@error="updateError = $event"
@ -607,7 +610,7 @@ export default {
v-if="showWorkItemLinkedItems"
:work-item-id="workItem.id"
:work-item-iid="workItemIid"
:work-item-full-path="workItem.project.fullPath"
:work-item-full-path="projectFullPath"
:work-item-type="workItem.workItemType.name"
@showModal="openInModal"
/>

View File

@ -8,6 +8,7 @@ import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_it
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
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 { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS, TRACKING_CATEGORY_SHOW } from '../constants';
import { isLabelsWidget } from '../utils';
@ -37,7 +38,7 @@ export default {
LabelItem,
},
mixins: [Tracking.mixin()],
inject: ['fullPath'],
inject: ['fullPath', 'isGroup'],
props: {
workItemId: {
type: String,
@ -65,7 +66,9 @@ export default {
},
apollo: {
workItem: {
query: workItemByIidQuery,
query() {
return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
},
variables() {
return {
fullPath: this.fullPath,

View File

@ -13,6 +13,7 @@ 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 WorkItemLinkChild from './work_item_link_child.vue';
@ -20,7 +21,7 @@ export default {
components: {
WorkItemLinkChild,
},
inject: ['fullPath'],
inject: ['fullPath', 'isGroup'],
props: {
workItemType: {
type: String,
@ -83,7 +84,14 @@ export default {
const { data } = await this.$apollo.mutate({
mutation: updateWorkItemMutation,
variables: { input: { id: child.id, hierarchyWidget: { parentId: null } } },
update: (cache) => removeHierarchyChild(cache, this.fullPath, this.workItemIid, child),
update: (cache) =>
removeHierarchyChild({
cache,
fullPath: this.fullPath,
iid: this.workItemIid,
isGroup: this.isGroup,
workItem: child,
}),
});
if (data.workItemUpdate.errors.length) {
@ -109,7 +117,14 @@ export default {
const { data } = await this.$apollo.mutate({
mutation: updateWorkItemMutation,
variables: { input: { id: child.id, hierarchyWidget: { parentId: this.workItemId } } },
update: (cache) => addHierarchyChild(cache, this.fullPath, this.workItemIid, child),
update: (cache) =>
addHierarchyChild({
cache,
fullPath: this.fullPath,
iid: this.workItemIid,
isGroup: this.isGroup,
workItem: child,
}),
});
if (data.workItemUpdate.errors.length) {
@ -124,7 +139,7 @@ export default {
},
addWorkItemQuery({ iid }) {
this.$apollo.addSmartQuery('prefetchedWorkItem', {
query: workItemByIidQuery,
query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: {
fullPath: this.fullPath,
iid,
@ -206,7 +221,7 @@ export default {
update: (store) => {
store.updateQuery(
{
query: workItemByIidQuery,
query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath: this.fullPath, iid: this.workItemIid },
},
(sourceData) =>

View File

@ -18,6 +18,7 @@ import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_sel
import { FORM_TYPES, WIDGET_ICONS, WORK_ITEM_STATUS_TEXT } 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 WidgetWrapper from '../widget_wrapper.vue';
import WorkItemDetailModal from '../work_item_detail_modal.vue';
@ -39,7 +40,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['fullPath', 'reportAbusePath'],
inject: ['fullPath', 'isGroup', 'reportAbusePath'],
props: {
issuableId: {
type: Number,
@ -52,7 +53,9 @@ export default {
},
apollo: {
workItem: {
query: workItemByIidQuery,
query() {
return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
},
variables() {
return {
fullPath: this.fullPath,
@ -171,7 +174,13 @@ export default {
},
handleWorkItemDeleted(child) {
const { defaultClient: cache } = this.$apollo.provider.clients;
removeHierarchyChild(cache, this.fullPath, this.iid, child);
removeHierarchyChild({
cache,
fullPath: this.fullPath,
iid: this.iid,
isGroup: this.isGroup,
workItem: child,
});
this.$toast.show(s__('WorkItem|Task deleted'));
},
updateWorkItemIdUrlQuery({ iid } = {}) {

View File

@ -37,7 +37,7 @@ export default {
GlTooltip,
WorkItemTokenInput,
},
inject: ['fullPath', 'hasIterationsFeature'],
inject: ['fullPath', 'hasIterationsFeature', 'isGroup'],
props: {
issuableGid: {
type: String,
@ -260,7 +260,13 @@ export default {
input: this.workItemInput,
},
update: (cache, { data }) =>
addHierarchyChild(cache, this.fullPath, this.workItemIid, data.workItemCreate.workItem),
addHierarchyChild({
cache,
fullPath: this.fullPath,
iid: this.workItemIid,
isGroup: this.isGroup,
workItem: data.workItemCreate.workItem,
}),
})
.then(({ data }) => {
if (data.workItemCreate?.errors?.length) {

View File

@ -3,6 +3,7 @@ import { GlLoadingIcon, GlIcon, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import { WIDGET_TYPE_LINKED_ITEMS, LINKED_CATEGORIES_MAP } from '../../constants';
@ -19,6 +20,7 @@ export default {
WorkItemRelationshipList,
WorkItemAddRelationshipForm,
},
inject: ['isGroup'],
props: {
workItemId: {
type: String,
@ -41,7 +43,9 @@ export default {
},
apollo: {
workItem: {
query: workItemByIidQuery,
query() {
return this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery;
},
variables() {
return {
fullPath: this.workItemFullPath,

View File

@ -4,9 +4,10 @@ import { produce } from 'immer';
import { s__ } from '~/locale';
import { updateGlobalTodoCount } from '~/sidebar/utils';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import createWorkItemTodosMutation from '~/work_items/graphql/create_work_item_todos.mutation.graphql';
import markDoneWorkItemTodosMutation from '~/work_items/graphql/mark_done_work_item_todos.mutation.graphql';
import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import createWorkItemTodosMutation from '../graphql/create_work_item_todos.mutation.graphql';
import markDoneWorkItemTodosMutation from '../graphql/mark_done_work_item_todos.mutation.graphql';
import {
TODO_ADD_ICON,
@ -28,6 +29,7 @@ export default {
GlIcon,
GlButton,
},
inject: ['isGroup'],
props: {
workItemId: {
type: String,
@ -148,7 +150,7 @@ export default {
},
updateWorkItemCurrentTodosWidgetCache({ cache, todos }) {
const query = {
query: workItemByIidQuery,
query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath: this.workItemFullpath, iid: this.workItemIid },
};

View File

@ -36,6 +36,11 @@ export default {
return this.workItemType.toUpperCase().split(' ').join('_');
},
iconName() {
// TODO Delete this conditional once we have an `issue-type-epic` icon
if (this.workItemIconName === 'issue-type-epic') {
return 'epic';
}
return (
this.workItemIconName ||
WORK_ITEMS_TYPE_MAP[this.workItemTypeUppercase]?.icon ||

View File

@ -1,5 +1,6 @@
import { produce } from 'immer';
import { WIDGET_TYPE_NOTES } 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 { findHierarchyWidgetChildren } from '~/work_items/utils';
@ -127,8 +128,11 @@ export const updateCacheAfterRemovingAwardEmojiFromNote = (currentNotes, note) =
});
};
export const addHierarchyChild = (cache, fullPath, iid, workItem) => {
const queryArgs = { query: workItemByIidQuery, variables: { fullPath, iid } };
export const addHierarchyChild = ({ cache, fullPath, iid, isGroup, workItem }) => {
const queryArgs = {
query: isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath, iid },
};
const sourceData = cache.readQuery(queryArgs);
if (!sourceData) {
@ -143,8 +147,11 @@ export const addHierarchyChild = (cache, fullPath, iid, workItem) => {
});
};
export const removeHierarchyChild = (cache, fullPath, iid, workItem) => {
const queryArgs = { query: workItemByIidQuery, variables: { fullPath, iid } };
export const removeHierarchyChild = ({ cache, fullPath, iid, isGroup, workItem }) => {
const queryArgs = {
query: isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath, iid },
};
const sourceData = cache.readQuery(queryArgs);
if (!sourceData) {

View File

@ -0,0 +1,12 @@
#import "./work_item.fragment.graphql"
query groupWorkItemByIid($fullPath: ID!, $iid: String) {
workspace: group(fullPath: $fullPath) @persist {
id
workItems(iid: $iid) {
nodes {
...WorkItem
}
}
}
}

View File

@ -1,17 +1,25 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { WORKSPACE_GROUP } from '~/issues/constants';
import { parseBoolean } from '~/lib/utils/common_utils';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import App from './components/app.vue';
import WorkItemRoot from './pages/work_item_root.vue';
import { createRouter } from './router';
Vue.use(VueApollo);
export const initWorkItemsRoot = () => {
export const initWorkItemsRoot = (workspace) => {
const el = document.querySelector('#js-work-items');
if (!el) {
return undefined;
}
const {
fullPath,
hasIssueWeightsFeature,
iid,
issuesListPath,
registerPath,
signInPath,
@ -22,6 +30,8 @@ export const initWorkItemsRoot = () => {
reportAbusePath,
} = el.dataset;
const Component = workspace === WORKSPACE_GROUP ? WorkItemRoot : App;
return new Vue({
el,
name: 'WorkItemsRoot',
@ -29,6 +39,7 @@ export const initWorkItemsRoot = () => {
apolloProvider,
provide: {
fullPath,
isGroup: workspace === WORKSPACE_GROUP,
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasOkrsFeature: parseBoolean(hasOkrsFeature),
issuesListPath,
@ -40,7 +51,11 @@ export const initWorkItemsRoot = () => {
reportAbusePath,
},
render(createElement) {
return createElement(App);
return createElement(Component, {
props: {
iid: workspace === WORKSPACE_GROUP ? iid : undefined,
},
});
},
});
};

View File

@ -10,6 +10,7 @@ import {
} from '../constants';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import ItemTitle from '../components/item_title.vue';
@ -22,7 +23,7 @@ export default {
ItemTitle,
GlFormSelect,
},
inject: ['fullPath'],
inject: ['fullPath', 'isGroup'],
props: {
initialTitle: {
type: String,
@ -94,7 +95,7 @@ export default {
const { workItem } = workItemCreate;
store.writeQuery({
query: workItemByIidQuery,
query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: {
fullPath: this.fullPath,
iid: workItem.iid,

View File

@ -26,11 +26,7 @@ module Repositories
end
if download_request?
if Feature.enabled?(:lfs_batch_direct_downloads, project)
render json: { objects: download_objects! }, content_type: LfsRequest::CONTENT_TYPE
else
render json: { objects: legacy_download_objects! }, content_type: LfsRequest::CONTENT_TYPE
end
render json: { objects: download_objects! }, content_type: LfsRequest::CONTENT_TYPE
elsif upload_request?
render json: { objects: upload_objects! }, content_type: LfsRequest::CONTENT_TYPE
else

View File

@ -22,9 +22,7 @@ module WikiHelper
end
def wiki_sidebar_toggle_button
content_tag :button, class: 'gl-button btn btn-default btn-icon sidebar-toggle js-sidebar-wiki-toggle', role: 'button', type: 'button' do
sprite_icon('chevron-double-lg-left')
end
render Pajamas::ButtonComponent.new(icon: 'chevron-double-lg-left', button_options: { class: 'sidebar-toggle js-sidebar-wiki-toggle' })
end
# Produces a pure text breadcrumb for a given page.
@ -60,17 +58,14 @@ module WikiHelper
end
def wiki_sort_controls(wiki, direction)
link_class = 'gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn rspec-reverse-sort'
link_class = 'has-tooltip reverse-sort-btn rspec-reverse-sort'
reversed_direction = direction == 'desc' ? 'asc' : 'desc'
icon_class = direction == 'desc' ? 'highest' : 'lowest'
title = direction == 'desc' ? _('Sort direction: Descending') : _('Sort direction: Ascending')
link_options = { action: :pages, direction: reversed_direction }
link_to(wiki_path(wiki, **link_options),
type: 'button', class: link_class, title: title) do
sprite_icon("sort-#{icon_class}")
end
render Pajamas::ButtonComponent.new(href: wiki_path(wiki, **link_options), icon: "sort-#{icon_class}", button_options: { class: link_class, title: title })
end
def wiki_sort_title(key)

View File

@ -1,10 +1,11 @@
# frozen_string_literal: true
module WorkItemsHelper
def work_items_index_data(project)
def work_items_index_data(resource_parent)
{
full_path: project.full_path,
issues_list_path: project_issues_path(project),
full_path: resource_parent.full_path,
issues_list_path:
resource_parent.is_a?(Group) ? issues_group_path(resource_parent) : project_issues_path(resource_parent),
register_path: new_user_registration_path(redirect_to_referer: 'yes'),
sign_in_path: new_session_path(:user, redirect_to_referer: 'yes'),
new_comment_template_path: profile_comment_templates_path,

View File

@ -60,6 +60,10 @@ module Integrations
super - ['deployment']
end
def avatar_url
ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/telegram.svg')
end
private
def set_webhook

View File

@ -695,7 +695,6 @@ class ProjectPolicy < BasePolicy
enable :read_merge_request
enable :read_note
enable :read_pipeline
enable :read_pipeline_schedule
enable :read_environment
enable :read_deployment
enable :read_commit_status
@ -712,7 +711,10 @@ class ProjectPolicy < BasePolicy
enable :read_issue
end
rule { can?(:public_access) & public_builds }.enable :read_ci_cd_analytics
rule { can?(:public_access) & public_builds }.policy do
enable :read_ci_cd_analytics
enable :read_pipeline_schedule
end
rule { public_builds }.policy do
enable :read_build

View File

@ -13,7 +13,7 @@ module Issues
return error_invalid_params unless valid_params?
@existing_ids = issue.customer_relations_contact_ids
determine_changes if params[:replace_ids].present?
determine_changes if set_present?
return error_too_many if too_many?
@added_count = 0
@ -108,7 +108,7 @@ module Issues
end
def set_present?
params[:replace_ids].present?
!params[:replace_ids].nil?
end
def add_or_remove_present?

View File

@ -1 +1,7 @@
.h1 Work Item
- page_title "##{request.params['iid']}"
- add_to_breadcrumbs _("Issues"), issues_group_path(@group)
- add_page_specific_style 'page_bundles/work_items'
- @gfm_form = true
- @noteable_type = 'WorkItem'
#js-work-items{ data: work_items_index_data(@group).merge(iid: request.params['iid']) }

View File

@ -1,5 +1,5 @@
.gl-display-flex.gl-mt-7
- submit_button_options = { type: :submit, variant: :confirm, button_options: { id: 'commit-changes', class: 'js-commit-button', data: { qa_selector: 'commit_button' } } }
- submit_button_options = { type: :submit, variant: :confirm, button_options: { id: 'commit-changes', class: 'js-commit-button', data: { testid: 'commit-button' } } }
= render Pajamas::ButtonComponent.new(**submit_button_options) do
= _('Commit changes')
= render Pajamas::ButtonComponent.new(loading: true, disabled: true, **submit_button_options.merge({ button_options: { class: 'js-commit-button-loading gl-display-none' } })) do

View File

@ -7,5 +7,5 @@
= _('Unfollow')
- else
= form_tag user_follow_path(@user, :json), class: link_classes do
= render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit, button_options: { class: 'gl-w-full', data: { qa_selector: 'follow_user_link', track_action: 'click_button', track_label: 'follow_from_profile' } }) do
= render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit, button_options: { class: 'gl-w-full', data: { testid: 'follow-user-link', track_action: 'click_button', track_label: 'follow_from_profile' } }) do
= _('Follow')

View File

@ -33,7 +33,7 @@
%h4.gl-flex-grow-1
= Feature.enabled?(:security_auto_fix) && @user.bot? ? s_('UserProfile|Bot activity') : s_('UserProfile|Activity')
= link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
.overview-content-list{ data: { href: user_activity_path, qa_selector: 'user_activity_content' } }
.overview-content-list{ data: { href: user_activity_path, testid: 'user-activity-content' } }
= gl_loading_icon(size: 'md', css_class: 'loading')
- unless Feature.enabled?(:security_auto_fix) && @user.bot?

View File

@ -18,8 +18,6 @@ class GitlabShellWorker # rubocop:disable Scalability/IdempotentWorker
raise(ArgumentError, "#{action} not allowed for #{self.class.name}")
end
Gitlab::GitalyClient::NamespaceService.allow do
gitlab_shell.public_send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
end
gitlab_shell.public_send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
end
end

View File

@ -1,8 +1,8 @@
---
name: lfs_batch_direct_downloads
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122221
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/421692
milestone: '16.1'
name: user_pat_rest_api
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131923
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/425967
milestone: '16.5'
type: development
group: group::source code
default_enabled: true
group: group::authentication and authorization
default_enabled: false

View File

@ -845,6 +845,8 @@ these are separate buckets. Use of bucket prefixes
Helm-based installs require separate buckets to
[handle backup restorations](https://docs.gitlab.com/charts/advanced/external-object-storage/#lfs-artifacts-uploads-packages-external-diffs-terraform-state-dependency-proxy).
If the same bucket is used for multiple object types, only the first object type in the configuration uses the bucket. Other object types revert to their default local storage (for the default storage locations, see the [table in the linked section)[https://docs.gitlab.com/omnibus/settings/configuration.html#disable-the-varoptgitlab-directory-management)).
### S3 API compatibility issues
Not all S3 providers [are fully compatible](../administration/backup_restore/backup_gitlab.md#other-s3-providers)

View File

@ -69,6 +69,7 @@ With this method:
- The instance must be publicly available.
- The instance must be on GitLab version 15.7 or later.
- You must set up [OAuth authentication](#set-up-oauth-authentication).
- If your instance is using HTTPS, your GitLab certificate must be publicly trusted or contain the full chained certificate.
- Your network must allow inbound and outbound connections between GitLab and Jira. For self-managed instances that are behind a
firewall and cannot be directly accessed from the internet:
- Open your firewall and only allow inbound traffic from [Atlassian IP addresses](https://support.atlassian.com/organization-administration/docs/ip-addresses-and-domains-for-atlassian-cloud-products/#Outgoing-Connections).

View File

@ -328,3 +328,8 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
## Create a personal access token (administrator only)
See the [Users API documentation](users.md#create-a-personal-access-token) for information on creating a personal access token.
## Create a personal access token with limited scopes for the currently authenticated user **(FREE SELF)**
See the [Users API documentation](users.md#create-a-personal-access-token-with-limited-scopes-for-the-currently-authenticated-user)
for information on creating a personal access token for the currently authenticated user.

View File

@ -2126,6 +2126,47 @@ Example response:
}
```
## Create a personal access token with limited scopes for the currently authenticated user **(FREE SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131923) in GitLab 16.5 with a flag named `user_pat_rest_api`.
Use this API to create a new personal access token for the currently authenticated user.
For security purposes, the scopes are limited to only `k8s_proxy` and by default the token will expire by
the end of the day it was created at.
Token values are returned once so, make sure you save it as you can't access it again.
```plaintext
POST /user/personal_access_tokens
```
| Attribute | Type | Required | Description |
|--------------|--------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name` | string | yes | Name of the personal access token |
| `scopes` | array | yes | Array of scopes of the personal access token. Possible values are `k8s_proxy` |
| `expires_at` | array | no | Expiration date of the access token in ISO format (`YYYY-MM-DD`). If no date is set, the expiration is at the end of the current day. The expiration is subject to the [maximum allowable lifetime of an access token](../user/profile/personal_access_tokens.md#when-personal-access-tokens-expire). |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --data "name=mytoken" --data "scopes[]=k8s_proxy" "https://gitlab.example.com/api/v4/user/personal_access_tokens"
```
Example response:
```json
{
"id": 3,
"name": "mytoken",
"revoked": false,
"created_at": "2020-10-14T11:58:53.526Z",
"scopes": [
"k8s_proxy"
],
"user_id": 42,
"active": true,
"expires_at": "2020-10-15",
"token": "ggbfKkC4n-Lujy8jwCR2"
}
```
## Get user activities **(FREE SELF)**
Pre-requisite:

View File

@ -96,13 +96,21 @@ of each ref do not expire and are not deleted.
## Error message `This job could not start because it could not retrieve the needed artifacts.`
A job configured with the [`needs:artifacts`](../yaml/index.md#needsartifacts) keyword
fails to start and returns this error message if:
A job fails to start and returns this error message if it can't fetch the artifacts
it expects. This error is returned when:
- The job's dependencies cannot be found.
- The job's dependencies are not found. By default, jobs in later stages fetch artifacts
from jobs in all earlier stages, so the earlier jobs are all considered dependent.
If the job uses the [`dependencies`](../yaml/index.md#dependencies) keyword, only
the listed jobs are dependent.
- The artifacts are already expired. You can set a longer expiry with [`artifacts:expire_in`](../yaml/index.md#artifactsexpire_in).
- The job cannot access the relevant resources due to insufficient permissions.
The troubleshooting steps to follow differ based on the syntax the job uses:
See these additional troubleshooting steps if the job uses the [`needs:artifacts`](../yaml/index.md#needsartifacts):
keyword with:
- [`needs:project`](#for-a-job-configured-with-needsproject)
- [`needs:pipeline:job`](#for-a-job-configured-with-needspipelinejob)
- [`needs:project`](#for-a-job-configured-with-needsproject)
- [`needs:pipeline:job`](#for-a-job-configured-with-needspipelinejob)

View File

@ -38,6 +38,11 @@ On self-managed GitLab instances:
- Administrators can [assign more compute minutes](#set-the-compute-quota-for-a-specific-namespace)
if a namespace uses all its monthly quota.
[Trigger jobs](../../ci/yaml/index.md#trigger) do not execute on runners, so they do not
consume compute minutes, even when using [`strategy:depend`](../yaml/index.md#triggerstrategy)
to wait for the [downstream pipeline](../pipelines/downstream_pipelines.md) status.
The triggered downstream pipeline consumes compute minutes the same as other pipelines.
[Project runners](../runners/runners_scope.md#project-runners) are not subject to a compute quota.
## Set the compute quota for all namespaces

View File

@ -898,6 +898,11 @@ job:
- Select **Keep** on the job page.
- [In GitLab 13.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/22761), set the value of
`expire_in` to `never`.
- If the expiry time is too short, jobs in later stages of a long pipeline might try to fetch
expired artifacts from earlier jobs. If the artifacts are expired, jobs that try to fetch
them fail with a [`could not retrieve the needed artifacts` error](../jobs/job_artifacts_troubleshooting.md#error-message-this-job-could-not-start-because-it-could-not-retrieve-the-needed-artifacts).
Set the expiry time to be longer, or use [`dependencies`](#dependencies) in later jobs
to ensure they don't try to fetch expired artifacts.
#### `artifacts:expose_as`
@ -1619,10 +1624,11 @@ to select a specific site profile and scanner profile.
### `dependencies`
Use the `dependencies` keyword to define a list of jobs to fetch [artifacts](#artifacts) from.
You can also set a job to download no artifacts at all.
Use the `dependencies` keyword to define a list of specific jobs to fetch [artifacts](#artifacts)
from. When `dependencies` is not defined in a job, all jobs in earlier stages are considered dependent
and the job fetches all artifacts from those jobs.
If you do not use `dependencies`, all artifacts from previous stages are passed to each job.
You can also set a job to download no artifacts at all.
**Keyword type**: Job keyword. You can use it only as part of a job.

View File

@ -869,7 +869,7 @@ Make sure to prepare for this task by having a
bundle exec rake gitlab:elastic:index_users RAILS_ENV=production
```
1. Enable replication and refreshing again after indexing (only if you previously disabled it):
1. Enable replication and refreshing again after indexing (only if you previously increased the `refresh_interval`):
```shell
curl --request PUT localhost:9200/gitlab-production/_settings --header 'Content-Type: application/json' \

View File

@ -338,6 +338,7 @@ Below is a list of Mattermost version changes for GitLab 14.0 and later:
| GitLab version | Mattermost version | Notes |
| :------------- | :----------------- | ---------------------------------------------------------------------------------------- |
| 16.5 | 9.0 | |
| 16.4 | 8.1 | |
| 16.3 | 8.0 | |
| 16.0 | 7.10 | |

View File

@ -193,7 +193,11 @@ When upgrading:
GitLab instances with multiple web nodes) > [`15.4.6`](versions/gitlab_15_changes.md#1540) >
[`15.11.13`](versions/gitlab_15_changes.md#15110).
- GitLab 16: [`16.0.x`](versions/gitlab_16_changes.md#1600) (only
[instances with lots of users](versions/gitlab_16_changes.md#long-running-user-type-data-change)) > [`16.1`](versions/gitlab_16_changes.md#1610)(instances with NPM packages in their Package Registry) > [`16.3`](versions/gitlab_16_changes.md#1630) > [latest `16.Y.Z`](https://gitlab.com/gitlab-org/gitlab/-/releases).
instances with [lots of users](versions/gitlab_16_changes.md#long-running-user-type-data-change) or
[large pipeline variables history](versions/gitlab_16_changes.md#1610)) >
[`16.1`](versions/gitlab_16_changes.md#1610)(instances with NPM packages in their Package Registry) >
[`16.2.x`](versions/gitlab_16_changes.md#1620) (only instances with [large pipeline variables history](versions/gitlab_16_changes.md#1630)) >
[`16.3`](versions/gitlab_16_changes.md#1630) > [latest `16.Y.Z`](https://gitlab.com/gitlab-org/gitlab/-/releases).
1. Check for [required upgrade stops](#required-upgrade-stops).
1. Consult the [version-specific upgrade instructions](#version-specific-upgrading-instructions).

View File

@ -100,6 +100,19 @@ For more information about upgrading GitLab Helm Chart, see [the release notes f
for any of the applications above before
upgrading.
- A `BackfillCiPipelineVariablesForPipelineIdBigintConversion` background migration is finalized with
the `EnsureAgainBackfillForCiPipelineVariablesPipelineIdIsFinished` post-deploy migration.
GitLab 16.2.0 introduced a [batched background migration](../background_migrations.md#batched-background-migrations) to
[backfill bigint `pipeline_id` values on the `ci_pipeline_variables` table](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123132). This
migration may take a long time to complete on larger GitLab instances (4 hours to process 50 million rows reported in one case).
To avoid a prolonged upgrade downtime, make sure the migration has completed successfully before upgrading to 16.3.
You can check the size of the `ci_pipeline_variables` table in the [database console](../../administration/troubleshooting/postgresql.md#start-a-database-console):
```sql
select count(*) from ci_pipeline_variables;
```
### Linux package installations
Specific information applies to Linux package installations:
@ -204,6 +217,18 @@ Specific information applies to installations using Geo:
migration may take multiple days to complete on larger GitLab instances. Make sure the migration
has completed successfully before upgrading to 16.1.0.
- GitLab 16.1.0 includes a [batched background migration](../background_migrations.md#batched-background-migrations) `MarkDuplicateNpmPackagesForDestruction` to mark duplicate NPM packages for destruction. Make sure the migration has completed successfully before upgrading to 16.3.0 or later.
- A `BackfillCiPipelineVariablesForBigintConversion` background migration is finalized with
the `EnsureBackfillBigintIdIsCompleted` post-deploy migration.
GitLab 16.0.0 introduced a [batched background migration](../background_migrations.md#batched-background-migrations) to
[backfill bigint `id` values on the `ci_pipeline_variables` table](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118878). This
migration may take a long time to complete on larger GitLab instances (4 hours to process 50 million rows reported in one case).
To avoid a prolonged upgrade downtime, make sure the migration has completed successfully before upgrading to 16.1.
You can check the size of the `ci_pipeline_variables` table in the [database console](../../administration/troubleshooting/postgresql.md#start-a-database-console):
```sql
select count(*) from ci_pipeline_variables;
```
### Self-compiled installations

View File

@ -1373,6 +1373,35 @@ module API
get 'status', feature_category: :user_profile do
present current_user.status || {}, with: Entities::UserStatus
end
resource :personal_access_tokens do
desc 'Create a personal access token with limited scopes for the currently authenticated user' do
detail 'This feature was introduced in GitLab 16.5'
success Entities::PersonalAccessTokenWithToken
end
params do
requires :name, type: String, desc: 'The name of the personal access token'
# NOTE: for security reasons only the k8s_proxy scope is allowed at the moment.
# See details in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131923#note_1571272897
# and in https://gitlab.com/gitlab-org/gitlab/-/issues/425171
requires :scopes, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, values: [::Gitlab::Auth::K8S_PROXY_SCOPE].map(&:to_s),
desc: 'The array of scopes of the personal access token'
optional :expires_at, type: Date, default: -> { 1.day.from_now.to_date }, desc: 'The expiration date in the format YEAR-MONTH-DAY of the personal access token'
end
post feature_category: :system_access do
bad_request!('Endpoint is disabled via user_pat_rest_api feature flag. Please contact your administrator to enable it.') unless Feature.enabled?(:user_pat_rest_api)
response = ::PersonalAccessTokens::CreateService.new(
current_user: current_user, target_user: current_user, params: declared_params(include_missing: false)
).execute
if response.success?
present response.payload[:personal_access_token], with: Entities::PersonalAccessTokenWithToken
else
render_api_error!(response.message, response.http_status || :unprocessable_entity)
end
end
end
end
end
end

View File

@ -1,39 +0,0 @@
# frozen_string_literal: true
# UserReferenceTransformer replaces specified user
# reference key with a user id being either:
# - A user id found by `public_email` in the group
# - Current user id
# under a new key `"#{@reference}_id"`.
module BulkImports
module Common
module Transformers
class UserReferenceTransformer
DEFAULT_REFERENCE = 'user'
def initialize(options = {})
@reference = options[:reference].to_s.presence || DEFAULT_REFERENCE
@suffixed_reference = "#{@reference}_id"
end
def transform(context, data)
return unless data
user = find_user(context, data&.dig(@reference, 'public_email')) || context.current_user
data
.except(@reference)
.merge(@suffixed_reference => user.id)
end
private
def find_user(context, email)
return if email.blank?
context.group.users.find_by_any_email(email, confirmed: true) # rubocop: disable CodeReuse/ActiveRecord
end
end
end
end
end

View File

@ -1,57 +0,0 @@
# frozen_string_literal: true
module Gitlab
module GitalyClient
class NamespaceService
extend Gitlab::TemporarilyAllow
NamespaceServiceAccessError = Class.new(StandardError)
ALLOW_KEY = :allow_namespace
def self.allow
temporarily_allow(ALLOW_KEY) { yield }
end
def self.denied?
!temporarily_allowed?(ALLOW_KEY)
end
def initialize(storage)
raise NamespaceServiceAccessError if self.class.denied?
@storage = storage
end
def add(name)
request = Gitaly::AddNamespaceRequest.new(storage_name: @storage, name: name)
gitaly_client_call(:add_namespace, request, timeout: GitalyClient.fast_timeout)
end
def remove(name)
request = Gitaly::RemoveNamespaceRequest.new(storage_name: @storage, name: name)
gitaly_client_call(:remove_namespace, request, timeout: GitalyClient.long_timeout)
end
def rename(from, to)
request = Gitaly::RenameNamespaceRequest.new(storage_name: @storage, from: from, to: to)
gitaly_client_call(:rename_namespace, request, timeout: GitalyClient.fast_timeout)
end
def exists?(name)
request = Gitaly::NamespaceExistsRequest.new(storage_name: @storage, name: name)
response = gitaly_client_call(:namespace_exists, request, timeout: GitalyClient.fast_timeout)
response.exists
end
private
def gitaly_client_call(type, request, timeout: nil)
GitalyClient.call(@storage, :namespace_service, type, request, timeout: timeout)
end
end
end
end

View File

@ -1,7 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Prometheus
ParsingError = Class.new(StandardError)
end
end

View File

@ -1,33 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Prometheus
module Queries
class BaseQuery
attr_accessor :client
delegate :query_range, :query, :label_values, :series, to: :client, prefix: true
def raw_memory_usage_query(environment_slug)
%{avg(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / 2^20}
end
def raw_cpu_usage_query(environment_slug)
%{avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) * 100}
end
def initialize(client)
@client = client
end
def query(*args)
raise NotImplementedError
end
def self.transform_reactive_result(result)
result
end
end
end
end
end

View File

@ -1,101 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Prometheus
module Queries
module QueryAdditionalMetrics
def query_metrics(project, environment, query_context)
matched_metrics(project).map(&query_group(query_context))
.select(&method(:group_with_any_metrics))
end
protected
def query_group(query_context)
query_processor = method(:process_query).curry[query_context]
lambda do |group|
metrics = group.metrics.map do |metric|
metric_hsh = {
title: metric.title,
weight: metric.weight,
y_label: metric.y_label,
queries: metric.queries.map(&query_processor).select(&method(:query_with_result))
}
metric_hsh[:id] = metric.id if metric.id
metric_hsh
end
{
group: group.name,
priority: group.priority,
metrics: metrics.select(&method(:metric_with_any_queries))
}
end
end
private
def metric_with_any_queries(metric)
metric[:queries]&.count&.> 0
end
def group_with_any_metrics(group)
group[:metrics]&.count&.> 0
end
def query_with_result(query)
query[:result]&.any? do |item|
item&.[](:values)&.any? || item&.[](:value)&.any?
end
end
def process_query(context, query)
query = query.dup
result =
if query.key?(:query_range)
query[:query_range] %= context
client_query_range(query[:query_range], start_time: context[:timeframe_start], end_time: context[:timeframe_end])
else
query[:query] %= context
client_query(query[:query], time: context[:timeframe_end])
end
query[:result] = result&.map(&:deep_symbolize_keys)
query
end
def available_metrics
@available_metrics ||= client_label_values || []
end
def matched_metrics(project)
result = Gitlab::Prometheus::MetricGroup.for_project(project).map do |group|
group.metrics.select! do |metric|
metric.required_metrics.all?(&available_metrics.method(:include?))
end
group
end
result.select { |group| group.metrics.any? }
end
def common_query_context(environment, timeframe_start:, timeframe_end:)
base_query_context(timeframe_start, timeframe_end)
.merge(QueryVariables.call(environment))
end
def base_query_context(timeframe_start, timeframe_end)
{
timeframe_start: timeframe_start,
timeframe_end: timeframe_end
}
end
end
end
end
end
Gitlab::Prometheus::Queries::QueryAdditionalMetrics.prepend_mod_with('Gitlab::Prometheus::Queries::QueryAdditionalMetrics')

View File

@ -1,31 +0,0 @@
# frozen_string_literal: true
module Gitlab
module Prometheus
module QueryVariables
# start_time and end_time should be Time objects.
def self.call(environment, start_time: nil, end_time: nil)
{
__range: range(start_time, end_time),
ci_environment_slug: environment.slug,
kube_namespace: environment.deployment_namespace || '',
environment_filter: %(container_name!="POD",environment="#{environment.slug}"),
ci_project_name: environment.project.name,
ci_project_namespace: environment.project.namespace.name,
ci_project_path: environment.project.full_path,
ci_environment_name: environment.name
}
end
private
def self.range(start_time, end_time)
if start_time && end_time
range_seconds = (end_time - start_time).to_i
"#{range_seconds}s"
end
end
private_class_method :range
end
end
end

View File

@ -15,8 +15,7 @@ module Gitlab
Error = Class.new(StandardError)
PERMITTED_ACTIONS = %w[
mv_repository remove_repository add_namespace mv_namespace
repository_exists?
mv_repository remove_repository repository_exists?
].freeze
class << self
@ -127,41 +126,6 @@ module Gitlab
false
end
# Add empty directory for storing repositories
#
# @example Add new namespace directory
# add_namespace("default", "gitlab")
#
# @param [String] storage project's storage path
# @param [String] name namespace name
#
# @deprecated
def add_namespace(storage, name)
Gitlab::GitalyClient.allow_n_plus_1_calls do
Gitlab::GitalyClient::NamespaceService.new(storage).add(name)
end
rescue GRPC::InvalidArgument => e
raise ArgumentError, e.message
end
# Move namespace directory inside repositories storage
#
# @example Move/rename a namespace directory
# mv_namespace("/path/to/storage", "gitlab", "gitlabhq")
#
# @param [String] storage project's storage path
# @param [String] old_name current namespace name
# @param [String] new_name new namespace name
#
# @deprecated
def mv_namespace(storage, old_name, new_name)
Gitlab::GitalyClient::NamespaceService.new(storage).rename(old_name, new_name)
rescue GRPC::InvalidArgument => e
Gitlab::ErrorTracking.track_exception(e, old_name: old_name, new_name: new_name, storage: storage)
false
end
# Check if repository exists on disk
#
# @example Check if repository exists

View File

@ -22882,7 +22882,7 @@ msgstr ""
msgid "GroupSelect|Select a group"
msgstr ""
msgid "GroupSettings| %{link_start}What are Experiment features?%{link_end}"
msgid "GroupSettings| %{link_start}What do Experiment and Beta mean?%{link_end}"
msgstr ""
msgid "GroupSettings|After the instance reaches the user cap, any user who is added or requests access must be approved by an administrator. Leave empty for an unlimited user cap. If you change the user cap to unlimited, you must re-enable %{project_sharing_docs_link_start}project sharing%{link_end} and %{group_sharing_docs_link_start}group sharing%{link_end}."
@ -22960,7 +22960,7 @@ msgstr ""
msgid "GroupSettings|Enabling these features is your acceptance of the %{link_start}GitLab Testing Agreement%{link_end}."
msgstr ""
msgid "GroupSettings|Experiment features"
msgid "GroupSettings|Experiment and Beta features"
msgstr ""
msgid "GroupSettings|Export group"
@ -23041,7 +23041,7 @@ msgstr ""
msgid "GroupSettings|There was a problem updating the pipeline settings: %{error_messages}."
msgstr ""
msgid "GroupSettings|These features can cause performance and stability issues and may change over time."
msgid "GroupSettings|These features are being developed and might be unstable."
msgstr ""
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
@ -23056,7 +23056,7 @@ msgstr ""
msgid "GroupSettings|Transfer group"
msgstr ""
msgid "GroupSettings|Use Experiment features"
msgid "GroupSettings|Use Experiment and Beta features"
msgstr ""
msgid "GroupSettings|Users can create %{link_start_project}project access tokens%{link_end} and %{link_start_group}group access tokens%{link_end} in this group"

View File

@ -27,11 +27,11 @@ module QA
end
base.view 'app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue' do
element :user_action_dropdown
element 'user-action-dropdown'
end
base.view 'app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue' do
element :delete_member_dropdown_item
element 'delete-member-dropdown-item'
end
base.view 'app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue' do
@ -60,8 +60,8 @@ module QA
def remove_member(username)
within_element(:member_row, text: username) do
click_element :user_action_dropdown
click_element :delete_member_dropdown_item
click_element 'user-action-dropdown'
click_element 'delete-member-dropdown-item'
end
confirm_remove_member

View File

@ -11,12 +11,12 @@ module QA
super
base.view 'app/views/projects/_commit_button.html.haml' do
element :commit_button
element 'commit-button'
end
end
def commit_changes
click_element(:commit_button)
click_element('commit-button')
wait_until(reload: false, max_duration: 60) do
finished_loading?

View File

@ -22,7 +22,7 @@ module QA
end
view 'app/assets/javascripts/super_sidebar/components/user_name_group.vue' do
element :user_profile_link
element 'user-profile-link'
end
view 'app/assets/javascripts/super_sidebar/components/user_bar.vue' do
@ -115,10 +115,10 @@ module QA
return false unless has_personal_area?
within_user_menu do
has_element?(:user_profile_link, text: /#{user.username}/)
has_element?('user-profile-link', text: /#{user.username}/)
end
# we need to close user menu because plain user link check will leave it open
click_element :user_avatar_content if has_element?(:user_profile_link, wait: 0)
click_element :user_avatar_content if has_element?('user-profile-link', wait: 0)
end
def not_signed_in?
@ -159,7 +159,7 @@ module QA
def click_user_profile_link
within_user_menu do
click_element(:user_profile_link)
click_element('user-profile-link')
end
end
@ -189,7 +189,7 @@ module QA
def within_user_menu(&block)
within_element(:navbar) do
click_element :user_avatar_content unless has_element?(:user_profile_link, wait: 1)
click_element :user_avatar_content unless has_element?('user-profile-link', wait: 1)
within_element('user-dropdown', &block)
end

View File

@ -18,11 +18,11 @@ module QA
end
view 'app/assets/javascripts/repository/components/table/row.vue' do
element :file_name_link
element 'file-name-link'
end
view 'app/assets/javascripts/repository/components/table/index.vue' do
element :file_tree_table
element 'file-tree-table'
end
view 'app/views/layouts/header/_new_dropdown.html.haml' do
@ -102,15 +102,15 @@ module QA
end
def click_file(filename)
within_element(:file_tree_table) do
click_element(:file_name_link, text: filename)
within_element('file-tree-table') do
click_element('file-name-link', text: filename)
end
end
def click_commit(commit_msg)
wait_for_requests
within_element(:file_tree_table) do
within_element('file-tree-table') do
click_on commit_msg
end
end
@ -120,16 +120,16 @@ module QA
end
def has_file?(name)
return false unless has_element?(:file_tree_table)
return false unless has_element?('file-tree-table')
within_element(:file_tree_table) do
has_element?(:file_name_link, text: name)
within_element('file-tree-table') do
has_element?('file-name-link', text: name)
end
end
def has_no_file?(name)
within_element(:file_tree_table) do
has_no_element?(:file_name_link, text: name)
within_element('file-tree-table') do
has_no_element?('file-name-link', text: name)
end
end

View File

@ -5,7 +5,7 @@ module QA
module User
class Show < Page::Base
view 'app/views/users/_follow_user.html.haml' do
element :follow_user_link
element 'follow-user-link'
end
view 'app/views/shared/users/_user.html.haml' do
@ -13,11 +13,11 @@ module QA
end
view 'app/views/users/_overview.html.haml' do
element :user_activity_content
element 'user-activity-content'
end
def click_follow_user_link
click_element(:follow_user_link)
click_element('follow-user-link')
end
def click_following_tab
@ -29,7 +29,7 @@ module QA
end
def has_activity?(activity)
within_element(:user_activity_content) do
within_element('user-activity-content') do
has_text?(activity)
end
end

View File

@ -46,7 +46,7 @@ module QA
Page::File::Show.perform(&:click_edit)
Page::File::Form.perform do |file_form|
expect(file_form).to have_element(:commit_button)
expect(file_form).to have_element('commit-button')
end
end
end

View File

@ -15,17 +15,14 @@ module RuboCop
--format RuboCop::Formatter::GracefulFormatter
]
available_cops = RuboCop::Cop::Registry.global.to_h
cop_names, paths = args.partition { available_cops.key?(_1) }
# Convert from Rake::TaskArguments into an Array to make `any?` work as expected.
cop_names = args.to_a
if cop_names.any?
list = cop_names.sort.join(',')
options.concat ['--only', list]
end
options.concat(paths)
puts <<~MSG
Running RuboCop in graceful mode:
rubocop #{options.join(' ')}

View File

@ -93,7 +93,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :continuous_integrat
wait_for_requests
end
describe 'The view' do
describe 'the view' do
it 'displays the required information description' do
page.within('[data-testid="pipeline-schedule-table-row"]') do
expect(page).to have_content('pipeline schedule')
@ -280,36 +280,47 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :continuous_integrat
end
end
context 'logged in as non-member' do
before do
gitlab_sign_in(user)
end
shared_examples 'when not logged in' do
describe 'GET /projects/pipeline_schedules' do
before do
visit_pipelines_schedules
end
describe 'The view' do
describe 'the view' do
it 'does not show create schedule button' do
visit_pipelines_schedules
expect(page).not_to have_link('New schedule')
end
context 'when project is public' do
let_it_be(:project) { create(:project, :repository, :public, public_builds: true) }
it 'shows Pipelines Schedules page' do
visit_pipelines_schedules
expect(page).to have_link('New schedule')
end
context 'when public pipelines are disabled' do
before do
project.update!(public_builds: false)
visit_pipelines_schedules
end
it 'shows Not Found page' do
expect(page).to have_content('Page Not Found')
end
end
end
end
end
end
context 'not logged in' do
describe 'GET /projects/pipeline_schedules' do
before do
visit_pipelines_schedules
end
it_behaves_like 'when not logged in'
describe 'The view' do
it 'does not show create schedule button' do
expect(page).not_to have_link('New schedule')
end
end
context 'logged in as non-member' do
before do
gitlab_sign_in(user)
end
it_behaves_like 'when not logged in'
end
def visit_new_pipeline_schedule

View File

@ -31,7 +31,7 @@ describe('Job Log Collapsible Section', () => {
});
it('renders clickable header line', () => {
expect(findLogLineHeader().text()).toBe('2 foo');
expect(findLogLineHeader().text()).toBe('1 foo');
expect(findLogLineHeader().attributes('role')).toBe('button');
});

View File

@ -16,7 +16,7 @@ describe('Job Log Header Line', () => {
style: 'term-fg-l-green',
},
],
lineNumber: 76,
lineNumber: 77,
},
isClosed: true,
path: '/jashkenas/underscore/-/jobs/335',

View File

@ -5,7 +5,7 @@ describe('Job Log Line Number', () => {
let wrapper;
const data = {
lineNumber: 0,
lineNumber: 1,
path: '/jashkenas/underscore/-/jobs/335',
};

View File

@ -224,7 +224,7 @@ describe('Job Log Line', () => {
offset: 24526,
content: [{ text: 'job log content' }],
section: 'custom-section',
lineNumber: 76,
lineNumber: 77,
},
path: '/root/ci-project/-/jobs/6353',
});

View File

@ -151,7 +151,7 @@ describe('Job Log', () => {
],
section: 'prepare-executor',
section_header: true,
lineNumber: 2,
lineNumber: 3,
},
];

View File

@ -172,7 +172,7 @@ export const collapsibleSectionClosed = {
offset: 80,
content: [{ text: 'this is a collapsible nested section' }],
section: 'prepare-script',
lineNumber: 3,
lineNumber: 2,
},
],
};
@ -193,7 +193,7 @@ export const collapsibleSectionOpened = {
offset: 80,
content: [{ text: 'this is a collapsible nested section' }],
section: 'prepare-script',
lineNumber: 3,
lineNumber: 2,
},
],
};

View File

@ -106,7 +106,7 @@ describe('Jobs Store Mutations', () => {
{
offset: 1,
content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
lineNumber: 0,
lineNumber: 1,
},
]);
});
@ -127,7 +127,7 @@ describe('Jobs Store Mutations', () => {
{
offset: 0,
content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }],
lineNumber: 0,
lineNumber: 1,
},
]);
});

View File

@ -6,7 +6,7 @@ import {
addDurationToHeader,
isCollapsibleSection,
findOffsetAndRemove,
getIncrementalLineNumber,
getNextLineNumber,
} from '~/ci/job_details/store/utils';
import {
mockJobLog,
@ -192,14 +192,14 @@ describe('Jobs Store Utils', () => {
describe('regular line', () => {
it('adds a lineNumber property with correct index', () => {
expect(result[0].lineNumber).toEqual(0);
expect(result[1].lineNumber).toEqual(1);
expect(result[2].line.lineNumber).toEqual(2);
expect(result[2].lines[0].lineNumber).toEqual(3);
expect(result[2].lines[1].lineNumber).toEqual(4);
expect(result[3].line.lineNumber).toEqual(5);
expect(result[3].lines[0].lineNumber).toEqual(6);
expect(result[3].lines[1].lineNumber).toEqual(7);
expect(result[0].lineNumber).toEqual(1);
expect(result[1].lineNumber).toEqual(2);
expect(result[2].line.lineNumber).toEqual(3);
expect(result[2].lines[0].lineNumber).toEqual(4);
expect(result[2].lines[1].lineNumber).toEqual(5);
expect(result[3].line.lineNumber).toEqual(6);
expect(result[3].lines[0].lineNumber).toEqual(7);
expect(result[3].lines[1].lineNumber).toEqual(8);
});
});
@ -326,17 +326,24 @@ describe('Jobs Store Utils', () => {
});
});
describe('getIncrementalLineNumber', () => {
describe('when last line is 0', () => {
describe('getNextLineNumber', () => {
describe('when there is no previous log', () => {
it('returns 1', () => {
expect(getNextLineNumber([])).toEqual(1);
expect(getNextLineNumber(undefined)).toEqual(1);
});
});
describe('when last line is 1', () => {
it('returns 1', () => {
const log = [
{
content: [],
lineNumber: 0,
lineNumber: 1,
},
];
expect(getIncrementalLineNumber(log)).toEqual(1);
expect(getNextLineNumber(log)).toEqual(2);
});
});
@ -353,7 +360,7 @@ describe('Jobs Store Utils', () => {
},
];
expect(getIncrementalLineNumber(log)).toEqual(102);
expect(getNextLineNumber(log)).toEqual(102);
});
});
@ -374,7 +381,7 @@ describe('Jobs Store Utils', () => {
},
];
expect(getIncrementalLineNumber(log)).toEqual(102);
expect(getNextLineNumber(log)).toEqual(102);
});
});
@ -401,7 +408,7 @@ describe('Jobs Store Utils', () => {
},
];
expect(getIncrementalLineNumber(log)).toEqual(104);
expect(getNextLineNumber(log)).toEqual(104);
});
});
});
@ -420,7 +427,7 @@ describe('Jobs Store Utils', () => {
text: 'Downloading',
},
],
lineNumber: 0,
lineNumber: 1,
},
{
offset: 2,
@ -429,7 +436,7 @@ describe('Jobs Store Utils', () => {
text: 'log line',
},
],
lineNumber: 1,
lineNumber: 2,
},
]);
});
@ -448,7 +455,7 @@ describe('Jobs Store Utils', () => {
text: 'log line',
},
],
lineNumber: 0,
lineNumber: 1,
},
]);
});
@ -472,7 +479,7 @@ describe('Jobs Store Utils', () => {
},
],
section: 'section',
lineNumber: 0,
lineNumber: 1,
},
lines: [],
},
@ -498,7 +505,7 @@ describe('Jobs Store Utils', () => {
},
],
section: 'section',
lineNumber: 0,
lineNumber: 1,
},
lines: [
{
@ -509,7 +516,7 @@ describe('Jobs Store Utils', () => {
},
],
section: 'section',
lineNumber: 1,
lineNumber: 2,
},
],
},

View File

@ -9,7 +9,7 @@ exports[`Repository table row component renders a symlink table row 1`] = `
>
<a
class="str-truncated tree-item-link"
data-qa-selector="file_name_link"
data-testid="file-name-link"
href="https://test.com"
title="test"
>
@ -65,7 +65,7 @@ exports[`Repository table row component renders table row 1`] = `
>
<a
class="str-truncated tree-item-link"
data-qa-selector="file_name_link"
data-testid="file-name-link"
href="https://test.com"
title="test"
>
@ -121,7 +121,7 @@ exports[`Repository table row component renders table row for path with special
>
<a
class="str-truncated tree-item-link"
data-qa-selector="file_name_link"
data-testid="file-name-link"
href="https://test.com"
title="test"
>

View File

@ -10,9 +10,11 @@ import WorkItemCommentLocked from '~/work_items/components/notes/work_item_comme
import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
import createNoteMutation from '~/work_items/graphql/notes/create_work_item_note.mutation.graphql';
import { TRACKING_CATEGORY_SHOW } 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 {
createWorkItemNoteResponse,
groupWorkItemByIidResponseFactory,
workItemByIidResponseFactory,
workItemQueryResponse,
} from '../../mock_data';
@ -29,6 +31,7 @@ describe('Work item add note', () => {
const mutationSuccessHandler = jest.fn().mockResolvedValue(createWorkItemNoteResponse);
let workItemResponseHandler;
let groupWorkItemResponseHandler;
const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
const findTextarea = () => wrapper.findByTestId('note-reply-textarea');
@ -40,27 +43,30 @@ describe('Work item add note', () => {
canCreateNote = true,
workItemIid = '1',
workItemResponse = workItemByIidResponseFactory({ canUpdate, canCreateNote }),
groupWorkItemResponse = groupWorkItemByIidResponseFactory({ canUpdate, canCreateNote }),
signedIn = true,
isEditing = true,
isGroup = false,
workItemType = 'Task',
isInternalThread = false,
} = {}) => {
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
groupWorkItemResponseHandler = jest.fn().mockResolvedValue(groupWorkItemResponse);
if (signedIn) {
window.gon.current_user_id = '1';
window.gon.current_user_avatar_url = 'avatar.png';
}
const apolloProvider = createMockApollo([
[workItemByIidQuery, workItemResponseHandler],
[createNoteMutation, mutationHandler],
]);
const { id } = workItemQueryResponse.data.workItem;
wrapper = shallowMountExtended(WorkItemAddNote, {
apolloProvider,
apolloProvider: createMockApollo([
[workItemByIidQuery, workItemResponseHandler],
[groupWorkItemByIidQuery, groupWorkItemResponseHandler],
[createNoteMutation, mutationHandler],
]),
provide: {
fullPath: 'test-project-path',
isGroup,
},
propsData: {
workItemId: id,
@ -272,16 +278,44 @@ describe('Work item add note', () => {
});
});
it('calls the work item query', async () => {
await createComponent();
describe('when project context', () => {
it('calls the project work item query', async () => {
await createComponent();
expect(workItemResponseHandler).toHaveBeenCalled();
expect(workItemResponseHandler).toHaveBeenCalled();
});
it('skips calling the group work item query', async () => {
await createComponent();
expect(groupWorkItemResponseHandler).not.toHaveBeenCalled();
});
it('skips calling the project work item query when missing workItemIid', async () => {
await createComponent({ workItemIid: '', isEditing: false });
expect(workItemResponseHandler).not.toHaveBeenCalled();
});
});
it('skips calling the work item query when missing workItemIid', async () => {
await createComponent({ workItemIid: '', isEditing: false });
describe('when group context', () => {
it('skips calling the project work item query', async () => {
await createComponent({ isGroup: true });
expect(workItemResponseHandler).not.toHaveBeenCalled();
expect(workItemResponseHandler).not.toHaveBeenCalled();
});
it('calls the group work item query', async () => {
await createComponent({ isGroup: true });
expect(groupWorkItemResponseHandler).toHaveBeenCalled();
});
it('skips calling the group work item query when missing workItemIid', async () => {
await createComponent({ isGroup: true, workItemIid: '', isEditing: false });
expect(groupWorkItemResponseHandler).not.toHaveBeenCalled();
});
});
it('wrapper adds `internal-note` class when internal thread', async () => {

View File

@ -15,8 +15,10 @@ import NoteActions from '~/work_items/components/notes/work_item_note_actions.vu
import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
import updateWorkItemNoteMutation from '~/work_items/graphql/notes/update_work_item_note.mutation.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
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 {
groupWorkItemByIidResponseFactory,
mockAssignees,
mockWorkItemCommentNote,
updateWorkItemMutationResponse,
@ -68,6 +70,9 @@ describe('Work Item Note', () => {
});
const workItemResponseHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory());
const groupWorkItemResponseHandler = jest
.fn()
.mockResolvedValue(groupWorkItemByIidResponseFactory());
const workItemByAuthoredByDifferentUser = jest
.fn()
.mockResolvedValue(mockWorkItemByDifferentUser);
@ -90,6 +95,7 @@ describe('Work Item Note', () => {
const createComponent = ({
note = mockWorkItemCommentNote,
isFirstNote = false,
isGroup = false,
updateNoteMutationHandler = successHandler,
workItemId = mockWorkItemId,
updateWorkItemMutationHandler = updateWorkItemMutationSuccessHandler,
@ -99,6 +105,7 @@ describe('Work Item Note', () => {
wrapper = shallowMount(WorkItemNote, {
provide: {
fullPath: 'test-project-path',
isGroup,
},
propsData: {
workItemId,
@ -112,6 +119,7 @@ describe('Work Item Note', () => {
},
apolloProvider: mockApollo([
[workItemByIidQuery, workItemByIidResponseHandler],
[groupWorkItemByIidQuery, groupWorkItemResponseHandler],
[updateWorkItemNoteMutation, updateNoteMutationHandler],
[updateWorkItemMutation, updateWorkItemMutationHandler],
]),
@ -442,4 +450,32 @@ describe('Work Item Note', () => {
expect(findAwardsList().props('workItemIid')).toBe('1');
});
});
describe('when project context', () => {
it('calls the project work item query', () => {
createComponent();
expect(workItemResponseHandler).toHaveBeenCalled();
});
it('skips calling the group work item query', () => {
createComponent();
expect(groupWorkItemResponseHandler).not.toHaveBeenCalled();
});
});
describe('when group context', () => {
it('skips calling the project work item query', () => {
createComponent({ isGroup: true });
expect(workItemResponseHandler).not.toHaveBeenCalled();
});
it('calls the group work item query', () => {
createComponent({ isGroup: true });
expect(groupWorkItemResponseHandler).toHaveBeenCalled();
});
});
});

View File

@ -132,6 +132,7 @@ describe('WorkItemActions component', () => {
},
provide: {
fullPath: mockFullPath,
isGroup: false,
glFeatures: { workItemsMvc2: true },
},
mocks: {

View File

@ -7,12 +7,18 @@ import waitForPromises from 'helpers/wait_for_promises';
import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
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 { workItemByIidResponseFactory, mockAssignees } from '../mock_data';
import {
groupWorkItemByIidResponseFactory,
mockAssignees,
workItemByIidResponseFactory,
} from '../mock_data';
describe('WorkItemCreatedUpdated component', () => {
let wrapper;
let successHandler;
let groupSuccessHandler;
Vue.use(VueApollo);
@ -30,19 +36,26 @@ describe('WorkItemCreatedUpdated component', () => {
updatedAt,
confidential = false,
updateInProgress = false,
isGroup = false,
} = {}) => {
const workItemQueryResponse = workItemByIidResponseFactory({
const workItemQueryResponse = workItemByIidResponseFactory({ author, updatedAt, confidential });
const groupWorkItemQueryResponse = groupWorkItemByIidResponseFactory({
author,
updatedAt,
confidential,
});
successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
groupSuccessHandler = jest.fn().mockResolvedValue(groupWorkItemQueryResponse);
wrapper = shallowMount(WorkItemCreatedUpdated, {
apolloProvider: createMockApollo([[workItemByIidQuery, successHandler]]),
apolloProvider: createMockApollo([
[workItemByIidQuery, successHandler],
[groupWorkItemByIidQuery, groupSuccessHandler],
]),
provide: {
fullPath: '/some/project',
isGroup,
},
propsData: { workItemIid, updateInProgress },
stubs: {
@ -54,10 +67,44 @@ describe('WorkItemCreatedUpdated component', () => {
await waitForPromises();
};
it('skips the work item query when workItemIid is not defined', async () => {
await createComponent({ workItemIid: null });
describe('when project context', () => {
it('calls the project work item query', async () => {
await createComponent();
expect(successHandler).not.toHaveBeenCalled();
expect(successHandler).toHaveBeenCalled();
});
it('skips calling the group work item query', async () => {
await createComponent();
expect(groupSuccessHandler).not.toHaveBeenCalled();
});
it('skips calling the project work item query when workItemIid is not defined', async () => {
await createComponent({ workItemIid: null });
expect(successHandler).not.toHaveBeenCalled();
});
});
describe('when group context', () => {
it('skips calling the project work item query', async () => {
await createComponent({ isGroup: true });
expect(successHandler).not.toHaveBeenCalled();
});
it('calls the group work item query', async () => {
await createComponent({ isGroup: true });
expect(groupSuccessHandler).toHaveBeenCalled();
});
it('skips calling the group work item query when workItemIid is not defined', async () => {
await createComponent({ isGroup: true, workItemIid: null });
expect(groupSuccessHandler).not.toHaveBeenCalled();
});
});
it('shows work item type metadata with type and icon', async () => {

View File

@ -13,9 +13,11 @@ import WorkItemDescription from '~/work_items/components/work_item_description.v
import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
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 { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
import {
groupWorkItemByIidResponseFactory,
updateWorkItemMutationResponse,
workItemByIidResponseFactory,
workItemQueryResponse,
@ -33,6 +35,7 @@ describe('WorkItemDescription', () => {
const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
let workItemResponseHandler;
let groupWorkItemResponseHandler;
const findForm = () => wrapper.findComponent(GlForm);
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
@ -51,14 +54,19 @@ describe('WorkItemDescription', () => {
canUpdate = true,
workItemResponse = workItemByIidResponseFactory({ canUpdate }),
isEditing = false,
isGroup = false,
workItemIid = '1',
} = {}) => {
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
groupWorkItemResponseHandler = jest
.fn()
.mockResolvedValue(groupWorkItemByIidResponseFactory({ canUpdate }));
const { id } = workItemQueryResponse.data.workItem;
wrapper = shallowMount(WorkItemDescription, {
apolloProvider: createMockApollo([
[workItemByIidQuery, workItemResponseHandler],
[groupWorkItemByIidQuery, groupWorkItemResponseHandler],
[updateWorkItemMutation, mutationHandler],
]),
propsData: {
@ -67,6 +75,7 @@ describe('WorkItemDescription', () => {
},
provide: {
fullPath: 'test-project-path',
isGroup,
},
});
@ -247,9 +256,31 @@ describe('WorkItemDescription', () => {
});
});
it('calls the work item query', async () => {
await createComponent();
describe('when project context', () => {
it('calls the project work item query', () => {
createComponent();
expect(workItemResponseHandler).toHaveBeenCalled();
expect(workItemResponseHandler).toHaveBeenCalled();
});
it('skips calling the group work item query', () => {
createComponent();
expect(groupWorkItemResponseHandler).not.toHaveBeenCalled();
});
});
describe('when group context', () => {
it('skips calling the project work item query', () => {
createComponent({ isGroup: true });
expect(workItemResponseHandler).not.toHaveBeenCalled();
});
it('calls the group work item query', () => {
createComponent({ isGroup: true });
expect(groupWorkItemResponseHandler).toHaveBeenCalled();
});
});
});

View File

@ -28,12 +28,14 @@ import WorkItemStateToggleButton from '~/work_items/components/work_item_state_t
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
import { i18n } 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 updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
import workItemUpdatedSubscription from '~/work_items/graphql/work_item_updated.subscription.graphql';
import {
groupWorkItemByIidResponseFactory,
mockParent,
workItemByIidResponseFactory,
objectiveType,
@ -49,6 +51,10 @@ describe('WorkItemDetail component', () => {
Vue.use(VueApollo);
const workItemQueryResponse = workItemByIidResponseFactory({ canUpdate: true, canDelete: true });
const groupWorkItemQueryResponse = groupWorkItemByIidResponseFactory({
canUpdate: true,
canDelete: true,
});
const workItemQueryResponseWithCannotUpdate = workItemByIidResponseFactory({
canUpdate: false,
canDelete: false,
@ -59,6 +65,7 @@ describe('WorkItemDetail component', () => {
canDelete: true,
});
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
const groupSuccessHandler = jest.fn().mockResolvedValue(groupWorkItemQueryResponse);
const showModalHandler = jest.fn();
const { id } = workItemQueryResponse.data.workspace.workItems.nodes[0];
const workItemUpdatedSubscriptionHandler = jest
@ -92,6 +99,7 @@ describe('WorkItemDetail component', () => {
const findWorkItemTypeIcon = () => wrapper.findComponent(WorkItemTypeIcon);
const createComponent = ({
isGroup = false,
isModal = false,
updateInProgress = false,
workItemIid = '1',
@ -101,14 +109,13 @@ describe('WorkItemDetail component', () => {
workItemsMvc2Enabled = false,
linkedWorkItemsEnabled = false,
} = {}) => {
const handlers = [
[workItemByIidQuery, handler],
[workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler],
confidentialityMock,
];
wrapper = shallowMountExtended(WorkItemDetail, {
apolloProvider: createMockApollo(handlers),
apolloProvider: createMockApollo([
[workItemByIidQuery, handler],
[groupWorkItemByIidQuery, groupSuccessHandler],
[workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler],
confidentialityMock,
]),
isLoggedIn: isLoggedIn(),
propsData: {
isModal,
@ -131,6 +138,7 @@ describe('WorkItemDetail component', () => {
hasIssuableHealthStatusFeature: true,
projectNamespace: 'namespace',
fullPath: 'group/project',
isGroup,
reportAbusePath: '/report/abuse/path',
},
stubs: {
@ -484,25 +492,64 @@ describe('WorkItemDetail component', () => {
expect(findAlert().text()).toBe(updateError);
});
it('calls the work item query', async () => {
createComponent();
await waitForPromises();
describe('when project context', () => {
it('calls the project work item query', async () => {
createComponent();
await waitForPromises();
expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' });
expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' });
});
it('skips calling the group work item query', async () => {
createComponent();
await waitForPromises();
expect(groupSuccessHandler).not.toHaveBeenCalled();
});
it('skips calling the project work item query when there is no workItemIid', async () => {
createComponent({ workItemIid: null });
await waitForPromises();
expect(successHandler).not.toHaveBeenCalled();
});
it('calls the project work item query when isModal=true', async () => {
createComponent({ isModal: true });
await waitForPromises();
expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' });
});
});
it('skips the work item query when there is no workItemIid', async () => {
createComponent({ workItemIid: null });
await waitForPromises();
describe('when group context', () => {
it('skips calling the project work item query', async () => {
createComponent({ isGroup: true });
await waitForPromises();
expect(successHandler).not.toHaveBeenCalled();
});
expect(successHandler).not.toHaveBeenCalled();
});
it('calls the work item query when isModal=true', async () => {
createComponent({ isModal: true });
await waitForPromises();
it('calls the group work item query', async () => {
createComponent({ isGroup: true });
await waitForPromises();
expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' });
expect(groupSuccessHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' });
});
it('skips calling the group work item query when there is no workItemIid', async () => {
createComponent({ isGroup: true, workItemIid: null });
await waitForPromises();
expect(groupSuccessHandler).not.toHaveBeenCalled();
});
it('calls the group work item query when isModal=true', async () => {
createComponent({ isGroup: true, isModal: true });
await waitForPromises();
expect(groupSuccessHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' });
});
});
describe('hierarchy widget', () => {

View File

@ -7,10 +7,12 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
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 WorkItemLabels from '~/work_items/components/work_item_labels.vue';
import { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS } from '~/work_items/constants';
import {
groupWorkItemByIidResponseFactory,
projectLabelsResponse,
mockLabels,
workItemByIidResponseFactory,
@ -32,6 +34,9 @@ describe('WorkItemLabels component', () => {
const workItemQuerySuccess = jest
.fn()
.mockResolvedValue(workItemByIidResponseFactory({ labels: null }));
const groupWorkItemQuerySuccess = jest
.fn()
.mockResolvedValue(groupWorkItemByIidResponseFactory({ labels: null }));
const successSearchQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse);
const successUpdateWorkItemMutationHandler = jest
.fn()
@ -40,6 +45,7 @@ describe('WorkItemLabels component', () => {
const createComponent = ({
canUpdate = true,
isGroup = false,
workItemQueryHandler = workItemQuerySuccess,
searchQueryHandler = successSearchQueryHandler,
updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler,
@ -48,11 +54,13 @@ describe('WorkItemLabels component', () => {
wrapper = mountExtended(WorkItemLabels, {
apolloProvider: createMockApollo([
[workItemByIidQuery, workItemQueryHandler],
[groupWorkItemByIidQuery, groupWorkItemQuerySuccess],
[labelSearchQuery, searchQueryHandler],
[updateWorkItemMutation, updateWorkItemMutationHandler],
]),
provide: {
fullPath: 'test-project-path',
isGroup,
},
propsData: {
workItemId,
@ -244,17 +252,49 @@ describe('WorkItemLabels component', () => {
});
});
it('calls the work item query', async () => {
createComponent();
await waitForPromises();
describe('when project context', () => {
it('calls the project work item query', async () => {
createComponent();
await waitForPromises();
expect(workItemQuerySuccess).toHaveBeenCalled();
expect(workItemQuerySuccess).toHaveBeenCalled();
});
it('skips calling the group work item query', async () => {
createComponent();
await waitForPromises();
expect(groupWorkItemQuerySuccess).not.toHaveBeenCalled();
});
it('skips calling the project work item query when missing workItemIid', async () => {
createComponent({ workItemIid: '' });
await waitForPromises();
expect(workItemQuerySuccess).not.toHaveBeenCalled();
});
});
it('skips calling the work item query when missing workItemIid', async () => {
createComponent({ workItemIid: '' });
await waitForPromises();
describe('when group context', () => {
it('skips calling the project work item query', async () => {
createComponent({ isGroup: true });
await waitForPromises();
expect(workItemQuerySuccess).not.toHaveBeenCalled();
expect(workItemQuerySuccess).not.toHaveBeenCalled();
});
it('calls the group work item query', async () => {
createComponent({ isGroup: true });
await waitForPromises();
expect(groupWorkItemQuerySuccess).toHaveBeenCalled();
});
it('skips calling the group work item query when missing workItemIid', async () => {
createComponent({ isGroup: true, workItemIid: '' });
await waitForPromises();
expect(groupWorkItemQuerySuccess).not.toHaveBeenCalled();
});
});
});

View File

@ -54,6 +54,7 @@ describe('WorkItemChildrenWrapper', () => {
apolloProvider: mockApollo,
provide: {
fullPath: 'test/project',
isGroup: false,
},
propsData: {
workItemType,

View File

@ -64,6 +64,7 @@ describe('WorkItemLinksForm', () => {
provide: {
fullPath: 'project/path',
hasIterationsFeature,
isGroup: false,
},
});

View File

@ -13,9 +13,11 @@ 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 {
getIssueDetailsResponse,
groupWorkItemByIidResponseFactory,
workItemHierarchyResponse,
workItemHierarchyEmptyResponse,
workItemHierarchyNoUpdatePermissionResponse,
@ -32,6 +34,9 @@ describe('WorkItemLinks', () => {
let mockApollo;
const responseWithAddChildPermission = jest.fn().mockResolvedValue(workItemHierarchyResponse);
const groupResponseWithAddChildPermission = jest
.fn()
.mockResolvedValue(groupWorkItemByIidResponseFactory());
const responseWithoutAddChildPermission = jest
.fn()
.mockResolvedValue(workItemByIidResponseFactory({ adminParentLink: false }));
@ -40,10 +45,12 @@ describe('WorkItemLinks', () => {
fetchHandler = responseWithAddChildPermission,
issueDetailsQueryHandler = jest.fn().mockResolvedValue(getIssueDetailsResponse()),
hasIterationsFeature = false,
isGroup = false,
} = {}) => {
mockApollo = createMockApollo(
[
[workItemByIidQuery, fetchHandler],
[groupWorkItemByIidQuery, groupResponseWithAddChildPermission],
[issueDetailsQuery, issueDetailsQueryHandler],
],
resolvers,
@ -54,6 +61,7 @@ describe('WorkItemLinks', () => {
provide: {
fullPath: 'project/path',
hasIterationsFeature,
isGroup,
reportAbusePath: '/report/abuse/path',
},
propsData: {
@ -243,4 +251,32 @@ describe('WorkItemLinks', () => {
expect(findAbuseCategorySelector().exists()).toBe(false);
});
});
describe('when project context', () => {
it('calls the project work item query', () => {
createComponent();
expect(responseWithAddChildPermission).toHaveBeenCalled();
});
it('skips calling the group work item query', () => {
createComponent();
expect(groupResponseWithAddChildPermission).not.toHaveBeenCalled();
});
});
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();
});
});
});

View File

@ -10,9 +10,11 @@ import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue';
import WorkItemRelationshipList from '~/work_items/components/work_item_relationships/work_item_relationship_list.vue';
import WorkItemAddRelationshipForm from '~/work_items/components/work_item_relationships/work_item_add_relationship_form.vue';
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 {
groupWorkItemByIidResponseFactory,
workItemByIidResponseFactory,
mockLinkedItems,
mockBlockingLinkedItem,
@ -25,21 +27,29 @@ describe('WorkItemRelationships', () => {
const emptyLinkedWorkItemsQueryHandler = jest
.fn()
.mockResolvedValue(workItemByIidResponseFactory());
const groupWorkItemsQueryHandler = jest
.fn()
.mockResolvedValue(groupWorkItemByIidResponseFactory());
const createComponent = async ({
workItemQueryHandler = emptyLinkedWorkItemsQueryHandler,
workItemType = 'Task',
isGroup = false,
} = {}) => {
const mockApollo = createMockApollo([[workItemByIidQuery, workItemQueryHandler]]);
wrapper = shallowMountExtended(WorkItemRelationships, {
apolloProvider: mockApollo,
apolloProvider: createMockApollo([
[workItemByIidQuery, workItemQueryHandler],
[groupWorkItemByIidQuery, groupWorkItemsQueryHandler],
]),
propsData: {
workItemId: 'gid://gitlab/WorkItem/1',
workItemIid: '1',
workItemFullPath: 'test-project-path',
workItemType,
},
provide: {
isGroup,
},
});
await waitForPromises();
@ -120,4 +130,32 @@ describe('WorkItemRelationships', () => {
await findWorkItemRelationshipForm().vm.$emit('cancel');
expect(findWorkItemRelationshipForm().exists()).toBe(false);
});
describe('when project context', () => {
it('calls the project work item query', () => {
createComponent();
expect(emptyLinkedWorkItemsQueryHandler).toHaveBeenCalled();
});
it('skips calling the group work item query', () => {
createComponent();
expect(groupWorkItemsQueryHandler).not.toHaveBeenCalled();
});
});
describe('when group context', () => {
it('skips calling the project work item query', () => {
createComponent({ isGroup: true });
expect(emptyLinkedWorkItemsQueryHandler).not.toHaveBeenCalled();
});
it('calls the group work item query', () => {
createComponent({ isGroup: true });
expect(groupWorkItemsQueryHandler).toHaveBeenCalled();
});
});
});

View File

@ -86,6 +86,9 @@ describe('WorkItemTodo component', () => {
workItemFullpath: mockWorkItemFullpath,
currentUserTodos,
},
provide: {
isGroup: false,
},
});
};

View File

@ -43,7 +43,7 @@ describe('work items graphql cache utils', () => {
title: 'New child',
};
addHierarchyChild(mockCache, fullPath, iid, child);
addHierarchyChild({ cache: mockCache, fullPath, iid, workItem: child });
expect(mockCache.writeQuery).toHaveBeenCalledWith({
query: workItemByIidQuery,
@ -88,7 +88,7 @@ describe('work items graphql cache utils', () => {
title: 'New child',
};
addHierarchyChild(mockCache, fullPath, iid, child);
addHierarchyChild({ cache: mockCache, fullPath, iid, workItem: child });
expect(mockCache.writeQuery).not.toHaveBeenCalled();
});
@ -106,7 +106,7 @@ describe('work items graphql cache utils', () => {
title: 'Child',
};
removeHierarchyChild(mockCache, fullPath, iid, childToRemove);
removeHierarchyChild({ cache: mockCache, fullPath, iid, workItem: childToRemove });
expect(mockCache.writeQuery).toHaveBeenCalledWith({
query: workItemByIidQuery,
@ -145,7 +145,7 @@ describe('work items graphql cache utils', () => {
title: 'Child',
};
removeHierarchyChild(mockCache, fullPath, iid, childToRemove);
removeHierarchyChild({ cache: mockCache, fullPath, iid, workItem: childToRemove });
expect(mockCache.writeQuery).not.toHaveBeenCalled();
});

View File

@ -833,6 +833,21 @@ export const workItemByIidResponseFactory = (options) => {
};
};
export const groupWorkItemByIidResponseFactory = (options) => {
const response = workItemResponseFactory(options);
return {
data: {
workspace: {
__typename: 'Group',
id: 'gid://gitlab/Group/1',
workItems: {
nodes: [response.data.workItem],
},
},
},
};
};
export const updateWorkItemMutationResponseFactory = (options) => {
const response = workItemResponseFactory(options);
return {

View File

@ -65,6 +65,7 @@ describe('Create work item component', () => {
},
provide: {
fullPath: 'full-path',
isGroup: false,
},
});
};

View File

@ -41,6 +41,7 @@ describe('Work items router', () => {
router,
provide: {
fullPath: 'full-path',
isGroup: false,
issuesListPath: 'full-path/-/issues',
hasIssueWeightsFeature: false,
hasIterationsFeature: false,

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe WikiHelper do
RSpec.describe WikiHelper, feature_category: :wiki do
describe '#wiki_page_title' do
let_it_be(:page) { create(:wiki_page) }
@ -75,38 +75,42 @@ RSpec.describe WikiHelper do
describe '#wiki_sort_controls' do
let(:wiki) { create(:project_wiki) }
let(:wiki_link) { helper.wiki_sort_controls(wiki, direction) }
let(:classes) { "gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn rspec-reverse-sort" }
def expected_link(direction, icon_class)
before do
allow(Pajamas::ButtonComponent).to receive(:new).and_call_original
end
def expected_link_args(direction, icon_class)
path = "/#{wiki.project.full_path}/-/wikis/pages?direction=#{direction}"
title = direction == 'desc' ? _('Sort direction: Ascending') : _('Sort direction: Descending')
helper.link_to(path, type: 'button', class: classes, title: title) do
helper.sprite_icon("sort-#{icon_class}")
{
href: path,
icon: "sort-#{icon_class}",
button_options: hash_including(title: title)
}
end
context 'when initially rendering' do
it 'uses default values' do
helper.wiki_sort_controls(wiki, nil)
expect(Pajamas::ButtonComponent).to have_received(:new).with(expected_link_args('desc', 'lowest'))
end
end
context 'initial call' do
let(:direction) { nil }
it 'renders with default values' do
expect(wiki_link).to eq(expected_link('desc', 'lowest'))
end
end
context 'sort by asc order' do
let(:direction) { 'asc' }
context 'when the current sort order is ascending' do
it 'renders a link with opposite direction' do
expect(wiki_link).to eq(expected_link('desc', 'lowest'))
helper.wiki_sort_controls(wiki, 'asc')
expect(Pajamas::ButtonComponent).to have_received(:new).with(expected_link_args('desc', 'lowest'))
end
end
context 'sort by desc order' do
let(:direction) { 'desc' }
context 'when the current sort order is descending' do
it 'renders a link with opposite direction' do
expect(wiki_link).to eq(expected_link('asc', 'highest'))
helper.wiki_sort_controls(wiki, 'desc')
expect(Pajamas::ButtonComponent).to have_received(:new).with(expected_link_args('asc', 'highest'))
end
end
end

View File

@ -1,77 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BulkImports::Common::Transformers::UserReferenceTransformer do
describe '#transform' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:bulk_import) { create(:bulk_import) }
let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) }
let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
let(:hash) do
{
'user' => {
'public_email' => email
}
}
end
before do
group.add_developer(user)
end
shared_examples 'sets user_id and removes user key' do
it 'sets found user_id and removes user key' do
transformed_hash = subject.transform(context, hash)
expect(transformed_hash['user']).to be_nil
expect(transformed_hash['user_id']).to eq(user.id)
end
end
context 'when user can be found by email' do
let(:email) { user.email }
include_examples 'sets user_id and removes user key'
end
context 'when user cannot be found by email' do
let(:user) { bulk_import.user }
let(:email) { nil }
include_examples 'sets user_id and removes user key'
end
context 'when there is no data to transform' do
it 'returns' do
expect(subject.transform(nil, nil)).to be_nil
end
end
context 'when custom reference is provided' do
shared_examples 'updates provided reference' do |reference|
let(:hash) do
{
'author' => {
'public_email' => user.email
}
}
end
it 'updates provided reference' do
transformer = described_class.new(reference: reference)
result = transformer.transform(context, hash)
expect(result['author']).to be_nil
expect(result['author_id']).to eq(user.id)
end
end
include_examples 'updates provided reference', 'author'
include_examples 'updates provided reference', :author
end
end
end

View File

@ -1,96 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Prometheus::QueryVariables do
describe '.call' do
let_it_be_with_refind(:environment) { create(:environment) }
let(:project) { environment.project }
let(:slug) { environment.slug }
let(:params) { {} }
subject { described_class.call(environment, **params) }
it { is_expected.to include(ci_environment_slug: slug) }
it { is_expected.to include(ci_project_name: project.name) }
it { is_expected.to include(ci_project_namespace: project.namespace.name) }
it { is_expected.to include(ci_project_path: project.full_path) }
it { is_expected.to include(ci_environment_name: environment.name) }
it do
is_expected.to include(environment_filter:
%[container_name!="POD",environment="#{slug}"])
end
context 'without deployment platform' do
it { is_expected.to include(kube_namespace: '') }
end
context 'with deployment platform' do
context 'with project cluster' do
let(:kube_namespace) { environment.deployment_namespace }
before do
create(:cluster, :project, :provided_by_user, projects: [project])
end
it { is_expected.to include(kube_namespace: kube_namespace) }
end
context 'with group cluster' do
let(:cluster) { create(:cluster, :group, :provided_by_user, groups: [group]) }
let(:group) { create(:group) }
let(:project2) { create(:project) }
let(:kube_namespace) { k8s_ns.namespace }
let!(:k8s_ns) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project, environment: environment) }
let!(:k8s_ns2) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project2, environment: environment) }
before do
group.projects << project
group.projects << project2
end
it { is_expected.to include(kube_namespace: kube_namespace) }
end
end
context '__range' do
context 'when start_time and end_time are present' do
let(:params) do
{
start_time: Time.rfc3339('2020-05-29T07:23:05.008Z'),
end_time: Time.rfc3339('2020-05-29T15:23:05.008Z')
}
end
it { is_expected.to include(__range: "#{8.hours.to_i}s") }
end
context 'when start_time and end_time are not present' do
it { is_expected.to include(__range: nil) }
end
context 'when end_time is not present' do
let(:params) do
{
start_time: Time.rfc3339('2020-05-29T07:23:05.008Z')
}
end
it { is_expected.to include(__range: nil) }
end
context 'when start_time is not present' do
let(:params) do
{
end_time: Time.rfc3339('2020-05-29T07:23:05.008Z')
}
end
it { is_expected.to include(__range: nil) }
end
end
end
end

Some files were not shown because too many files have changed in this diff Show More