diff options
author | Tim Zallmann <tzallmann@gitlab.com> | 2018-04-06 20:49:06 +0300 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2018-04-06 20:49:06 +0300 |
commit | b2daa846a13d3f637c4065cb5028eac2f4b2aca0 (patch) | |
tree | 943e86012ec269f95d8589453f83b3afeb1067df | |
parent | 08e5876403edd65b08712d3167c053daf4fdccb9 (diff) |
Web IDE blob image + default fallback
12 files changed, 246 insertions, 28 deletions
diff --git a/.scss-lint.yml b/.scss-lint.yml index dcd4cac780a..180d377d6f8 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -59,6 +59,8 @@ linters: # Reports when you define the same property twice in a single rule set. DuplicateProperty: enabled: true + ignore_consecutive: + - cursor # Separate rule, function, and mixin declarations with empty lines. EmptyLineBetweenBlocks: diff --git a/app/assets/javascripts/ide/components/ide_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue index 6d07329df71..a6c6f46a144 100644 --- a/app/assets/javascripts/ide/components/ide_file_buttons.vue +++ b/app/assets/javascripts/ide/components/ide_file_buttons.vue @@ -36,6 +36,7 @@ export default { > <a v-tooltip + v-if="!file.binary" :href="file.blamePath" :title="__('Blame')" class="btn btn-xs btn-transparent blame" diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index 9c386896448..152a5f632ad 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -1,25 +1,23 @@ <script> - import icon from '~/vue_shared/components/icon.vue'; - import tooltip from '~/vue_shared/directives/tooltip'; - import timeAgoMixin from '~/vue_shared/mixins/timeago'; +import icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import timeAgoMixin from '~/vue_shared/mixins/timeago'; - export default { - components: { - icon, +export default { + components: { + icon, + }, + directives: { + tooltip, + }, + mixins: [timeAgoMixin], + props: { + file: { + type: Object, + required: true, }, - directives: { - tooltip, - }, - mixins: [ - timeAgoMixin, - ], - props: { - file: { - type: Object, - required: true, - }, - }, - }; + }, +}; </script> <template> @@ -50,7 +48,9 @@ <div class="text-right"> {{ file.eol }} </div> - <div class="text-right"> + <div + class="text-right" + v-if="!file.binary"> {{ file.editorRow }}:{{ file.editorColumn }} </div> <div class="text-right"> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 8a709b31ea0..6aa44ca2c11 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -171,10 +171,10 @@ export default { id="ide" class="blob-viewer-container blob-editor-container" > - <div - class="ide-mode-tabs clearfix" - v-if="!shouldHideEditor"> - <ul class="nav-links pull-left"> + <div class="ide-mode-tabs clearfix"> + <ul + class="nav-links pull-left" + v-if="!shouldHideEditor"> <li :class="editTabCSS"> <a href="javascript:void(0);" @@ -210,9 +210,10 @@ export default { > </div> <content-viewer - v-if="!shouldHideEditor && file.viewMode === 'preview'" + v-if="shouldHideEditor || file.viewMode === 'preview'" :content="file.content || file.raw" - :path="file.path" + :path="file.rawPath" + :file-size="file.size" :project-path="file.projectId"/> </div> </template> diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 6a143e518f9..eeb14b5490c 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -43,6 +43,7 @@ export default { raw: null, baseRaw: null, html: data.html, + size: data.size, }); }, [types.SET_FILE_RAW_DATA](state, { file, raw }) { diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 4befcc501ef..05a019de54f 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -40,6 +40,7 @@ export const dataStructure = () => ({ eol: '', viewMode: 'edit', previewMode: null, + size: 0, }); export const decorateData = entity => { 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 index fb8ccea91c7..4155e1bab9c 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue @@ -1,17 +1,24 @@ <script> import { viewerInformationForPath } from './lib/viewer_utils'; import MarkdownViewer from './viewers/markdown_viewer.vue'; +import ImageViewer from './viewers/image_viewer.vue'; +import DownloadViewer from './viewers/download_viewer.vue'; export default { props: { content: { type: String, - required: true, + default: '', }, path: { type: String, required: true, }, + fileSize: { + type: Number, + required: false, + default: 0, + }, projectPath: { type: String, required: false, @@ -20,12 +27,18 @@ export default { }, computed: { viewer() { + if (!this.path) return null; + const previewInfo = viewerInformationForPath(this.path); + if (!previewInfo) return DownloadViewer; + switch (previewInfo.id) { case 'markdown': return MarkdownViewer; + case 'image': + return ImageViewer; default: - return null; + return DownloadViewer; } }, }, @@ -36,6 +49,8 @@ export default { <div class="preview-container"> <component :is="viewer" + :path="path" + :file-size="fileSize" :project-path="projectPath" :content="content" /> 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 index 4f2e1e47dd1..f01a51da0b3 100644 --- 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 @@ -1,4 +1,7 @@ const viewers = { + image: { + id: 'image', + }, markdown: { id: 'markdown', previewTitle: 'Preview Markdown', @@ -7,6 +10,12 @@ const viewers = { const fileNameViewers = {}; const fileExtensionViewers = { + jpg: 'image', + jpeg: 'image', + gif: 'image', + png: 'image', + bmp: 'image', + ico: 'image', md: 'markdown', markdown: 'markdown', }; diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue new file mode 100644 index 00000000000..395a71acccf --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue @@ -0,0 +1,52 @@ +<script> +import Icon from '../../icon.vue'; +import { numberToHumanSize } from '../../../../lib/utils/number_utils'; + +export default { + components: { + Icon, + }, + props: { + path: { + type: String, + required: true, + }, + fileSize: { + type: Number, + required: false, + default: 0, + }, + }, + computed: { + fileSizeReadable() { + return numberToHumanSize(this.fileSize); + }, + fileName() { + return this.path.split('/').pop(); + }, + }, +}; +</script> + +<template> + <div class="file-container"> + <div class="file-content"> + <p class="prepend-top-10 file-info"> + {{ fileName }} ({{ fileSizeReadable }}) + </p> + <a + :href="path" + class="btn btn-default" + rel="nofollow" + download + target="_blank"> + <icon + name="download" + css-classes="pull-left append-right-8" + :size="16" + /> + {{ __('Download') }} + </a> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue new file mode 100644 index 00000000000..a5999f909ca --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue @@ -0,0 +1,68 @@ +<script> +import { numberToHumanSize } from '../../../../lib/utils/number_utils'; + +export default { + props: { + path: { + type: String, + required: true, + }, + fileSize: { + type: Number, + required: false, + default: 0, + }, + }, + data() { + return { + width: 0, + height: 0, + isZoomable: false, + isZoomed: false, + }; + }, + computed: { + fileSizeReadable() { + return numberToHumanSize(this.fileSize); + }, + }, + methods: { + onImgLoad() { + const contentImg = this.$refs.contentImg; + this.isZoomable = + contentImg.naturalWidth > contentImg.width || contentImg.naturalHeight > contentImg.height; + + this.width = contentImg.naturalWidth; + this.height = contentImg.naturalHeight; + }, + onImgClick() { + if (this.isZoomable) this.isZoomed = !this.isZoomed; + }, + }, +}; +</script> + +<template> + <div class="file-container"> + <div class="file-content image_file"> + <img + ref="contentImg" + :class="{ 'isZoomable': isZoomable, 'isZoomed': isZoomed }" + :src="path" + :alt="path" + @load="onImgLoad" + @click="onImgClick"/> + <p class="file-info prepend-top-10"> + <template v-if="fileSize>0"> + {{ fileSizeReadable }} + </template> + <template v-if="fileSize>0 && width && height"> + - + </template> + <template v-if="width && height"> + {{ width }} x {{ height }} + </template> + </p> + </div> + </div> +</template> diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 8cc5c8fc877..a414deb8921 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -312,6 +312,45 @@ height: 100%; overflow: auto; + .file-container { + background-color: $gray-darker; + display: flex; + height: 100%; + align-items: center; + justify-content: center; + + text-align: center; + + .file-content { + padding: $gl-padding; + max-width: 100%; + max-height: 100%; + + img { + max-width: 90%; + max-height: 90%; + } + + .isZoomable { + cursor: pointer; + cursor: zoom-in; + + &.isZoomed { + cursor: pointer; + cursor: zoom-out; + max-width: none; + max-height: none; + margin-right: $gl-padding; + } + } + } + + .file-info { + font-size: $label-font-size; + color: $diff-image-info-color; + } + } + .md-previewer { padding: $gl-padding; } diff --git a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js index c7c454a0b45..383f0cd29ea 100644 --- a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js +++ b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js @@ -38,4 +38,33 @@ describe('ContentViewer', () => { done(); }); }); + + it('renders image preview', done => { + createComponent({ + path: 'test.jpg', + fileSize: 1024, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.image_file img').getAttribute('src')).toBe('test.jpg'); + + done(); + }); + }); + + it('renders fallback download control', done => { + createComponent({ + path: 'test.abc', + fileSize: 1024, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.file-info').textContent.trim()).toContain( + 'test.abc (1.00 KiB)', + ); + expect(vm.$el.querySelector('.btn.btn-default').textContent.trim()).toContain('Download'); + + done(); + }); + }); }); |