Commit ca7a50bc authored by Doug Stull's avatar Doug Stull

Merge branch '339117-fix-live-markdown-preview-for-single-file-editor' into 'master'

Fix Live Markdown Preview in personal and subgroup projects

See merge request gitlab-org/gitlab!68803
parents 2de8f69f 1a2a9fe9
...@@ -69,6 +69,7 @@ export default () => { ...@@ -69,6 +69,7 @@ export default () => {
const currentAction = $('.js-file-title').data('currentAction'); const currentAction = $('.js-file-title').data('currentAction');
const projectId = editBlobForm.data('project-id'); const projectId = editBlobForm.data('project-id');
const isMarkdown = editBlobForm.data('is-markdown'); const isMarkdown = editBlobForm.data('is-markdown');
const previewMarkdownPath = editBlobForm.data('previewMarkdownPath');
const commitButton = $('.js-commit-button'); const commitButton = $('.js-commit-button');
const cancelLink = $('.btn.btn-cancel'); const cancelLink = $('.btn.btn-cancel');
...@@ -80,6 +81,7 @@ export default () => { ...@@ -80,6 +81,7 @@ export default () => {
currentAction, currentAction,
projectId, projectId,
isMarkdown, isMarkdown,
previewMarkdownPath,
}); });
initPopovers(); initPopovers();
initCodeQualityWalkthroughStep(); initCodeQualityWalkthroughStep();
......
...@@ -11,7 +11,7 @@ import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants'; ...@@ -11,7 +11,7 @@ import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants';
export default class EditBlob { export default class EditBlob {
// The options object has: // The options object has:
// assetsPath, filePath, currentAction, projectId, isMarkdown // assetsPath, filePath, currentAction, projectId, isMarkdown, previewMarkdownPath
constructor(options) { constructor(options) {
this.options = options; this.options = options;
this.configureMonacoEditor(); this.configureMonacoEditor();
...@@ -30,7 +30,10 @@ export default class EditBlob { ...@@ -30,7 +30,10 @@ export default class EditBlob {
import('~/editor/extensions/source_editor_markdown_ext') import('~/editor/extensions/source_editor_markdown_ext')
.then(({ EditorMarkdownExtension: MarkdownExtension } = {}) => { .then(({ EditorMarkdownExtension: MarkdownExtension } = {}) => {
this.editor.use( this.editor.use(
new MarkdownExtension({ instance: this.editor, projectPath: this.options.projectPath }), new MarkdownExtension({
instance: this.editor,
previewMarkdownPath: this.options.previewMarkdownPath,
}),
); );
this.hasMarkdownExtension = true; this.hasMarkdownExtension = true;
addEditorMarkdownListeners(this.editor); addEditorMarkdownListeners(this.editor);
......
...@@ -14,17 +14,9 @@ import { ...@@ -14,17 +14,9 @@ import {
} from '../constants'; } from '../constants';
import { SourceEditorExtension } from './source_editor_extension_base'; import { SourceEditorExtension } from './source_editor_extension_base';
const getPreview = (text, projectPath = '') => { const getPreview = (text, previewMarkdownPath) => {
let url;
if (projectPath) {
url = `/${projectPath}/preview_markdown`;
} else {
const { group, project } = document.body.dataset;
url = `/${group}/${project}/preview_markdown`;
}
return axios return axios
.post(url, { .post(previewMarkdownPath, {
text, text,
}) })
.then(({ data }) => { .then(({ data }) => {
...@@ -43,10 +35,10 @@ const setupDomElement = ({ injectToEl = null } = {}) => { ...@@ -43,10 +35,10 @@ const setupDomElement = ({ injectToEl = null } = {}) => {
}; };
export class EditorMarkdownExtension extends SourceEditorExtension { export class EditorMarkdownExtension extends SourceEditorExtension {
constructor({ instance, projectPath, ...args } = {}) { constructor({ instance, previewMarkdownPath, ...args } = {}) {
super({ instance, ...args }); super({ instance, ...args });
Object.assign(instance, { Object.assign(instance, {
projectPath, previewMarkdownPath,
preview: { preview: {
el: undefined, el: undefined,
action: undefined, action: undefined,
...@@ -112,7 +104,7 @@ export class EditorMarkdownExtension extends SourceEditorExtension { ...@@ -112,7 +104,7 @@ export class EditorMarkdownExtension extends SourceEditorExtension {
fetchPreview() { fetchPreview() {
const { el: previewEl } = this.preview; const { el: previewEl } = this.preview;
getPreview(this.getValue(), this.projectPath) getPreview(this.getValue(), this.previewMarkdownPath)
.then((data) => { .then((data) => {
previewEl.innerHTML = sanitize(data); previewEl.innerHTML = sanitize(data);
syntaxHighlight(previewEl.querySelectorAll('.js-syntax-highlight')); syntaxHighlight(previewEl.querySelectorAll('.js-syntax-highlight'));
......
...@@ -79,6 +79,7 @@ export default { ...@@ -79,6 +79,7 @@ export default {
'editorTheme', 'editorTheme',
'entries', 'entries',
'currentProjectId', 'currentProjectId',
'previewMarkdownPath',
]), ]),
...mapGetters([ ...mapGetters([
'getAlert', 'getAlert',
...@@ -314,14 +315,15 @@ export default { ...@@ -314,14 +315,15 @@ export default {
if ( if (
this.fileType === MARKDOWN_FILE_TYPE && this.fileType === MARKDOWN_FILE_TYPE &&
this.editor?.getEditorType() === EDITOR_TYPE_CODE this.editor?.getEditorType() === EDITOR_TYPE_CODE &&
this.previewMarkdownPath
) { ) {
import('~/editor/extensions/source_editor_markdown_ext') import('~/editor/extensions/source_editor_markdown_ext')
.then(({ EditorMarkdownExtension: MarkdownExtension } = {}) => { .then(({ EditorMarkdownExtension: MarkdownExtension } = {}) => {
this.editor.use( this.editor.use(
new MarkdownExtension({ new MarkdownExtension({
instance: this.editor, instance: this.editor,
projectPath: this.currentProjectId, previewMarkdownPath: this.previewMarkdownPath,
}), }),
); );
}) })
......
...@@ -63,6 +63,7 @@ export function initIde(el, options = {}) { ...@@ -63,6 +63,7 @@ export function initIde(el, options = {}) {
editorTheme: window.gon?.user_color_scheme || DEFAULT_THEME, editorTheme: window.gon?.user_color_scheme || DEFAULT_THEME,
codesandboxBundlerUrl: el.dataset.codesandboxBundlerUrl, codesandboxBundlerUrl: el.dataset.codesandboxBundlerUrl,
environmentsGuidanceAlertDismissed: !parseBoolean(el.dataset.enableEnvironmentsGuidance), environmentsGuidanceAlertDismissed: !parseBoolean(el.dataset.enableEnvironmentsGuidance),
previewMarkdownPath: el.dataset.previewMarkdownPath,
}); });
}, },
beforeDestroy() { beforeDestroy() {
......
...@@ -32,4 +32,5 @@ export default () => ({ ...@@ -32,4 +32,5 @@ export default () => ({
codesandboxBundlerUrl: null, codesandboxBundlerUrl: null,
environmentsGuidanceAlertDismissed: false, environmentsGuidanceAlertDismissed: false,
environmentsGuidanceAlertDetected: false, environmentsGuidanceAlertDetected: false,
previewMarkdownPath: '',
}); });
...@@ -220,7 +220,8 @@ module BlobHelper ...@@ -220,7 +220,8 @@ module BlobHelper
'assets-prefix' => Gitlab::Application.config.assets.prefix, 'assets-prefix' => Gitlab::Application.config.assets.prefix,
'blob-filename' => @blob && @blob.path, 'blob-filename' => @blob && @blob.path,
'project-id' => project.id, 'project-id' => project.id,
'is-markdown' => @blob && @blob.path && Gitlab::MarkupHelper.gitlab_markdown?(@blob.path) 'is-markdown' => @blob && @blob.path && Gitlab::MarkupHelper.gitlab_markdown?(@blob.path),
'preview-markdown-path' => preview_markdown_path(project)
} }
end end
......
...@@ -19,7 +19,8 @@ module IdeHelper ...@@ -19,7 +19,8 @@ module IdeHelper
'merge-request' => @merge_request, 'merge-request' => @merge_request,
'fork-info' => @fork_info&.to_json, 'fork-info' => @fork_info&.to_json,
'project' => convert_to_project_entity_json(@project), 'project' => convert_to_project_entity_json(@project),
'enable-environments-guidance' => enable_environments_guidance?.to_s 'enable-environments-guidance' => enable_environments_guidance?.to_s,
'preview-markdown-path' => @project && preview_markdown_path(@project)
} }
end end
......
...@@ -8,6 +8,8 @@ jest.mock('~/editor/source_editor'); ...@@ -8,6 +8,8 @@ jest.mock('~/editor/source_editor');
jest.mock('~/editor/extensions/source_editor_markdown_ext'); jest.mock('~/editor/extensions/source_editor_markdown_ext');
jest.mock('~/editor/extensions/source_editor_file_template_ext'); jest.mock('~/editor/extensions/source_editor_file_template_ext');
const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown';
describe('Blob Editing', () => { describe('Blob Editing', () => {
const useMock = jest.fn(); const useMock = jest.fn();
const mockInstance = { const mockInstance = {
...@@ -34,6 +36,7 @@ describe('Blob Editing', () => { ...@@ -34,6 +36,7 @@ describe('Blob Editing', () => {
const editorInst = (isMarkdown) => { const editorInst = (isMarkdown) => {
return new EditBlob({ return new EditBlob({
isMarkdown, isMarkdown,
previewMarkdownPath: PREVIEW_MARKDOWN_PATH,
}); });
}; };
...@@ -44,6 +47,7 @@ describe('Blob Editing', () => { ...@@ -44,6 +47,7 @@ describe('Blob Editing', () => {
it('loads FileTemplateExtension by default', async () => { it('loads FileTemplateExtension by default', async () => {
await initEditor(); await initEditor();
expect(useMock).toHaveBeenCalledWith(expect.any(FileTemplateExtension));
expect(FileTemplateExtension).toHaveBeenCalledTimes(1); expect(FileTemplateExtension).toHaveBeenCalledTimes(1);
}); });
...@@ -55,9 +59,12 @@ describe('Blob Editing', () => { ...@@ -55,9 +59,12 @@ describe('Blob Editing', () => {
it('loads MarkdownExtension only for the markdown files', async () => { it('loads MarkdownExtension only for the markdown files', async () => {
await initEditor(true); await initEditor(true);
expect(useMock).toHaveBeenCalledTimes(2); expect(useMock).toHaveBeenCalledWith(expect.any(EditorMarkdownExtension));
expect(FileTemplateExtension).toHaveBeenCalledTimes(1);
expect(EditorMarkdownExtension).toHaveBeenCalledTimes(1); expect(EditorMarkdownExtension).toHaveBeenCalledTimes(1);
expect(EditorMarkdownExtension).toHaveBeenCalledWith({
instance: mockInstance,
previewMarkdownPath: PREVIEW_MARKDOWN_PATH,
});
}); });
}); });
......
...@@ -23,7 +23,7 @@ describe('Markdown Extension for Source Editor', () => { ...@@ -23,7 +23,7 @@ describe('Markdown Extension for Source Editor', () => {
let editorEl; let editorEl;
let panelSpy; let panelSpy;
let mockAxios; let mockAxios;
const projectPath = 'fooGroup/barProj'; const previewMarkdownPath = '/gitlab/fooGroup/barProj/preview_markdown';
const firstLine = 'This is a'; const firstLine = 'This is a';
const secondLine = 'multiline'; const secondLine = 'multiline';
const thirdLine = 'string with some **markup**'; const thirdLine = 'string with some **markup**';
...@@ -57,7 +57,7 @@ describe('Markdown Extension for Source Editor', () => { ...@@ -57,7 +57,7 @@ describe('Markdown Extension for Source Editor', () => {
blobPath: markdownPath, blobPath: markdownPath,
blobContent: text, blobContent: text,
}); });
editor.use(new EditorMarkdownExtension({ instance, projectPath })); editor.use(new EditorMarkdownExtension({ instance, previewMarkdownPath }));
panelSpy = jest.spyOn(EditorMarkdownExtension, 'togglePreviewPanel'); panelSpy = jest.spyOn(EditorMarkdownExtension, 'togglePreviewPanel');
}); });
...@@ -74,7 +74,7 @@ describe('Markdown Extension for Source Editor', () => { ...@@ -74,7 +74,7 @@ describe('Markdown Extension for Source Editor', () => {
shown: false, shown: false,
modelChangeListener: undefined, modelChangeListener: undefined,
}); });
expect(instance.projectPath).toBe(projectPath); expect(instance.previewMarkdownPath).toBe(previewMarkdownPath);
}); });
describe('model language changes listener', () => { describe('model language changes listener', () => {
...@@ -223,34 +223,24 @@ describe('Markdown Extension for Source Editor', () => { ...@@ -223,34 +223,24 @@ describe('Markdown Extension for Source Editor', () => {
}); });
describe('fetchPreview', () => { describe('fetchPreview', () => {
const group = 'foo';
const project = 'bar';
const setData = (path, g, p) => {
instance.projectPath = path;
document.body.setAttribute('data-group', g);
document.body.setAttribute('data-project', p);
};
const fetchPreview = async () => { const fetchPreview = async () => {
instance.fetchPreview(); instance.fetchPreview();
await waitForPromises(); await waitForPromises();
}; };
let previewMarkdownSpy;
beforeEach(() => { beforeEach(() => {
mockAxios.onPost().reply(200, { body: responseData }); previewMarkdownSpy = jest.fn().mockImplementation(() => [200, { body: responseData }]);
mockAxios.onPost(previewMarkdownPath).replyOnce((req) => previewMarkdownSpy(req));
}); });
it('correctly fetches preview based on projectPath', async () => { it('correctly fetches preview based on previewMarkdownPath', async () => {
setData(projectPath, group, project);
await fetchPreview(); await fetchPreview();
expect(mockAxios.history.post[0].url).toBe(`/${projectPath}/preview_markdown`);
expect(mockAxios.history.post[0].data).toEqual(JSON.stringify({ text }));
});
it('correctly fetches preview based on group and project data attributes', async () => { expect(previewMarkdownSpy).toHaveBeenCalledWith(
setData(undefined, group, project); expect.objectContaining({ data: JSON.stringify({ text }) }),
await fetchPreview(); );
expect(mockAxios.history.post[0].url).toBe(`/${group}/${project}/preview_markdown`);
expect(mockAxios.history.post[0].data).toEqual(JSON.stringify({ text }));
}); });
it('puts the fetched content into the preview DOM element', async () => { it('puts the fetched content into the preview DOM element', async () => {
......
...@@ -24,6 +24,8 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -24,6 +24,8 @@ import axios from '~/lib/utils/axios_utils';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import { file } from '../helpers'; import { file } from '../helpers';
const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown';
const defaultFileProps = { const defaultFileProps = {
...file('file.txt'), ...file('file.txt'),
content: 'hello world', content: 'hello world',
...@@ -77,6 +79,7 @@ const prepareStore = (state, activeFile) => { ...@@ -77,6 +79,7 @@ const prepareStore = (state, activeFile) => {
entries: { entries: {
[activeFile.path]: activeFile, [activeFile.path]: activeFile,
}, },
previewMarkdownPath: PREVIEW_MARKDOWN_PATH,
}; };
const storeOptions = createStoreOptions(); const storeOptions = createStoreOptions();
return new Vuex.Store({ return new Vuex.Store({
...@@ -278,10 +281,10 @@ describe('RepoEditor', () => { ...@@ -278,10 +281,10 @@ describe('RepoEditor', () => {
async ({ activeFile, viewer, shouldHaveMarkdownExtension } = {}) => { async ({ activeFile, viewer, shouldHaveMarkdownExtension } = {}) => {
await createComponent({ state: { viewer }, activeFile }); await createComponent({ state: { viewer }, activeFile });
if (shouldHaveMarkdownExtension) { if (shouldHaveMarkdownExtension) {
expect(vm.editor.projectPath).toBe(vm.currentProjectId); expect(vm.editor.previewMarkdownPath).toBe(PREVIEW_MARKDOWN_PATH);
expect(vm.editor.togglePreview).toBeDefined(); expect(vm.editor.togglePreview).toBeDefined();
} else { } else {
expect(vm.editor.projectPath).toBeUndefined(); expect(vm.editor.previewMarkdownPath).toBeUndefined();
expect(vm.editor.togglePreview).toBeUndefined(); expect(vm.editor.togglePreview).toBeUndefined();
} }
}, },
......
...@@ -18,7 +18,8 @@ RSpec.describe IdeHelper do ...@@ -18,7 +18,8 @@ RSpec.describe IdeHelper do
'file-path' => nil, 'file-path' => nil,
'merge-request' => nil, 'merge-request' => nil,
'fork-info' => nil, 'fork-info' => nil,
'project' => nil 'project' => nil,
'preview-markdown-path' => nil
) )
end end
end end
...@@ -41,7 +42,8 @@ RSpec.describe IdeHelper do ...@@ -41,7 +42,8 @@ RSpec.describe IdeHelper do
'file-path' => 'foo/bar', 'file-path' => 'foo/bar',
'merge-request' => '1', 'merge-request' => '1',
'fork-info' => fork_info.to_json, 'fork-info' => fork_info.to_json,
'project' => serialized_project 'project' => serialized_project,
'preview-markdown-path' => Gitlab::Routing.url_helpers.preview_markdown_project_path(project)
) )
end end
end end
......
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