Commit 7c42b1e0 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents e2596c1f 384861b8
1ce9d77e4ed41e124d840b23689392fda6945b3c
6ee267a02055a7f48871d66fae594417c4cdec9b
......@@ -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
......
......@@ -7,6 +7,7 @@
= f.select :first_day_of_week, first_day_of_week_choices, {}, class: 'form-control'
.form-text.text-muted
= _('Default first day of the week in calendars and date pickers.')
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/index.md', anchor: 'default-first-day-of-the-week'), target: '_blank'
.form-group
= f.label :time_tracking, _('Time tracking'), class: 'label-bold'
......@@ -14,5 +15,9 @@
= f.check_box :time_tracking_limit_to_hours, class: 'form-check-input'
= f.label :time_tracking_limit_to_hours, class: 'form-check-label' do
= _('Limit display of time tracking units to hours.')
.form-text.text-muted
= _('Display time tracking in issues in total hours only.')
= link_to _('What is time tracking?'), help_page_path('user/project/time_tracking.md'), target: '_blank'
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
......@@ -77,6 +77,6 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Various localization settings.')
= _('Configure the default first day of the week and time tracking units.')
.settings-content
= render 'localization'
......@@ -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
......
......@@ -163,7 +163,7 @@ You can choose one of the following options as the first day of the week:
- Sunday
- Monday
If you select **System Default**, the system-wide default setting is used.
If you select **System Default**, the [instance default](../admin_area/settings/index.md#default-first-day-of-the-week) setting is used.
## Integrations
......
<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);
});
});
});
......@@ -8277,6 +8277,9 @@ msgstr ""
msgid "Configure the %{link} integration."
msgstr ""
msgid "Configure the default first day of the week and time tracking units."
msgstr ""
msgid "Configure the way a user creates a new account."
msgstr ""
......@@ -11524,6 +11527,9 @@ msgstr ""
msgid "Display source"
msgstr ""
msgid "Display time tracking in issues in total hours only."
msgstr ""
msgid "Do not display offers from third parties"
msgstr ""
......@@ -12817,6 +12823,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 ""
......@@ -30456,6 +30465,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 ""
......@@ -35895,9 +35907,6 @@ msgstr ""
msgid "Various email settings."
msgstr ""
msgid "Various localization settings."
msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
......@@ -36721,6 +36730,9 @@ msgstr ""
msgid "What is squashing?"
msgstr ""
msgid "What is time tracking?"
msgstr ""
msgid "What is your job title? (optional)"
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