Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-04-01 21:07:30 +00:00
parent 1d5ba345b1
commit 101c2f3ed9
149 changed files with 2149 additions and 594 deletions

View File

@ -230,7 +230,8 @@ e2e:test-on-gdk:
DYNAMIC_PIPELINE_YML: ${CI_PROJECT_DIR}/qa/tmp/test-on-gdk-pipeline.yml
GDK_IMAGE: "${CI_REGISTRY_IMAGE}/gitlab-qa-gdk:${CI_COMMIT_SHA}"
e2e:test-product-analytics:
# Disabling PA tests as we're moving to decommission in FY26
.e2e:test-product-analytics:
extends:
- .qa:rules:e2e:test-on-gdk
stage: qa

View File

@ -811,6 +811,16 @@ entry.
- [Quarantine a flaky test](https://gitlab.com/gitlab-org/gitlab/-/commit/998d8028213da6bf0c3c1c08301797c8b3395c28) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180234))
- [Quarantine a flaky test](https://gitlab.com/gitlab-org/gitlab/-/commit/8ae69a3765cfb7561db95e43faa30cc60fac6444) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177662))
## 17.9.4 (2025-04-01)
### Fixed (1 change)
- [Ensure runner taggings are copied from taggings](https://gitlab.com/gitlab-org/gitlab/-/commit/40aa3c8333f19f65f7a74526d61c228e2525f3c0) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186278))
### Other (1 change)
- [No-op ci_runner_machines_687967fa8a table backfill migration](https://gitlab.com/gitlab-org/gitlab/-/commit/9339d0b92dae455ad74d27cde67bc897b153eaf8) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/185303))
## 17.9.3 (2025-03-26)
### Fixed (2 changes)

View File

@ -1 +1 @@
59a2ee5afdfb999a59d701a272bce212e9a49db1
4c89bc3a1957b7f6766a5dab1446c45721d96753

View File

@ -80,7 +80,7 @@ export default {
</template>
<template #list-item="{ item }">
<preview-item :draft="item" :is-last="item.last" />
<preview-item :draft="item" />
</template>
</gl-disclosure-dropdown>
</template>

View File

@ -1,7 +1,8 @@
<script>
import { GlSprintf, GlIcon } from '@gitlab/ui';
import { GlSprintf, GlIcon, GlButton } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
import { sprintf, __ } from '~/locale';
import {
@ -15,17 +16,14 @@ export default {
components: {
GlIcon,
GlSprintf,
GlButton,
},
mixins: [resolvedStatusMixin],
mixins: [resolvedStatusMixin, glFeatureFlagsMixin()],
props: {
draft: {
type: Object,
required: true,
},
isLast: {
type: Boolean,
required: true,
},
},
computed: {
...mapGetters('diffs', ['getDiffFileByHash']),
@ -101,12 +99,23 @@ export default {
<template>
<span>
<span class="review-preview-item-header">
<gl-icon class="flex-shrink-0" :name="iconName" />
<span class="text-nowrap gl-items-center gl-font-bold">
<span class="review-preview-item-header-text block-truncated gl-ml-2">
{{ titleText }}
</span>
<component
:is="glFeatures.improvedReviewExperience ? 'gl-button' : 'span'"
:variant="glFeatures.improvedReviewExperience ? 'link' : undefined"
class="review-preview-item-header"
:class="{ '!gl-justify-start': glFeatures.improvedReviewExperience }"
data-testid="preview-item-header"
@click="$emit('click', draft)"
>
<gl-icon class="flex-shrink-0" :name="iconName" /><span
class="text-nowrap gl-items-center"
:class="{ 'gl-font-bold': !glFeatures.improvedReviewExperience }"
>
<span
class="review-preview-item-header-text block-truncated"
:class="{ 'gl-ml-2': !glFeatures.improvedReviewExperience }"
>{{ titleText }}</span
>
<template v-if="showLinePosition">
<template v-if="startLineNumber === endLineNumber">
:<span :class="getLineClasses(startLineNumber)">{{ startLineNumber }}</span>
@ -125,7 +134,7 @@ export default {
</gl-sprintf>
</template>
</span>
</span>
</component>
<span class="review-preview-item-content">
<p>{{ content }}</p>
</span>

View File

@ -5,11 +5,13 @@ import { mapGetters as mapVuexGetters } from 'vuex';
import { GlButton, GlTooltipDirective as GlTooltip, GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { SET_REVIEW_BAR_RENDERED } from '~/batch_comments/stores/modules/batch_comments/mutation_types';
import { useBatchComments } from '~/batch_comments/store';
import { REVIEW_BAR_VISIBLE_CLASS_NAME } from '../constants';
import PreviewDropdown from './preview_dropdown.vue';
import SubmitDropdown from './submit_dropdown.vue';
import SubmitDrawer from './submit_drawer.vue';
export default {
components: {
@ -17,10 +19,12 @@ export default {
GlButton,
PreviewDropdown,
SubmitDropdown,
SubmitDrawer,
},
directives: {
GlTooltip,
},
mixins: [glFeatureFlagsMixin()],
data() {
return {
discarding: false,
@ -67,20 +71,23 @@ export default {
<template>
<nav class="review-bar-component js-review-bar" data-testid="review_bar_component">
<div class="review-bar-content gl-flex gl-justify-end" data-testid="review-bar-content">
<gl-button
v-gl-tooltip
icon="remove"
variant="danger"
category="tertiary"
class="gl-mr-3"
:title="__('Discard review')"
:aria-label="__('Discard review')"
:loading="discarding"
data-testid="discard-review-btn"
@click="showDiscardModal = true"
/>
<preview-dropdown />
<submit-dropdown />
<submit-drawer v-if="glFeatures.improvedReviewExperience" />
<template v-else>
<gl-button
v-gl-tooltip
icon="remove"
variant="danger"
category="tertiary"
class="gl-mr-3"
:title="__('Discard review')"
:aria-label="__('Discard review')"
:loading="discarding"
data-testid="discard-review-btn"
@click="showDiscardModal = true"
/>
<preview-dropdown />
<submit-dropdown />
</template>
</div>
<gl-modal
v-model="showDiscardModal"

View File

@ -0,0 +1,94 @@
<script>
import { GlButton, GlDrawer } from '@gitlab/ui';
import { mapState, mapActions } from 'pinia';
// eslint-disable-next-line no-restricted-imports
import { mapActions as mapVuexActions } from 'vuex';
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { useBatchComments } from '~/batch_comments/store';
import DraftsCount from './drafts_count.vue';
import PreviewItem from './preview_item.vue';
export default {
components: {
GlButton,
GlDrawer,
DraftsCount,
PreviewItem,
},
data() {
return {
open: false,
};
},
computed: {
...mapState(useBatchComments, ['sortedDrafts', 'draftsCount']),
getDrawerHeaderHeight() {
if (!this.open) return '0';
return getContentWrapperHeight();
},
},
methods: {
...mapVuexActions('diffs', ['goToFile']),
...mapActions(useBatchComments, ['scrollToDraft']),
async onClickDraft(draft) {
if (this.viewDiffsFileByFile) {
await this.goToFile({ path: draft.file_path });
}
if (draft.position && !this.isOnLatestDiff(draft)) {
const url = new URL(setUrlParams({ commit_id: draft.position.head_sha }));
url.hash = `note_${draft.id}`;
visitUrl(url.toString());
} else {
await this.scrollToDraft(draft);
}
},
},
DRAWER_Z_INDEX,
};
</script>
<template>
<div>
<gl-button variant="confirm" data-testid="review-drawer-toggle" @click="open = !open">
{{ __('Your review') }}
<drafts-count
v-if="draftsCount > 0"
variant="info"
data-testid="reviewer-drawer-drafts-count-badge"
/>
</gl-button>
<gl-drawer
:header-height="getDrawerHeaderHeight"
:z-index="$options.DRAWER_Z_INDEX"
:open="open"
class="merge-request-review-drawer"
data-testid="review-drawer-toggle"
@close="open = false"
>
<template #title>
<h4 class="gl-m-0">{{ __('Submit your review') }}</h4>
</template>
<div>
<h5 class="h6 gl-mb-5 gl-mt-0" data-testid="reviewer-drawer-heading">
<template v-if="draftsCount > 0">
{{ n__('%d pending comment', '%d pending comments', draftsCount) }}
</template>
<template v-else>
{{ __('No pending comments') }}
</template>
</h5>
<preview-item
v-for="draft in sortedDrafts"
:key="draft.id"
:draft="draft"
class="gl-mb-3 gl-block"
@click="onClickDraft"
/>
</div>
</gl-drawer>
</div>
</template>

View File

@ -93,7 +93,9 @@ export default {
computed: {
issuesDrawerEnabled() {
if (gon.current_user_use_work_items_view) return true;
if (gon.current_user_use_work_items_view || this.glFeatures.workItemViewForIssues) {
return true;
}
return Boolean(
this.isIssueBoard ? this.glFeatures.issuesListDrawer : this.glFeatures.epicsListDrawer,
);

View File

@ -183,7 +183,9 @@ export default {
return this.isEpicBoard ? WORK_ITEM_TYPE_NAME_EPIC : this.item.type;
},
workItemDrawerEnabled() {
if (gon.current_user_use_work_items_view) return true;
if (gon.current_user_use_work_items_view || this.glFeatures.workItemViewForIssues) {
return true;
}
return this.isEpicBoard ? this.glFeatures.epicsListDrawer : this.glFeatures.issuesListDrawer;
},
workItemFullPath() {

View File

@ -57,6 +57,7 @@ export default {
return this.pipeline.source === API_ORIGIN;
},
},
buttonClass: '!gl-cursor-default gl-rounded-pill gl-border-none gl-bg-transparent gl-p-0',
};
</script>
<template>
@ -71,23 +72,27 @@ export default {
data-testid="pipeline-url-scheduled"
>{{ __('scheduled') }}</gl-badge
>
<gl-badge
<button
v-if="isTriggered"
v-gl-tooltip
:class="$options.buttonClass"
:title="__('This pipeline was created by an API call authenticated with a trigger token')"
variant="info"
data-testid="pipeline-url-triggered"
>{{ __('trigger token') }}</gl-badge
>
<gl-badge
<gl-badge variant="info">{{ __('trigger token') }}</gl-badge>
</button>
<button
v-if="pipeline.flags.latest"
v-gl-tooltip
:class="$options.buttonClass"
:title="__('Latest pipeline for the most recent commit on this ref')"
variant="success"
data-testid="pipeline-url-latest"
>{{ __('latest') }}</gl-badge
>
<gl-badge
<gl-badge variant="success">{{ __('latest') }}</gl-badge>
</button>
<button
v-if="pipeline.flags.merge_train_pipeline"
v-gl-tooltip
:title="
@ -95,26 +100,31 @@ export default {
'Pipeline|This pipeline ran on the contents of the merge request combined with the contents of all other merge requests queued for merging into the target branch.',
)
"
variant="info"
:class="$options.buttonClass"
data-testid="pipeline-url-train"
>{{ s__('Pipeline|merge train') }}</gl-badge
>
<gl-badge
<gl-badge variant="info">{{ s__('Pipeline|merge train') }}</gl-badge>
</button>
<button
v-if="pipeline.flags.yaml_errors"
v-gl-tooltip
:title="pipeline.yaml_errors"
variant="danger"
:class="$options.buttonClass"
data-testid="pipeline-url-yaml"
>{{ __('yaml invalid') }}</gl-badge
>
<gl-badge
<gl-badge variant="danger">{{ __('yaml invalid') }}</gl-badge>
</button>
<button
v-if="pipeline.flags.failure_reason"
v-gl-tooltip
:title="pipeline.failure_reason"
variant="danger"
:class="$options.buttonClass"
data-testid="pipeline-url-failure"
>{{ __('error') }}</gl-badge
>
<gl-badge variant="danger">{{ __('error') }}</gl-badge>
</button>
<template v-if="pipeline.flags.auto_devops">
<gl-link
:id="autoDevopsTagId"
@ -155,23 +165,27 @@ export default {
<gl-badge v-if="pipeline.flags.stuck" variant="warning" data-testid="pipeline-url-stuck">{{
__('stuck')
}}</gl-badge>
<gl-badge
<button
v-if="showTagBadge"
v-gl-tooltip
:title="s__(`Pipeline|This pipeline ran for a tag.`)"
variant="info"
:class="$options.buttonClass"
data-testid="pipeline-url-tag"
>{{ s__('Pipeline|tag') }}</gl-badge
>
<gl-badge
<gl-badge variant="info">{{ s__('Pipeline|tag') }}</gl-badge>
</button>
<button
v-if="showBranchBadge"
v-gl-tooltip
:title="s__(`Pipeline|This pipeline ran for a branch.`)"
variant="info"
:class="$options.buttonClass"
data-testid="pipeline-url-branch"
>{{ s__('Pipeline|branch') }}</gl-badge
>
<gl-badge
<gl-badge variant="info">{{ s__('Pipeline|branch') }}</gl-badge>
</button>
<button
v-if="pipeline.flags.detached_merge_request_pipeline"
v-gl-tooltip
:title="
@ -179,11 +193,12 @@ export default {
`Pipeline|This pipeline ran on the contents of the merge request's source branch, not the target branch.`,
)
"
variant="info"
:class="$options.buttonClass"
data-testid="pipeline-url-detached"
>{{ s__('Pipeline|merge request') }}</gl-badge
>
<gl-badge
<gl-badge variant="info">{{ s__('Pipeline|merge request') }}</gl-badge>
</button>
<button
v-if="showMergedResultsBadge"
v-gl-tooltip
:title="
@ -191,25 +206,28 @@ export default {
`Pipeline|This pipeline ran on the contents of the merge request combined with the contents of the target branch.`,
)
"
variant="info"
:class="$options.buttonClass"
data-testid="pipeline-url-merged-results"
>{{ s__('Pipeline|merged results') }}</gl-badge
>
<gl-badge
<gl-badge variant="info">{{ s__('Pipeline|merged results') }}</gl-badge>
</button>
<button
v-if="isForked"
v-gl-tooltip
:title="__('Pipeline ran in fork of project')"
variant="info"
:class="$options.buttonClass"
data-testid="pipeline-url-fork"
>{{ __('fork') }}</gl-badge
>
<gl-badge
<gl-badge variant="info">{{ __('fork') }}</gl-badge>
</button>
<button
v-if="isApi"
v-gl-tooltip
:title="__('This pipeline was triggered using the api')"
variant="info"
:class="$options.buttonClass"
data-testid="pipeline-api-badge"
>{{ s__('Pipeline|api') }}</gl-badge
>
<gl-badge variant="info">{{ s__('Pipeline|api') }}</gl-badge>
</button>
</div>
</template>

View File

@ -1,6 +1,7 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapMutations } from 'vuex';
import { debounce } from 'lodash';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import { getCookie, setCookie } from '~/lib/utils/common_utils';
import * as types from '~/diffs/store/mutation_types';
@ -23,6 +24,11 @@ export default {
required: false,
default: null,
},
floatingResize: {
type: Boolean,
required: false,
default: false,
},
},
data() {
const treeWidth =
@ -33,42 +39,102 @@ export default {
return {
treeWidth,
newWidth: null,
cachedHeight: null,
cachedTop: null,
floating: false,
};
},
computed: {
hideFileStats() {
return this.treeWidth <= TREE_HIDE_STATS_WIDTH;
},
applyNewWidthDebounced() {
return debounce(this.applyNewWidth, 250);
},
rootStyle() {
return {
width: `${this.treeWidth}px`,
height: this.cachedHeight ? `${this.cachedHeight}px` : undefined,
};
},
},
watch: {
newWidth() {
this.applyNewWidthDebounced();
},
},
methods: {
...mapMutations('diffs', {
setCurrentDiffFile: types.SET_CURRENT_DIFF_FILE,
}),
cacheTreeListWidth(size) {
setCookie(TREE_LIST_WIDTH_STORAGE_KEY, size);
},
onFileClick(file) {
this.setCurrentDiffFile(file.fileHash);
this.$emit('clickFile', file);
},
onResizeStart() {
if (!this.floatingResize) return;
this.floating = true;
this.newWidth = this.treeWidth;
const { height, top } = this.$el.getBoundingClientRect();
this.cachedHeight = height;
this.cachedTop = top;
},
onResizeEnd(size) {
setCookie(TREE_LIST_WIDTH_STORAGE_KEY, size);
if (!this.floatingResize) return;
this.floating = false;
this.cachedHeight = null;
this.newWidth = null;
this.cachedTop = null;
this.treeWidth = size;
},
onSizeUpdate(value) {
if (this.floating) {
this.newWidth = value;
} else {
this.treeWidth = value;
}
},
applyNewWidth() {
if (this.newWidth) {
this.treeWidth = this.newWidth;
}
},
},
};
</script>
<template>
<div :style="{ width: `${treeWidth}px` }" class="rd-app-sidebar diff-tree-list">
<panel-resizer
:size.sync="treeWidth"
:start-size="treeWidth"
:min-size="$options.minTreeWidth"
:max-size="$options.maxTreeWidth"
side="right"
@resize-end="cacheTreeListWidth"
/>
<tree-list
:hide-file-stats="hideFileStats"
:loaded-files="loadedFiles"
@clickFile="onFileClick"
/>
<div
data-testid="file-browser-tree"
:style="rootStyle"
class="rd-app-sidebar diff-tree-list"
:class="{ 'diff-tree-list-floating': floating }"
>
<div
data-testid="file-browser-floating-wrapper"
class="diff-tree-list-floating-wrapper"
:style="{
width: newWidth ? `${newWidth}px` : undefined,
top: cachedTop ? `${cachedTop}px` : undefined,
}"
>
<panel-resizer
class="diff-tree-list-resizer"
:start-size="treeWidth"
:min-size="$options.minTreeWidth"
:max-size="$options.maxTreeWidth"
side="right"
@update:size="onSizeUpdate"
@resize-start="onResizeStart"
@resize-end="onResizeEnd"
/>
<tree-list
:hide-file-stats="hideFileStats"
:loaded-files="loadedFiles"
@clickFile="onFileClick"
/>
</div>
</div>
</template>

View File

@ -18,8 +18,21 @@ import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initWorkItemLinks from '~/work_items/components/work_item_links';
import ZenMode from '~/zen_mode';
import initAwardsApp from '~/emoji/awards_app';
import { __ } from '~/locale';
import { issuableInitialDataById, isLegacyIssueType } from './show/utils/issuable_data';
const feedback = {};
if (gon.features?.workItemViewForIssues) {
feedback.feedbackIssue = 'https://gitlab.com/gitlab-org/gitlab/-/issues/463598';
feedback.feedbackIssueText = __('Provide feedback on the experience');
feedback.content = __(
'Weve introduced some improvements to the issue page such as real time updates, additional features, and a refreshed design. Have questions or thoughts on the changes?',
);
feedback.title = __('New issue look');
feedback.featureName = 'work_item_epic_feedback';
}
export function initForm() {
new IssuableForm($('.issue-form')); // eslint-disable-line no-new
IssuableLabelSelector();
@ -31,10 +44,10 @@ export function initForm() {
initTypeSelect();
mountMilestoneDropdown();
if (gon.features.workItemsViewPreference) {
if (gon.features.workItemsViewPreference || gon.features.workItemViewForIssues) {
import(/* webpackChunkName: 'work_items_feedback' */ '~/work_items_feedback')
.then(({ initWorkItemsFeedback }) => {
initWorkItemsFeedback();
initWorkItemsFeedback(feedback);
})
.catch({});
}
@ -62,10 +75,13 @@ export function initShow() {
.then((module) => module.default())
.catch(() => {});
if (!isLegacyIssueType(issuableData) && gon.features.workItemsViewPreference) {
if (
!isLegacyIssueType(issuableData) &&
(gon.features.workItemsViewPreference || gon.features.workItemViewForIssues)
) {
import(/* webpackChunkName: 'work_items_feedback' */ '~/work_items_feedback')
.then(({ initWorkItemsFeedback }) => {
initWorkItemsFeedback();
initWorkItemsFeedback(feedback);
})
.catch({});
}

View File

@ -4,7 +4,7 @@ import { STATUS_CLOSED } from '~/issues/constants';
import { humanTimeframe, isInPast, localeDateFormat, newDate } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import { STATE_CLOSED } from '~/work_items/constants';
import { isMilestoneWidget, isStartAndDueDateWidget } from '~/work_items/utils';
import { findMilestoneWidget, findStartAndDueDateWidget } from '~/work_items/utils';
import IssuableMilestone from '~/vue_shared/issuable/list/components/issuable_milestone.vue';
import WorkItemAttribute from '~/vue_shared/components/work_item_attribute.vue';
@ -22,10 +22,10 @@ export default {
},
computed: {
milestone() {
return this.issue.milestone || this.issue.widgets?.find(isMilestoneWidget)?.milestone;
return this.issue.milestone || findMilestoneWidget(this.issue)?.milestone;
},
dueDate() {
return this.issue.dueDate || this.issue.widgets?.find(isStartAndDueDateWidget)?.dueDate;
return this.issue.dueDate || findStartAndDueDateWidget(this.issue)?.dueDate;
},
dueDateText() {
if (this.startDate) {
@ -52,7 +52,7 @@ export default {
return this.isOverdue ? 'calendar-overdue' : 'calendar';
},
startDate() {
return this.issue.widgets?.find(isStartAndDueDateWidget)?.startDate;
return findStartAndDueDateWidget(this.issue)?.startDate;
},
timeEstimate() {
return this.issue.humanTimeEstimate || this.issue.timeStats?.humanTimeEstimate;

View File

@ -620,7 +620,11 @@ export default {
return !isEmpty(this.activeIssuable);
},
issuesDrawerEnabled() {
return this.glFeatures?.issuesListDrawer || gon.current_user_use_work_items_view;
return (
this.glFeatures?.issuesListDrawer ||
this.glFeatures?.workItemViewForIssues ||
gon.current_user_use_work_items_view
);
},
},
watch: {

View File

@ -323,7 +323,7 @@ export default {
const noteId = (hash && hash.startsWith('note_') && hash.replace(/^note_/, '')) ?? null;
if (noteId) {
const discussion = this.discussions.find((d) => d.notes.some(({ id }) => id === noteId));
const discussion = this.discussions.find((d) => d.notes?.some(({ id }) => id === noteId));
if (discussion) {
this.expandDiscussion({ discussionId: discussion.id });

View File

@ -25,6 +25,7 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import HighlightWorker from '~/vue_shared/components/source_viewer/workers/highlight_worker?worker';
import initAmbiguousRefModal from '~/ref/init_ambiguous_ref_modal';
import { InternalEvents } from '~/tracking';
import { HISTORY_BUTTON_CLICK } from '~/tracking/constants';
import { initFindFileShortcut } from '~/projects/behaviors';
import initHeaderApp from '~/repository/init_header_app';
import createRouter from '~/repository/router';
@ -229,6 +230,7 @@ const initTreeHistoryLinkApp = (el) => {
{
attrs: {
href: url.href,
'data-event-tracking': HISTORY_BUTTON_CLICK,
},
},
[__('History')],

View File

@ -2,6 +2,7 @@ import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import { mountIssuesListApp, mountJiraIssuesListApp } from '~/issues/list';
import { initWorkItemsRoot } from '~/work_items';
import { __ } from '~/locale';
mountIssuesListApp();
mountJiraIssuesListApp();
@ -9,10 +10,22 @@ addShortcutsExtension(ShortcutsNavigation);
initWorkItemsRoot();
if (gon.features.workItemsViewPreference) {
const feedback = {};
if (gon.features.workItemViewForIssues) {
feedback.feedbackIssue = 'https://gitlab.com/gitlab-org/gitlab/-/issues/463598';
feedback.feedbackIssueText = __('Provide feedback on the experience');
feedback.content = __(
'Weve introduced some improvements to the issue page such as real time updates, additional features, and a refreshed design. Have questions or thoughts on the changes?',
);
feedback.title = __('New issue look');
feedback.featureName = 'work_item_epic_feedback';
}
if (gon.features.workItemsViewPreference || gon.features.workItemViewForIssues) {
import(/* webpackChunkName: 'work_items_feedback' */ '~/work_items_feedback')
.then(({ initWorkItemsFeedback }) => {
initWorkItemsFeedback();
initWorkItemsFeedback(feedback);
})
.catch({});
}

View File

@ -2,6 +2,7 @@ import { initForm } from 'ee_else_ce/issues';
import { mountMarkdownEditor } from 'ee_else_ce/vue_shared/components/markdown/mount_markdown_editor';
import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors';
import { initWorkItemsRoot } from '~/work_items';
import { __ } from '~/locale';
initForm();
initWorkItemsRoot();
@ -9,10 +10,22 @@ initWorkItemsRoot();
// eslint-disable-next-line no-new
new IssuableTemplateSelectors({ warnTemplateOverride: true, editor: mountMarkdownEditor() });
if (gon.features.workItemsViewPreference) {
const feedback = {};
if (gon.features.workItemViewForIssues) {
feedback.feedbackIssue = 'https://gitlab.com/gitlab-org/gitlab/-/issues/463598';
feedback.feedbackIssueText = __('Provide feedback on the experience');
feedback.content = __(
'Weve introduced some improvements to the issue page such as real time updates, additional features, and a refreshed design. Have questions or thoughts on the changes?',
);
feedback.title = __('New issue look');
feedback.featureName = 'work_item_epic_feedback';
}
if (gon.features.workItemsViewPreference || gon.features.workItemViewForIssues) {
import(/* webpackChunkName: 'work_items_feedback' */ '~/work_items_feedback')
.then(({ initWorkItemsFeedback }) => {
initWorkItemsFeedback();
initWorkItemsFeedback(feedback);
})
.catch({});
}

View File

@ -15,8 +15,8 @@ const issuableData = issuableInitialDataById('js-issuable-app');
if (
!isLegacyIssueType(issuableData) &&
gon.features.workItemsViewPreference &&
gon.current_user_use_work_items_view
(gon.features.workItemViewForIssues ||
(gon.features.workItemsViewPreference && gon.current_user_use_work_items_view))
) {
initWorkItemPage();
} else {

View File

@ -1,19 +1,19 @@
<script>
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlDropdownSectionHeader } from '@gitlab/ui';
import { GlCollapsibleListbox } from '@gitlab/ui';
import { debounce } from 'lodash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import { logError } from '~/lib/logger';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { ENTER_KEY } from '~/lib/utils/keys';
const EMPTY_DROPDOWN_TEXT = s__('CompareRevisions|Select branch/tag');
const SEARCH_DEBOUNCE_MS = 300;
export default {
components: {
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlSearchBoxByType,
GlCollapsibleListbox,
},
props: {
refsProjectPath: {
@ -34,17 +34,43 @@ export default {
return {
branches: [],
tags: [],
loading: true,
isLoadingBranchesAndTags: true,
isLoadingSearch: false,
searchTerm: '',
selectedRevision: this.getDefaultBranch(),
};
},
computed: {
hasBranches() {
return Boolean(this.branches?.length);
return this.branches?.length > 0;
},
hasTags() {
return Boolean(this.tags?.length);
return this.tags?.length > 0;
},
dropdownItems() {
return [
...(this.hasBranches
? [
{
text: s__('CompareRevisions|Branches'),
options: this.branches.map((branch) => ({ text: branch, value: branch })),
},
]
: []),
...(this.hasTags
? [
{
text: s__('CompareRevisions|Tags'),
options: this.tags.map((tag) => ({ text: tag, value: tag })),
},
]
: []),
];
},
searchInputElement() {
const listbox = this.$refs.collapsibleDropdown;
const { searchBox } = listbox.$refs;
return searchBox.$refs.input;
},
},
watch: {
@ -65,6 +91,8 @@ export default {
},
methods: {
searchBranchesAndTags() {
this.isLoadingSearch = true;
return axios
.get(this.refsProjectPath, {
params: {
@ -75,16 +103,21 @@ export default {
this.branches = data.Branches || [];
this.tags = data.Tags || [];
})
.catch(() => {
.catch((e) => {
Sentry.captureException(e);
logError(`There was an error while searching the branch/tag list.`, e);
createAlert({
message: s__(
'CompareRevisions|There was an error while searching the branch/tag list. Please try again.',
),
});
})
.finally(() => {
this.isLoadingSearch = false;
});
},
fetchBranchesAndTags(reset = false) {
this.loading = true;
this.isLoadingBranchesAndTags = true;
if (reset) {
this.setSelectedRevision(this.paramsBranch);
@ -96,7 +129,9 @@ export default {
this.branches = data.Branches || [];
this.tags = data.Tags || [];
})
.catch(() => {
.catch((e) => {
Sentry.captureException(e);
logError(`There was an error while loading the branch/tag list. Please try again.`, e);
createAlert({
message: s__(
'CompareRevisions|There was an error while loading the branch/tag list. Please try again.',
@ -104,7 +139,7 @@ export default {
});
})
.finally(() => {
this.loading = false;
this.isLoadingBranchesAndTags = false;
});
},
getDefaultBranch() {
@ -114,8 +149,22 @@ export default {
this.setSelectedRevision(revision);
this.$emit('selectRevision', { direction: this.paramsName, revision });
},
onSearchEnter() {
this.setSelectedRevision(this.searchTerm);
handleKeyDown(e) {
// temporary hacks to support searching for commits on enter
// more elegant solution comes up in https://gitlab.com/gitlab-org/gitlab/-/issues/525192
const { code, target } = e;
if (code === ENTER_KEY) {
this.setSelectedRevision(target.value);
const listbox = this.$refs.collapsibleDropdown;
const dropdown = listbox.$refs.baseDropdown;
dropdown.close();
}
},
onShown() {
this.searchInputElement.addEventListener('keydown', this.handleKeyDown);
},
onHidden() {
this.searchInputElement.removeEventListener('keydown', this.handleKeyDown);
},
setSelectedRevision(revision) {
this.selectedRevision = revision || EMPTY_DROPDOWN_TEXT;
@ -127,46 +176,23 @@ export default {
<template>
<div :class="`js-compare-${paramsName}-dropdown`">
<input type="hidden" :name="paramsName" :value="selectedRevision" />
<gl-dropdown
<gl-collapsible-listbox
ref="collapsibleDropdown"
block
searchable
class="gl-w-full gl-font-monospace"
toggle-class="form-control compare-dropdown-toggle gl-min-w-0"
:text="selectedRevision"
:items="dropdownItems"
:toggle-text="selectedRevision"
:header-text="s__('CompareRevisions|Select Git revision')"
:loading="loading"
>
<template #header>
<gl-search-box-by-type
v-model.trim="searchTerm"
:placeholder="s__('CompareRevisions|Filter by Git revision')"
@keyup.enter="onSearchEnter"
/>
</template>
<gl-dropdown-section-header v-if="hasBranches">
{{ s__('CompareRevisions|Branches') }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="branch in branches"
:key="`branch-${branch}`"
is-check-item
:is-checked="selectedRevision === branch"
data-testid="branches-dropdown-item"
@click="onClick(branch)"
>
{{ branch }}
</gl-dropdown-item>
<gl-dropdown-section-header v-if="hasTags">
{{ s__('CompareRevisions|Tags') }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="tag in tags"
:key="`tag-${tag}`"
is-check-item
:is-checked="selectedRevision === tag"
data-testid="tags-dropdown-item"
@click="onClick(tag)"
>
{{ tag }}
</gl-dropdown-item>
</gl-dropdown>
:loading="isLoadingBranchesAndTags"
:searching="isLoadingSearch"
:search-placeholder="s__('CompareRevisions|Filter by Git revision')"
no-results-text=""
@shown="onShown"
@hidden="onHidden"
@search="searchTerm = $event"
@select="onClick"
/>
</div>
</template>

View File

@ -29,5 +29,10 @@ export default {
</script>
<template>
<diffs-file-tree v-if="fileBrowserVisible" :loaded-files="loadedFiles" @clickFile="clickFile" />
<diffs-file-tree
v-if="fileBrowserVisible"
floating-resize
:loaded-files="loadedFiles"
@clickFile="clickFile"
/>
</template>

View File

@ -1,6 +1,8 @@
<script>
import { GlTooltipDirective, GlIcon, GlLink, GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { InternalEvents } from '~/tracking';
import { HISTORY_BUTTON_CLICK } from '~/tracking/constants';
import SafeHtml from '~/vue_shared/directives/safe_html';
import defaultAvatarUrl from 'images/no_avatar.png';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@ -8,6 +10,8 @@ import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import getRefMixin from '../mixins/get_ref';
const trackingMixin = InternalEvents.mixin();
export default {
components: {
UserAvatarLink,
@ -21,7 +25,7 @@ export default {
GlTooltip: GlTooltipDirective,
SafeHtml,
},
mixins: [getRefMixin],
mixins: [getRefMixin, trackingMixin],
props: {
commit: {
type: Object,
@ -52,6 +56,9 @@ export default {
toggleShowDescription() {
this.showDescription = !this.showDescription;
},
handleHistoryClick() {
this.trackEvent(HISTORY_BUTTON_CLICK);
},
},
defaultAvatarUrl,
safeHtmlConfig: {
@ -107,7 +114,12 @@ export default {
data-testid="text-expander"
@click="toggleShowDescription"
/>
<gl-button size="small" data-testid="collapsible-commit-history" :href="historyUrl">
<gl-button
size="small"
data-testid="collapsible-commit-history"
:href="historyUrl"
@click="handleHistoryClick"
>
{{ __('History') }}
</gl-button>
</div>

View File

@ -22,7 +22,7 @@ import {
} from '~/behaviors/shortcuts/keybindings';
import { sanitize } from '~/lib/dompurify';
import { InternalEvents } from '~/tracking';
import { FIND_FILE_BUTTON_CLICK } from '~/tracking/constants';
import { FIND_FILE_BUTTON_CLICK, BLAME_BUTTON_CLICK } from '~/tracking/constants';
import { updateElementsVisibility } from '~/repository/utils/dom';
import {
showSingleFileEditorForkSuggestion,
@ -59,7 +59,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [getRefMixin, glFeatureFlagMixin()],
mixins: [getRefMixin, glFeatureFlagMixin(), InternalEvents.mixin()],
apollo: {
project: {
query: blobControlsQuery,
@ -247,9 +247,12 @@ export default {
);
},
handleFindFile() {
InternalEvents.trackEvent(FIND_FILE_BUTTON_CLICK);
this.trackEvent(FIND_FILE_BUTTON_CLICK);
Shortcuts.focusSearchFile();
},
handleBlameClick() {
this.trackEvent(BLAME_BUTTON_CLICK);
},
onCopy() {
navigator.clipboard.writeText(this.blobInfo.rawTextBlob);
},
@ -301,6 +304,7 @@ export default {
{ 'gl-hidden sm:gl-inline-flex': glFeatures.blobOverflowMenu },
]"
class="js-blob-blame-link"
@click="handleBlameClick"
>
{{ $options.i18n.blame }}
</gl-button>

View File

@ -1,5 +1,7 @@
<script>
import { GlTooltipDirective, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
import { InternalEvents } from '~/tracking';
import { HISTORY_BUTTON_CLICK } from '~/tracking/constants';
import SafeHtml from '~/vue_shared/directives/safe_html';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@ -14,6 +16,7 @@ import { FORK_UPDATED_EVENT } from '../constants';
import CommitInfo from './commit_info.vue';
import CollapsibleCommitInfo from './collapsible_commit_info.vue';
const trackingMixin = InternalEvents.mixin();
export default {
components: {
CommitInfo,
@ -29,7 +32,7 @@ export default {
GlTooltip: GlTooltipDirective,
SafeHtml,
},
mixins: [getRefMixin, glFeatureFlagMixin()],
mixins: [getRefMixin, glFeatureFlagMixin(), trackingMixin],
apollo: {
projectPath: {
query: projectPathQuery,
@ -108,6 +111,9 @@ export default {
refetchLastCommit() {
this.$apollo.queries.commit.refetch();
},
handleHistoryClick() {
this.trackEvent(HISTORY_BUTTON_CLICK);
},
},
};
</script>
@ -145,6 +151,7 @@ export default {
data-testid="last-commit-history"
:href="historyUrl"
class="!gl-ml-0"
@click="handleHistoryClick"
>
{{ __('History') }}
</gl-button>

View File

@ -12,8 +12,8 @@ import {
stringifyTime,
} from '~/lib/utils/datetime_utility';
import { __, s__ } from '~/locale';
import { WIDGET_TYPE_TIME_TRACKING } from '~/work_items/constants';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import { findTimeTrackingWidget } from '~/work_items/utils';
import { timelogQueries } from '../../queries/constants';
import deleteTimelogMutation from '../../queries/delete_timelog.mutation.graphql';
@ -155,9 +155,7 @@ export default {
},
(sourceData) =>
produce(sourceData, (draftState) => {
const timeTrackingWidget = draftState.workspace.workItem.widgets.find(
(widget) => widget.type === WIDGET_TYPE_TIME_TRACKING,
);
const timeTrackingWidget = findTimeTrackingWidget(draftState.workspace.workItem);
const timelogs = timeTrackingWidget.timelogs.nodes;
const index = timelogs.findIndex((timelog) => timelog.id === timelogId);

View File

@ -49,4 +49,7 @@ export const SERVICE_PING_PIPELINE_SECURITY_VISIT = 'users_visiting_pipeline_sec
export const FIND_FILE_BUTTON_CLICK = 'click_find_file_button_on_repository_pages';
export const FIND_FILE_SHORTCUT_CLICK = 'click_go_to_file_shortcut';
export const BLAME_BUTTON_CLICK = 'click_blame_control_on_blob_page';
export const HISTORY_BUTTON_CLICK = 'click_history_control_on_blob_page';
export const REF_SELECTOR_CLICK = 'click_ref_selector_on_blob_page';

View File

@ -30,7 +30,7 @@ import {
} from '~/work_items/constants';
import {
isAssigneesWidget,
isLabelsWidget,
findLabelsWidget,
findLinkedItemsWidget,
canRouterNav,
} from '~/work_items/utils';
@ -158,7 +158,7 @@ export default {
return (
this.issuable.labels?.nodes ||
this.issuable.labels ||
this.issuable.widgets?.find(isLabelsWidget)?.labels.nodes ||
findLabelsWidget(this.issuable)?.labels.nodes ||
[]
);
},
@ -266,8 +266,8 @@ export default {
// incidents and Service Desk issues
!this.isIncident &&
!this.isServiceDeskIssue &&
this.glFeatures.workItemsViewPreference &&
gon.current_user_use_work_items_view
(this.glFeatures.workItemViewForIssues ||
(this.glFeatures.workItemsViewPreference && gon.current_user_use_work_items_view))
);
},
hiddenIssuableTitle() {
@ -289,8 +289,7 @@ export default {
},
scopedLabel(label) {
const allowsScopedLabels =
this.hasScopedLabelsFeature ||
this.issuable.widgets?.find(isLabelsWidget)?.allowsScopedLabels;
this.hasScopedLabelsFeature || findLabelsWidget(this.issuable)?.allowsScopedLabels;
return allowsScopedLabels && isScopedLabel(label);
},
labelTitle(label) {

View File

@ -1,7 +1,7 @@
import produce from 'immer';
import { differenceBy } from 'lodash';
import { createAlert } from '~/alert';
import { findDesignWidget } from '~/work_items/utils';
import { findDesignsWidget } from '~/work_items/utils';
import { designWidgetOf, extractCurrentDiscussion } from './utils';
import {
designArchiveError,
@ -28,7 +28,7 @@ const addNewDesignToStore = (store, designManagementUpload, query) => {
store.writeQuery({
...query,
data: produce(sourceData, (draftData) => {
const designWidget = findDesignWidget(draftData.workItem.widgets);
const designWidget = findDesignsWidget(draftData.workItem);
const currentDesigns = designWidget.designCollection.designs.nodes;
const difference = differenceBy(designManagementUpload.designs, currentDesigns, 'filename');
@ -78,7 +78,7 @@ const addNewVersionToStore = (store, query, version) => {
const sourceData = store.readQuery(query);
const data = produce(sourceData, (draftData) => {
const designWidget = findDesignWidget(draftData.workItem.widgets);
const designWidget = findDesignsWidget(draftData.workItem);
designWidget.designCollection.versions.nodes = [
version,
...designWidgetOf(draftData).designCollection.versions.nodes,
@ -103,7 +103,7 @@ const moveDesignInStore = (store, designManagementMove, query) => {
const sourceData = store.readQuery(query);
const data = produce(sourceData, (draftData) => {
const designWidget = findDesignWidget(draftData.workItem.widgets);
const designWidget = findDesignsWidget(draftData.workItem);
designWidget.designCollection.designs.nodes =
designManagementMove.designCollection.designs.nodes;
});

View File

@ -214,7 +214,7 @@ export default {
</template>
<template #default>
<div class="gl-flex gl-flex-col gl-truncate">
<div class="gl-flex gl-flex-col gl-truncate" data-testid="design-file-name">
<span
v-gl-tooltip
class="gl-truncate gl-text-sm"

View File

@ -8,7 +8,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { TYPENAME_DESIGN_VERSION } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { findDesignWidget, canRouterNav } from '~/work_items/utils';
import { findDesignsWidget, canRouterNav } from '~/work_items/utils';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import {
@ -99,7 +99,7 @@ export default {
};
},
update(data) {
const designWidget = findDesignWidget(data.workItem.widgets);
const designWidget = findDesignsWidget(data.workItem);
if (designWidget.designCollection === null) {
return null;
}
@ -207,8 +207,8 @@ export default {
issueAsWorkItem() {
return Boolean(
!this.isGroup &&
this.glFeatures.workItemsViewPreference &&
gon.current_user_use_work_items_view,
(this.glFeatures.workItemViewForIssues ||
(this.glFeatures.workItemsViewPreference && gon.current_user_use_work_items_view)),
);
},
canUseRouter() {

View File

@ -237,6 +237,7 @@ export default {
variant="confirm"
type="submit"
data-track-action="click_button"
data-testid="save-comment-button"
@click="submitForm"
>
{{ buttonText }}

View File

@ -1,17 +1,17 @@
import { uniqueId } from 'lodash';
import { findDesignWidget } from '../../utils';
import { findDesignsWidget } from '../../utils';
export const findVersionId = (id) => (id.match('::Version/(.+$)') || [])[1];
export const findNoteId = (id) => (id.match('DiffNote/(.+$)') || [])[1];
export const extractDesigns = (data) =>
findDesignWidget(data.project.workItems.nodes[0].widgets).designCollection.designs.nodes;
findDesignsWidget(data.project.workItems.nodes[0]).designCollection.designs.nodes;
export const extractDesign = (data) => (extractDesigns(data) || [])[0];
export const extractVersions = (data) =>
findDesignWidget(data.project.workItems.nodes[0].widgets).designCollection.versions.nodes;
findDesignsWidget(data.project.workItems.nodes[0]).designCollection.versions.nodes;
export const extractDiscussions = (discussions) =>
discussions.nodes.map((discussion, index) => ({
@ -22,7 +22,7 @@ export const extractDiscussions = (discussions) =>
export const getPageLayoutElement = () => document.querySelector('.layout-page');
export const designWidgetOf = (data) => findDesignWidget(data.workItem.widgets);
export const designWidgetOf = (data) => findDesignsWidget(data.workItem);
export const extractCurrentDiscussion = (discussions, id) =>
discussions.nodes.find((discussion) => discussion.id === id);

View File

@ -8,7 +8,7 @@ import {
import { __ } from '~/locale';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { visitUrl } from '~/lib/utils/url_utility';
import { findHierarchyWidgets } from '~/work_items/utils';
import { findHierarchyWidget } from '~/work_items/utils';
import moveIssueMutation from '~/sidebar/queries/move_issue.mutation.graphql';
import searchUserProjectsToMove from '~/work_items/graphql/search_user_projects_to_move.query.graphql';
import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql';
@ -98,7 +98,7 @@ export default {
return !this.workItemId;
},
update(data) {
return findHierarchyWidgets(data?.workItem?.widgets)?.hasChildren;
return findHierarchyWidget(data?.workItem)?.hasChildren;
},
error() {
// If was not able to fetch children, show warning message anyway just in case

View File

@ -268,7 +268,7 @@ export default {
<div class="discussion-body">
<div class="discussion-wrapper">
<div class="discussion-notes">
<ul class="notes">
<ul class="notes" data-testid="note-container">
<work-item-note
is-first-note
:note="note"

View File

@ -259,7 +259,7 @@ export default {
<gl-button
v-if="showEdit"
v-gl-tooltip
data-testid="note-actions-edit"
data-testid="note-edit-button"
data-track-action="click_button"
data-track-label="edit_button"
category="tertiary"

View File

@ -154,8 +154,8 @@ export default {
issueAsWorkItem() {
return (
!this.isGroup &&
this.glFeatures.workItemsViewPreference &&
gon.current_user_use_work_items_view
(this.glFeatures.workItemViewForIssues ||
(this.glFeatures.workItemsViewPreference && gon.current_user_use_work_items_view))
);
},
childItemUniqueId() {

View File

@ -662,7 +662,7 @@ export default {
@action="handleDelete"
>
<template #list-item>
<span class="gl-text-danger">{{ i18n.deleteWorkItem }}</span>
<span>{{ i18n.deleteWorkItem }}</span>
</template>
</gl-disclosure-dropdown-item>
</template>
@ -710,6 +710,7 @@ export default {
<gl-modal
ref="modal"
modal-id="work-item-confirm-delete"
data-testid="work-item-confirm-delete"
:title="i18n.deleteWorkItem"
:ok-title="i18n.deleteWorkItem"
ok-variant="danger"

View File

@ -5,7 +5,7 @@ import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { findHierarchyWidgets, formatAncestors } from '../../utils';
import { findHierarchyWidget, formatAncestors } from '../../utils';
import workItemAncestorsQuery from '../../graphql/work_item_ancestors.query.graphql';
import workItemAncestorsUpdatedSubscription from '../../graphql/work_item_ancestors.subscription.graphql';
import WorkItemStateBadge from '../work_item_state_badge.vue';
@ -52,7 +52,7 @@ export default {
},
update(data) {
const formattedAncestors = formatAncestors(data.workItem).flatMap((ancestor) => {
const ancestorHierarchyWidget = findHierarchyWidgets(ancestor.widgets);
const ancestorHierarchyWidget = findHierarchyWidget(ancestor);
// Condition is to check if it `hasParent` is true and the parent object is null i.e, inaccessible
// then add "ancestor is not available" with other parents
return ancestorHierarchyWidget?.hasParent && !ancestorHierarchyWidget?.parent
@ -62,7 +62,7 @@ export default {
// If the work item has a parent at root level but the parent object is null i.e, inaccessible
// then add "ancestor is not available" as the only item
const widgets = findHierarchyWidgets(data.workItem?.widgets);
const widgets = findHierarchyWidget(data.workItem);
if (formattedAncestors.length === 0 && widgets?.hasParent && !widgets?.parent) {
formattedAncestors.push(ANCESTOR_NOT_AVAILABLE);
}

View File

@ -11,7 +11,8 @@ import { TYPENAME_USER } from '~/graphql_shared/constants';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
import projectWorkItemAwardEmojiQuery from '../graphql/award_emoji.query.graphql';
import updateAwardEmojiMutation from '../graphql/update_award_emoji.mutation.graphql';
import { WIDGET_TYPE_AWARD_EMOJI, DEFAULT_PAGE_SIZE_EMOJIS } from '../constants';
import { DEFAULT_PAGE_SIZE_EMOJIS } from '../constants';
import { findAwardEmojiWidget } from '../utils';
export default {
defaultAwards: [EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN],
@ -82,8 +83,7 @@ export default {
};
},
update(data) {
const widgets = data.workspace?.workItem?.widgets;
return widgets?.find((widget) => widget.type === WIDGET_TYPE_AWARD_EMOJI).awardEmoji || {};
return findAwardEmojiWidget(data.workspace?.workItem).awardEmoji || {};
},
skip() {
return !this.workItemIid;
@ -180,8 +180,7 @@ export default {
const sourceData = cache.readQuery(query);
const newData = produce(sourceData, (draftState) => {
const { widgets } = draftState.workspace.workItem;
const widgetAwardEmoji = widgets.find((widget) => widget.type === WIDGET_TYPE_AWARD_EMOJI);
const widgetAwardEmoji = findAwardEmojiWidget(draftState.workspace.workItem);
widgetAwardEmoji.awardEmoji.nodes = this.getAwardEmojiNodes(name, toggledOn);
});

View File

@ -46,8 +46,8 @@ export default {
issueAsWorkItem() {
return (
!this.isGroup &&
this.glFeatures.workItemsViewPreference &&
gon.current_user_use_work_items_view &&
(this.glFeatures.workItemViewForIssues ||
(this.glFeatures.workItemsViewPreference && gon.current_user_use_work_items_view)) &&
this.glFeatures.workItemsAlpha
);
},

View File

@ -3,7 +3,7 @@ import { GlModal, GlFormGroup, GlFormSelect, GlAlert } from '@gitlab/ui';
import { differenceBy } from 'lodash';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __, s__, sprintf } from '~/locale';
import { findDesignWidget, findMilestoneWidget, getParentGroupName } from '~/work_items/utils';
import { findDesignsWidget, getParentGroupName, isMilestoneWidget } from '~/work_items/utils';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
@ -128,7 +128,7 @@ export default {
};
},
update(data) {
return findDesignWidget(data.workItem.widgets)?.designCollection?.designs.nodes?.length > 0;
return findDesignsWidget(data.workItem)?.designCollection?.designs.nodes?.length > 0;
},
error(e) {
this.throwError(e);
@ -170,7 +170,7 @@ export default {
return this.selectedWorkItemType?.value === WORK_ITEM_TYPE_ENUM_EPIC;
},
milestoneWidget() {
return findMilestoneWidget(this.widgets)?.milestone;
return this.widgets.find(isMilestoneWidget)?.milestone;
},
selectedWorkItemTypeWidgetDefinitions() {
return this.isSelectedWorkItemTypeEpic

View File

@ -12,6 +12,7 @@ import Tracking from '~/tracking';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
findDescriptionWidget,
newWorkItemId,
newWorkItemFullPath,
autocompleteDataSources,
@ -19,13 +20,7 @@ import {
} from '~/work_items/utils';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import workItemDescriptionTemplateQuery from '../graphql/work_item_description_template.query.graphql';
import {
i18n,
NEW_WORK_ITEM_IID,
TRACKING_CATEGORY_SHOW,
WIDGET_TYPE_DESCRIPTION,
ROUTES,
} from '../constants';
import { i18n, NEW_WORK_ITEM_IID, TRACKING_CATEGORY_SHOW, ROUTES } from '../constants';
import WorkItemDescriptionRendered from './work_item_description_rendered.vue';
import WorkItemDescriptionTemplateListbox from './work_item_description_template_listbox.vue';
@ -166,9 +161,7 @@ export default {
};
},
workItemDescription() {
const descriptionWidget = this.workItem?.widgets?.find(
(widget) => widget.type === WIDGET_TYPE_DESCRIPTION,
);
const descriptionWidget = findDescriptionWidget(this.workItem);
return {
...descriptionWidget,
description: descriptionWidget?.description || '',

View File

@ -139,6 +139,7 @@ export default {
size="small"
:selected="selectedTemplateValue"
:loading="loading"
data-testid="template-dropdown"
searchable
block
class="gl-w-30"
@ -146,7 +147,7 @@ export default {
@search="handleSearch"
>
<template #list-item="{ item }">
<span class="gl-break-words">
<span class="gl-break-words" data-testid="template-item">
{{ item.text }}
</span>
</template>

View File

@ -530,7 +530,11 @@ export default {
return this.workItem?.namespace?.fullName || '';
},
contextualViewEnabled() {
return gon.current_user_use_work_items_view || this.workItemsAlphaEnabled;
return (
gon.current_user_use_work_items_view ||
this.workItemsAlphaEnabled ||
this.glFeatures?.workItemViewForIssues
);
},
hasChildren() {
return this.workItemHierarchy?.hasChildren;

View File

@ -81,8 +81,8 @@ export default {
issueAsWorkItem() {
return (
!this.isGroup &&
this.glFeatures.workItemsViewPreference &&
gon.current_user_use_work_items_view
(this.glFeatures.workItemViewForIssues ||
(this.glFeatures.workItemsViewPreference && gon.current_user_use_work_items_view))
);
},
},

View File

@ -1,5 +1,5 @@
<script>
import { isHealthStatusWidget } from '~/work_items/utils';
import { findHealthStatusWidget } from '~/work_items/utils';
export default {
components: {
@ -15,9 +15,7 @@ export default {
},
computed: {
healthStatus() {
return (
this.issue.healthStatus || this.issue.widgets?.find(isHealthStatusWidget)?.healthStatus
);
return this.issue.healthStatus || findHealthStatusWidget(this.issue)?.healthStatus;
},
hasUpdateTimeStamp() {
return this.issue.updatedAt !== this.issue.createdAt;

View File

@ -15,7 +15,7 @@ import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql';
import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
import { isLabelsWidget, newWorkItemId, newWorkItemFullPath } from '../utils';
import { findLabelsWidget, newWorkItemId, newWorkItemFullPath } from '../utils';
function formatLabelForListbox(label) {
return {
@ -134,7 +134,7 @@ export default {
];
},
labelsWidget() {
return this.workItem?.widgets?.find(isLabelsWidget);
return findLabelsWidget(this.workItem);
},
localLabels() {
return this.labelsWidget?.labels?.nodes || [];
@ -185,8 +185,7 @@ export default {
return data.workspace?.workItem || {};
},
result({ data }) {
const labels =
data?.workspace?.workItem?.widgets?.find(isLabelsWidget)?.labels?.nodes || [];
const labels = findLabelsWidget(data?.workspace?.workItem)?.labels?.nodes || [];
this.labelsCache = unionBy(this.labelsCache, labels, 'id');
},
skip() {
@ -356,7 +355,7 @@ export default {
</div>
</template>
<template #readonly>
<div class="gl-mt-1 gl-flex gl-flex-wrap gl-gap-2">
<div class="gl-mt-1 gl-flex gl-flex-wrap gl-gap-2" data-testid="selected-label-content">
<gl-label
v-for="label in localLabels"
:key="label.id"
@ -366,6 +365,7 @@ export default {
:scoped="scopedLabel(label)"
:show-close-button="canUpdate"
:target="labelFilterUrl(label)"
:data-testid="label.title"
@close="removeLabel(label)"
/>
</div>

View File

@ -12,7 +12,7 @@ import { defaultSortableOptions, DRAG_DELAY } from '~/sortable/constants';
import { sortableStart, sortableEnd } from '~/sortable/utils';
import { WORK_ITEM_TYPE_NAME_OBJECTIVE, WORK_ITEM_TYPE_NAME_EPIC } from '../../constants';
import { findHierarchyWidgetChildren, getItems, findHierarchyWidgets } from '../../utils';
import { findHierarchyWidget, findHierarchyWidgetChildren, getItems } from '../../utils';
import {
addHierarchyChild,
removeHierarchyChild,
@ -146,7 +146,7 @@ export default {
return this.canReorder ? options : {};
},
parentHeirarchyWidget() {
return findHierarchyWidgets(this.parent.widgets);
return findHierarchyWidget(this.parent);
},
disableList() {
return this.disableContent || this.updateInProgress;
@ -404,7 +404,7 @@ export default {
cache.writeQuery({
...queryArgs,
data: produce(sourceData, (draftState) => {
const hierarchyWidget = findHierarchyWidgets(draftState?.workItem.widgets);
const hierarchyWidget = findHierarchyWidget(draftState?.workItem);
hierarchyWidget.children.nodes = findHierarchyWidgetChildren(parentWorkItem);
}),
});

View File

@ -3,7 +3,7 @@ import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { createAlert } from '~/alert';
import { STATE_OPEN, WORK_ITEM_TYPE_NAME_TASK } from '../../constants';
import { findHierarchyWidgets, getDefaultHierarchyChildrenCount, getItems } from '../../utils';
import { findHierarchyWidget, getDefaultHierarchyChildrenCount, getItems } from '../../utils';
import toggleHierarchyTreeChildMutation from '../../graphql/client/toggle_hierarchy_tree_child.mutation.graphql';
import isExpandedHierarchyTreeChildQuery from '../../graphql/client/is_expanded_hierarchy_tree_child.query.graphql';
import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql';
@ -116,7 +116,7 @@ export default {
update({ workItem }) {
if (workItem) {
this.isLoadingChildren = false;
const { hasChildren, children } = findHierarchyWidgets(workItem.widgets);
const { hasChildren, children } = findHierarchyWidget(workItem);
this.children = children.nodes;
return {
pageInfo: children.pageInfo,
@ -154,10 +154,7 @@ export default {
},
computed: {
hasChildren() {
return (
findHierarchyWidgets(this.childItem.widgets).hasChildren ||
this.hierarchyWidget?.hasChildren
);
return findHierarchyWidget(this.childItem).hasChildren || this.hierarchyWidget?.hasChildren;
},
shouldExpandChildren() {
// In case the parent is the same as the child,
@ -166,8 +163,7 @@ export default {
if (this.parentId === this.childItem.id) {
return false;
}
const rolledUpCountsByType =
findHierarchyWidgets(this.childItem.widgets)?.rolledUpCountsByType || [];
const rolledUpCountsByType = findHierarchyWidget(this.childItem)?.rolledUpCountsByType || [];
const nrOpenChildren = rolledUpCountsByType
.map((i) => i.countsByState.all - i.countsByState.closed)
.reduce((sum, n) => sum + n, 0);

View File

@ -22,7 +22,7 @@ import {
WORKITEM_TREE_SHOWCLOSED_LOCALSTORAGEKEY,
} from '../../constants';
import {
findHierarchyWidgets,
findHierarchyWidget,
saveToggleToLocalStorage,
getToggleFromLocalStorage,
getItems,
@ -143,7 +143,7 @@ export default {
return this.parentIssue?.milestone;
},
hierarchyWidget() {
return this.workItem ? findHierarchyWidgets(this.workItem.widgets) : {};
return this.workItem ? findHierarchyWidget(this.workItem) : {};
},
children() {
return this.hierarchyWidget?.children?.nodes || [];

View File

@ -22,7 +22,7 @@ import {
DETAIL_VIEW_QUERY_PARAM_NAME,
} from '../../constants';
import {
findHierarchyWidgets,
findHierarchyWidget,
getDefaultHierarchyChildrenCount,
saveToggleToLocalStorage,
getToggleFromLocalStorage,
@ -162,7 +162,7 @@ export default {
return !this.workItemId;
},
update({ workItem = {} }) {
const { children } = findHierarchyWidgets(workItem.widgets);
const { children } = findHierarchyWidget(workItem);
this.workItem = workItem;
return children || {};
},
@ -241,7 +241,7 @@ export default {
},
hasIndirectChildren() {
return this.children
.map((child) => findHierarchyWidgets(child.widgets) || {})
.map((child) => findHierarchyWidget(child) || {})
.some((hierarchy) => hierarchy.hasChildren);
},
isLoadingChildren() {

View File

@ -255,6 +255,7 @@ export default {
data-reference-type="milestone"
data-placement="left"
:href="localMilestone.webPath"
data-testid="work-item-milestone-link"
>
{{ localMilestone.title }}{{ expired }}
</gl-link>

View File

@ -20,7 +20,7 @@ import {
NEW_WORK_ITEM_IID,
} from '~/work_items/constants';
import { ASC, DESC } from '~/notes/constants';
import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
import { autocompleteDataSources, findNotesWidget, markdownPreviewPath } from '~/work_items/utils';
import {
updateCacheAfterCreatingNote,
updateCacheAfterDeletingNote,
@ -310,8 +310,7 @@ export default {
};
},
update(data) {
const widgets = data.workspace?.workItem?.widgets;
return widgets?.find((widget) => widget.type === 'NOTES')?.discussions || [];
return findNotesWidget(data.workspace?.workItem)?.discussions || [];
},
skip() {
return !this.workItemIid;

View File

@ -13,7 +13,7 @@ import {
LINKED_CATEGORIES_MAP,
i18n,
} from '../constants';
import { findHierarchyWidgets, findLinkedItemsWidget } from '../utils';
import { findHierarchyWidget, findLinkedItemsWidget } from '../utils';
import { updateCountsForParent } from '../graphql/cache_utils';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
@ -141,7 +141,7 @@ export default {
if (!namespace?.workItem) return 0;
/** @type {Array<{countsByState: { opened : number }}> } */
const countsByType = findHierarchyWidgets(namespace.workItem.widgets)?.rolledUpCountsByType;
const countsByType = findHierarchyWidget(namespace.workItem)?.rolledUpCountsByType;
if (!countsByType) {
return 0;

View File

@ -10,7 +10,8 @@ import { convertEachWordToTitleCase } from '~/lib/utils/text_utility';
import { getDraft, clearDraft } from '~/lib/utils/autosave';
import { findWidget } from '~/issues/list/utils';
import {
findHierarchyWidgets,
findCurrentUserTodosWidget,
findHierarchyWidget,
findHierarchyWidgetChildren,
findNotesWidget,
getNewWorkItemAutoSaveKey,
@ -34,7 +35,6 @@ import {
WIDGET_TYPE_DESCRIPTION,
WIDGET_TYPE_CRM_CONTACTS,
NEW_WORK_ITEM_IID,
WIDGET_TYPE_CURRENT_USER_TODOS,
WIDGET_TYPE_LINKED_ITEMS,
STATE_CLOSED,
} from '../constants';
@ -182,7 +182,7 @@ export const addHierarchyChild = ({ cache, id, workItem, atIndex = null }) => {
cache.writeQuery({
...queryArgs,
data: produce(sourceData, (draftState) => {
const widget = findHierarchyWidgets(draftState?.workItem.widgets);
const widget = findHierarchyWidget(draftState?.workItem);
widget.hasChildren = true;
const children = findHierarchyWidgetChildren(draftState?.workItem) || [];
const existingChild = children.find((child) => child.id === workItem?.id);
@ -215,7 +215,7 @@ export const addHierarchyChildren = ({ cache, id, workItem, childrenIds }) => {
cache.writeQuery({
...queryArgs,
data: produce(sourceData, (draftState) => {
const widget = findHierarchyWidgets(draftState?.workItem.widgets);
const widget = findHierarchyWidget(draftState?.workItem);
const newChildren = findHierarchyWidgetChildren(workItem);
const existingChildren = findHierarchyWidgetChildren(draftState?.workItem);
@ -251,7 +251,7 @@ export const removeHierarchyChild = ({ cache, id, workItem }) => {
cache.writeQuery({
...queryArgs,
data: produce(sourceData, (draftState) => {
const widget = findHierarchyWidgets(draftState?.workItem.widgets);
const widget = findHierarchyWidget(draftState?.workItem);
const children = findHierarchyWidgetChildren(draftState?.workItem);
const index = children.findIndex((child) => child.id === workItem.id);
if (index >= 0) children.splice(index, 1);
@ -295,11 +295,7 @@ export const updateWorkItemCurrentTodosWidget = ({ cache, fullPath, iid, todos }
}
const newData = produce(sourceData, (draftState) => {
const { widgets } = draftState.workspace.workItem;
const widgetCurrentUserTodos = widgets.find(
(widget) => widget.type === WIDGET_TYPE_CURRENT_USER_TODOS,
);
const widgetCurrentUserTodos = findCurrentUserTodosWidget(draftState.workspace.workItem);
widgetCurrentUserTodos.currentUserTodos.nodes = todos;
});
@ -657,7 +653,7 @@ export const updateCountsForParent = ({ cache, parentId, workItemType, isClosing
}
const updatedParent = produce(parent, (draft) => {
const hierarchyWidget = findHierarchyWidgets(draft.workItem.widgets);
const hierarchyWidget = findHierarchyWidget(draft.workItem);
const counts = hierarchyWidget.rolledUpCountsByType.find(
(i) => i.workItemType.name === workItemType,

View File

@ -8,6 +8,7 @@ import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import { parseBoolean } from '~/lib/utils/common_utils';
import { injectVueAppBreadcrumbs } from '~/lib/utils/breadcrumbs';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import { __ } from '~/locale';
import App from './components/app.vue';
import WorkItemBreadcrumb from './components/work_item_breadcrumb.vue';
import activeDiscussionQuery from './components/design_management/graphql/client/active_design_discussion.query.graphql';
@ -86,10 +87,27 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType, withTabs } = {}
},
});
if (workItemType === 'issue' && gon.features.workItemsViewPreference && !isGroup) {
const feedback = {};
if (gon.features.workItemViewForIssues) {
feedback.feedbackIssue = 'https://gitlab.com/gitlab-org/gitlab/-/issues/463598';
feedback.feedbackIssueText = __('Provide feedback on the experience');
feedback.content = __(
'Weve introduced some improvements to the issue page such as real time updates, additional features, and a refreshed design. Have questions or thoughts on the changes?',
);
feedback.title = __('New issue look');
feedback.featureName = 'work_item_epic_feedback';
}
if (
workItemType === 'issue' &&
gon.features.workItemsViewPreference &&
!isGroup &&
!gon.features.useWiViewForIssues
) {
import(/* webpackChunkName: 'work_items_feedback' */ '~/work_items_feedback')
.then(({ initWorkItemsFeedback }) => {
initWorkItemsFeedback();
initWorkItemsFeedback(feedback);
})
.catch({});
}

View File

@ -258,7 +258,9 @@ export default {
});
},
workItemDrawerEnabled() {
if (gon.current_user_use_work_items_view) return true;
if (gon.current_user_use_work_items_view || this.glFeatures.workItemViewForIssues) {
return true;
}
return this.isEpicsList ? this.glFeatures.epicsListDrawer : this.glFeatures.issuesListDrawer;
},
isEpicsList() {

View File

@ -8,17 +8,20 @@ import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import {
NEW_WORK_ITEM_IID,
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_AWARD_EMOJI,
WIDGET_TYPE_COLOR,
WIDGET_TYPE_CURRENT_USER_TODOS,
WIDGET_TYPE_DESCRIPTION,
WIDGET_TYPE_DESIGNS,
WIDGET_TYPE_HEALTH_STATUS,
WIDGET_TYPE_HIERARCHY,
WIDGET_TYPE_LABELS,
WIDGET_TYPE_LINKED_ITEMS,
WIDGET_TYPE_MILESTONE,
WIDGET_TYPE_NOTES,
WIDGET_TYPE_START_AND_DUE_DATE,
WIDGET_TYPE_TIME_TRACKING,
WIDGET_TYPE_WEIGHT,
WIDGET_TYPE_AWARD_EMOJI,
WIDGET_TYPE_LINKED_ITEMS,
ISSUABLE_EPIC,
WORK_ITEMS_TYPE_MAP,
WORK_ITEM_TYPE_ENUM_EPIC,
@ -31,47 +34,57 @@ import {
export const isAssigneesWidget = (widget) => widget.type === WIDGET_TYPE_ASSIGNEES;
export const isHealthStatusWidget = (widget) => widget.type === WIDGET_TYPE_HEALTH_STATUS;
export const isLabelsWidget = (widget) => widget.type === WIDGET_TYPE_LABELS;
export const isMilestoneWidget = (widget) => widget.type === WIDGET_TYPE_MILESTONE;
export const isNotesWidget = (widget) => widget.type === WIDGET_TYPE_NOTES;
export const isStartAndDueDateWidget = (widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE;
export const findAwardEmojiWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_AWARD_EMOJI);
export const isWeightWidget = (widget) => widget.type === WIDGET_TYPE_WEIGHT;
export const findColorWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_COLOR);
export const findHierarchyWidgets = (widgets) =>
widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY);
export const findCurrentUserTodosWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_CURRENT_USER_TODOS);
export const findDescriptionWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESCRIPTION);
export const findDesignsWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESIGNS);
export const findHealthStatusWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_HEALTH_STATUS);
export const findHierarchyWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY);
export const findLabelsWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
export const findLinkedItemsWidget = (workItem) =>
workItem.widgets?.find((widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS);
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS);
export const findMilestoneWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_MILESTONE);
export const findNotesWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_NOTES);
export const findStartAndDueDateWidget = (workItem) =>
workItem.widgets?.find((widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE);
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE);
export const findAwardEmojiWidget = (workItem) =>
workItem.widgets?.find((widget) => widget.type === WIDGET_TYPE_AWARD_EMOJI);
export const findTimeTrackingWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_TIME_TRACKING);
export const findWeightWidget = (workItem) =>
workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_WEIGHT);
export const findHierarchyWidgetChildren = (workItem) =>
findHierarchyWidgets(workItem?.widgets)?.children?.nodes || [];
findHierarchyWidget(workItem)?.children?.nodes || [];
export const findHierarchyWidgetAncestors = (workItem) =>
findHierarchyWidgets(workItem?.widgets)?.ancestors?.nodes || [];
export const findDesignWidget = (widgets) =>
widgets?.find((widget) => widget.type === WIDGET_TYPE_DESIGNS);
export const findMilestoneWidget = (widgets) =>
widgets?.find((widget) => widget.type === WIDGET_TYPE_MILESTONE);
findHierarchyWidget(workItem)?.ancestors?.nodes || [];
export const convertTypeEnumToName = (workItemTypeEnum) =>
Object.keys(WORK_ITEM_TYPE_VALUE_MAP).find(

View File

@ -295,3 +295,8 @@ $comparison-empty-state-height: 62px;
width: 12px;
height: 12px;
}
.merge-request-review-drawer {
max-width: 680px;
width: 100%;
}

View File

@ -238,16 +238,17 @@
}
}
// This 11px value should match the additional value found in
// /assets/stylesheets/framework/diffs.scss
// for the $mr-file-header-top SCSS variable within the
// .file-title,
// .file-title-flex-parent {
// rule.
// If they don't match, the file tree and the diff files stick
// to the top at different heights, which is a bad-looking defect
$diff-file-header-top: 11px;
.diff-tree-list {
// This 11px value should match the additional value found in
// /assets/stylesheets/framework/diffs.scss
// for the $mr-file-header-top SCSS variable within the
// .file-title,
// .file-title-flex-parent {
// rule.
// If they don't match, the file tree and the diff files stick
// to the top at different heights, which is a bad-looking defect
$diff-file-header-top: 11px;
position: sticky;
top: calc(#{$calc-application-header-height} + #{$mr-sticky-header-height} + #{$diff-file-header-top});
@ -260,6 +261,35 @@
}
}
.diff-tree-list-floating {
z-index: 9;
}
.diff-tree-list-floating-wrapper {
position: relative;
height: 100%;
}
.diff-tree-list-floating .diff-tree-list-floating-wrapper {
position: fixed;
background-color: var(--gl-background-color-default);
box-shadow: $gl-spacing-scale-4 ($diff-file-header-top * -1) var(--gl-background-color-default);
}
.diff-tree-list-resizer {
$drag-handle-width: 4px;
right: calc(($gl-spacing-scale-4 + $drag-handle-width / 2) * -1) !important;
}
.diff-tree-list-resizer::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: -4px;
right: -4px;
}
.tree-list-holder {
--file-row-height: 32px;
height: 100%;

View File

@ -55,6 +55,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_force_frontend_feature_flag(:continue_indented_text, !!project&.continue_indented_text_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_beta, !!project&.work_items_beta_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_alpha, !!project&.work_items_alpha_feature_flag_enabled?)
push_frontend_feature_flag(:work_item_view_for_issues, project&.group)
end
before_action only: [:index, :show] do
@ -195,7 +196,7 @@ class Projects::IssuesController < Projects::ApplicationController
new_project = Project.find(params[:move_to_project_id])
return render_404 unless issue.can_move?(current_user, new_project)
@issue = if Feature.enabled?(:work_item_move_and_clone, project)
@issue = if project.work_item_move_and_clone_flag_enabled?
::WorkItems::DataSync::MoveService.new(
work_item: issue, current_user: current_user, target_namespace: new_project.project_namespace
).execute[:work_item]
@ -410,8 +411,9 @@ class Projects::IssuesController < Projects::ApplicationController
# Service Desk issues and incidents should not use the work item view
!issue.from_service_desk? &&
!issue.work_item_type&.incident? &&
Feature.enabled?(:work_items_view_preference, current_user) &&
current_user&.user_preference&.use_work_items_view
(Feature.enabled?(:work_item_view_for_issues, project&.group) ||
(Feature.enabled?(:work_items_view_preference, current_user) &&
current_user&.user_preference&.use_work_items_view))
end
def work_item_redirect_except_actions
@ -466,7 +468,7 @@ class Projects::IssuesController < Projects::ApplicationController
def create_vulnerability_issue_feedback(issue); end
def redirect_if_work_item
return unless use_work_items_path?(issue) && !show_work_item?
return unless use_work_items_path?(issue)
redirect_to project_work_item_path(project, issue.iid, params: request.query_parameters)
end

View File

@ -43,6 +43,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:mr_pipelines_graphql, project)
push_frontend_feature_flag(:notifications_todos_buttons, current_user)
push_frontend_feature_flag(:mr_show_reports_immediately, project)
push_frontend_feature_flag(:improved_review_experience, current_user)
end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :diffs, :rapid_diffs, :discussions]

View File

@ -18,7 +18,7 @@ module Mutations
target_project = resolve_project(full_path: target_project_path).sync
begin
moved_issue = if Feature.enabled?(:work_item_move_and_clone, source_project)
moved_issue = if source_project.work_item_move_and_clone_flag_enabled?
response = ::WorkItems::DataSync::MoveService.new(
work_item: issue, current_user: current_user,
target_namespace: target_project.project_namespace

View File

@ -2,13 +2,9 @@
module Resolvers
class BulkLabelsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::LabelType.connection_type, null: true
def resolve
authorize!(object)
bulk_load_labels
end
@ -23,10 +19,6 @@ module Resolvers
private
def authorized_resource?(object)
Ability.allowed?(current_user, :read_label, object.issuing_parent)
end
def bulk_load_labels
BatchLoader::GraphQL.for(object.id).batch(key: object.class.name, cache: false) do |ids, loader, args|
labels = Label.for_targets(object.class.id_in(ids)).group_by(&:target_id)

View File

@ -38,7 +38,6 @@ module WorkItems
participants: WorkItem.participant_includes,
parent: :work_item_parent,
children: { work_item_children_by_relative_position: [:author, { project: :project_feature }] },
labels: :labels,
milestone: { milestone: [:project, :group] },
subscribed: [:assignees, :award_emoji, { notes: [:author, :award_emoji] }],
award_emoji: { award_emoji: :awardable },

View File

@ -2,21 +2,14 @@
module Resolvers
class UserDiscussionsCountResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
# This resolver does not need to authorize object(Issue, MR, Epic, Work Item), because if object is not authorized
# in the first place we'll not even get to query the count of discussions
type GraphQL::Types::Int, null: true
def resolve
authorize!(object)
load_discussions_counts
end
def authorized_resource?(object)
ability = "read_#{object.class.name.underscore}".to_sym
context[:current_user].present? && Ability.allowed?(context[:current_user], ability, object)
end
private
def load_discussions_counts

View File

@ -29,6 +29,7 @@ module Types
field :body_html, GraphQL::Types::String,
method: :note_html,
null: true,
calls_gitaly: true,
description: "GitLab Flavored Markdown rendering of the content of the note."
field :created_at, Types::TimeType,

View File

@ -90,7 +90,8 @@ module NavHelper
def new_issue_look?
current_controller?('issues') &&
current_user&.user_preference&.use_work_items_view &&
(Feature.enabled?(:work_item_view_for_issues, @project&.group) ||
current_user&.user_preference&.use_work_items_view) &&
!@issue&.work_item_type&.incident? &&
!@issue&.from_service_desk?
end

View File

@ -1035,6 +1035,10 @@ class Group < Namespace
].compact.min
end
def work_item_move_and_clone_flag_enabled?
feature_flag_enabled_for_self_or_ancestor?(:work_item_move_and_clone, type: :wip)
end
def work_items_feature_flag_enabled?
feature_flag_enabled_for_self_or_ancestor?(:work_items)
end

View File

@ -71,6 +71,10 @@ module Namespaces
assign_attributes(attributes_to_sync)
end
def work_item_move_and_clone_flag_enabled?
project.work_item_move_and_clone_flag_enabled?
end
# It's always 1 project but it has to be an AR relation
def all_projects
Project.where(id: project.id)

View File

@ -3347,6 +3347,10 @@ class Project < ApplicationRecord
pending_delete? || hidden?
end
def work_item_move_and_clone_flag_enabled?
Feature.enabled?(:work_item_move_and_clone, self, type: :wip) || group&.work_item_move_and_clone_flag_enabled?
end
def work_items_feature_flag_enabled?
group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self)
end

View File

@ -103,7 +103,7 @@ module Issues
update(issue)
if Feature.enabled?(:work_item_move_and_clone, container)
if container.work_item_move_and_clone_flag_enabled?
move_service_container = target_container.is_a?(Project) ? target_container.project_namespace : target_container
::WorkItems::DataSync::MoveService.new(
work_item: issue, current_user: current_user, target_namespace: move_service_container
@ -157,7 +157,7 @@ module Issues
# we've pre-empted this from running in #execute, so let's go ahead and update the Issue now.
update(issue)
if Feature.enabled?(:work_item_move_and_clone, container)
if container.work_item_move_and_clone_flag_enabled?
clone_service_container = target_container.is_a?(Project) ? target_container.project_namespace : target_container
::WorkItems::DataSync::CloneService.new(
work_item: issue, current_user: current_user, target_namespace: clone_service_container,

View File

@ -19,7 +19,10 @@ module Packages
reason: :conflict
).freeze
UNAUTHORIZED_ERROR = ServiceResponse.error(message: 'Unauthorized', reason: :unauthorized).freeze
def execute
return UNAUTHORIZED_ERROR unless can?(current_user, :create_package, project)
return DUPLICATE_ERROR unless ::Namespace::PackageSetting.duplicates_allowed?(existing_package)
package = try_obtain_lease { process_package }

View File

@ -4,8 +4,11 @@ module Packages
module Nuget
class CreateTemporaryPackageService
ERRORS = {
failed_to_create_temporary_package: ServiceResponse.error(message: 'Failed to create temporary package'),
failed_to_create_package_file: ServiceResponse.error(message: 'Failed to create package file')
failed_to_create_temporary_package: ServiceResponse.error(message: 'Failed to create temporary package',
reason: :bad_request),
failed_to_create_package_file: ServiceResponse.error(message: 'Failed to create package file',
reason: :bad_request),
unauthorized: ServiceResponse.error(message: 'Unauthorized', reason: :unauthorized)
}.freeze
def initialize(project:, user:, params: {})
@ -16,6 +19,8 @@ module Packages
end
def execute
return ERRORS[:unauthorized] unless user.can?(:create_package, project)
response = ERRORS[:failed_to_create_temporary_package]
# Transaction to cover temporary package and package file creation

View File

@ -17,82 +17,82 @@
"concurrent_bitbucket_import_jobs_limit": {
"type": "integer",
"minimum": 1,
"description": "Maximum number of simultaneous import jobs for Bitbucket Cloud importer"
"description": "Maximum number of simultaneous import jobs for Bitbucket Cloud importer."
},
"concurrent_bitbucket_server_import_jobs_limit": {
"type": "integer",
"minimum": 1,
"description": "Maximum number of simultaneous import jobs for Bitbucket Server importer"
"description": "Maximum number of simultaneous import jobs for Bitbucket Server importer."
},
"concurrent_github_import_jobs_limit": {
"type": "integer",
"minimum": 1,
"description": "Maximum number of simultaneous import jobs for GitHub importer"
"description": "Maximum number of simultaneous import jobs for GitHub importer."
},
"concurrent_relation_batch_export_limit": {
"type": "integer",
"minimum": 1,
"description": "Maximum number of simultaneous batch export jobs to process"
"description": "Maximum number of simultaneous batch export jobs to process."
},
"create_organization_api_limit": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the POST /api/v4/organizations API."
"description": "Number of requests allowed to the POST /api/v4/organizations endpoint."
},
"group_api_limit": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /api/v4/groups/:id API."
"description": "Number of requests allowed to the GET /api/v4/groups/:id endpoint."
},
"group_invited_groups_api_limit": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /api/v4/groups/:id/invited_groups API."
"description": "Number of requests allowed to the GET /api/v4/groups/:id/invited_groups endpoint."
},
"group_projects_api_limit": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /api/v4/groups/:id/projects API."
"description": "Number of requests allowed to the GET /api/v4/groups/:id/projects endpoint."
},
"group_shared_groups_api_limit": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /api/v4/groups/:id/groups/shared API."
"description": "Number of requests allowed to the GET /api/v4/groups/:id/groups/shared endpoint."
},
"groups_api_limit": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /api/v4/groups API."
"description": "Number of requests allowed to the GET /api/v4/groups endpoint."
},
"project_api_limit": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /api/v4/projects/:id API."
"description": "Number of requests allowed to the GET /api/v4/projects/:id endpoint."
},
"project_invited_groups_api_limit": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /api/v4/projects/:id/invited_groups API."
"description": "Number of requests allowed to the GET /api/v4/projects/:id/invited_groups endpoint."
},
"projects_api_limit": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /api/v4/projects API."
"description": "Number of requests allowed to the GET /api/v4/projects endpoint."
},
"user_contributed_projects_api_limit": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /api/v4/users/:user_id/contributed_projects API."
"description": "Number of requests allowed to the GET /api/v4/users/:user_id/contributed_projects endpoint."
},
"user_projects_api_limit": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /api/v4/users/:user_id/projects API."
"description": "Number of requests allowed to the GET /api/v4/users/:user_id/projects endpoint."
},
"user_starred_projects_api_limit": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /api/v4/users/:user_id/starred_projects."
"description": "Number of requests allowed to the GET /api/v4/users/:user_id/starred_projects endpoint."
},
"members_delete_limit": {
"type": "integer",
@ -107,37 +107,37 @@
"users_api_limit_followers": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /users/:id/followers API."
"description": "Number of requests allowed to the GET /users/:id/followers endpoint."
},
"users_api_limit_following": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /users/:id/following API."
"description": "Number of requests allowed to the GET /users/:id/following endpoint."
},
"users_api_limit_status": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /users/:user_id/status API."
"description": "Number of requests allowed to the GET /users/:user_id/status endpoint."
},
"users_api_limit_ssh_keys": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /users/:user_id/keys API."
"description": "Number of requests allowed to the GET /users/:user_id/keys endpoint."
},
"users_api_limit_ssh_key": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /users/:id/keys/:key_id API."
"description": "Number of requests allowed to the GET /users/:id/keys/:key_id endpoint."
},
"users_api_limit_gpg_keys": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /users/:id/gpg_keys API."
"description": "Number of requests allowed to the GET /users/:id/gpg_keys endpoint."
},
"users_api_limit_gpg_key": {
"type": "integer",
"minimum": 0,
"description": "Number of requests allowed to the GET /users/:id/gpg_keys/:key_id API."
"description": "Number of requests allowed to the GET /users/:id/gpg_keys/:key_id endpoint."
}
}
}

View File

@ -12,7 +12,7 @@
issues_path: project_issues_path(@project),
project_path: @project.full_path } }
- if @project&.work_items_alpha_feature_flag_enabled? && current_user&.user_preference&.use_work_items_view
- if Feature.enabled?(:work_item_view_for_issues, @project.group) || @project&.work_items_alpha_feature_flag_enabled? && current_user&.user_preference&.use_work_items_view
#js-work-items{ data: work_items_data(@project, current_user) }
- else
.js-issues-list-root{ data: project_issues_list_data(@project, current_user) }

View File

@ -23,7 +23,7 @@
.follow-up-description
= @issue.description
- if Feature.enabled?(:work_items_view_preference, current_user) && current_user&.user_preference&.use_work_items_view
- if Feature.enabled?(:work_item_view_for_issues, @project.group) || (Feature.enabled?(:work_items_view_preference, current_user) && current_user&.user_preference&.use_work_items_view)
#js-work-items{ data: work_items_data(@project, current_user) }
- else
.page-title-holder

View File

@ -3,3 +3,5 @@
work_item.work_item_type.name.pluralize
- page_description work_item.description_html
- page_card_attributes work_item.card_attributes
- if @work_item.relocation_target
- page_canonical_link @work_item.relocation_target.present(current_user: current_user).web_url

View File

@ -0,0 +1,9 @@
---
name: work_item_view_for_issues
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/461855
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/182330
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/520791
milestone: '17.11'
group: group::project management
type: beta
default_enabled: true

View File

@ -0,0 +1,9 @@
---
name: improved_review_experience
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/525841
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/185795
rollout_issue_url:
milestone: '17.11'
group: group::code review
type: wip
default_enabled: false

View File

@ -8,14 +8,6 @@ description: https://docs.gitlab.com/ee/user/project/releases/#associate-milesto
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/a43ab8d6a430014e875deb3bff3fd8d8da256747
milestone: '12.3'
gitlab_schema: gitlab_main_cell
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: release_id
table: releases
sharding_key: project_id
belongs_to: release
desired_sharding_key_migration_job_name: BackfillMilestoneReleasesProjectId
sharding_key:
project_id: projects
table_size: small

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddMilestoneReleasesProjectIdNotNull < Gitlab::Database::Migration[2.2]
milestone '17.11'
disable_ddl_transaction!
def up
add_not_null_constraint :milestone_releases, :project_id
end
def down
remove_not_null_constraint :milestone_releases, :project_id
end
end

View File

@ -0,0 +1 @@
8edd9083db215282fb49f0240769f218ae6c9c630842622ca6a1711991ab1c31

View File

@ -17489,7 +17489,8 @@ ALTER SEQUENCE metrics_users_starred_dashboards_id_seq OWNED BY metrics_users_st
CREATE TABLE milestone_releases (
milestone_id bigint NOT NULL,
release_id bigint NOT NULL,
project_id bigint
project_id bigint,
CONSTRAINT check_8141b5b804 CHECK ((project_id IS NOT NULL))
);
CREATE TABLE milestones (

View File

@ -196,6 +196,66 @@ Example request:
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/345/service_accounts/181"
```
## List all personal access tokens for a service account user
{{< history >}}
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/526924) in GitLab 17.11.
{{< /history >}}
Lists all personal access tokens for a service account user in a top-level group.
Supported attributes:
| Attribute | Type | Required | Description |
| ------------------ | ------------------- | -------- | ----------- |
| `created_after` | datetime (ISO 8601) | No | If defined, returns tokens created after the specified time. |
| `created_before` | datetime (ISO 8601) | No | If defined, returns tokens created before the specified time. |
| `expires_after` | date (ISO 8601) | No | If defined, returns tokens that expire after the specified time. |
| `expires_before` | date (ISO 8601) | No | If defined, returns tokens that expire before the specified time. |
| `last_used_after` | datetime (ISO 8601) | No | If defined, returns tokens last used after the specified time. |
| `last_used_before` | datetime (ISO 8601) | No | If defined, returns tokens last used before the specified time. |
| `revoked` | boolean | No | If `true`, only returns revoked tokens. |
| `search` | string | No | If defined, returns tokens that include the specified value in the name. |
| `sort` | string | No | If defined, sorts the results by the specified value. Possible values: `created_asc`, `created_desc`, `expires_before_asc`, `expires_after_desc`, `last_used_before_asc`, `last_used_after_desc`, `name_asc`, `name_desc`. |
| `state` | string | No | If defined, returns tokens with the specified state. Possible values: `active` and `inactive`. |
| `user_id` | integer or string | No | If defined, returns tokens owned by the specified user. |
Example request:
```shell
curl --request GET \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/personal_access_tokens?sort=id_desc&search=token2b&created_before=2025-03-27"
```
Example response:
```json
[
{
"id": 187,
"name": "service_accounts_token2b",
"revoked": false,
"created_at": "2025-03-26T14:42:51.084Z",
"description": null,
"scopes": [
"api"
],
"user_id": 195,
"last_used_at": null,
"active": true,
"expires_at": null
}
]
```
Example of unsuccessful responses:
- `401: Unauthorized`
- `404 Personal access token(s) Not Found`
## Create a personal access token for a service account user
{{< history >}}
@ -206,12 +266,6 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
Creates a personal access token for an existing service account user in a given top-level group.
{{< alert type="note" >}}
This endpoint only works on top-level groups.
{{< /alert >}}
```plaintext
POST /groups/:id/service_accounts/:user_id/personal_access_tokens
```

View File

@ -62,6 +62,7 @@ Example of response:
},
"coverage": null,
"archived": false,
"source": "push",
"allow_failure": false,
"created_at": "2015-12-24T15:51:21.802Z",
"started_at": "2015-12-24T17:54:27.722Z",
@ -155,6 +156,7 @@ Example of response:
},
"coverage": null,
"archived": false,
"source": "push",
"allow_failure": false,
"created_at": "2015-12-24T15:51:21.727Z",
"started_at": "2015-12-24T17:54:24.729Z",
@ -253,6 +255,7 @@ Example of response:
},
"coverage": null,
"archived": false,
"source": "push",
"allow_failure": false,
"created_at": "2015-12-24T15:51:21.727Z",
"started_at": "2015-12-24T17:54:24.729Z",
@ -336,6 +339,7 @@ Example of response:
},
"coverage": null,
"archived": false,
"source": "push",
"allow_failure": false,
"created_at": "2015-12-24T15:51:21.802Z",
"started_at": "2015-12-24T17:54:27.722Z",
@ -434,6 +438,7 @@ Example of response:
},
"coverage": null,
"archived": false,
"source": "push",
"allow_failure": false,
"created_at": "2015-12-24T15:51:21.802Z",
"started_at": "2015-12-24T17:54:27.722Z",
@ -529,6 +534,7 @@ Example of response:
},
"coverage": null,
"archived": false,
"source": "push",
"allow_failure": false,
"created_at": "2015-12-24T15:51:21.880Z",
"started_at": "2015-12-24T17:54:30.733Z",
@ -686,6 +692,7 @@ Example of response:
},
"coverage": null,
"archived": false,
"source": "push",
"allow_failure": false,
"created_at": "2015-12-24T15:51:21.880Z",
"started_at": "2015-12-24T17:54:30.733Z",
@ -798,6 +805,7 @@ Example of response:
},
"coverage": null,
"archived": false,
"source": "push",
"allow_failure": false,
"created_at": "2016-01-11T10:13:33.506Z",
"started_at": "2016-01-11T10:14:09.526Z",
@ -856,6 +864,7 @@ Example of response:
},
"coverage": null,
"archived": false,
"source": "push",
"allow_failure": false,
"created_at": "2016-01-11T10:13:33.506Z",
"started_at": null,
@ -924,6 +933,7 @@ Example of response:
},
"coverage": null,
"archived": false,
"source": "push",
"allow_failure": false,
"download_url": null,
"id": 1,
@ -1012,6 +1022,7 @@ Example response:
},
"coverage": null,
"archived": false,
"source": "push",
"allow_failure": false,
"created_at": "2016-01-11T10:13:33.506Z",
"started_at": null,

View File

@ -242,7 +242,44 @@ To view the full list of jobs that ran in a project:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Build > Jobs**.
You can filter the list by [job status](#view-jobs-in-a-pipeline) and [job name](#job-names).
You can filter the list by [job status](#view-jobs-in-a-pipeline), [job name](#job-names) and [job source](#available-job-sources).
### View the source of a job
{{< history >}}
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181159) job source in GitLab 17.9 [with a flag](../../administration/feature_flags.md) named `populate_and_use_build_source_table`. Enabled by default.
- [Generally available](https://gitlab.com/groups/gitlab-org/-/epics/11796) on GitLab.com, GitLab Self-Managed, and GitLab Dedicated in GitLab 17.11.
{{< /history >}}
GitLab CI/CD jobs now include a source attribute that indicates the action that initially triggered a CI/CD job. Use this attribute to track how a job was initiated or filter job runs based on the specific sources.
#### Available job sources
The source attribute can have the following values:
`api`: Job initiated by a REST call to the Jobs API.
`chat`: Job initiated by a chat command using GitLab ChatOps.
`container_registry_push`: Job initiated by container registry push.
`duo_workflow`: Job initiated by GitLab Duo Workflow.
`external`: Job initiated by an event in an external repository integrated with GitLab. This does not include pull request events.
`external_pull_request_event`: Job initiated by a pull request event in an external repository.
`merge_request_event`: Job initiated by a merge request event.
`ondemand_dast_scan`:Job initiated by an on-demand DAST scan.
`ondemand_dast_validation`: Job initiated by an on-demand DAST validation.
`parent_pipeline`: Job initiated by a parent pipeline
`pipeline`: Job initiated by a user manually running a pipeline.
`pipeline_execution_policy`: Job initiated by a triggered pipeline execution policy.
`pipeline_execution_policy_schedule`: Job initiated by a scheduled pipeline execution policy.
`push`: Job initiated by a code push.
`scan_execution_policy`: Job initiated by a scan execution policy.
`schedule`: Job initiated by a scheduled pipeline.
`security_orchestration_policy`: Job initiated by a security orchestration policy.
`trigger`: Job initiated by another job or pipeline.
`unknown` Job initiated by an unknown source.
`web` Job initiated by a user from the GitLab UI.
`webide` Job initiated by a user from the Web IDE.
### Group similar jobs together in pipeline views

View File

@ -65,8 +65,10 @@ Follow these guidelines when you document GitLab Duo features.
When documenting a GitLab Duo experiment:
- On the [top-level GitLab Duo page](../../user/gitlab_duo/_index.md), add a topic to the
`Experimental features` section.
- On the [top-level GitLab Duo page](../../user/gitlab_duo/_index.md#summary-of-gitlab-duo-features):
- Add a row to the table.
- Add the feature to an area at the top of the page, near other features that are available
during a similar stage of the software development lifecycle.
- Document the feature near other similar features.
- Make sure you add history and status values, including any
[add-on information](styleguide/availability_details.md#add-ons).
@ -78,8 +80,8 @@ When documenting a GitLab Duo experiment:
When a GitLab Duo experiment moves to beta:
- On the [top-level GitLab Duo page](../../user/gitlab_duo/_index.md), move the topic from the
`Experimental features` to the `Beta features` section.
- On the [top-level GitLab Duo page](../../user/gitlab_duo/_index.md#summary-of-gitlab-duo-features),
update the row in the table.
- Make sure you update the history and status values, including any
[add-on information](styleguide/availability_details.md#add-ons).
- For features that are part of the [Early Access Program](../../policy/early_access_program/_index.md#add-a-feature-to-the-program)
@ -90,8 +92,8 @@ When a GitLab Duo experiment moves to beta:
When a GitLab Duo feature becomes generally available:
- On the [top-level GitLab Duo page](../../user/gitlab_duo/_index.md), move the topic from the
`Beta features` or `Experimental features` section to the `Generally available features` section.
- On the [top-level GitLab Duo page](../../user/gitlab_duo/_index.md#summary-of-gitlab-duo-features),
update the row in the table.
- Make sure you update the history and status values, including any
[add-on information](styleguide/availability_details.md#add-ons).
- For features that are part of the [Early Access Program](../../policy/early_access_program/_index.md#add-a-feature-to-the-program)

View File

@ -225,14 +225,17 @@ the appropriate project and followed up from there.
When you're creating a new issue, you can complete the following fields:
- Title
- Project: defaults to the current project
- Type: either issue (default) or incident
- [Description template](../description_templates.md): overwrites anything in the Description text box
- Description: you can use [Markdown](../../markdown.md) and [quick actions](../quick_actions.md)
- Checkbox to make the issue [confidential](confidential_issues.md)
- [Assignees](managing_issues.md#assignees)
- [Weight](issue_weight.md)
- [Epic](../../group/epics/_index.md)
- [Due date](due_dates.md)
- [Epic](../../group/epics/_index.md) (named Parent if [the new look for issues](issue_work_items.md) is enabled)
- [Due date](due_dates.md) (named Dates if [the new look for issues](issue_work_items.md) is enabled)
- [Milestone](../milestones/_index.md)
- [Labels](../labels.md)
- [Iteration](../../group/iterations/_index.md)
- [Health status](managing_issues.md#health-status) ([the new look for issues](issue_work_items.md) must be enabled)
- [Contacts](../../crm/_index.md) ([the new look for issues](issue_work_items.md) must be enabled)

View File

@ -18,6 +18,7 @@ title: Test a new look for issues
- [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9584) in GitLab 17.5 [with a flag](../../../administration/feature_flags.md) named `work_items_view_preference`. Disabled by default. This feature is in [beta](../../../policy/development_stages_support.md#beta).
- Feature flag named `work_items_view_preference` enabled on GitLab.com in GitLab 17.9 for a subset of users.
- Feature flag named `work_items_view_preference` [enabled](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/184496) on GitLab.com, GitLab Self-Managed, and GitLab Dedicated in 17.10.
- **New look** toggle [hidden](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/182330) with feature flag named `work_item_view_for_issues`. Flag enabled on GitLab.com, GitLab Self-Managed, and GitLab Dedicated in 17.11.
{{< /history >}}
@ -69,7 +70,14 @@ The new issues experience includes these improvements:
## Toggle the new experience
When you view an issue list or issue detail page, you can manage the new experience:
When you view the Issues page or issue detail page, you can toggle the new experience.
Prerequisites:
- The feature flag `work_items_view_preference` must be enabled.
- The feature flag `work_item_view_for_issues` must be disabled.
To toggle the new issue look:
1. In the upper-right corner look for the **New look** badge.
1. Select the badge to toggle the experience on or off.

View File

@ -39,7 +39,7 @@ To edit an issue:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Plan > Issues**, then select the title of your issue to view it.
1. To the right of the title, select **Edit title and description** ({{< icon name="pencil" >}}).
1. To the right of the title, select **Edit** ({{< icon name="pencil" >}}).
1. Edit the available fields.
1. Select **Save changes**.
@ -809,6 +809,7 @@ You can use the OR operator (**is one of: `||`**) when you [filter the list of i
{{< history >}}
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/464063) in GitLab 17.4 [with a flag](../../../administration/feature_flags.md) named `issues_list_drawer`. Disabled by default.
- In GitLab 17.11, if [the new look for issues](issue_work_items.md) is enabled, this feature is also enabled.
{{< /history >}}
@ -816,7 +817,6 @@ You can use the OR operator (**is one of: `||`**) when you [filter the list of i
The availability of this feature is controlled by a feature flag.
For more information, see the history.
This feature is available for testing, but not ready for production use.
{{< /alert >}}

View File

@ -381,7 +381,7 @@ module API
not_found!('Project') unless new_project
begin
issue = if Feature.enabled?(:work_item_move_and_clone, user_project)
issue = if user_project.work_item_move_and_clone_flag_enabled?
response = ::WorkItems::DataSync::MoveService.new(
work_item: issue, current_user: current_user, target_namespace: new_project.project_namespace
).execute
@ -421,7 +421,7 @@ module API
not_found!('Project') unless target_project
begin
issue = if Feature.enabled?(:work_item_move_and_clone, user_project)
issue = if user_project.work_item_move_and_clone_flag_enabled?
response = ::WorkItems::DataSync::CloneService.new(
work_item: issue, current_user: current_user, target_namespace: target_project.project_namespace,
params: { clone_with_notes: params[:with_notes] }

View File

@ -125,7 +125,7 @@ module API
}
).execute
bad_request!(response.message) if response.error?
render_api_error!(response.message, response.reason) if response.error?
end
def extracted_metadata

View File

@ -88,6 +88,7 @@ module Gitlab
# To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/399248
push_frontend_feature_flag(:remove_monitor_metrics)
push_frontend_feature_flag(:work_items_view_preference, current_user)
push_frontend_feature_flag(:work_item_view_for_issues)
push_frontend_feature_flag(:search_button_top_right, current_user)
push_frontend_feature_flag(:merge_request_dashboard, current_user, type: :wip)
push_frontend_feature_flag(:new_project_creation_form, current_user, type: :wip)

View File

@ -452,7 +452,7 @@ module Gitlab
def can_be_moved_or_cloned?
return true unless quick_action_target.is_a?(WorkItem) && quick_action_target.work_item_type.epic?
::Feature.enabled?(:work_item_move_and_clone, container)
container.work_item_move_and_clone_flag_enabled?
end
end
end

View File

@ -29,7 +29,7 @@ module Gitlab
return Gitlab::SlashCommands::Presenters::Access.new.not_found
end
new_issue = if Feature.enabled?(:work_item_move_and_clone, project)
new_issue = if project.work_item_move_and_clone_flag_enabled?
response = ::WorkItems::DataSync::MoveService.new(
work_item: old_issue, current_user: current_user,
target_namespace: target_project.project_namespace

View File

@ -38911,6 +38911,9 @@ msgid_plural "New issues"
msgstr[0] ""
msgstr[1] ""
msgid "New issue look"
msgstr ""
msgid "New issue title"
msgstr ""
@ -39364,6 +39367,9 @@ msgstr ""
msgid "No parent group"
msgstr ""
msgid "No pending comments"
msgstr ""
msgid "No phone number data for matching"
msgstr ""
@ -57755,6 +57761,9 @@ msgstr ""
msgid "Submit the current review."
msgstr ""
msgid "Submit your review"
msgstr ""
msgid "Submit/save changes"
msgstr ""
@ -66469,6 +66478,9 @@ msgstr ""
msgid "Weve introduced some improvements to the epic page such as real time updates, additional features, and a refreshed design. Have questions or thoughts on the changes?"
msgstr ""
msgid "Weve introduced some improvements to the issue page such as real time updates, additional features, and a refreshed design. Have questions or thoughts on the changes?"
msgstr ""
msgid "What are some examples?"
msgstr ""
@ -69346,6 +69358,9 @@ msgstr ""
msgid "Your resource access tokens will expire in %{days_to_expire} or less"
msgstr ""
msgid "Your review"
msgstr ""
msgid "Your search didn't match any commits. Try a different query."
msgstr ""

View File

@ -42,6 +42,10 @@ module QA
view 'app/assets/javascripts/design_management/components/delete_button.vue' do
element 'confirm-archiving-button'
end
view 'app/assets/javascripts/work_items/components/design_management/design_management_widget.vue' do
element 'design-item'
end
end
end
@ -65,8 +69,14 @@ module QA
# It accepts a `class:` option, but that only works for class attributes
# It doesn't work as a CSS selector.
# So instead we use the name attribute as a locator
within_element('design-dropzone-content') do
page.attach_file("upload_file", design_file_path, make_visible: { display: 'block' })
if work_item_enabled?
page.attach_file("design_file", design_file_path, make_visible: { display: 'block' }, match: :first)
else
within_element('design-dropzone-content') do
page.attach_file("upload_file", design_file_path, make_visible: { display: 'block' })
end
end
filename = ::File.basename(design_file_path)
@ -74,7 +84,7 @@ module QA
wait_until(reload: false, sleep_interval: 1, message: "Design upload") do
image = find_element('design-image', filename: filename).find('img')
has_element?('design-file-name', text: filename) && image["complete"] && image["naturalWidth"].to_i > 0
image["complete"] && image["naturalWidth"].to_i > 0
end
end
@ -103,7 +113,11 @@ module QA
end
def has_design?(filename)
has_element?('design-file-name', text: filename)
if work_item_enabled?
has_element?('design-item', text: filename)
else
has_element?('design-file-name', text: filename)
end
end
def has_no_design?(filename)

View File

@ -75,7 +75,7 @@ module QA
# Attachment option should be an absolute path
def comment(text, attachment: nil, filter: :all_activities)
method("select_#{filter}_filter").call
fill_editor_element 'comment-field', "#{text}\n"
fill_editor_element('comment-field', "#{text}\n")
unless attachment.nil?
QA::Page::Component::Dropzone.new(self, '.new-note')
@ -83,7 +83,7 @@ module QA
end
has_active_element?('comment-button', wait: 0.5)
click_element 'comment-button'
click_element('comment-button')
end
def edit_comment(text)
@ -165,14 +165,14 @@ module QA
end
def start_review_with_comment(text)
fill_editor_element 'comment-field', text
click_element 'start-review-button'
fill_editor_element('comment-field', text)
click_element('start-review-button')
has_comment?(text)
end
def add_comment_to_review(text)
fill_editor_element 'comment-field', text
click_element 'add-to-review-button'
fill_editor_element('comment-field', text)
click_element('add-to-review-button')
has_comment?(text)
end
@ -190,7 +190,7 @@ module QA
def select_filter_with_text(text)
retry_on_exception do
click_element('issue-title')
click_element 'discussion-preferences-dropdown'
click_element('discussion-preferences-dropdown')
find_element('filter-menu-item', text: text).click
wait_for_requests

View File

@ -26,6 +26,10 @@ module QA
element 'confirm-button'
end
base.view 'app/assets/javascripts/work_items/components/notes/work_item_discussion.vue' do
element 'note-container'
end
base.view 'app/assets/javascripts/work_items/components/notes/work_item_note_body.vue' do
element 'work-item-note-body'
end
@ -33,14 +37,50 @@ module QA
base.view 'app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue' do
element 'work-item-filter'
end
base.view 'app/assets/javascripts/work_items/components/notes/work_item_note.vue' do
element 'note-wrapper'
end
base.view 'app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue' do
element 'note-edit-button'
end
base.view 'app/assets/javascripts/notes/components/toggle_replies_widget.vue' do
element 'expand-replies-button'
element 'collapse-replies-button'
end
end
def comment(text, filter: :all_activities)
def collapse_replies
click_element 'collapse-replies-button'
end
# Attachment option should be an absolute path
def comment(text, attachment: nil, filter: :all_activities)
method(:"select_#{filter}_filter").call
fill_element 'markdown-editor-form-field', "#{text}\n"
fill_editor_element('markdown-editor-form-field', "#{text}\n")
unless attachment.nil?
QA::Page::Component::Dropzone.new(self, '.new-note')
.attach_file(attachment)
end
has_active_element?('confirm-button', wait: 0.5)
click_element 'confirm-button'
end
def edit_comment(text)
click_element 'note-edit-button'
within_element 'note-wrapper' do
fill_and_submit_comment(text)
end
end
def expand_replies
click_element 'expand-replies-button'
end
def has_comment?(comment_text)
has_element?(
'work-item-note-body',
@ -49,10 +89,27 @@ module QA
)
end
def has_comment_author?(author_username)
within_element('work-item-note-body') do
has_element?('author-name', text: author_username, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
end
end
def has_system_note?(note_text)
has_element?('system-note-content', text: note_text, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
end
def noteable_note_item
find_element('work-item-note-body')
end
def reply_to_comment(position, reply_text)
all_elements('reply-icon', minimum: position)[position - 1].click
within_element 'note-container' do
fill_and_submit_comment(reply_text)
end
end
def select_all_activities_filter
select_filter_with_type('ALL_NOTES')
@ -79,6 +136,12 @@ module QA
private
def fill_and_submit_comment(text)
fill_editor_element('markdown-editor-form-field', "#{text}\n")
has_active_element?('confirm-button', wait: 0.5)
click_element 'confirm-button'
end
def select_filter_with_type(type)
retry_on_exception do
click_element('work-item-title')

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