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 ...@@ -425,6 +425,15 @@ module IssuablesHelper
} }
end 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 def parent
@project || @group @project || @group
end end
......
...@@ -81,7 +81,7 @@ ...@@ -81,7 +81,7 @@
#js-severity #js-severity
- if issuable_sidebar.dig(:features_available, :health_status) - 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) - 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#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe
......
<script> <script>
import { mapGetters } from 'vuex'; import produce from 'immer';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __, sprintf } from '~/locale';
import { OPENED, REOPENED } from '~/notes/constants'; import { OPENED, REOPENED } from '~/notes/constants';
import { healthStatusQueries } from '../../constants';
import Status from './status.vue'; import Status from './status.vue';
export default { export default {
...@@ -10,27 +11,134 @@ export default { ...@@ -10,27 +11,134 @@ export default {
Status, Status,
}, },
props: { props: {
mediator: { issuableType: {
type: String,
required: true, required: true,
type: Object, },
validator(mediatorObject) { iid: {
return Boolean(mediatorObject.store); type: String,
}, required: true,
},
fullPath: {
type: String,
required: true,
},
canUpdate: {
type: Boolean,
required: false,
default: false,
}, },
}, },
computed: { computed: {
...mapGetters(['getNoteableData']),
isOpen() { 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: { methods: {
handleDropdownClick(status) { handleDropdownClick(status) {
this.mediator.updateStatus(status).catch(() => { return this.$apollo
createFlash({ .mutate({
message: __('Error occurred while updating the issue status'), 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 { ...@@ -39,9 +147,9 @@ export default {
<template> <template>
<status <status
:is-open="isOpen" :is-open="isOpen"
:is-editable="mediator.store.editable" :is-editable="canUpdate"
:is-fetching="mediator.store.isFetching.status" :is-fetching="isLoading"
:status="mediator.store.status" :status="healthStatus"
@onDropdownClick="handleDropdownClick" @onDropdownClick="handleDropdownClick"
/> />
</template> </template>
...@@ -5,9 +5,11 @@ import { ...@@ -5,9 +5,11 @@ import {
IssuableAttributeState as IssuableAttributeStateFoss, IssuableAttributeState as IssuableAttributeStateFoss,
issuableAttributesQueries as issuableAttributesQueriesFoss, issuableAttributesQueries as issuableAttributesQueriesFoss,
} from '~/sidebar/constants'; } from '~/sidebar/constants';
import updateStatusMutation from '~/sidebar/queries/updateStatus.mutation.graphql';
import epicAncestorsQuery from './queries/epic_ancestors.query.graphql'; import epicAncestorsQuery from './queries/epic_ancestors.query.graphql';
import groupEpicsQuery from './queries/group_epics.query.graphql'; import groupEpicsQuery from './queries/group_epics.query.graphql';
import groupIterationsQuery from './queries/group_iterations.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 projectIssueEpicMutation from './queries/project_issue_epic.mutation.graphql';
import projectIssueEpicQuery from './queries/project_issue_epic.query.graphql'; import projectIssueEpicQuery from './queries/project_issue_epic.query.graphql';
import projectIssueIterationMutation from './queries/project_issue_iteration.mutation.graphql'; import projectIssueIterationMutation from './queries/project_issue_iteration.mutation.graphql';
...@@ -128,3 +130,14 @@ export const ancestorsQueries = { ...@@ -128,3 +130,14 @@ export const ancestorsQueries = {
query: epicAncestorsQuery, query: epicAncestorsQuery,
}, },
}; };
export const healthStatusQueries = {
[IssuableType.Issue]: {
mutation: updateStatusMutation,
query: issueHealthStatusQuery,
},
[IssuableType.Epic]: {
mutation: updateStatusMutation,
query: issueHealthStatusQuery,
},
};
...@@ -28,15 +28,18 @@ const mountWeightComponent = () => { ...@@ -28,15 +28,18 @@ const mountWeightComponent = () => {
}); });
}; };
const mountStatusComponent = (mediator) => { const mountStatusComponent = () => {
const el = document.querySelector('.js-sidebar-status-entry-point'); const el = document.querySelector('.js-sidebar-status-entry-point');
if (!el) { if (!el) {
return false; return false;
} }
const { iid, fullPath, issuableType, canEdit } = el.dataset;
return new Vue({ return new Vue({
el, el,
apolloProvider,
store, store,
components: { components: {
SidebarStatus, SidebarStatus,
...@@ -44,7 +47,10 @@ const mountStatusComponent = (mediator) => { ...@@ -44,7 +47,10 @@ const mountStatusComponent = (mediator) => {
render: (createElement) => render: (createElement) =>
createElement('sidebar-status', { createElement('sidebar-status', {
props: { 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 Store from 'ee/sidebar/stores/sidebar_store';
import updateStatusMutation from '~/sidebar/queries/updateStatus.mutation.graphql';
import CESidebarMediator from '~/sidebar/sidebar_mediator'; import CESidebarMediator from '~/sidebar/sidebar_mediator';
export default class SidebarMediator extends CESidebarMediator { export default class SidebarMediator extends CESidebarMediator {
...@@ -28,20 +27,4 @@ export default class SidebarMediator extends CESidebarMediator { ...@@ -28,20 +27,4 @@ export default class SidebarMediator extends CESidebarMediator {
throw err; 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 { shallowMount } from '@vue/test-utils';
import Vue from 'vue'; import { ApolloMutation, ApolloQuery } from 'vue-apollo';
import Vuex from 'vuex';
import SidebarStatus from 'ee/sidebar/components/status/sidebar_status.vue'; import SidebarStatus from 'ee/sidebar/components/status/sidebar_status.vue';
import Status from 'ee/sidebar/components/status/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', () => { describe('SidebarStatus', () => {
let mediator;
let wrapper; let wrapper;
const createMediator = (states) => { const createWrapper = ({
mediator = { issuableType = 'issue',
updateStatus: jest.fn().mockResolvedValue(), state = 'opened',
store: { healthStatus = 'onTrack',
isFetching: { loading = false,
status: true, } = {}) => {
const $apollo = {
queries: {
issuableData: {
loading,
}, },
status: '',
...states,
}, },
mutate,
}; };
};
const createWrapper = ({ noteableState } = {}) => {
const store = new Vuex.Store({
getters: {
getNoteableData: () => ({ state: noteableState }),
},
});
wrapper = shallowMount(SidebarStatus, { wrapper = shallowMount(SidebarStatus, {
propsData: { propsData: {
mediator, issuableType,
iid: '1',
fullPath: 'foo/bar',
canUpdate: true,
},
data() {
return {
issuableData: {
state,
healthStatus,
},
};
},
sync: false,
mocks: { $apollo },
stubs: {
ApolloMutation,
ApolloQuery,
}, },
store,
}); });
}; };
beforeEach(() => { beforeEach(() => {
createMediator(); createWrapper();
createWrapper({
getters: {
getNoteableData: {},
},
});
}); });
afterEach(() => { afterEach(() => {
...@@ -53,17 +58,16 @@ describe('SidebarStatus', () => { ...@@ -53,17 +58,16 @@ describe('SidebarStatus', () => {
describe('computed', () => { describe('computed', () => {
describe.each` describe.each`
noteableState | isOpen state | isOpen
${'opened'} | ${true} ${'opened'} | ${true}
${'reopened'} | ${true} ${'reopened'} | ${true}
${'closed'} | ${false} ${'closed'} | ${false}
`('isOpen', ({ noteableState, isOpen }) => { `('isOpen', ({ state, isOpen }) => {
beforeEach(() => { beforeEach(() => {
createMediator({ editable: true }); createWrapper({ state });
createWrapper({ noteableState });
}); });
it(`returns ${isOpen} when issue is ${noteableState}`, () => { it(`returns ${isOpen} when issue is ${state}`, () => {
expect(wrapper.vm.isOpen).toBe(isOpen); expect(wrapper.vm.isOpen).toBe(isOpen);
}); });
}); });
...@@ -76,10 +80,21 @@ describe('SidebarStatus', () => { ...@@ -76,10 +80,21 @@ describe('SidebarStatus', () => {
expect(wrapper.find(Status).exists()).toBe(true); 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'); 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 SidebarMediator from 'ee/sidebar/sidebar_mediator';
import waitForPromises from 'helpers/wait_for_promises';
import SidebarService from '~/sidebar/services/sidebar_service'; import SidebarService from '~/sidebar/services/sidebar_service';
import CESidebarMediator from '~/sidebar/sidebar_mediator'; import CESidebarMediator from '~/sidebar/sidebar_mediator';
import CESidebarStore from '~/sidebar/stores/sidebar_store'; import CESidebarStore from '~/sidebar/stores/sidebar_store';
...@@ -27,28 +26,4 @@ describe('EE Sidebar mediator', () => { ...@@ -27,28 +26,4 @@ describe('EE Sidebar mediator', () => {
expect(mediator.store.weight).toBe(mockData.weight); expect(mediator.store.weight).toBe(mockData.weight);
expect(mediator.store.status).toBe(mockGraphQlData.project.issue.healthStatus); 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 "" ...@@ -12742,6 +12742,9 @@ msgstr ""
msgid "Error occurred when saving reviewers" msgid "Error occurred when saving reviewers"
msgstr "" msgstr ""
msgid "Error occurred while updating the %{issuableType} status"
msgstr ""
msgid "Error occurred while updating the issue status" msgid "Error occurred while updating the issue status"
msgstr "" msgstr ""
...@@ -30369,6 +30372,9 @@ msgstr "" ...@@ -30369,6 +30372,9 @@ msgstr ""
msgid "Something went wrong while setting %{issuableType} confidentiality." msgid "Something went wrong while setting %{issuableType} confidentiality."
msgstr "" msgstr ""
msgid "Something went wrong while setting %{issuableType} health status."
msgstr ""
msgid "Something went wrong while setting %{issuableType} notifications." msgid "Something went wrong while setting %{issuableType} notifications."
msgstr "" 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