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> <script>
import $ from 'jquery'; import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { isEmpty } from 'lodash';
import Autosize from 'autosize'; import Autosize from 'autosize';
import { GlButton, GlIcon } from '@gitlab/ui'; import { GlButton, GlIcon, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import { deprecatedCreateFlash as Flash } from '~/flash'; import { deprecatedCreateFlash as Flash } from '~/flash';
...@@ -34,6 +33,10 @@ export default { ...@@ -34,6 +33,10 @@ export default {
TimelineEntryItem, TimelineEntryItem,
GlIcon, GlIcon,
CommentFieldLayout, CommentFieldLayout,
GlFormCheckbox,
},
directives: {
GlTooltip: GlTooltipDirective,
}, },
mixins: [glFeatureFlagsMixin(), issuableStateMixin], mixins: [glFeatureFlagsMixin(), issuableStateMixin],
props: { props: {
...@@ -46,8 +49,8 @@ export default { ...@@ -46,8 +49,8 @@ export default {
return { return {
note: '', note: '',
noteType: constants.COMMENT, noteType: constants.COMMENT,
noteIsConfidential: false,
isSubmitting: false, isSubmitting: false,
isSubmitButtonDisabled: true,
}; };
}, },
computed: { computed: {
...@@ -80,6 +83,9 @@ export default { ...@@ -80,6 +83,9 @@ export default {
canCreateNote() { canCreateNote() {
return this.getNoteableData.current_user.can_create_note; return this.getNoteableData.current_user.can_create_note;
}, },
canSetConfidential() {
return this.getNoteableData.current_user.can_update;
},
issueActionButtonTitle() { issueActionButtonTitle() {
const openOrClose = this.isOpen ? 'close' : 'reopen'; const openOrClose = this.isOpen ? 'close' : 'reopen';
...@@ -146,13 +152,11 @@ export default { ...@@ -146,13 +152,11 @@ export default {
hasCloseAndCommentButton() { hasCloseAndCommentButton() {
return !this.glFeatures.removeCommentCloseReopen; return !this.glFeatures.removeCommentCloseReopen;
}, },
}, confidentialNotesEnabled() {
watch: { return Boolean(this.glFeatures.confidentialNotes);
note(newNote) {
this.setIsSubmitButtonDisabled(newNote, this.isSubmitting);
}, },
isSubmitting(newValue) { disableSubmitButton() {
this.setIsSubmitButtonDisabled(this.note, newValue); return this.note.length === 0 || this.isSubmitting;
}, },
}, },
mounted() { mounted() {
...@@ -173,13 +177,6 @@ export default { ...@@ -173,13 +177,6 @@ export default {
'reopenIssuable', 'reopenIssuable',
'toggleIssueLocalState', 'toggleIssueLocalState',
]), ]),
setIsSubmitButtonDisabled(note, isSubmitting) {
if (!isEmpty(note) && !isSubmitting) {
this.isSubmitButtonDisabled = false;
} else {
this.isSubmitButtonDisabled = true;
}
},
handleSave(withIssueAction) { handleSave(withIssueAction) {
if (this.note.length) { if (this.note.length) {
const noteData = { const noteData = {
...@@ -189,6 +186,7 @@ export default { ...@@ -189,6 +186,7 @@ export default {
note: { note: {
noteable_type: this.noteableType, noteable_type: this.noteableType,
noteable_id: this.getNoteableData.id, noteable_id: this.getNoteableData.id,
confidential: this.noteIsConfidential,
note: this.note, note: this.note,
}, },
merge_request_diff_head_sha: this.getNoteableData.diff_head_sha, merge_request_diff_head_sha: this.getNoteableData.diff_head_sha,
...@@ -252,6 +250,7 @@ export default { ...@@ -252,6 +250,7 @@ export default {
if (shouldClear) { if (shouldClear) {
this.note = ''; this.note = '';
this.noteIsConfidential = false;
this.resizeTextarea(); this.resizeTextarea();
this.$refs.markdownField.previewMarkdown = false; this.$refs.markdownField.previewMarkdown = false;
} }
...@@ -340,11 +339,26 @@ export default { ...@@ -340,11 +339,26 @@ export default {
</markdown-field> </markdown-field>
</comment-field-layout> </comment-field-layout>
<div class="note-form-actions"> <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 <div
class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
> >
<gl-button <gl-button
:disabled="isSubmitButtonDisabled" :disabled="disableSubmitButton"
class="js-comment-button js-comment-submit-button" class="js-comment-button js-comment-submit-button"
data-qa-selector="comment_button" data-qa-selector="comment_button"
data-testid="comment-button" data-testid="comment-button"
...@@ -357,7 +371,7 @@ export default { ...@@ -357,7 +371,7 @@ export default {
>{{ commentButtonTitle }}</gl-button >{{ commentButtonTitle }}</gl-button
> >
<gl-button <gl-button
:disabled="isSubmitButtonDisabled" :disabled="disableSubmitButton"
name="button" name="button"
category="primary" category="primary"
variant="success" variant="success"
......
...@@ -210,9 +210,9 @@ export default { ...@@ -210,9 +210,9 @@ export default {
v-gl-tooltip:tooltipcontainer.bottom v-gl-tooltip:tooltipcontainer.bottom
data-testid="confidentialIndicator" data-testid="confidentialIndicator"
name="eye-slash" name="eye-slash"
:size="14" :size="16"
:title="s__('Notes|Private comments are accessible by internal staff only')" :title="s__('Notes|This comment is confidential and only visible to project members')"
class="gl-ml-1 gl-text-gray-700 align-middle" class="gl-ml-1 gl-text-orange-700 align-middle"
/> />
<slot name="extra-controls"></slot> <slot name="extra-controls"></slot>
<gl-loading-icon <gl-loading-icon
......
...@@ -243,7 +243,8 @@ module NotesActions ...@@ -243,7 +243,8 @@ module NotesActions
:type, :type,
:note, :note,
:line_code, # LegacyDiffNote :line_code, # LegacyDiffNote
:position # DiffNote :position, # DiffNote
:confidential
).tap do |create_params| ).tap do |create_params|
create_params.merge!( create_params.merge!(
params.permit(:merge_request_diff_head_sha, :in_reply_to_discussion_id) params.permit(:merge_request_diff_head_sha, :in_reply_to_discussion_id)
......
...@@ -52,6 +52,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -52,6 +52,7 @@ class Projects::IssuesController < Projects::ApplicationController
real_time_enabled = Gitlab::ActionCable::Config.in_app? || Feature.enabled?(real_time_feature_flag, @project) 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_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_a)
record_experiment_user(:invite_members_version_b) 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 ...@@ -7,12 +7,12 @@ type: reference, howto
# Threads **(FREE)** # 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 - Issues
- Epics **(ULTIMATE)** - Epics
- Merge requests - Merge requests
- Snippets - Snippets
- Commits - Commits
...@@ -281,6 +281,23 @@ edit existing comments. Non-team members are restricted from adding or editing c ...@@ -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. 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 ## Merge Request Reviews
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/4213) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.4. > - [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 ...@@ -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 and push the suggested change directly into the codebase in the merge request's
branch. [Developer permission](../permissions.md) is required to do so. 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 ### Multi-line Suggestions
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53310) in GitLab 11.10. > [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. ...@@ -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") ![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 ## Start a thread by replying to a standard comment
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/30299) in GitLab 11.9 > [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 ...@@ -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. Click the button again to unassign the commenter.
![Assign to commenting user](img/quickly_assign_commenter_v13_1.png) ![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 "" ...@@ -20251,7 +20251,10 @@ msgstr ""
msgid "Notes|Collapse replies" msgid "Notes|Collapse replies"
msgstr "" 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 "" msgstr ""
msgid "Notes|Show all activity" msgid "Notes|Show all activity"
...@@ -20266,6 +20269,9 @@ msgstr "" ...@@ -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" 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 "" 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." 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 "" msgstr ""
......
...@@ -315,7 +315,7 @@ RSpec.describe Projects::NotesController do ...@@ -315,7 +315,7 @@ RSpec.describe Projects::NotesController do
let(:note_text) { 'some note' } let(:note_text) { 'some note' }
let(:request_params) do 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, namespace_id: project.namespace,
project_id: project, project_id: project,
merge_request_diff_head_sha: 'sha', merge_request_diff_head_sha: 'sha',
...@@ -325,6 +325,7 @@ RSpec.describe Projects::NotesController do ...@@ -325,6 +325,7 @@ RSpec.describe Projects::NotesController do
end end
let(:extra_request_params) { {} } let(:extra_request_params) { {} }
let(:extra_note_params) { {} }
let(:project_visibility) { Gitlab::VisibilityLevel::PUBLIC } let(:project_visibility) { Gitlab::VisibilityLevel::PUBLIC }
let(:merge_requests_access_level) { ProjectFeature::ENABLED } let(:merge_requests_access_level) { ProjectFeature::ENABLED }
...@@ -423,6 +424,41 @@ RSpec.describe Projects::NotesController do ...@@ -423,6 +424,41 @@ RSpec.describe Projects::NotesController do
end end
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 'when creating a note with quick actions' do
context 'with commands that return changes' do context 'with commands that return changes' do
let(:note_text) { "/award :thumbsup:\n/estimate 1d\n/spend 3h" } let(:note_text) { "/award :thumbsup:\n/estimate 1d\n/spend 3h" }
......
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Autosize from 'autosize'; 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 { deprecatedCreateFlash as flash } from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createStore from '~/notes/stores'; import createStore from '~/notes/stores';
...@@ -21,11 +22,25 @@ describe('issue_comment_form component', () => { ...@@ -21,11 +22,25 @@ describe('issue_comment_form component', () => {
let wrapper; let wrapper;
let axiosMock; 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 = ({ const mountComponent = ({
initialData = {}, initialData = {},
...@@ -33,23 +48,29 @@ describe('issue_comment_form component', () => { ...@@ -33,23 +48,29 @@ describe('issue_comment_form component', () => {
noteableData = noteableDataMock, noteableData = noteableDataMock,
notesData = notesDataMock, notesData = notesDataMock,
userData = userDataMock, userData = userDataMock,
features = {},
mountFunction = shallowMount, mountFunction = shallowMount,
} = {}) => { } = {}) => {
store.dispatch('setNoteableData', noteableData); store.dispatch('setNoteableData', noteableData);
store.dispatch('setNotesData', notesData); store.dispatch('setNotesData', notesData);
store.dispatch('setUserData', userData); store.dispatch('setUserData', userData);
wrapper = mountFunction(CommentForm, { wrapper = extendedWrapper(
propsData: { mountFunction(CommentForm, {
noteableType, propsData: {
}, noteableType,
data() { },
return { data() {
...initialData, return {
}; ...initialData,
}, };
store, },
}); store,
provide: {
glFeatures: features,
},
}),
);
}; };
beforeEach(() => { beforeEach(() => {
...@@ -359,6 +380,83 @@ describe('issue_comment_form component', () => { ...@@ -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', () => { 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