Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.scss-lint.yml2
-rw-r--r--app/assets/javascripts/ide/components/ide_file_buttons.vue1
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue40
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue13
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js1
-rw-r--r--app/assets/javascripts/ide/stores/utils.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue19
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue52
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue68
-rw-r--r--app/assets/stylesheets/pages/repo.scss39
-rw-r--r--spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js29
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();
+ });
+ });
});