Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-09-27 06:10:19 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-09-27 06:10:19 +0300
commit0a0dcc392ca69b7f0567bff6bc1040ded035a11b (patch)
tree1a892633e20f593140612e700a31c4460b2a08c0 /app/assets/javascripts
parent272c39ac05e0d68444114aed58ef2b44e1af30d6 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/admin/abuse_report/constants.js2
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/constants.js3
-rw-r--r--app/assets/javascripts/ci/common/private/job_action_component.vue2
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue87
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue29
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue (renamed from app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue)23
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/jobs_table.vue36
-rw-r--r--app/assets/javascripts/ci/jobs_page/constants.js15
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue3
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue9
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue6
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_token_input.vue26
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue1
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue245
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue55
-rw-r--r--app/assets/javascripts/work_items/constants.js17
-rw-r--r--app/assets/javascripts/work_items/graphql/add_linked_items.mutation.graphql10
-rw-r--r--app/assets/javascripts/work_items/graphql/project_work_items.query.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql1
23 files changed, 438 insertions, 142 deletions
diff --git a/app/assets/javascripts/admin/abuse_report/constants.js b/app/assets/javascripts/admin/abuse_report/constants.js
index 6cae6b24f20..1ecef44ab8f 100644
--- a/app/assets/javascripts/admin/abuse_report/constants.js
+++ b/app/assets/javascripts/admin/abuse_report/constants.js
@@ -87,6 +87,7 @@ export const REPORTED_CONTENT_I18N = {
comment: s__('AbuseReport|Reported comment'),
issue: s__('AbuseReport|Reported issue'),
merge_request: s__('AbuseReport|Reported merge request'),
+ epic: s__('AbuseReport|Reported epic'),
unknown: s__('AbuseReport|Reported content'),
},
viewScreenshot: s__('AbuseReport|View screenshot'),
@@ -96,6 +97,7 @@ export const REPORTED_CONTENT_I18N = {
comment: s__('AbuseReport|Go to comment'),
issue: s__('AbuseReport|Go to issue'),
merge_request: s__('AbuseReport|Go to merge request'),
+ epic: s__('AbuseReport|Go to epic'),
unknown: s__('AbuseReport|Go to content'),
},
reportedBy: s__('AbuseReport|Reported by'),
diff --git a/app/assets/javascripts/ci/admin/jobs_table/constants.js b/app/assets/javascripts/ci/admin/jobs_table/constants.js
index ff0efdb1f5b..86c9ab53e75 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/constants.js
+++ b/app/assets/javascripts/ci/admin/jobs_table/constants.js
@@ -26,9 +26,6 @@ export const DEFAULT_FIELDS_ADMIN = [
{ key: 'project', label: __('Project'), columnClass: 'gl-w-20p' },
{ key: 'runner', label: __('Runner'), columnClass: 'gl-w-15p' },
{ key: 'pipeline', label: __('Pipeline'), columnClass: 'gl-w-10p' },
- { key: 'stage', label: __('Stage'), columnClass: 'gl-w-10p' },
- { key: 'name', label: __('Name'), columnClass: 'gl-w-15p' },
- { key: 'duration', label: __('Duration'), columnClass: 'gl-w-15p' },
{ key: 'actions', label: '', columnClass: 'gl-w-10p' },
];
diff --git a/app/assets/javascripts/ci/common/private/job_action_component.vue b/app/assets/javascripts/ci/common/private/job_action_component.vue
index f649750ce8a..b0fa724d450 100644
--- a/app/assets/javascripts/ci/common/private/job_action_component.vue
+++ b/app/assets/javascripts/ci/common/private/job_action_component.vue
@@ -120,7 +120,7 @@ export default {
:class="cssClass"
:disabled="isDisabled"
class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center"
- data-testid="ci-action-component"
+ data-testid="ci-action-button"
@click.stop="onClickAction"
>
<div
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
index b435eb283fd..fbdfc7c9c6a 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
@@ -35,9 +35,6 @@ export default {
jobRef() {
return this.job?.refName;
},
- jobRefPath() {
- return this.job?.refPath;
- },
jobTags() {
return this.job.tags;
},
@@ -72,61 +69,60 @@ export default {
<template>
<div>
<div class="gl-text-truncate gl-p-3 gl-mt-n3 gl-mx-n3 gl-mb-n2">
- <gl-link
- v-if="canReadJob"
- class="gl-text-blue-600!"
- :href="jobPath"
- data-testid="job-id-link"
- >
- {{ jobId }}
- </gl-link>
-
- <span v-else data-testid="job-id-limited-access">{{ jobId }}</span>
-
<gl-icon
v-if="jobStuck"
v-gl-tooltip="$options.i18n.stuckText"
name="warning"
:size="$options.iconSize"
+ class="gl-mr-2"
data-testid="stuck-icon"
/>
- <div
- class="gl-display-flex gl-text-gray-700 gl-align-items-center gl-lg-justify-content-start gl-justify-content-end gl-mt-2"
+ <gl-link
+ v-if="canReadJob"
+ class="gl-text-blue-600!"
+ :href="jobPath"
+ data-testid="job-id-link"
>
- <div
- v-if="jobRef"
- class="gl-p-2 gl-rounded-base gl-bg-gray-50 gl-max-w-15 gl-text-truncate"
- >
- <gl-icon
- v-if="createdByTag"
- name="label"
- :size="$options.iconSize"
- data-testid="label-icon"
- />
- <gl-icon v-else name="fork" :size="$options.iconSize" data-testid="fork-icon" />
- <gl-link
- class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
- :href="job.refPath"
- data-testid="job-ref"
- >{{ job.refName }}</gl-link
- >
- </div>
+ <span class="gl-text-truncate">
+ <span data-testid="job-name">{{ jobId }}: {{ job.name }}</span>
+ </span>
+ </gl-link>
- <span v-else>{{ __('none') }}</span>
- <div class="gl-ml-2 gl-p-2 gl-rounded-base gl-bg-gray-50">
- <gl-icon class="gl-mx-2" name="commit" :size="$options.iconSize" />
- <gl-link
- class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
- :href="job.commitPath"
- data-testid="job-sha"
- >{{ job.shortSha }}</gl-link
- >
- </div>
+ <span v-else data-testid="job-id-limited-access">{{ jobId }}: {{ job.name }}</span>
+ </div>
+
+ <div
+ class="gl-display-flex gl-text-gray-700 gl-align-items-center gl-lg-justify-content-start gl-justify-content-end gl-mt-1"
+ >
+ <div v-if="jobRef" class="gl-p-2 gl-rounded-base gl-bg-gray-50 gl-max-w-26 gl-text-truncate">
+ <gl-icon
+ v-if="createdByTag"
+ name="label"
+ :size="$options.iconSize"
+ data-testid="label-icon"
+ />
+ <gl-icon v-else name="fork" :size="$options.iconSize" data-testid="fork-icon" />
+ <gl-link
+ class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
+ :href="job.refPath"
+ data-testid="job-ref"
+ >{{ job.refName }}</gl-link
+ >
+ </div>
+ <span v-else>{{ __('none') }}</span>
+ <div class="gl-ml-2 gl-p-2 gl-rounded-base gl-bg-gray-50">
+ <gl-icon class="gl-mx-2" name="commit" :size="$options.iconSize" />
+ <gl-link
+ class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
+ :href="job.commitPath"
+ data-testid="job-sha"
+ >{{ job.shortSha }}</gl-link
+ >
</div>
</div>
- <div>
+ <div class="gl-mt-2">
<gl-badge
v-for="tag in jobTags"
:key="tag"
@@ -136,7 +132,6 @@ export default {
>
{{ tag }}
</gl-badge>
-
<gl-badge
v-if="triggered"
variant="info"
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
index 18d68ee8a29..945674153c4 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
@@ -1,8 +1,12 @@
<script>
import { GlAvatar, GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
export default {
+ i18n: {
+ stageLabel: s__('Jobs|Stage'),
+ },
components: {
GlAvatar,
GlLink,
@@ -36,21 +40,22 @@ export default {
<template>
<div>
- <div class="gl-p-3 gl-mt-n3">
- <gl-link
- class="gl-text-truncate gl-ml-n3 gl-text-gray-500!"
- :href="pipelinePath"
- data-testid="pipeline-id"
- >
+ <div class="gl-p-3 gl-mt-n3 gl-mx-n3">
+ <gl-link class="gl-text-truncate" :href="pipelinePath" data-testid="pipeline-id">
{{ pipelineId }}
</gl-link>
+
+ <span class="gl-text-secondary">
+ <span>{{ __('created by') }}</span>
+ <gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link">
+ <gl-avatar :src="pipelineUserAvatar" :size="16" />
+ </gl-link>
+ <span v-else>{{ __('API') }}</span>
+ </span>
</div>
- <div class="gl-font-sm gl-text-secondary gl-mt-n2">
- <span>{{ __('created by') }}</span>
- <gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link">
- <gl-avatar :src="pipelineUserAvatar" :size="16" />
- </gl-link>
- <span v-else>{{ __('API') }}</span>
+
+ <div v-if="job.stage" class="gl-text-truncate gl-font-sm gl-text-secondary gl-mt-1">
+ <span data-testid="job-stage-name">{{ $options.i18n.stageLabel }}: {{ job.stage.name }}</span>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
index dbf1dfe7a29..a2b6a430138 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
@@ -1,12 +1,14 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { formatTime } from '~/lib/utils/datetime_utility';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
iconSize: 12,
components: {
+ CiBadgeLink,
GlIcon,
TimeAgoTooltip,
},
@@ -36,17 +38,16 @@ export default {
<template>
<div>
- <div v-if="duration" data-testid="job-duration">
- <gl-icon name="timer" :size="$options.iconSize" data-testid="duration-icon" />
- {{ durationFormatted }}
- </div>
- <div
- v-if="finishedTime"
- :class="{ 'gl-mt-2': hasDurationAndFinishedTime }"
- data-testid="job-finished-time"
- >
- <gl-icon name="calendar" :size="$options.iconSize" data-testid="finished-time-icon" />
- <time-ago-tooltip :time="finishedTime" />
+ <ci-badge-link :status="job.detailedStatus" />
+ <div class="gl-font-sm gl-text-secondary gl-mt-2 gl-ml-3">
+ <div v-if="duration" data-testid="job-duration">
+ <gl-icon name="timer" :size="$options.iconSize" data-testid="duration-icon" />
+ {{ durationFormatted }}
+ </div>
+ <div v-if="finishedTime" data-testid="job-finished-time">
+ <gl-icon name="calendar" :size="$options.iconSize" data-testid="finished-time-icon" />
+ <time-ago-tooltip :time="finishedTime" />
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue b/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
index 23100a3f3db..d81d19cfd52 100644
--- a/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
@@ -1,12 +1,11 @@
<script>
import { GlTable } from '@gitlab/ui';
import { s__ } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import ProjectCell from '~/ci/admin/jobs_table/components/cells/project_cell.vue';
import RunnerCell from '~/ci/admin/jobs_table/components/cells/runner_cell.vue';
-import { DEFAULT_FIELDS } from '../constants';
+import { JOBS_DEFAULT_FIELDS } from '../constants';
import ActionsCell from './job_cells/actions_cell.vue';
-import DurationCell from './job_cells/duration_cell.vue';
+import StatusCell from './job_cells/status_cell.vue';
import JobCell from './job_cells/job_cell.vue';
import PipelineCell from './job_cells/pipeline_cell.vue';
@@ -16,13 +15,12 @@ export default {
},
components: {
ActionsCell,
- CiBadgeLink,
- DurationCell,
- GlTable,
+ StatusCell,
JobCell,
PipelineCell,
ProjectCell,
RunnerCell,
+ GlTable,
},
props: {
jobs: {
@@ -32,7 +30,7 @@ export default {
tableFields: {
type: Array,
required: false,
- default: () => DEFAULT_FIELDS,
+ default: () => JOBS_DEFAULT_FIELDS,
},
admin: {
type: Boolean,
@@ -64,7 +62,7 @@ export default {
</template>
<template #cell(status)="{ item }">
- <ci-badge-link :status="item.detailedStatus" />
+ <status-cell :job="item" />
</template>
<template #cell(job)="{ item }">
@@ -75,28 +73,20 @@ export default {
<pipeline-cell :job="item" />
</template>
- <template v-if="admin" #cell(project)="{ item }">
- <project-cell :job="item" />
- </template>
-
- <template v-if="admin" #cell(runner)="{ item }">
- <runner-cell :job="item" />
- </template>
-
<template #cell(stage)="{ item }">
<div class="gl-text-truncate">
- <span v-if="item.stage" data-testid="job-stage-name">{{ item.stage.name }}</span>
+ <span v-if="item.stage" data-testid="job-stage-name" class="gl-text-secondary">{{
+ item.stage.name
+ }}</span>
</div>
</template>
- <template #cell(name)="{ item }">
- <div class="gl-text-truncate">
- <span data-testid="job-name">{{ item.name }}</span>
- </div>
+ <template v-if="admin" #cell(project)="{ item }">
+ <project-cell :job="item" />
</template>
- <template #cell(duration)="{ item }">
- <duration-cell :job="item" />
+ <template v-if="admin" #cell(runner)="{ item }">
+ <runner-cell :job="item" />
</template>
<template #cell(coverage)="{ item }">
diff --git a/app/assets/javascripts/ci/jobs_page/constants.js b/app/assets/javascripts/ci/jobs_page/constants.js
index 1b572e60c58..dec355ddff6 100644
--- a/app/assets/javascripts/ci/jobs_page/constants.js
+++ b/app/assets/javascripts/ci/jobs_page/constants.js
@@ -29,6 +29,7 @@ export const PLAY_JOB_CONFIRMATION_MESSAGE = s__(
export const RUN_JOB_NOW_HEADER_TITLE = s__('DelayedJobs|Run the delayed job now?');
/* Table constants */
+/* There is another field list based on this one in app/assets/javascripts/ci/admin/jobs_table/constants.js */
export const DEFAULT_FIELDS = [
{
key: 'status',
@@ -38,7 +39,7 @@ export const DEFAULT_FIELDS = [
{
key: 'job',
label: __('Job'),
- columnClass: 'gl-w-20p',
+ columnClass: 'gl-w-quarter',
},
{
key: 'pipeline',
@@ -51,16 +52,6 @@ export const DEFAULT_FIELDS = [
columnClass: 'gl-w-10p',
},
{
- key: 'name',
- label: __('Name'),
- columnClass: 'gl-w-15p',
- },
- {
- key: 'duration',
- label: __('Duration'),
- columnClass: 'gl-w-15p',
- },
- {
key: 'coverage',
label: __('Coverage'),
tdClass: 'gl-display-none! gl-lg-display-table-cell!',
@@ -69,8 +60,10 @@ export const DEFAULT_FIELDS = [
{
key: 'actions',
label: '',
+ tdClass: 'gl-text-right',
columnClass: 'gl-w-10p',
},
];
+export const JOBS_DEFAULT_FIELDS = DEFAULT_FIELDS.filter((field) => field.key !== 'stage');
export const JOBS_TAB_FIELDS = DEFAULT_FIELDS.filter((field) => field.key !== 'pipeline');
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue
index 7538ad87af8..ec8f30e94b4 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue
@@ -65,7 +65,7 @@ export default {
<div
:id="computedJobId"
class="ci-job-dropdown-container dropdown dropright"
- data-qa-selector="job_dropdown_container"
+ data-testid="job-dropdown-container"
>
<button
type="button"
@@ -90,7 +90,7 @@ export default {
<ul
class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown"
- data-qa-selector="jobs_dropdown_menu"
+ data-testid="jobs-dropdown-menu"
>
<li class="scrollable-menu">
<ul>
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
index 4298052d1c0..6e295de5e67 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
@@ -312,7 +312,6 @@ export default {
<div
:id="computedJobId"
class="ci-job-component gl-display-flex gl-justify-content-space-between gl-pipeline-job-width"
- data-qa-selector="job_item_container"
>
<component
:is="nameComponent"
@@ -326,7 +325,6 @@ export default {
:href="detailsPath"
class="js-pipeline-graph-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none gl-w-full"
:data-testid="testId"
- data-qa-selector="job_link"
@click="jobItemClick"
@mouseout="hideTooltips"
>
@@ -356,7 +354,6 @@ export default {
class="gl-mr-1"
:should-trigger-click="shouldTriggerActionClick"
:with-confirmation-modal="withConfirmationModal"
- data-qa-selector="job_action_button"
@actionButtonClicked="handleConfirmationModalPreferences"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
@showActionConfirmationModal="showActionConfirmationModal"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
index d6adaf78da4..9ce1c912c49 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
@@ -233,7 +233,7 @@ export default {
ref="linkedPipeline"
class="gl-h-full gl-display-flex! gl-px-2"
:class="flexDirection"
- data-qa-selector="linked_pipeline_container"
+ data-testid="linked-pipeline-container"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
@@ -247,11 +247,7 @@ export default {
<div
class="gl-display-flex gl-downstream-pipeline-job-width gl-flex-direction-column gl-line-height-normal"
>
- <span
- class="gl-text-truncate"
- data-testid="downstream-title"
- data-qa-selector="downstream_title_content"
- >
+ <span class="gl-text-truncate" data-testid="downstream-title-content">
{{ downstreamTitle }}
</span>
<div class="gl-text-truncate">
@@ -294,7 +290,6 @@ export default {
:icon="expandedIcon"
:aria-label="expandBtnText"
data-testid="expand-pipeline-button"
- data-qa-selector="expand_linked_pipeline_button"
@mouseover="setExpandBtnActiveState(true)"
@mouseout="setExpandBtnActiveState(false)"
@focus="setExpandBtnActiveState(true)"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
index 1401bdba5ca..6030adc96ad 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
@@ -179,6 +179,7 @@ export default {
{ 'gl-opacity-3': isFadedOut(group.name) },
'gl-transition-duration-slow gl-transition-timing-function-ease',
]"
+ data-testid="job-item-container"
@pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
@setSkipRetryModal="$emit('setSkipRetryModal')"
/>
diff --git a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
index ec7be3a3ffc..2244936dd5d 100644
--- a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
+++ b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
@@ -396,11 +396,7 @@ export default {
</div>
</gl-alert>
<gl-loading-icon v-if="loading" class="gl-text-left" size="lg" />
- <div
- v-else
- class="gl-display-flex gl-justify-content-space-between gl-flex-wrap"
- data-qa-selector="pipeline_details_header"
- >
+ <div v-else class="gl-display-flex gl-justify-content-space-between gl-flex-wrap">
<div>
<h3 v-if="name" class="gl-mt-0 gl-mb-3" data-testid="pipeline-name">{{ name }}</h3>
<h3 v-else class="gl-mt-0 gl-mb-3" data-testid="pipeline-commit-title">
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
index 7b38e838033..3595ab631df 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
@@ -7,7 +7,6 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
import {
WORK_ITEMS_TYPE_MAP,
- WORK_ITEM_TYPE_ENUM_TASK,
I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
sprintfWorkItem,
} from '../../constants';
@@ -29,7 +28,7 @@ export default {
childrenType: {
type: String,
required: false,
- default: WORK_ITEM_TYPE_ENUM_TASK,
+ default: '',
},
childrenIds: {
type: Array,
@@ -53,7 +52,7 @@ export default {
return {
fullPath: this.fullPath,
searchTerm: this.search?.title || this.search,
- types: [this.childrenType],
+ types: this.childrenType ? [this.childrenType] : [],
in: this.search ? 'TITLE' : undefined,
};
},
@@ -106,6 +105,7 @@ export default {
},
handleFocus() {
this.searchStarted = true;
+ this.$emit('searching', true);
},
handleMouseOver() {
this.timeout = setTimeout(() => {
@@ -115,11 +115,22 @@ export default {
handleMouseOut() {
clearTimeout(this.timeout);
},
+ handleBlur() {
+ this.$emit('searching', false);
+ },
+ focusInputText() {
+ this.$nextTick(() => {
+ if (this.areWorkItemsToAddValid) {
+ this.$refs.tokenSelector.$el.querySelector('input[type="text"]').focus();
+ }
+ });
+ },
},
};
</script>
<template>
<gl-token-selector
+ ref="tokenSelector"
v-model="workItemsToAdd"
:dropdown-items="availableWorkItems"
:loading="isLoading"
@@ -131,13 +142,14 @@ export default {
@focus="handleFocus"
@mouseover.native="handleMouseOver"
@mouseout.native="handleMouseOut"
+ @token-add="focusInputText"
+ @token-remove="focusInputText"
+ @blur="handleBlur"
>
- <template #token-content="{ token }">
- {{ token.title }}
- </template>
+ <template #token-content="{ token }"> {{ token.iid }} {{ token.title }} </template>
<template #dropdown-item-content="{ dropdownItem }">
<div class="gl-display-flex">
- <div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(dropdownItem.id) }}</div>
+ <div class="gl-text-secondary gl-font-sm gl-mr-4">{{ dropdownItem.iid }}</div>
<div class="gl-text-truncate">{{ dropdownItem.title }}</div>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index edecd7addcc..b0a6a3f39d5 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -605,8 +605,10 @@ export default {
/>
<work-item-relationships
v-if="showWorkItemLinkedItems"
+ :work-item-id="workItem.id"
:work-item-iid="workItemIid"
:work-item-full-path="workItem.project.fullPath"
+ :work-item-type="workItem.workItemType.name"
@showModal="openInModal"
/>
<work-item-notes
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
index 55440e1603c..456dee8dab1 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
@@ -225,7 +225,6 @@ export default {
this.error = null;
},
addChild() {
- this.searchStarted = false;
this.$apollo
.mutate({
mutation: updateWorkItemMutation,
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
new file mode 100644
index 00000000000..757c186e2e9
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
@@ -0,0 +1,245 @@
+<script>
+import { produce } from 'immer';
+import { GlFormGroup, GlForm, GlFormRadioGroup, GlButton, GlAlert } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import WorkItemTokenInput from '../shared/work_item_token_input.vue';
+import addLinkedItemsMutation from '../../graphql/add_linked_items.mutation.graphql';
+import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
+import {
+ LINK_ITEM_FORM_HEADER_LABEL,
+ WIDGET_TYPE_LINKED_ITEMS,
+ LINKED_ITEM_TYPE_VALUE,
+} from '../../constants';
+
+export default {
+ components: {
+ GlForm,
+ GlButton,
+ GlFormGroup,
+ GlFormRadioGroup,
+ GlAlert,
+ WorkItemTokenInput,
+ },
+ props: {
+ workItemId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ workItemIid: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ workItemFullPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ workItemType: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ childrenIds: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ linkedItemType: LINKED_ITEM_TYPE_VALUE.RELATED,
+ linkedItemTypes: [
+ {
+ text: this.$options.i18n.relatedToLabel,
+ value: LINKED_ITEM_TYPE_VALUE.RELATED,
+ },
+ {
+ text: this.$options.i18n.blockingLabel,
+ value: LINKED_ITEM_TYPE_VALUE.BLOCKS,
+ },
+ {
+ text: this.$options.i18n.blockedByLabel,
+ value: LINKED_ITEM_TYPE_VALUE.BLOCKED_BY,
+ },
+ ],
+ workItemsToAdd: [],
+ error: null,
+ showWorkItemsToAddInvalidMessage: false,
+ isSubmitting: false,
+ searchInProgress: false,
+ };
+ },
+ computed: {
+ linkItemFormHeaderLabel() {
+ return LINK_ITEM_FORM_HEADER_LABEL[this.workItemType];
+ },
+ workItemsToAddInvalidMessage() {
+ return this.$options.i18n.addChildErrorMessage;
+ },
+ isSubmitButtonDisabled() {
+ return this.workItemsToAdd.length <= 0 || !this.areWorkItemsToAddValid;
+ },
+ areWorkItemsToAddValid() {
+ return this.workItemsToAdd.length < 4;
+ },
+ errorMessage() {
+ return !this.areWorkItemsToAddValid ? this.$options.i18n.max3ItemsErrorMessage : '';
+ },
+ },
+ methods: {
+ async linkWorkItem() {
+ try {
+ if (this.searchInProgress) {
+ return;
+ }
+ this.isSubmitting = true;
+ const {
+ data: {
+ workItemAddLinkedItems: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: addLinkedItemsMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ linkType: this.linkedItemType,
+ workItemsIds: this.workItemsToAdd.map((wi) => wi.id),
+ },
+ },
+ update: (
+ cache,
+ {
+ data: {
+ workItemAddLinkedItems: { workItem },
+ },
+ },
+ ) => {
+ const queryArgs = {
+ query: workItemByIidQuery,
+ variables: { fullPath: this.workItemFullPath, iid: this.workItemIid },
+ };
+ const sourceData = cache.readQuery(queryArgs);
+
+ if (!sourceData) {
+ return;
+ }
+
+ cache.writeQuery({
+ ...queryArgs,
+ data: produce(sourceData, (draftState) => {
+ const linkedItemsWidget = draftState.workspace.workItems.nodes[0].widgets?.find(
+ (widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS,
+ );
+
+ linkedItemsWidget.linkedItems = workItem.widgets?.find(
+ (widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS,
+ ).linkedItems;
+ }),
+ });
+ },
+ });
+
+ if (errors.length > 0) {
+ [this.error] = errors;
+ return;
+ }
+
+ this.workItemsToAdd = [];
+ this.unsetError();
+ this.showWorkItemsToAddInvalidMessage = false;
+ this.linkedItemType = LINKED_ITEM_TYPE_VALUE.RELATED;
+ this.$emit('submitted');
+ } catch (e) {
+ this.error = this.$options.i18n.addLinkedItemErrorMessage;
+ } finally {
+ this.isSubmitting = false;
+ }
+ },
+ unsetError() {
+ this.error = null;
+ },
+ },
+ i18n: {
+ addButtonLabel: __('Add'),
+ relatedToLabel: s__('WorkItem|relates to'),
+ blockingLabel: s__('WorkItem|blocks'),
+ blockedByLabel: s__('WorkItem|is blocked by'),
+ max3ItemsNoteLabel: s__('WorkItem|Add a maximum of 3 items at a time.'),
+ linkItemInputLabel: s__('WorkItem|the following item(s)'),
+ addLinkedItemErrorMessage: s__(
+ 'WorkItem|Something went wrong when trying to link a item. Please try again.',
+ ),
+ max3ItemsErrorMessage: s__('WorkItem|Only 3 items can be added at a time.'),
+ },
+};
+</script>
+
+<template>
+ <gl-form
+ class="gl-new-card-add-form"
+ data-testid="link-work-item-form"
+ @submit.stop.prevent="linkWorkItem"
+ >
+ <gl-alert v-if="error" variant="danger" class="gl-mb-3" @dismiss="unsetError">
+ {{ error }}
+ </gl-alert>
+ <gl-form-group
+ :label="linkItemFormHeaderLabel"
+ label-for="linked-item-type-radio"
+ label-class="label-bold"
+ class="gl-mb-3"
+ >
+ <gl-form-radio-group
+ id="linked-item-type-radio"
+ v-model="linkedItemType"
+ :options="linkedItemTypes"
+ :checked="linkedItemType"
+ />
+ </gl-form-group>
+ <p class="gl-font-weight-bold gl-mb-2">
+ {{ $options.i18n.linkItemInputLabel }}
+ </p>
+ <div class="gl-mb-5">
+ <work-item-token-input
+ v-model="workItemsToAdd"
+ class="gl-mb-2"
+ :parent-work-item-id="workItemId"
+ :children-ids="childrenIds"
+ :are-work-items-to-add-valid="areWorkItemsToAddValid"
+ :full-path="workItemFullPath"
+ :max-selection-limit="3"
+ @searching="searchInProgress = $event"
+ />
+ <div v-if="errorMessage" class="gl-mb-2 gl-text-red-500">
+ {{ $options.i18n.max3ItemsErrorMessage }}
+ </div>
+ <div v-if="!errorMessage" data-testid="max-work-item-note" class="gl-text-gray-500">
+ {{ $options.i18n.max3ItemsNoteLabel }}
+ </div>
+ <div
+ v-if="showWorkItemsToAddInvalidMessage"
+ class="gl-text-red-500"
+ data-testid="work-items-invalid"
+ >
+ {{ workItemsToAddInvalidMessage }}
+ </div>
+ </div>
+ <gl-button
+ data-testid="link-work-item-button"
+ category="primary"
+ variant="confirm"
+ size="small"
+ type="submit"
+ :disabled="isSubmitButtonDisabled"
+ :loading="isSubmitting"
+ class="gl-mr-2"
+ >
+ {{ $options.i18n.addButtonLabel }}
+ </gl-button>
+ <gl-button category="secondary" size="small" @click="$emit('cancel')">
+ {{ s__('WorkItem|Cancel') }}
+ </gl-button>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
index cbe830f9565..279e6ad01b3 100644
--- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
@@ -33,7 +33,7 @@ export default {
};
</script>
<template>
- <div>
+ <div data-testid="work-item-linked-items-list">
<h4
v-if="heading"
data-testid="work-items-list-heading"
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
index 4f6879e9605..d32e3d3a5e5 100644
--- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
@@ -8,6 +8,7 @@ import { WIDGET_TYPE_LINKED_ITEMS, LINKED_CATEGORIES_MAP } from '../../constants
import WidgetWrapper from '../widget_wrapper.vue';
import WorkItemRelationshipList from './work_item_relationship_list.vue';
+import WorkItemAddRelationshipForm from './work_item_add_relationship_form.vue';
export default {
components: {
@@ -16,8 +17,14 @@ export default {
GlButton,
WidgetWrapper,
WorkItemRelationshipList,
+ WorkItemAddRelationshipForm,
},
props: {
+ workItemId: {
+ type: String,
+ required: false,
+ default: null,
+ },
workItemIid: {
type: String,
required: true,
@@ -26,6 +33,11 @@ export default {
type: String,
required: true,
},
+ workItemType: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
apollo: {
workItem: {
@@ -74,13 +86,13 @@ export default {
linksRelatesTo: [],
linksIsBlockedBy: [],
linksBlocks: [],
+ isShownLinkItemForm: false,
widgetName: 'linkeditems',
};
},
computed: {
- canUpdate() {
- // This will be false untill we implement remove item mutation
- return false;
+ canAdminWorkItemLink() {
+ return this.workItem?.userPermissions?.adminWorkItemLink;
},
isLoading() {
return this.$apollo.queries.workItem.loading;
@@ -91,11 +103,22 @@ export default {
linkedWorkItems() {
return this.linkedWorkItemsWidget?.linkedItems?.nodes || [];
},
+ childrenIds() {
+ return this.linkedWorkItems.map((item) => item.workItem.id);
+ },
linkedWorkItemsCount() {
return this.linkedWorkItems.length;
},
isEmptyRelatedWorkItems() {
- return !this.error && this.linkedWorkItems.length === 0;
+ return !this.isShownLinkItemForm && !this.error && this.linkedWorkItems.length === 0;
+ },
+ },
+ methods: {
+ showLinkItemForm() {
+ this.isShownLinkItemForm = true;
+ },
+ hideLinkItemForm() {
+ this.isShownLinkItemForm = false;
},
},
i18n: {
@@ -131,12 +154,28 @@ export default {
</div>
</template>
<template #header-right>
- <gl-button size="small" class="gl-ml-3">
+ <gl-button
+ v-if="canAdminWorkItemLink"
+ data-testid="link-item-add-button"
+ size="small"
+ class="gl-ml-3"
+ @click="showLinkItemForm"
+ >
<slot name="add-button-text">{{ $options.i18n.addLinkedWorkItemButtonLabel }}</slot>
</gl-button>
</template>
<template #body>
<div class="gl-new-card-content">
+ <work-item-add-relationship-form
+ v-if="isShownLinkItemForm"
+ :work-item-id="workItemId"
+ :work-item-iid="workItemIid"
+ :work-item-full-path="workItemFullPath"
+ :children-ids="childrenIds"
+ :work-item-type="workItemType"
+ @submitted="hideLinkItemForm"
+ @cancel="hideLinkItemForm"
+ />
<gl-loading-icon v-if="isLoading" color="dark" class="gl-my-2" />
<template v-else>
<div v-if="isEmptyRelatedWorkItems" data-testid="links-empty">
@@ -154,7 +193,7 @@ export default {
:linked-items="linksBlocks"
:heading="$options.i18n.blockingTitle"
:work-item-full-path="workItemFullPath"
- :can-update="canUpdate"
+ :can-update="false"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
/>
<work-item-relationship-list
@@ -166,7 +205,7 @@ export default {
:linked-items="linksIsBlockedBy"
:heading="$options.i18n.blockedByTitle"
:work-item-full-path="workItemFullPath"
- :can-update="canUpdate"
+ :can-update="false"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
/>
<work-item-relationship-list
@@ -174,7 +213,7 @@ export default {
:linked-items="linksRelatesTo"
:heading="$options.i18n.relatedToTitle"
:work-item-full-path="workItemFullPath"
- :can-update="canUpdate"
+ :can-update="false"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
/>
</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 2b118247426..04ef2a65aab 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -113,7 +113,7 @@ export const I18N_WORK_ITEM_COPY_CREATE_NOTE_EMAIL = s__(
);
export const sprintfWorkItem = (msg, workItemTypeArg, parentWorkItemType = '') => {
- const workItemType = workItemTypeArg || s__('WorkItem|Work item');
+ const workItemType = workItemTypeArg || s__('WorkItem|item');
return capitalizeFirstCharacter(
sprintf(msg, {
workItemType: workItemType.toLocaleLowerCase(),
@@ -186,8 +186,11 @@ export const WORK_ITEM_NAME_TO_ICON_MAP = {
Issue: 'issue-type-issue',
Task: 'issue-type-task',
Objective: 'issue-type-objective',
+ Incident: 'issue-type-incident',
// eslint-disable-next-line @gitlab/require-i18n-strings
'Key Result': 'issue-type-keyresult',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ 'Test Case': 'issue-type-test-case',
};
export const FORM_TYPES = {
@@ -262,3 +265,15 @@ export const LINKED_CATEGORIES_MAP = {
IS_BLOCKED_BY: 'is_blocked_by',
BLOCKS: 'blocks',
};
+
+export const LINKED_ITEM_TYPE_VALUE = {
+ RELATED: 'RELATED',
+ BLOCKED_BY: 'BLOCKED_BY',
+ BLOCKS: 'BLOCKS',
+};
+
+export const LINK_ITEM_FORM_HEADER_LABEL = {
+ [WORK_ITEM_TYPE_VALUE_OBJECTIVE]: s__('WorkItem|The current objective'),
+ [WORK_ITEM_TYPE_VALUE_KEY_RESULT]: s__('WorkItem|The current key result'),
+ [WORK_ITEM_TYPE_VALUE_TASK]: s__('WorkItem|The current task'),
+};
diff --git a/app/assets/javascripts/work_items/graphql/add_linked_items.mutation.graphql b/app/assets/javascripts/work_items/graphql/add_linked_items.mutation.graphql
new file mode 100644
index 00000000000..ba12c7f9b51
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/add_linked_items.mutation.graphql
@@ -0,0 +1,10 @@
+#import "./work_item.fragment.graphql"
+
+mutation addLinkedItems($input: WorkItemAddLinkedItemsInput!) {
+ workItemAddLinkedItems(input: $input) {
+ workItem {
+ ...WorkItem
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
index 7d63af448d4..2be436aa8c2 100644
--- a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
@@ -9,6 +9,7 @@ query projectWorkItems(
workItems(search: $searchTerm, types: $types, in: $in) {
nodes {
id
+ iid
title
state
confidential
diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
index 1ae5617f04d..fac99310890 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -33,6 +33,7 @@ fragment WorkItem on WorkItem {
adminParentLink
setWorkItemMetadata
createNote
+ adminWorkItemLink
}
widgets {
...WorkItemWidgets