Commit c64be14d authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'ph/placeholderNoteRendering' into 'master'

Renders placeholder notes markdown as rendered HTML

See merge request gitlab-org/gitlab!65840
parents a42cbe9c 70685628
...@@ -421,3 +421,61 @@ export const isValidSha1Hash = (str) => { ...@@ -421,3 +421,61 @@ export const isValidSha1Hash = (str) => {
export function insertFinalNewline(content, endOfLine = '\n') { export function insertFinalNewline(content, endOfLine = '\n') {
return content.slice(-endOfLine.length) !== endOfLine ? `${content}${endOfLine}` : content; return content.slice(-endOfLine.length) !== endOfLine ? `${content}${endOfLine}` : content;
} }
export const markdownConfig = {
// allowedTags from GitLab's inline HTML guidelines
// https://docs.gitlab.com/ee/user/markdown.html#inline-html
ALLOWED_TAGS: [
'a',
'abbr',
'b',
'blockquote',
'br',
'code',
'dd',
'del',
'div',
'dl',
'dt',
'em',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'hr',
'i',
'img',
'ins',
'kbd',
'li',
'ol',
'p',
'pre',
'q',
'rp',
'rt',
'ruby',
's',
'samp',
'span',
'strike',
'strong',
'sub',
'summary',
'sup',
'table',
'tbody',
'td',
'tfoot',
'th',
'thead',
'tr',
'tt',
'ul',
'var',
],
ALLOWED_ATTR: ['class', 'style', 'href', 'src'],
ALLOW_DATA_ATTR: false,
};
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
import katex from 'katex'; import katex from 'katex';
import marked from 'marked'; import marked from 'marked';
import { sanitize } from '~/lib/dompurify'; import { sanitize } from '~/lib/dompurify';
import { hasContent } from '~/lib/utils/text_utility'; import { hasContent, markdownConfig } from '~/lib/utils/text_utility';
import Prompt from './prompt.vue'; import Prompt from './prompt.vue';
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
...@@ -140,63 +140,7 @@ export default { ...@@ -140,63 +140,7 @@ export default {
markdown() { markdown() {
renderer.attachments = this.cell.attachments; renderer.attachments = this.cell.attachments;
return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), { return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), markdownConfig);
// allowedTags from GitLab's inline HTML guidelines
// https://docs.gitlab.com/ee/user/markdown.html#inline-html
ALLOWED_TAGS: [
'a',
'abbr',
'b',
'blockquote',
'br',
'code',
'dd',
'del',
'div',
'dl',
'dt',
'em',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'hr',
'i',
'img',
'ins',
'kbd',
'li',
'ol',
'p',
'pre',
'q',
'rp',
'rt',
'ruby',
's',
'samp',
'span',
'strike',
'strong',
'sub',
'summary',
'sup',
'table',
'tbody',
'td',
'tfoot',
'th',
'thead',
'tr',
'tt',
'ul',
'var',
],
ALLOWED_ATTR: ['class', 'style', 'href', 'src'],
ALLOW_DATA_ATTR: false,
});
}, },
}, },
}; };
......
...@@ -13,6 +13,7 @@ import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_ ...@@ -13,6 +13,7 @@ import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import noteable from '../mixins/noteable'; import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
import { renderMarkdown } from '../utils';
import { import {
getStartLineNumber, getStartLineNumber,
getEndLineNumber, getEndLineNumber,
...@@ -300,7 +301,7 @@ export default { ...@@ -300,7 +301,7 @@ export default {
this.isRequesting = true; this.isRequesting = true;
this.oldContent = this.note.note_html; this.oldContent = this.note.note_html;
// eslint-disable-next-line vue/no-mutating-props // eslint-disable-next-line vue/no-mutating-props
this.note.note_html = escape(noteText); this.note.note_html = renderMarkdown(noteText);
this.updateNote(data) this.updateNote(data)
.then(() => { .then(() => {
......
...@@ -312,25 +312,23 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -312,25 +312,23 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
$('.notes-form .flash-container').hide(); // hide previous flash notification $('.notes-form .flash-container').hide(); // hide previous flash notification
commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders
if (replyId) { if (hasQuickActions) {
if (hasQuickActions) { placeholderText = utils.stripQuickActions(placeholderText);
placeholderText = utils.stripQuickActions(placeholderText); }
}
if (placeholderText.length) { if (placeholderText.length) {
commit(types.SHOW_PLACEHOLDER_NOTE, { commit(types.SHOW_PLACEHOLDER_NOTE, {
noteBody: placeholderText, noteBody: placeholderText,
replyId, replyId,
}); });
} }
if (hasQuickActions) { if (hasQuickActions) {
commit(types.SHOW_PLACEHOLDER_NOTE, { commit(types.SHOW_PLACEHOLDER_NOTE, {
isSystemNote: true, isSystemNote: true,
noteBody: utils.getQuickActionText(note), noteBody: utils.getQuickActionText(note),
replyId, replyId,
}); });
}
} }
const processQuickActions = (res) => { const processQuickActions = (res) => {
...@@ -400,9 +398,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -400,9 +398,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
}; };
const removePlaceholder = (res) => { const removePlaceholder = (res) => {
if (replyId) { commit(types.REMOVE_PLACEHOLDER_NOTES);
commit(types.REMOVE_PLACEHOLDER_NOTES);
}
return res; return res;
}; };
......
...@@ -279,7 +279,7 @@ export const getDiscussion = (state) => (discussionId) => ...@@ -279,7 +279,7 @@ export const getDiscussion = (state) => (discussionId) =>
export const commentsDisabled = (state) => state.commentsDisabled; export const commentsDisabled = (state) => state.commentsDisabled;
export const suggestionsCount = (state, getters) => export const suggestionsCount = (state, getters) =>
Object.values(getters.notesById).filter((n) => n.suggestions.length).length; Object.values(getters.notesById).filter((n) => n.suggestions?.length).length;
export const hasDrafts = (state, getters, rootState, rootGetters) => export const hasDrafts = (state, getters, rootState, rootGetters) =>
Boolean(rootGetters['batchComments/hasDrafts']); Boolean(rootGetters['batchComments/hasDrafts']);
/* eslint-disable @gitlab/require-i18n-strings */ /* eslint-disable @gitlab/require-i18n-strings */
import marked from 'marked';
import { sanitize } from '~/lib/dompurify';
import { markdownConfig } from '~/lib/utils/text_utility';
/** /**
* Tracks snowplow event when User toggles timeline view * Tracks snowplow event when User toggles timeline view
...@@ -10,3 +13,7 @@ export const trackToggleTimelineView = (enabled) => ({ ...@@ -10,3 +13,7 @@ export const trackToggleTimelineView = (enabled) => ({
label: 'Status', label: 'Status',
property: enabled, property: enabled,
}); });
export const renderMarkdown = (rawMarkdown) => {
return sanitize(marked(rawMarkdown), markdownConfig);
};
...@@ -16,12 +16,15 @@ ...@@ -16,12 +16,15 @@
* :note="{body: 'This is a note'}" * :note="{body: 'This is a note'}"
* /> * />
*/ */
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { renderMarkdown } from '~/notes/utils';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import userAvatarLink from '../user_avatar/user_avatar_link.vue'; import userAvatarLink from '../user_avatar/user_avatar_link.vue';
export default { export default {
name: 'PlaceholderNote', name: 'PlaceholderNote',
directives: { SafeHtml },
components: { components: {
userAvatarLink, userAvatarLink,
TimelineEntryItem, TimelineEntryItem,
...@@ -34,6 +37,9 @@ export default { ...@@ -34,6 +37,9 @@ export default {
}, },
computed: { computed: {
...mapGetters(['getUserData']), ...mapGetters(['getUserData']),
renderedNote() {
return renderMarkdown(this.note.body);
},
}, },
}; };
</script> </script>
...@@ -57,9 +63,7 @@ export default { ...@@ -57,9 +63,7 @@ export default {
</div> </div>
</div> </div>
<div class="note-body"> <div class="note-body">
<div class="note-text md"> <div v-safe-html="renderedNote" class="note-text md"></div>
<p>{{ note.body }}</p>
</div>
</div> </div>
</div> </div>
</timeline-entry-item> </timeline-entry-item>
......
...@@ -13,6 +13,8 @@ RSpec.describe 'Merge request > Batch comments', :js do ...@@ -13,6 +13,8 @@ RSpec.describe 'Merge request > Batch comments', :js do
end end
before do before do
stub_feature_flags(paginated_notes: false)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { escape } from 'lodash';
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
...@@ -263,7 +262,9 @@ describe('issue_note', () => { ...@@ -263,7 +262,9 @@ describe('issue_note', () => {
await waitForPromises(); await waitForPromises();
expect(alertSpy).not.toHaveBeenCalled(); expect(alertSpy).not.toHaveBeenCalled();
expect(wrapper.vm.note.note_html).toBe(escape(noteBody)); expect(wrapper.vm.note.note_html).toBe(
'<p><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"></p>\n',
);
}); });
}); });
...@@ -291,7 +292,7 @@ describe('issue_note', () => { ...@@ -291,7 +292,7 @@ describe('issue_note', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
let noteBodyProps = noteBody.props(); let noteBodyProps = noteBody.props();
expect(noteBodyProps.note.note_html).toBe(updatedText); expect(noteBodyProps.note.note_html).toBe(`<p>${updatedText}</p>\n`);
noteBody.vm.$emit('cancelForm'); noteBody.vm.$emit('cancelForm');
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
......
...@@ -55,6 +55,8 @@ exports[`Issue placeholder note component matches snapshot 1`] = ` ...@@ -55,6 +55,8 @@ exports[`Issue placeholder note component matches snapshot 1`] = `
<p> <p>
Foo Foo
</p> </p>
</div> </div>
</div> </div>
</div> </div>
......
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