Commit b28db7d3 authored by Natalia Tepluhina's avatar Natalia Tepluhina Committed by Simon Knox

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
parent 48e8c06d
......@@ -17,6 +17,7 @@ import commentForm from './comment_form.vue';
import discussionFilterNote from './discussion_filter_note.vue';
import noteableDiscussion from './noteable_discussion.vue';
import noteableNote from './noteable_note.vue';
import SidebarSubscription from './sidebar_subscription.vue';
export default {
name: 'NotesApp',
......@@ -30,6 +31,7 @@ export default {
skeletonLoadingContainer,
discussionFilterNote,
OrderedLayout,
SidebarSubscription,
},
mixins: [glFeatureFlagsMixin()],
props: {
......@@ -261,6 +263,7 @@ export default {
<template>
<div v-show="shouldShow" id="notes">
<sidebar-subscription :iid="noteableData.iid" :noteable-data="noteableData" />
<ordered-layout :slot-keys="slotKeys">
<template #form>
<comment-form
......
<script>
import { mapActions } from 'vuex';
import { IssuableType } from '~/issue_show/constants';
import { fetchPolicies } from '~/lib/graphql';
import { confidentialityQueries } from '~/sidebar/constants';
import { defaultClient as gqlClient } from '~/sidebar/graphql';
export default {
props: {
noteableData: {
type: Object,
required: true,
},
iid: {
type: Number,
required: true,
},
},
computed: {
fullPath() {
if (this.noteableData.web_url) {
return this.noteableData.web_url.split('/-/')[0].substring(1);
}
return null;
},
issuableType() {
return this.noteableData.noteableType.toLowerCase();
},
},
created() {
if (this.issuableType !== IssuableType.Issue) {
return;
}
gqlClient
.watchQuery({
query: confidentialityQueries[this.issuableType].query,
variables: {
iid: String(this.iid),
fullPath: this.fullPath,
},
fetchPolicy: fetchPolicies.CACHE_ONLY,
})
.subscribe((res) => {
const issuable = res.data?.workspace?.issuable;
if (issuable) {
this.setConfidentiality(issuable.confidential);
}
});
},
methods: {
...mapActions(['setConfidentiality']),
},
render() {
return null;
},
};
</script>
......@@ -4,7 +4,7 @@ import Vue from 'vue';
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql';
import { confidentialWidget } from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
import loadAwardsHandler from '../../awards_handler';
......@@ -340,6 +340,15 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
if (hasQuickActions && message) {
eTagPoll.makeRequest();
// synchronizing the quick action with the sidebar widget
// this is a temporary solution until we have confidentiality real-time updates
if (
confidentialWidget.setConfidentiality &&
message.some((m) => m.includes('confidential'))
) {
confidentialWidget.setConfidentiality();
}
$('.js-gfm-input').trigger('clear-commands-cache.atwho');
Flash(message || __('Commands applied'), 'notice', noteData.flashContainer);
......@@ -719,33 +728,3 @@ 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, errors },
} = data;
if (errors?.length) {
Flash(errors[0], 'alert');
} else {
setConfidentiality({ commit }, issue.confidential);
}
});
};
......@@ -26,6 +26,8 @@ Sidebar.prototype.removeListeners = function () {
// eslint-disable-next-line @gitlab/no-global-event-off
this.sidebar.off('hidden.gl.dropdown');
// eslint-disable-next-line @gitlab/no-global-event-off
this.sidebar.off('hiddenGlDropdown');
// eslint-disable-next-line @gitlab/no-global-event-off
$('.dropdown').off('loading.gl.dropdown');
// eslint-disable-next-line @gitlab/no-global-event-off
$('.dropdown').off('loaded.gl.dropdown');
......@@ -37,6 +39,7 @@ Sidebar.prototype.addEventListeners = function () {
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
this.sidebar.on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
this.sidebar.on('hiddenGlDropdown', this, this.onSidebarDropdownHidden);
$document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked);
return $(document)
......
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
export default {
components: {
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
confidential: {
type: Boolean,
required: true,
},
},
computed: {
confidentialText() {
return this.confidential
? sprintf(__('This %{issuableType} is confidential'), {
issuableType: this.issuableType,
})
: __('Not confidential');
},
confidentialIcon() {
return this.confidential ? 'eye-slash' : 'eye';
},
tooltipLabel() {
return this.confidential ? __('Confidential') : __('Not confidential');
},
},
};
</script>
<template>
<div>
<div v-gl-tooltip.viewport.left :title="tooltipLabel" class="sidebar-collapsed-icon">
<gl-icon
:size="16"
:name="confidentialIcon"
class="sidebar-item-icon inline"
:class="{ 'is-active': confidential }"
/>
</div>
<gl-icon
:size="16"
:name="confidentialIcon"
class="sidebar-item-icon inline hide-collapsed"
:class="{ 'is-active': confidential }"
/>
<span class="hide-collapsed" data-testid="confidential-text">{{ confidentialText }}</span>
</div>
</template>
<script>
import { GlSprintf, GlButton } from '@gitlab/ui';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import { confidentialityQueries } from '~/sidebar/constants';
export default {
i18n: {
confidentialityOnWarning: __(
'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}.',
),
confidentialityOffWarning: __(
'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}.',
),
},
components: {
GlSprintf,
GlButton,
},
inject: ['fullPath', 'iid'],
props: {
confidential: {
required: true,
type: Boolean,
},
issuableType: {
required: true,
type: String,
},
},
data() {
return {
loading: false,
};
},
computed: {
toggleButtonText() {
if (this.loading) {
return __('Applying');
}
return this.confidential ? __('Turn off') : __('Turn on');
},
warningMessage() {
return this.confidential
? this.$options.i18n.confidentialityOffWarning
: this.$options.i18n.confidentialityOnWarning;
},
},
methods: {
submitForm() {
this.loading = true;
this.$apollo
.mutate({
mutation: confidentialityQueries[this.issuableType].mutation,
variables: {
input: {
projectPath: this.fullPath,
iid: this.iid,
confidential: !this.confidential,
},
},
})
.then(
({
data: {
issuableSetConfidential: { errors },
},
}) => {
if (errors.length) {
createFlash({
message: errors[0],
});
} else {
this.$emit('closeForm');
}
},
)
.catch(() => {
createFlash({
message: sprintf(
__('Something went wrong while setting %{issuableType} confidentiality.'),
{
issuableType: this.issuableType,
},
),
});
})
.finally(() => {
this.loading = false;
});
},
},
};
</script>
<template>
<div class="dropdown show">
<div class="dropdown-menu sidebar-item-warning-message">
<div>
<p data-testid="warning-message">
<gl-sprintf :message="warningMessage">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
<template #issuableType>{{ issuableType }}</template>
</gl-sprintf>
</p>
<div class="sidebar-item-warning-message-actions">
<gl-button class="gl-mr-3" data-testid="confidential-cancel" @click="$emit('closeForm')">
{{ __('Cancel') }}
</gl-button>
<gl-button
category="secondary"
variant="warning"
:disabled="loading"
:loading="loading"
data-testid="confidential-toggle"
@click.prevent="submitForm"
>
{{ toggleButtonText }}
</gl-button>
</div>
</div>
</div>
</div>
</template>
<script>
import produce from 'immer';
import Vue from 'vue';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { confidentialityQueries } from '~/sidebar/constants';
import SidebarConfidentialityContent from './sidebar_confidentiality_content.vue';
import SidebarConfidentialityForm from './sidebar_confidentiality_form.vue';
export const confidentialWidget = Vue.observable({
setConfidentiality: null,
});
const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', {
bubbles: true,
});
export default {
tracking: {
event: 'click_edit_button',
label: 'right_sidebar',
property: 'confidentiality',
},
components: {
SidebarEditableItem,
SidebarConfidentialityContent,
SidebarConfidentialityForm,
},
inject: ['fullPath', 'iid'],
props: {
issuableType: {
required: true,
type: String,
},
},
data() {
return {
confidential: false,
};
},
apollo: {
confidential: {
query() {
return confidentialityQueries[this.issuableType].query;
},
variables() {
return {
fullPath: this.fullPath,
iid: this.iid,
};
},
update(data) {
return data.workspace?.issuable?.confidential || false;
},
error() {
createFlash({
message: sprintf(
__('Something went wrong while setting %{issuableType} confidentiality.'),
{
issuableType: this.issuableType,
},
),
});
},
},
},
computed: {
isLoading() {
return this.$apollo.queries.confidential.loading;
},
},
mounted() {
confidentialWidget.setConfidentiality = this.setConfidentiality;
},
destroyed() {
confidentialWidget.setConfidentiality = null;
},
methods: {
closeForm() {
this.$refs.editable.collapse();
this.$el.dispatchEvent(hideDropdownEvent);
},
// synchronizing the quick action with the sidebar widget
// this is a temporary solution until we have confidentiality real-time updates
setConfidentiality() {
const { defaultClient: client } = this.$apollo.provider.clients;
const sourceData = client.readQuery({
query: confidentialityQueries[this.issuableType].query,
variables: { fullPath: this.fullPath, iid: this.iid },
});
const data = produce(sourceData, (draftData) => {
// eslint-disable-next-line no-param-reassign
draftData.workspace.issuable.confidential = !this.confidential;
});
client.writeQuery({
query: confidentialityQueries[this.issuableType].query,
variables: { fullPath: this.fullPath, iid: this.iid },
data,
});
},
},
};
</script>
<template>
<sidebar-editable-item
ref="editable"
:title="__('Confidentiality')"
:tracking="$options.tracking"
:loading="isLoading"
class="block confidentiality"
>
<template #collapsed>
<div>
<sidebar-confidentiality-content v-if="!isLoading" :confidential="confidential" />
</div>
</template>
<template #default>
<sidebar-confidentiality-content :confidential="confidential" />
<sidebar-confidentiality-form
:confidential="confidential"
:issuable-type="issuableType"
@closeForm="closeForm"
/>
</template>
</sidebar-editable-item>
</template>
......@@ -15,6 +15,15 @@ export default {
required: false,
default: false,
},
tracking: {
type: Object,
required: false,
default: () => ({
event: null,
label: null,
property: null,
}),
},
},
data() {
return {
......@@ -71,14 +80,18 @@ export default {
<template>
<div>
<div class="gl-display-flex gl-align-items-center gl-mb-3" @click.self="collapse">
<span data-testid="title">{{ title }}</span>
<gl-loading-icon v-if="loading" inline class="gl-ml-2" />
<div class="gl-display-flex gl-align-items-center" @click.self="collapse">
<span class="hide-collapsed" data-testid="title">{{ title }}</span>
<gl-loading-icon v-if="loading" inline class="gl-ml-2 hide-collapsed" />
<gl-loading-icon v-if="loading" inline class="gl-mx-auto gl-my-0 hide-expanded" />
<gl-button
v-if="canUpdate"
variant="link"
class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto js-sidebar-dropdown-toggle"
class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto js-sidebar-dropdown-toggle hide-collapsed"
data-testid="edit-button"
:data-track-event="tracking.event"
:data-track-label="tracking.label"
:data-track-property="tracking.property"
@keyup.esc="toggle"
@click="toggle"
>
......
import { IssuableType } from '~/issue_show/constants';
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.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';
import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
......@@ -14,3 +16,10 @@ export const assigneesQueries = {
mutation: updateMergeRequestParticipantsMutation,
},
};
export const confidentialityQueries = {
[IssuableType.Issue]: {
query: issueConfidentialQuery,
mutation: updateIssueConfidentialMutation,
},
};
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
export const defaultClient = createDefaultClient();
export const apolloProvider = new VueApollo({
defaultClient,
});
......@@ -2,7 +2,6 @@ import $ from 'jquery';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createFlash from '~/flash';
import createDefaultClient from '~/lib/graphql';
import {
isInIssuePage,
isInDesignPage,
......@@ -10,9 +9,10 @@ import {
parseBoolean,
} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import { apolloProvider } from '~/sidebar/graphql';
import Translate from '../vue_shared/translate';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
import SidebarLabels from './components/labels/sidebar_labels.vue';
import IssuableLockForm from './components/lock/issuable_lock_form.vue';
......@@ -54,9 +54,6 @@ function getSidebarAssigneeAvailabilityData() {
function mountAssigneesComponent(mediator) {
const el = document.getElementById('js-vue-sidebar-assignees');
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
if (!el) return;
......@@ -87,9 +84,6 @@ function mountAssigneesComponent(mediator) {
function mountReviewersComponent(mediator) {
const el = document.getElementById('js-vue-sidebar-reviewers');
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
if (!el) return;
......@@ -121,10 +115,6 @@ export function mountSidebarLabels() {
return false;
}
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({
el,
apolloProvider,
......@@ -139,39 +129,37 @@ export function mountSidebarLabels() {
});
}
function mountConfidentialComponent(mediator) {
function mountConfidentialComponent() {
const el = document.getElementById('js-confidential-entry-point');
if (!el) {
return;
}
const { fullPath, iid } = getSidebarOptions();
if (!el) return;
const dataNode = document.getElementById('js-confidential-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
import(/* webpackChunkName: 'notesStore' */ '~/notes/stores')
.then(
({ store }) =>
new Vue({
el,
store,
components: {
ConfidentialIssueSidebar,
},
render: (createElement) =>
createElement('confidential-issue-sidebar', {
props: {
iid: String(iid),
fullPath,
isEditable: initialData.is_editable,
service: mediator.service,
},
}),
}),
)
.catch(() => {
createFlash({ message: __('Failed to load sidebar confidential toggle') });
});
// eslint-disable-next-line no-new
new Vue({
el,
apolloProvider,
components: {
SidebarConfidentialityWidget,
},
provide: {
iid: String(iid),
fullPath,
canUpdate: initialData.is_editable,
},
render: (createElement) =>
createElement('sidebar-confidentiality-widget', {
props: {
issuableType:
isInIssuePage() || isInIncidentPage() || isInDesignPage() ? 'issue' : 'merge_request',
},
}),
});
}
function mountLockComponent() {
......@@ -280,9 +268,6 @@ function mountSeverityComponent() {
if (!severityContainerEl) {
return false;
}
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
const { fullPath, iid, severity } = getSidebarOptions();
......
query issueConfidential($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
__typename
issuable: issue(iid: $iid) {
__typename
id
confidential
}
}
}
mutation updateIssueConfidential($input: IssueSetConfidentialInput!) {
issuableSetConfidential: issueSetConfidential(input: $input) {
issuable: issue {
id
confidential
}
errors
}
}
---
title: Sidebar confidentiality component updates in real-time
merge_request: 53858
author:
type: changed
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import { store } from '~/notes/stores';
import { apolloProvider } from '~/sidebar/graphql';
import * as CEMountSidebar from '~/sidebar/mount_sidebar';
import IterationSelect from './components/iteration_select.vue';
import SidebarItemEpicsSelect from './components/sidebar_item_epics_select.vue';
......@@ -85,10 +85,6 @@ function mountIterationSelect() {
if (!el) {
return false;
}
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
const { groupPath, canEdit, projectPath, issueIid } = el.dataset;
return new Vue({
......
......@@ -100,6 +100,7 @@ describe('BoardCardAssigneeDropdown', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
fakeApollo = null;
delete window.gon.current_username;
});
......
......@@ -12493,9 +12493,6 @@ msgstr ""
msgid "Failed to load related branches"
msgstr ""
msgid "Failed to load sidebar confidential toggle"
msgstr ""
msgid "Failed to load sidebar lock status"
msgstr ""
......@@ -27809,6 +27806,9 @@ msgstr ""
msgid "Something went wrong while resolving this discussion. Please try again."
msgstr ""
msgid "Something went wrong while setting %{issuableType} confidentiality."
msgstr ""
msgid "Something went wrong while stopping this environment. Please try again."
msgstr ""
......@@ -31500,6 +31500,12 @@ msgstr ""
msgid "Turn On"
msgstr ""
msgid "Turn off"
msgstr ""
msgid "Turn on"
msgstr ""
msgid "Turn on %{strongStart}usage ping%{strongEnd} to activate analysis of user activity, known as %{docLinkStart}Cohorts%{docLinkEnd}."
msgstr ""
......@@ -33804,6 +33810,9 @@ msgstr ""
msgid "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}."
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 ""
......
......@@ -10,7 +10,6 @@ import * as actions from '~/notes/stores/actions';
import * as mutationTypes from '~/notes/stores/mutation_types';
import mutations from '~/notes/stores/mutations';
import * as utils from '~/notes/stores/utils';
import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql';
import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
......@@ -1276,68 +1275,6 @@ describe('Actions Notes Store', () => {
});
});
describe('updateConfidentialityOnIssuable', () => {
state = { noteableData: { confidential: false } };
const iid = '1';
const projectPath = 'full/path';
const getters = { getNoteableData: { iid } };
const actionArgs = { fullPath: projectPath, confidential: true };
const confidential = true;
beforeEach(() => {
jest
.spyOn(utils.gqClient, 'mutate')
.mockResolvedValue({ data: { issueSetConfidential: { issue: { confidential } } } });
});
it('calls gqClient mutation one time', () => {
actions.updateConfidentialityOnIssuable({ commit: () => {}, state, getters }, actionArgs);
expect(utils.gqClient.mutate).toHaveBeenCalledTimes(1);
});
it('calls gqClient mutation with the correct values', () => {
actions.updateConfidentialityOnIssuable({ commit: () => {}, state, getters }, actionArgs);
expect(utils.gqClient.mutate).toHaveBeenCalledWith({
mutation: updateIssueConfidentialMutation,
variables: { input: { iid, projectPath, confidential } },
});
});
describe('on success of mutation', () => {
it('calls commit with the correct values', () => {
const commitSpy = jest.fn();
return actions
.updateConfidentialityOnIssuable({ commit: commitSpy, state, getters }, actionArgs)
.then(() => {
expect(Flash).not.toHaveBeenCalled();
expect(commitSpy).toHaveBeenCalledWith(
mutationTypes.SET_ISSUE_CONFIDENTIAL,
confidential,
);
});
});
});
describe('on user recoverable error', () => {
it('sends the error to Flash', () => {
const error = 'error';
jest
.spyOn(utils.gqClient, 'mutate')
.mockResolvedValue({ data: { issueSetConfidential: { errors: [error] } } });
return actions
.updateConfidentialityOnIssuable({ commit: () => {}, state, getters }, actionArgs)
.then(() => {
expect(Flash).toHaveBeenCalledWith(error, 'alert');
});
});
});
});
describe.each`
issuableType
${'issue'} | ${'merge_request'}
......
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SidebarConfidentialityContent from '~/sidebar/components/confidential/sidebar_confidentiality_content.vue';
describe('Sidebar Confidentiality Content', () => {
let wrapper;
const findIcon = () => wrapper.findComponent(GlIcon);
const findText = () => wrapper.find('[data-testid="confidential-text"]');
const createComponent = (confidential = false) => {
wrapper = shallowMount(SidebarConfidentialityContent, {
propsData: {
confidential,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('when issue is non-confidential', () => {
beforeEach(() => {
createComponent();
});
it('renders a non-confidential icon', () => {
expect(findIcon().props('name')).toBe('eye');
});
it('does not add `is-active` class to the icon', () => {
expect(findIcon().classes()).not.toContain('is-active');
});
it('displays a non-confidential text', () => {
expect(findText().text()).toBe('Not confidential');
});
});
describe('when issue is confidential', () => {
beforeEach(() => {
createComponent(true);
});
it('renders a non-confidential icon', () => {
expect(findIcon().props('name')).toBe('eye-slash');
});
it('does not add `is-active` class to the icon', () => {
expect(findIcon().classes()).toContain('is-active');
});
it('displays a non-confidential text', () => {
expect(findText().text()).toBe('This is confidential');
});
});
});
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import SidebarConfidentialityForm from '~/sidebar/components/confidential/sidebar_confidentiality_form.vue';
import { confidentialityQueries } from '~/sidebar/constants';
jest.mock('~/flash');
describe('Sidebar Confidentiality Form', () => {
let wrapper;
const findWarningMessage = () => wrapper.find(`[data-testid="warning-message"]`);
const findConfidentialToggle = () => wrapper.find(`[data-testid="confidential-toggle"]`);
const findCancelButton = () => wrapper.find(`[data-testid="confidential-cancel"]`);
const createComponent = ({
props = {},
mutate = jest.fn().mockResolvedValue('Success'),
} = {}) => {
wrapper = shallowMount(SidebarConfidentialityForm, {
provide: {
fullPath: 'group/project',
iid: '1',
},
propsData: {
confidential: false,
issuableType: 'issue',
...props,
},
mocks: {
$apollo: {
mutate,
},
},
stubs: {
GlSprintf,
},
});
};
afterEach(() => {
wrapper.destroy();
});
it('emits a `closeForm` event when Cancel button is clicked', () => {
createComponent();
findCancelButton().vm.$emit('click');
expect(wrapper.emitted().closeForm).toHaveLength(1);
});
it('renders a loading state after clicking on turn on/off button', async () => {
createComponent();
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
await nextTick();
expect(findConfidentialToggle().props('loading')).toBe(true);
});
it('creates a flash if mutation is rejected', async () => {
createComponent({ mutate: jest.fn().mockRejectedValue('Error!') });
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
message: 'Something went wrong while setting issue confidentiality.',
});
});
it('creates a flash if mutation contains errors', async () => {
createComponent({
mutate: jest.fn().mockResolvedValue({
data: { issuableSetConfidential: { errors: ['Houston, we have a problem!'] } },
}),
});
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
message: 'Houston, we have a problem!',
});
});
describe('when issue is not confidential', () => {
beforeEach(() => {
createComponent();
});
it('renders a message about making an issue confidential', () => {
expect(findWarningMessage().text()).toBe(
'You are going to turn on confidentiality. Only team members with at least Reporter access will be able to see and leave comments on the issue.',
);
});
it('has a `Turn on` button text', () => {
expect(findConfidentialToggle().text()).toBe('Turn on');
});
it('calls a mutation to set confidential to true on button click', () => {
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: confidentialityQueries[wrapper.vm.issuableType].mutation,
variables: {
input: {
confidential: true,
iid: '1',
projectPath: 'group/project',
},
},
});
});
});
describe('when issue is confidential', () => {
beforeEach(() => {
createComponent({ props: { confidential: true } });
});
it('renders a message about making an issue 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 issue.',
);
});
it('has a `Turn off` button text', () => {
expect(findConfidentialToggle().text()).toBe('Turn off');
});
it('calls a mutation to set confidential to false on button click', () => {
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',
projectPath: 'group/project',
},
},
});
});
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import SidebarConfidentialityContent from '~/sidebar/components/confidential/sidebar_confidentiality_content.vue';
import SidebarConfidentialityForm from '~/sidebar/components/confidential/sidebar_confidentiality_form.vue';
import SidebarConfidentialityWidget, {
confidentialWidget,
} from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
import { issueConfidentialityResponse } from '../../mock_data';
jest.mock('~/flash');
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Sidebar Confidentiality Widget', () => {
let wrapper;
let fakeApollo;
const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findConfidentialityForm = () => wrapper.findComponent(SidebarConfidentialityForm);
const findConfidentialityContent = () => wrapper.findComponent(SidebarConfidentialityContent);
const createComponent = ({
confidentialQueryHandler = jest.fn().mockResolvedValue(issueConfidentialityResponse()),
} = {}) => {
fakeApollo = createMockApollo([[issueConfidentialQuery, confidentialQueryHandler]]);
wrapper = shallowMount(SidebarConfidentialityWidget, {
localVue,
apolloProvider: fakeApollo,
provide: {
fullPath: 'group/project',
iid: '1',
canUpdate: true,
},
propsData: {
issuableType: 'issue',
},
stubs: {
SidebarEditableItem,
},
});
};
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
});
it('passes a `loading` prop as true to editable item when query is loading', () => {
createComponent();
expect(findEditableItem().props('loading')).toBe(true);
});
it('exposes a method via external observable', () => {
createComponent();
expect(confidentialWidget.setConfidentiality).toEqual(wrapper.vm.setConfidentiality);
});
describe('when issue is not confidential', () => {
beforeEach(async () => {
createComponent();
await waitForPromises();
});
it('passes a `loading` prop as false to editable item', () => {
expect(findEditableItem().props('loading')).toBe(false);
});
it('passes false to `confidential` prop of child components', () => {
expect(findConfidentialityForm().props('confidential')).toBe(false);
expect(findConfidentialityContent().props('confidential')).toBe(false);
});
it('changes confidentiality to true after setConfidentiality is called', async () => {
confidentialWidget.setConfidentiality();
await nextTick();
expect(findConfidentialityForm().props('confidential')).toBe(true);
expect(findConfidentialityContent().props('confidential')).toBe(true);
});
});
describe('when issue is confidential', () => {
beforeEach(async () => {
createComponent({
confidentialQueryHandler: jest.fn().mockResolvedValue(issueConfidentialityResponse(true)),
});
await waitForPromises();
});
it('passes a `loading` prop as false to editable item', () => {
expect(findEditableItem().props('loading')).toBe(false);
});
it('passes false to `confidential` prop of child components', () => {
expect(findConfidentialityForm().props('confidential')).toBe(true);
expect(findConfidentialityContent().props('confidential')).toBe(true);
});
it('changes confidentiality to false after setConfidentiality is called', async () => {
confidentialWidget.setConfidentiality();
await nextTick();
expect(findConfidentialityForm().props('confidential')).toBe(false);
expect(findConfidentialityContent().props('confidential')).toBe(false);
});
});
it('displays a flash message when query is rejected', async () => {
createComponent({
confidentialQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
});
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
});
it('closes the form and dispatches an event when `closeForm` is emitted', async () => {
createComponent();
const el = wrapper.vm.$el;
jest.spyOn(el, 'dispatchEvent');
await waitForPromises();
wrapper.vm.$refs.editable.expand();
await nextTick();
expect(findConfidentialityForm().isVisible()).toBe(true);
findConfidentialityForm().vm.$emit('closeForm');
await nextTick();
expect(findConfidentialityForm().isVisible()).toBe(false);
expect(el.dispatchEvent).toHaveBeenCalled();
});
});
......@@ -220,4 +220,17 @@ const mockData = {
},
};
export const issueConfidentialityResponse = (confidential = false) => ({
data: {
workspace: {
__typename: 'Project',
issuable: {
__typename: 'Issue',
id: 'gid://gitlab/Issue/4',
confidential,
},
},
},
});
export default mockData;
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