diff --git a/app/assets/javascripts/work_items/graphql/provider.js b/app/assets/javascripts/work_items/graphql/provider.js index 676fffb12d83dae4f24da34c2c3287188a3eab78..3ea7b118d5dae8a80cb31c4206eaaf15d2be562f 100644 --- a/app/assets/javascripts/work_items/graphql/provider.js +++ b/app/assets/javascripts/work_items/graphql/provider.js @@ -10,6 +10,11 @@ export function createApolloProvider() { const defaultClient = createDefaultClient(resolvers, { typeDefs, + cacheConfig: { + possibleTypes: { + LocalWorkItemWidget: ['LocalTitleWidget'], + }, + }, }); defaultClient.cache.writeQuery({ @@ -18,7 +23,7 @@ export function createApolloProvider() { id: '1', }, data: { - workItem: { + localWorkItem: { __typename: 'LocalWorkItem', id: '1', type: 'FEATURE', diff --git a/app/assets/javascripts/work_items/graphql/resolvers.js b/app/assets/javascripts/work_items/graphql/resolvers.js index 63d5234d0833fed67c4532b8d2d9916078753858..60ad290f493ac2c56e9a388f8048109fd4bce46d 100644 --- a/app/assets/javascripts/work_items/graphql/resolvers.js +++ b/app/assets/javascripts/work_items/graphql/resolvers.js @@ -22,7 +22,11 @@ export const resolvers = { }, }; - cache.writeQuery({ query: workItemQuery, variables: { id }, data: { workItem } }); + cache.writeQuery({ + query: workItemQuery, + variables: { id }, + data: { localWorkItem: workItem }, + }); return { __typename: 'LocalCreateWorkItemPayload', @@ -47,7 +51,11 @@ export const resolvers = { }, }; - cache.writeQuery({ query: workItemQuery, variables: { id: input.id }, data: { workItem } }); + cache.writeQuery({ + query: workItemQuery, + variables: { id: input.id }, + data: { localWorkItem: workItem }, + }); return { __typename: 'LocalUpdateWorkItemPayload', diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql index 177eea0032264ba295f50b13194798dae3d8967d..9091b08bf104b2088822bd031c9dc4ff4b90ecbe 100644 --- a/app/assets/javascripts/work_items/graphql/typedefs.graphql +++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql @@ -51,7 +51,7 @@ type LocalUpdateWorkItemPayload { } extend type Query { - workItem(id: ID!): LocalWorkItem! + localWorkItem(id: ID!): LocalWorkItem! } extend type Mutation { diff --git a/app/assets/javascripts/work_items/graphql/work_item.query.graphql b/app/assets/javascripts/work_items/graphql/work_item.query.graphql index 9f173f7c3028b11463643601e6590ffb57c4770e..c4fc7dad56a9b13671d3f16a605bccb49cbbb960 100644 --- a/app/assets/javascripts/work_items/graphql/work_item.query.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql @@ -1,7 +1,7 @@ #import './widget.fragment.graphql' query WorkItem($id: ID!) { - workItem(id: $id) @client { + localWorkItem(id: $id) @client { id type widgets { diff --git a/app/assets/javascripts/work_items/pages/work_item_root.vue b/app/assets/javascripts/work_items/pages/work_item_root.vue index 4262e169655377a840e0019d5016253c5ee81349..9e6ace5d48e52f9bd6990a7a930f42fd16ac46a0 100644 --- a/app/assets/javascripts/work_items/pages/work_item_root.vue +++ b/app/assets/javascripts/work_items/pages/work_item_root.vue @@ -36,6 +36,9 @@ export default { id: this.id, }; }, + update(data) { + return data.localWorkItem; + }, }, }, computed: { diff --git a/app/graphql/resolvers/work_item_resolver.rb b/app/graphql/resolvers/work_item_resolver.rb new file mode 100644 index 0000000000000000000000000000000000000000..b1733d657e49dee361b0a946d0f27e426ccb5fd8 --- /dev/null +++ b/app/graphql/resolvers/work_item_resolver.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Resolvers + class WorkItemResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + authorize :read_issue + + type Types::WorkItemType, null: true + + argument :id, ::Types::GlobalIDType[::WorkItem], required: true, description: 'Global ID of the work item.' + + def resolve(id:) + work_item = authorized_find!(id: id) + return unless Feature.enabled?(:work_items, work_item.project) + + work_item + end + + private + + def find_object(id:) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[::WorkItem].coerce_isolated_input(id) + GitlabSchema.find_by_gid(id) + end + end +end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 4a4d6727c3ffd8fba1214d0ccd51afbb65fad85b..6e89a51618ff0619471808a3c26d54171fc8f007 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -87,6 +87,11 @@ class QueryType < ::Types::BaseObject argument :id, ::Types::GlobalIDType[::Issue], required: true, description: 'Global ID of the issue.' end + field :work_item, Types::WorkItemType, + null: true, + resolver: Resolvers::WorkItemResolver, + description: 'Find a work item. Returns `null` if `work_items` feature flag is disabled.' + field :merge_request, Types::MergeRequestType, null: true, description: 'Find a merge request.' do diff --git a/app/graphql/types/work_item_type.rb b/app/graphql/types/work_item_type.rb index 15a5557b4892a12c2833b4a27e2e514188bed934..78cef150b11d9a2a42ef4527a8ae75a707336013 100644 --- a/app/graphql/types/work_item_type.rb +++ b/app/graphql/types/work_item_type.rb @@ -12,6 +12,8 @@ class WorkItemType < BaseObject description: 'Global ID of the work item.' field :iid, GraphQL::Types::ID, null: false, description: 'Internal ID of the work item.' + field :lock_version, GraphQL::Types::Int, null: false, + description: 'Lock version of the work item. Incremented each time the work item is updated.' field :state, WorkItemStateEnum, null: false, description: 'State of the work item.' field :title, GraphQL::Types::String, null: false, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index f126c828240c9b3d813d574faff7ba9f019d3d81..15c380795b2158144b41555433121254a4b180c2 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -560,6 +560,18 @@ Returns [`Vulnerability`](#vulnerability). | ---- | ---- | ----------- | | <a id="queryvulnerabilityid"></a>`id` | [`VulnerabilityID!`](#vulnerabilityid) | Global ID of the Vulnerability. | +### `Query.workItem` + +Find a work item. Returns `null` if `work_items` feature flag is disabled. + +Returns [`WorkItem`](#workitem). + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="queryworkitemid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. | + ## `Mutation` type The `Mutation` type contains all the mutations you can execute. @@ -16726,6 +16738,7 @@ Represents vulnerability letter grades with associated projects. | <a id="workitemdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. | | <a id="workitemid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. | | <a id="workitemiid"></a>`iid` | [`ID!`](#id) | Internal ID of the work item. | +| <a id="workitemlockversion"></a>`lockVersion` | [`Int!`](#int) | Lock version of the work item. Incremented each time the work item is updated. | | <a id="workitemstate"></a>`state` | [`WorkItemState!`](#workitemstate) | State of the work item. | | <a id="workitemtitle"></a>`title` | [`String!`](#string) | Title of the work item. | | <a id="workitemtitlehtml"></a>`titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. | diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index a98722bc4656d753dcfa38b5301d616a82623076..42d04153ef0e77582aa31c8683cf7428e53482d3 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -1,5 +1,5 @@ export const workItemQueryResponse = { - workItem: { + localWorkItem: { __typename: 'LocalWorkItem', id: '1', type: 'FEATURE', diff --git a/spec/graphql/resolvers/work_item_resolver_spec.rb b/spec/graphql/resolvers/work_item_resolver_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c7e2beecb515500247ea6b59a8ebfe6190c140e6 --- /dev/null +++ b/spec/graphql/resolvers/work_item_resolver_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::WorkItemResolver do + include GraphqlHelpers + + describe '#resolve' do + let_it_be(:developer) { create(:user) } + let_it_be(:project) { create(:project, :private).tap { |project| project.add_developer(developer) } } + let_it_be(:work_item) { create(:work_item, project: project) } + + let(:current_user) { developer } + + subject(:resolved_work_item) { resolve_work_item('id' => work_item.to_gid.to_s) } + + context 'when the user can read the work item' do + it { is_expected.to eq(work_item) } + end + + context 'when the user can not read the work item' do + let(:current_user) { create(:user) } + + it 'raises a resource not available error' do + expect { resolved_work_item }.to raise_error(::Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when the work_items feature flag is disabled' do + before do + stub_feature_flags(work_items: false) + end + + it { is_expected.to be_nil } + end + end + + private + + def resolve_work_item(args = {}) + resolve(described_class, args: args, ctx: { current_user: current_user }) + end +end diff --git a/spec/graphql/types/work_item_type_spec.rb b/spec/graphql/types/work_item_type_spec.rb index d0bab477f36cb95053d791b61d995bd817b935c8..4e44123d475dc328cd7baad720154a3a5d584614 100644 --- a/spec/graphql/types/work_item_type_spec.rb +++ b/spec/graphql/types/work_item_type_spec.rb @@ -5,8 +5,10 @@ RSpec.describe GitlabSchema.types['WorkItem'] do specify { expect(described_class.graphql_name).to eq('WorkItem') } + specify { expect(described_class).to require_graphql_authorizations(:read_issue) } + it 'has specific fields' do - fields = %i[description description_html id iid state title title_html work_item_type] + fields = %i[description description_html id iid lock_version state title title_html work_item_type] fields.each do |field_name| expect(described_class).to have_graphql_fields(*fields) diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5ed9a3bf7e61ec910002062495e9485a4de36293 --- /dev/null +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.work_item(id)' do + include GraphqlHelpers + + let_it_be(:developer) { create(:user) } + let_it_be(:project) { create(:project, :private).tap { |project| project.add_developer(developer) } } + let_it_be(:work_item) { create(:work_item, project: project) } + + let(:current_user) { developer } + let(:work_item_data) { graphql_data['workItem'] } + let(:work_item_fields) { all_graphql_fields_for('WorkItem') } + + let(:query) do + graphql_query_for('workItem', { 'id' => work_item.to_gid.to_s }, work_item_fields) + end + + context 'when the user can read the work item' do + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + + it 'returns all fields' do + expect(work_item_data).to include( + 'description' => work_item.description, + 'id' => work_item.to_gid.to_s, + 'iid' => work_item.iid.to_s, + 'lockVersion' => work_item.lock_version, + 'state' => "OPEN", + 'title' => work_item.title, + 'workItemType' => hash_including('id' => work_item.work_item_type.to_gid.to_s) + ) + end + end + + context 'when the user can not read the work item' do + let(:current_user) { create(:user) } + + before do + post_graphql(query) + end + + it 'returns an access error' do + expect(work_item_data).to be_nil + expect(graphql_errors).to contain_exactly( + hash_including('message' => ::Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR) + ) + end + end + + context 'when the work_items feature flag is disabled' do + before do + stub_feature_flags(work_items: false) + end + + it 'returns nil' do + post_graphql(query) + + expect(work_item_data).to be_nil + end + end +end