diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb
index 8f18c2cbce5..9c7931a4edb 100644
--- a/app/graphql/resolvers/work_items_resolver.rb
+++ b/app/graphql/resolvers/work_items_resolver.rb
@@ -35,6 +35,12 @@ module Resolvers
private
+ def preloads
+ {
+ last_edited_by: :last_edited_by
+ }
+ end
+
# Allows to apply lookahead for fields
# selected from WidgetInterface
override :node_selection
diff --git a/app/graphql/types/work_items/widgets/description_type.rb b/app/graphql/types/work_items/widgets/description_type.rb
index 4c365a67bfd..4861f7f46d8 100644
--- a/app/graphql/types/work_items/widgets/description_type.rb
+++ b/app/graphql/types/work_items/widgets/description_type.rb
@@ -13,8 +13,18 @@ module Types
implements Types::WorkItems::WidgetInterface
field :description, GraphQL::Types::String,
- null: true,
- description: 'Description of the work item.'
+ null: true,
+ description: 'Description of the work item.'
+ field :edited, GraphQL::Types::Boolean,
+ null: false,
+ description: 'Whether the description has been edited since the work item was created.',
+ method: :edited?
+ field :last_edited_at, Types::TimeType,
+ null: true,
+ description: 'Timestamp of when the work item\'s description was last edited.'
+ field :last_edited_by, Types::UserType,
+ null: true,
+ description: 'User that made the last edit to the work item\'s description.'
markdown_field :description_html, null: true do |resolved_object|
resolved_object.work_item
diff --git a/app/models/work_items/widgets/description.rb b/app/models/work_items/widgets/description.rb
index 1e84d172bef..ec3b7957c79 100644
--- a/app/models/work_items/widgets/description.rb
+++ b/app/models/work_items/widgets/description.rb
@@ -3,7 +3,13 @@
module WorkItems
module Widgets
class Description < Base
- delegate :description, to: :work_item
+ delegate :description, :edited?, :last_edited_at, to: :work_item
+
+ def last_edited_by
+ return unless work_item.edited?
+
+ work_item.last_edited_by
+ end
end
end
end
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index fb014d9522d..6db6184892b 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -19367,6 +19367,9 @@ Represents a description widget.
| ---- | ---- | ----------- |
| `description` | [`String`](#string) | Description of the work item. |
| `descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. |
+| `edited` | [`Boolean!`](#boolean) | Whether the description has been edited since the work item was created. |
+| `lastEditedAt` | [`Time`](#time) | Timestamp of when the work item's description was last edited. |
+| `lastEditedBy` | [`UserCore`](#usercore) | User that made the last edit to the work item's description. |
| `type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
### `WorkItemWidgetHierarchy`
diff --git a/doc/ci/runners/saas/linux_saas_runner.md b/doc/ci/runners/saas/linux_saas_runner.md
index a6430b4b64d..a7d1b8722a5 100644
--- a/doc/ci/runners/saas/linux_saas_runner.md
+++ b/doc/ci/runners/saas/linux_saas_runner.md
@@ -18,7 +18,7 @@ For Free, Premium, and Ultimate plan customers, jobs on these instances consume
| | Small | Medium | Large |
|-------------------|---------------------------|---------------------------|--------------------------|
| Specs | 1 vCPU, 3.75GB RAM | 2 vCPUs, 8GB RAM | 4 vCPUs, 16GB RAM |
-| GitLab CI/CD tags | `saas-linux-medium-amd64` | `saas-linux-medium-amd64` | `saas-linux-large-amd64` |
+| GitLab CI/CD tags | `saas-linux-small-amd64` | `saas-linux-medium-amd64` | `saas-linux-large-amd64` |
| Subscription | Free, Premium, Ultimate | Free, Premium, Ultimate | Premium, Ultimate |
The `small` machine type is the default. Your job runs on this machine type if you don't specify
diff --git a/spec/factories/work_items.rb b/spec/factories/work_items.rb
index 267ea9710b3..205b071a5d4 100644
--- a/spec/factories/work_items.rb
+++ b/spec/factories/work_items.rb
@@ -23,5 +23,9 @@ FactoryBot.define do
issue_type { :incident }
association :work_item_type, :default, :incident
end
+
+ trait :last_edited_by_user do
+ association :last_edited_by, factory: :user
+ end
end
end
diff --git a/spec/graphql/types/work_item_type_spec.rb b/spec/graphql/types/work_item_type_spec.rb
index c556424b0b4..11b02a88dbd 100644
--- a/spec/graphql/types/work_item_type_spec.rb
+++ b/spec/graphql/types/work_item_type_spec.rb
@@ -28,8 +28,6 @@ RSpec.describe GitlabSchema.types['WorkItem'] do
closed_at
]
- fields.each do |field_name|
- expect(described_class).to have_graphql_fields(*fields)
- end
+ expect(described_class).to have_graphql_fields(*fields)
end
end
diff --git a/spec/graphql/types/work_items/widgets/description_type_spec.rb b/spec/graphql/types/work_items/widgets/description_type_spec.rb
index 5ade1fe4aa2..aee388ce82a 100644
--- a/spec/graphql/types/work_items/widgets/description_type_spec.rb
+++ b/spec/graphql/types/work_items/widgets/description_type_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Types::WorkItems::Widgets::DescriptionType do
it 'exposes the expected fields' do
- expected_fields = %i[description description_html type]
+ expected_fields = %i[description description_html edited last_edited_at last_edited_by type]
expect(described_class).to have_graphql_fields(*expected_fields)
end
diff --git a/spec/models/work_items/widgets/description_spec.rb b/spec/models/work_items/widgets/description_spec.rb
index 8359db31bff..c24dc9cfb9c 100644
--- a/spec/models/work_items/widgets/description_spec.rb
+++ b/spec/models/work_items/widgets/description_spec.rb
@@ -3,7 +3,10 @@
require 'spec_helper'
RSpec.describe WorkItems::Widgets::Description do
- let_it_be(:work_item) { create(:work_item, description: '# Title') }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:work_item, refind: true) do
+ create(:work_item, description: 'Title', last_edited_at: 10.days.ago, last_edited_by: user)
+ end
describe '.type' do
subject { described_class.type }
@@ -22,4 +25,42 @@ RSpec.describe WorkItems::Widgets::Description do
it { is_expected.to eq(work_item.description) }
end
+
+ describe '#edited?' do
+ subject { described_class.new(work_item).edited? }
+
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#last_edited_at' do
+ subject { described_class.new(work_item).last_edited_at }
+
+ it { is_expected.to eq(work_item.last_edited_at) }
+ end
+
+ describe '#last_edited_by' do
+ subject { described_class.new(work_item).last_edited_by }
+
+ context 'when the work item is edited' do
+ context 'when last edited user still exists in the DB' do
+ it { is_expected.to eq(user) }
+ end
+
+ context 'when last edited user no longer exists' do
+ before do
+ work_item.update!(last_edited_by: nil)
+ end
+
+ it { is_expected.to eq(User.ghost) }
+ end
+ end
+
+ context 'when the work item is not edited yet' do
+ before do
+ work_item.update!(last_edited_at: nil)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb
index 1dd0046f782..69f8d1cac74 100644
--- a/spec/requests/api/graphql/project/work_items_spec.rb
+++ b/spec/requests/api/graphql/project/work_items_spec.rb
@@ -10,7 +10,10 @@ RSpec.describe 'getting an work item list for a project' do
let_it_be(:current_user) { create(:user) }
let_it_be(:item1) { create(:work_item, project: project, discussion_locked: true, title: 'item1') }
- let_it_be(:item2) { create(:work_item, project: project, title: 'item2') }
+ let_it_be(:item2) do
+ create(:work_item, project: project, title: 'item2', last_edited_by: current_user, last_edited_at: 1.day.ago)
+ end
+
let_it_be(:confidential_item) { create(:work_item, confidential: true, project: project, title: 'item3') }
let_it_be(:other_item) { create(:work_item) }
@@ -75,6 +78,40 @@ RSpec.describe 'getting an work item list for a project' do
end
end
+ context 'when fetching description edit information' do
+ let(:fields) do
+ <<~GRAPHQL
+ nodes {
+ widgets {
+ type
+ ... on WorkItemWidgetDescription {
+ edited
+ lastEditedAt
+ lastEditedBy {
+ webPath
+ username
+ }
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'avoids N+1 queries' do
+ post_graphql(query, current_user: current_user) # warm-up
+
+ control = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: current_user)
+ end
+ expect_graphql_errors_to_be_empty
+
+ create_list(:work_item, 3, :last_edited_by_user, last_edited_at: 1.week.ago, project: project)
+
+ expect_graphql_errors_to_be_empty
+ expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
+ end
+ end
+
context 'when filtering by search' do
it_behaves_like 'query with a search term' do
let(:issuable_data) { items_data }
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index 34644e5893a..e4bb4109c76 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -14,7 +14,10 @@ RSpec.describe 'Query.work_item(id)' do
project: project,
description: '- List item',
start_date: Date.today,
- due_date: 1.week.from_now
+ due_date: 1.week.from_now,
+ created_at: 1.week.ago,
+ last_edited_at: 1.day.ago,
+ last_edited_by: guest
)
end
@@ -67,6 +70,12 @@ RSpec.describe 'Query.work_item(id)' do
... on WorkItemWidgetDescription {
description
descriptionHtml
+ edited
+ lastEditedBy {
+ webPath
+ username
+ }
+ lastEditedAt
}
}
GRAPHQL
@@ -79,7 +88,13 @@ RSpec.describe 'Query.work_item(id)' do
hash_including(
'type' => 'DESCRIPTION',
'description' => work_item.description,
- 'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {})
+ 'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {}),
+ 'edited' => true,
+ 'lastEditedAt' => work_item.last_edited_at.iso8601,
+ 'lastEditedBy' => {
+ 'webPath' => "/#{guest.full_path}",
+ 'username' => guest.username
+ }
)
)
)