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'; ...@@ -17,6 +17,7 @@ import commentForm from './comment_form.vue';
import discussionFilterNote from './discussion_filter_note.vue'; import discussionFilterNote from './discussion_filter_note.vue';
import noteableDiscussion from './noteable_discussion.vue'; import noteableDiscussion from './noteable_discussion.vue';
import noteableNote from './noteable_note.vue'; import noteableNote from './noteable_note.vue';
import SidebarSubscription from './sidebar_subscription.vue';
export default { export default {
name: 'NotesApp', name: 'NotesApp',
...@@ -30,6 +31,7 @@ export default { ...@@ -30,6 +31,7 @@ export default {
skeletonLoadingContainer, skeletonLoadingContainer,
discussionFilterNote, discussionFilterNote,
OrderedLayout, OrderedLayout,
SidebarSubscription,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
props: { props: {
...@@ -261,6 +263,7 @@ export default { ...@@ -261,6 +263,7 @@ export default {
<template> <template>
<div v-show="shouldShow" id="notes"> <div v-show="shouldShow" id="notes">
<sidebar-subscription :iid="noteableData.iid" :noteable-data="noteableData" />
<ordered-layout :slot-keys="slotKeys"> <ordered-layout :slot-keys="slotKeys">
<template #form> <template #form>
<comment-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'; ...@@ -4,7 +4,7 @@ import Vue from 'vue';
import Api from '~/api'; import Api from '~/api';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale'; 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 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 updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
import loadAwardsHandler from '../../awards_handler'; import loadAwardsHandler from '../../awards_handler';
...@@ -340,6 +340,15 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -340,6 +340,15 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
if (hasQuickActions && message) { if (hasQuickActions && message) {
eTagPoll.makeRequest(); 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'); $('.js-gfm-input').trigger('clear-commands-cache.atwho');
Flash(message || __('Commands applied'), 'notice', noteData.flashContainer); Flash(message || __('Commands applied'), 'notice', noteData.flashContainer);
...@@ -719,33 +728,3 @@ export const updateAssignees = ({ commit }, assignees) => { ...@@ -719,33 +728,3 @@ export const updateAssignees = ({ commit }, assignees) => {
export const updateDiscussionPosition = ({ commit }, updatedPosition) => { export const updateDiscussionPosition = ({ commit }, updatedPosition) => {
commit(types.UPDATE_DISCUSSION_POSITION, 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 () { ...@@ -26,6 +26,8 @@ Sidebar.prototype.removeListeners = function () {
// eslint-disable-next-line @gitlab/no-global-event-off // eslint-disable-next-line @gitlab/no-global-event-off
this.sidebar.off('hidden.gl.dropdown'); this.sidebar.off('hidden.gl.dropdown');
// eslint-disable-next-line @gitlab/no-global-event-off // 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'); $('.dropdown').off('loading.gl.dropdown');
// eslint-disable-next-line @gitlab/no-global-event-off // eslint-disable-next-line @gitlab/no-global-event-off
$('.dropdown').off('loaded.gl.dropdown'); $('.dropdown').off('loaded.gl.dropdown');
...@@ -37,6 +39,7 @@ Sidebar.prototype.addEventListeners = function () { ...@@ -37,6 +39,7 @@ Sidebar.prototype.addEventListeners = function () {
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
this.sidebar.on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); this.sidebar.on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
this.sidebar.on('hiddenGlDropdown', this, this.onSidebarDropdownHidden);
$document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked); $document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked);
return $(document) 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 { ...@@ -15,6 +15,15 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
tracking: {
type: Object,
required: false,
default: () => ({
event: null,
label: null,
property: null,
}),
},
}, },
data() { data() {
return { return {
...@@ -71,14 +80,18 @@ export default { ...@@ -71,14 +80,18 @@ export default {
<template> <template>
<div> <div>
<div class="gl-display-flex gl-align-items-center gl-mb-3" @click.self="collapse"> <div class="gl-display-flex gl-align-items-center" @click.self="collapse">
<span data-testid="title">{{ title }}</span> <span class="hide-collapsed" data-testid="title">{{ title }}</span>
<gl-loading-icon v-if="loading" inline class="gl-ml-2" /> <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 <gl-button
v-if="canUpdate" v-if="canUpdate"
variant="link" 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-testid="edit-button"
:data-track-event="tracking.event"
:data-track-label="tracking.label"
:data-track-property="tracking.property"
@keyup.esc="toggle" @keyup.esc="toggle"
@click="toggle" @click="toggle"
> >
......
import { IssuableType } from '~/issue_show/constants'; 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 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 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'; import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
...@@ -14,3 +16,10 @@ export const assigneesQueries = { ...@@ -14,3 +16,10 @@ export const assigneesQueries = {
mutation: updateMergeRequestParticipantsMutation, 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'; ...@@ -2,7 +2,6 @@ import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createFlash from '~/flash'; import createFlash from '~/flash';
import createDefaultClient from '~/lib/graphql';
import { import {
isInIssuePage, isInIssuePage,
isInDesignPage, isInDesignPage,
...@@ -10,9 +9,10 @@ import { ...@@ -10,9 +9,10 @@ import {
parseBoolean, parseBoolean,
} from '~/lib/utils/common_utils'; } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; 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 Translate from '../vue_shared/translate';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; 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 CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
import SidebarLabels from './components/labels/sidebar_labels.vue'; import SidebarLabels from './components/labels/sidebar_labels.vue';
import IssuableLockForm from './components/lock/issuable_lock_form.vue'; import IssuableLockForm from './components/lock/issuable_lock_form.vue';
...@@ -54,9 +54,6 @@ function getSidebarAssigneeAvailabilityData() { ...@@ -54,9 +54,6 @@ function getSidebarAssigneeAvailabilityData() {
function mountAssigneesComponent(mediator) { function mountAssigneesComponent(mediator) {
const el = document.getElementById('js-vue-sidebar-assignees'); const el = document.getElementById('js-vue-sidebar-assignees');
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
if (!el) return; if (!el) return;
...@@ -87,9 +84,6 @@ function mountAssigneesComponent(mediator) { ...@@ -87,9 +84,6 @@ function mountAssigneesComponent(mediator) {
function mountReviewersComponent(mediator) { function mountReviewersComponent(mediator) {
const el = document.getElementById('js-vue-sidebar-reviewers'); const el = document.getElementById('js-vue-sidebar-reviewers');
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
if (!el) return; if (!el) return;
...@@ -121,10 +115,6 @@ export function mountSidebarLabels() { ...@@ -121,10 +115,6 @@ export function mountSidebarLabels() {
return false; return false;
} }
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({ return new Vue({
el, el,
apolloProvider, apolloProvider,
...@@ -139,38 +129,36 @@ export function mountSidebarLabels() { ...@@ -139,38 +129,36 @@ export function mountSidebarLabels() {
}); });
} }
function mountConfidentialComponent(mediator) { function mountConfidentialComponent() {
const el = document.getElementById('js-confidential-entry-point'); const el = document.getElementById('js-confidential-entry-point');
if (!el) {
return;
}
const { fullPath, iid } = getSidebarOptions(); const { fullPath, iid } = getSidebarOptions();
if (!el) return;
const dataNode = document.getElementById('js-confidential-issue-data'); const dataNode = document.getElementById('js-confidential-issue-data');
const initialData = JSON.parse(dataNode.innerHTML); const initialData = JSON.parse(dataNode.innerHTML);
import(/* webpackChunkName: 'notesStore' */ '~/notes/stores') // eslint-disable-next-line no-new
.then(
({ store }) =>
new Vue({ new Vue({
el, el,
store, apolloProvider,
components: { components: {
ConfidentialIssueSidebar, SidebarConfidentialityWidget,
}, },
render: (createElement) => provide: {
createElement('confidential-issue-sidebar', {
props: {
iid: String(iid), iid: String(iid),
fullPath, fullPath,
isEditable: initialData.is_editable, canUpdate: initialData.is_editable,
service: mediator.service, },
render: (createElement) =>
createElement('sidebar-confidentiality-widget', {
props: {
issuableType:
isInIssuePage() || isInIncidentPage() || isInDesignPage() ? 'issue' : 'merge_request',
}, },
}), }),
}),
)
.catch(() => {
createFlash({ message: __('Failed to load sidebar confidential toggle') });
}); });
} }
...@@ -280,9 +268,6 @@ function mountSeverityComponent() { ...@@ -280,9 +268,6 @@ function mountSeverityComponent() {
if (!severityContainerEl) { if (!severityContainerEl) {
return false; return false;
} }
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
const { fullPath, iid, severity } = getSidebarOptions(); 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 Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import { store } from '~/notes/stores'; import { store } from '~/notes/stores';
import { apolloProvider } from '~/sidebar/graphql';
import * as CEMountSidebar from '~/sidebar/mount_sidebar'; import * as CEMountSidebar from '~/sidebar/mount_sidebar';
import IterationSelect from './components/iteration_select.vue'; import IterationSelect from './components/iteration_select.vue';
import SidebarItemEpicsSelect from './components/sidebar_item_epics_select.vue'; import SidebarItemEpicsSelect from './components/sidebar_item_epics_select.vue';
...@@ -85,10 +85,6 @@ function mountIterationSelect() { ...@@ -85,10 +85,6 @@ function mountIterationSelect() {
if (!el) { if (!el) {
return false; return false;
} }
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
const { groupPath, canEdit, projectPath, issueIid } = el.dataset; const { groupPath, canEdit, projectPath, issueIid } = el.dataset;
return new Vue({ return new Vue({
......
...@@ -100,6 +100,7 @@ describe('BoardCardAssigneeDropdown', () => { ...@@ -100,6 +100,7 @@ describe('BoardCardAssigneeDropdown', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
fakeApollo = null;
delete window.gon.current_username; delete window.gon.current_username;
}); });
......
...@@ -12493,9 +12493,6 @@ msgstr "" ...@@ -12493,9 +12493,6 @@ msgstr ""
msgid "Failed to load related branches" msgid "Failed to load related branches"
msgstr "" msgstr ""
msgid "Failed to load sidebar confidential toggle"
msgstr ""
msgid "Failed to load sidebar lock status" msgid "Failed to load sidebar lock status"
msgstr "" msgstr ""
...@@ -27821,6 +27818,9 @@ msgstr "" ...@@ -27821,6 +27818,9 @@ msgstr ""
msgid "Something went wrong while resolving this discussion. Please try again." msgid "Something went wrong while resolving this discussion. Please try again."
msgstr "" msgstr ""
msgid "Something went wrong while setting %{issuableType} confidentiality."
msgstr ""
msgid "Something went wrong while stopping this environment. Please try again." msgid "Something went wrong while stopping this environment. Please try again."
msgstr "" msgstr ""
...@@ -31509,6 +31509,12 @@ msgstr "" ...@@ -31509,6 +31509,12 @@ msgstr ""
msgid "Turn On" msgid "Turn On"
msgstr "" 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}." msgid "Turn on %{strongStart}usage ping%{strongEnd} to activate analysis of user activity, known as %{docLinkStart}Cohorts%{docLinkEnd}."
msgstr "" msgstr ""
...@@ -33813,6 +33819,9 @@ 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}." 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 "" 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}." 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 "" msgstr ""
......
...@@ -10,7 +10,6 @@ import * as actions from '~/notes/stores/actions'; ...@@ -10,7 +10,6 @@ import * as actions from '~/notes/stores/actions';
import * as mutationTypes from '~/notes/stores/mutation_types'; import * as mutationTypes from '~/notes/stores/mutation_types';
import mutations from '~/notes/stores/mutations'; import mutations from '~/notes/stores/mutations';
import * as utils from '~/notes/stores/utils'; 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 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 updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub'; import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
...@@ -1276,68 +1275,6 @@ describe('Actions Notes Store', () => { ...@@ -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` describe.each`
issuableType issuableType
${'issue'} | ${'merge_request'} ${'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 = { ...@@ -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; 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