diff options
author | Tim Zallmann <tzallmann@gitlab.com> | 2018-04-05 11:12:40 +0000 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2018-04-05 11:12:40 +0000 |
commit | c88cc0c0ec9872b2d4830d88faff7a4588ca4f9f (patch) | |
tree | 25a8f4aaebe1628e1c6ee51562862125cb6e5a9c /app/assets | |
parent | 21488c74223524aee9ee6e1fb5274a2d8dec7cb2 (diff) | |
download | gitlab-ce-c88cc0c0ec9872b2d4830d88faff7a4588ca4f9f.tar.gz |
Web IDE markdown preview
Diffstat (limited to 'app/assets')
13 files changed, 339 insertions, 93 deletions
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index d22869466c9..1c237c0ec97 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -3,7 +3,6 @@ import { mapState, mapGetters } from 'vuex'; import ideSidebar from './ide_side_bar.vue'; import ideContextbar from './ide_context_bar.vue'; import repoTabs from './repo_tabs.vue'; -import repoFileButtons from './repo_file_buttons.vue'; import ideStatusBar from './ide_status_bar.vue'; import repoEditor from './repo_editor.vue'; @@ -12,7 +11,6 @@ export default { ideSidebar, ideContextbar, repoTabs, - repoFileButtons, ideStatusBar, repoEditor, }, @@ -70,9 +68,6 @@ export default { class="multi-file-edit-pane-content" :file="activeFile" /> - <repo-file-buttons - :file="activeFile" - /> <ide-status-bar :file="activeFile" /> diff --git a/app/assets/javascripts/ide/components/ide_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue new file mode 100644 index 00000000000..6d07329df71 --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_file_buttons.vue @@ -0,0 +1,83 @@ +<script> +import { __ } from '~/locale'; +import tooltip from '~/vue_shared/directives/tooltip'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + file: { + type: Object, + required: true, + }, + }, + computed: { + showButtons() { + return ( + this.file.rawPath || this.file.blamePath || this.file.commitsPath || this.file.permalink + ); + }, + rawDownloadButtonLabel() { + return this.file.binary ? __('Download') : __('Raw'); + }, + }, +}; +</script> + +<template> + <div + v-if="showButtons" + class="pull-right ide-btn-group" + > + <a + v-tooltip + :href="file.blamePath" + :title="__('Blame')" + class="btn btn-xs btn-transparent blame" + > + <icon + name="blame" + :size="16" + /> + </a> + <a + v-tooltip + :href="file.commitsPath" + :title="__('History')" + class="btn btn-xs btn-transparent history" + > + <icon + name="history" + :size="16" + /> + </a> + <a + v-tooltip + :href="file.permalink" + :title="__('Permalink')" + class="btn btn-xs btn-transparent permalink" + > + <icon + name="link" + :size="16" + /> + </a> + <a + v-tooltip + :href="file.rawPath" + target="_blank" + class="btn btn-xs btn-transparent prepend-left-10 raw" + rel="noopener noreferrer" + :title="rawDownloadButtonLabel"> + <icon + name="download" + :size="16" + /> + </a> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index b1a16350c19..99423362924 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -2,10 +2,16 @@ /* global monaco */ import { mapState, mapGetters, mapActions } from 'vuex'; import flash from '~/flash'; +import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; +import IdeFileButtons from './ide_file_buttons.vue'; export default { + components: { + ContentViewer, + IdeFileButtons, + }, props: { file: { type: Object, @@ -18,6 +24,16 @@ export default { shouldHideEditor() { return this.file && this.file.binary && !this.file.raw; }, + editTabCSS() { + return { + active: this.file.viewMode === 'edit', + }; + }, + previewTabCSS() { + return { + active: this.file.viewMode === 'preview', + }; + }, }, watch: { file(oldVal, newVal) { @@ -56,6 +72,7 @@ export default { 'changeFileContent', 'setFileLanguage', 'setEditorPosition', + 'setFileViewMode', 'setFileEOL', 'updateViewer', 'updateDelayViewerUpdated', @@ -153,15 +170,47 @@ export default { class="blob-viewer-container blob-editor-container" > <div - v-if="shouldHideEditor" - v-html="file.html" - > + class="ide-mode-tabs clearfix" + v-if="!shouldHideEditor"> + <ul class="nav-links pull-left"> + <li :class="editTabCSS"> + <a + href="javascript:void(0);" + role="button" + @click.prevent="setFileViewMode({ file, viewMode: 'edit' })"> + <template v-if="viewer === 'editor'"> + {{ __('Edit') }} + </template> + <template v-else> + {{ __('Review') }} + </template> + </a> + </li> + <li + v-if="file.previewMode" + :class="previewTabCSS"> + <a + href="javascript:void(0);" + role="button" + @click.prevent="setFileViewMode({ file, viewMode:'preview' })"> + {{ file.previewMode.previewTitle }} + </a> + </li> + </ul> + <ide-file-buttons + :file="file" + /> </div> <div - v-show="!shouldHideEditor" + v-show="!shouldHideEditor && file.viewMode === 'edit'" ref="editor" class="multi-file-editor-holder" > </div> + <content-viewer + v-if="!shouldHideEditor && file.viewMode === 'preview'" + :content="file.content || file.raw" + :path="file.path" + :project-path="file.projectId"/> </div> </template> diff --git a/app/assets/javascripts/ide/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue deleted file mode 100644 index 4ea8cf7504b..00000000000 --- a/app/assets/javascripts/ide/components/repo_file_buttons.vue +++ /dev/null @@ -1,61 +0,0 @@ -<script> -export default { - props: { - file: { - type: Object, - required: true, - }, - }, - computed: { - showButtons() { - return this.file.rawPath || - this.file.blamePath || - this.file.commitsPath || - this.file.permalink; - }, - rawDownloadButtonLabel() { - return this.file.binary ? 'Download' : 'Raw'; - }, - }, -}; -</script> - -<template> - <div - v-if="showButtons" - class="multi-file-editor-btn-group" - > - <a - :href="file.rawPath" - target="_blank" - class="btn btn-default btn-sm raw" - rel="noopener noreferrer"> - {{ rawDownloadButtonLabel }} - </a> - - <div - class="btn-group" - role="group" - aria-label="File actions" - > - <a - :href="file.blamePath" - class="btn btn-default btn-sm blame" - > - Blame - </a> - <a - :href="file.commitsPath" - class="btn btn-default btn-sm history" - > - History - </a> - <a - :href="file.permalink" - class="btn btn-default btn-sm permalink" - > - Permalink - </a> - </div> - </div> -</template> diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 6b034ea1e82..1a17320a1ea 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -149,6 +149,10 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn } }; +export const setFileViewMode = ({ state, commit }, { file, viewMode }) => { + commit(types.SET_FILE_VIEWMODE, { file, viewMode }); +}; + export const discardFileChanges = ({ state, commit }, path) => { const file = state.entries[path]; diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index ee759bff516..e3f504e5ab0 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -38,6 +38,7 @@ export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA'; export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT'; export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE'; export const SET_FILE_POSITION = 'SET_FILE_POSITION'; +export const SET_FILE_VIEWMODE = 'SET_FILE_VIEWMODE'; export const SET_FILE_EOL = 'SET_FILE_EOL'; export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES'; export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED'; diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 926b6f66d78..6a143e518f9 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -42,6 +42,7 @@ export default { renderError: data.render_error, raw: null, baseRaw: null, + html: data.html, }); }, [types.SET_FILE_RAW_DATA](state, { file, raw }) { @@ -83,6 +84,11 @@ export default { mrChange, }); }, + [types.SET_FILE_VIEWMODE](state, { file, viewMode }) { + Object.assign(state.entries[file.path], { + viewMode, + }); + }, [types.DISCARD_FILE_CHANGES](state, path) { Object.assign(state.entries[path], { content: state.entries[path].raw, diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 63e4de3b17d..4befcc501ef 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -38,6 +38,8 @@ export const dataStructure = () => ({ editorColumn: 1, fileLanguage: '', eol: '', + viewMode: 'edit', + previewMode: null, }); export const decorateData = entity => { @@ -57,8 +59,9 @@ export const decorateData = entity => { changed = false, parentTreeUrl = '', base64 = false, - + previewMode, file_lock, + html, } = entity; return { @@ -79,8 +82,9 @@ export const decorateData = entity => { renderError, content, base64, - + previewMode, file_lock, + html, }; }; diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js index a4cd1ab099f..a1673276900 100644 --- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js +++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js @@ -1,14 +1,8 @@ +import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils'; import { decorateData, sortTree } from '../utils'; self.addEventListener('message', e => { - const { - data, - projectId, - branchId, - tempFile = false, - content = '', - base64 = false, - } = e.data; + const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data; const treeList = []; let file; @@ -19,9 +13,7 @@ self.addEventListener('message', e => { if (pathSplit.length > 0) { pathSplit.reduce((pathAcc, folderName) => { const parentFolder = acc[pathAcc[pathAcc.length - 1]]; - const folderPath = `${ - parentFolder ? `${parentFolder.path}/` : '' - }${folderName}`; + const folderPath = `${parentFolder ? `${parentFolder.path}/` : ''}${folderName}`; const foundEntry = acc[folderPath]; if (!foundEntry) { @@ -33,9 +25,7 @@ self.addEventListener('message', e => { path: folderPath, url: `/${projectId}/tree/${branchId}/${folderPath}/`, type: 'tree', - parentTreeUrl: parentFolder - ? parentFolder.url - : `/${projectId}/tree/${branchId}/`, + parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`, tempFile, changed: tempFile, opened: tempFile, @@ -70,13 +60,12 @@ self.addEventListener('message', e => { path, url: `/${projectId}/blob/${branchId}/${path}`, type: 'blob', - parentTreeUrl: fileFolder - ? fileFolder.url - : `/${projectId}/blob/${branchId}`, + parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`, tempFile, changed: tempFile, content, base64, + previewMode: viewerInformationForPath(blobName), }); Object.assign(acc, { diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue new file mode 100644 index 00000000000..fb8ccea91c7 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue @@ -0,0 +1,43 @@ +<script> +import { viewerInformationForPath } from './lib/viewer_utils'; +import MarkdownViewer from './viewers/markdown_viewer.vue'; + +export default { + props: { + content: { + type: String, + required: true, + }, + path: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + computed: { + viewer() { + const previewInfo = viewerInformationForPath(this.path); + switch (previewInfo.id) { + case 'markdown': + return MarkdownViewer; + default: + return null; + } + }, + }, +}; +</script> + +<template> + <div class="preview-container"> + <component + :is="viewer" + :project-path="projectPath" + :content="content" + /> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js new file mode 100644 index 00000000000..4f2e1e47dd1 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js @@ -0,0 +1,23 @@ +const viewers = { + markdown: { + id: 'markdown', + previewTitle: 'Preview Markdown', + }, +}; + +const fileNameViewers = {}; +const fileExtensionViewers = { + md: 'markdown', + markdown: 'markdown', +}; + +export function viewerInformationForPath(path) { + if (!path) return null; + const name = path.split('/').pop(); + const viewerName = + fileNameViewers[name] || fileExtensionViewers[name ? name.split('.').pop() : ''] || ''; + + return viewers[viewerName]; +} + +export default viewers; diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue new file mode 100644 index 00000000000..09e0094054d --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue @@ -0,0 +1,90 @@ +<script> +import axios from '~/lib/utils/axios_utils'; +import { __ } from '~/locale'; +import $ from 'jquery'; +import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; + +const CancelToken = axios.CancelToken; +let axiosSource; + +export default { + components: { + SkeletonLoadingContainer, + }, + props: { + content: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + }, + data() { + return { + previewContent: null, + isLoading: false, + }; + }, + watch: { + content() { + this.previewContent = null; + }, + }, + created() { + axiosSource = CancelToken.source(); + this.fetchMarkdownPreview(); + }, + updated() { + this.fetchMarkdownPreview(); + }, + destroyed() { + if (this.isLoading) axiosSource.cancel('Cancelling Preview'); + }, + methods: { + fetchMarkdownPreview() { + if (this.content && this.previewContent === null) { + this.isLoading = true; + const postBody = { + text: this.content, + }; + const postOptions = { + cancelToken: axiosSource.token, + }; + + axios + .post( + `${gon.relative_url_root}/${this.projectPath}/preview_markdown`, + postBody, + postOptions, + ) + .then(({ data }) => { + this.previewContent = data.body; + this.isLoading = false; + + this.$nextTick(() => { + $(this.$refs['markdown-preview']).renderGFM(); + }); + }) + .catch(() => { + this.previewContent = __('An error occurred while fetching markdown preview'); + this.isLoading = false; + }); + } + }, + }, +}; +</script> + +<template> + <div + ref="markdown-preview" + class="md md-previewer"> + <skeleton-loading-container v-if="isLoading" /> + <div + v-else + v-html="previewContent"> + </div> + </div> +</template> diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 1f6f7138e1f..8cc5c8fc877 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -308,14 +308,34 @@ height: 100%; } -.multi-file-editor-btn-group { - padding: $gl-bar-padding $gl-padding; - border-top: 1px solid $white-dark; +.preview-container { + height: 100%; + overflow: auto; + + .md-previewer { + padding: $gl-padding; + } +} + +.ide-mode-tabs { border-bottom: 1px solid $white-dark; - background: $white-light; + + .nav-links { + border-bottom: 0; + + li a { + padding: $gl-padding-8 $gl-padding; + line-height: $gl-btn-line-height; + } + } +} + +.ide-btn-group { + padding: $gl-padding-4 $gl-vert-padding; } .ide-status-bar { + border-top: 1px solid $white-dark; padding: $gl-bar-padding $gl-padding; background: $white-light; display: flex; |