Commit 9e3cdc02 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'tz-ide-blob-image' into 'master'

Web IDE blob image + default fallback

See merge request gitlab-org/gitlab-ce!18186
parents 08e58764 b2daa846
......@@ -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:
......
......@@ -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"
......
<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">
......
......@@ -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>
......@@ -43,6 +43,7 @@ export default {
raw: null,
baseRaw: null,
html: data.html,
size: data.size,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
......
......@@ -40,6 +40,7 @@ export const dataStructure = () => ({
eol: '',
viewMode: 'edit',
previewMode: null,
size: 0,
});
export const decorateData = entity => {
......
<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"
/>
......
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',
};
......
<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>
<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>
......@@ -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;
}
......
......@@ -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();
});
});
});
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