Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
272c39ac05
commit
0a0dcc392c
|
|
@ -3,4 +3,3 @@ Cop/ExperimentsTestCoverage:
|
|||
Exclude:
|
||||
- 'app/controllers/groups/boards_controller.rb'
|
||||
- 'app/controllers/projects/boards_controller.rb'
|
||||
- 'app/experiments/build_ios_app_guide_email_experiment.rb'
|
||||
|
|
|
|||
|
|
@ -266,7 +266,6 @@ Layout/LineEndStringConcatenationIndentation:
|
|||
- 'spec/lib/gitlab/ci/templates/themekit_gitlab_ci_yaml_spec.rb'
|
||||
- 'spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb'
|
||||
- 'spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb'
|
||||
- 'spec/lib/gitlab/email/message/build_ios_app_guide_spec.rb'
|
||||
- 'spec/lib/gitlab/email/message/in_product_marketing/helper_spec.rb'
|
||||
- 'spec/lib/gitlab/github_import/markdown_text_spec.rb'
|
||||
- 'spec/lib/gitlab/github_import/representation/issue_event_spec.rb'
|
||||
|
|
|
|||
|
|
@ -3498,7 +3498,6 @@ RSpec/MissingFeatureCategory:
|
|||
- 'spec/lib/gitlab/email/hook/delivery_metrics_observer_spec.rb'
|
||||
- 'spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb'
|
||||
- 'spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb'
|
||||
- 'spec/lib/gitlab/email/message/build_ios_app_guide_spec.rb'
|
||||
- 'spec/lib/gitlab/email/message/in_product_marketing/admin_verify_spec.rb'
|
||||
- 'spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb'
|
||||
- 'spec/lib/gitlab/email/message/in_product_marketing/create_spec.rb'
|
||||
|
|
|
|||
|
|
@ -137,7 +137,6 @@ RSpec/ReturnFromStub:
|
|||
- 'spec/lib/gitlab/daemon_spec.rb'
|
||||
- 'spec/lib/gitlab/database_importers/instance_administrators/create_group_spec.rb'
|
||||
- 'spec/lib/gitlab/diff/file_spec.rb'
|
||||
- 'spec/lib/gitlab/email/message/build_ios_app_guide_spec.rb'
|
||||
- 'spec/lib/gitlab/email/message/in_product_marketing/helper_spec.rb'
|
||||
- 'spec/lib/gitlab/exclusive_lease_helpers/sleeping_lock_spec.rb'
|
||||
- 'spec/lib/gitlab/exclusive_lease_helpers_spec.rb'
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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-icon
|
||||
v-if="jobStuck"
|
||||
v-gl-tooltip="$options.i18n.stuckText"
|
||||
name="warning"
|
||||
:size="$options.iconSize"
|
||||
class="gl-mr-2"
|
||||
data-testid="stuck-icon"
|
||||
/>
|
||||
|
||||
<gl-link
|
||||
v-if="canReadJob"
|
||||
class="gl-text-blue-600!"
|
||||
:href="jobPath"
|
||||
data-testid="job-id-link"
|
||||
>
|
||||
{{ jobId }}
|
||||
<span class="gl-text-truncate">
|
||||
<span data-testid="job-name">{{ jobId }}: {{ job.name }}</span>
|
||||
</span>
|
||||
</gl-link>
|
||||
|
||||
<span v-else data-testid="job-id-limited-access">{{ jobId }}</span>
|
||||
<span v-else data-testid="job-id-limited-access">{{ jobId }}: {{ job.name }}</span>
|
||||
</div>
|
||||
|
||||
<gl-icon
|
||||
v-if="jobStuck"
|
||||
v-gl-tooltip="$options.i18n.stuckText"
|
||||
name="warning"
|
||||
:size="$options.iconSize"
|
||||
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"
|
||||
>
|
||||
<div
|
||||
v-if="jobRef"
|
||||
class="gl-p-2 gl-rounded-base gl-bg-gray-50 gl-max-w-15 gl-text-truncate"
|
||||
<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
|
||||
>
|
||||
<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>
|
||||
<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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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,6 +73,14 @@ export default {
|
|||
<pipeline-cell :job="item" />
|
||||
</template>
|
||||
|
||||
<template #cell(stage)="{ item }">
|
||||
<div class="gl-text-truncate">
|
||||
<span v-if="item.stage" data-testid="job-stage-name" class="gl-text-secondary">{{
|
||||
item.stage.name
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="admin" #cell(project)="{ item }">
|
||||
<project-cell :job="item" />
|
||||
</template>
|
||||
|
|
@ -83,22 +89,6 @@ export default {
|
|||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell(name)="{ item }">
|
||||
<div class="gl-text-truncate">
|
||||
<span data-testid="job-name">{{ item.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell(duration)="{ item }">
|
||||
<duration-cell :job="item" />
|
||||
</template>
|
||||
|
||||
<template #cell(coverage)="{ item }">
|
||||
<span v-if="item.coverage" data-testid="job-coverage">{{
|
||||
formatCoverage(item.coverage)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
@ -50,16 +51,6 @@ export const DEFAULT_FIELDS = [
|
|||
label: __('Stage'),
|
||||
columnClass: 'gl-w-10p',
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
label: __('Name'),
|
||||
columnClass: 'gl-w-15p',
|
||||
},
|
||||
{
|
||||
key: 'duration',
|
||||
label: __('Duration'),
|
||||
columnClass: 'gl-w-15p',
|
||||
},
|
||||
{
|
||||
key: 'coverage',
|
||||
label: __('Coverage'),
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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')"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -225,7 +225,6 @@ export default {
|
|||
this.error = null;
|
||||
},
|
||||
addChild() {
|
||||
this.searchStarted = false;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: updateWorkItemMutation,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
#import "./work_item.fragment.graphql"
|
||||
|
||||
mutation addLinkedItems($input: WorkItemAddLinkedItemsInput!) {
|
||||
workItemAddLinkedItems(input: $input) {
|
||||
workItem {
|
||||
...WorkItem
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ query projectWorkItems(
|
|||
workItems(search: $searchTerm, types: $types, in: $in) {
|
||||
nodes {
|
||||
id
|
||||
iid
|
||||
title
|
||||
state
|
||||
confidential
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ fragment WorkItem on WorkItem {
|
|||
adminParentLink
|
||||
setWorkItemMetadata
|
||||
createNote
|
||||
adminWorkItemLink
|
||||
}
|
||||
widgets {
|
||||
...WorkItemWidgets
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BuildIosAppGuideEmailExperiment < ApplicationExperiment
|
||||
control { false }
|
||||
candidate { true }
|
||||
end
|
||||
|
|
@ -12,12 +12,6 @@ module Emails
|
|||
'X-Mailgun-Tag' => 'marketing'
|
||||
}.freeze
|
||||
|
||||
def build_ios_app_guide_email(recipient_email)
|
||||
@message = ::Gitlab::Email::Message::BuildIosAppGuide.new
|
||||
|
||||
mail_to(to: recipient_email, subject: @message.subject_line)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mail_to(to:, subject:)
|
||||
|
|
|
|||
|
|
@ -158,6 +158,10 @@ class AbuseReport < ApplicationRecord
|
|||
Project.find_by_full_path(route_hash.values_at(:namespace_id, :project_id).join('/'))
|
||||
end
|
||||
|
||||
def group
|
||||
Group.find_by_full_path(route_hash[:group_id])
|
||||
end
|
||||
|
||||
def route_hash
|
||||
match = Rails.application.routes.recognize_path(reported_from_url)
|
||||
return {} if match[:unmatched_route].present?
|
||||
|
|
|
|||
|
|
@ -3,30 +3,21 @@
|
|||
module Users
|
||||
class InProductMarketingEmail < ApplicationRecord
|
||||
include BulkInsertSafe
|
||||
include IgnorableColumns
|
||||
|
||||
BUILD_IOS_APP_GUIDE = 'build_ios_app_guide'
|
||||
CAMPAIGNS = [BUILD_IOS_APP_GUIDE].freeze
|
||||
ignore_column :campaign, remove_with: '16.7', remove_after: '2023-11-15'
|
||||
|
||||
belongs_to :user
|
||||
|
||||
validates :user, presence: true
|
||||
|
||||
validates :track, :series, presence: true, if: -> { campaign.blank? }
|
||||
validates :campaign, presence: true, if: -> { track.blank? && series.blank? }
|
||||
validates :campaign, inclusion: { in: CAMPAIGNS }, allow_nil: true
|
||||
validates :track, presence: true
|
||||
validates :series, presence: true
|
||||
|
||||
validates :user_id, uniqueness: {
|
||||
scope: [:track, :series],
|
||||
message: 'track series email has already been sent'
|
||||
}, if: -> { track.present? }
|
||||
|
||||
validates :user_id, uniqueness: {
|
||||
scope: :campaign,
|
||||
message: 'campaign email has already been sent'
|
||||
}, if: -> { campaign.present? }
|
||||
|
||||
validate :campaign_or_track_series
|
||||
|
||||
enum track: {
|
||||
create: 0,
|
||||
verify: 1,
|
||||
|
|
@ -53,11 +44,6 @@ module Users
|
|||
users_without_records(join_condition)
|
||||
end
|
||||
|
||||
scope :without_campaign, ->(campaign) do
|
||||
join_condition = for_user.and(for_campaign(campaign))
|
||||
users_without_records(join_condition)
|
||||
end
|
||||
|
||||
def self.users_table
|
||||
User.arel_table
|
||||
end
|
||||
|
|
@ -78,10 +64,6 @@ module Users
|
|||
arel_table[:user_id].eq(users_table[:id])
|
||||
end
|
||||
|
||||
def self.for_campaign(campaign)
|
||||
arel_table[:campaign].eq(campaign)
|
||||
end
|
||||
|
||||
def self.for_track_and_series(track, series)
|
||||
arel_table[:track].eq(ACTIVE_TRACKS[track])
|
||||
.and(arel_table[:series]).eq(series)
|
||||
|
|
@ -92,13 +74,5 @@ module Users
|
|||
|
||||
email.update(cta_clicked_at: Time.zone.now) if email && email.cta_clicked_at.blank?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def campaign_or_track_series
|
||||
if campaign.present? && (track.present? || series.present?)
|
||||
errors.add(:campaign, 'should be a campaign or a track and series but not both')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Projects
|
||||
class InProductMarketingCampaignEmailsService
|
||||
include Gitlab::Experiment::Dsl
|
||||
|
||||
def initialize(project, campaign)
|
||||
@project = project
|
||||
@campaign = campaign
|
||||
@sent_email_records = ::Users::InProductMarketingEmailRecords.new
|
||||
end
|
||||
|
||||
def execute
|
||||
send_emails
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :project, :campaign, :sent_email_records
|
||||
|
||||
def send_emails
|
||||
project_users.each do |user|
|
||||
send_email(user)
|
||||
end
|
||||
|
||||
sent_email_records.save!
|
||||
end
|
||||
|
||||
def project_users
|
||||
@project_users ||= project.users.merge(Users::InProductMarketingEmail.without_campaign(campaign))
|
||||
end
|
||||
|
||||
def project_users_max_access_levels
|
||||
ids = project_users.map(&:id)
|
||||
@project_users_max_access_levels ||= project.team.max_member_access_for_user_ids(ids)
|
||||
end
|
||||
|
||||
def send_email(user)
|
||||
return unless user.can?(:receive_notifications)
|
||||
return unless target_user?(user)
|
||||
|
||||
Notify.build_ios_app_guide_email(user.notification_email_or_default).deliver_later
|
||||
|
||||
sent_email_records.add(user, campaign: campaign)
|
||||
experiment(:build_ios_app_guide_email, project: project).track(:email_sent)
|
||||
end
|
||||
|
||||
def target_user?(user)
|
||||
max_access_level = project_users_max_access_levels[user.id]
|
||||
max_access_level >= Gitlab::Access::DEVELOPER
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -28,26 +28,11 @@ module Projects
|
|||
|
||||
project_setting.target_platforms = target_platforms
|
||||
project_setting.save
|
||||
|
||||
send_build_ios_app_guide_email
|
||||
|
||||
project_setting.target_platforms
|
||||
end
|
||||
|
||||
def project_setting
|
||||
@project_setting ||= ::ProjectSetting.find_or_initialize_by(project: project) # rubocop:disable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
def experiment_candidate?
|
||||
experiment(:build_ios_app_guide_email, project: project).run
|
||||
end
|
||||
|
||||
def send_build_ios_app_guide_email
|
||||
return unless target_platforms.include? :ios
|
||||
return unless experiment_candidate?
|
||||
|
||||
campaign = Users::InProductMarketingEmail::BUILD_IOS_APP_GUIDE
|
||||
Projects::InProductMarketingCampaignEmailsService.new(project, campaign).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,10 +13,9 @@ module Users
|
|||
@records = []
|
||||
end
|
||||
|
||||
def add(user, campaign: nil, track: nil, series: nil)
|
||||
def add(user, track: nil, series: nil)
|
||||
@records << Users::InProductMarketingEmail.new(
|
||||
user: user,
|
||||
campaign: campaign,
|
||||
track: track,
|
||||
series: series,
|
||||
created_at: Time.zone.now,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ module WorkItems
|
|||
|
||||
override :issuables_already_assigned_message
|
||||
def issuables_already_assigned_message
|
||||
_('Work items are already linked')
|
||||
_('Items are already linked')
|
||||
end
|
||||
|
||||
override :issuables_not_found_message
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
- unless @skip_current_level_breadcrumb
|
||||
- push_to_schema_breadcrumb(@breadcrumb_title, breadcrumb_title_link)
|
||||
|
||||
%nav.breadcrumbs{ 'aria-label': _('Breadcrumbs'), data: { testid: 'breadcrumb-links', qa_selector: 'breadcrumb_links_content' } }
|
||||
%nav.breadcrumbs{ 'aria-label': _('Breadcrumbs'), data: { testid: 'breadcrumb-links' } }
|
||||
%ul.list-unstyled.breadcrumbs-list.js-breadcrumbs-list
|
||||
- unless hide_top_links
|
||||
= header_title
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
= breadcrumb_list_item link_to(extra[:text], extra[:link])
|
||||
= render "layouts/nav/breadcrumbs/collapsed_inline_list", location: :after
|
||||
- unless @skip_current_level_breadcrumb
|
||||
%li{ data: { testid: 'breadcrumb-current-link', qa_selector: 'breadcrumb_current_link' } }
|
||||
%li{ data: { testid: 'breadcrumb-current-link' } }
|
||||
= link_to @breadcrumb_title, breadcrumb_title_link
|
||||
-# haml-lint:disable InlineJavaScript
|
||||
%script{ type: 'application/ld+json' }
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
%tr
|
||||
%td{ bgcolor: "#ffffff", height: "auto", style: "max-width: 600px; width: 100%; text-align: center; height: 200px; padding: 25px 15px; mso-line-height-rule: exactly; min-height: 40px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;", valign: "middle", width: "100%" }
|
||||
= inline_image_link(@message.logo_path, { width: '150', style: 'width: 150px;' })
|
||||
%h1{ style: "font-size: 40px; line-height: 46x; color: #000000; padding: 20px 0 0 0; font-weight: normal;" }
|
||||
= @message.title
|
||||
%tr
|
||||
%td{ style: "padding: 10px 20px 30px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; color:#000000; font-size: 18px; line-height: 24px;" }
|
||||
%p{ style: "margin: 0 0 20px 0;" }
|
||||
= @message.body_line1.html_safe
|
||||
%tr
|
||||
%td{ align: "center", style: "padding: 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" }
|
||||
.cta_link.cta_link_primary= @message.cta_link
|
||||
.cta_link.cta_link_secondary= @message.cta2_link
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<%= @message.title %>
|
||||
|
||||
<%= @message.body_line1 %>
|
||||
|
||||
<%= @message.cta_link %>
|
||||
|
||||
<%= @message.cta2_link %>
|
||||
|
||||
<%= @message.footer_links %>
|
||||
|
||||
<%= @message.address %>
|
||||
|
||||
<%= @message.unsubscribe %>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: build_ios_app_guide_email
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83817
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/357803
|
||||
milestone: '15.0'
|
||||
type: experiment
|
||||
group: group::activation
|
||||
default_enabled: false
|
||||
|
|
@ -63,24 +63,24 @@ To run a DAST authenticated scan:
|
|||
|
||||
### Available CI/CD variables
|
||||
|
||||
| CI/CD variable | Type | Description |
|
||||
|:-----------------------------------------------|:------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `DAST_AUTH_COOKIES` | string | Set to a comma-separated list of cookie names to specify which cookies are used for authentication. |
|
||||
| `DAST_AUTH_REPORT` | boolean | Set to `true` to generate a report detailing steps taken during the authentication process. You must also define `gl-dast-debug-auth-report.html` as a CI job artifact to be able to access the generated report. The report's content aids when debugging authentication failures. |
|
||||
| `DAST_AUTH_TYPE` <sup>2</sup> | string | The authentication type to use. Example: `basic-digest`. |
|
||||
| `DAST_AUTH_URL` <sup>1</sup> | URL | The URL of the page containing the sign-in HTML form on the target website. `DAST_USERNAME` and `DAST_PASSWORD` are submitted with the login form to create an authenticated scan. Example: `https://login.example.com`. |
|
||||
| `DAST_AUTH_VERIFICATION_LOGIN_FORM` | boolean | Verifies successful authentication by checking for the absence of a login form after the login form has been submitted. |
|
||||
| `DAST_AUTH_VERIFICATION_SELECTOR` | [selector](#finding-an-elements-selector) | Verifies successful authentication by checking for presence of a selector after the login form has been submitted. Example: `css:.user-photo`. |
|
||||
| `DAST_AUTH_VERIFICATION_URL` <sup>1</sup> | URL | Verifies successful authentication by checking the URL in the browser after the login form has been submitted. Example: `"https://example.com/loggedin_page"`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207335) in GitLab 13.8. |
|
||||
| `DAST_BROWSER_PATH_TO_LOGIN_FORM` <sup>1</sup> | [selector](#finding-an-elements-selector) | Comma-separated list of selectors that are selected prior to attempting to enter `DAST_USERNAME` and `DAST_PASSWORD` into the login form. Example: `"css:.navigation-menu,css:.login-menu-item"`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/326633) in GitLab 14.1. |
|
||||
| `DAST_EXCLUDE_URLS` <sup>1</sup> | URLs | The URLs to skip during the authenticated scan; comma-separated. Regular expression syntax can be used to match multiple URLs. For example, `.*` matches an arbitrary character sequence. |
|
||||
| `DAST_FIRST_SUBMIT_FIELD` | string | The `id` or `name` of the element that when selected submits the username form of a multi-page login process. For example, `css:button[type='user-submit']`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9894) in GitLab 12.4. |
|
||||
| `DAST_PASSWORD` <sup>1</sup> | string | The password to authenticate to in the website. Example: `P@55w0rd!` |
|
||||
| `DAST_PASSWORD_FIELD` | string | The selector of password field at the sign-in HTML form. Example: `id:password` |
|
||||
| `DAST_SUBMIT_FIELD` | string | The `id` or `name` of the element that when selected submits the login form or the password form of a multi-page login process. For example, `css:button[type='submit']`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9894) in GitLab 12.4. |
|
||||
| `DAST_USERNAME` <sup>1</sup> | string | The username to authenticate to in the website. Example: `admin` |
|
||||
| `DAST_USERNAME_FIELD` <sup>1</sup> | string | The selector of username field at the sign-in HTML form. Example: `name:username` |
|
||||
| `DAST_AUTH_DISABLE_CLEAR_FIELDS` | boolean | Disables clearing of username and password fields before attempting manual login. Set to `false` by default. |
|
||||
| CI/CD variable | Type | Description |
|
||||
|:-----------------------------------------------|:------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `DAST_AUTH_COOKIES` | string | Set to a comma-separated list of cookie names to specify which cookies are used for authentication. |
|
||||
| `DAST_AUTH_REPORT` | boolean | Set to `true` to generate a report detailing steps taken during the authentication process. You must also define `gl-dast-debug-auth-report.html` as a CI job artifact to be able to access the generated report. The report's content aids when debugging authentication failures. |
|
||||
| `DAST_AUTH_TYPE` <sup>2</sup> | string | The authentication type to use. Example: `basic-digest`. |
|
||||
| `DAST_AUTH_URL` <sup>1</sup> | URL | The URL of the page containing the login form on the target website. `DAST_USERNAME` and `DAST_PASSWORD` are submitted with the login form to create an authenticated scan. Example: `https://login.example.com`. |
|
||||
| `DAST_AUTH_VERIFICATION_LOGIN_FORM` | boolean | Verifies successful authentication by checking for the absence of a login form after the login form has been submitted. |
|
||||
| `DAST_AUTH_VERIFICATION_SELECTOR` | [selector](#finding-an-elements-selector) | A selector describing an element whose presence is used to determine if authentication has succeeded after the login form is submitted. Example: `css:.user-photo`. |
|
||||
| `DAST_AUTH_VERIFICATION_URL` <sup>1</sup> | URL | A URL that is compared to the URL in the browser to determine if authentication has succeeded after the login form is submitted. Example: `"https://example.com/loggedin_page"`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207335) in GitLab 13.8. |
|
||||
| `DAST_BROWSER_PATH_TO_LOGIN_FORM` <sup>1</sup> | [selector](#finding-an-elements-selector) | A comma-separated list of selectors representing elements to click on prior to entering the `DAST_USERNAME` and `DAST_PASSWORD` into the login form. Example: `"css:.navigation-menu,css:.login-menu-item"`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/326633) in GitLab 14.1. |
|
||||
| `DAST_EXCLUDE_URLS` <sup>1</sup> | URLs | The URLs to skip during the authenticated scan; comma-separated. Regular expression syntax can be used to match multiple URLs. For example, `.*` matches an arbitrary character sequence. |
|
||||
| `DAST_FIRST_SUBMIT_FIELD` | [selector](#finding-an-elements-selector) | A selector describing the element that is clicked on to submit the username form of a multi-page login process. For example, `css:button[type='user-submit']`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9894) in GitLab 12.4. |
|
||||
| `DAST_PASSWORD` <sup>1</sup> | string | The password to authenticate to in the website. Example: `P@55w0rd!` |
|
||||
| `DAST_PASSWORD_FIELD` | [selector](#finding-an-elements-selector) | A selector describing the element used to enter the password on the login form. Example: `id:password` |
|
||||
| `DAST_SUBMIT_FIELD` | [selector](#finding-an-elements-selector) | A selector describing the element clicked on to submit the login form for a single-page login form, or the password form for a multi-page login form. For example, `css:button[type='submit']`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9894) in GitLab 12.4. |
|
||||
| `DAST_USERNAME` <sup>1</sup> | string | The username to authenticate to in the website. Example: `admin` |
|
||||
| `DAST_USERNAME_FIELD` <sup>1</sup> | [selector](#finding-an-elements-selector) | A selector describing the element used to enter the username on the login form. Example: `name:username` |
|
||||
| `DAST_AUTH_DISABLE_CLEAR_FIELDS` | boolean | Disables clearing of username and password fields before attempting manual login. Set to `false` by default. |
|
||||
|
||||
1. Available to an on-demand proxy-based DAST scan.
|
||||
1. Not available to proxy-based scans.
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Email
|
||||
module Message
|
||||
class BuildIosAppGuide
|
||||
include Gitlab::Email::Message::InProductMarketing::Helper
|
||||
include Gitlab::Routing
|
||||
|
||||
attr_accessor :format
|
||||
|
||||
def initialize(format: :html)
|
||||
@format = format
|
||||
end
|
||||
|
||||
def subject_line
|
||||
s_('InProductMarketing|Get set up to build for iOS')
|
||||
end
|
||||
|
||||
def title
|
||||
s_("InProductMarketing|Building for iOS? We've got you covered.")
|
||||
end
|
||||
|
||||
def body_line1
|
||||
s_(
|
||||
'InProductMarketing|Want to get your iOS app up and running, including publishing all the way to ' \
|
||||
'TestFlight? Follow our guide to set up GitLab and fastlane to publish iOS apps to the App Store.'
|
||||
)
|
||||
end
|
||||
|
||||
def cta_text
|
||||
s_('InProductMarketing|Learn how to build for iOS')
|
||||
end
|
||||
|
||||
def cta_link
|
||||
action_link(cta_text, 'https://about.gitlab.com/blog/2019/03/06/ios-publishing-with-gitlab-and-fastlane/')
|
||||
end
|
||||
|
||||
def cta2_text
|
||||
s_('InProductMarketing|Watch iOS building in action.')
|
||||
end
|
||||
|
||||
def cta2_link
|
||||
action_link(cta2_text, 'https://www.youtube.com/watch?v=325FyJt7ZG8')
|
||||
end
|
||||
|
||||
def logo_path
|
||||
'mailers/in_product_marketing/create-0.png'
|
||||
end
|
||||
|
||||
def unsubscribe
|
||||
unsubscribe_message
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2333,6 +2333,9 @@ msgstr ""
|
|||
msgid "AbuseReport|Go to content"
|
||||
msgstr ""
|
||||
|
||||
msgid "AbuseReport|Go to epic"
|
||||
msgstr ""
|
||||
|
||||
msgid "AbuseReport|Go to issue"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -2399,6 +2402,9 @@ msgstr ""
|
|||
msgid "AbuseReport|Reported content"
|
||||
msgstr ""
|
||||
|
||||
msgid "AbuseReport|Reported epic"
|
||||
msgstr ""
|
||||
|
||||
msgid "AbuseReport|Reported issue"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -24541,9 +24547,6 @@ msgstr ""
|
|||
msgid "InProductMarketing|Blog"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Building for iOS? We've got you covered."
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Built-in security"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -24556,9 +24559,6 @@ msgstr ""
|
|||
msgid "InProductMarketing|Free guest users"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Get set up to build for iOS"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|If you don't want to receive marketing emails directly from GitLab, %{marketing_preference_link}."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -24568,9 +24568,6 @@ msgstr ""
|
|||
msgid "InProductMarketing|Invite unlimited colleagues"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Learn how to build for iOS"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|No credit card required"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -24589,15 +24586,9 @@ msgstr ""
|
|||
msgid "InProductMarketing|Twitter"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Want to get your iOS app up and running, including publishing all the way to TestFlight? Follow our guide to set up GitLab and fastlane to publish iOS apps to the App Store."
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Want to host GitLab on your servers?"
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|Watch iOS building in action."
|
||||
msgstr ""
|
||||
|
||||
msgid "InProductMarketing|YouTube"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26186,6 +26177,9 @@ msgstr ""
|
|||
msgid "Item with ID: %{id} cannot be added. You don't have permission to perform this action."
|
||||
msgstr ""
|
||||
|
||||
msgid "Items are already linked"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iteration"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -26969,6 +26963,9 @@ msgstr ""
|
|||
msgid "Jobs|Root cause analysis"
|
||||
msgstr ""
|
||||
|
||||
msgid "Jobs|Stage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Jobs|There was a problem fetching the failed jobs."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53543,9 +53540,6 @@ msgstr ""
|
|||
msgid "Work item promoted successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Work items are already linked"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|%{count} more assignees"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53570,6 +53564,9 @@ msgstr ""
|
|||
msgid "WorkItem|Add %{workItemType}s"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add a maximum of 3 items at a time."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Add a title"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53728,6 +53725,9 @@ msgstr ""
|
|||
msgid "WorkItem|Objective"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Only 3 items can be added at a time."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Only project members with at least the Reporter role, the author, and assignees can view or be notified about this %{workItemType}."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53788,6 +53788,9 @@ msgstr ""
|
|||
msgid "WorkItem|Something went wrong when trying to create a child. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong when trying to link a item. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong while copying the %{workItemType} email address. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53836,6 +53839,15 @@ msgstr ""
|
|||
msgid "WorkItem|Test case"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|The current key result"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|The current objective"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|The current task"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|This %{workItemType} is confidential and should only be visible to team members with at least Reporter access"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53866,6 +53878,21 @@ msgstr ""
|
|||
msgid "WorkItem|You're only seeing %{boldStart}other activity%{boldEnd} in the feed. To add a comment, switch to one of the following options."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|blocks"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|is blocked by"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|item"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|relates to"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|the following item(s)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Workspaces"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@
|
|||
"@gitlab/svgs": "3.63.0",
|
||||
"@gitlab/ui": "66.4.0",
|
||||
"@gitlab/visual-review-tools": "1.7.3",
|
||||
"@gitlab/web-ide": "0.0.1-dev-20230915130935",
|
||||
"@gitlab/web-ide": "0.0.1-dev-20230921201626",
|
||||
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
|
||||
"@popperjs/core": "^2.11.2",
|
||||
"@rails/actioncable": "7.0.8",
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ module QA
|
|||
super
|
||||
|
||||
base.view 'app/views/layouts/nav/breadcrumbs/_breadcrumbs.html.haml' do
|
||||
element :breadcrumb_links_content
|
||||
element 'breadcrumb-links'
|
||||
end
|
||||
end
|
||||
|
||||
def has_breadcrumb?(text)
|
||||
has_element?(:breadcrumb_links_content, text: text)
|
||||
has_element?('breadcrumb-links', text: text)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ module QA
|
|||
end
|
||||
|
||||
base.view 'app/views/layouts/nav/breadcrumbs/_breadcrumbs.html.haml' do
|
||||
element :breadcrumb_links_content
|
||||
element :breadcrumb_current_link
|
||||
element 'breadcrumb-links'
|
||||
element 'breadcrumb-current-link'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -273,8 +273,8 @@ module QA
|
|||
end
|
||||
|
||||
def snippet_id
|
||||
within_element(:breadcrumb_links_content) do
|
||||
find_element(:breadcrumb_current_link).text.delete_prefix('$')
|
||||
within_element('breadcrumb-links') do
|
||||
find_element('breadcrumb-current-link').text.delete_prefix('$')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,81 +8,64 @@ module QA
|
|||
include Component::CiBadgeLink
|
||||
|
||||
view 'app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue' do
|
||||
element :pipeline_details_header, required: true
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue' do
|
||||
element :pipeline_graph, /class.*pipeline-graph.*/ # rubocop:disable QA/ElementWithPattern
|
||||
element 'pipeline-details-header', required: true
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue' do
|
||||
element :job_item_container, required: true
|
||||
element :job_link, required: true
|
||||
element :job_action_button
|
||||
element 'job-with-link', required: true
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/ci/common/private/job_action_component.vue' do
|
||||
element 'ci-action-button'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue' do
|
||||
element :expand_linked_pipeline_button
|
||||
element :linked_pipeline_container
|
||||
element :downstream_title_content
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/ci/reports/components/report_section.vue' do
|
||||
element :expand_report_button
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/vue_shared/components/ci_icon.vue' do
|
||||
element :status_icon, 'ci-status-icon-${status}' # rubocop:disable QA/ElementWithPattern
|
||||
element 'expand-pipeline-button'
|
||||
element 'linked-pipeline-container'
|
||||
element 'downstream-title-content'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue' do
|
||||
element :job_dropdown_container
|
||||
element :jobs_dropdown_menu
|
||||
element 'job-dropdown-container'
|
||||
element 'jobs-dropdown-menu'
|
||||
end
|
||||
|
||||
view 'app/views/layouts/nav/breadcrumbs/_breadcrumbs.html.haml' do
|
||||
element :breadcrumb_links_content
|
||||
element :breadcrumb_current_link
|
||||
end
|
||||
|
||||
def pipeline_id
|
||||
within_element(:breadcrumb_links_content) do
|
||||
find_element(:breadcrumb_current_link).text.delete_prefix('#')
|
||||
end
|
||||
view 'app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue' do
|
||||
element 'job-item-container', required: true
|
||||
end
|
||||
|
||||
def running?(wait: 0)
|
||||
within_element(:pipeline_details_header) do
|
||||
within_element('pipeline-details-header') do
|
||||
page.has_content?('running', wait: wait)
|
||||
end
|
||||
end
|
||||
|
||||
def has_build?(name, status: :success, wait: nil)
|
||||
if status
|
||||
within_element(:job_item_container, text: name) do
|
||||
within_element('job-item-container', text: name) do
|
||||
has_selector?(".ci-status-icon-#{status}", **{ wait: wait }.compact)
|
||||
end
|
||||
else
|
||||
has_element?(:job_item_container, text: name)
|
||||
has_element?('job-item-container', text: name)
|
||||
end
|
||||
end
|
||||
|
||||
def has_job?(job_name)
|
||||
has_element?(:job_link, text: job_name)
|
||||
has_element?('job-with-link', text: job_name)
|
||||
end
|
||||
|
||||
def has_no_job?(job_name)
|
||||
has_no_element?(:job_link, text: job_name)
|
||||
has_no_element?('job-with-link', text: job_name)
|
||||
end
|
||||
|
||||
def linked_pipelines
|
||||
all_elements(:linked_pipeline_container, minimum: 1)
|
||||
all_elements('linked-pipeline-container', minimum: 1)
|
||||
end
|
||||
|
||||
def find_linked_pipeline_by_title(title)
|
||||
linked_pipelines.find do |pipeline|
|
||||
within(pipeline) do
|
||||
find_element(:downstream_title_content).text.include?(title)
|
||||
find_element('downstream-title-content').text.include?(title)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -91,14 +74,14 @@ module QA
|
|||
# If the pipeline page has loaded linked pipelines should appear, but it can take a little while,
|
||||
# especially on busier environments.
|
||||
retry_until(reload: true, message: 'Waiting for linked pipeline to appear') do
|
||||
title ? find_linked_pipeline_by_title(title) : has_element?(:linked_pipeline_container)
|
||||
title ? find_linked_pipeline_by_title(title) : has_element?('linked-pipeline-container')
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :has_child_pipeline?, :has_linked_pipeline?
|
||||
|
||||
def has_no_linked_pipeline?
|
||||
has_no_element?(:linked_pipeline_container)
|
||||
has_no_element?('linked-pipeline-container')
|
||||
end
|
||||
|
||||
alias_method :has_no_child_pipeline?, :has_no_linked_pipeline?
|
||||
|
|
@ -106,19 +89,13 @@ module QA
|
|||
def expand_linked_pipeline(title: nil)
|
||||
linked_pipeline = title ? find_linked_pipeline_by_title(title) : linked_pipelines.first
|
||||
|
||||
within_element_by_index(:linked_pipeline_container, linked_pipelines.index(linked_pipeline)) do
|
||||
click_element(:expand_linked_pipeline_button)
|
||||
within_element_by_index('linked-pipeline-container', linked_pipelines.index(linked_pipeline)) do
|
||||
click_element('expand-pipeline-button')
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :expand_child_pipeline, :expand_linked_pipeline
|
||||
|
||||
def expand_license_report
|
||||
within_element(:license_report_widget) do
|
||||
click_element(:expand_report_button)
|
||||
end
|
||||
end
|
||||
|
||||
def click_on_first_job
|
||||
first('.js-pipeline-graph-job-link', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).click
|
||||
end
|
||||
|
|
@ -126,33 +103,33 @@ module QA
|
|||
def click_job(job_name)
|
||||
# Retry due to transient bug https://gitlab.com/gitlab-org/gitlab/-/issues/347126
|
||||
QA::Support::Retrier.retry_on_exception do
|
||||
click_element(:job_link, Project::Job::Show, text: job_name)
|
||||
click_element('job-with-link', Project::Job::Show, text: job_name)
|
||||
end
|
||||
end
|
||||
|
||||
def click_job_action(job_name)
|
||||
wait_for_requests
|
||||
|
||||
within_element(:job_item_container, text: job_name) do
|
||||
click_element(:job_action_button)
|
||||
within_element('job-item-container', text: job_name) do
|
||||
click_element('ci-action-button')
|
||||
end
|
||||
end
|
||||
|
||||
def click_job_dropdown(job_dropdown_name)
|
||||
click_element(:job_dropdown_container, text: job_dropdown_name)
|
||||
click_element('job-dropdown-container', text: job_dropdown_name)
|
||||
end
|
||||
|
||||
def has_skipped_job_in_group?
|
||||
within_element(:jobs_dropdown_menu) do
|
||||
all_elements(:job_item_container, minimum: 1).all? do
|
||||
within_element('jobs-dropdown-menu') do
|
||||
all_elements('job-with-link', minimum: 1).all? do
|
||||
has_selector?('.ci-status-icon-skipped')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def has_no_skipped_job_in_group?
|
||||
within_element(:jobs_dropdown_menu) do
|
||||
all_elements(:job_item_container, minimum: 1).all? do
|
||||
within_element('jobs-dropdown-menu') do
|
||||
all_elements('job-with-link', minimum: 1).all? do
|
||||
has_no_selector?('.ci-status-icon-skipped')
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ module QA
|
|||
Flow::Login.sign_in
|
||||
end
|
||||
|
||||
shared_examples 'milestone assigned to existing issue' do |testcase|
|
||||
it 'is assigned to an existing issue', testcase: testcase do
|
||||
shared_examples 'when assigned to existing issue' do |testcase|
|
||||
it 'is assigned', testcase: testcase do
|
||||
issue.visit!
|
||||
|
||||
Page::Project::Issue::Show.perform do |existing_issue|
|
||||
|
|
@ -30,8 +30,8 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'milestone assigned to new issue' do |testcase|
|
||||
it 'is assigned to a new issue', testcase: testcase do
|
||||
shared_examples 'when assigned to new issue' do |testcase|
|
||||
it 'is assigned', testcase: testcase do
|
||||
Resource::Issue.fabricate_via_browser_ui! do |new_issue|
|
||||
new_issue.project = project
|
||||
new_issue.milestone = milestone
|
||||
|
|
@ -46,15 +46,15 @@ module QA
|
|||
context 'Group milestone' do
|
||||
let(:milestone) { create(:group_milestone, group: group, start_date: start_date, due_date: due_date) }
|
||||
|
||||
it_behaves_like 'milestone assigned to existing issue', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347964'
|
||||
it_behaves_like 'milestone assigned to new issue', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347965'
|
||||
it_behaves_like 'when assigned to existing issue', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347964'
|
||||
it_behaves_like 'when assigned to new issue', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347965'
|
||||
end
|
||||
|
||||
context 'Project milestone' do
|
||||
let(:milestone) { create(:project_milestone, project: project, start_date: start_date, due_date: due_date) }
|
||||
|
||||
it_behaves_like 'milestone assigned to existing issue', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347962'
|
||||
it_behaves_like 'milestone assigned to new issue', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347963'
|
||||
it_behaves_like 'when assigned to existing issue', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347962'
|
||||
it_behaves_like 'when assigned to new issue', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347963'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,11 +6,5 @@ FactoryBot.define do
|
|||
|
||||
track { 'create' }
|
||||
series { 0 }
|
||||
|
||||
trait :campaign do
|
||||
track { nil }
|
||||
series { nil }
|
||||
campaign { Users::InProductMarketingEmail::BUILD_IOS_APP_GUIDE }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Work item linked items', :js, feature_category: :team_planning do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, :public, namespace: group) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:work_item) { create(:work_item, project: project) }
|
||||
let(:work_items_path) { project_work_items_path(project, work_items_path: work_item.iid) }
|
||||
let_it_be(:task) { create(:work_item, :task, project: project, title: 'Task 1') }
|
||||
|
||||
context 'for signed in user' do
|
||||
include ContentEditorHelpers
|
||||
before_all do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
|
||||
stub_feature_flags(work_items: true)
|
||||
stub_feature_flags(linked_work_items: true)
|
||||
|
||||
visit work_items_path
|
||||
|
||||
wait_for_requests
|
||||
|
||||
close_rich_text_promo_popover_if_present
|
||||
end
|
||||
|
||||
it 'are not displayed when issue does not have work item links', :aggregate_failures do
|
||||
page.within('.work-item-relationships') do
|
||||
expect(page).to have_selector('[data-testid="link-item-add-button"]')
|
||||
expect(page).not_to have_selector('[data-testid="link-work-item-form"]')
|
||||
expect(page).not_to have_selector('[data-testid="work-item-linked-items-list"]')
|
||||
end
|
||||
end
|
||||
|
||||
it 'toggles widget body', :aggregate_failures do
|
||||
page.within('.work-item-relationships') do
|
||||
expect(page).to have_selector('[data-testid="widget-body"]')
|
||||
|
||||
click_button 'Collapse'
|
||||
|
||||
expect(page).not_to have_selector('[data-testid="widget-body"]')
|
||||
|
||||
click_button 'Expand'
|
||||
|
||||
expect(page).to have_selector('[data-testid="widget-body"]')
|
||||
end
|
||||
end
|
||||
|
||||
it 'toggles form', :aggregate_failures do
|
||||
page.within('.work-item-relationships') do
|
||||
expect(page).not_to have_selector('[data-testid="link-work-item-form"]')
|
||||
|
||||
click_button 'Add'
|
||||
|
||||
expect(page).to have_selector('[data-testid="link-work-item-form"]')
|
||||
|
||||
click_button 'Cancel'
|
||||
|
||||
expect(page).not_to have_selector('[data-testid="link-work-item-form"]')
|
||||
end
|
||||
end
|
||||
|
||||
it 'links a new item', :aggregate_failures do
|
||||
page.within('.work-item-relationships') do
|
||||
click_button 'Add'
|
||||
|
||||
within_testid('link-work-item-form') do
|
||||
expect(page).to have_button('Add', disabled: true)
|
||||
find_by_testid('work-item-token-select-input').set(task.title)
|
||||
wait_for_all_requests
|
||||
click_button task.title
|
||||
|
||||
expect(page).to have_button('Add', disabled: false)
|
||||
|
||||
click_button 'Add'
|
||||
|
||||
wait_for_all_requests
|
||||
end
|
||||
|
||||
expect(find('.work-items-list')).to have_content('Task 1')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -124,7 +124,9 @@ RSpec.describe 'Work item children', :js, feature_category: :team_planning do
|
|||
|
||||
expect(page).to have_button('Add task', disabled: false)
|
||||
|
||||
click_button 'Add task'
|
||||
send_keys :escape
|
||||
|
||||
click_button('Add task')
|
||||
|
||||
wait_for_all_requests
|
||||
|
||||
|
|
@ -162,7 +164,9 @@ RSpec.describe 'Work item children', :js, feature_category: :team_planning do
|
|||
wait_for_all_requests
|
||||
click_button task.title
|
||||
|
||||
click_button 'Add task'
|
||||
send_keys :escape
|
||||
|
||||
click_button('Add task')
|
||||
|
||||
wait_for_all_requests
|
||||
end
|
||||
|
|
|
|||
|
|
@ -40,20 +40,20 @@ describe('Job Cell', () => {
|
|||
};
|
||||
|
||||
describe('Job Id', () => {
|
||||
it('displays the job id and links to the job', () => {
|
||||
it('displays the job id, job name and links to the job', () => {
|
||||
createComponent();
|
||||
|
||||
const expectedJobId = `#${getIdFromGraphQLId(mockJob.id)}`;
|
||||
const expectedJobId = `#${getIdFromGraphQLId(mockJob.id)}: ${mockJob.name}`;
|
||||
|
||||
expect(findJobIdLink().text()).toBe(expectedJobId);
|
||||
expect(findJobIdLink().attributes('href')).toBe(mockJob.detailedStatus.detailsPath);
|
||||
expect(findJobIdNoLink().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('display the job id with no link', () => {
|
||||
it('display the job id and job name with no link', () => {
|
||||
createComponent(jobAsGuest);
|
||||
|
||||
const expectedJobId = `#${getIdFromGraphQLId(jobAsGuest.id)}`;
|
||||
const expectedJobId = `#${getIdFromGraphQLId(jobAsGuest.id)}: ${jobAsGuest.name}`;
|
||||
|
||||
expect(findJobIdNoLink().text()).toBe(expectedJobId);
|
||||
expect(findJobIdNoLink().exists()).toBe(true);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import DurationCell from '~/ci/jobs_page/components/job_cells/duration_cell.vue';
|
||||
import StatusCell from '~/ci/jobs_page/components/job_cells/status_cell.vue';
|
||||
|
||||
describe('Duration Cell', () => {
|
||||
let wrapper;
|
||||
|
|
@ -12,7 +12,7 @@ describe('Duration Cell', () => {
|
|||
|
||||
const createComponent = (props) => {
|
||||
wrapper = extendedWrapper(
|
||||
shallowMount(DurationCell, {
|
||||
shallowMount(StatusCell, {
|
||||
propsData: {
|
||||
job: {
|
||||
...props,
|
||||
|
|
@ -2,6 +2,7 @@ import { GlTable } from '@gitlab/ui';
|
|||
import { mount } from '@vue/test-utils';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
|
||||
import { DEFAULT_FIELDS_ADMIN } from '~/ci/admin/jobs_table/constants';
|
||||
import ProjectCell from '~/ci/admin/jobs_table/components/cells/project_cell.vue';
|
||||
|
|
@ -47,11 +48,11 @@ describe('Jobs Table', () => {
|
|||
expect(findCiBadgeLink().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('displays the job stage and name', () => {
|
||||
it('displays the job stage, id and name', () => {
|
||||
const [firstJob] = mockJobsNodes;
|
||||
|
||||
expect(findJobStage().text()).toBe(firstJob.stage.name);
|
||||
expect(findJobName().text()).toBe(firstJob.name);
|
||||
expect(findJobStage().text()).toBe(`Stage: ${firstJob.stage.name}`);
|
||||
expect(findJobName().text()).toBe(`#${getIdFromGraphQLId(firstJob.id)}: ${firstJob.name}`);
|
||||
});
|
||||
|
||||
it('displays the coverage for only jobs that have coverage', () => {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ describe('pipeline graph job item', () => {
|
|||
const findJobWithoutLink = () => wrapper.findByTestId('job-without-link');
|
||||
const findJobWithLink = () => wrapper.findByTestId('job-with-link');
|
||||
const findActionVueComponent = () => wrapper.findComponent(ActionComponent);
|
||||
const findActionComponent = () => wrapper.findByTestId('ci-action-component');
|
||||
const findActionComponent = () => wrapper.findByTestId('ci-action-button');
|
||||
const findBadge = () => wrapper.findComponent(GlBadge);
|
||||
const findJobLink = () => wrapper.findByTestId('job-with-link');
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ describe('Linked pipeline', () => {
|
|||
const findButton = () => wrapper.findComponent(GlButton);
|
||||
const findCancelButton = () => wrapper.findByLabelText('Cancel downstream pipeline');
|
||||
const findCardTooltip = () => wrapper.findComponent(GlTooltip);
|
||||
const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title');
|
||||
const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title-content');
|
||||
const findExpandButton = () => wrapper.findByTestId('expand-pipeline-button');
|
||||
const findLinkedPipeline = () => wrapper.findComponent({ ref: 'linkedPipeline' });
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ describe('HeaderActions component', () => {
|
|||
const findMobileDropdownItems = () => findMobileDropdown().findAllComponents(GlDropdownItem);
|
||||
const findDesktopDropdownItems = () => findDesktopDropdown().findAllComponents(GlDropdownItem);
|
||||
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
|
||||
const findReportAbuseSelectorItem = () => wrapper.find(`[data-testid="report-abuse-item"]`);
|
||||
const findReportAbuseButton = () => wrapper.find(`[data-testid="report-abuse-item"]`);
|
||||
const findNotificationWidget = () => wrapper.find(`[data-testid="notification-toggle"]`);
|
||||
const findLockIssueWidget = () => wrapper.find(`[data-testid="lock-issue-toggle"]`);
|
||||
const findCopyRefenceDropdownItem = () => wrapper.find(`[data-testid="copy-reference"]`);
|
||||
|
|
@ -490,29 +490,41 @@ describe('HeaderActions component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('abuse category selector', () => {
|
||||
describe('report abuse to admin button', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent({ props: { isIssueAuthor: false } });
|
||||
});
|
||||
|
||||
it("doesn't render", () => {
|
||||
it('renders the button but not the abuse category drawer', () => {
|
||||
expect(findReportAbuseButton().exists()).toBe(true);
|
||||
expect(findAbuseCategorySelector().exists()).toEqual(false);
|
||||
});
|
||||
|
||||
it('opens the drawer', async () => {
|
||||
findReportAbuseSelectorItem().vm.$emit('click');
|
||||
it('opens the abuse category drawer', async () => {
|
||||
findReportAbuseButton().vm.$emit('click');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findAbuseCategorySelector().props('showDrawer')).toEqual(true);
|
||||
});
|
||||
|
||||
it('closes the drawer', async () => {
|
||||
await findReportAbuseSelectorItem().vm.$emit('click');
|
||||
await findAbuseCategorySelector().vm.$emit('close-drawer');
|
||||
it('closes the abuse category drawer', async () => {
|
||||
await findReportAbuseButton().vm.$emit('click');
|
||||
expect(findAbuseCategorySelector().exists()).toEqual(true);
|
||||
|
||||
await findAbuseCategorySelector().vm.$emit('close-drawer');
|
||||
expect(findAbuseCategorySelector().exists()).toEqual(false);
|
||||
});
|
||||
|
||||
describe('when the logged in user is the issue author', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent({ props: { isIssueAuthor: true } });
|
||||
});
|
||||
|
||||
it('does not render the button', () => {
|
||||
expect(findReportAbuseButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('notification toggle', () => {
|
||||
|
|
@ -694,7 +706,7 @@ describe('HeaderActions component', () => {
|
|||
expect(findDesktopDropdown().exists()).toBe(headerActionsVisible);
|
||||
expect(findCopyRefenceDropdownItem().exists()).toBe(headerActionsVisible);
|
||||
expect(findNotificationWidget().exists()).toBe(false);
|
||||
expect(findReportAbuseSelectorItem().exists()).toBe(false);
|
||||
expect(findReportAbuseButton().exists()).toBe(false);
|
||||
expect(findLockIssueWidget().exists()).toBe(false);
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`WorkItemRelationshipList renders linked item list 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="work-item-linked-items-list"
|
||||
>
|
||||
<h4
|
||||
class="gl-font-sm gl-font-weight-semibold gl-mb-2 gl-mt-3 gl-mx-2 gl-text-gray-700"
|
||||
data-testid="work-items-list-heading"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlForm, GlFormRadioGroup, GlAlert } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
|
||||
import WorkItemAddRelationshipForm from '~/work_items/components/work_item_relationships/work_item_add_relationship_form.vue';
|
||||
import WorkItemTokenInput from '~/work_items/components/shared/work_item_token_input.vue';
|
||||
import addLinkedItemsMutation from '~/work_items/graphql/add_linked_items.mutation.graphql';
|
||||
import { LINKED_ITEM_TYPE_VALUE } from '~/work_items/constants';
|
||||
|
||||
import { linkedWorkItemResponse } from '../../mock_data';
|
||||
|
||||
describe('WorkItemAddRelationshipForm', () => {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
let wrapper;
|
||||
const linkedWorkItemsSuccessMutationHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(linkedWorkItemResponse());
|
||||
|
||||
const createComponent = async ({
|
||||
workItemId = 'gid://gitlab/WorkItem/1',
|
||||
workItemIid = '1',
|
||||
workItemType = 'Objective',
|
||||
childrenIds = [],
|
||||
linkedWorkItemsMutationHandler = linkedWorkItemsSuccessMutationHandler,
|
||||
} = {}) => {
|
||||
const mockApolloProvider = createMockApollo([
|
||||
[addLinkedItemsMutation, linkedWorkItemsMutationHandler],
|
||||
]);
|
||||
|
||||
wrapper = shallowMountExtended(WorkItemAddRelationshipForm, {
|
||||
apolloProvider: mockApolloProvider,
|
||||
propsData: {
|
||||
workItemId,
|
||||
workItemIid,
|
||||
workItemFullPath: 'test-project-path',
|
||||
workItemType,
|
||||
childrenIds,
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
};
|
||||
|
||||
const findLinkWorkItemForm = () => wrapper.findComponent(GlForm);
|
||||
const findLinkWorkItemButton = () => wrapper.findByTestId('link-work-item-button');
|
||||
const findMaxWorkItemNote = () => wrapper.findByTestId('max-work-item-note');
|
||||
const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
|
||||
const findWorkItemTokenInput = () => wrapper.findComponent(WorkItemTokenInput);
|
||||
const findGlAlert = () => wrapper.findComponent(GlAlert);
|
||||
|
||||
beforeEach(async () => {
|
||||
await createComponent();
|
||||
});
|
||||
|
||||
it('renders link work item form with default values', () => {
|
||||
expect(findLinkWorkItemForm().exists()).toBe(true);
|
||||
expect(findRadioGroup().props('options')).toEqual([
|
||||
{ text: s__('WorkItem|relates to'), value: LINKED_ITEM_TYPE_VALUE.RELATED },
|
||||
{ text: s__('WorkItem|blocks'), value: LINKED_ITEM_TYPE_VALUE.BLOCKS },
|
||||
{ text: s__('WorkItem|is blocked by'), value: LINKED_ITEM_TYPE_VALUE.BLOCKED_BY },
|
||||
]);
|
||||
expect(findLinkWorkItemButton().attributes('disabled')).toBe('true');
|
||||
expect(findMaxWorkItemNote().text()).toBe(s__('WorkItem|Add a maximum of 3 items at a time.'));
|
||||
});
|
||||
|
||||
it('renders work item token input with default props', () => {
|
||||
expect(findWorkItemTokenInput().props()).toMatchObject({
|
||||
value: [],
|
||||
fullPath: 'test-project-path',
|
||||
childrenIds: [],
|
||||
parentWorkItemId: 'gid://gitlab/WorkItem/1',
|
||||
areWorkItemsToAddValid: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('linking a work item', () => {
|
||||
const selectWorkItemTokens = (workItems) => {
|
||||
findWorkItemTokenInput().vm.$emit('input', workItems);
|
||||
};
|
||||
|
||||
it('enables add button when work item is selected', async () => {
|
||||
await selectWorkItemTokens([
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/644',
|
||||
},
|
||||
]);
|
||||
expect(findLinkWorkItemButton().attributes('disabled')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('disables button when more than 3 work items are selected', async () => {
|
||||
await selectWorkItemTokens([
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/641',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/642',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/643',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/644',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(findWorkItemTokenInput().props('areWorkItemsToAddValid')).toBe(false);
|
||||
expect(findLinkWorkItemButton().attributes('disabled')).toBe('true');
|
||||
});
|
||||
|
||||
it.each`
|
||||
assertionName | linkTypeInput
|
||||
${'related'} | ${LINKED_ITEM_TYPE_VALUE.RELATED}
|
||||
${'blocking'} | ${LINKED_ITEM_TYPE_VALUE.BLOCKED_BY}
|
||||
`('selects and links $assertionName work item', async ({ linkTypeInput }) => {
|
||||
findRadioGroup().vm.$emit('input', linkTypeInput);
|
||||
await selectWorkItemTokens([
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/641',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/642',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(findWorkItemTokenInput().props('areWorkItemsToAddValid')).toBe(true);
|
||||
|
||||
findLinkWorkItemForm().vm.$emit('submit', {
|
||||
preventDefault: jest.fn(),
|
||||
stopPropagation: jest.fn(),
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(linkedWorkItemsSuccessMutationHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
id: 'gid://gitlab/WorkItem/1',
|
||||
linkType: linkTypeInput,
|
||||
workItemsIds: ['gid://gitlab/WorkItem/641', 'gid://gitlab/WorkItem/642'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each`
|
||||
errorType | mutationMock | errorMessage
|
||||
${'an error in the mutation response'} | ${jest.fn().mockResolvedValue(linkedWorkItemResponse({}, ['Linked Item failed']))} | ${'Linked Item failed'}
|
||||
${'a network error'} | ${jest.fn().mockRejectedValue(new Error('Network Error'))} | ${'Something went wrong when trying to link a item. Please try again.'}
|
||||
`('shows an error message when there is $errorType', async ({ mutationMock, errorMessage }) => {
|
||||
createComponent({ linkedWorkItemsMutationHandler: mutationMock });
|
||||
await selectWorkItemTokens([
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/641',
|
||||
},
|
||||
]);
|
||||
|
||||
findLinkWorkItemForm().vm.$emit('submit', {
|
||||
preventDefault: jest.fn(),
|
||||
stopPropagation: jest.fn(),
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(findGlAlert().exists()).toBe(true);
|
||||
expect(findGlAlert().text()).toBe(errorMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -9,6 +9,7 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
|
||||
import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue';
|
||||
import WorkItemRelationshipList from '~/work_items/components/work_item_relationships/work_item_relationship_list.vue';
|
||||
import WorkItemAddRelationshipForm from '~/work_items/components/work_item_relationships/work_item_add_relationship_form.vue';
|
||||
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
|
||||
|
||||
import {
|
||||
|
|
@ -24,23 +25,20 @@ describe('WorkItemRelationships', () => {
|
|||
const emptyLinkedWorkItemsQueryHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(workItemByIidResponseFactory());
|
||||
const linkedWorkItemsQueryHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockLinkedItems }));
|
||||
const blockingLinkedWorkItemQueryHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockBlockingLinkedItem }));
|
||||
|
||||
const createComponent = async ({
|
||||
workItemQueryHandler = emptyLinkedWorkItemsQueryHandler,
|
||||
workItemType = 'Task',
|
||||
} = {}) => {
|
||||
const mockApollo = createMockApollo([[workItemByIidQuery, workItemQueryHandler]]);
|
||||
|
||||
wrapper = shallowMountExtended(WorkItemRelationships, {
|
||||
apolloProvider: mockApollo,
|
||||
propsData: {
|
||||
workItemId: 'gid://gitlab/WorkItem/1',
|
||||
workItemIid: '1',
|
||||
workItemFullPath: 'test-project-path',
|
||||
workItemType,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -53,6 +51,8 @@ describe('WorkItemRelationships', () => {
|
|||
const findLinkedItemsCountContainer = () => wrapper.findByTestId('linked-items-count');
|
||||
const findAllWorkItemRelationshipListComponents = () =>
|
||||
wrapper.findAllComponents(WorkItemRelationshipList);
|
||||
const findAddButton = () => wrapper.findByTestId('link-item-add-button');
|
||||
const findWorkItemRelationshipForm = () => wrapper.findComponent(WorkItemAddRelationshipForm);
|
||||
|
||||
it('shows loading icon when query is not processed', () => {
|
||||
createComponent();
|
||||
|
|
@ -60,22 +60,32 @@ describe('WorkItemRelationships', () => {
|
|||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the component with empty message when there are no items', async () => {
|
||||
it('renders the component with with defaults', async () => {
|
||||
await createComponent();
|
||||
|
||||
expect(wrapper.find('.work-item-relationships').exists()).toBe(true);
|
||||
expect(findEmptyRelatedMessageContainer().exists()).toBe(true);
|
||||
expect(findAddButton().exists()).toBe(true);
|
||||
expect(findWorkItemRelationshipForm().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders blocking linked item lists', async () => {
|
||||
await createComponent({ workItemQueryHandler: blockingLinkedWorkItemQueryHandler });
|
||||
await createComponent({
|
||||
workItemQueryHandler: jest
|
||||
.fn()
|
||||
.mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockBlockingLinkedItem })),
|
||||
});
|
||||
|
||||
expect(findAllWorkItemRelationshipListComponents().length).toBe(1);
|
||||
expect(findLinkedItemsCountContainer().text()).toBe('1');
|
||||
});
|
||||
|
||||
it('renders blocking, blocked by and related to linked item lists with proper count', async () => {
|
||||
await createComponent({ workItemQueryHandler: linkedWorkItemsQueryHandler });
|
||||
await createComponent({
|
||||
workItemQueryHandler: jest
|
||||
.fn()
|
||||
.mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockLinkedItems })),
|
||||
});
|
||||
|
||||
// renders all 3 lists: blocking, blocked by and related to
|
||||
expect(findAllWorkItemRelationshipListComponents().length).toBe(3);
|
||||
|
|
@ -90,4 +100,24 @@ describe('WorkItemRelationships', () => {
|
|||
|
||||
expect(findWidgetWrapper().props('error')).toBe(errorMessage);
|
||||
});
|
||||
|
||||
it('does not render add button when there is no permission', async () => {
|
||||
await createComponent({
|
||||
workItemQueryHandler: jest
|
||||
.fn()
|
||||
.mockResolvedValue(workItemByIidResponseFactory({ canAdminWorkItemLink: false })),
|
||||
});
|
||||
|
||||
expect(findAddButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows form on add button and hides when cancel button is clicked', async () => {
|
||||
await createComponent();
|
||||
|
||||
await findAddButton().vm.$emit('click');
|
||||
expect(findWorkItemRelationshipForm().exists()).toBe(true);
|
||||
|
||||
await findWorkItemRelationshipForm().vm.$emit('cancel');
|
||||
expect(findWorkItemRelationshipForm().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ export const workItemQueryResponse = {
|
|||
setWorkItemMetadata: false,
|
||||
adminParentLink: false,
|
||||
createNote: false,
|
||||
adminWorkItemLink: true,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
widgets: [
|
||||
|
|
@ -251,6 +252,7 @@ export const updateWorkItemMutationResponse = {
|
|||
setWorkItemMetadata: false,
|
||||
adminParentLink: false,
|
||||
createNote: false,
|
||||
adminWorkItemLink: true,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
reference: 'test-project-path#1',
|
||||
|
|
@ -360,6 +362,7 @@ export const convertWorkItemMutationResponse = {
|
|||
setWorkItemMetadata: false,
|
||||
adminParentLink: false,
|
||||
createNote: false,
|
||||
adminWorkItemLink: true,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
reference: 'gitlab-org/gitlab-test#1',
|
||||
|
|
@ -579,6 +582,7 @@ export const workItemResponseFactory = ({
|
|||
canDelete = false,
|
||||
canCreateNote = false,
|
||||
adminParentLink = false,
|
||||
canAdminWorkItemLink = true,
|
||||
notificationsWidgetPresent = true,
|
||||
currentUserTodosWidgetPresent = true,
|
||||
awardEmojiWidgetPresent = true,
|
||||
|
|
@ -636,6 +640,7 @@ export const workItemResponseFactory = ({
|
|||
updateWorkItem: canUpdate,
|
||||
setWorkItemMetadata: canUpdate,
|
||||
adminParentLink,
|
||||
adminWorkItemLink: canAdminWorkItemLink,
|
||||
createNote: canCreateNote,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
|
|
@ -914,6 +919,7 @@ export const createWorkItemMutationResponse = {
|
|||
setWorkItemMetadata: false,
|
||||
adminParentLink: false,
|
||||
createNote: false,
|
||||
adminWorkItemLink: true,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
reference: 'test-project-path#1',
|
||||
|
|
@ -996,6 +1002,7 @@ export const workItemHierarchyEmptyResponse = {
|
|||
setWorkItemMetadata: false,
|
||||
adminParentLink: false,
|
||||
createNote: false,
|
||||
adminWorkItemLink: true,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
confidential: false,
|
||||
|
|
@ -1046,6 +1053,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
|
|||
setWorkItemMetadata: false,
|
||||
adminParentLink: false,
|
||||
createNote: false,
|
||||
adminWorkItemLink: true,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
project: {
|
||||
|
|
@ -1196,6 +1204,7 @@ export const workItemHierarchyResponse = {
|
|||
setWorkItemMetadata: true,
|
||||
adminParentLink: true,
|
||||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
author: {
|
||||
|
|
@ -1297,6 +1306,7 @@ export const workItemObjectiveWithChild = {
|
|||
setWorkItemMetadata: true,
|
||||
adminParentLink: true,
|
||||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
author: {
|
||||
|
|
@ -1368,6 +1378,7 @@ export const workItemHierarchyTreeResponse = {
|
|||
setWorkItemMetadata: true,
|
||||
adminParentLink: true,
|
||||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
confidential: false,
|
||||
|
|
@ -1449,6 +1460,7 @@ export const changeIndirectWorkItemParentMutationResponse = {
|
|||
setWorkItemMetadata: true,
|
||||
adminParentLink: true,
|
||||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
description: null,
|
||||
|
|
@ -1517,6 +1529,7 @@ export const changeWorkItemParentMutationResponse = {
|
|||
setWorkItemMetadata: true,
|
||||
adminParentLink: true,
|
||||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
description: null,
|
||||
|
|
@ -1568,6 +1581,7 @@ export const availableWorkItemsResponse = {
|
|||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/458',
|
||||
iid: '2',
|
||||
title: 'Task 1',
|
||||
state: 'OPEN',
|
||||
createdAt: '2022-08-03T12:41:54Z',
|
||||
|
|
@ -1576,6 +1590,7 @@ export const availableWorkItemsResponse = {
|
|||
},
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/459',
|
||||
iid: '3',
|
||||
title: 'Task 2',
|
||||
state: 'OPEN',
|
||||
createdAt: '2022-08-03T12:41:54Z',
|
||||
|
|
@ -1584,6 +1599,7 @@ export const availableWorkItemsResponse = {
|
|||
},
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/460',
|
||||
iid: '4',
|
||||
title: 'Task 3',
|
||||
state: 'OPEN',
|
||||
createdAt: '2022-08-03T12:41:54Z',
|
||||
|
|
@ -1605,6 +1621,7 @@ export const searchedWorkItemsResponse = {
|
|||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/WorkItem/459',
|
||||
iid: '3',
|
||||
title: 'Task 2',
|
||||
state: 'OPEN',
|
||||
createdAt: '2022-08-03T12:41:54Z',
|
||||
|
|
@ -3439,6 +3456,19 @@ export const getTodosMutationResponse = (state) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const linkedWorkItemResponse = (options, errors = []) => {
|
||||
const response = workItemResponseFactory(options);
|
||||
return {
|
||||
data: {
|
||||
workItemAddLinkedItems: {
|
||||
workItem: response.data.workItem,
|
||||
errors,
|
||||
__typename: 'WorkItemAddLinkedItemsPayload',
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const groupWorkItemsQueryResponse = {
|
||||
data: {
|
||||
group: {
|
||||
|
|
|
|||
|
|
@ -199,8 +199,6 @@ describe('Create work item component', () => {
|
|||
wrapper.find('form').trigger('submit');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findAlert().text()).toBe(
|
||||
'Something went wrong when creating work item. Please try again.',
|
||||
);
|
||||
expect(findAlert().text()).toBe('Something went wrong when creating item. Please try again.');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Email::Message::BuildIosAppGuide, :saas do
|
||||
subject(:message) { described_class.new }
|
||||
|
||||
it 'contains the correct message', :aggregate_failures do
|
||||
expect(message.subject_line).to eq 'Get set up to build for iOS'
|
||||
expect(message.title).to eq "Building for iOS? We've got you covered."
|
||||
expect(message.body_line1).to eq "Want to get your iOS app up and running, including " \
|
||||
"publishing all the way to TestFlight? Follow our guide to set up GitLab and fastlane to publish iOS apps to " \
|
||||
"the App Store."
|
||||
expect(message.cta_text).to eq 'Learn how to build for iOS'
|
||||
expect(message.cta2_text).to eq 'Watch iOS building in action.'
|
||||
expect(message.logo_path).to eq 'mailers/in_product_marketing/create-0.png'
|
||||
expect(message.unsubscribe).to include('%tag_unsubscribe_url%')
|
||||
end
|
||||
end
|
||||
|
|
@ -1,51 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'email_spec'
|
||||
|
||||
RSpec.describe Emails::InProductMarketing do
|
||||
include EmailSpec::Matchers
|
||||
include Gitlab::Routing.url_helpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
shared_examples 'has custom headers when on gitlab.com' do
|
||||
context 'when on gitlab.com', :saas do
|
||||
it 'has custom headers' do
|
||||
aggregate_failures do
|
||||
expect(subject).to deliver_from(described_class::FROM_ADDRESS)
|
||||
expect(subject).to reply_to(described_class::FROM_ADDRESS)
|
||||
expect(subject).to have_header('X-Mailgun-Track', 'yes')
|
||||
expect(subject).to have_header('X-Mailgun-Track-Clicks', 'yes')
|
||||
expect(subject).to have_header('X-Mailgun-Track-Opens', 'yes')
|
||||
expect(subject).to have_header('X-Mailgun-Tag', 'marketing')
|
||||
expect(subject).to have_body_text('%tag_unsubscribe_url%')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#build_ios_app_guide_email' do
|
||||
subject { Notify.build_ios_app_guide_email(user.notification_email_or_default) }
|
||||
|
||||
it 'sends to the right user' do
|
||||
expect(subject).to deliver_to(user.notification_email_or_default)
|
||||
end
|
||||
|
||||
it 'has the correct subject and content' do
|
||||
message = Gitlab::Email::Message::BuildIosAppGuide.new
|
||||
cta_url = 'https://about.gitlab.com/blog/2019/03/06/ios-publishing-with-gitlab-and-fastlane/'
|
||||
cta2_url = 'https://www.youtube.com/watch?v=325FyJt7ZG8'
|
||||
|
||||
aggregate_failures do
|
||||
is_expected.to have_subject(message.subject_line)
|
||||
is_expected.to have_body_text(message.title)
|
||||
is_expected.to have_body_text(message.body_line1)
|
||||
is_expected.to have_body_text(CGI.unescapeHTML(message.cta_link))
|
||||
is_expected.to have_body_text(CGI.unescapeHTML(message.cta2_link))
|
||||
is_expected.to have_body_text(cta_url)
|
||||
is_expected.to have_body_text(cta2_url)
|
||||
end
|
||||
end
|
||||
it 'has correct custom headers' do
|
||||
expect(described_class::FROM_ADDRESS).to be('GitLab <team@gitlab.com>')
|
||||
expect(described_class::CUSTOM_HEADERS).to eq({
|
||||
from: 'GitLab <team@gitlab.com>',
|
||||
reply_to: 'GitLab <team@gitlab.com>',
|
||||
'X-Mailgun-Track' => 'yes',
|
||||
'X-Mailgun-Track-Clicks' => 'yes',
|
||||
'X-Mailgun-Track-Opens' => 'yes',
|
||||
'X-Mailgun-Tag' => 'marketing'
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do
|
|||
let_it_be(:merge_request) { create(:merge_request) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
subject { report.report_type }
|
||||
subject(:report_type) { report.report_type }
|
||||
|
||||
context 'when reported from an issue' do
|
||||
let(:url) { project_issue_url(issue.project, issue) }
|
||||
|
|
@ -322,7 +322,7 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do
|
|||
let_it_be(:merge_request) { create(:merge_request, description: 'mr description') }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
subject { report.reported_content }
|
||||
subject(:reported_content) { report.reported_content }
|
||||
|
||||
context 'when reported from an issue' do
|
||||
let(:url) { project_issue_url(issue.project, issue) }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Users::InProductMarketingEmail, type: :model do
|
||||
RSpec.describe Users::InProductMarketingEmail, type: :model, feature_category: :onboarding do
|
||||
let(:track) { :create }
|
||||
let(:series) { 0 }
|
||||
|
||||
|
|
@ -24,28 +24,6 @@ RSpec.describe Users::InProductMarketingEmail, type: :model do
|
|||
.scoped_to([:track, :series]).with_message('track series email has already been sent')
|
||||
}
|
||||
end
|
||||
|
||||
context 'when campaign email' do
|
||||
subject { build(:in_product_marketing_email, :campaign) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:campaign) }
|
||||
it { is_expected.not_to validate_presence_of(:track) }
|
||||
it { is_expected.not_to validate_presence_of(:series) }
|
||||
|
||||
it {
|
||||
is_expected.to validate_uniqueness_of(:user_id)
|
||||
.scoped_to(:campaign).with_message('campaign email has already been sent')
|
||||
}
|
||||
|
||||
it { is_expected.to validate_inclusion_of(:campaign).in_array(described_class::CAMPAIGNS) }
|
||||
end
|
||||
|
||||
context 'when mixing campaign and track+series' do
|
||||
it 'is not valid' do
|
||||
expect(build(:in_product_marketing_email, :campaign, track: :create)).not_to be_valid
|
||||
expect(build(:in_product_marketing_email, :campaign, series: 0)).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.without_track_and_series' do
|
||||
|
|
@ -84,27 +62,6 @@ RSpec.describe Users::InProductMarketingEmail, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.without_campaign' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:other_user) { create(:user) }
|
||||
|
||||
let(:campaign) { Users::InProductMarketingEmail::BUILD_IOS_APP_GUIDE }
|
||||
|
||||
subject(:without_campaign) { User.merge(described_class.without_campaign(campaign)) }
|
||||
|
||||
context 'when record for campaign already exists' do
|
||||
before do
|
||||
create(:in_product_marketing_email, :campaign, campaign: campaign, user: user)
|
||||
end
|
||||
|
||||
it { is_expected.to match_array [other_user] }
|
||||
end
|
||||
|
||||
context 'when record for campaign does not exist' do
|
||||
it { is_expected.to match_array [user, other_user] }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.for_user_with_track_and_series' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:in_product_marketing_email) { create(:in_product_marketing_email, series: 0, track: 0, user: user) }
|
||||
|
|
|
|||
|
|
@ -1,136 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::InProductMarketingCampaignEmailsService, feature_category: :activation do
|
||||
describe '#execute' do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
let(:campaign) { Users::InProductMarketingEmail::BUILD_IOS_APP_GUIDE }
|
||||
|
||||
before do
|
||||
allow(Notify)
|
||||
.to receive(:build_ios_app_guide_email)
|
||||
.and_return(instance_double(ActionMailer::MessageDelivery, deliver_later: true))
|
||||
end
|
||||
|
||||
subject(:execute) do
|
||||
described_class.new(project, campaign).execute
|
||||
end
|
||||
|
||||
context 'users can receive marketing emails' do
|
||||
let(:maintainer) { create(:user) }
|
||||
let(:developer) { create(:user) }
|
||||
|
||||
before do
|
||||
project.add_developer(developer)
|
||||
project.add_maintainer(maintainer)
|
||||
end
|
||||
|
||||
it 'sends the email to all project members with access_level >= Developer', :aggregate_failures do
|
||||
[project.owner, maintainer, developer].each do |user|
|
||||
email = user.notification_email_or_default
|
||||
|
||||
expect(Notify).to receive(:build_ios_app_guide_email).with(email)
|
||||
end
|
||||
|
||||
execute
|
||||
end
|
||||
|
||||
it 'records sent emails', :aggregate_failures do
|
||||
expect { execute }.to change { Users::InProductMarketingEmail.count }.from(0).to(3)
|
||||
|
||||
[project.owner, maintainer, developer].each do |user|
|
||||
expect(
|
||||
Users::InProductMarketingEmail.where(
|
||||
user: user,
|
||||
campaign: campaign
|
||||
)
|
||||
).to exist
|
||||
end
|
||||
end
|
||||
|
||||
it 'tracks experiment :email_sent event', :experiment do
|
||||
expect(experiment(:build_ios_app_guide_email)).to track(:email_sent)
|
||||
.on_next_instance
|
||||
.with_context(project: project)
|
||||
|
||||
execute
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'does not send the email' do
|
||||
it do
|
||||
email = user.notification_email_or_default
|
||||
expect(Notify).not_to receive(:build_ios_app_guide_email).with(email)
|
||||
execute
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'does not create a record of the sent email' do
|
||||
it do
|
||||
expect(
|
||||
Users::InProductMarketingEmail.where(
|
||||
user: user,
|
||||
campaign: campaign
|
||||
)
|
||||
).not_to exist
|
||||
|
||||
execute
|
||||
end
|
||||
end
|
||||
|
||||
context "when user can't receive marketing emails" do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
context 'when user.can?(:receive_notifications) is false' do
|
||||
it 'does not send the email' do
|
||||
allow_next_found_instance_of(User) do |user|
|
||||
allow(user).to receive(:can?).with(:receive_notifications) { false }
|
||||
|
||||
email = user.notification_email_or_default
|
||||
expect(Notify).not_to receive(:build_ios_app_guide_email).with(email)
|
||||
|
||||
expect(
|
||||
Users::InProductMarketingEmail.where(
|
||||
user: user,
|
||||
campaign: campaign
|
||||
)
|
||||
).not_to exist
|
||||
end
|
||||
|
||||
execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when campaign email has already been sent to the user' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
create(:in_product_marketing_email, :campaign, user: user, campaign: campaign)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not send the email'
|
||||
end
|
||||
|
||||
context "when user is a reporter" do
|
||||
before do
|
||||
project.add_reporter(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not send the email'
|
||||
it_behaves_like 'does not create a record of the sent email'
|
||||
end
|
||||
|
||||
context "when user is a guest" do
|
||||
before do
|
||||
project.add_guest(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not send the email'
|
||||
it_behaves_like 'does not create a record of the sent email'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -51,52 +51,6 @@ RSpec.describe Projects::RecordTargetPlatformsService, '#execute', feature_categ
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Build iOS guide email experiment' do
|
||||
shared_examples 'tracks experiment assignment event' do
|
||||
it 'tracks the assignment event', :experiment do
|
||||
expect(experiment(:build_ios_app_guide_email))
|
||||
.to track(:assignment)
|
||||
.with_context(project: project)
|
||||
.on_next_instance
|
||||
|
||||
execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'experiment candidate' do
|
||||
before do
|
||||
stub_experiments(build_ios_app_guide_email: :candidate)
|
||||
end
|
||||
|
||||
it 'executes a Projects::InProductMarketingCampaignEmailsService' do
|
||||
service_double = instance_double(Projects::InProductMarketingCampaignEmailsService, execute: true)
|
||||
|
||||
expect(Projects::InProductMarketingCampaignEmailsService)
|
||||
.to receive(:new).with(project, Users::InProductMarketingEmail::BUILD_IOS_APP_GUIDE)
|
||||
.and_return service_double
|
||||
expect(service_double).to receive(:execute)
|
||||
|
||||
execute
|
||||
end
|
||||
|
||||
it_behaves_like 'tracks experiment assignment event'
|
||||
end
|
||||
|
||||
context 'experiment control' do
|
||||
before do
|
||||
stub_experiments(build_ios_app_guide_email: :control)
|
||||
end
|
||||
|
||||
it 'does not execute a Projects::InProductMarketingCampaignEmailsService' do
|
||||
expect(Projects::InProductMarketingCampaignEmailsService).not_to receive(:new)
|
||||
|
||||
execute
|
||||
end
|
||||
|
||||
it_behaves_like 'tracks experiment assignment event'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project is not an XCode project' do
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ RSpec.describe Users::InProductMarketingEmailRecords, feature_category: :onboard
|
|||
|
||||
records.add(user, track: :team_short, series: 0)
|
||||
records.add(user, track: :create, series: 1)
|
||||
records.add(user, campaign: Users::InProductMarketingEmail::BUILD_IOS_APP_GUIDE)
|
||||
end
|
||||
|
||||
it 'bulk inserts added records' do
|
||||
|
|
@ -36,30 +35,20 @@ RSpec.describe Users::InProductMarketingEmailRecords, feature_category: :onboard
|
|||
freeze_time do
|
||||
records.add(user, track: :team_short, series: 0)
|
||||
records.add(user, track: :create, series: 1)
|
||||
records.add(user, campaign: Users::InProductMarketingEmail::BUILD_IOS_APP_GUIDE)
|
||||
|
||||
first, second, third = records.records
|
||||
first, second = records.records
|
||||
|
||||
expect(first).to be_a Users::InProductMarketingEmail
|
||||
expect(first.campaign).to be_nil
|
||||
expect(first.track.to_sym).to eq :team_short
|
||||
expect(first.series).to eq 0
|
||||
expect(first.created_at).to eq Time.zone.now
|
||||
expect(first.updated_at).to eq Time.zone.now
|
||||
|
||||
expect(second).to be_a Users::InProductMarketingEmail
|
||||
expect(second.campaign).to be_nil
|
||||
expect(second.track.to_sym).to eq :create
|
||||
expect(second.series).to eq 1
|
||||
expect(second.created_at).to eq Time.zone.now
|
||||
expect(second.updated_at).to eq Time.zone.now
|
||||
|
||||
expect(third).to be_a Users::InProductMarketingEmail
|
||||
expect(third.campaign).to eq Users::InProductMarketingEmail::BUILD_IOS_APP_GUIDE
|
||||
expect(third.track).to be_nil
|
||||
expect(third.series).to be_nil
|
||||
expect(third.created_at).to eq Time.zone.now
|
||||
expect(third.updated_at).to eq Time.zone.now
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ RSpec.describe WorkItems::RelatedWorkItemLinks::CreateService, feature_category:
|
|||
|
||||
it_behaves_like 'issuable link creation', use_references: false do
|
||||
let(:response_keys) { [:status, :created_references, :message] }
|
||||
let(:already_assigned_error_msg) { "Work items are already linked" }
|
||||
let(:already_assigned_error_msg) { "Items are already linked" }
|
||||
let(:no_found_error_msg) do
|
||||
'No matching work item found. Make sure you are adding a valid ID and you have access to the item.'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6244,7 +6244,6 @@
|
|||
- './spec/lib/gitlab/email/hook/delivery_metrics_observer_spec.rb'
|
||||
- './spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb'
|
||||
- './spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb'
|
||||
- './spec/lib/gitlab/email/message/build_ios_app_guide_spec.rb'
|
||||
- './spec/lib/gitlab/email/message/in_product_marketing/admin_verify_spec.rb'
|
||||
- './spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb'
|
||||
- './spec/lib/gitlab/email/message/in_product_marketing/create_spec.rb'
|
||||
|
|
@ -9433,7 +9432,6 @@
|
|||
- './spec/services/projects/import_export/export_service_spec.rb'
|
||||
- './spec/services/projects/import_export/relation_export_service_spec.rb'
|
||||
- './spec/services/projects/import_service_spec.rb'
|
||||
- './spec/services/projects/in_product_marketing_campaign_emails_service_spec.rb'
|
||||
- './spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb'
|
||||
- './spec/services/projects/lfs_pointers/lfs_download_service_spec.rb'
|
||||
- './spec/services/projects/lfs_pointers/lfs_import_service_spec.rb'
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
#!/usr/bin/env sh -e
|
||||
|
||||
for RUBY in 1.9.3 2.0.0 2.1 2.2
|
||||
do
|
||||
for RAILS in 2.3.8 3.0.0 3.1.0 3.2.0 4.0.0 4.1.0 4.2.0
|
||||
do
|
||||
if [[ $RUBY -gt 1.9.3 && $RAILS -lt 4.0.0 ]]; then
|
||||
continue
|
||||
fi
|
||||
RBENV_VERSION=$RUBY ACTIVERECORD=$RAILS bundle && bundle exec rake
|
||||
done
|
||||
done
|
||||
|
|
@ -1292,10 +1292,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.3.tgz#9ea641146436da388ffbad25d7f2abe0df52c235"
|
||||
integrity sha512-NMV++7Ew1FSBDN1xiZaauU9tfeSfgDHcOLpn+8bGpP+O5orUPm2Eu66R5eC5gkjBPaXosNAxNWtriee+aFk4+g==
|
||||
|
||||
"@gitlab/web-ide@0.0.1-dev-20230915130935":
|
||||
version "0.0.1-dev-20230915130935"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20230915130935.tgz#a7c8585d3413b235a23d54c5fb3d5f22be95f443"
|
||||
integrity sha512-80zCGoVU1GhovWOH/ieYKzYx4IWmXovbjpM2elwvggAZBVqwHN1pvhgm/W4XjE18kSRAxRCjHIkhEflb0Mhntg==
|
||||
"@gitlab/web-ide@0.0.1-dev-20230921201626":
|
||||
version "0.0.1-dev-20230921201626"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20230921201626.tgz#9c31a29e6552c48395dcb7b7b82db5e18d367e4b"
|
||||
integrity sha512-EQENU4fv9u7SZEjkEOwjvMr9vmGC8QPeIiaDIVmpHUl51LPp42fN7HsW1zGtg4JF0Aq8R0jrvn1nMhLst5XnRA==
|
||||
|
||||
"@graphql-eslint/eslint-plugin@3.20.1":
|
||||
version "3.20.1"
|
||||
|
|
|
|||
Loading…
Reference in New Issue