Commit 0f65bbb5 authored by Lee Tickett's avatar Lee Tickett Committed by Miguel Rincon

Support setting confidential note attribute in UI [RUN ALL RSPEC] [RUN AS-IF-FOSS]

parent f4908170
<script>
import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import { isEmpty } from 'lodash';
import Autosize from 'autosize';
import { GlButton, GlIcon } from '@gitlab/ui';
import { GlButton, GlIcon, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import { deprecatedCreateFlash as Flash } from '~/flash';
......@@ -34,6 +33,10 @@ export default {
TimelineEntryItem,
GlIcon,
CommentFieldLayout,
GlFormCheckbox,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin(), issuableStateMixin],
props: {
......@@ -46,8 +49,8 @@ export default {
return {
note: '',
noteType: constants.COMMENT,
noteIsConfidential: false,
isSubmitting: false,
isSubmitButtonDisabled: true,
};
},
computed: {
......@@ -80,6 +83,9 @@ export default {
canCreateNote() {
return this.getNoteableData.current_user.can_create_note;
},
canSetConfidential() {
return this.getNoteableData.current_user.can_update;
},
issueActionButtonTitle() {
const openOrClose = this.isOpen ? 'close' : 'reopen';
......@@ -146,13 +152,11 @@ export default {
hasCloseAndCommentButton() {
return !this.glFeatures.removeCommentCloseReopen;
},
},
watch: {
note(newNote) {
this.setIsSubmitButtonDisabled(newNote, this.isSubmitting);
confidentialNotesEnabled() {
return Boolean(this.glFeatures.confidentialNotes);
},
isSubmitting(newValue) {
this.setIsSubmitButtonDisabled(this.note, newValue);
disableSubmitButton() {
return this.note.length === 0 || this.isSubmitting;
},
},
mounted() {
......@@ -173,13 +177,6 @@ export default {
'reopenIssuable',
'toggleIssueLocalState',
]),
setIsSubmitButtonDisabled(note, isSubmitting) {
if (!isEmpty(note) && !isSubmitting) {
this.isSubmitButtonDisabled = false;
} else {
this.isSubmitButtonDisabled = true;
}
},
handleSave(withIssueAction) {
if (this.note.length) {
const noteData = {
......@@ -189,6 +186,7 @@ export default {
note: {
noteable_type: this.noteableType,
noteable_id: this.getNoteableData.id,
confidential: this.noteIsConfidential,
note: this.note,
},
merge_request_diff_head_sha: this.getNoteableData.diff_head_sha,
......@@ -252,6 +250,7 @@ export default {
if (shouldClear) {
this.note = '';
this.noteIsConfidential = false;
this.resizeTextarea();
this.$refs.markdownField.previewMarkdown = false;
}
......@@ -340,11 +339,26 @@ export default {
</markdown-field>
</comment-field-layout>
<div class="note-form-actions">
<gl-form-checkbox
v-if="confidentialNotesEnabled && canSetConfidential"
v-model="noteIsConfidential"
class="gl-mb-6"
data-testid="confidential-note-checkbox"
>
{{ s__('Notes|Make this comment confidential') }}
<gl-icon
v-gl-tooltip:tooltipcontainer.bottom
name="question"
:size="16"
:title="s__('Notes|Confidential comments are only visible to project members')"
class="gl-text-gray-500"
/>
</gl-form-checkbox>
<div
class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
>
<gl-button
:disabled="isSubmitButtonDisabled"
:disabled="disableSubmitButton"
class="js-comment-button js-comment-submit-button"
data-qa-selector="comment_button"
data-testid="comment-button"
......@@ -357,7 +371,7 @@ export default {
>{{ commentButtonTitle }}</gl-button
>
<gl-button
:disabled="isSubmitButtonDisabled"
:disabled="disableSubmitButton"
name="button"
category="primary"
variant="success"
......
......@@ -210,9 +210,9 @@ export default {
v-gl-tooltip:tooltipcontainer.bottom
data-testid="confidentialIndicator"
name="eye-slash"
:size="14"
:title="s__('Notes|Private comments are accessible by internal staff only')"
class="gl-ml-1 gl-text-gray-700 align-middle"
:size="16"
:title="s__('Notes|This comment is confidential and only visible to project members')"
class="gl-ml-1 gl-text-orange-700 align-middle"
/>
<slot name="extra-controls"></slot>
<gl-loading-icon
......
......@@ -243,7 +243,8 @@ module NotesActions
:type,
:note,
:line_code, # LegacyDiffNote
:position # DiffNote
:position, # DiffNote
:confidential
).tap do |create_params|
create_params.merge!(
params.permit(:merge_request_diff_head_sha, :in_reply_to_discussion_id)
......
......@@ -52,6 +52,7 @@ class Projects::IssuesController < Projects::ApplicationController
real_time_enabled = Gitlab::ActionCable::Config.in_app? || Feature.enabled?(real_time_feature_flag, @project)
push_to_gon_attributes(:features, real_time_feature_flag, real_time_enabled)
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
record_experiment_user(:invite_members_version_a)
record_experiment_user(:invite_members_version_b)
......
---
title: Support setting confidential note attribute in UI
merge_request: 52949
author: Lee Tickett @leetickett
type: added
---
name: confidential_notes
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52949
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/207474
milestone: '13.9'
type: development
group: group::product planning
default_enabled: false
......@@ -7,12 +7,12 @@ type: reference, howto
# Threads **(FREE)**
The ability to contribute conversationally is offered throughout GitLab.
You can use words to communicate with other users all over GitLab.
You can leave a comment in the following places:
For example, you can leave a comment in the following places:
- Issues
- Epics **(ULTIMATE)**
- Epics
- Merge requests
- Snippets
- Commits
......@@ -281,6 +281,23 @@ edit existing comments. Non-team members are restricted from adding or editing c
Additionally, locked issues and merge requests can not be reopened.
## Confidential Comments
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207473) in GitLab 13.9.
> - It's [deployed behind a feature flag](../feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to enable it. **(FREE SELF)**
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
When creating a comment, you can decide to make it visible only to the project members (users with Reporter and higher permissions).
To create a confidential comment, select the **Make this comment confidential** checkbox before you submit it.
![Confidential comments](img/confidential_comments_v13_9.png)
## Merge Request Reviews
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/4213) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.4.
......@@ -418,25 +435,6 @@ the thread will be automatically resolved, and GitLab will create a new commit
and push the suggested change directly into the codebase in the merge request's
branch. [Developer permission](../permissions.md) is required to do so.
### Enable or disable Custom commit messages for suggestions **(FREE SELF)**
Custom commit messages for suggestions is under development but ready for production use. It is
deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can opt to disable it.
To disable custom commit messages for suggestions:
```ruby
Feature.disable(:suggestions_custom_commit)
```
To enable custom commit messages for suggestions:
```ruby
Feature.enable(:suggestions_custom_commit)
```
### Multi-line Suggestions
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53310) in GitLab 11.10.
......@@ -532,27 +530,6 @@ to your branch to address your reviewers' requests.
![A code change suggestion displayed, with the button to apply the batch of suggestions highlighted.](img/apply_batch_of_suggestions_v13_1.jpg "Apply a batch of suggestions")
#### Enable or disable Batch Suggestions **(FREE SELF)**
Batch Suggestions is
deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can opt to disable it for your instance.
To enable it:
```ruby
# Instance-wide
Feature.enable(:batch_suggestions)
```
To disable it:
```ruby
# Instance-wide
Feature.disable(:batch_suggestions)
```
## Start a thread by replying to a standard comment
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/30299) in GitLab 11.9
......@@ -585,3 +562,62 @@ In the comment, click the **More Actions** menu and click **Assign to commenting
Click the button again to unassign the commenter.
![Assign to commenting user](img/quickly_assign_commenter_v13_1.png)
## Enable or disable Confidential Comments **(FREE SELF)**
Confidential Comments is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:confidential_notes)
```
To disable it:
```ruby
Feature.disable(:confidential_notes)
```
## Enable or disable Custom commit messages for suggestions **(FREE SELF)**
Custom commit messages for suggestions is under development but ready for production use. It is
deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can opt to disable it.
To disable custom commit messages for suggestions:
```ruby
Feature.disable(:suggestions_custom_commit)
```
To enable custom commit messages for suggestions:
```ruby
Feature.enable(:suggestions_custom_commit)
```
## Enable or disable Batch Suggestions **(FREE SELF)**
Batch Suggestions is
deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can opt to disable it for your instance.
To enable it:
```ruby
# Instance-wide
Feature.enable(:batch_suggestions)
```
To disable it:
```ruby
# Instance-wide
Feature.disable(:batch_suggestions)
```
......@@ -20251,7 +20251,10 @@ msgstr ""
msgid "Notes|Collapse replies"
msgstr ""
msgid "Notes|Private comments are accessible by internal staff only"
msgid "Notes|Confidential comments are only visible to project members"
msgstr ""
msgid "Notes|Make this comment confidential"
msgstr ""
msgid "Notes|Show all activity"
......@@ -20266,6 +20269,9 @@ msgstr ""
msgid "Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost"
msgstr ""
msgid "Notes|This comment is confidential and only visible to project members"
msgstr ""
msgid "Notes|You're only seeing %{boldStart}other activity%{boldEnd} in the feed. To add a comment, switch to one of the following options."
msgstr ""
......
......@@ -315,7 +315,7 @@ RSpec.describe Projects::NotesController do
let(:note_text) { 'some note' }
let(:request_params) do
{
note: { note: note_text, noteable_id: merge_request.id, noteable_type: 'MergeRequest' },
note: { note: note_text, noteable_id: merge_request.id, noteable_type: 'MergeRequest' }.merge(extra_note_params),
namespace_id: project.namespace,
project_id: project,
merge_request_diff_head_sha: 'sha',
......@@ -325,6 +325,7 @@ RSpec.describe Projects::NotesController do
end
let(:extra_request_params) { {} }
let(:extra_note_params) { {} }
let(:project_visibility) { Gitlab::VisibilityLevel::PUBLIC }
let(:merge_requests_access_level) { ProjectFeature::ENABLED }
......@@ -423,6 +424,41 @@ RSpec.describe Projects::NotesController do
end
end
context 'when creating a confidential note' do
let(:extra_request_params) { { format: :json } }
context 'when `confidential` parameter is not provided' do
it 'sets `confidential` to `false` in JSON response' do
create!
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['confidential']).to be false
end
end
context 'when `confidential` parameter is `false`' do
let(:extra_note_params) { { confidential: false } }
it 'sets `confidential` to `false` in JSON response' do
create!
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['confidential']).to be false
end
end
context 'when `confidential` parameter is `true`' do
let(:extra_note_params) { { confidential: true } }
it 'sets `confidential` to `true` in JSON response' do
create!
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['confidential']).to be true
end
end
end
context 'when creating a note with quick actions' do
context 'with commands that return changes' do
let(:note_text) { "/award :thumbsup:\n/estimate 1d\n/spend 3h" }
......
import { nextTick } from 'vue';
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Autosize from 'autosize';
import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { deprecatedCreateFlash as flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import createStore from '~/notes/stores';
......@@ -21,11 +22,25 @@ describe('issue_comment_form component', () => {
let wrapper;
let axiosMock;
const findCloseReopenButton = () => wrapper.find('[data-testid="close-reopen-button"]');
const findCloseReopenButton = () => wrapper.findByTestId('close-reopen-button');
const findCommentButton = () => wrapper.findByTestId('comment-button');
const findTextArea = () => wrapper.findByTestId('comment-field');
const findConfidentialNoteCheckbox = () => wrapper.findByTestId('confidential-note-checkbox');
const createNotableDataMock = (data = {}) => {
return {
...noteableDataMock,
...data,
};
};
const findCommentButton = () => wrapper.find('[data-testid="comment-button"]');
const notableDataMockCanUpdateIssuable = createNotableDataMock({
current_user: { can_update: true, can_create_note: true },
});
const findTextArea = () => wrapper.find('[data-testid="comment-field"]');
const notableDataMockCannotUpdateIssuable = createNotableDataMock({
current_user: { can_update: false, can_create_note: true },
});
const mountComponent = ({
initialData = {},
......@@ -33,23 +48,29 @@ describe('issue_comment_form component', () => {
noteableData = noteableDataMock,
notesData = notesDataMock,
userData = userDataMock,
features = {},
mountFunction = shallowMount,
} = {}) => {
store.dispatch('setNoteableData', noteableData);
store.dispatch('setNotesData', notesData);
store.dispatch('setUserData', userData);
wrapper = mountFunction(CommentForm, {
propsData: {
noteableType,
},
data() {
return {
...initialData,
};
},
store,
});
wrapper = extendedWrapper(
mountFunction(CommentForm, {
propsData: {
noteableType,
},
data() {
return {
...initialData,
};
},
store,
provide: {
glFeatures: features,
},
}),
);
};
beforeEach(() => {
......@@ -359,6 +380,83 @@ describe('issue_comment_form component', () => {
});
});
});
describe('confidential notes checkbox', () => {
describe('when confidentialNotes feature flag is `false`', () => {
const features = { confidentialNotes: false };
it('should not render checkbox', () => {
mountComponent({
mountFunction: mount,
initialData: { note: 'confidential note' },
noteableData: { ...notableDataMockCanUpdateIssuable },
features,
});
const checkbox = findConfidentialNoteCheckbox();
expect(checkbox.exists()).toBe(false);
});
});
describe('when confidentialNotes feature flag is `true`', () => {
const features = { confidentialNotes: true };
it('should render checkbox as unchecked by default', () => {
mountComponent({
mountFunction: mount,
initialData: { note: 'confidential note' },
noteableData: { ...notableDataMockCanUpdateIssuable },
features,
});
const checkbox = findConfidentialNoteCheckbox();
expect(checkbox.exists()).toBe(true);
expect(checkbox.element.checked).toBe(false);
});
describe.each`
shouldCheckboxBeChecked
${true}
${false}
`('when checkbox value is `$shouldCheckboxBeChecked`', ({ shouldCheckboxBeChecked }) => {
it(`sets \`confidential\` to \`${shouldCheckboxBeChecked}\``, async () => {
mountComponent({
mountFunction: mount,
initialData: { note: 'confidential note' },
noteableData: { ...notableDataMockCanUpdateIssuable },
features,
});
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue({});
const checkbox = findConfidentialNoteCheckbox();
// check checkbox
checkbox.element.checked = shouldCheckboxBeChecked;
checkbox.trigger('change');
await wrapper.vm.$nextTick();
// submit comment
wrapper.findByTestId('comment-button').trigger('click');
const [providedData] = wrapper.vm.saveNote.mock.calls[0];
expect(providedData.data.note.confidential).toBe(shouldCheckboxBeChecked);
});
});
describe('when user cannot update issuable', () => {
it('should not render checkbox', () => {
mountComponent({
mountFunction: mount,
noteableData: { ...notableDataMockCannotUpdateIssuable },
features,
});
expect(findConfidentialNoteCheckbox().exists()).toBe(false);
});
});
});
});
});
describe('user is not logged in', () => {
......
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