Commit de91a48a authored by Natalia Tepluhina's avatar Natalia Tepluhina Committed by Nicolò Maria Mezzopera

Added confidentiality widget to epic

Added sidebar confidentiality widget

Added the first prototype of new widget

Added icons and text

Added an edit form

Reimplemented edit form

Fixed imports order

Fixed imports order

Added a query to fetch confidentiality

Added a mutation to change confidentiality

Connected Apollo Client to Vuex

- added issue types
- added component to trigger mutation
Fixed a query name

Replaced default slot

Added a guard for confidentiality

Fixed a wrong conditional

Fixed subscription to work with MR

Synced up with the quick action

Added aliases to the query

Handled loading state

Fixes after rebase

Fixed canUpdate conditional

Removed subscription for MRs

Added confidentiality error handling

Regenerated a translation file

Fixed collapsed sidebar state

Fixed spinner alignment

Removed mock event
Added tests for the widget

Started unit test

Added a mock test for widget component

Added confidential false tests

Added tests for issue confidential

Finished a widget test

Created a base spec for form

Tested a mutation error

Finished the confidentiality form test

Added tests for non-confidential content

Finished the content spec

Fixed a typo

Fixed a computed property
Applied maintainer feedback

Apply 1 suggestion(s) to 1 file(s)
Apply 1 suggestion(s) to 1 file(s)
Apply 1 suggestion(s) to 1 file(s)
Apply 1 suggestion(s) to 1 file(s)
Apply 1 suggestion(s) to 1 file(s)
Regenerated translation file

Fixed toString and optional chaining

Apply 3 suggestion(s) to 2 file(s)
Made issuable type prop required

Fixed parent and test for the widget

Renamed isLoading to loading

Added issuable type to error messages

Fixed passing a prop

Removed space-between class

Regenerated translations file

Fixed confidentiality form spec
Started describing widgets

Revert "Started describing widgets"

This reverts commit 5538fa30a1a0720282f55cd59fd17a33de64e7d9.
Added confidentiality widget to epic sidebar

- added proper queries/mutations
- fixed a full path
Fixed iid type

Cleanup after rebase

Added issuable type prop

Added test for correct message

Made a sync with Vuex

Fixed actions spec

Deleted old confidentiality component

Added changelog entry

Fixed tests and sidebar collapsing

Returned back optional chaining

Fixed issue sidebar toggling

Fixed epic body spec

Rewritten epic unit tests

