Commit 7374bf93 authored by Rajat Jain's avatar Rajat Jain

Refactor health status widget to use Apollo

Change the current health status widget implementation
to use apollo instead of using VueX and other bindings

Changelog: other
EE: true
MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6498
parent 9dce5fff
......@@ -425,6 +425,15 @@ module IssuablesHelper
}
end
def sidebar_status_data(issuable_sidebar, project)
{
iid: issuable_sidebar[:iid],
issuable_type: issuable_sidebar[:type],
full_path: project.full_path,
can_edit: issuable_sidebar.dig(:current_user, :can_edit).to_s
}
end
def parent
@project || @group
end
......
......@@ -81,7 +81,7 @@
#js-severity
- if issuable_sidebar.dig(:features_available, :health_status)
.js-sidebar-status-entry-point
.js-sidebar-status-entry-point{ data: sidebar_status_data(issuable_sidebar, @project) }
- if issuable_sidebar.has_key?(:confidential)
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe
......
<script>
import { mapGetters } from 'vuex';
import produce from 'immer';
import createFlash from '~/flash';
import { __ } from '~/locale';
import { __, sprintf } from '~/locale';
import { OPENED, REOPENED } from '~/notes/constants';
import { healthStatusQueries } from '../../constants';
import Status from './status.vue';
export default {
......@@ -10,27 +11,134 @@ export default {
Status,
},
props: {
mediator: {
issuableType: {
type: String,
required: true,
type: Object,
validator(mediatorObject) {
return Boolean(mediatorObject.store);
},
},
iid: {
type: String,
required: true,
},
fullPath: {
type: String,
required: true,
},
canUpdate: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapGetters(['getNoteableData']),
isOpen() {
return this.getNoteableData.state === OPENED || this.getNoteableData.state === REOPENED;
return this.issuableData?.state === OPENED || this.issuableData?.state === REOPENED;
},
isLoading() {
return this.$apollo.queries.issuableData.loading;
},
healthStatus() {
return this.issuableData?.healthStatus;
},
},
apollo: {
issuableData: {
query() {
return healthStatusQueries[this.issuableType].query;
},
variables() {
return {
fullPath: this.fullPath,
iid: this.iid,
};
},
update(data) {
return {
healthStatus: data.workspace?.issuable?.healthStatus,
state: data.workspace?.issuable?.state,
};
},
result({ data }) {
this.$emit('issuableData', {
healthStatus: data.workspace?.issuable?.healthStatus,
state: data.workspace?.issuable?.state,
});
},
error() {
createFlash({
message: sprintf(
__('Something went wrong while setting %{issuableType} health status.'),
{
issuableType: this.issuableType,
},
),
});
},
},
},
methods: {
handleDropdownClick(status) {
this.mediator.updateStatus(status).catch(() => {
createFlash({
message: __('Error occurred while updating the issue status'),
return this.$apollo
.mutate({
mutation: healthStatusQueries[this.issuableType].mutation,
variables: {
projectPath: this.fullPath,
iid: this.iid,
healthStatus: status,
},
update: (
_,
{
data: {
updateIssue: {
issue: { healthStatus },
},
},
},
) => {
const { defaultClient: client } = this.$apollo.provider.clients;
const queryVariables = { fullPath: this.fullPath, iid: this.iid };
const sourceData = client.readQuery({
query: healthStatusQueries[this.issuableType].query,
variables: queryVariables,
});
const data = produce(sourceData, (draftData) => {
draftData.workspace.issuable.healthStatus = healthStatus;
});
client.writeQuery({
query: healthStatusQueries[this.issuableType].query,
variables: queryVariables,
data,
});
},
optimisticResponse: {
__typename: 'Mutation', // eslint-disable-line @gitlab/require-i18n-strings
updateIssue: {
issue: {
healthStatus: status,
__typename: 'Issue', // eslint-disable-line @gitlab/require-i18n-strings
},
errors: [],
__typename: 'UpdateIssuePayload',
},
},
})
.then(({ data: { updateIssue } = {} } = {}) => {
const error = updateIssue?.errors[0];
if (error) {
createFlash({ message: error });
}
})
.catch(() => {
createFlash({
message: sprintf(__('Error occurred while updating the %{issuableType} status'), {
issuableType: this.issuableType,
}),
});
});
});
},
},
};
......@@ -39,9 +147,9 @@ export default {
<template>
<status
:is-open="isOpen"
:is-editable="mediator.store.editable"
:is-fetching="mediator.store.isFetching.status"
:status="mediator.store.status"
:is-editable="canUpdate"
:is-fetching="isLoading"
:status="healthStatus"
@onDropdownClick="handleDropdownClick"
/>
</template>
......@@ -5,9 +5,11 @@ import {
IssuableAttributeState as IssuableAttributeStateFoss,
issuableAttributesQueries as issuableAttributesQueriesFoss,
} from '~/sidebar/constants';
import updateStatusMutation from '~/sidebar/queries/updateStatus.mutation.graphql';
import epicAncestorsQuery from './queries/epic_ancestors.query.graphql';
import groupEpicsQuery from './queries/group_epics.query.graphql';
import groupIterationsQuery from './queries/group_iterations.query.graphql';
import issueHealthStatusQuery from './queries/issue_health_status.query.graphql';
import projectIssueEpicMutation from './queries/project_issue_epic.mutation.graphql';
import projectIssueEpicQuery from './queries/project_issue_epic.query.graphql';
import projectIssueIterationMutation from './queries/project_issue_iteration.mutation.graphql';
......@@ -128,3 +130,14 @@ export const ancestorsQueries = {
query: epicAncestorsQuery,
},
};
export const healthStatusQueries = {
[IssuableType.Issue]: {
mutation: updateStatusMutation,
query: issueHealthStatusQuery,
},
[IssuableType.Epic]: {
mutation: updateStatusMutation,
query: issueHealthStatusQuery,
},
};
......@@ -28,15 +28,18 @@ const mountWeightComponent = () => {
});
};
const mountStatusComponent = (mediator) => {
const mountStatusComponent = () => {
const el = document.querySelector('.js-sidebar-status-entry-point');
if (!el) {
return false;
}
const { iid, fullPath, issuableType, canEdit } = el.dataset;
return new Vue({
el,
apolloProvider,
store,
components: {
SidebarStatus,
......@@ -44,7 +47,10 @@ const mountStatusComponent = (mediator) => {
render: (createElement) =>
createElement('sidebar-status', {
props: {
mediator,
issuableType,
iid,
fullPath,
canUpdate: parseBoolean(canEdit),
},
}),
});
......
query issueHealthStatus($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
__typename
issuable: issue(iid: $iid) {
__typename
id
healthStatus
state
}
}
}
import Store from 'ee/sidebar/stores/sidebar_store';
import updateStatusMutation from '~/sidebar/queries/updateStatus.mutation.graphql';
import CESidebarMediator from '~/sidebar/sidebar_mediator';
export default class SidebarMediator extends CESidebarMediator {
......@@ -28,20 +27,4 @@ export default class SidebarMediator extends CESidebarMediator {
throw err;
});
}
updateStatus(healthStatus) {
this.store.setFetchingState('status', true);
return this.service
.updateWithGraphQl(updateStatusMutation, { healthStatus })
.then(({ data }) => {
if (data?.updateIssue?.errors?.length > 0) {
throw data.updateIssue.errors[0];
}
this.store.setStatus(data?.updateIssue?.issue?.healthStatus);
})
.catch((error) => {
throw error;
})
.finally(() => this.store.setFetchingState('status', false));
}
}
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { ApolloMutation, ApolloQuery } from 'vue-apollo';
import SidebarStatus from 'ee/sidebar/components/status/sidebar_status.vue';
import Status from 'ee/sidebar/components/status/status.vue';
import { healthStatusQueries } from 'ee/sidebar/constants';
Vue.use(Vuex);
const mutate = jest.fn().mockResolvedValue();
describe('SidebarStatus', () => {
let mediator;
let wrapper;
const createMediator = (states) => {
mediator = {
updateStatus: jest.fn().mockResolvedValue(),
store: {
isFetching: {
status: true,
const createWrapper = ({
issuableType = 'issue',
state = 'opened',
healthStatus = 'onTrack',
loading = false,
} = {}) => {
const $apollo = {
queries: {
issuableData: {
loading,
},
status: '',
...states,
},
mutate,
};
};
const createWrapper = ({ noteableState } = {}) => {
const store = new Vuex.Store({
getters: {
getNoteableData: () => ({ state: noteableState }),
},
});
wrapper = shallowMount(SidebarStatus, {
propsData: {
mediator,
issuableType,
iid: '1',
fullPath: 'foo/bar',
canUpdate: true,
},
data() {
return {
issuableData: {
state,
healthStatus,
},
};
},
sync: false,
mocks: { $apollo },
stubs: {
ApolloMutation,
ApolloQuery,
},
store,
});
};
beforeEach(() => {
createMediator();
createWrapper({
getters: {
getNoteableData: {},
},
});
createWrapper();
});
afterEach(() => {
......@@ -53,17 +58,16 @@ describe('SidebarStatus', () => {
describe('computed', () => {
describe.each`
noteableState | isOpen
state | isOpen
${'opened'} | ${true}
${'reopened'} | ${true}
${'closed'} | ${false}
`('isOpen', ({ noteableState, isOpen }) => {
`('isOpen', ({ state, isOpen }) => {
beforeEach(() => {
createMediator({ editable: true });
createWrapper({ noteableState });
createWrapper({ state });
});
it(`returns ${isOpen} when issue is ${noteableState}`, () => {
it(`returns ${isOpen} when issue is ${state}`, () => {
expect(wrapper.vm.isOpen).toBe(isOpen);
});
});
......@@ -76,10 +80,21 @@ describe('SidebarStatus', () => {
expect(wrapper.find(Status).exists()).toBe(true);
});
it('calls mediator status update when receiving an onDropdownClick event from Status component', () => {
it('calls apollo mutate when receiving an onDropdownClick event from Status component', () => {
wrapper.find(Status).vm.$emit('onDropdownClick', 'onTrack');
expect(mediator.updateStatus).toHaveBeenCalledWith('onTrack');
const mutationVariables = {
mutation: healthStatusQueries.issue.mutation,
update: expect.anything(),
optimisticResponse: expect.anything(),
variables: {
projectPath: 'foo/bar',
iid: '1',
healthStatus: 'onTrack',
},
};
expect(mutate).toHaveBeenCalledWith(mutationVariables);
});
});
});
import SidebarMediator from 'ee/sidebar/sidebar_mediator';
import waitForPromises from 'helpers/wait_for_promises';
import SidebarService from '~/sidebar/services/sidebar_service';
import CESidebarMediator from '~/sidebar/sidebar_mediator';
import CESidebarStore from '~/sidebar/stores/sidebar_store';
......@@ -27,28 +26,4 @@ describe('EE Sidebar mediator', () => {
expect(mediator.store.weight).toBe(mockData.weight);
expect(mediator.store.status).toBe(mockGraphQlData.project.issue.healthStatus);
});
it('updates status when updateStatus is called', () => {
const healthStatus = 'onTrack';
jest.spyOn(mediator.service, 'updateWithGraphQl').mockReturnValue(
Promise.resolve({
data: {
updateIssue: {
issue: {
healthStatus,
},
},
},
}),
);
expect(mediator.store.status).toBe('');
mediator.updateStatus(healthStatus);
return waitForPromises().then(() => {
expect(mediator.store.status).toBe(healthStatus);
});
});
});
......@@ -12742,6 +12742,9 @@ msgstr ""
msgid "Error occurred when saving reviewers"
msgstr ""
msgid "Error occurred while updating the %{issuableType} status"
msgstr ""
msgid "Error occurred while updating the issue status"
msgstr ""
......@@ -30369,6 +30372,9 @@ msgstr ""
msgid "Something went wrong while setting %{issuableType} confidentiality."
msgstr ""
msgid "Something went wrong while setting %{issuableType} health status."
msgstr ""
msgid "Something went wrong while setting %{issuableType} notifications."
msgstr ""
......
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