Commit e763c44e authored by Simon Knox's avatar Simon Knox

Merge branch '300654-refactor-confidentiality-sidebar-component-to-use-vue-apollo' into 'master'

Resolve "Refactor Confidentiality sidebar component to use Vue +Apollo"

See merge request gitlab-org/gitlab!53858
parents cee02b00 b28db7d3
......@@ -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 ""
......@@ -27821,6 +27818,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 ""
......@@ -31509,6 +31509,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 ""
......@@ -33813,6 +33819,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