Regenerated translation file
parent 02fcb882
......@@ -19,7 +19,7 @@ export default {
computed: {
fullPath() {
if (this.noteableData.web_url) {
return this.noteableData.web_url.split('/-/')[0].substring(1);
return this.noteableData.web_url.split('/-/')[0].substring(1).replace('groups/', '');
}
return null;
},
......@@ -28,7 +28,7 @@ export default {
},
},
created() {
if (this.issuableType !== IssuableType.Issue) {
if (this.issuableType !== IssuableType.Issue && this.issuableType !== IssuableType.Epic) {
return;
}
......
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { mapState } from 'vuex';
import { __, sprintf } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import EditForm from './edit_form.vue';
export default {
components: {
EditForm,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
fullPath: {
required: true,
type: String,
},
isEditable: {
required: true,
type: Boolean,
},
issuableType: {
required: false,
type: String,
default: 'issue',
},
},
data() {
return {
edit: false,
};
},
computed: {
...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);
},
beforeDestroy() {
eventHub.$off('closeConfidentialityForm', this.toggleForm);
},
methods: {
toggleForm() {
this.edit = !this.edit;
},
},
};
</script>
<template>
<div class="block issuable-sidebar-item confidentiality">
<div
ref="collapseIcon"
v-gl-tooltip.viewport.left
:title="tooltipLabel"
class="sidebar-collapsed-icon"
@click="toggleForm"
>
<gl-icon :name="confidentialityIcon" />
</div>
<div class="title hide-collapsed">
{{ __('Confidentiality') }}
<a
v-if="isEditable"
ref="editLink"
class="float-right confidential-edit"
href="#"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="confidentiality"
@click.prevent="toggleForm"
>{{ __('Edit') }}</a
>
</div>
<div class="value sidebar-item-value hide-collapsed">
<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">
<gl-icon :size="16" name="eye" class="sidebar-item-icon inline" />
{{ __('Not confidential') }}
</div>
<div v-else class="value sidebar-item-value hide-collapsed">
<gl-icon :size="16" name="eye-slash" class="sidebar-item-icon inline is-active" />
{{ confidentialText }}
</div>
</div>
</div>
</template>
<script>
import { GlSprintf } from '@gitlab/ui';
import { __ } from '../../../locale';
import editFormButtons from './edit_form_buttons.vue';
export default {
components: {
editFormButtons,
GlSprintf,
},
props: {
confidential: {
required: true,
type: Boolean,
},
fullPath: {
required: true,
type: String,
},
issuableType: {
required: true,
type: String,
},
},
computed: {
confidentialityOnWarning() {
return __(
'You are going to turn on the confidentiality. This means that only team members with %{strongStart}at least Reporter access%{strongEnd} are able to see and leave comments on the %{issuableType}.',
);
},
confidentialityOffWarning() {
return __(
'You are going to turn off the confidentiality. This means %{strongStart}everyone%{strongEnd} will be able to see and leave a comment on this %{issuableType}.',
);
},
},
};
</script>
<template>
<div class="dropdown show">
<div class="dropdown-menu sidebar-item-warning-message">
<div>
<p v-if="!confidential">
<gl-sprintf :message="confidentialityOnWarning">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
<template #issuableType>{{ issuableType }}</template>
</gl-sprintf>
</p>
<p v-else>
<gl-sprintf :message="confidentialityOffWarning">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
<template #issuableType>{{ issuableType }}</template>
</gl-sprintf>
</p>
<edit-form-buttons :full-path="fullPath" :confidential="confidential" />
</div>
</div>
</div>
</template>
<script>
import { GlButton } from '@gitlab/ui';
import $ from 'jquery';
import { mapActions } from 'vuex';
import { deprecatedCreateFlash as Flash } from '~/flash';
import { __ } from '~/locale';
import eventHub from '../../event_hub';
export default {
components: {
GlButton,
},
props: {
fullPath: {
required: true,
type: String,
},
confidential: {
required: true,
type: Boolean,
},
},
data() {
return {
isLoading: false,
};
},
computed: {
toggleButtonText() {
if (this.isLoading) {
return __('Applying');
}
return this.confidential ? __('Turn Off') : __('Turn On');
},
},
methods: {
...mapActions(['updateConfidentialityOnIssuable']),
closeForm() {
eventHub.$emit('closeConfidentialityForm');
$(this.$el).trigger('hidden.gl.dropdown');
},
submitForm() {
this.isLoading = true;
const confidential = !this.confidential;
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();
this.isLoading = false;
});
},
},
};
</script>
<template>
<div class="sidebar-item-warning-message-actions">
<gl-button class="gl-mr-3" @click="closeForm">
{{ __('Cancel') }}
</gl-button>
<gl-button
category="secondary"
variant="warning"
:disabled="isLoading"
:loading="isLoading"
data-testid="confidential-toggle"
@click.prevent="submitForm"
>
{{ toggleButtonText }}
</gl-button>
</div>
</template>
mutation updateIssueConfidential($input: IssueSetConfidentialInput!) {
issueSetConfidential(input: $input) {
issue {
confidential
}
errors
}
}
......@@ -14,6 +14,10 @@ export default {
type: Boolean,
required: true,
},
issuableType: {
type: String,
required: true,
},
},
computed: {
confidentialText() {
......@@ -35,7 +39,13 @@ export default {
<template>
<div>
<div v-gl-tooltip.viewport.left :title="tooltipLabel" class="sidebar-collapsed-icon">
<div
v-gl-tooltip.viewport.left
:title="tooltipLabel"
class="sidebar-collapsed-icon"
data-testid="sidebar-collapsed-icon"
@click="$emit('expandSidebar')"
>
<gl-icon
:size="16"
:name="confidentialIcon"
......
<script>
import { GlSprintf, GlButton } from '@gitlab/ui';
import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
import { __, sprintf } from '~/locale';
import { confidentialityQueries } from '~/sidebar/constants';
......@@ -45,6 +46,15 @@ export default {
? this.$options.i18n.confidentialityOffWarning
: this.$options.i18n.confidentialityOnWarning;
},
workspacePath() {
return this.issuableType === IssuableType.Issue
? {
projectPath: this.fullPath,
}
: {
groupPath: this.fullPath,
};
},
},
methods: {
submitForm() {
......@@ -54,7 +64,7 @@ export default {
mutation: confidentialityQueries[this.issuableType].mutation,
variables: {
input: {
projectPath: this.fullPath,
...this.workspacePath,
iid: this.iid,
confidential: !this.confidential,
},
......
......@@ -47,12 +47,15 @@ export default {
variables() {
return {
fullPath: this.fullPath,
iid: this.iid,
iid: String(this.iid),
};
},
update(data) {
return data.workspace?.issuable?.confidential || false;
},
result({ data }) {
this.$emit('confidentialityUpdated', data.workspace?.issuable?.confidential);
},
error() {
createFlash({
message: sprintf(
......@@ -80,6 +83,7 @@ export default {
closeForm() {
this.$refs.editable.collapse();
this.$el.dispatchEvent(hideDropdownEvent);
this.$emit('closeForm');
},
// synchronizing the quick action with the sidebar widget
// this is a temporary solution until we have confidentiality real-time updates
......@@ -101,6 +105,10 @@ export default {
data,
});
},
expandSidebar() {
this.$refs.editable.expand();
this.$emit('expandSidebar');
},
},
};
</script>
......@@ -115,11 +123,16 @@ export default {
>
<template #collapsed>
<div>
<sidebar-confidentiality-content v-if="!isLoading" :confidential="confidential" />
<sidebar-confidentiality-content
v-if="!isLoading"
:confidential="confidential"
:issuable-type="issuableType"
@expandSidebar="expandSidebar"
/>
</div>
</template>
<template #default>
<sidebar-confidentiality-content :confidential="confidential" />
<sidebar-confidentiality-content :confidential="confidential" :issuable-type="issuableType" />
<sidebar-confidentiality-form
:confidential="confidential"
:issuable-type="issuableType"
......
......@@ -87,7 +87,7 @@ export default {
<gl-button
v-if="canUpdate"
variant="link"
class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto js-sidebar-dropdown-toggle hide-collapsed"
class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto hide-collapsed"
data-testid="edit-button"
:data-track-event="tracking.event"
:data-track-label="tracking.label"
......
import { IssuableType } from '~/issue_show/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
import updateEpicMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql';
import getIssueParticipants from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql';
......@@ -22,4 +24,8 @@ export const confidentialityQueries = {
query: issueConfidentialQuery,
mutation: updateIssueConfidentialMutation,
},
[IssuableType.Epic]: {
query: epicConfidentialQuery,
mutation: updateEpicMutation,
},
};
query epicConfidential($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
__typename
issuable: epic(iid: $iid) {
__typename
id
confidential
}
}
}
mutation updateEpic($input: UpdateEpicInput!) {
issuableSetConfidential: updateEpic(input: $input) {
issuable: epic {
id
confidential
}
errors
}
}
......@@ -30,9 +30,6 @@ export default {
'sidebarCollapsed',
]),
...mapGetters(['isUserSignedIn']),
sidebarStatusClass() {
return this.sidebarCollapsed ? 'right-sidebar-collapsed' : 'right-sidebar-expanded';
},
},
};
</script>
......
......@@ -4,7 +4,7 @@ import { mapState, mapGetters, mapActions } from 'vuex';
import AncestorsTree from 'ee/sidebar/components/ancestors_tree/ancestors_tree.vue';
import notesEventHub from '~/notes/event_hub';
import ConfidentialIssueSidebar from '~/sidebar/components/confidential/confidential_issue_sidebar.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarParticipants from '~/sidebar/components/participants/participants.vue';
import sidebarEventHub from '~/sidebar/event_hub';
import SidebarDatePickerCollapsed from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
......@@ -28,7 +28,12 @@ export default {
AncestorsTree,
SidebarParticipants,
SidebarSubscription,
ConfidentialIssueSidebar,
SidebarConfidentialityWidget,
},
data() {
return {
sidebarExpandedOnClick: false,
};
},
computed: {
...mapState([
......@@ -80,6 +85,7 @@ export default {
'toggleStartDateType',
'toggleDueDateType',
'saveDate',
'updateConfidentialityOnIssuable',
]),
getDateFromMilestonesTooltip(dateType) {
return epicUtils.getDateFromMilestonesTooltip({
......@@ -129,6 +135,15 @@ export default {
updateEpicConfidentiality(confidential) {
notesEventHub.$emit('notesApp.updateIssuableConfidentiality', confidential);
},
handleSidebarToggle() {
if (this.sidebarCollapsed) {
this.sidebarExpandedOnClick = true;
this.toggleSidebar({ sidebarCollapsed: true });
} else if (this.sidebarExpandedOnClick) {
this.sidebarExpandedOnClick = false;
this.toggleSidebar({ sidebarCollapsed: false });
}
},
},
};
</script>
......@@ -209,13 +224,12 @@ export default {
<div v-if="allowSubEpics" class="block ancestors">
<ancestors-tree :ancestors="ancestors" :is-fetching="false" data-testid="ancestors" />
</div>
<confidential-issue-sidebar
:is-editable="canUpdate"
:full-path="fullPath"
<sidebar-confidentiality-widget
issuable-type="epic"
@closeForm="handleSidebarToggle"
@expandSidebar="handleSidebarToggle"
@confidentialityUpdated="updateConfidentialityOnIssuable($event)"
/>
<div class="block participants">
<sidebar-participants
:participants="participants"
......
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Cookies from 'js-cookie';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapActions } from 'vuex';
import { parseIssuableData } from '~/issue_show/utils/parse_data';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import { defaultClient } from '~/sidebar/graphql';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
import EpicApp from './components/epic_app.vue';
import createStore from './store';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient,
});
export default () => {
const el = document.getElementById('epic-app-root');
......@@ -31,8 +38,14 @@ export default () => {
return new Vue({
el,
apolloProvider,
store,
components: { EpicApp },
provide: {
canUpdate: epicData.canUpdate,
fullPath: epicData.fullPath,
iid: epicMeta.epicIid,
},
created() {
this.setEpicMeta({
...epicMeta,
......
......@@ -195,34 +195,8 @@ 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;
});
export const updateConfidentialityOnIssuable = ({ commit }, confidential) => {
commit(types.SET_EPIC_CONFIDENTIAL, confidential);
};
/**
......
---
title: Epic confidentiality widget
merge_request: 55350
author:
type: changed
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import EpicApp from 'ee/epic/components/epic_app.vue';
import createStore from 'ee/epic/store';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import { initialRequest } from 'jest/issue_show/mock_data';
import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import { mockEpicMeta, mockEpicData } from '../mock_data';
import EpicBody from 'ee/epic/components/epic_body.vue';
import EpicHeader from 'ee/epic/components/epic_header.vue';
describe('EpicAppComponent', () => {
useMockIntersectionObserver();
let vm;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(`${TEST_HOST}/realtime_changes`).reply(200, initialRequest);
let wrapper;
const Component = Vue.extend(EpicApp);
const store = createStore();
store.dispatch('setEpicMeta', mockEpicMeta);
store.dispatch('setEpicData', mockEpicData);
vm = mountComponentWithStore(Component, {
store,
});
jest.advanceTimersByTime(2);
});
const createComponent = () => {
wrapper = shallowMount(EpicApp);
};
afterEach(() => {
mock.restore();
vm.$destroy();
wrapper.destroy();
});
describe('template', () => {
it('renders component container element with class `epic-page-container`', () => {
expect(vm.$el.classList.contains('epic-page-container')).toBe(true);
});
it('renders epic header and epic body', () => {
createComponent();
expect(wrapper.findComponent(EpicHeader).exists()).toBe(true);
expect(wrapper.findComponent(EpicBody).exists()).toBe(true);
});
});
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import EpicBody from 'ee/epic/components/epic_body.vue';
import createStore from 'ee/epic/store';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import { initialRequest } from 'jest/issue_show/mock_data';
import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import IssuableBody from '~/issue_show/components/app.vue';
import { mockEpicMeta, mockEpicData } from '../mock_data';
describe('EpicBodyComponent', () => {
useMockIntersectionObserver();
let wrapper;
let vm;
let mock;
const findIssuableBody = () => wrapper.findComponent(IssuableBody);
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(`${TEST_HOST}/realtime_changes`).reply(200, initialRequest);
const store = createStore();
store.dispatch('setEpicMeta', mockEpicMeta);
store.dispatch('setEpicData', mockEpicData);
const Component = Vue.extend(EpicBody);
const store = createStore();
store.dispatch('setEpicMeta', mockEpicMeta);
store.dispatch('setEpicData', mockEpicData);
vm = mountComponentWithStore(Component, {
const createComponent = () => {
wrapper = shallowMount(EpicBody, {
store,
});
jest.advanceTimersByTime(5);
});
};
afterEach(() => {
mock.restore();
vm.$destroy();
wrapper.destroy();
});
describe('template', () => {
it('renders epic body container element with class `detail-page-description` & `issuable-details` & `content-block`', () => {
const el = vm.$el.querySelector('.detail-page-description');
expect(el).not.toBeNull();
expect(el.classList.contains('issuable-details')).toBe(true);
expect(el.classList.contains('content-block')).toBe(true);
});
it('renders epic body elements', () => {
expect(vm.$el.querySelector('.title-container')).not.toBeNull();
expect(vm.$el.querySelector('.description')).not.toBeNull();
it('renders an issuable body component', () => {
createComponent();
expect(findIssuableBody().exists()).toBe(true);
expect(findIssuableBody().props()).toMatchObject({
endpoint: 'http://test.host',
updateEndpoint: '/groups/frontend-fixtures-group/-/epics/1.json',
canUpdate: true,
canDestroy: true,
showInlineEditButton: true,
showDeleteButton: true,
enableAutocomplete: true,
zoomMeetingUrl: '',
publishedIncidentUrl: '',
issuableRef: '',
issuableStatus: '',
initialTitleHtml: 'This is a sample epic',
initialTitleText: 'This is a sample epic',
});
});
});
......@@ -1247,61 +1247,16 @@ 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,
});
it('should commit `SET_EPIC_CONFIDENTIAL` mutation with param `sidebarCollapsed', (done) => {
const confidential = true;
testAction(
actions.updateConfidentialityOnIssuable,
{ ...data },
confidential,
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);
});
});
});
......@@ -11876,9 +11876,6 @@ 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 ""
......@@ -27879,9 +27876,6 @@ msgstr ""
msgid "Something went wrong on our end. Please try again."
msgstr ""
msgid "Something went wrong trying to change the confidentiality of this issue"
msgstr ""
msgid "Something went wrong trying to change the locked state of this %{issuableDisplayName}"
msgstr ""
......@@ -31708,12 +31702,6 @@ msgstr ""
msgid "Tuning settings"
msgstr ""
msgid "Turn Off"
msgstr ""
msgid "Turn On"
msgstr ""
msgid "Turn off"
msgstr ""
......@@ -34033,9 +34021,6 @@ msgstr ""
msgid "You are going to turn on confidentiality. Only team members with %{strongStart}at least Reporter access%{strongEnd} will be able to see and leave comments on the %{issuableType}."
msgstr ""
msgid "You are going to turn on the confidentiality. This means that only team members with %{strongStart}at least Reporter access%{strongEnd} are able to see and leave comments on the %{issuableType}."
msgstr ""
msgid "You are not allowed to approve a user"
msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Confidential Issue Sidebar Block renders for confidential = false and isEditable = false 1`] = `
<div
class="block issuable-sidebar-item confidentiality"
iid=""
>
<div
class="sidebar-collapsed-icon"
title="Not confidential"
>
<gl-icon-stub
name="eye"
size="16"
/>
</div>
<div
class="title hide-collapsed"
>
Confidentiality
<!---->
</div>
<div
class="value sidebar-item-value hide-collapsed"
>
<!---->
<div
class="no-value sidebar-item-value"
data-testid="not-confidential"
>
<gl-icon-stub
class="sidebar-item-icon inline"
name="eye"
size="16"
/>
Not confidential
</div>
</div>
</div>
`;
exports[`Confidential Issue Sidebar Block renders for confidential = false and isEditable = true 1`] = `
<div
class="block issuable-sidebar-item confidentiality"
iid=""
>
<div
class="sidebar-collapsed-icon"
title="Not confidential"
>
<gl-icon-stub
name="eye"
size="16"
/>
</div>
<div
class="title hide-collapsed"
>
Confidentiality
<a
class="float-right confidential-edit"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="confidentiality"
href="#"
>
Edit
</a>
</div>
<div
class="value sidebar-item-value hide-collapsed"
>
<!---->
<div
class="no-value sidebar-item-value"
data-testid="not-confidential"
>
<gl-icon-stub
class="sidebar-item-icon inline"
name="eye"
size="16"
/>
Not confidential
</div>
</div>
</div>
`;
exports[`Confidential Issue Sidebar Block renders for confidential = true and isEditable = false 1`] = `
<div
class="block issuable-sidebar-item confidentiality"
iid=""
>
<div
class="sidebar-collapsed-icon"
title="Confidential"
>
<gl-icon-stub
name="eye-slash"
size="16"
/>
</div>
<div
class="title hide-collapsed"
>
Confidentiality
<!---->
</div>
<div
class="value sidebar-item-value hide-collapsed"
>
<!---->
<div
class="value sidebar-item-value hide-collapsed"
>
<gl-icon-stub
class="sidebar-item-icon inline is-active"
name="eye-slash"
size="16"
/>
This issue is confidential
</div>
</div>
</div>
`;
exports[`Confidential Issue Sidebar Block renders for confidential = true and isEditable = true 1`] = `
<div
class="block issuable-sidebar-item confidentiality"
iid=""
>
<div
class="sidebar-collapsed-icon"
title="Confidential"
>
<gl-icon-stub
name="eye-slash"
size="16"
/>
</div>
<div
class="title hide-collapsed"
>
Confidentiality
<a
class="float-right confidential-edit"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="confidentiality"
href="#"
>
Edit
</a>
</div>
<div
class="value sidebar-item-value hide-collapsed"
>
<!---->
<div
class="value sidebar-item-value hide-collapsed"
>
<gl-icon-stub
class="sidebar-item-icon inline is-active"
name="eye-slash"
size="16"
/>
This issue is confidential
</div>
</div>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Edit Form Dropdown when confidential renders on or off text based on confidentiality 1`] = `
<div
class="dropdown show"
toggleform="function () {}"
updateconfidentialattribute="function () {}"
>
<div
class="dropdown-menu sidebar-item-warning-message"
>
<div>
<p>
<gl-sprintf-stub
message="You are going to turn off the confidentiality. This means %{strongStart}everyone%{strongEnd} will be able to see and leave a comment on this %{issuableType}."
/>
</p>
<edit-form-buttons-stub
confidential="true"
fullpath=""
/>
</div>
</div>
</div>
`;
exports[`Edit Form Dropdown when not confidential renders "You are going to turn on the confidentiality." in the 1`] = `
<div
class="dropdown show"
toggleform="function () {}"
updateconfidentialattribute="function () {}"
>
<div
class="dropdown-menu sidebar-item-warning-message"
>
<div>
<p>
<gl-sprintf-stub
message="You are going to turn on the confidentiality. This means that only team members with %{strongStart}at least Reporter access%{strongEnd} are able to see and leave comments on the %{issuableType}."
/>
</p>
<edit-form-buttons-stub
fullpath=""
/>
</div>
</div>
</div>
`;
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { deprecatedCreateFlash as flash } from '~/flash';
import createStore from '~/notes/stores';
import EditFormButtons from '~/sidebar/components/confidential/edit_form_buttons.vue';
import eventHub from '~/sidebar/event_hub';
jest.mock('~/sidebar/event_hub', () => ({ $emit: jest.fn() }));
jest.mock('~/flash');
describe('Edit Form Buttons', () => {
let wrapper;
let store;
const findConfidentialToggle = () => wrapper.find('[data-testid="confidential-toggle"]');
const createComponent = ({ props = {}, data = {}, resolved = true }) => {
store = createStore();
if (resolved) {
jest.spyOn(store, 'dispatch').mockResolvedValue();
} else {
jest.spyOn(store, 'dispatch').mockRejectedValue();
}
wrapper = shallowMount(EditFormButtons, {
propsData: {
fullPath: '',
...props,
},
data() {
return {
isLoading: true,
...data,
};
},
store,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when isLoading', () => {
beforeEach(() => {
createComponent({
props: {
confidential: false,
},
});
});
it('renders "Applying" in the toggle button', () => {
expect(findConfidentialToggle().text()).toBe('Applying');
});
it('disables the toggle button', () => {
expect(findConfidentialToggle().props('disabled')).toBe(true);
});
it('sets loading on the toggle button', () => {
expect(findConfidentialToggle().props('loading')).toBe(true);
});
});
describe('when not confidential', () => {
it('renders Turn On in the toggle button', () => {
createComponent({
data: {
isLoading: false,
},
props: {
confidential: false,
},
});
expect(findConfidentialToggle().text()).toBe('Turn On');
});
});
describe('when confidential', () => {
beforeEach(() => {
createComponent({
data: {
isLoading: false,
},
props: {
confidential: true,
},
});
});
it('renders on or off text based on confidentiality', () => {
expect(findConfidentialToggle().text()).toBe('Turn Off');
});
});
describe('when succeeds', () => {
beforeEach(() => {
createComponent({ data: { isLoading: false }, props: { confidential: true } });
findConfidentialToggle().vm.$emit('click', new Event('click'));
});
it('dispatches the correct action', () => {
expect(store.dispatch).toHaveBeenCalledWith('updateConfidentialityOnIssuable', {
confidential: false,
fullPath: '',
});
});
it('resets loading on the toggle button', () => {
return waitForPromises().then(() => {
expect(findConfidentialToggle().props('loading')).toBe(false);
});
});
it('emits close form', () => {
return waitForPromises().then(() => {
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,
});
findConfidentialToggle().vm.$emit('click', new Event('click'));
});
it('calls flash with the correct message', () => {
expect(flash).toHaveBeenCalledWith(
'Something went wrong trying to change the confidentiality of this issue',
);
});
});
});
import { shallowMount } from '@vue/test-utils';
import EditForm from '~/sidebar/components/confidential/edit_form.vue';
describe('Edit Form Dropdown', () => {
let wrapper;
const toggleForm = () => {};
const updateConfidentialAttribute = () => {};
const createComponent = (props) => {
wrapper = shallowMount(EditForm, {
propsData: {
...props,
isLoading: false,
fullPath: '',
issuableType: 'issue',
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when not confidential', () => {
it('renders "You are going to turn on the confidentiality." in the ', () => {
createComponent({
confidential: false,
toggleForm,
updateConfidentialAttribute,
});
expect(wrapper.element).toMatchSnapshot();
});
});
describe('when confidential', () => {
it('renders on or off text based on confidentiality', () => {
createComponent({
confidential: true,
toggleForm,
updateConfidentialAttribute,
});
expect(wrapper.element).toMatchSnapshot();
});
});
});
......@@ -7,11 +7,13 @@ describe('Sidebar Confidentiality Content', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
const findText = () => wrapper.find('[data-testid="confidential-text"]');
const findCollapsedIcon = () => wrapper.find('[data-testid="sidebar-collapsed-icon"]');
const createComponent = (confidential = false) => {
const createComponent = ({ confidential = false, issuableType = 'issue' } = {}) => {
wrapper = shallowMount(SidebarConfidentialityContent, {
propsData: {
confidential,
issuableType,
},
});
};
......@@ -20,6 +22,13 @@ describe('Sidebar Confidentiality Content', () => {
wrapper.destroy();
});
it('emits `expandSidebar` event on collapsed icon click', () => {
createComponent();
findCollapsedIcon().trigger('click');
expect(wrapper.emitted('expandSidebar')).toHaveLength(1);
});
describe('when issue is non-confidential', () => {
beforeEach(() => {
createComponent();
......@@ -39,20 +48,24 @@ describe('Sidebar Confidentiality Content', () => {
});
describe('when issue is confidential', () => {
beforeEach(() => {
createComponent(true);
});
it('renders a non-confidential icon', () => {
it('renders a confidential icon', () => {
createComponent({ confidential: true });
expect(findIcon().props('name')).toBe('eye-slash');
});
it('does not add `is-active` class to the icon', () => {
it('adds `is-active` class to the icon', () => {
createComponent({ confidential: true });
expect(findIcon().classes()).toContain('is-active');
});
it('displays a non-confidential text', () => {
expect(findText().text()).toBe('This is confidential');
it('displays a correct confidential text for issue', () => {
createComponent({ confidential: true });
expect(findText().text()).toBe('This issue is confidential');
});
it('displays a correct confidential text for epic', () => {
createComponent({ confidential: true, issuableType: 'epic' });
expect(findText().text()).toBe('This epic is confidential');
});
});
});
......@@ -143,4 +143,31 @@ describe('Sidebar Confidentiality Form', () => {
});
});
});
describe('when issuable type is `epic`', () => {
beforeEach(() => {
createComponent({ props: { confidential: true, issuableType: 'epic' } });
});
it('renders a message about making an epic non-confidential', () => {
expect(findWarningMessage().text()).toBe(
'You are going to turn off the confidentiality. This means everyone will be able to see and leave a comment on this epic.',
);
});
it('calls a mutation to set epic confidentiality with correct parameters', () => {
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: confidentialityQueries[wrapper.vm.issuableType].mutation,
variables: {
input: {
confidential: false,
iid: '1',
groupPath: 'group/project',
},
},
});
});
});
});
......@@ -86,6 +86,10 @@ describe('Sidebar Confidentiality Widget', () => {
expect(findConfidentialityForm().props('confidential')).toBe(true);
expect(findConfidentialityContent().props('confidential')).toBe(true);
});
it('emits `confidentialityUpdated` event with a `false` payload', () => {
expect(wrapper.emitted('confidentialityUpdated')).toEqual([[false]]);
});
});
describe('when issue is confidential', () => {
......@@ -111,6 +115,10 @@ describe('Sidebar Confidentiality Widget', () => {
expect(findConfidentialityForm().props('confidential')).toBe(false);
expect(findConfidentialityContent().props('confidential')).toBe(false);
});
it('emits `confidentialityUpdated` event with a `true` payload', () => {
expect(wrapper.emitted('confidentialityUpdated')).toEqual([[true]]);
});
});
it('displays a flash message when query is rejected', async () => {
......@@ -138,5 +146,14 @@ describe('Sidebar Confidentiality Widget', () => {
expect(findConfidentialityForm().isVisible()).toBe(false);
expect(el.dispatchEvent).toHaveBeenCalled();
expect(wrapper.emitted('closeForm')).toHaveLength(1);
});
it('emits `expandSidebar` event when it is emitted from child component', async () => {
createComponent();
await waitForPromises();
findConfidentialityContent().vm.$emit('expandSidebar');
expect(wrapper.emitted('expandSidebar')).toHaveLength(1);
});
});
import { shallowMount } from '@vue/test-utils';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import createStore from '~/notes/stores';
import * as types from '~/notes/stores/mutation_types';
import ConfidentialIssueSidebar from '~/sidebar/components/confidential/confidential_issue_sidebar.vue';
import EditForm from '~/sidebar/components/confidential/edit_form.vue';
jest.mock('~/flash');
jest.mock('~/sidebar/services/sidebar_service');
describe('Confidential Issue Sidebar Block', () => {
useMockLocationHelper();
let wrapper;
const mutate = jest
.fn()
.mockResolvedValue({ data: { issueSetConfidential: { issue: { confidential: true } } } });
const createComponent = ({ propsData, data = {} }) => {
const store = createStore();
wrapper = shallowMount(ConfidentialIssueSidebar, {
store,
data() {
return data;
},
propsData: {
iid: '',
fullPath: '',
...propsData,
},
mocks: {
$apollo: {
mutate,
},
},
});
};
afterEach(() => {
wrapper.destroy();
});
it.each`
confidential | isEditable
${false} | ${false}
${false} | ${true}
${true} | ${false}
${true} | ${true}
`(
'renders for confidential = $confidential and isEditable = $isEditable',
({ confidential, isEditable }) => {
createComponent({
propsData: {
isEditable,
},
});
wrapper.vm.$store.state.noteableData.confidential = confidential;
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.element).toMatchSnapshot();
});
},
);
describe('if editable', () => {
beforeEach(() => {
createComponent({
propsData: {
isEditable: true,
},
});
wrapper.vm.$store.state.noteableData.confidential = true;
});
it('displays the edit form when editable', () => {
wrapper.setData({ edit: false });
return wrapper.vm
.$nextTick()
.then(() => {
wrapper.find({ ref: 'editLink' }).trigger('click');
return wrapper.vm.$nextTick();
})
.then(() => {
expect(wrapper.find(EditForm).exists()).toBe(true);
});
});
it('displays the edit form when opened from collapsed state', () => {
wrapper.setData({ edit: false });
return wrapper.vm
.$nextTick()
.then(() => {
wrapper.find({ ref: 'collapseIcon' }).trigger('click');
return wrapper.vm.$nextTick();
})
.then(() => {
expect(wrapper.find(EditForm).exists()).toBe(true);
});
});
it('tracks the event when "Edit" is clicked', () => {
const spy = mockTracking('_category_', wrapper.element, jest.spyOn);
const editLink = wrapper.find({ ref: 'editLink' });
triggerEvent(editLink.element);
expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
label: 'right_sidebar',
property: 'confidentiality',
});
});
});
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