Commit 7f682a50 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch 'psi-no-all' into 'master'

Warn when mention all users in a group

See merge request gitlab-org/gitlab!77624
parents 99ac6732 1c858cc3
...@@ -124,13 +124,6 @@ const writeButtonSelector = '.js-md-write-button'; ...@@ -124,13 +124,6 @@ const writeButtonSelector = '.js-md-write-button';
lastTextareaPreviewed = null; lastTextareaPreviewed = null;
const markdownToolbar = $('.md-header-toolbar'); const markdownToolbar = $('.md-header-toolbar');
$.fn.setupMarkdownPreview = function () {
const $form = $(this);
$form.find('textarea.markdown-area').on('input', () => {
markdownPreview.hideReferencedUsers($form);
});
};
$(document).on('markdown-preview:show', (e, $form) => { $(document).on('markdown-preview:show', (e, $form) => {
if (!$form) { if (!$form) {
return; return;
......
...@@ -51,7 +51,6 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { ...@@ -51,7 +51,6 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
// Add dropzone area to the form. // Add dropzone area to the form.
const $mdArea = formTextarea.closest('.md-area'); const $mdArea = formTextarea.closest('.md-area');
form.setupMarkdownPreview();
const $formDropzone = form.find('.div-dropzone'); const $formDropzone = form.find('.div-dropzone');
$formDropzone.parent().addClass('div-dropzone-wrapper'); $formDropzone.parent().addClass('div-dropzone-wrapper');
$formDropzone.append(divHover); $formDropzone.append(divHover);
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { GlIcon } from '@gitlab/ui'; import { GlIcon } from '@gitlab/ui';
import $ from 'jquery'; import $ from 'jquery';
import '~/behaviors/markdown/render_gfm'; import '~/behaviors/markdown/render_gfm';
import { unescape } from 'lodash'; import { debounce, unescape } from 'lodash';
import createFlash from '~/flash'; import createFlash from '~/flash';
import GLForm from '~/gl_form'; import GLForm from '~/gl_form';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
...@@ -110,7 +110,7 @@ export default { ...@@ -110,7 +110,7 @@ export default {
return { return {
markdownPreview: '', markdownPreview: '',
referencedCommands: '', referencedCommands: '',
referencedUsers: '', referencedUsers: [],
hasSuggestion: false, hasSuggestion: false,
markdownPreviewLoading: false, markdownPreviewLoading: false,
previewMarkdown: false, previewMarkdown: false,
...@@ -188,6 +188,24 @@ export default { ...@@ -188,6 +188,24 @@ export default {
}); });
} }
}, },
textareaValue: {
immediate: true,
handler(textareaValue, oldVal) {
const all = /@all([^\w._-]|$)/;
const hasAll = all.test(textareaValue);
const hadAll = all.test(oldVal);
const justAddedAll = !hadAll && hasAll;
const justRemovedAll = hadAll && !hasAll;
if (justAddedAll) {
this.debouncedFetchMarkdown();
} else if (justRemovedAll) {
this.referencedUsers = [];
}
},
},
}, },
mounted() { mounted() {
// GLForm class handles all the toolbar buttons // GLForm class handles all the toolbar buttons
...@@ -222,9 +240,9 @@ export default { ...@@ -222,9 +240,9 @@ export default {
if (this.textareaValue) { if (this.textareaValue) {
this.markdownPreviewLoading = true; this.markdownPreviewLoading = true;
this.markdownPreview = __('Loading…'); this.markdownPreview = __('Loading…');
axios
.post(this.markdownPreviewPath, { text: this.textareaValue }) this.fetchMarkdown()
.then((response) => this.renderMarkdown(response.data)) .then((data) => this.renderMarkdown(data))
.catch(() => .catch(() =>
createFlash({ createFlash({
message: __('Error loading markdown preview'), message: __('Error loading markdown preview'),
...@@ -239,17 +257,28 @@ export default { ...@@ -239,17 +257,28 @@ export default {
this.previewMarkdown = false; this.previewMarkdown = false;
}, },
fetchMarkdown() {
return axios.post(this.markdownPreviewPath, { text: this.textareaValue }).then(({ data }) => {
const { references } = data;
if (references) {
this.referencedCommands = references.commands;
this.referencedUsers = references.users;
this.hasSuggestion = references.suggestions?.length > 0;
this.suggestions = references.suggestions;
}
return data;
});
},
debouncedFetchMarkdown: debounce(function debouncedFetchMarkdown() {
return this.fetchMarkdown();
}, 400),
renderMarkdown(data = {}) { renderMarkdown(data = {}) {
this.markdownPreviewLoading = false; this.markdownPreviewLoading = false;
this.markdownPreview = data.body || __('Nothing to preview.'); this.markdownPreview = data.body || __('Nothing to preview.');
if (data.references) {
this.referencedCommands = data.references.commands;
this.referencedUsers = data.references.users;
this.hasSuggestion = data.references.suggestions && data.references.suggestions.length;
this.suggestions = data.references.suggestions;
}
this.$nextTick() this.$nextTick()
.then(() => $(this.$refs['markdown-preview']).renderGFM()) .then(() => $(this.$refs['markdown-preview']).renderGFM())
.catch(() => .catch(() =>
...@@ -326,18 +355,14 @@ export default { ...@@ -326,18 +355,14 @@ export default {
v-html="markdownPreview /* eslint-disable-line vue/no-v-html */" v-html="markdownPreview /* eslint-disable-line vue/no-v-html */"
></div> ></div>
</template> </template>
<template v-if="previewMarkdown && !markdownPreviewLoading"> <div
<div v-if="referencedCommands && previewMarkdown && !markdownPreviewLoading"
v-if="referencedCommands" class="referenced-commands"
class="referenced-commands" v-html="referencedCommands /* eslint-disable-line vue/no-v-html */"
v-html="referencedCommands /* eslint-disable-line vue/no-v-html */" ></div>
></div> <div v-if="shouldShowReferencedUsers" class="referenced-users">
<div v-if="shouldShowReferencedUsers" class="referenced-users"> <gl-icon name="warning-solid" />
<gl-icon name="warning-solid" /> <span v-html="addMultipleToDiscussionWarning /* eslint-disable-line vue/no-v-html */"></span>
<span </div>
v-html="addMultipleToDiscussionWarning /* eslint-disable-line vue/no-v-html */"
></span>
</div>
</template>
</div> </div>
</template> </template>
...@@ -90,6 +90,8 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] = ...@@ -90,6 +90,8 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
/> />
<!----> <!---->
<!---->
</div> </div>
</div> </div>
</div> </div>
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter'; import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery'; import $ from 'jquery';
import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants'; import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
...@@ -242,6 +243,41 @@ describe('Markdown field component', () => { ...@@ -242,6 +243,41 @@ describe('Markdown field component', () => {
expect(dropzoneSpy).toHaveBeenCalled(); expect(dropzoneSpy).toHaveBeenCalled();
}); });
describe('mentioning all users', () => {
const users = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((i) => `user_${i}`);
it('shows warning on mention of all users', async () => {
axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } });
subject.setProps({ textareaValue: 'hello @all' });
await axios.waitFor(markdownPreviewPath).then(() => {
expect(subject.text()).toContain(
'You are about to add 11 people to the discussion. They will all receive a notification.',
);
});
});
it('removes warning when all mention is removed', async () => {
axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } });
subject.setProps({ textareaValue: 'hello @all' });
await axios.waitFor(markdownPreviewPath);
jest.spyOn(axios, 'post');
subject.setProps({ textareaValue: 'hello @allan' });
await nextTick();
expect(axios.post).not.toHaveBeenCalled();
expect(subject.text()).not.toContain(
'You are about to add 11 people to the discussion. They will all receive a notification.',
);
});
});
}); });
}); });
......
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