Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4df4a22481
commit
5fabe42e23
|
@ -53,6 +53,7 @@ eslint-report.html
|
|||
/db/data.yml
|
||||
/doc/code/*
|
||||
/dump.rdb
|
||||
/glfm_specification/input/github_flavored_markdown/ghfm_spec_v_*.txt
|
||||
/jsconfig.json
|
||||
/lefthook-local.yml
|
||||
/log/*.log*
|
||||
|
|
|
@ -273,7 +273,7 @@ export default {
|
|||
return IssuableStatusText[this.issuableStatus];
|
||||
},
|
||||
shouldShowStickyHeader() {
|
||||
return this.issuableType === IssuableType.Issue;
|
||||
return [IssuableType.Issue, IssuableType.Epic].includes(this.issuableType);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlButton, GlLoadingIcon, GlFormInput, GlFormGroup } from '@gitlab/ui';
|
||||
|
||||
import eventHub from '~/blob/components/eventhub';
|
||||
import createFlash from '~/flash';
|
||||
|
@ -11,7 +11,6 @@ import {
|
|||
} from '~/performance/constants';
|
||||
import { performanceMarkAndMeasure } from '~/performance/utils';
|
||||
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
|
||||
import TitleField from '~/vue_shared/components/form/title.vue';
|
||||
|
||||
import { SNIPPET_CREATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR } from '../constants';
|
||||
import { getSnippetMixin } from '../mixins/snippets';
|
||||
|
@ -31,10 +30,11 @@ export default {
|
|||
SnippetDescriptionEdit,
|
||||
SnippetVisibilityEdit,
|
||||
SnippetBlobActionsEdit,
|
||||
TitleField,
|
||||
FormFooterActions,
|
||||
GlButton,
|
||||
GlLoadingIcon,
|
||||
GlFormInput,
|
||||
GlFormGroup,
|
||||
},
|
||||
mixins: [getSnippetMixin],
|
||||
inject: ['selectedLevel'],
|
||||
|
@ -67,6 +67,7 @@ export default {
|
|||
description: '',
|
||||
visibilityLevel: this.selectedLevel,
|
||||
},
|
||||
showValidation: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -85,8 +86,11 @@ export default {
|
|||
hasValidBlobs() {
|
||||
return this.actions.every((x) => x.content);
|
||||
},
|
||||
updatePrevented() {
|
||||
return this.snippet.title === '' || !this.hasValidBlobs || this.isUpdating;
|
||||
isTitleValid() {
|
||||
return this.snippet.title !== '';
|
||||
},
|
||||
isFormValid() {
|
||||
return this.isTitleValid && this.hasValidBlobs;
|
||||
},
|
||||
isProjectSnippet() {
|
||||
return Boolean(this.projectPath);
|
||||
|
@ -112,6 +116,12 @@ export default {
|
|||
}
|
||||
return this.snippet.webUrl;
|
||||
},
|
||||
shouldShowBlobsErrors() {
|
||||
return this.showValidation && !this.hasValidBlobs;
|
||||
},
|
||||
shouldShowTitleErrors() {
|
||||
return this.showValidation && !this.isTitleValid;
|
||||
},
|
||||
},
|
||||
beforeCreate() {
|
||||
performanceMarkAndMeasure({ mark: SNIPPET_MARK_EDIT_APP_START });
|
||||
|
@ -165,6 +175,12 @@ export default {
|
|||
};
|
||||
},
|
||||
handleFormSubmit() {
|
||||
this.showValidation = true;
|
||||
|
||||
if (!this.isFormValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isUpdating = true;
|
||||
|
||||
this.$apollo
|
||||
|
@ -206,19 +222,31 @@ export default {
|
|||
class="loading-animation prepend-top-20 gl-mb-6"
|
||||
/>
|
||||
<template v-else>
|
||||
<title-field
|
||||
id="snippet-title"
|
||||
v-model="snippet.title"
|
||||
data-qa-selector="snippet_title_field"
|
||||
required
|
||||
:autofocus="true"
|
||||
/>
|
||||
<gl-form-group
|
||||
:label="__('Title')"
|
||||
label-for="snippet-title"
|
||||
:invalid-feedback="__('This field is required.')"
|
||||
:state="!shouldShowTitleErrors"
|
||||
>
|
||||
<gl-form-input
|
||||
id="snippet-title"
|
||||
v-model="snippet.title"
|
||||
data-testid="snippet-title-input"
|
||||
data-qa-selector="snippet_title_field"
|
||||
:autofocus="true"
|
||||
/>
|
||||
</gl-form-group>
|
||||
|
||||
<snippet-description-edit
|
||||
v-model="snippet.description"
|
||||
:markdown-preview-path="markdownPreviewPath"
|
||||
:markdown-docs-path="markdownDocsPath"
|
||||
/>
|
||||
<snippet-blob-actions-edit :init-blobs="blobs" @actions="updateActions" />
|
||||
<snippet-blob-actions-edit
|
||||
:init-blobs="blobs"
|
||||
:is-valid="!shouldShowBlobsErrors"
|
||||
@actions="updateActions"
|
||||
/>
|
||||
|
||||
<snippet-visibility-edit
|
||||
v-model="snippet.visibilityLevel"
|
||||
|
@ -228,12 +256,13 @@ export default {
|
|||
<form-footer-actions>
|
||||
<template #prepend>
|
||||
<gl-button
|
||||
class="js-no-auto-disable"
|
||||
category="primary"
|
||||
type="submit"
|
||||
variant="confirm"
|
||||
:disabled="updatePrevented"
|
||||
data-qa-selector="submit_button"
|
||||
data-testid="snippet-submit-btn"
|
||||
:disabled="isUpdating"
|
||||
>{{ saveButtonLabel }}</gl-button
|
||||
>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { GlButton, GlFormGroup } from '@gitlab/ui';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import { SNIPPET_MAX_BLOBS } from '../constants';
|
||||
|
@ -10,12 +10,18 @@ export default {
|
|||
components: {
|
||||
SnippetBlobEdit,
|
||||
GlButton,
|
||||
GlFormGroup,
|
||||
},
|
||||
props: {
|
||||
initBlobs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
isValid: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -124,16 +130,26 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<div class="form-group">
|
||||
<label :for="firstInputId">{{ s__('Snippets|Files') }}</label>
|
||||
<snippet-blob-edit
|
||||
v-for="(blobId, index) in blobIds"
|
||||
:key="blobId"
|
||||
:class="{ 'gl-mt-3': index > 0 }"
|
||||
:blob="blobs[blobId]"
|
||||
:can-delete="canDelete"
|
||||
@blob-updated="updateBlob(blobId, $event)"
|
||||
@delete="deleteBlob(blobId)"
|
||||
/>
|
||||
<gl-form-group
|
||||
:label="s__('Snippets|Files')"
|
||||
:label-for="firstInputId"
|
||||
:invalid-feedback="
|
||||
s__(
|
||||
'Snippets|Snippets can\'t contain empty files. Ensure all files have content, or delete them.',
|
||||
)
|
||||
"
|
||||
:state="isValid"
|
||||
>
|
||||
<snippet-blob-edit
|
||||
v-for="(blobId, index) in blobIds"
|
||||
:key="blobId"
|
||||
:class="{ 'gl-mt-3': index > 0 }"
|
||||
:blob="blobs[blobId]"
|
||||
:can-delete="canDelete"
|
||||
@blob-updated="updateBlob(blobId, $event)"
|
||||
@delete="deleteBlob(blobId)"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-button
|
||||
:disabled="!canAdd"
|
||||
data-testid="add_button"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlForm, GlFormInput } from '@gitlab/ui';
|
||||
import { GlForm, GlFormInput, GlFormGroup } from '@gitlab/ui';
|
||||
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
||||
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
|
||||
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
|
||||
|
@ -9,6 +9,7 @@ export default {
|
|||
components: {
|
||||
GlForm,
|
||||
GlFormInput,
|
||||
GlFormGroup,
|
||||
MarkdownField,
|
||||
LabelsSelect,
|
||||
},
|
||||
|
@ -37,6 +38,7 @@ export default {
|
|||
selectedLabels: [],
|
||||
};
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
handleUpdateSelectedLabels(labels) {
|
||||
if (labels.length) {
|
||||
|
@ -52,12 +54,15 @@ export default {
|
|||
<div data-testid="issuable-title" class="form-group row">
|
||||
<label for="issuable-title" class="col-form-label col-sm-2">{{ __('Title') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<gl-form-input
|
||||
id="issuable-title"
|
||||
v-model="issuableTitle"
|
||||
:autofocus="true"
|
||||
:placeholder="__('Title')"
|
||||
/>
|
||||
<gl-form-group :description="__('Maximum of 255 characters')">
|
||||
<gl-form-input
|
||||
id="issuable-title"
|
||||
v-model="issuableTitle"
|
||||
maxlength="255"
|
||||
:autofocus="true"
|
||||
:placeholder="__('Title')"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</div>
|
||||
</div>
|
||||
<div data-testid="issuable-description" class="form-group row">
|
||||
|
|
|
@ -188,7 +188,7 @@ export default {
|
|||
<form @submit.prevent="createWorkItem">
|
||||
<gl-alert v-if="error" variant="danger" @dismiss="error = null">{{ error }}</gl-alert>
|
||||
<div :class="{ 'gl-px-5': isModal }" data-testid="content">
|
||||
<item-title :title="title" data-testid="title-input" @title-input="handleTitleInput" />
|
||||
<item-title :title="initialTitle" data-testid="title-input" @title-input="handleTitleInput" />
|
||||
<div>
|
||||
<gl-loading-icon
|
||||
v-if="$apollo.queries.workItemTypes.loading"
|
||||
|
|
|
@ -7,7 +7,7 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
|
|||
}
|
||||
|
||||
.design-detail {
|
||||
background-color: rgba($black, 0.9);
|
||||
background-color: rgba($modal-backdrop-bg, $modal-backdrop-opacity);
|
||||
|
||||
.with-performance-bar & {
|
||||
top: 35px;
|
||||
|
|
|
@ -453,7 +453,7 @@ $border-radius-small: 2px;
|
|||
$border-radius-large: 8px;
|
||||
$default-icon-size: 16px;
|
||||
$layout-link-gray: #7e7c7c;
|
||||
$btn-side-margin: 10px;
|
||||
$btn-side-margin: $grid-size;
|
||||
$btn-sm-side-margin: 7px;
|
||||
$btn-margin-5: 5px;
|
||||
$count-arrow-border: #dce0e5;
|
||||
|
|
|
@ -191,7 +191,7 @@
|
|||
}
|
||||
|
||||
.board-card {
|
||||
background: var(--white, $white);
|
||||
background: var(--gray-10, $white);
|
||||
box-shadow: 0 1px 2px rgba(var(--black, $black), 0.1);
|
||||
line-height: $gl-padding;
|
||||
list-style: none;
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module WorkItems
|
||||
class DeleteTask < BaseMutation
|
||||
graphql_name 'WorkItemDeleteTask'
|
||||
|
||||
description "Deletes a task in a work item's description." \
|
||||
' Available only when feature flag `work_items` is enabled. This feature is experimental and' \
|
||||
' is subject to change without notice.'
|
||||
|
||||
authorize :update_work_item
|
||||
|
||||
argument :id, ::Types::GlobalIDType[::WorkItem],
|
||||
required: true,
|
||||
description: 'Global ID of the work item.'
|
||||
argument :lock_version, GraphQL::Types::Int,
|
||||
required: true,
|
||||
description: 'Current lock version of the work item containing the task in the description.'
|
||||
argument :task_data, ::Types::WorkItems::DeletedTaskInputType,
|
||||
required: true,
|
||||
description: 'Arguments necessary to delete a task from a work item\'s description.',
|
||||
prepare: ->(attributes, _ctx) { attributes.to_h }
|
||||
|
||||
field :work_item, Types::WorkItemType,
|
||||
null: true,
|
||||
description: 'Updated work item.'
|
||||
|
||||
def resolve(id:, lock_version:, task_data:)
|
||||
work_item = authorized_find!(id: id)
|
||||
task_data[:task] = authorized_find_task!(task_data[:id])
|
||||
|
||||
unless work_item.project.work_items_feature_flag_enabled?
|
||||
return { errors: ['`work_items` feature flag disabled for this project'] }
|
||||
end
|
||||
|
||||
result = ::WorkItems::DeleteTaskService.new(
|
||||
work_item: work_item,
|
||||
current_user: current_user,
|
||||
lock_version: lock_version,
|
||||
task_params: task_data
|
||||
).execute
|
||||
|
||||
response = { errors: result.errors }
|
||||
response[:work_item] = work_item if result.success?
|
||||
|
||||
response
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorized_find_task!(task_id)
|
||||
# TODO: Remove coercion when working on https://gitlab.com/gitlab-org/gitlab/-/issues/257883
|
||||
task_id = ::Types::GlobalIDType[::WorkItem].coerce_isolated_input(task_id)
|
||||
task = ::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(task_id))
|
||||
|
||||
if current_user.can?(:delete_work_item, task)
|
||||
task
|
||||
else
|
||||
# Fail early if user cannot delete task
|
||||
raise_resource_not_available_error!
|
||||
end
|
||||
end
|
||||
|
||||
# method used by `authorized_find!(id: id)`
|
||||
def find_object(id:)
|
||||
# TODO: Remove coercion when working on https://gitlab.com/gitlab-org/gitlab/-/issues/257883
|
||||
id = ::Types::GlobalIDType[::WorkItem].coerce_isolated_input(id)
|
||||
GitlabSchema.find_by_gid(id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -131,6 +131,7 @@ module Types
|
|||
mount_mutation Mutations::WorkItems::Create
|
||||
mount_mutation Mutations::WorkItems::CreateFromTask
|
||||
mount_mutation Mutations::WorkItems::Delete
|
||||
mount_mutation Mutations::WorkItems::DeleteTask
|
||||
mount_mutation Mutations::WorkItems::Update
|
||||
mount_mutation Mutations::SavedReplies::Create
|
||||
mount_mutation Mutations::SavedReplies::Update
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module WorkItems
|
||||
class DeletedTaskInputType < BaseInputObject
|
||||
graphql_name 'WorkItemDeletedTaskInput'
|
||||
|
||||
argument :id, ::Types::GlobalIDType[::WorkItem],
|
||||
required: true,
|
||||
description: 'Global ID of the task referenced in the work item\'s description.'
|
||||
argument :line_number_end, GraphQL::Types::Int,
|
||||
required: true,
|
||||
description: 'Last line in the Markdown source that defines the list item task.'
|
||||
argument :line_number_start, GraphQL::Types::Int,
|
||||
required: true,
|
||||
description: 'First line in the Markdown source that defines the list item task.'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -208,11 +208,11 @@ module MergeRequestsHelper
|
|||
|
||||
def how_merge_modal_data(merge_request)
|
||||
{
|
||||
is_fork: merge_request.for_fork?,
|
||||
can_merge: merge_request.can_be_merged_by?(current_user),
|
||||
is_fork: merge_request.for_fork?.to_s,
|
||||
can_merge: merge_request.can_be_merged_by?(current_user).to_s,
|
||||
source_branch: merge_request.source_branch,
|
||||
source_project: merge_request.source_project,
|
||||
source_project_full_path: merge_request.source_project&.full_path,
|
||||
source_project_default_url: merge_request.source_project && default_url_to_repo(merge_request.source_project),
|
||||
target_branch: merge_request.target_branch,
|
||||
reviewing_docs_path: help_page_path('user/project/merge_requests/reviews/index.md', anchor: "checkout-merge-requests-locally-through-the-head-ref")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module WorkItems
|
||||
class DeleteTaskService
|
||||
def initialize(work_item:, current_user: nil, task_params: {}, lock_version:)
|
||||
@work_item = work_item
|
||||
@current_user = current_user
|
||||
@task_params = task_params
|
||||
@lock_version = lock_version
|
||||
@task = task_params[:task]
|
||||
@errors = []
|
||||
end
|
||||
|
||||
def execute
|
||||
transaction_result = ::WorkItem.transaction do
|
||||
replacement_result = TaskListReferenceRemovalService.new(
|
||||
work_item: @work_item,
|
||||
task: @task,
|
||||
line_number_start: @task_params[:line_number_start],
|
||||
line_number_end: @task_params[:line_number_end],
|
||||
lock_version: @lock_version,
|
||||
current_user: @current_user
|
||||
).execute
|
||||
|
||||
break ::ServiceResponse.error(message: replacement_result.errors, http_status: 422) if replacement_result.error?
|
||||
|
||||
delete_result = ::WorkItems::DeleteService.new(
|
||||
project: @task.project,
|
||||
current_user: @current_user
|
||||
).execute(@task)
|
||||
|
||||
if delete_result.error?
|
||||
@errors += delete_result.errors
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
|
||||
delete_result
|
||||
end
|
||||
|
||||
return transaction_result if transaction_result
|
||||
|
||||
::ServiceResponse.error(message: @errors, http_status: 422)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,63 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module WorkItems
|
||||
class TaskListReferenceRemovalService
|
||||
STALE_OBJECT_MESSAGE = 'Stale work item. Check lock version'
|
||||
|
||||
def initialize(work_item:, task:, line_number_start:, line_number_end:, lock_version:, current_user:)
|
||||
@work_item = work_item
|
||||
@task = task
|
||||
@line_number_start = line_number_start
|
||||
@line_number_end = line_number_end
|
||||
@lock_version = lock_version
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
def execute
|
||||
return ::ServiceResponse.error(message: 'line_number_start must be greater than 0') if @line_number_start < 1
|
||||
return ::ServiceResponse.error(message: "Work item description can't be blank") if @work_item.description.blank?
|
||||
|
||||
if @line_number_end < @line_number_start
|
||||
return ::ServiceResponse.error(message: 'line_number_end must be greater or equal to line_number_start')
|
||||
end
|
||||
|
||||
source_lines = @work_item.description.split("\n")
|
||||
|
||||
line_matches_reference = (@line_number_start..@line_number_end).any? do |line_number|
|
||||
markdown_line = source_lines[line_number - 1]
|
||||
|
||||
/#{Regexp.escape(@task.to_reference)}(?!\d)/.match?(markdown_line)
|
||||
end
|
||||
|
||||
unless line_matches_reference
|
||||
return ::ServiceResponse.error(
|
||||
message: "Unable to detect a task on lines #{@line_number_start}-#{@line_number_end}"
|
||||
)
|
||||
end
|
||||
|
||||
remove_task_lines!(source_lines)
|
||||
|
||||
::WorkItems::UpdateService.new(
|
||||
project: @work_item.project,
|
||||
current_user: @current_user,
|
||||
params: { description: source_lines.join("\n"), lock_version: @lock_version }
|
||||
).execute(@work_item)
|
||||
|
||||
if @work_item.valid?
|
||||
::ServiceResponse.success
|
||||
else
|
||||
::ServiceResponse.error(message: @work_item.errors.full_messages)
|
||||
end
|
||||
rescue ActiveRecord::StaleObjectError
|
||||
::ServiceResponse.error(message: STALE_OBJECT_MESSAGE)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def remove_task_lines!(source_lines)
|
||||
source_lines.delete_if.each_with_index do |_line, index|
|
||||
index >= @line_number_start - 1 && index < @line_number_end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,8 +1,8 @@
|
|||
- name: "Permissions change for downloading Composer dependencies"
|
||||
announcement_milestone: "14.9"
|
||||
announcement_date: "2022-03-22"
|
||||
removal_milestone: "15.0"
|
||||
removal_date: "2022-05-22"
|
||||
removal_milestone: "14.10"
|
||||
removal_date: "2022-04-22"
|
||||
breaking_change: true
|
||||
reporter: trizzi
|
||||
body: | # Do not modify this line, instead modify the lines below.
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
- name: "Permissions change for downloading Composer dependencies"
|
||||
announcement_milestone: "14.9"
|
||||
announcement_date: "2022-03-22"
|
||||
removal_milestone: "14.10"
|
||||
removal_date: "2022-04-22"
|
||||
breaking_change: true
|
||||
reporter: trizzi
|
||||
body: | # Do not modify this line, instead modify the lines below.
|
||||
The GitLab Composer repository can be used to push, search, fetch metadata about, and download PHP dependencies. All these actions require authentication, except for downloading dependencies.
|
||||
|
||||
Downloading Composer dependencies without authentication is deprecated in GitLab 14.9, and will be removed in GitLab 15.0. Starting with GitLab 15.0, you must authenticate to download Composer dependencies.
|
||||
stage: package
|
|
@ -2281,7 +2281,7 @@ The following tables and diagram detail the hybrid environment using the same fo
|
|||
as the normal environment above.
|
||||
|
||||
First are the components that run in Kubernetes. The recommendation at this time is to
|
||||
use Google Cloud's Kubernetes Engine (GKE) and associated machine types, but the memory
|
||||
use Google Cloud's Kubernetes Engine (GKE) or AWS Elastic Kubernetes Service (EKS) and associated machine types, but the memory
|
||||
and CPU requirements should translate to most other providers. We hope to update this in the
|
||||
future with further specific cloud provider details.
|
||||
|
||||
|
|
|
@ -2279,7 +2279,7 @@ The following tables and diagram detail the hybrid environment using the same fo
|
|||
as the normal environment above.
|
||||
|
||||
First are the components that run in Kubernetes. The recommendation at this time is to
|
||||
use Google Cloud's Kubernetes Engine (GKE) and associated machine types, but the memory
|
||||
use Google Cloud's Kubernetes Engine (GKE) or AWS Elastic Kubernetes Service (EKS) and associated machine types, but the memory
|
||||
and CPU requirements should translate to most other providers. We hope to update this in the
|
||||
future with further specific cloud provider details.
|
||||
|
||||
|
|
|
@ -1000,7 +1000,7 @@ The following tables and diagram detail the hybrid environment using the same fo
|
|||
as the normal environment above.
|
||||
|
||||
First are the components that run in Kubernetes. The recommendation at this time is to
|
||||
use Google Cloud's Kubernetes Engine (GKE) and associated machine types, but the memory
|
||||
use Google Cloud's Kubernetes Engine (GKE) or AWS Elastic Kubernetes Service (EKS) and associated machine types, but the memory
|
||||
and CPU requirements should translate to most other providers. We hope to update this in the
|
||||
future with further specific cloud provider details.
|
||||
|
||||
|
|
|
@ -2239,7 +2239,7 @@ The following tables and diagram detail the hybrid environment using the same fo
|
|||
as the normal environment above.
|
||||
|
||||
First are the components that run in Kubernetes. The recommendation at this time is to
|
||||
use Google Cloud's Kubernetes Engine (GKE) and associated machine types, but the memory
|
||||
use Google Cloud's Kubernetes Engine (GKE) or AWS Elastic Kubernetes Service (EKS) and associated machine types, but the memory
|
||||
and CPU requirements should translate to most other providers. We hope to update this in the
|
||||
future with further specific cloud provider details.
|
||||
|
||||
|
|
|
@ -2295,7 +2295,7 @@ The following tables and diagram detail the hybrid environment using the same fo
|
|||
as the normal environment above.
|
||||
|
||||
First are the components that run in Kubernetes. The recommendation at this time is to
|
||||
use Google Cloud's Kubernetes Engine (GKE) and associated machine types, but the memory
|
||||
use Google Cloud's Kubernetes Engine (GKE) or AWS Elastic Kubernetes Service (EKS) and associated machine types, but the memory
|
||||
and CPU requirements should translate to most other providers. We hope to update this in the
|
||||
future with further specific cloud provider details.
|
||||
|
||||
|
|
|
@ -2214,7 +2214,7 @@ The following tables and diagram detail the hybrid environment using the same fo
|
|||
as the normal environment above.
|
||||
|
||||
First are the components that run in Kubernetes. The recommendation at this time is to
|
||||
use Google Cloud's Kubernetes Engine (GKE) and associated machine types, but the memory
|
||||
use Google Cloud's Kubernetes Engine (GKE) or AWS Elastic Kubernetes Service (EKS) and associated machine types, but the memory
|
||||
and CPU requirements should translate to most other providers. We hope to update this in the
|
||||
future with further specific cloud provider details.
|
||||
|
||||
|
|
|
@ -5371,6 +5371,29 @@ Input type: `WorkItemDeleteInput`
|
|||
| <a id="mutationworkitemdeleteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationworkitemdeleteproject"></a>`project` | [`Project`](#project) | Project the deleted work item belonged to. |
|
||||
|
||||
### `Mutation.workItemDeleteTask`
|
||||
|
||||
Deletes a task in a work item's description. Available only when feature flag `work_items` is enabled. This feature is experimental and is subject to change without notice.
|
||||
|
||||
Input type: `WorkItemDeleteTaskInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationworkitemdeletetaskclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationworkitemdeletetaskid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
|
||||
| <a id="mutationworkitemdeletetasklockversion"></a>`lockVersion` | [`Int!`](#int) | Current lock version of the work item containing the task in the description. |
|
||||
| <a id="mutationworkitemdeletetasktaskdata"></a>`taskData` | [`WorkItemDeletedTaskInput!`](#workitemdeletedtaskinput) | Arguments necessary to delete a task from a work item's description. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationworkitemdeletetaskclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationworkitemdeletetaskerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationworkitemdeletetaskworkitem"></a>`workItem` | [`WorkItem`](#workitem) | Updated work item. |
|
||||
|
||||
### `Mutation.workItemUpdate`
|
||||
|
||||
Updates a work item by Global ID. Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice.
|
||||
|
@ -21033,3 +21056,13 @@ A time-frame defined as a closed inclusive range of two dates.
|
|||
| <a id="workitemconverttaskinputlockversion"></a>`lockVersion` | [`Int!`](#int) | Current lock version of the work item containing the task in the description. |
|
||||
| <a id="workitemconverttaskinputtitle"></a>`title` | [`String!`](#string) | Full string of the task to be replaced. New title for the created work item. |
|
||||
| <a id="workitemconverttaskinputworkitemtypeid"></a>`workItemTypeId` | [`WorkItemsTypeID!`](#workitemstypeid) | Global ID of the work item type used to create the new work item. |
|
||||
|
||||
### `WorkItemDeletedTaskInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="workitemdeletedtaskinputid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the task referenced in the work item's description. |
|
||||
| <a id="workitemdeletedtaskinputlinenumberend"></a>`lineNumberEnd` | [`Int!`](#int) | Last line in the Markdown source that defines the list item task. |
|
||||
| <a id="workitemdeletedtaskinputlinenumberstart"></a>`lineNumberStart` | [`Int!`](#int) | First line in the Markdown source that defines the list item task. |
|
||||
|
|
|
@ -202,7 +202,7 @@ The [`custom_hooks_dir`](https://docs.gitlab.com/ee/administration/server_hooks.
|
|||
### Permissions change for downloading Composer dependencies
|
||||
|
||||
WARNING:
|
||||
This feature will be changed or removed in 15.0
|
||||
This feature will be changed or removed in 14.10
|
||||
as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes).
|
||||
Before updating GitLab, review the details carefully to determine if you need to make any
|
||||
changes to your code, settings, or workflow.
|
||||
|
@ -211,7 +211,7 @@ The GitLab Composer repository can be used to push, search, fetch metadata about
|
|||
|
||||
Downloading Composer dependencies without authentication is deprecated in GitLab 14.9, and will be removed in GitLab 15.0. Starting with GitLab 15.0, you must authenticate to download Composer dependencies.
|
||||
|
||||
**Planned removal milestone: 15.0 (2022-05-22)**
|
||||
**Planned removal milestone: 14.10 (2022-04-22)**
|
||||
|
||||
### htpasswd Authentication for the Container Registry
|
||||
|
||||
|
|
|
@ -173,6 +173,20 @@ The new security approvals feature is similar to vulnerability check. For exampl
|
|||
- A two-step approval process can be enforced for any desired changes to security approval rules.
|
||||
- A single set of security policies can be applied to multiple development projects to allow for ease in maintaining a single, centralized ruleset.
|
||||
|
||||
## 14.10
|
||||
|
||||
### Permissions change for downloading Composer dependencies
|
||||
|
||||
WARNING:
|
||||
This feature was changed or removed in 14.10
|
||||
as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes).
|
||||
Before updating GitLab, review the details carefully to determine if you need to make any
|
||||
changes to your code, settings, or workflow.
|
||||
|
||||
The GitLab Composer repository can be used to push, search, fetch metadata about, and download PHP dependencies. All these actions require authentication, except for downloading dependencies.
|
||||
|
||||
Downloading Composer dependencies without authentication is deprecated in GitLab 14.9, and will be removed in GitLab 15.0. Starting with GitLab 15.0, you must authenticate to download Composer dependencies.
|
||||
|
||||
## 14.9
|
||||
|
||||
### Integrated error tracking disabled by default
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 20 KiB |
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
|
@ -13,7 +13,7 @@ Value stream analytics provides metrics about each stage of your software develo
|
|||
|
||||
A **value stream** is the entire work process that delivers value to customers. For example,
|
||||
the [DevOps lifecycle](https://about.gitlab.com/stages-devops-lifecycle/) is a value stream that starts
|
||||
with the "manage" and ends with the "protect" stage.
|
||||
with the "manage" stage and ends with the "protect" stage.
|
||||
|
||||
Use value stream analytics to identify:
|
||||
|
||||
|
@ -34,7 +34,7 @@ Value stream analytics is also available for [projects](../../analytics/value_st
|
|||
Prerequisite:
|
||||
|
||||
- You must have at least the Reporter role to view value stream analytics for groups.
|
||||
- You must create a [custom value stream](#custom-value-streams). Value stream analytics only shows custom value streams created for your group.
|
||||
- You must create a [custom value stream](#create-a-value-stream-with-gitlab-default-stages). Value stream analytics only shows custom value streams created for your group.
|
||||
|
||||
To view value stream analytics for your group:
|
||||
|
||||
|
@ -95,7 +95,10 @@ To view the median time spent in each stage by a group:
|
|||
Value stream analytics shows the lead time and cycle time for issues in your groups:
|
||||
|
||||
- Lead time: Median time from when the issue was created to when it was closed.
|
||||
- Cycle time: Median time from first commit to issue closed. GitLab measures cycle time from the earliest commit of a [linked issue's merge request](../../project/issues/crosslinking_issues.md#from-commit-messages) to when that issue is closed. The cycle time approach underestimates the lead time because merge request creation is always later than commit time.
|
||||
- Cycle time: Median time from first commit to issue closed. GitLab measures cycle time from the earliest
|
||||
commit of a [linked issue's merge request](../../project/issues/crosslinking_issues.md#from-commit-messages)
|
||||
to when that issue is closed. The cycle time approach underestimates the lead time because merge request creation
|
||||
is always later than commit time.
|
||||
|
||||
To view the lead time and cycle time for issues:
|
||||
|
||||
|
@ -185,7 +188,7 @@ To preview this functionality, you can use the **Filter by stop date** toggle to
|
|||
|
||||
If you turn on the **Filter by stop date** toggle, the results show items with a stop event within the date range. When this function is enabled, it may take up to 10 minutes for results to show due to data aggregation. There are occasions when it may take longer than 10 minutes for results to display:
|
||||
|
||||
- If this is the first time you are viewing value stream analytics and have not yet [created a value stream](#create-a-value-stream).
|
||||
- If this is the first time you are viewing value stream analytics and have not yet [created a value stream](#create-a-value-stream-with-gitlab-default-stages).
|
||||
- If the group hierarchy has been re-arranged.
|
||||
- If there have been bulk updates on issues and merge requests.
|
||||
|
||||
|
@ -265,80 +268,58 @@ These patterns are not case-sensitive.
|
|||
|
||||
You can change the name of a project environment in your GitLab CI/CD configuration.
|
||||
|
||||
## Custom value streams
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12196) in GitLab 12.9.
|
||||
|
||||
Use custom value streams to create custom stages that align with your own development processes,
|
||||
and hide default stages. The dashboard depicts stages as a horizontal process
|
||||
flow.
|
||||
|
||||
### Create a value stream
|
||||
## Create a value stream with GitLab default stages
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/221202) in GitLab 13.3
|
||||
|
||||
A default value stream is readily available for each group. You can create additional value streams
|
||||
based on the different areas of work that you would like to measure.
|
||||
|
||||
Once created, a new value stream includes the stages that follow
|
||||
[GitLab workflow](../../../topics/gitlab_flow.md)
|
||||
best practices. You can customize this flow by adding, hiding or re-ordering stages.
|
||||
|
||||
To create a value stream:
|
||||
When you create a value stream, you can use GitLab default stages and hide or re-order them to customize. You can also
|
||||
create custom stages in addition to those provided in the default template.
|
||||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Analytics > Value Stream**.
|
||||
1. If this is the first time you are creating a value stream, select **Create custom value stream**. Otherwise, in the top right, select the dropdown list and then **Create new Value Stream**.
|
||||
1. Enter a name for the new Value Stream.
|
||||
- You can [customize the stages](#create-a-value-stream-with-stages).
|
||||
1. Select **Create Value Stream**.
|
||||
|
||||
![New value stream](img/new_value_stream_v13_12.png "Creating a new value stream")
|
||||
1. Select **Create new Value Stream**.
|
||||
1. Enter a name for the value stream.
|
||||
1. Select **Create from default template**.
|
||||
1. Customize the default stages:
|
||||
- To re-oder stages, select the up or down arrows.
|
||||
- To hide a stage, select **Hide** ({**eye-slash**}).
|
||||
1. To add a custom stage, select **Add another stage**.
|
||||
- Enter a name for the stage.
|
||||
- Select a **Start event** and a **Stop event**.
|
||||
1. Select **Create value stream**.
|
||||
|
||||
NOTE:
|
||||
If you have recently upgraded to GitLab Premium, it can take up to 30 minutes for data to collect and display.
|
||||
|
||||
### Create a value stream with stages
|
||||
## Create a value stream with custom stages
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50229) in GitLab 13.7.
|
||||
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55572) in GitLab 13.10.
|
||||
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/294190) in GitLab 13.11.
|
||||
|
||||
You can create value streams with stages, starting with a default or a blank template. You can
|
||||
add stages as desired.
|
||||
|
||||
To create a value stream with stages:
|
||||
When you create a value stream, you can create and add custom stages that align with your own development workflows.
|
||||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Analytics > Value Stream**.
|
||||
1. If this is the first time you are creating a value stream, select **Create custom value stream**. Otherwise, in the top right, select the dropdown list and then **Create new Value Stream**.
|
||||
1. Select either **Create from default template** or **Create from no template**.
|
||||
- You can hide or re-order default stages in the value stream.
|
||||
1. Select **Create value stream**.
|
||||
1. For each stage:
|
||||
- Enter a name for the stage.
|
||||
- Select a **Start event** and a **Stop event**.
|
||||
1. To add another stage, select **Add another stage**.
|
||||
1. To re-order the stages, select the up or down arrows.
|
||||
1. Select **Create value stream**.
|
||||
|
||||
![Default stage actions](img/vsa_default_stage_v13_10.png "Default stage actions")
|
||||
### Label-based stages for custom value streams
|
||||
|
||||
- You can add new stages by selecting **Add another stage**.
|
||||
- You can select the name and start and end events for the stage.
|
||||
|
||||
![Custom stage actions](img/vsa_custom_stage_v13_10.png "Custom stage actions")
|
||||
1. Select **Create Value Stream**.
|
||||
|
||||
### Label-based stages
|
||||
|
||||
The pre-defined start and end events can cover many use cases involving both issues and merge requests.
|
||||
|
||||
In more complex workflows, use stages based on group labels. These events are based on
|
||||
added or removed labels. In particular, [scoped labels](../../project/labels.md#scoped-labels)
|
||||
are useful for complex workflows.
|
||||
|
||||
In this example, we'd like to measure times for deployment from a staging environment to production. The workflow is the following:
|
||||
To measure complex workflows, you can use [scoped labels](../../project/labels.md#scoped-labels). For example, to measure deployment
|
||||
time from a staging environment to production, you could use the following labels:
|
||||
|
||||
- When the code is deployed to staging, the `workflow::staging` label is added to the merge request.
|
||||
- When the code is deployed to production, the `workflow::production` label is added to the merge request.
|
||||
|
||||
![Label-based value stream analytics stage](img/vsa_label_based_stage_v14_0.png "Creating a label-based value stream analytics stage")
|
||||
|
||||
### Edit a value stream
|
||||
## Edit a value stream
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/267537) in GitLab 13.10.
|
||||
|
||||
|
@ -346,19 +327,18 @@ After you create a value stream, you can customize it to suit your purposes. To
|
|||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Analytics > Value Stream**.
|
||||
1. In the top right, select the dropdown list and then select the relevant value stream.
|
||||
1. In the top right, select the dropdown list, and select a value stream.
|
||||
1. Next to the value stream dropdown list, select **Edit**.
|
||||
The edit form is populated with the value stream details.
|
||||
1. Optional:
|
||||
- Rename the value stream.
|
||||
- Hide or re-order default stages.
|
||||
- Remove existing custom stages.
|
||||
- Add new stages by selecting the 'Add another stage' button
|
||||
- To add new stages, select **Add another stage**.
|
||||
- Select the start and end events for the stage.
|
||||
1. Optional. To undo any modifications, select **Restore value stream defaults**.
|
||||
1. Select **Save Value Stream**.
|
||||
|
||||
### Delete a value stream
|
||||
## Delete a value stream
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/221205) in GitLab 13.4.
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
This directory contains the GitLab Flavored Markdown (GLFM) specification.
|
||||
|
||||
See the GitLab Flavored Markdown Specification Guide developer documentation for more information:
|
||||
|
||||
https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#specification-files
|
|
@ -0,0 +1,23 @@
|
|||
# First GitLab-Specific Section with Examples
|
||||
|
||||
## Strong but with two asterisks
|
||||
|
||||
```````````````````````````````` example
|
||||
**bold**
|
||||
.
|
||||
<p><strong>bold</strong></p>
|
||||
````````````````````````````````
|
||||
|
||||
# Second GitLab-Specific Section with Examples
|
||||
|
||||
## Strong but with HTML
|
||||
|
||||
```````````````````````````````` example
|
||||
<strong>
|
||||
bold
|
||||
</strong>
|
||||
.
|
||||
<p><strong>
|
||||
bold
|
||||
</strong></p>
|
||||
````````````````````````````````
|
|
@ -0,0 +1,3 @@
|
|||
# Introduction
|
||||
|
||||
TODO: Write a GitLab-specific version of the GitHub Flavored Markdown intro section.
|
File diff suppressed because it is too large
Load Diff
|
@ -212,7 +212,7 @@ module API
|
|||
requires :id, type: Integer, desc: %q(Job's ID)
|
||||
optional :token, type: String, desc: %q(Job's authentication token)
|
||||
end
|
||||
patch '/:id/trace', urgency: :default, feature_category: :continuous_integration do
|
||||
patch '/:id/trace', urgency: :low, feature_category: :continuous_integration do
|
||||
job = authenticate_job!(heartbeat_runner: true)
|
||||
|
||||
error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range')
|
||||
|
|
|
@ -23718,6 +23718,9 @@ msgstr ""
|
|||
msgid "Maximum number of unique IP addresses per user."
|
||||
msgstr ""
|
||||
|
||||
msgid "Maximum of 255 characters"
|
||||
msgstr ""
|
||||
|
||||
msgid "Maximum page reached"
|
||||
msgstr ""
|
||||
|
||||
|
@ -35593,6 +35596,9 @@ msgstr ""
|
|||
msgid "Snippets|Optionally add a description about what your snippet does or how to use it…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Snippets|Snippets can't contain empty files. Ensure all files have content, or delete them."
|
||||
msgstr ""
|
||||
|
||||
msgid "Snowplow"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
"@gitlab/at.js": "1.5.7",
|
||||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/svgs": "2.11.0",
|
||||
"@gitlab/ui": "39.4.0",
|
||||
"@gitlab/ui": "39.6.0",
|
||||
"@gitlab/visual-review-tools": "1.7.0",
|
||||
"@rails/actioncable": "6.1.4-7",
|
||||
"@rails/ujs": "6.1.4-7",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../lib/glfm/update_specification'
|
||||
Glfm::UpdateSpecification.new.process
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Glfm
|
||||
module Constants
|
||||
# Root dir containing all specification files
|
||||
specification_path = Pathname.new(File.expand_path("../../../glfm_specification", __dir__))
|
||||
|
||||
# GitHub Flavored Markdown specification file
|
||||
GHFM_SPEC_TXT_URI = 'https://raw.githubusercontent.com/github/cmark-gfm/master/test/spec.txt'
|
||||
GHFM_SPEC_VERSION = '0.29'
|
||||
GHFM_SPEC_TXT_FILENAME = "ghfm_spec_v_#{GHFM_SPEC_VERSION}.txt"
|
||||
GHFM_SPEC_TXT_PATH = specification_path.join('input/github_flavored_markdown', GHFM_SPEC_TXT_FILENAME)
|
||||
|
||||
# GitLab Flavored Markdown specification files
|
||||
specification_input_glfm_path = specification_path.join('input/gitlab_flavored_markdown')
|
||||
GLFM_INTRO_TXT_PATH = specification_input_glfm_path.join('glfm_intro.txt')
|
||||
GLFM_EXAMPLES_TXT_PATH = specification_input_glfm_path.join('glfm_canonical_examples.txt')
|
||||
GLFM_SPEC_TXT_PATH = specification_path.join('output/spec.txt')
|
||||
|
||||
# Other constants used for processing files
|
||||
GLFM_SPEC_TXT_HEADER = <<~GLFM_SPEC_TXT_HEADER
|
||||
---
|
||||
title: GitLab Flavored Markdown (GLFM) Spec
|
||||
version: alpha
|
||||
...
|
||||
GLFM_SPEC_TXT_HEADER
|
||||
INTRODUCTION_HEADER_LINE_TEXT = /\A# Introduction\Z/.freeze
|
||||
END_TESTS_COMMENT_LINE_TEXT = /\A<!-- END TESTS -->\Z/.freeze
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
require 'fileutils'
|
||||
|
||||
module Glfm
|
||||
module Shared
|
||||
def write_file(file_path, file_content_string)
|
||||
FileUtils.mkdir_p(File.dirname(file_path))
|
||||
# NOTE: We don't use the block form of File.open because we want to be able to easily
|
||||
# mock it for testing.
|
||||
io = File.open(file_path, 'w')
|
||||
io.binmode
|
||||
io.write(file_content_string)
|
||||
# NOTE: We are using #fsync + #close_write instead of just #close`, in order to unit test
|
||||
# with a real StringIO and not just a mock object.
|
||||
io.fsync
|
||||
io.close_write
|
||||
end
|
||||
|
||||
# All script output goes through this method. This makes it easy to mock in order to prevent
|
||||
# output from being printed to the console during tests. We don't want to directly mock
|
||||
# Kernel#puts, because that could interfere or cause spurious test failures when #puts is used
|
||||
# for debugging. And for some reason RuboCop says `rubocop:disable Rails/Output` would be
|
||||
# redundant here, so not including it.
|
||||
def output(string)
|
||||
puts string
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,105 @@
|
|||
# frozen_string_literal: true
|
||||
require 'fileutils'
|
||||
require 'open-uri'
|
||||
require 'pathname'
|
||||
require_relative 'constants'
|
||||
require_relative 'shared'
|
||||
|
||||
module Glfm
|
||||
class UpdateSpecification
|
||||
include Constants
|
||||
include Shared
|
||||
|
||||
def process
|
||||
output('Updating specification...')
|
||||
ghfm_spec_txt_lines = download_and_write_ghfm_spec_txt
|
||||
glfm_spec_txt_string = build_glfm_spec_txt(ghfm_spec_txt_lines)
|
||||
write_glfm_spec_txt(glfm_spec_txt_string)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def download_and_write_ghfm_spec_txt
|
||||
output("Downloading #{GHFM_SPEC_TXT_URI}...")
|
||||
ghfm_spec_txt_uri_io = URI.open(GHFM_SPEC_TXT_URI)
|
||||
|
||||
# Read IO stream into an array of lines for easy processing later
|
||||
ghfm_spec_txt_lines = ghfm_spec_txt_uri_io.readlines
|
||||
raise "Unable to read lines from #{GHFM_SPEC_TXT_URI}" if ghfm_spec_txt_lines.empty?
|
||||
|
||||
# Make sure the GHFM spec version has not changed
|
||||
validate_expected_spec_version!(ghfm_spec_txt_lines[2])
|
||||
|
||||
# Reset IO stream and re-read into a single string for easy writing
|
||||
# noinspection RubyNilAnalysis
|
||||
ghfm_spec_txt_uri_io.seek(0)
|
||||
ghfm_spec_txt_string = ghfm_spec_txt_uri_io.read
|
||||
raise "Unable to read string from #{GHFM_SPEC_TXT_URI}" unless ghfm_spec_txt_string
|
||||
|
||||
output("Writing #{GHFM_SPEC_TXT_PATH}...")
|
||||
GHFM_SPEC_TXT_PATH.dirname.mkpath
|
||||
write_file(GHFM_SPEC_TXT_PATH, ghfm_spec_txt_string)
|
||||
|
||||
ghfm_spec_txt_lines
|
||||
end
|
||||
|
||||
def validate_expected_spec_version!(version_line)
|
||||
return if version_line =~ /\Aversion: #{GHFM_SPEC_VERSION}\Z/o
|
||||
|
||||
raise "GitHub Flavored Markdown spec.txt version mismatch! " \
|
||||
"Expected 'version: #{GHFM_SPEC_VERSION}', got '#{version_line}'"
|
||||
end
|
||||
|
||||
def build_glfm_spec_txt(ghfm_spec_txt_lines)
|
||||
glfm_spec_txt_lines = ghfm_spec_txt_lines.dup
|
||||
replace_header(glfm_spec_txt_lines)
|
||||
replace_intro_section(glfm_spec_txt_lines)
|
||||
insert_examples_txt(glfm_spec_txt_lines)
|
||||
glfm_spec_txt_lines.join('')
|
||||
end
|
||||
|
||||
def replace_header(spec_txt_lines)
|
||||
spec_txt_lines[0, spec_txt_lines.index("...\n") + 1] = GLFM_SPEC_TXT_HEADER
|
||||
end
|
||||
|
||||
def replace_intro_section(spec_txt_lines)
|
||||
glfm_intro_txt_lines = File.open(GLFM_INTRO_TXT_PATH).readlines
|
||||
raise "Unable to read lines from #{GLFM_INTRO_TXT_PATH}" if glfm_intro_txt_lines.empty?
|
||||
|
||||
ghfm_intro_header_begin_index = spec_txt_lines.index do |line|
|
||||
line =~ INTRODUCTION_HEADER_LINE_TEXT
|
||||
end
|
||||
raise "Unable to locate introduction header line in #{GHFM_SPEC_TXT_PATH}" if ghfm_intro_header_begin_index.nil?
|
||||
|
||||
# Find the index of the next header after the introduction header, starting from the index
|
||||
# of the introduction header this is the length of the intro section
|
||||
ghfm_intro_section_length = spec_txt_lines[ghfm_intro_header_begin_index + 1..].index do |line|
|
||||
line.start_with?('# ')
|
||||
end
|
||||
|
||||
# Replace the intro section with the GitLab flavored Markdown intro section
|
||||
spec_txt_lines[ghfm_intro_header_begin_index, ghfm_intro_section_length] = glfm_intro_txt_lines
|
||||
end
|
||||
|
||||
def insert_examples_txt(spec_txt_lines)
|
||||
glfm_examples_txt_lines = File.open(GLFM_EXAMPLES_TXT_PATH).readlines
|
||||
raise "Unable to read lines from #{GLFM_EXAMPLES_TXT_PATH}" if glfm_examples_txt_lines.empty?
|
||||
|
||||
ghfm_end_tests_comment_index = spec_txt_lines.index do |line|
|
||||
line =~ END_TESTS_COMMENT_LINE_TEXT
|
||||
end
|
||||
raise "Unable to locate 'END TESTS' comment line in #{GHFM_SPEC_TXT_PATH}" if ghfm_end_tests_comment_index.nil?
|
||||
|
||||
# Insert the GLFM examples before the 'END TESTS' comment line
|
||||
spec_txt_lines[ghfm_end_tests_comment_index - 1] = ["\n", glfm_examples_txt_lines, "\n"].flatten
|
||||
|
||||
spec_txt_lines
|
||||
end
|
||||
|
||||
def write_glfm_spec_txt(glfm_spec_txt_string)
|
||||
output("Writing #{GLFM_SPEC_TXT_PATH}...")
|
||||
FileUtils.mkdir_p(Pathname.new(GLFM_SPEC_TXT_PATH).dirname)
|
||||
write_file(GLFM_SPEC_TXT_PATH, glfm_spec_txt_string)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Merge request > User opens checkout branch modal', :js do
|
||||
include ProjectForksHelper
|
||||
|
||||
let(:project) { create(:project, :public, :repository) }
|
||||
let(:user) { project.creator }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
describe 'for fork' do
|
||||
let(:author) { create(:user) }
|
||||
let(:source_project) { fork_project(project, author, repository: true) }
|
||||
|
||||
let(:merge_request) do
|
||||
create(:merge_request,
|
||||
source_project: source_project,
|
||||
target_project: project,
|
||||
source_branch: 'fix',
|
||||
target_branch: 'master',
|
||||
author: author,
|
||||
allow_collaboration: true)
|
||||
end
|
||||
|
||||
it 'shows instructions' do
|
||||
visit project_merge_request_path(project, merge_request)
|
||||
|
||||
click_button 'Code'
|
||||
click_button 'Check out branch'
|
||||
|
||||
expect(page).to have_content(source_project.http_url_to_repo)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -85,13 +85,4 @@ RSpec.describe 'Projects > Snippets > Create Snippet', :js do
|
|||
expect(page).to have_content('New Snippet')
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not allow submitting the form without title and content' do
|
||||
snippet_fill_in_title(title)
|
||||
|
||||
expect(page).not_to have_button('Create snippet')
|
||||
|
||||
snippet_fill_in_form(title: title, content: file_content)
|
||||
expect(page).to have_button('Create snippet')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -109,13 +109,22 @@ RSpec.describe 'User creates snippet', :js do
|
|||
end
|
||||
end
|
||||
|
||||
it 'validation fails for the first time' do
|
||||
fill_in snippet_title_field, with: title
|
||||
it 'shows validation errors' do
|
||||
title_validation_message = _("This field is required.")
|
||||
files_validation_message = _("Snippets can't contain empty files. Ensure all files have content, or delete them.")
|
||||
|
||||
expect(page).not_to have_button('Create snippet')
|
||||
click_button('Create snippet')
|
||||
|
||||
expect(page).to have_content(title_validation_message)
|
||||
expect(page).to have_content(files_validation_message)
|
||||
|
||||
snippet_fill_in_title(title)
|
||||
|
||||
expect(page).not_to have_content(title_validation_message)
|
||||
|
||||
snippet_fill_in_form(title: title, content: file_content)
|
||||
expect(page).to have_button('Create snippet')
|
||||
|
||||
expect(page).not_to have_content(files_validation_message)
|
||||
end
|
||||
|
||||
it 'previews a snippet with file' do
|
||||
|
|
|
@ -465,6 +465,14 @@ describe('Issuable output', () => {
|
|||
expect(findStickyHeader().text()).toContain('Sticky header title');
|
||||
});
|
||||
|
||||
it('shows with title for an epic', async () => {
|
||||
wrapper.setProps({ issuableType: 'epic' });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findStickyHeader().text()).toContain('Sticky header title');
|
||||
});
|
||||
|
||||
it.each`
|
||||
title | state
|
||||
${'shows with Open when status is opened'} | ${IssuableStatus.Open}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlFormGroup, GlLoadingIcon } from '@gitlab/ui';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import { merge } from 'lodash';
|
||||
|
||||
|
@ -7,6 +6,7 @@ import VueApollo, { ApolloMutation } from 'vue-apollo';
|
|||
import { useFakeDate } from 'helpers/fake_date';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import GetSnippetQuery from 'shared_queries/snippet/snippet.query.graphql';
|
||||
import createFlash from '~/flash';
|
||||
import * as urlUtils from '~/lib/utils/url_utility';
|
||||
|
@ -22,7 +22,6 @@ import {
|
|||
import CreateSnippetMutation from '~/snippets/mutations/create_snippet.mutation.graphql';
|
||||
import UpdateSnippetMutation from '~/snippets/mutations/update_snippet.mutation.graphql';
|
||||
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
|
||||
import TitleField from '~/vue_shared/components/form/title.vue';
|
||||
import { testEntries, createGQLSnippetsQueryResponse, createGQLSnippet } from '../test_utils';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
@ -112,19 +111,19 @@ describe('Snippet Edit app', () => {
|
|||
gon.relative_url_root = originalRelativeUrlRoot;
|
||||
});
|
||||
|
||||
const findBlobActions = () => wrapper.find(SnippetBlobActionsEdit);
|
||||
const findSubmitButton = () => wrapper.find('[data-testid="snippet-submit-btn"]');
|
||||
const findCancelButton = () => wrapper.find('[data-testid="snippet-cancel-btn"]');
|
||||
const hasDisabledSubmit = () => Boolean(findSubmitButton().attributes('disabled'));
|
||||
const clickSubmitBtn = () => wrapper.find('[data-testid="snippet-edit-form"]').trigger('submit');
|
||||
const findBlobActions = () => wrapper.findComponent(SnippetBlobActionsEdit);
|
||||
const findCancelButton = () => wrapper.findByTestId('snippet-cancel-btn');
|
||||
const clickSubmitBtn = () => wrapper.findByTestId('snippet-edit-form').trigger('submit');
|
||||
|
||||
const triggerBlobActions = (actions) => findBlobActions().vm.$emit('actions', actions);
|
||||
const setUploadFilesHtml = (paths) => {
|
||||
wrapper.vm.$el.innerHTML = paths
|
||||
.map((path) => `<input name="files[]" value="${path}">`)
|
||||
.join('');
|
||||
};
|
||||
const setTitle = (val) => wrapper.find(TitleField).vm.$emit('input', val);
|
||||
const setDescription = (val) => wrapper.find(SnippetDescriptionEdit).vm.$emit('input', val);
|
||||
const setTitle = (val) => wrapper.findByTestId('snippet-title-input').vm.$emit('input', val);
|
||||
const setDescription = (val) =>
|
||||
wrapper.findComponent(SnippetDescriptionEdit).vm.$emit('input', val);
|
||||
|
||||
const createComponent = ({ props = {}, selectedLevel = SNIPPET_VISIBILITY_PRIVATE } = {}) => {
|
||||
if (wrapper) {
|
||||
|
@ -139,7 +138,7 @@ describe('Snippet Edit app', () => {
|
|||
];
|
||||
const apolloProvider = createMockApollo(requestHandlers);
|
||||
|
||||
wrapper = shallowMount(SnippetEditApp, {
|
||||
wrapper = shallowMountExtended(SnippetEditApp, {
|
||||
apolloProvider,
|
||||
stubs: {
|
||||
ApolloMutation,
|
||||
|
@ -177,7 +176,7 @@ describe('Snippet Edit app', () => {
|
|||
it('renders loader', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -193,10 +192,10 @@ describe('Snippet Edit app', () => {
|
|||
});
|
||||
|
||||
it('should render components', () => {
|
||||
expect(wrapper.find(TitleField).exists()).toBe(true);
|
||||
expect(wrapper.find(SnippetDescriptionEdit).exists()).toBe(true);
|
||||
expect(wrapper.find(SnippetVisibilityEdit).exists()).toBe(true);
|
||||
expect(wrapper.find(FormFooterActions).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlFormGroup).attributes('label')).toEqual('Title');
|
||||
expect(wrapper.findComponent(SnippetDescriptionEdit).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(SnippetVisibilityEdit).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(FormFooterActions).exists()).toBe(true);
|
||||
expect(findBlobActions().exists()).toBe(true);
|
||||
});
|
||||
|
||||
|
@ -207,25 +206,34 @@ describe('Snippet Edit app', () => {
|
|||
|
||||
describe('default', () => {
|
||||
it.each`
|
||||
title | actions | shouldDisable
|
||||
${''} | ${[]} | ${true}
|
||||
${''} | ${[TEST_ACTIONS.VALID]} | ${true}
|
||||
${'foo'} | ${[]} | ${false}
|
||||
${'foo'} | ${[TEST_ACTIONS.VALID]} | ${false}
|
||||
${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_CONTENT]} | ${true}
|
||||
${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_PATH]} | ${false}
|
||||
title | actions | titleHasErrors | blobActionsHasErrors
|
||||
${''} | ${[]} | ${true} | ${false}
|
||||
${''} | ${[TEST_ACTIONS.VALID]} | ${true} | ${false}
|
||||
${'foo'} | ${[]} | ${false} | ${false}
|
||||
${'foo'} | ${[TEST_ACTIONS.VALID]} | ${false} | ${false}
|
||||
${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_CONTENT]} | ${false} | ${true}
|
||||
${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_PATH]} | ${false} | ${false}
|
||||
`(
|
||||
'should handle submit disable (title="$title", actions="$actions", shouldDisable="$shouldDisable")',
|
||||
async ({ title, actions, shouldDisable }) => {
|
||||
'validates correctly (title="$title", actions="$actions", titleHasErrors="$titleHasErrors", blobActionsHasErrors="$blobActionsHasErrors")',
|
||||
async ({ title, actions, titleHasErrors, blobActionsHasErrors }) => {
|
||||
getSpy.mockResolvedValue(createQueryResponse({ title }));
|
||||
|
||||
await createComponentAndLoad();
|
||||
|
||||
triggerBlobActions(actions);
|
||||
|
||||
clickSubmitBtn();
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(hasDisabledSubmit()).toBe(shouldDisable);
|
||||
expect(wrapper.findComponent(GlFormGroup).exists()).toBe(true);
|
||||
expect(Boolean(wrapper.findComponent(GlFormGroup).attributes('state'))).toEqual(
|
||||
!titleHasErrors,
|
||||
);
|
||||
|
||||
expect(wrapper.find(SnippetBlobActionsEdit).props('isValid')).toEqual(
|
||||
!blobActionsHasErrors,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -262,35 +270,64 @@ describe('Snippet Edit app', () => {
|
|||
);
|
||||
|
||||
describe('form submission handling', () => {
|
||||
it.each`
|
||||
snippetGid | projectPath | uploadedFiles | input | mutationType
|
||||
${''} | ${'project/path'} | ${[]} | ${{ ...getApiData(), projectPath: 'project/path', uploadedFiles: [] }} | ${'createSnippet'}
|
||||
${''} | ${''} | ${[]} | ${{ ...getApiData(), projectPath: '', uploadedFiles: [] }} | ${'createSnippet'}
|
||||
${''} | ${''} | ${TEST_UPLOADED_FILES} | ${{ ...getApiData(), projectPath: '', uploadedFiles: TEST_UPLOADED_FILES }} | ${'createSnippet'}
|
||||
${TEST_SNIPPET_GID} | ${'project/path'} | ${[]} | ${getApiData(createSnippet())} | ${'updateSnippet'}
|
||||
${TEST_SNIPPET_GID} | ${''} | ${[]} | ${getApiData(createSnippet())} | ${'updateSnippet'}
|
||||
`(
|
||||
'should submit mutation $mutationType (snippetGid=$snippetGid, projectPath=$projectPath, uploadedFiles=$uploadedFiles)',
|
||||
async ({ snippetGid, projectPath, uploadedFiles, mutationType, input }) => {
|
||||
await createComponentAndLoad({
|
||||
props: {
|
||||
snippetGid,
|
||||
projectPath,
|
||||
},
|
||||
});
|
||||
describe('when creating a new snippet', () => {
|
||||
it.each`
|
||||
projectPath | uploadedFiles | input
|
||||
${''} | ${TEST_UPLOADED_FILES} | ${{ ...getApiData({ title: 'Title' }), projectPath: '', uploadedFiles: TEST_UPLOADED_FILES }}
|
||||
${'project/path'} | ${TEST_UPLOADED_FILES} | ${{ ...getApiData({ title: 'Title' }), projectPath: 'project/path', uploadedFiles: TEST_UPLOADED_FILES }}
|
||||
`(
|
||||
'should submit a createSnippet mutation (projectPath=$projectPath, uploadedFiles=$uploadedFiles)',
|
||||
async ({ projectPath, uploadedFiles, input }) => {
|
||||
await createComponentAndLoad({
|
||||
props: {
|
||||
snippetGid: '',
|
||||
projectPath,
|
||||
},
|
||||
});
|
||||
|
||||
setUploadFilesHtml(uploadedFiles);
|
||||
setTitle(input.title);
|
||||
setUploadFilesHtml(uploadedFiles);
|
||||
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
clickSubmitBtn();
|
||||
clickSubmitBtn();
|
||||
|
||||
expect(mutateSpy).toHaveBeenCalledTimes(1);
|
||||
expect(mutateSpy).toHaveBeenCalledWith(mutationType, {
|
||||
input,
|
||||
});
|
||||
},
|
||||
);
|
||||
expect(mutateSpy).toHaveBeenCalledTimes(1);
|
||||
expect(mutateSpy).toHaveBeenCalledWith('createSnippet', {
|
||||
input,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('when updating a snippet', () => {
|
||||
it.each`
|
||||
projectPath | uploadedFiles | input
|
||||
${''} | ${[]} | ${getApiData(createSnippet())}
|
||||
${'project/path'} | ${[]} | ${getApiData(createSnippet())}
|
||||
`(
|
||||
'should submit an updateSnippet mutation (projectPath=$projectPath, uploadedFiles=$uploadedFiles)',
|
||||
async ({ projectPath, uploadedFiles, input }) => {
|
||||
await createComponentAndLoad({
|
||||
props: {
|
||||
snippetGid: TEST_SNIPPET_GID,
|
||||
projectPath,
|
||||
},
|
||||
});
|
||||
|
||||
setUploadFilesHtml(uploadedFiles);
|
||||
|
||||
await nextTick();
|
||||
|
||||
clickSubmitBtn();
|
||||
|
||||
expect(mutateSpy).toHaveBeenCalledTimes(1);
|
||||
expect(mutateSpy).toHaveBeenCalledWith('updateSnippet', {
|
||||
input,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should redirect to snippet view on successful mutation', async () => {
|
||||
await createComponentAndSubmit();
|
||||
|
@ -298,30 +335,55 @@ describe('Snippet Edit app', () => {
|
|||
expect(urlUtils.redirectTo).toHaveBeenCalledWith(TEST_WEB_URL);
|
||||
});
|
||||
|
||||
it.each`
|
||||
snippetGid | projectPath | mutationRes | expectMessage
|
||||
${''} | ${'project/path'} | ${createMutationResponseWithErrors('createSnippet')} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`}
|
||||
${''} | ${''} | ${createMutationResponseWithErrors('createSnippet')} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`}
|
||||
${TEST_SNIPPET_GID} | ${'project/path'} | ${createMutationResponseWithErrors('updateSnippet')} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`}
|
||||
${TEST_SNIPPET_GID} | ${''} | ${createMutationResponseWithErrors('updateSnippet')} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`}
|
||||
`(
|
||||
'should flash error with (snippet=$snippetGid, projectPath=$projectPath)',
|
||||
async ({ snippetGid, projectPath, mutationRes, expectMessage }) => {
|
||||
mutateSpy.mockResolvedValue(mutationRes);
|
||||
describe('when there are errors after creating a new snippet', () => {
|
||||
it.each`
|
||||
projectPath
|
||||
${'project/path'}
|
||||
${''}
|
||||
`('should flash error (projectPath=$projectPath)', async ({ projectPath }) => {
|
||||
mutateSpy.mockResolvedValue(createMutationResponseWithErrors('createSnippet'));
|
||||
|
||||
await createComponentAndSubmit({
|
||||
props: {
|
||||
projectPath,
|
||||
snippetGid,
|
||||
},
|
||||
await createComponentAndLoad({
|
||||
props: { projectPath, snippetGid: '' },
|
||||
});
|
||||
|
||||
setTitle('Title');
|
||||
|
||||
clickSubmitBtn();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(urlUtils.redirectTo).not.toHaveBeenCalled();
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: expectMessage,
|
||||
message: `Can't create snippet: ${TEST_MUTATION_ERROR}`,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are errors after updating a snippet', () => {
|
||||
it.each`
|
||||
projectPath
|
||||
${'project/path'}
|
||||
${''}
|
||||
`(
|
||||
'should flash error with (snippet=$snippetGid, projectPath=$projectPath)',
|
||||
async ({ projectPath }) => {
|
||||
mutateSpy.mockResolvedValue(createMutationResponseWithErrors('updateSnippet'));
|
||||
|
||||
await createComponentAndSubmit({
|
||||
props: {
|
||||
projectPath,
|
||||
snippetGid: TEST_SNIPPET_GID,
|
||||
},
|
||||
});
|
||||
|
||||
expect(urlUtils.redirectTo).not.toHaveBeenCalled();
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: `Can't update snippet: ${TEST_MUTATION_ERROR}`,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('with apollo network error', () => {
|
||||
beforeEach(async () => {
|
||||
|
@ -382,6 +444,7 @@ describe('Snippet Edit app', () => {
|
|||
false,
|
||||
() => {
|
||||
triggerBlobActions([testEntries.updated.diff]);
|
||||
setTitle('test');
|
||||
clickSubmitBtn();
|
||||
},
|
||||
],
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { times } from 'lodash';
|
||||
import { nextTick } from 'vue';
|
||||
import { GlFormGroup } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
|
||||
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
|
||||
import {
|
||||
|
@ -8,6 +9,7 @@ import {
|
|||
SNIPPET_BLOB_ACTION_CREATE,
|
||||
SNIPPET_BLOB_ACTION_MOVE,
|
||||
} from '~/snippets/constants';
|
||||
import { s__ } from '~/locale';
|
||||
import { testEntries, createBlobFromTestEntry } from '../test_utils';
|
||||
|
||||
const TEST_BLOBS = [
|
||||
|
@ -29,7 +31,7 @@ describe('snippets/components/snippet_blob_actions_edit', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findLabel = () => wrapper.find('label');
|
||||
const findLabel = () => wrapper.findComponent(GlFormGroup);
|
||||
const findBlobEdits = () => wrapper.findAll(SnippetBlobEdit);
|
||||
const findBlobsData = () =>
|
||||
findBlobEdits().wrappers.map((x) => ({
|
||||
|
@ -65,7 +67,7 @@ describe('snippets/components/snippet_blob_actions_edit', () => {
|
|||
});
|
||||
|
||||
it('renders label', () => {
|
||||
expect(findLabel().text()).toBe('Files');
|
||||
expect(findLabel().attributes('label')).toBe('Files');
|
||||
});
|
||||
|
||||
it(`renders delete button (show=true)`, () => {
|
||||
|
@ -280,4 +282,32 @@ describe('snippets/components/snippet_blob_actions_edit', () => {
|
|||
expect(findAddButton().props('disabled')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValid prop', () => {
|
||||
const validationMessage = s__(
|
||||
"Snippets|Snippets can't contain empty files. Ensure all files have content, or delete them.",
|
||||
);
|
||||
|
||||
describe('when not present', () => {
|
||||
it('sets the label validation state to true', () => {
|
||||
createComponent();
|
||||
|
||||
const label = findLabel();
|
||||
|
||||
expect(Boolean(label.attributes('state'))).toEqual(true);
|
||||
expect(label.attributes('invalid-feedback')).toEqual(validationMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when present', () => {
|
||||
it('sets the label validation state to the value', () => {
|
||||
createComponent({ isValid: false });
|
||||
|
||||
const label = findLabel();
|
||||
|
||||
expect(Boolean(label.attributes('state'))).toEqual(false);
|
||||
expect(label.attributes('invalid-feedback')).toEqual(validationMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe "Delete a task in a work item's description" do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
|
||||
let_it_be(:task) { create(:work_item, :task, project: project, author: developer) }
|
||||
let_it_be(:work_item, refind: true) do
|
||||
create(:work_item, project: project, description: "- [ ] #{task.to_reference}+", lock_version: 3)
|
||||
end
|
||||
|
||||
before_all do
|
||||
create(:issue_link, source_id: work_item.id, target_id: task.id)
|
||||
end
|
||||
|
||||
let(:lock_version) { work_item.lock_version }
|
||||
let(:input) do
|
||||
{
|
||||
'id' => work_item.to_global_id.to_s,
|
||||
'lockVersion' => lock_version,
|
||||
'taskData' => {
|
||||
'id' => task.to_global_id.to_s,
|
||||
'lineNumberStart' => 1,
|
||||
'lineNumberEnd' => 1
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
let(:mutation) { graphql_mutation(:workItemDeleteTask, input) }
|
||||
let(:mutation_response) { graphql_mutation_response(:work_item_delete_task) }
|
||||
|
||||
context 'the user is not allowed to update a work item' do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it_behaves_like 'a mutation that returns a top-level access error'
|
||||
end
|
||||
|
||||
context 'when user can update the description but not delete the task' do
|
||||
let(:current_user) { create(:user).tap { |u| project.add_developer(u) } }
|
||||
|
||||
it_behaves_like 'a mutation that returns a top-level access error'
|
||||
end
|
||||
|
||||
context 'when user has permissions to remove a task' do
|
||||
let(:current_user) { developer }
|
||||
|
||||
it 'removes the task from the work item' do
|
||||
expect do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
work_item.reload
|
||||
end.to change(WorkItem, :count).by(-1).and(
|
||||
change(IssueLink, :count).by(-1)
|
||||
).and(
|
||||
change(work_item, :description).from("- [ ] #{task.to_reference}+").to('')
|
||||
)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['workItem']).to include('id' => work_item.to_global_id.to_s)
|
||||
end
|
||||
|
||||
context 'when removing the task fails' do
|
||||
let(:lock_version) { 2 }
|
||||
|
||||
it 'makes no changes to the DB and returns an error message' do
|
||||
expect do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
work_item.reload
|
||||
end.to not_change(WorkItem, :count).and(
|
||||
not_change(work_item, :description)
|
||||
)
|
||||
|
||||
expect(mutation_response['errors']).to contain_exactly('Stale work item. Check lock version')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the work_items feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(work_items: false)
|
||||
end
|
||||
|
||||
it 'does nothing and returns and error' do
|
||||
expect do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
end.to not_change(WorkItem, :count)
|
||||
|
||||
expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
require 'fast_spec_helper'
|
||||
require_relative '../../../../scripts/lib/glfm/shared'
|
||||
|
||||
RSpec.describe Glfm::Shared do
|
||||
describe '#output' do
|
||||
# NOTE: The #output method is normally always mocked, to prevent output while the specs are
|
||||
# running. However, in order to provide code coverage for the method, we have to invoke
|
||||
# it at least once.
|
||||
it 'has code coverage' do
|
||||
clazz = Class.new do
|
||||
include Glfm::Shared
|
||||
end
|
||||
instance = clazz.new
|
||||
allow(instance).to receive(:puts)
|
||||
instance.output('')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,173 @@
|
|||
# frozen_string_literal: true
|
||||
require 'fast_spec_helper'
|
||||
require_relative '../../../../scripts/lib/glfm/update_specification'
|
||||
|
||||
RSpec.describe Glfm::UpdateSpecification, '#process' do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:ghfm_spec_txt_uri) { described_class::GHFM_SPEC_TXT_URI }
|
||||
let(:ghfm_spec_txt_uri_io) { StringIO.new(ghfm_spec_txt_contents) }
|
||||
let(:ghfm_spec_txt_path) { described_class::GHFM_SPEC_TXT_PATH }
|
||||
let(:ghfm_spec_txt_local_io) { StringIO.new }
|
||||
|
||||
let(:glfm_intro_txt_path) { described_class::GLFM_INTRO_TXT_PATH }
|
||||
let(:glfm_intro_txt_io) { StringIO.new(glfm_intro_txt_contents) }
|
||||
let(:glfm_examples_txt_path) { described_class::GLFM_EXAMPLES_TXT_PATH }
|
||||
let(:glfm_examples_txt_io) { StringIO.new(glfm_examples_txt_contents) }
|
||||
let(:glfm_spec_txt_path) { described_class::GLFM_SPEC_TXT_PATH }
|
||||
let(:glfm_spec_txt_io) { StringIO.new }
|
||||
|
||||
let(:ghfm_spec_txt_contents) do
|
||||
<<~GHFM_SPEC_TXT_CONTENTS
|
||||
---
|
||||
title: GitHub Flavored Markdown Spec
|
||||
version: 0.29
|
||||
date: '2019-04-06'
|
||||
license: '[CC-BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/)'
|
||||
...
|
||||
|
||||
# Introduction
|
||||
|
||||
## What is GitHub Flavored Markdown?
|
||||
|
||||
It's like GLFM, but with an H.
|
||||
|
||||
# Section with Examples
|
||||
|
||||
## Strong
|
||||
|
||||
```````````````````````````````` example
|
||||
__bold__
|
||||
.
|
||||
<p><strong>bold</strong></p>
|
||||
````````````````````````````````
|
||||
|
||||
End of last GitHub examples section.
|
||||
|
||||
<!-- END TESTS -->
|
||||
|
||||
# Appendix
|
||||
|
||||
Appendix text.
|
||||
GHFM_SPEC_TXT_CONTENTS
|
||||
end
|
||||
|
||||
let(:glfm_intro_txt_contents) do
|
||||
<<~GLFM_INTRO_TXT_CONTENTS
|
||||
# Introduction
|
||||
|
||||
## What is GitLab Flavored Markdown?
|
||||
|
||||
Intro text about GitLab Flavored Markdown.
|
||||
GLFM_INTRO_TXT_CONTENTS
|
||||
end
|
||||
|
||||
let(:glfm_examples_txt_contents) do
|
||||
<<~GLFM_EXAMPLES_TXT_CONTENTS
|
||||
# GitLab-Specific Section with Examples
|
||||
|
||||
Some examples.
|
||||
GLFM_EXAMPLES_TXT_CONTENTS
|
||||
end
|
||||
|
||||
before do
|
||||
# We mock out the URI and local file IO objects with real StringIO, instead of just mock
|
||||
# objects. This gives better and more realistic coverage, while still avoiding
|
||||
# actual network and filesystem I/O during the spec run.
|
||||
allow(URI).to receive(:open).with(ghfm_spec_txt_uri) { ghfm_spec_txt_uri_io }
|
||||
allow(File).to receive(:open).with(ghfm_spec_txt_path, 'w') { ghfm_spec_txt_local_io }
|
||||
allow(File).to receive(:open).with(glfm_intro_txt_path) { glfm_intro_txt_io }
|
||||
allow(File).to receive(:open).with(glfm_examples_txt_path) { glfm_examples_txt_io }
|
||||
allow(File).to receive(:open).with(glfm_spec_txt_path, 'w') { glfm_spec_txt_io }
|
||||
|
||||
# Prevent console output when running tests
|
||||
allow(subject).to receive(:output)
|
||||
end
|
||||
|
||||
describe 'retrieving latest GHFM spec.txt' do
|
||||
context 'with success' do
|
||||
it 'downloads and saves' do
|
||||
subject.process
|
||||
|
||||
expect(reread_io(ghfm_spec_txt_local_io)).to eq(ghfm_spec_txt_contents)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with error handling' do
|
||||
context 'with a version mismatch' do
|
||||
let(:ghfm_spec_txt_contents) do
|
||||
<<~GHFM_SPEC_TXT_CONTENTS
|
||||
---
|
||||
title: GitHub Flavored Markdown Spec
|
||||
version: 0.30
|
||||
...
|
||||
GHFM_SPEC_TXT_CONTENTS
|
||||
end
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject.process }.to raise_error /version mismatch.*expected.*29.*got.*30/i
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a failed read of file lines' do
|
||||
let(:ghfm_spec_txt_contents) { '' }
|
||||
|
||||
it 'raises an error if lines cannot be read' do
|
||||
expect { subject.process }.to raise_error /unable to read lines/i
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a failed re-read of file string' do
|
||||
before do
|
||||
allow(ghfm_spec_txt_uri_io).to receive(:read).and_return(nil)
|
||||
end
|
||||
|
||||
it 'raises an error if file is blank' do
|
||||
expect { subject.process }.to raise_error /unable to read string/i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'writing GLFM spec.txt' do
|
||||
let(:glfm_contents) { reread_io(glfm_spec_txt_io) }
|
||||
|
||||
before do
|
||||
subject.process
|
||||
end
|
||||
|
||||
it 'replaces the header text with the GitLab version' do
|
||||
expect(glfm_contents).not_to match(/GitHub Flavored Markdown Spec/m)
|
||||
expect(glfm_contents).not_to match(/^version: \d\.\d/m)
|
||||
expect(glfm_contents).not_to match(/^date: /m)
|
||||
expect(glfm_contents).not_to match(/^license: /m)
|
||||
expect(glfm_contents).to match(/#{Regexp.escape(described_class::GLFM_SPEC_TXT_HEADER)}\n/m)
|
||||
end
|
||||
|
||||
it 'replaces the intro section with the GitLab version' do
|
||||
expect(glfm_contents).not_to match(/What is GitHub Flavored Markdown/m)
|
||||
expect(glfm_contents).to match(/#{Regexp.escape(glfm_intro_txt_contents)}/m)
|
||||
end
|
||||
|
||||
it 'inserts the GitLab examples sections before the appendix section' do
|
||||
expected = <<~GHFM_SPEC_TXT_CONTENTS
|
||||
End of last GitHub examples section.
|
||||
|
||||
# GitLab-Specific Section with Examples
|
||||
|
||||
Some examples.
|
||||
|
||||
<!-- END TESTS -->
|
||||
|
||||
# Appendix
|
||||
GHFM_SPEC_TXT_CONTENTS
|
||||
expect(glfm_contents).to match(/#{Regexp.escape(expected)}/m)
|
||||
end
|
||||
end
|
||||
|
||||
def reread_io(io)
|
||||
# Reset the io StringIO to the beginning position of the buffer
|
||||
io.seek(0)
|
||||
io.read
|
||||
end
|
||||
end
|
|
@ -0,0 +1,88 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe WorkItems::DeleteTaskService do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
|
||||
let_it_be_with_refind(:task) { create(:work_item, project: project, author: developer) }
|
||||
let_it_be_with_refind(:list_work_item) do
|
||||
create(:work_item, project: project, description: "- [ ] #{task.to_reference}+")
|
||||
end
|
||||
|
||||
let(:current_user) { developer }
|
||||
let(:line_number_start) { 1 }
|
||||
let(:params) do
|
||||
{
|
||||
line_number_start: line_number_start,
|
||||
line_number_end: 1,
|
||||
task: task
|
||||
}
|
||||
end
|
||||
|
||||
before_all do
|
||||
create(:issue_link, source_id: list_work_item.id, target_id: task.id)
|
||||
end
|
||||
|
||||
shared_examples 'failing WorkItems::DeleteTaskService' do |error_message|
|
||||
it { is_expected.to be_error }
|
||||
|
||||
it 'does not remove work item or issue links' do
|
||||
expect do
|
||||
service_result
|
||||
list_work_item.reload
|
||||
end.to not_change(WorkItem, :count).and(
|
||||
not_change(IssueLink, :count)
|
||||
).and(
|
||||
not_change(list_work_item, :description)
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns an error message' do
|
||||
expect(service_result.errors).to contain_exactly(error_message)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
subject(:service_result) do
|
||||
described_class.new(
|
||||
work_item: list_work_item,
|
||||
current_user: current_user,
|
||||
lock_version: list_work_item.lock_version,
|
||||
task_params: params
|
||||
).execute
|
||||
end
|
||||
|
||||
context 'when work item params are valid' do
|
||||
it { is_expected.to be_success }
|
||||
|
||||
it 'deletes the work item and the related issue link' do
|
||||
expect do
|
||||
service_result
|
||||
end.to change(WorkItem, :count).by(-1).and(
|
||||
change(IssueLink, :count).by(-1)
|
||||
)
|
||||
end
|
||||
|
||||
it 'removes the task list item with the work item reference' do
|
||||
expect do
|
||||
service_result
|
||||
end.to change(list_work_item, :description).from(list_work_item.description).to('')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when first operation fails' do
|
||||
let(:line_number_start) { -1 }
|
||||
|
||||
it_behaves_like 'failing WorkItems::DeleteTaskService', 'line_number_start must be greater than 0'
|
||||
end
|
||||
|
||||
context 'when last operation fails' do
|
||||
let_it_be(:non_member_user) { create(:user) }
|
||||
|
||||
let(:current_user) { non_member_user }
|
||||
|
||||
it_behaves_like 'failing WorkItems::DeleteTaskService', 'User not authorized to delete work item'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,151 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe WorkItems::TaskListReferenceRemovalService do
|
||||
let_it_be(:developer) { create(:user) }
|
||||
let_it_be(:project) { create(:project, :repository).tap { |project| project.add_developer(developer) } }
|
||||
let_it_be(:task) { create(:work_item, project: project) }
|
||||
let_it_be(:single_line_work_item, refind: true) do
|
||||
create(:work_item, project: project, description: "- [ ] #{task.to_reference}+ single line")
|
||||
end
|
||||
|
||||
let_it_be(:multiple_line_work_item, refind: true) do
|
||||
create(
|
||||
:work_item,
|
||||
project: project,
|
||||
description: <<~MARKDOWN
|
||||
Any text
|
||||
|
||||
* [ ] Item to be converted
|
||||
#{task.to_reference}+ second line
|
||||
third line
|
||||
* [x] task
|
||||
|
||||
More text
|
||||
MARKDOWN
|
||||
)
|
||||
end
|
||||
|
||||
let(:line_number_start) { 3 }
|
||||
let(:line_number_end) { 5 }
|
||||
let(:work_item) { multiple_line_work_item }
|
||||
let(:lock_version) { work_item.lock_version }
|
||||
|
||||
shared_examples 'successful work item task reference removal service' do |expected_description|
|
||||
it { is_expected.to be_success }
|
||||
|
||||
it 'removes the task list item containing the task reference' do
|
||||
expect do
|
||||
result
|
||||
end.to change(work_item, :description).from(work_item.description).to(expected_description)
|
||||
end
|
||||
|
||||
it 'creates system notes' do
|
||||
expect do
|
||||
result
|
||||
end.to change(Note, :count).by(1)
|
||||
|
||||
expect(Note.last.note).to include('changed the description')
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'failing work item task reference removal service' do |error_message|
|
||||
it { is_expected.to be_error }
|
||||
|
||||
it 'does not change the work item description' do
|
||||
expect do
|
||||
result
|
||||
work_item.reload
|
||||
end.to not_change(work_item, :description)
|
||||
end
|
||||
|
||||
it 'returns an error message' do
|
||||
expect(result.errors).to contain_exactly(error_message)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
subject(:result) do
|
||||
described_class.new(
|
||||
work_item: work_item,
|
||||
task: task,
|
||||
line_number_start: line_number_start,
|
||||
line_number_end: line_number_end,
|
||||
lock_version: lock_version,
|
||||
current_user: developer
|
||||
).execute
|
||||
end
|
||||
|
||||
context 'when task mardown spans a single line' do
|
||||
let(:line_number_start) { 1 }
|
||||
let(:line_number_end) { 1 }
|
||||
let(:work_item) { single_line_work_item }
|
||||
|
||||
it_behaves_like 'successful work item task reference removal service', ''
|
||||
|
||||
context 'when description does not contain a task' do
|
||||
let_it_be(:no_matching_work_item) { create(:work_item, project: project, description: 'no matching task') }
|
||||
|
||||
let(:work_item) { no_matching_work_item }
|
||||
|
||||
it_behaves_like 'failing work item task reference removal service', 'Unable to detect a task on lines 1-1'
|
||||
end
|
||||
|
||||
context 'when description reference does not exactly match the task reference' do
|
||||
before do
|
||||
work_item.update!(description: work_item.description.gsub(task.to_reference, "#{task.to_reference}200"))
|
||||
end
|
||||
|
||||
it_behaves_like 'failing work item task reference removal service', 'Unable to detect a task on lines 1-1'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when task mardown spans multiple lines' do
|
||||
it_behaves_like 'successful work item task reference removal service', "Any text\n\n* [x] task\n\nMore text"
|
||||
end
|
||||
|
||||
context 'when updating the work item fails' do
|
||||
before do
|
||||
work_item.title = nil
|
||||
end
|
||||
|
||||
it_behaves_like 'failing work item task reference removal service', "Title can't be blank"
|
||||
end
|
||||
|
||||
context 'when description is empty' do
|
||||
let_it_be(:empty_work_item) { create(:work_item, project: project, description: '') }
|
||||
|
||||
let(:work_item) { empty_work_item }
|
||||
|
||||
it_behaves_like 'failing work item task reference removal service', "Work item description can't be blank"
|
||||
end
|
||||
|
||||
context 'when line_number_start is lower than 1' do
|
||||
let(:line_number_start) { 0 }
|
||||
|
||||
it_behaves_like 'failing work item task reference removal service', 'line_number_start must be greater than 0'
|
||||
end
|
||||
|
||||
context 'when line_number_end is lower than line_number_start' do
|
||||
let(:line_number_end) { line_number_start - 1 }
|
||||
|
||||
it_behaves_like 'failing work item task reference removal service',
|
||||
'line_number_end must be greater or equal to line_number_start'
|
||||
end
|
||||
|
||||
context 'when lock_version is older than current' do
|
||||
let(:lock_version) { work_item.lock_version - 1 }
|
||||
|
||||
it_behaves_like 'failing work item task reference removal service', 'Stale work item. Check lock version'
|
||||
end
|
||||
|
||||
context 'when work item is stale before updating' do
|
||||
it_behaves_like 'failing work item task reference removal service', 'Stale work item. Check lock version' do
|
||||
before do
|
||||
::WorkItem.where(id: work_item.id).update_all(lock_version: lock_version + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -67,14 +67,19 @@ module Spec
|
|||
end
|
||||
|
||||
def snippet_fill_in_form(title: nil, content: nil, file_name: nil, description: nil, visibility: nil)
|
||||
if content
|
||||
snippet_fill_in_content(content)
|
||||
# It takes some time after sending keys for the vue component to
|
||||
# update so let Capybara wait for the content before proceeding
|
||||
expect(page).to have_content(content)
|
||||
end
|
||||
|
||||
snippet_fill_in_title(title) if title
|
||||
|
||||
snippet_fill_in_description(description) if description
|
||||
|
||||
snippet_fill_in_file_name(file_name) if file_name
|
||||
|
||||
snippet_fill_in_content(content) if content
|
||||
|
||||
snippet_fill_in_visibility(visibility) if visibility
|
||||
end
|
||||
end
|
||||
|
|
|
@ -968,10 +968,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.11.0.tgz#06edc30c58a539b2cb60ef30d61ce470c0f57f2b"
|
||||
integrity sha512-IkiMrt3NY4IHonEgSHfv6JoJ+3McppZpt7XYgG6QtVtiCHFuwbkTH+gLMeZHG127AjtuHr54wS/cYp4MSNZZ4Q==
|
||||
|
||||
"@gitlab/ui@39.4.0":
|
||||
version "39.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-39.4.0.tgz#7afacad3556c8bcfc136d38922c15a3fe7cd5cd5"
|
||||
integrity sha512-OZRGLS/308paAHRSoiDCR+ZDuXZkE4tzFRdHlTBO0P6/7ZKLtIL6koNWa0Lpedlr2BfmGxkjvAsotvHk2F9U1w==
|
||||
"@gitlab/ui@39.6.0":
|
||||
version "39.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-39.6.0.tgz#d39ee45a6b629498a60af9683ecc9c2d50486c13"
|
||||
integrity sha512-RQBD4r8ii9xZ3Hwxfn3RKZkBWfberffX93MDWlxujD43DHJ0aE0Uhw/fqfE41i/uEUNqhhNVRtl77XLSlYP/iw==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.11.2"
|
||||
bootstrap-vue "2.20.1"
|
||||
|
|
Loading…
Reference in New Issue