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