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: ...@@ -59,6 +59,8 @@ linters:
# Reports when you define the same property twice in a single rule set. # Reports when you define the same property twice in a single rule set.
DuplicateProperty: DuplicateProperty:
enabled: true enabled: true
ignore_consecutive:
- cursor
# Separate rule, function, and mixin declarations with empty lines. # Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks: EmptyLineBetweenBlocks:
......
...@@ -36,6 +36,7 @@ export default { ...@@ -36,6 +36,7 @@ export default {
> >
<a <a
v-tooltip v-tooltip
v-if="!file.binary"
:href="file.blamePath" :href="file.blamePath"
:title="__('Blame')" :title="__('Blame')"
class="btn btn-xs btn-transparent blame" class="btn btn-xs btn-transparent blame"
......
<script> <script>
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago'; import timeAgoMixin from '~/vue_shared/mixins/timeago';
export default { export default {
components: { components: {
icon, icon,
},
directives: {
tooltip,
},
mixins: [timeAgoMixin],
props: {
file: {
type: Object,
required: true,
}, },
directives: { },
tooltip, };
},
mixins: [
timeAgoMixin,
],
props: {
file: {
type: Object,
required: true,
},
},
};
</script> </script>
<template> <template>
...@@ -50,7 +48,9 @@ ...@@ -50,7 +48,9 @@
<div class="text-right"> <div class="text-right">
{{ file.eol }} {{ file.eol }}
</div> </div>
<div class="text-right"> <div
class="text-right"
v-if="!file.binary">
{{ file.editorRow }}:{{ file.editorColumn }} {{ file.editorRow }}:{{ file.editorColumn }}
</div> </div>
<div class="text-right"> <div class="text-right">
......
...@@ -171,10 +171,10 @@ export default { ...@@ -171,10 +171,10 @@ export default {
id="ide" id="ide"
class="blob-viewer-container blob-editor-container" class="blob-viewer-container blob-editor-container"
> >
<div <div class="ide-mode-tabs clearfix">
class="ide-mode-tabs clearfix" <ul
v-if="!shouldHideEditor"> class="nav-links pull-left"
<ul class="nav-links pull-left"> v-if="!shouldHideEditor">
<li :class="editTabCSS"> <li :class="editTabCSS">
<a <a
href="javascript:void(0);" href="javascript:void(0);"
...@@ -210,9 +210,10 @@ export default { ...@@ -210,9 +210,10 @@ export default {
> >
</div> </div>
<content-viewer <content-viewer
v-if="!shouldHideEditor && file.viewMode === 'preview'" v-if="shouldHideEditor || file.viewMode === 'preview'"
:content="file.content || file.raw" :content="file.content || file.raw"
:path="file.path" :path="file.rawPath"
:file-size="file.size"
:project-path="file.projectId"/> :project-path="file.projectId"/>
</div> </div>
</template> </template>
...@@ -43,6 +43,7 @@ export default { ...@@ -43,6 +43,7 @@ export default {
raw: null, raw: null,
baseRaw: null, baseRaw: null,
html: data.html, html: data.html,
size: data.size,
}); });
}, },
[types.SET_FILE_RAW_DATA](state, { file, raw }) { [types.SET_FILE_RAW_DATA](state, { file, raw }) {
......
...@@ -40,6 +40,7 @@ export const dataStructure = () => ({ ...@@ -40,6 +40,7 @@ export const dataStructure = () => ({
eol: '', eol: '',
viewMode: 'edit', viewMode: 'edit',
previewMode: null, previewMode: null,
size: 0,
}); });
export const decorateData = entity => { export const decorateData = entity => {
......
<script> <script>
import { viewerInformationForPath } from './lib/viewer_utils'; import { viewerInformationForPath } from './lib/viewer_utils';
import MarkdownViewer from './viewers/markdown_viewer.vue'; import MarkdownViewer from './viewers/markdown_viewer.vue';
import ImageViewer from './viewers/image_viewer.vue';
import DownloadViewer from './viewers/download_viewer.vue';
export default { export default {
props: { props: {
content: { content: {
type: String, type: String,
required: true, default: '',
}, },
path: { path: {
type: String, type: String,
required: true, required: true,
}, },
fileSize: {
type: Number,
required: false,
default: 0,
},
projectPath: { projectPath: {
type: String, type: String,
required: false, required: false,
...@@ -20,12 +27,18 @@ export default { ...@@ -20,12 +27,18 @@ export default {
}, },
computed: { computed: {
viewer() { viewer() {
if (!this.path) return null;
const previewInfo = viewerInformationForPath(this.path); const previewInfo = viewerInformationForPath(this.path);
if (!previewInfo) return DownloadViewer;
switch (previewInfo.id) { switch (previewInfo.id) {
case 'markdown': case 'markdown':
return MarkdownViewer; return MarkdownViewer;
case 'image':
return ImageViewer;
default: default:
return null; return DownloadViewer;
} }
}, },
}, },
...@@ -36,6 +49,8 @@ export default { ...@@ -36,6 +49,8 @@ export default {
<div class="preview-container"> <div class="preview-container">
<component <component
:is="viewer" :is="viewer"
:path="path"
:file-size="fileSize"
:project-path="projectPath" :project-path="projectPath"
:content="content" :content="content"
/> />
......
const viewers = { const viewers = {
image: {
id: 'image',
},
markdown: { markdown: {
id: 'markdown', id: 'markdown',
previewTitle: 'Preview Markdown', previewTitle: 'Preview Markdown',
...@@ -7,6 +10,12 @@ const viewers = { ...@@ -7,6 +10,12 @@ const viewers = {
const fileNameViewers = {}; const fileNameViewers = {};
const fileExtensionViewers = { const fileExtensionViewers = {
jpg: 'image',
jpeg: 'image',
gif: 'image',
png: 'image',
bmp: 'image',
ico: 'image',
md: 'markdown', md: 'markdown',
markdown: '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 @@ ...@@ -312,6 +312,45 @@
height: 100%; height: 100%;
overflow: auto; 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 { .md-previewer {
padding: $gl-padding; padding: $gl-padding;
} }
......
...@@ -38,4 +38,33 @@ describe('ContentViewer', () => { ...@@ -38,4 +38,33 @@ describe('ContentViewer', () => {
done(); 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