Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1d5ba345b1
commit
101c2f3ed9
|
|
@ -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
|
||||
|
|
|
|||
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
59a2ee5afdfb999a59d701a272bce212e9a49db1
|
||||
4c89bc3a1957b7f6766a5dab1446c45721d96753
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 = __(
|
||||
'We’ve 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({});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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')],
|
||||
|
|
|
|||
|
|
@ -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 = __(
|
||||
'We’ve 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({});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = __(
|
||||
'We’ve 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({});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ export default {
|
|||
variant="confirm"
|
||||
type="submit"
|
||||
data-track-action="click_button"
|
||||
data-testid="save-comment-button"
|
||||
@click="submitForm"
|
||||
>
|
||||
{{ buttonText }}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 || '',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 || [];
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = __(
|
||||
'We’ve 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({});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -295,3 +295,8 @@ $comparison-empty-state-height: 62px;
|
|||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.merge-request-review-drawer {
|
||||
max-width: 680px;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
8edd9083db215282fb49f0240769f218ae6c9c630842622ca6a1711991ab1c31
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 >}}
|
||||
|
||||
|
|
|
|||
|
|
@ -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] }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 "We’ve 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 "We’ve 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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue