Skip to content

Commit

Permalink
Added file preview in file list
Browse files Browse the repository at this point in the history
Added new component FileItem wrapped around oc-file to
better encapsulated the formatting methods as computed properties
instead of using the mixin.
Added preview support in file list loaded using mediaSource.
  • Loading branch information
Vincent Petry committed Mar 19, 2020
1 parent 3439e1c commit e530fbd
Show file tree
Hide file tree
Showing 11 changed files with 247 additions and 17 deletions.
40 changes: 29 additions & 11 deletions apps/files/src/components/AllFilesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,13 @@
<oc-star class="uk-display-block" @click.native.stop="toggleFileFavorite(item)" :shining="item.starred" />
</div>
<div class="uk-text-truncate uk-width-expand" :ref="index === 0 ? 'firstRowNameColumn' : null">
<oc-file @click.native.stop="item.type === 'folder' ? navigateTo(item.path.substr(1)) : openFileActionBar(item)"
:name="$_ocFileName(item)" :extension="item.extension" class="file-row-name" :icon="fileTypeIcon(item)"
:filename="item.name" :key="item.id"/>
<file-item
@click.native.stop="item.type === 'folder' ? navigateTo(item.path.substr(1)) : openFileActionBar(item)"
:item="item"
:dav-url="davUrl"
:show-path="$_isFavoritesList"
class="file-row-name"
:key="item.id"/>
<oc-spinner
v-if="actionInProgress(item)"
size="small"
Expand Down Expand Up @@ -111,6 +115,7 @@
</template>
<script>
import FileList from './FileList.vue'
import FileItem from './FileItem.vue'
import NoContentMessage from './NoContentMessage.vue'
import { mapGetters, mapActions, mapState } from 'vuex'
Expand All @@ -124,6 +129,7 @@ export default {
name: 'AllFilesList',
components: {
FileList,
FileItem,
StatusIndicators,
NoContentMessage,
SortableColumnHeader
Expand Down Expand Up @@ -156,6 +162,26 @@ export default {
return this.$route.params.item
},
davUrl () {
let davUrl
// FIXME: use SDK once it switches to DAV v2
if (this.publicPage()) {
davUrl = [
'..',
'dav',
'public-files'
].join('/')
} else {
davUrl = [
'..',
'dav',
'files',
this.$store.getters.user.id
].join('/')
}
return this.$client.files.getFileUrl(davUrl)
},
$_isFavoritesList () {
return (this.$route.name === 'files-favorites')
},
Expand Down Expand Up @@ -237,14 +263,6 @@ export default {
file: item
})
},
$_ocFileName (item) {
if (this.$_isFavoritesList) {
const pathSplit = item.path.substr(1).split('/')
if (pathSplit.length === 2) return `${pathSplit[pathSplit.length - 2]}/${item.basename}`
if (pathSplit.length > 2) return `…/${pathSplit[pathSplit.length - 2]}/${item.basename}`
}
return item.basename
},
isActionEnabled (item, action) {
return action.isEnabled(item, this.parentFolder)
Expand Down
23 changes: 20 additions & 3 deletions apps/files/src/components/Collaborators/SharedFilesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@
</template>
<template #rowColumns="{ item }">
<div class="uk-text-truncate uk-width-expand">
<oc-file @click.native.stop="item.type === 'folder' ? navigateTo(item.path.substr(1)) : openFileActionBar(item)"
:name="item.basename" :extension="item.extension" class="file-row-name" :icon="fileTypeIcon(item)"
:filename="item.name" :key="item.path" />
<file-item
@click.native.stop="item.type === 'folder' ? navigateTo(item.path.substr(1)) : openFileActionBar(item)"
:item="item"
:dav-url="davUrl"
class="file-row-name"
:key="item.path" />
<oc-spinner
v-if="actionInProgress(item)"
size="small"
Expand Down Expand Up @@ -110,6 +113,7 @@ import { mapGetters, mapActions } from 'vuex'
import Mixins from '../../mixins'
import FileActions from '../../fileactions'
import FileList from '../FileList.vue'
import FileItem from '../FileItem.vue'
import NoContentMessage from '../NoContentMessage.vue'
import SortableColumnHeader from '../FilesLists/SortableColumnHeader.vue'
import { shareTypes } from '../../helpers/shareTypes'
Expand All @@ -119,6 +123,7 @@ export default {
name: 'SharedFilesList',
components: {
FileList,
FileItem,
NoContentMessage,
SortableColumnHeader
},
Expand All @@ -145,7 +150,19 @@ export default {
$_isSharedWithMe () {
return (this.$route.name === 'files-shared-with-me')
},
davUrl () {
// FIXME: use SDK once it switches to DAV v2
const davUrl = [
'..',
'dav',
'files',
this.$store.getters.user.id
].join('/')
return this.$client.files.getFileUrl(davUrl)
}
},
watch: {
$route () {
Expand Down
88 changes: 88 additions & 0 deletions apps/files/src/components/FileItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<template>
<oc-file
:name="fileName"
:extension="item.extension"
:icon="fileTypeIcon"
:iconUrl="previewUrl"
:filename="item.name"
/>
</template>
<script>
import queryString from 'query-string'
import fileTypeIconMappings from '../fileTypeIconMappings.json'
export default {
name: 'FileItem',
props: {
/**
* The html element used for the avatar container.
* `div, span`
*/
item: {
type: Object
},
davUrl: {
type: String
},
showPath: {
type: Boolean,
default: false
}
},
data: function () {
return {
previewUrl: this.item.previewUrl
}
},
computed: {
fileName () {
if (this.showPath) {
const pathSplit = this.item.path.substr(1).split('/')
if (pathSplit.length === 2) return `${pathSplit[pathSplit.length - 2]}/${this.item.basename}`
if (pathSplit.length > 2) return `…/${pathSplit[pathSplit.length - 2]}/${this.item.basename}`
}
return this.item.basename
},
// FIXME: duplicate in mixin
fileTypeIcon () {
if (this.item.type === 'folder') {
return 'folder'
}
const icon = fileTypeIconMappings[this.item.extension]
if (icon) return `${icon}`
return 'x-office-document'
}
},
mounted () {
this.loadPreview()
},
methods: {
loadPreview () {
if (this.item.previewUrl) {
this.previewUrl = this.item.previewUrl
return
}
// TODO: check if previews are globally enabled (requires capability entry)
// don't load previews for pending or rejected shares (status)
if (!this.davUrl || this.item.type === 'folder' || (typeof this.item.status !== 'undefined' && this.item.status !== 0)) {
return
}
const query = queryString.stringify({
x: this.thumbDimensions,
y: this.thumbDimensions,
c: this.item.etag,
scalingup: 0,
preview: 1,
a: 1
})
const previewUrl = this.davUrl + '/' + this.item.path + '?' + query
this.mediaSource(previewUrl, 'url', this.requestHeaders).then(dataUrl => {
// cache inside item
this.previewUrl = this.item.previewUrl = dataUrl
})
}
}
}
</script>
10 changes: 7 additions & 3 deletions apps/files/src/components/Trashbin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
</template>
<template #rowColumns="{ item }">
<div class="uk-text-truncate uk-width-expand">
<oc-file
:name="$_ocTrashbin_fileName(item)" :extension="item.extension" class="file-row-name" :icon="fileTypeIcon(item)"
:filename="item.name" :key="item.id"/>
<file-item
:item="item"
class="file-row-name"
:key="item.id"
/>
</div>
<div class="uk-text-meta uk-text-nowrap uk-width-small uk-text-right" :class="{ 'uk-visible@s' : !_sidebarOpen, 'uk-hidden' : _sidebarOpen }">
{{ formDateFromNow(item.deleteTimestamp) }}
Expand All @@ -58,6 +60,7 @@
import { mapGetters, mapActions } from 'vuex'
import Mixins from '../mixins'
import FileList from './FileList.vue'
import FileItem from './FileItem.vue'
import NoContentMessage from './NoContentMessage.vue'
import OcDialogPrompt from './ocDialogPrompt.vue'
import SortableColumnHeader from './FilesLists/SortableColumnHeader.vue'
Expand All @@ -69,6 +72,7 @@ export default {
components: {
OcDialogPrompt,
FileList,
FileItem,
NoContentMessage,
SortableColumnHeader
},
Expand Down
15 changes: 15 additions & 0 deletions apps/files/src/mixins.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ export default {

_sidebarOpen () {
return this.highlightedFile !== null
},

requestHeaders () {
if (!this.publicPage()) {
return null
}

const headers = new Headers()
headers.append('X-Requested-With', 'XMLHttpRequest')

const password = this.publicLinkPassword
if (password) {
headers.append('Authorization', 'Basic ' + Buffer.from('public:' + password).toString('base64'))
}
return headers
}
},
methods: {
Expand Down
6 changes: 6 additions & 0 deletions apps/files/src/store/mutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,12 @@ export default {

CLEAR_CURRENT_FILES_LIST (state) {
state.currentFolder = null
// release blob urls
state.files.forEach(item => {
if (item.previewUrl && item.previewUrl.startsWith('blob:')) {
window.URL.revokeObjectURL(item.previewUrl)
}
})
state.files = []
}
}
7 changes: 7 additions & 0 deletions changelog/unreleased/276
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Enhancement: Added thumbnails in file list

Thumbnails are now displayed in the file list for known file types.
When no thumbnail was returned, fall back to the file type icon.

https://github.com/owncloud/phoenix/issues/276
https://github.com/owncloud/phoenix/pull/3187
10 changes: 10 additions & 0 deletions tests/acceptance/features/webUIFiles/fileList.feature
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,13 @@ Feature: User can view files inside a folder
And the user opens folder "empty-thing" directly on the webUI
Then there should be no resources listed on the webUI

@issue-276
Scenario: Thumbnails are loaded for known file types
When the user uploads file "new-lorem.txt" using the webUI
Then the resource "new-lorem.txt" should have a thumbnail displayed on the webUI

@issue-276
Scenario: Thumbnails are not loaded for known file types
When the user uploads file "new-data.zip" using the webUI
Then the resource "new-data.zip" should have a file type icon displayed on the webUI

Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ Feature: Share by public link
| simple-folder | Public |
But file "data.zip" should not be listed on the webUI

@issue-276
Scenario: Thumbnails are loaded for known file types in public link file list
Given user "user1" has shared folder "simple-folder" with link with "read,create" permissions
When the public uses the webUI to access the last public link created by user "user1"
And the user uploads file "new-lorem.txt" using the webUI
Then the resource "new-lorem.txt" should have a thumbnail displayed on the webUI

@issue-276
Scenario: Thumbnails are not loaded for known file types in public link file list
Given user "user1" has shared folder "simple-folder" with link with "read,create" permissions
When the public uses the webUI to access the last public link created by user "user1"
And the user uploads file "new-data.zip" using the webUI
Then the resource "new-data.zip" should have a file type icon displayed on the webUI

Scenario: opening public-link page of the files-drop link protected with password should redirect to files-drop page
Given user "user1" has shared folder "simple-folder" with link with "create" permissions and password "pass123"
When the public tries to open the public link page of the last public link created by user "user1" with password "pass123"
Expand Down
37 changes: 37 additions & 0 deletions tests/acceptance/pageObjects/FilesPageElement/filesList.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,37 @@ module.exports = {
.useCss()
return this
},
getResourceThumbnail: async function (resourceName) {
const fileRowPreviewSelector = this.getFileRowSelectorByFileName(resourceName) + this.elements.filePreviewInFileRow.selector
const fileRowIconSelector = this.getFileRowSelectorByFileName(resourceName) + this.elements.fileIconInFileRow.selector
let iconUrl = null
await this.waitForFileVisible(resourceName)
// while the preview is loading, it will first display the file type icon,
// so we might misdetect if we don't wait long enough
await this.useXpath()
.getAttribute(
{
selector: fileRowPreviewSelector,
timeout: 5000,
suppressNotFoundErrors: true
},
'src',
(result) => {
// somehow when element was not found the result.value is an empty array...
if (result.status !== -1 && typeof result.value === 'string') {
iconUrl = result.value
}
}
)
.useCss()
if (!iconUrl) {
// check that at least the file type icon svg is displayed
await this.useXpath()
.waitForElementVisible(fileRowIconSelector)
.useCss()
}
return iconUrl
},
/**
* Wait for A filerow with given path to be visible
* This only works in the favorites page as it uses the whole path of a file rather than just the name
Expand Down Expand Up @@ -719,6 +750,12 @@ module.exports = {
fileLinkInFileRow: {
selector: '//span[contains(@class, "file-row-name")]'
},
fileIconInFileRow: {
selector: '//span[contains(@class, "file-row-name")]//*[local-name() = "svg"]'
},
filePreviewInFileRow: {
selector: '//span[contains(@class, "file-row-name")]//img'
},
notMarkedFavoriteInFileRow: {
selector: '//span[contains(@class, "oc-star-dimm")]',
locateStrategy: 'xpath'
Expand Down
14 changes: 14 additions & 0 deletions tests/acceptance/stepDefinitions/filesContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -926,3 +926,17 @@ When('the user has set the sort order of the {string} column to descending order
When('the user has set the sort order of the {string} column to ascending order', async function (column) {
await _setFilesTableSort(column, false)
})
Then('the file/folder/resource {string} should have a thumbnail displayed on the webUI', async function (resource) {
const iconUrl = await client
.page
.FilesPageElement.filesList()
.getResourceThumbnail(resource)
assert.ok(iconUrl && iconUrl.startsWith('blob:'), 'Icon URL expected to be set when thumbnail is displayed')
})
Then('the file/folder/resource {string} should have a file type icon displayed on the webUI', async function (resource) {
const iconUrl = await client
.page
.FilesPageElement.filesList()
.getResourceThumbnail(resource)
assert.strictEqual(null, iconUrl, 'No icon URL expected when file type icon is displayed')
})

0 comments on commit e530fbd

Please sign in to comment.