Commit 55545ce9 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents d5fb657a 0cec0605
......@@ -117,7 +117,7 @@ export const createCommitPayload = ({
action: commitActionForFile(f),
file_path: f.path,
previous_path: f.prevPath || undefined,
content: f.prevPath && !f.changed ? null : content || undefined,
content: content || undefined,
encoding: isBlob ? 'base64' : 'text',
last_commit_id: newBranch || f.deleted || f.prevPath ? undefined : f.lastCommitSha,
};
......
......@@ -24,7 +24,10 @@ class Projects::BadgesController < Projects::ApplicationController
.new(project, params[:ref], opts: {
job: params[:job],
key_text: params[:key_text],
key_width: params[:key_width]
key_width: params[:key_width],
min_good: params[:min_good],
min_acceptable: params[:min_acceptable],
min_medium: params[:min_medium]
})
render_badge coverage_report
......
......@@ -107,8 +107,6 @@ class Issue < ApplicationRecord
scope :order_due_date_asc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'ASC')) }
scope :order_due_date_desc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'DESC')) }
scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) }
scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) }
scope :order_relative_position_desc, -> { reorder(::Gitlab::Database.nulls_first_order('relative_position', 'DESC')) }
scope :order_closed_date_desc, -> { reorder(closed_at: :desc) }
scope :order_created_at_desc, -> { reorder(created_at: :desc) }
scope :order_severity_asc, -> { includes(:issuable_severity).order('issuable_severities.severity ASC NULLS FIRST') }
......@@ -267,8 +265,8 @@ class Issue < ApplicationRecord
'due_date' => -> { order_due_date_asc.with_order_id_desc },
'due_date_asc' => -> { order_due_date_asc.with_order_id_desc },
'due_date_desc' => -> { order_due_date_desc.with_order_id_desc },
'relative_position' => -> { order_relative_position_asc.with_order_id_desc },
'relative_position_asc' => -> { order_relative_position_asc.with_order_id_desc }
'relative_position' => -> { order_by_relative_position },
'relative_position_asc' => -> { order_by_relative_position }
}
)
end
......@@ -278,7 +276,7 @@ class Issue < ApplicationRecord
when 'closest_future_date', 'closest_future_date_asc' then order_closest_future_date
when 'due_date', 'due_date_asc' then order_due_date_asc.with_order_id_desc
when 'due_date_desc' then order_due_date_desc.with_order_id_desc
when 'relative_position', 'relative_position_asc' then order_relative_position_asc.with_order_id_desc
when 'relative_position', 'relative_position_asc' then order_by_relative_position
when 'severity_asc' then order_severity_asc.with_order_id_desc
when 'severity_desc' then order_severity_desc.with_order_id_desc
else
......@@ -286,13 +284,8 @@ class Issue < ApplicationRecord
end
end
# `with_cte` argument allows sorting when using CTE queries and prevents
# errors in postgres when using CTE search optimisation
def self.order_by_position_and_priority(with_cte: false)
order = Gitlab::Pagination::Keyset::Order.build([column_order_relative_position, column_order_highest_priority, column_order_id_desc])
order_labels_priority(with_cte: with_cte)
.reorder(order)
def self.order_by_relative_position
reorder(Gitlab::Pagination::Keyset::Order.build([column_order_relative_position, column_order_id_asc]))
end
def self.column_order_relative_position
......@@ -307,25 +300,6 @@ class Issue < ApplicationRecord
)
end
def self.column_order_highest_priority
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'highest_priority',
column_expression: Arel.sql('highest_priorities.label_priority'),
order_expression: Gitlab::Database.nulls_last_order('highest_priorities.label_priority', 'ASC'),
reversed_order_expression: Gitlab::Database.nulls_last_order('highest_priorities.label_priority', 'DESC'),
order_direction: :asc,
nullable: :nulls_last,
distinct: false
)
end
def self.column_order_id_desc
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id',
order_expression: arel_table[:id].desc
)
end
def self.column_order_id_asc
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id',
......
......@@ -22,7 +22,7 @@ module Boards
def order(items)
return items.order_closed_date_desc if list&.closed?
items.order_by_position_and_priority(with_cte: params[:search].present?)
items.order_by_relative_position
end
def finder
......
......@@ -82,7 +82,7 @@ module Issues
collection.each do |project|
caching.cache_current_project_id(project.id)
index += 1
scope = Issue.in_projects(project).reorder(custom_reorder).select(:id, :relative_position)
scope = Issue.in_projects(project).order_by_relative_position.select(:id, :relative_position)
with_retry(PREFETCH_ISSUES_BATCH_SIZE, 100) do |batch_size|
Gitlab::Pagination::Keyset::Iterator.new(scope: scope).each_batch(of: batch_size) do |batch|
......@@ -166,10 +166,6 @@ module Issues
@start_position ||= (RelativePositioning::START_POSITION - (gaps / 2) * gap_size).to_i
end
def custom_reorder
::Gitlab::Pagination::Keyset::Order.build([Issue.column_order_relative_position, Issue.column_order_id_asc])
end
def with_retry(initial_batch_size, exit_batch_size)
retries = 0
batch_size = initial_batch_size
......
# frozen_string_literal: true
class UpdateIssuesRelativePositionIndexes < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
RELATIVE_POSITION_INDEX_NAME = 'idx_issues_on_project_id_and_rel_asc_and_id'
RELATIVE_POSITION_STATE_INDEX_NAME = 'idx_issues_on_project_id_and_rel_position_and_state_id_and_id'
NEW_RELATIVE_POSITION_STATE_INDEX_NAME = 'idx_issues_on_project_id_and_rel_position_and_id_and_state_id'
def up
add_concurrent_index :issues, [:project_id, :relative_position, :id, :state_id], name: NEW_RELATIVE_POSITION_STATE_INDEX_NAME
remove_concurrent_index_by_name :issues, RELATIVE_POSITION_INDEX_NAME
remove_concurrent_index_by_name :issues, RELATIVE_POSITION_STATE_INDEX_NAME
end
def down
add_concurrent_index :issues, [:project_id, :relative_position, :state_id, :id], order: { id: :desc }, name: RELATIVE_POSITION_STATE_INDEX_NAME
add_concurrent_index :issues, [:project_id, :relative_position, :id], name: RELATIVE_POSITION_INDEX_NAME
remove_concurrent_index_by_name :issues, NEW_RELATIVE_POSITION_STATE_INDEX_NAME
end
end
8e54f43a955023e422bf40476f468fbdf04f06e806b08fddf35208c65607fec3
\ No newline at end of file
......@@ -24045,9 +24045,7 @@ CREATE INDEX idx_issues_on_project_id_and_created_at_and_id_and_state_id ON issu
CREATE INDEX idx_issues_on_project_id_and_due_date_and_id_and_state_id ON issues USING btree (project_id, due_date, id, state_id) WHERE (due_date IS NOT NULL);
CREATE INDEX idx_issues_on_project_id_and_rel_asc_and_id ON issues USING btree (project_id, relative_position, id);
CREATE INDEX idx_issues_on_project_id_and_rel_position_and_state_id_and_id ON issues USING btree (project_id, relative_position, state_id, id DESC);
CREATE INDEX idx_issues_on_project_id_and_rel_position_and_id_and_state_id ON issues USING btree (project_id, relative_position, id, state_id);
CREATE INDEX idx_issues_on_project_id_and_updated_at_and_id_and_state_id ON issues USING btree (project_id, updated_at, id, state_id);
......@@ -358,6 +358,29 @@ in your `README.md`:
![coverage](https://gitlab.com/gitlab-org/gitlab/badges/main/coverage.svg?job=coverage)
```
#### Test coverage report badge colors and limits
The default colors and limits for the badge are as follows:
- 95 up to and including 100% - good (`#4c1`)
- 90 up to 95% - acceptable (`#a3c51c`)
- 75 up to 90% - medium (`#dfb317`)
- 0 up to 75% - low (`#e05d44`)
- no coverage - unknown (`#9f9f9f`)
NOTE:
*Up to* means up to, but not including, the upper bound.
You can overwrite the limits by using the following additional parameters ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/28317) in GitLab 14.4):
- `min_good` (default 95, can use any value between 3 and 100)
- `min_acceptable` (default 90, can use any value between 2 and min_good-1)
- `min_medium` (default 75, can use any value between 1 and min_acceptable-1)
If an invalid boundary is set, GitLab automatically adjusts it to be valid. For example,
if `min_good` is set `80`, and `min_acceptable` is set to `85` (too high), GitLab automatically
sets `min_acceptable` to `79` (`min_good` - `1`).
### Badge styles
Pipeline badges can be rendered in different styles by adding the `style=style_name` parameter to the URL. Two styles are available:
......
......@@ -203,17 +203,13 @@ When visiting a board, issues appear ordered in any list. You're able to change
that order by dragging the issues. The changed order is saved, so that anybody who visits the same
board later sees the reordering, with some exceptions.
The first time an issue appears in any board (that is, the first time a user
loads a board containing that issue), it is ordered in relation to other issues in that list.
The order is done according to [label priority](labels.md#label-priority).
When an issue is created, the system assigns a relative order value that is greater than the maximum value
of that issue's project or root group. This means the issue will be at the bottom of any issue list that
it appears in.
At this point, that issue is assigned a relative order value by the system,
with respect to the other issues in the list. Any time
you drag and reorder the issue, its relative order value changes accordingly.
Also, any time that issue appears in any board, the ordering is done according to
the updated relative order value. It's only the first
time an issue appears that it takes from the priority order mentioned above. If a user in your GitLab instance
Any time you drag and reorder the issue, its relative order value changes accordingly.
Then, any time that issue appears in any board, the ordering is done according to
the updated relative order value. If a user in your GitLab instance
drags issue `A` above issue `B`, the ordering is maintained when these two issues are subsequently
loaded in any board in the same instance. This could be a different project board or a different group
board, for example.
......
......@@ -81,8 +81,8 @@ RSpec.describe 'Project issue boards', :js do
context 'total weight' do
let!(:label) { create(:label, project: project, name: 'Label 1') }
let!(:list) { create(:list, board: board, label: label, position: 0) }
let!(:issue) { create(:issue, project: project, weight: 3) }
let!(:issue_2) { create(:issue, project: project, weight: 2) }
let!(:issue) { create(:issue, project: project, weight: 3, relative_position: 2) }
let!(:issue_2) { create(:issue, project: project, weight: 2, relative_position: 1) }
before do
project.add_developer(user)
......
......@@ -22,7 +22,7 @@ RSpec.describe IssuablesAnalytics do
context 'when issuable relation is ordered by priority' do
it 'generates chart data correctly' do
issues = project.issues.order_by_position_and_priority
issues = project.issues.order_labels_priority
data = described_class.new(issuables: issues).data
seed.each_pair do |months_back, issues_count|
......
......@@ -15,7 +15,10 @@ module Gitlab::Ci
@job = opts[:job]
@customization = {
key_width: opts[:key_width].to_i,
key_text: opts[:key_text]
key_text: opts[:key_text],
min_good: opts[:min_good].to_i,
min_acceptable: opts[:min_acceptable].to_i,
min_medium: opts[:min_medium].to_i
}
end
......
......@@ -16,12 +16,20 @@ module Gitlab::Ci
low: '#e05d44',
unknown: '#9f9f9f'
}.freeze
COVERAGE_MAX = 100
COVERAGE_MIN = 0
MIN_GOOD_DEFAULT = 95
MIN_ACCEPTABLE_DEFAULT = 90
MIN_MEDIUM_DEFAULT = 75
def initialize(badge)
@entity = badge.entity
@status = badge.status
@key_text = badge.customization.dig(:key_text)
@key_width = badge.customization.dig(:key_width)
@min_good = badge.customization.dig(:min_good)
@min_acceptable = badge.customization.dig(:min_acceptable)
@min_medium = badge.customization.dig(:min_medium)
end
def value_text
......@@ -32,12 +40,36 @@ module Gitlab::Ci
@status ? 54 : 58
end
def min_good_value
if @min_good && @min_good.between?(3, COVERAGE_MAX)
@min_good
else
MIN_GOOD_DEFAULT
end
end
def min_acceptable_value
if @min_acceptable && @min_acceptable.between?(2, min_good_value - 1)
@min_acceptable
else
[MIN_ACCEPTABLE_DEFAULT, (min_good_value - 1)].min
end
end
def min_medium_value
if @min_medium && @min_medium.between?(1, min_acceptable_value - 1)
@min_medium
else
[MIN_MEDIUM_DEFAULT, (min_acceptable_value - 1)].min
end
end
def value_color
case @status
when 95..100 then STATUS_COLOR[:good]
when 90..95 then STATUS_COLOR[:acceptable]
when 75..90 then STATUS_COLOR[:medium]
when 0..75 then STATUS_COLOR[:low]
when min_good_value..COVERAGE_MAX then STATUS_COLOR[:good]
when min_acceptable_value..min_good_value then STATUS_COLOR[:acceptable]
when min_medium_value..min_acceptable_value then STATUS_COLOR[:medium]
when COVERAGE_MIN..min_medium_value then STATUS_COLOR[:low]
else
STATUS_COLOR[:unknown]
end
......
......@@ -116,7 +116,7 @@ RSpec.describe Boards::IssuesController do
it 'does not query issues table more than once' do
recorder = ActiveRecord::QueryRecorder.new { list_issues(user: user, board: board, list: list1) }
query_count = recorder.occurrences.select { |query,| query.start_with?('SELECT issues.*') }.each_value.first
query_count = recorder.occurrences.select { |query,| query.match?(/FROM "?issues"?/) }.each_value.first
expect(query_count).to eq(1)
end
......
......@@ -12,6 +12,120 @@ RSpec.describe 'test coverage badge' do
sign_in(user)
end
it 'user requests coverage badge image for pipeline with custom limits - 80% good' do
create_pipeline do |pipeline|
create_build(pipeline, coverage: 80, name: 'test:1')
end
show_test_coverage_badge(min_good: 75, min_acceptable: 50, min_medium: 25)
expect_coverage_badge_color(:good)
expect_coverage_badge('80.00%')
end
it 'user requests coverage badge image for pipeline with custom limits - 74% - bad config' do
create_pipeline do |pipeline|
create_build(pipeline, coverage: 74, name: 'test:1')
end
# User sets a minimum good value that is lower than min acceptable and min medium,
# in which case we force the min acceptable value to be min good -1 and min medium value to be min acceptable -1
show_test_coverage_badge(min_good: 75, min_acceptable: 76, min_medium: 77)
expect_coverage_badge_color(:acceptable)
expect_coverage_badge('74.00%')
end
it 'user requests coverage badge image for pipeline with custom limits - 73% - bad config' do
create_pipeline do |pipeline|
create_build(pipeline, coverage: 73, name: 'test:1')
end
# User sets a minimum good value that is lower than min acceptable and min medium,
# in which case we force the min acceptable value to be min good -1 and min medium value to be min acceptable -1
show_test_coverage_badge(min_good: 75, min_acceptable: 76, min_medium: 77)
expect_coverage_badge_color(:medium)
expect_coverage_badge('73.00%')
end
it 'user requests coverage badge image for pipeline with custom limits - 72% - partial config - low' do
create_pipeline do |pipeline|
create_build(pipeline, coverage: 72, name: 'test:1')
end
# User only sets good to 75 and leaves the others on the default settings,
# in which case we force the min acceptable value to be min good -1 and min medium value to be min acceptable -1
show_test_coverage_badge(min_good: 75)
expect_coverage_badge_color(:low)
expect_coverage_badge('72.00%')
end
it 'user requests coverage badge image for pipeline with custom limits - 72% - partial config - medium' do
create_pipeline do |pipeline|
create_build(pipeline, coverage: 72, name: 'test:1')
end
# User only sets good to 74 and leaves the others on the default settings,
# in which case we force the min acceptable value to be min good -1 and min medium value to be min acceptable -1
show_test_coverage_badge(min_good: 74)
expect_coverage_badge_color(:medium)
expect_coverage_badge('72.00%')
end
it 'user requests coverage badge image for pipeline with custom limits - 72% - partial config - medium v2' do
create_pipeline do |pipeline|
create_build(pipeline, coverage: 72, name: 'test:1')
end
# User only sets medium to 72 and leaves the others on the defaults good as 95 and acceptable as 90
show_test_coverage_badge(min_medium: 72)
expect_coverage_badge_color(:medium)
expect_coverage_badge('72.00%')
end
it 'user requests coverage badge image for pipeline with custom limits - 70% acceptable' do
create_pipeline do |pipeline|
create_build(pipeline, coverage: 70, name: 'test:1')
end
show_test_coverage_badge(min_good: 75, min_acceptable: 50, min_medium: 25)
expect_coverage_badge_color(:acceptable)
expect_coverage_badge('70.00%')
end
it 'user requests coverage badge image for pipeline with custom limits - 30% medium' do
create_pipeline do |pipeline|
create_build(pipeline, coverage: 30, name: 'test:1')
end
show_test_coverage_badge(min_good: 75, min_acceptable: 50, min_medium: 25)
expect_coverage_badge_color(:medium)
expect_coverage_badge('30.00%')
end
it 'user requests coverage badge image for pipeline with custom limits - 20% low' do
create_pipeline do |pipeline|
create_build(pipeline, coverage: 20, name: 'test:1')
end
show_test_coverage_badge(min_good: 75, min_acceptable: 50, min_medium: 25)
expect_coverage_badge_color(:low)
expect_coverage_badge('20.00%')
end
it 'user requests coverage badge image for pipeline with custom limits - nonsense values which use the defaults' do
create_pipeline do |pipeline|
create_build(pipeline, coverage: 92, name: 'test:1')
end
show_test_coverage_badge(min_good: "nonsense", min_acceptable: "rubbish", min_medium: "NaN")
expect_coverage_badge_color(:acceptable)
expect_coverage_badge('92.00%')
end
it 'user requests coverage badge image for pipeline' do
create_pipeline do |pipeline|
create_build(pipeline, coverage: 100, name: 'test:1')
......@@ -20,6 +134,7 @@ RSpec.describe 'test coverage badge' do
show_test_coverage_badge
expect_coverage_badge_color(:good)
expect_coverage_badge('95.00%')
end
......@@ -32,6 +147,7 @@ RSpec.describe 'test coverage badge' do
show_test_coverage_badge(job: 'coverage')
expect_coverage_badge_color(:medium)
expect_coverage_badge('85.00%')
end
......@@ -73,8 +189,9 @@ RSpec.describe 'test coverage badge' do
create(:ci_build, :success, opts)
end
def show_test_coverage_badge(job: nil)
visit coverage_project_badges_path(project, ref: :master, job: job, format: :svg)
def show_test_coverage_badge(job: nil, min_good: nil, min_acceptable: nil, min_medium: nil)
visit coverage_project_badges_path(project, ref: :master, job: job, min_good: min_good,
min_acceptable: min_acceptable, min_medium: min_medium, format: :svg)
end
def expect_coverage_badge(coverage)
......@@ -82,4 +199,12 @@ RSpec.describe 'test coverage badge' do
expect(page.response_headers['Content-Type']).to include('image/svg+xml')
expect(svg.at(%Q{text:contains("#{coverage}")})).to be_truthy
end
def expect_coverage_badge_color(color)
svg = Nokogiri::HTML(page.body)
expect(page.response_headers['Content-Type']).to include('image/svg+xml')
badge_color = svg.xpath("//path[starts-with(@d, 'M62')]")[0].attributes['fill'].to_s
expected_badge_color = Gitlab::Ci::Badge::Coverage::Template::STATUS_COLOR[color]
expect(badge_color).to eq(expected_badge_color)
end
end
......@@ -94,7 +94,7 @@ describe('Multi-file store utils', () => {
{
action: commitActionTypes.move,
file_path: 'renamedFile',
content: null,
content: undefined,
encoding: 'text',
last_commit_id: undefined,
previous_path: 'prevPath',
......
......@@ -35,7 +35,7 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
# by relative_position and then ID
result = resolve_board_list_issues
expect(result.map(&:id)).to eq [issue3.id, issue1.id, issue2.id, issue4.id]
expect(result.map(&:id)).to eq [issue1.id, issue3.id, issue2.id, issue4.id]
expect(result.map(&:relative_position)).not_to include(nil)
end
......
......@@ -338,7 +338,7 @@ RSpec.describe Resolvers::IssuesResolver do
let_it_be(:relative_issue4) { create(:issue, project: project, relative_position: nil) }
it 'sorts issues ascending' do
expect(resolve_issues(sort: :relative_position_asc).to_a).to eq [relative_issue3, relative_issue1, relative_issue4, relative_issue2]
expect(resolve_issues(sort: :relative_position_asc).to_a).to eq [relative_issue3, relative_issue1, relative_issue2, relative_issue4]
end
end
......
......@@ -115,7 +115,7 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do
end
it 'orders exported issues by custom column(relative_position)' do
expected_issues = exportable.issues.order_relative_position_desc.order(id: :desc).map(&:to_json)
expected_issues = exportable.issues.reorder(::Gitlab::Database.nulls_first_order('relative_position', 'DESC')).order(id: :desc).map(&:to_json)
expect(json_writer).to receive(:write_relation_array).with(exportable_path, :issues, expected_issues)
......
......@@ -73,7 +73,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Iterator do
iterator.each_batch(of: 2) { |rel| positions.concat(rel.pluck(:relative_position, :id)) }
expect(positions).to eq(project.issues.order_relative_position_asc.order(id: :asc).pluck(:relative_position, :id))
expect(positions).to eq(project.issues.reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')).order(id: :asc).pluck(:relative_position, :id))
end
end
......@@ -85,7 +85,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Iterator do
iterator.each_batch(of: 2) { |rel| positions.concat(rel.pluck(:relative_position, :id)) }
expect(positions).to eq(project.issues.order_relative_position_desc.order(id: :desc).pluck(:relative_position, :id))
expect(positions).to eq(project.issues.reorder(::Gitlab::Database.nulls_first_order('relative_position', 'DESC')).order(id: :desc).pluck(:relative_position, :id))
end
end
......
......@@ -222,17 +222,15 @@ RSpec.describe Issue do
end
end
describe '#order_by_position_and_priority' do
describe '#order_by_relative_position' do
let(:project) { reusable_project }
let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
let!(:issue1) { create(:labeled_issue, project: project, labels: [p1]) }
let!(:issue2) { create(:labeled_issue, project: project, labels: [p2]) }
let!(:issue1) { create(:issue, project: project) }
let!(:issue2) { create(:issue, project: project) }
let!(:issue3) { create(:issue, project: project, relative_position: -200) }
let!(:issue4) { create(:issue, project: project, relative_position: -100) }
it 'returns ordered list' do
expect(project.issues.order_by_position_and_priority)
expect(project.issues.order_by_relative_position)
.to match [issue3, issue4, issue1, issue2]
end
end
......
......@@ -233,7 +233,7 @@ RSpec.describe 'getting an issue list for a project' do
let(:all_records) do
[
relative_issue5.iid, relative_issue3.iid, relative_issue1.iid,
relative_issue4.iid, relative_issue2.iid
relative_issue2.iid, relative_issue4.iid
]
end
end
......
......@@ -27,7 +27,7 @@ RSpec.describe IssuePlacementWorker do
it 'places all issues created at most 5 minutes before this one at the end, most recent last' do
expect { run_worker }.not_to change { irrelevant.reset.relative_position }
expect(project.issues.order_relative_position_asc)
expect(project.issues.order_by_relative_position)
.to eq([issue_e, issue_b, issue_a, issue, issue_c, issue_f, issue_d])
expect(project.issues.where(relative_position: nil)).not_to exist
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment