Skip to content
Snippets Groups Projects
Verified Commit 4306d4a1 authored by Phil Hughes's avatar Phil Hughes
Browse files

Subscription type for issuable todo update

This adds a new subscription to GraphQL for when an issuable todo is updated.

#449018
parent 1e2eaf09
No related branches found
No related tags found
2 merge requests!170053Security patch upgrade alert: Only expose to admins 17-4,!147456Subscription type for issuable todo update
Showing
with 193 additions and 11 deletions
......@@ -48,11 +48,11 @@ export default {
},
data() {
return {
todoId: null,
loading: false,
};
},
apollo: {
// eslint-disable-next-line @gitlab/vue-no-undef-apollo-properties
todoId: {
query() {
return todoQueries[this.issuableType].query;
......@@ -85,6 +85,21 @@ export default {
}),
});
},
subscribeToMore: {
document() {
return todoQueries[this.issuableType].subscription;
},
variables() {
return {
issuableId: this.issuableId,
};
},
skip() {
return (
!this.glFeatures.realtimeIssuableTodo || !todoQueries[this.issuableType].subscription
);
},
},
},
},
computed: {
......@@ -146,7 +161,6 @@ export default {
query: this.todoIdQuery,
variables: this.todoIdQueryVariables,
};
const sourceData = store.readQuery(queryProps);
const data = produce(sourceData, (draftState) => {
draftState.workspace.issuable.currentUserTodos.nodes = this.hasTodo ? [] : [todo];
......
......@@ -42,6 +42,7 @@ import mergeRequestReferenceQuery from './merge_request_reference.query.graphql'
import mergeRequestSubscribed from './merge_request_subscribed.query.graphql';
import mergeRequestTimeTrackingQuery from './merge_request_time_tracking.query.graphql';
import mergeRequestTodoQuery from './merge_request_todo.query.graphql';
import mergeRequestTodoSubscription from './merge_request_todo.subscription.graphql';
import todoCreateMutation from './todo_create.mutation.graphql';
import todoMarkDoneMutation from './todo_mark_done.mutation.graphql';
import updateEpicConfidentialMutation from './update_epic_confidential.mutation.graphql';
......@@ -278,6 +279,7 @@ export const todoQueries = {
},
[TYPE_MERGE_REQUEST]: {
query: mergeRequestTodoQuery,
subscription: mergeRequestTodoSubscription,
},
};
......
subscription issuableTodoUpdated($issuableId: IssuableID!) {
issuableTodoUpdated(issuableId: $issuableId) {
... on MergeRequest {
id
currentUserTodos(state: pending) {
nodes {
id
}
}
}
}
}
......@@ -46,6 +46,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:notifications_todos_buttons, current_user)
push_frontend_feature_flag(:reviewer_assign_drawer, current_user)
push_frontend_feature_flag(:issue_autocomplete_backend_filtering, project)
push_frontend_feature_flag(:realtime_issuable_todo, current_user)
end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :diffs, :rapid_diffs, :discussions]
......
......@@ -72,6 +72,15 @@ def self.work_item_updated(work_item)
::GitlabSchema.subscriptions.trigger('workItemUpdated', { work_item_id: work_item.to_gid }, work_item)
end
def self.issuable_todo_updated(issuable, user)
return if ::Feature.disabled?(:realtime_issuable_todo, user)
return unless issuable.respond_to?(:to_gid)
::GitlabSchema.subscriptions.trigger(
:issuable_todo_updated, { issuable_id: issuable.to_gid }, issuable
)
end
end
GraphqlTriggers.prepend_mod
......@@ -67,6 +67,11 @@ class SubscriptionType < ::Types::BaseObject
field :merge_request_diff_generated,
subscription: Subscriptions::IssuableUpdated, null: true,
description: 'Triggered when a merge request diff is generated.'
field :issuable_todo_updated,
subscription: Subscriptions::IssuableUpdated, null: true,
description: 'Triggered when a todo on an issuable is updated.',
alpha: { milestone: '17.5' }
end
end
......
......@@ -184,6 +184,8 @@ def resolve_todos_for_target(target, current_user)
attributes = attributes_for_target(target)
resolve_todos(pending_todos([current_user], attributes), current_user)
GraphqlTriggers.issuable_todo_updated(target, current_user)
end
# Resolves all todos related to target for all users
......@@ -212,6 +214,8 @@ def resolve_todo(todo, current_user, resolution: :done, resolved_by_action: :sys
todo.update(state: resolution, resolved_by_action: resolved_by_action)
GraphqlTriggers.issuable_todo_updated(todo.target, current_user)
current_user.update_todos_count_cache
end
......
---
name: realtime_issuable_todo
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/449018
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147456
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/495542
milestone: '17.5'
group: group::code review
type: beta
default_enabled: false
......@@ -2,13 +2,16 @@ import { GlIcon, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { createMockSubscription } from 'mock-apollo-client';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
import mergeRequestTodoQuery from '~/sidebar/queries/merge_request_todo.query.graphql';
import mergeRequestTodoSubscription from '~/sidebar/queries/merge_request_todo.subscription.graphql';
import TodoButton from '~/sidebar/components/todo_toggle/todo_button.vue';
import { todosResponse, noTodosResponse } from '../../mock_data';
import { todosResponse, noMergeRequestTodosResponse, noTodosResponse } from '../../mock_data';
jest.mock('~/alert');
......@@ -16,18 +19,17 @@ Vue.use(VueApollo);
describe('Sidebar Todo Widget', () => {
let wrapper;
let fakeApollo;
const findTodoButton = () => wrapper.findComponent(TodoButton);
const createComponent = ({
todosQueryHandler = jest.fn().mockResolvedValue(noTodosResponse),
provide = {},
propsData = {},
apolloProvider = createMockApollo([[epicTodoQuery, todosQueryHandler]]),
} = {}) => {
fakeApollo = createMockApollo([[epicTodoQuery, todosQueryHandler]]);
wrapper = shallowMount(SidebarTodoWidget, {
apolloProvider: fakeApollo,
apolloProvider,
provide: {
canUpdate: true,
isClassicSidebar: true,
......@@ -38,14 +40,11 @@ describe('Sidebar Todo Widget', () => {
issuableIid: '1',
issuableId: 'gid://gitlab/Epic/4',
issuableType: 'epic',
...propsData,
},
});
};
afterEach(() => {
fakeApollo = null;
});
describe('when user does not have a todo for the issuable', () => {
beforeEach(() => {
createComponent();
......@@ -143,4 +142,76 @@ describe('Sidebar Todo Widget', () => {
expect(findTodoButton().attributes().disabled).toBe('true');
});
});
describe('for merge request issuable type', () => {
let mockSubscription;
let subscriptionHandler;
let apolloProvider;
beforeEach(() => {
mockSubscription = createMockSubscription();
subscriptionHandler = jest.fn().mockReturnValue(mockSubscription);
apolloProvider = createMockApollo([
[mergeRequestTodoQuery, jest.fn().mockResolvedValue(noMergeRequestTodosResponse)],
]);
apolloProvider.defaultClient.setRequestHandler(
mergeRequestTodoSubscription,
subscriptionHandler,
);
});
describe('realtimeIssuableTodo feature flag is enabled', () => {
beforeEach(() => {
createComponent({
provide: {
glFeatures: { realtimeIssuableTodo: true },
},
propsData: { issuableType: 'merge_request' },
apolloProvider,
});
return nextTick();
});
it('updates todo button to have a new todo when subscription receives data', async () => {
mockSubscription.next({
data: {
issuableTodoUpdated: {
__typename: 'MergeRequest',
id: 'gid://gitlab/MergeRequest/1',
currentUserTodos: {
nodes: [{ id: 1 }],
},
},
},
});
await nextTick();
expect(subscriptionHandler).toHaveBeenCalled();
expect(findTodoButton().props('isTodo')).toBe(true);
});
});
describe('realtimeIssuableTodo feature flag is disabled', () => {
beforeEach(() => {
createComponent({
provide: {
glFeatures: { realtimeIssuableTodo: false },
},
propsData: { issuableType: 'merge_request' },
apolloProvider,
});
return nextTick();
});
it('does not subscribe to more', async () => {
await nextTick();
expect(subscriptionHandler).not.toHaveBeenCalled();
});
});
});
});
......@@ -868,6 +868,22 @@ export const todosResponse = {
},
};
export const noMergeRequestTodosResponse = {
data: {
workspace: {
id: '1',
__typename: 'Project',
issuable: {
__typename: 'MergeRequest',
id: 'gid://gitlab/MergeRequest/1',
currentUserTodos: {
nodes: [],
},
},
},
},
};
export const noTodosResponse = {
data: {
workspace: {
......
......@@ -172,4 +172,30 @@
end
end
end
describe '.issuable_todo_updated' do
let_it_be(:user) { create(:user) }
context 'when realtime_issuable_todo feature flag is disabled' do
before do
stub_feature_flags(realtime_issuable_todo: false)
end
it 'does not trigger the issuable_todo_updated subscription' do
expect(GitlabSchema.subscriptions).not_to receive(:trigger)
described_class.issuable_todo_updated(issuable, user)
end
end
it 'triggers the issuable_todo_updated subscription' do
expect(GitlabSchema.subscriptions).to receive(:trigger).with(
:issuable_todo_updated,
{ issuable_id: issuable.to_gid },
issuable
).and_call_original
described_class.issuable_todo_updated(issuable, user)
end
end
end
......@@ -17,6 +17,7 @@
merge_request_approval_state_updated
merge_request_diff_generated
work_item_updated
issuable_todo_updated
]
expect(described_class).to include_graphql_fields(*expected_fields)
......
......@@ -383,6 +383,12 @@
expect(second_todo.reload).to be_done
end
it 'calls GraphQL.issuable_todo_updated' do
expect(GraphqlTriggers).to receive(:issuable_todo_updated).with(issue, john_doe)
service.resolve_todos_for_target(issue, john_doe)
end
describe 'cached counts' do
it 'updates when todos change' do
create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
......@@ -1415,6 +1421,12 @@
end.to change { todo.resolved_by_mark_done? }.to(true)
end
it 'calls GraphQL.issuable_todo_updated' do
expect(GraphqlTriggers).to receive(:issuable_todo_updated).with(todo.target, john_doe)
service.resolve_todo(todo, john_doe)
end
context 'cached counts' do
it 'updates when todos change' do
expect(john_doe.todos_done_count).to eq(0)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment