Commit 8b3b26eb authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'epic-confidential' into 'master'

Toggle epic confidential

See merge request gitlab-org/gitlab!37678
parents e9dd17cd c776b8c6
......@@ -136,6 +136,8 @@ export default {
}
window.addEventListener('hashchange', this.handleHashChanged);
eventHub.$on('notesApp.updateIssuableConfidentiality', this.setConfidentiality);
},
updated() {
this.$nextTick(() => {
......@@ -146,6 +148,7 @@ export default {
beforeDestroy() {
this.stopPolling();
window.removeEventListener('hashchange', this.handleHashChanged);
eventHub.$off('notesApp.updateIssuableConfidentiality', this.setConfidentiality);
},
methods: {
...mapActions([
......@@ -164,6 +167,7 @@ export default {
'startTaskList',
'convertToDiscussion',
'stopPolling',
'setConfidentiality',
]),
discussionIsIndividualNoteAndNotConverted(discussion) {
return discussion.individual_note && !this.convertedDisscussionIds.includes(discussion.id);
......
......@@ -21,29 +21,6 @@ import Api from '~/api';
let eTagPoll;
export const updateConfidentialityOnIssue = ({ commit, getters }, { confidential, fullPath }) => {
const { iid } = getters.getNoteableData;
return utils.gqClient
.mutate({
mutation: updateIssueConfidentialMutation,
variables: {
input: {
projectPath: fullPath,
iid: String(iid),
confidential,
},
},
})
.then(({ data }) => {
const {
issueSetConfidential: { issue },
} = data;
commit(types.SET_ISSUE_CONFIDENTIAL, issue.confidential);
});
};
export const updateLockedAttribute = ({ commit, getters }, { locked, fullPath }) => {
const { iid, targetType } = getters.getNoteableData;
......@@ -712,3 +689,29 @@ export const updateAssignees = ({ commit }, assignees) => {
export const updateDiscussionPosition = ({ commit }, updatedPosition) => {
commit(types.UPDATE_DISCUSSION_POSITION, updatedPosition);
};
export const updateConfidentialityOnIssuable = (
{ getters, commit },
{ confidential, fullPath },
) => {
const { iid } = getters.getNoteableData;
return utils.gqClient
.mutate({
mutation: updateIssueConfidentialMutation,
variables: {
input: {
projectPath: fullPath,
iid: String(iid),
confidential,
},
},
})
.then(({ data }) => {
const {
issueSetConfidential: { issue },
} = data;
setConfidentiality({ commit }, issue.confidential);
});
};
<script>
import { mapState, mapActions } from 'vuex';
import { __ } from '~/locale';
import { mapState } from 'vuex';
import { __, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
......@@ -23,9 +23,10 @@ export default {
required: true,
type: Boolean,
},
service: {
required: true,
type: Object,
issuableType: {
required: false,
type: String,
default: 'issue',
},
},
data() {
......@@ -34,13 +35,25 @@ export default {
};
},
computed: {
...mapState({ confidential: ({ noteableData }) => noteableData.confidential }),
...mapState({
confidential: ({ noteableData, confidential }) => {
if (noteableData) {
return noteableData.confidential;
}
return Boolean(confidential);
},
}),
confidentialityIcon() {
return this.confidential ? 'eye-slash' : 'eye';
},
tooltipLabel() {
return this.confidential ? __('Confidential') : __('Not confidential');
},
confidentialText() {
return sprintf(__('This %{issuableType} is confidential'), {
issuableType: this.issuableType,
});
},
},
created() {
eventHub.$on('closeConfidentialityForm', this.toggleForm);
......@@ -49,7 +62,6 @@ export default {
eventHub.$off('closeConfidentialityForm', this.toggleForm);
},
methods: {
...mapActions(['setConfidentiality']),
toggleForm() {
this.edit = !this.edit;
},
......@@ -86,7 +98,12 @@ export default {
>
</div>
<div class="value sidebar-item-value hide-collapsed">
<edit-form v-if="edit" :is-confidential="confidential" :full-path="fullPath" />
<edit-form
v-if="edit"
:confidential="confidential"
:full-path="fullPath"
:issuable-type="issuableType"
/>
<div v-if="!confidential" class="no-value sidebar-item-value" data-testid="not-confidential">
<icon :size="16" name="eye" aria-hidden="true" class="sidebar-item-icon inline" />
{{ __('Not confidential') }}
......@@ -98,7 +115,7 @@ export default {
aria-hidden="true"
class="sidebar-item-icon inline is-active"
/>
{{ __('This issue is confidential') }}
{{ confidentialText }}
</div>
</div>
</div>
......
<script>
import editFormButtons from './edit_form_buttons.vue';
import { s__ } from '../../../locale';
import { __, sprintf } from '../../../locale';
export default {
components: {
editFormButtons,
},
props: {
isConfidential: {
confidential: {
required: true,
type: Boolean,
},
......@@ -15,16 +15,32 @@ export default {
required: true,
type: String,
},
issuableType: {
required: true,
type: String,
},
},
computed: {
confidentialityOnWarning() {
return s__(
'confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.',
const accessLevel = __('at least Reporter access');
return sprintf(
__(
'You are going to turn on the confidentiality. This means that only team members with %{accessLevel} are able to see and leave comments on the %{issuableType}.',
),
{ issuableType: this.issuableType, accessLevel: `<strong>${accessLevel}</strong>` },
false,
);
},
confidentialityOffWarning() {
return s__(
'confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.',
const accessLevel = __('everyone');
return sprintf(
__(
'You are going to turn off the confidentiality. This means %{accessLevel} will be able to see and leave a comment on this %{issuableType}.',
),
{ issuableType: this.issuableType, accessLevel: `<strong>${accessLevel}</strong>` },
false,
);
},
},
......@@ -35,9 +51,9 @@ export default {
<div class="dropdown show">
<div class="dropdown-menu sidebar-item-warning-message">
<div>
<p v-if="!isConfidential" v-html="confidentialityOnWarning"></p>
<p v-if="!confidential" v-html="confidentialityOnWarning"></p>
<p v-else v-html="confidentialityOffWarning"></p>
<edit-form-buttons :full-path="fullPath" />
<edit-form-buttons :full-path="fullPath" :confidential="confidential" />
</div>
</div>
</div>
......
<script>
import $ from 'jquery';
import { GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import { mapActions } from 'vuex';
import { __ } from '~/locale';
import Flash from '~/flash';
import eventHub from '../../event_hub';
......@@ -15,6 +15,10 @@ export default {
required: true,
type: String,
},
confidential: {
required: true,
type: Boolean,
},
},
data() {
return {
......@@ -22,7 +26,6 @@ export default {
};
},
computed: {
...mapState({ confidential: ({ noteableData }) => noteableData.confidential }),
toggleButtonText() {
if (this.isLoading) {
return __('Applying');
......@@ -32,7 +35,7 @@ export default {
},
},
methods: {
...mapActions(['updateConfidentialityOnIssue']),
...mapActions(['updateConfidentialityOnIssuable']),
closeForm() {
eventHub.$emit('closeConfidentialityForm');
$(this.$el).trigger('hidden.gl.dropdown');
......@@ -41,9 +44,14 @@ export default {
this.isLoading = true;
const confidential = !this.confidential;
this.updateConfidentialityOnIssue({ confidential, fullPath: this.fullPath })
.catch(() => {
Flash(__('Something went wrong trying to change the confidentiality of this issue'));
this.updateConfidentialityOnIssuable({ confidential, fullPath: this.fullPath })
.then(() => {
eventHub.$emit('updateIssuableConfidentiality', confidential);
})
.catch(err => {
Flash(
err || __('Something went wrong trying to change the confidentiality of this issue'),
);
})
.finally(() => {
this.closeForm();
......
......@@ -12,6 +12,10 @@ import SidebarDatePickerCollapsed from '~/vue_shared/components/sidebar/collapse
import SidebarLabels from './sidebar_items/sidebar_labels.vue';
import SidebarParticipants from '~/sidebar/components/participants/participants.vue';
import SidebarSubscription from './sidebar_items/sidebar_subscription.vue';
import ConfidentialIssueSidebar from '~/sidebar/components/confidential/confidential_issue_sidebar.vue';
import notesEventHub from '~/notes/event_hub';
import sidebarEventHub from '~/sidebar/event_hub';
import { dateTypes } from '../constants';
......@@ -26,6 +30,7 @@ export default {
AncestorsTree,
SidebarParticipants,
SidebarSubscription,
ConfidentialIssueSidebar,
},
computed: {
...mapState([
......@@ -45,6 +50,7 @@ export default {
'dueDateFromMilestones',
'epicStartDateSaveInProgress',
'epicDueDateSaveInProgress',
'fullPath',
]),
...mapGetters([
'isUserSignedIn',
......@@ -63,6 +69,10 @@ export default {
mounted() {
this.toggleSidebarFlag(epicUtils.getCollapsedGutter());
this.fetchEpicDetails();
sidebarEventHub.$on('updateIssuableConfidentiality', this.updateEpicConfidentiality);
},
beforeDestroy() {
sidebarEventHub.$off('updateIssuableConfidentiality', this.updateEpicConfidentiality);
},
methods: {
...mapActions([
......@@ -118,6 +128,9 @@ export default {
dateTypeIsFixed: true,
});
},
updateEpicConfidentiality(confidential) {
notesEventHub.$emit('notesApp.updateIssuableConfidentiality', confidential);
},
},
};
</script>
......@@ -190,6 +203,13 @@ export default {
<div v-if="allowSubEpics" class="block ancestors">
<ancestors-tree :ancestors="ancestors" :is-fetching="false" />
</div>
<confidential-issue-sidebar
:is-editable="canUpdate"
:full-path="fullPath"
issuable-type="epic"
/>
<div class="block participants">
<sidebar-participants
:participants="participants"
......
......@@ -194,6 +194,36 @@ export const saveDate = ({ state, dispatch }, { dateType, dateTypeIsFixed, newDa
});
};
export const updateConfidentialityOnIssuable = ({ state, commit }, { confidential }) => {
const updateEpicInput = {
iid: `${state.epicIid}`,
groupPath: state.fullPath,
confidential,
};
return epicUtils.gqClient
.mutate({
mutation: updateEpic,
variables: {
updateEpicInput,
},
})
.then(({ data }) => {
if (!data?.updateEpic?.errors.length) {
commit(types.SET_EPIC_CONFIDENTIAL, confidential);
} else {
const errMsg =
data?.updateEpic?.errors[0]?.replace(/Confidential /, '') ||
s__('Epics|Unable to perform this action');
throw errMsg;
}
})
.catch(error => {
flash(error);
throw error;
});
};
/**
* Methods to handle Epic labels selection from sidebar
*/
......
......@@ -30,3 +30,5 @@ export const REQUEST_EPIC_CREATE_FAILURE = 'REQUEST_EPIC_CREATE_FAILURE';
export const REQUEST_EPIC_LABELS_SELECT = 'REQUEST_EPIC_LABELS_SELECT';
export const RECEIVE_EPIC_LABELS_SELECT_SUCCESS = 'RECEIVE_EPIC_LABELS_SELECT_SUCCESS';
export const RECEIVE_EPIC_LABELS_SELECT_FAILURE = 'RECEIVE_EPIC_LABELS_SELECT_FAILURE';
export const SET_EPIC_CONFIDENTIAL = 'SET_EPIC_CONFIDENTIAL';
......@@ -121,4 +121,7 @@ export default {
[types.RECEIVE_EPIC_LABELS_SELECT_FAILURE](state) {
state.epicLabelsSelectInProgress = false;
},
[types.SET_EPIC_CONFIDENTIAL](state, confidential) {
state.confidential = confidential;
},
};
---
title: Toggle epic confidential
merge_request: 37678
author:
type: added
......@@ -1238,4 +1238,63 @@ describe('Epic Store Actions', () => {
});
});
});
describe('updateConfidentialityOnIssuable', () => {
let mock;
const mockUpdateConfidentialMutationRes = {
updateEpic: {
clientMutationId: null,
errors: [],
__typename: 'UpdateEpicPayload',
},
};
const data = {
confidential: true,
};
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
it('commits SET_EPIC_CONFIDENTIAL when request is successful', done => {
mock.onPut(/(.*)/).replyOnce(200, {});
jest.spyOn(epicUtils.gqClient, 'mutate').mockResolvedValue({
data: mockUpdateConfidentialMutationRes,
});
testAction(
actions.updateConfidentialityOnIssuable,
{ ...data },
state,
[{ payload: true, type: 'SET_EPIC_CONFIDENTIAL' }],
[],
done,
);
});
it("doesn't commit/dispatch and throws error when request fails", done => {
mock.onPut(/(.*)/).replyOnce(500, {});
const errors = ['bar'];
jest.spyOn(epicUtils.gqClient, 'mutate').mockResolvedValue({
data: {
updateEpic: {
...mockUpdateConfidentialMutationRes,
errors,
},
},
});
testAction(actions.updateConfidentialityOnIssuable, { ...data }, state, [], [])
.catch(err => {
expect(err).toEqual('bar');
})
.finally(done);
});
});
});
......@@ -422,4 +422,18 @@ describe('Epic Store Mutations', () => {
expect(state.epicLabelsSelectInProgress).toBe(false);
});
});
describe('SET_EPIC_CONFIDENTIAL', () => {
it('Should set `confidential` flag on state to `true`', () => {
const state = {
confidential: false,
};
const confidential = true;
mutations[types.SET_EPIC_CONFIDENTIAL](state, confidential);
expect(state.confidential).toBe(true);
});
});
});
......@@ -9584,6 +9584,9 @@ msgstr ""
msgid "Epics|To schedule your epic's %{epicDateType} date based on milestones, assign a milestone with a %{epicDateType} date to any issue in the epic."
msgstr ""
msgid "Epics|Unable to perform this action"
msgstr ""
msgid "Epics|Unable to save epic. Please try again"
msgstr ""
......@@ -24656,6 +24659,9 @@ msgstr ""
msgid "This %{issuableDisplayName} is locked. Only project members can comment."
msgstr ""
msgid "This %{issuableType} is confidential"
msgstr ""
msgid "This %{issuable} is locked. Only %{strong_open}project members%{strong_close} can comment."
msgstr ""
......@@ -24860,9 +24866,6 @@ msgstr ""
msgid "This is your current session"
msgstr ""
msgid "This issue is confidential"
msgstr ""
msgid "This issue is currently blocked by the following issues: %{issues}."
msgstr ""
......@@ -27663,6 +27666,12 @@ msgstr ""
msgid "You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?"
msgstr ""
msgid "You are going to turn off the confidentiality. This means %{accessLevel} will be able to see and leave a comment on this %{issuableType}."
msgstr ""
msgid "You are going to turn on the confidentiality. This means that only team members with %{accessLevel} are able to see and leave comments on the %{issuableType}."
msgstr ""
msgid "You are not allowed to push into this branch. Create another branch or open a merge request."
msgstr ""
......@@ -28391,6 +28400,9 @@ msgstr ""
msgid "assign yourself"
msgstr ""
msgid "at least Reporter access"
msgstr ""
msgid "at risk"
msgstr ""
......@@ -28672,12 +28684,6 @@ msgstr ""
msgid "committed"
msgstr ""
msgid "confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue."
msgstr ""
msgid "confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue."
msgstr ""
msgid "connecting"
msgstr ""
......@@ -28787,6 +28793,9 @@ msgstr ""
msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
msgid "everyone"
msgstr ""
msgid "exceeds the limit of %{bytes} bytes"
msgstr ""
......
......@@ -1221,7 +1221,7 @@ describe('Actions Notes Store', () => {
});
});
describe('updateConfidentialityOnIssue', () => {
describe('updateConfidentialityOnIssuable', () => {
state = { noteableData: { confidential: false } };
const iid = '1';
const projectPath = 'full/path';
......@@ -1236,13 +1236,13 @@ describe('Actions Notes Store', () => {
});
it('calls gqClient mutation one time', () => {
actions.updateConfidentialityOnIssue({ commit: () => {}, state, getters }, actionArgs);
actions.updateConfidentialityOnIssuable({ commit: () => {}, state, getters }, actionArgs);
expect(utils.gqClient.mutate).toHaveBeenCalledTimes(1);
});
it('calls gqClient mutation with the correct values', () => {
actions.updateConfidentialityOnIssue({ commit: () => {}, state, getters }, actionArgs);
actions.updateConfidentialityOnIssuable({ commit: () => {}, state, getters }, actionArgs);
expect(utils.gqClient.mutate).toHaveBeenCalledWith({
mutation: updateIssueConfidentialMutation,
......@@ -1255,7 +1255,7 @@ describe('Actions Notes Store', () => {
const commitSpy = jest.fn();
return actions
.updateConfidentialityOnIssue({ commit: commitSpy, state, getters }, actionArgs)
.updateConfidentialityOnIssuable({ commit: commitSpy, state, getters }, actionArgs)
.then(() => {
expect(commitSpy).toHaveBeenCalledWith(
mutationTypes.SET_ISSUE_CONFIDENTIAL,
......
......@@ -44,9 +44,11 @@ describe('Edit Form Buttons', () => {
describe('when isLoading', () => {
beforeEach(() => {
createComponent({});
wrapper.vm.$store.state.noteableData.confidential = false;
createComponent({
props: {
confidential: false,
},
});
});
it('renders "Applying" in the toggle button', () => {
......@@ -68,6 +70,9 @@ describe('Edit Form Buttons', () => {
data: {
isLoading: false,
},
props: {
confidential: false,
},
});
expect(findConfidentialToggle().text()).toBe('Turn On');
......@@ -80,9 +85,10 @@ describe('Edit Form Buttons', () => {
data: {
isLoading: false,
},
props: {
confidential: true,
},
});
wrapper.vm.$store.state.noteableData.confidential = true;
});
it('renders on or off text based on confidentiality', () => {
......@@ -92,13 +98,12 @@ describe('Edit Form Buttons', () => {
describe('when succeeds', () => {
beforeEach(() => {
createComponent({ data: { isLoading: false } });
wrapper.vm.$store.state.noteableData.confidential = true;
createComponent({ data: { isLoading: false }, props: { confidential: true } });
findConfidentialToggle().trigger('click');
});
it('dispatches the correct action', () => {
expect(store.dispatch).toHaveBeenCalledWith('updateConfidentialityOnIssue', {
expect(store.dispatch).toHaveBeenCalledWith('updateConfidentialityOnIssuable', {
confidential: false,
fullPath: '',
});
......@@ -115,15 +120,21 @@ describe('Edit Form Buttons', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('closeConfidentialityForm');
});
});
it('emits updateOnConfidentiality event', () => {
return waitForPromises().then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('updateIssuableConfidentiality', false);
});
});
});
describe('when fails', () => {
beforeEach(() => {
createComponent({
data: { isLoading: false },
props: { confidential: true },
resolved: false,
});
wrapper.vm.$store.state.noteableData.confidential = true;
findConfidentialToggle().trigger('click');
});
......
......@@ -12,6 +12,7 @@ describe('Edit Form Dropdown', () => {
...props,
isLoading: false,
fullPath: '',
issuableType: 'issue',
},
});
};
......@@ -24,7 +25,7 @@ describe('Edit Form Dropdown', () => {
describe('when not confidential', () => {
it('renders "You are going to turn off the confidentiality." in the ', () => {
createComponent({
isConfidential: false,
confidential: false,
toggleForm,
updateConfidentialAttribute,
});
......@@ -36,7 +37,7 @@ describe('Edit Form Dropdown', () => {
describe('when confidential', () => {
it('renders on or off text based on confidentiality', () => {
createComponent({
isConfidential: true,
confidential: true,
toggleForm,
updateConfidentialAttribute,
});
......
......@@ -2,9 +2,9 @@ import { shallowMount } from '@vue/test-utils';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import ConfidentialIssueSidebar from '~/sidebar/components/confidential/confidential_issue_sidebar.vue';
import EditForm from '~/sidebar/components/confidential/edit_form.vue';
import SidebarService from '~/sidebar/services/sidebar_service';
import createStore from '~/notes/stores';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import * as types from '~/notes/stores/mutation_types';
jest.mock('~/flash');
jest.mock('~/sidebar/services/sidebar_service');
......@@ -19,14 +19,12 @@ describe('Confidential Issue Sidebar Block', () => {
const createComponent = ({ propsData, data = {} }) => {
const store = createStore();
const service = new SidebarService();
wrapper = shallowMount(ConfidentialIssueSidebar, {
store,
data() {
return data;
},
propsData: {
service,
iid: '',
fullPath: '',
...propsData,
......@@ -115,4 +113,47 @@ describe('Confidential Issue Sidebar Block', () => {
});
});
});
describe('computed confidential', () => {
beforeEach(() => {
createComponent({
propsData: {
isEditable: true,
},
});
});
it('returns false when noteableData is not present', () => {
wrapper.vm.$store.commit(types.SET_NOTEABLE_DATA, null);
expect(wrapper.vm.confidential).toBe(false);
});
it('returns true when noteableData has confidential attr as true', () => {
wrapper.vm.$store.commit(types.SET_NOTEABLE_DATA, {});
wrapper.vm.$store.commit(types.SET_ISSUE_CONFIDENTIAL, true);
expect(wrapper.vm.confidential).toBe(true);
});
it('returns false when noteableData has confidential attr as false', () => {
wrapper.vm.$store.commit(types.SET_NOTEABLE_DATA, {});
wrapper.vm.$store.commit(types.SET_ISSUE_CONFIDENTIAL, false);
expect(wrapper.vm.confidential).toBe(false);
});
it('returns true when confidential attr is true', () => {
wrapper.vm.$store.commit(types.SET_NOTEABLE_DATA, {});
wrapper.vm.$store.commit(types.SET_ISSUE_CONFIDENTIAL, true);
expect(wrapper.vm.confidential).toBe(true);
});
it('returns false when confidential attr is false', () => {
wrapper.vm.$store.commit(types.SET_NOTEABLE_DATA, {});
wrapper.vm.$store.commit(types.SET_ISSUE_CONFIDENTIAL, false);
expect(wrapper.vm.confidential).toBe(false);
});
});
});
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