Skip to content

Commit

Permalink
List virtualization and lazy thumbnails (#2161)
Browse files Browse the repository at this point in the history
  • Loading branch information
goto-bus-stop authored May 5, 2020
1 parent 216e481 commit 0cc2c36
Show file tree
Hide file tree
Showing 10 changed files with 417 additions and 76 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ PRs are welcome! Please do open an issue to discuss first if it's a big feature,
- [ ] uploaders: consider not showing progress updates from the server after an upload’s been paused. Perhaps the button can be disabled and say `Pausing..` until Companion has actually stopped transmitting updates (@arturi, @ifedapoolarewaju)
- [ ] website: add an example of a mini UI that features drop & progress (may involve a `mini: true` options for dashboard, may involve drop+progress) (@arturi)
- [ ] xhr: allow sending custom headers per file (as proposed in #785)
- [ ] dashboard: focus jumps weirdly if you remove a file https://github.com/transloadit/uppy/pull/2161#issuecomment-613565486

## 2.0

Expand Down
15 changes: 14 additions & 1 deletion packages/@uppy/dashboard/src/components/Dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ module.exports = function Dashboard (props) {
'uppy-Dashboard--isInnerWrapVisible': props.areInsidesReadyToBeVisible
})

// Important: keep these in sync with the percent width values in `src/components/FileItem/index.scss`.
let itemsPerRow = 1 // mobile
if (props.containerWidth > WIDTH_XL) {
itemsPerRow = 5
} else if (props.containerWidth > WIDTH_LG) {
itemsPerRow = 4
} else if (props.containerWidth > WIDTH_MD) {
itemsPerRow = 3
}

const showFileList = props.showSelectedFiles && !noFiles

return (
Expand Down Expand Up @@ -99,7 +109,10 @@ module.exports = function Dashboard (props) {
{showFileList && <PanelTopBar {...props} />}

{showFileList ? (
<FileList {...props} />
<FileList
{...props}
itemsPerRow={itemsPerRow}
/>
) : (
<AddFiles {...props} isSizeMD={isSizeMD} />
)}
Expand Down
91 changes: 64 additions & 27 deletions packages/@uppy/dashboard/src/components/FileItem/Buttons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,44 @@ const copyToClipboard = require('../../../utils/copyToClipboard')

const { iconPencil, iconCross, iconCopyLink } = require('../../icons')

const renderEditButton = (props) => (
!props.uploadInProgressOrComplete &&
props.metaFields &&
props.metaFields.length > 0 &&
<button
class="uppy-u-reset uppy-DashboardItem-action uppy-DashboardItem-action--edit"
type="button"
aria-label={props.i18n('editFile') + ' ' + props.file.meta.name}
title={props.i18n('editFile')}
onclick={(e) => props.toggleFileCard(props.file.id)}
>
{iconPencil()}
</button>
)
function EditButton ({
file,
uploadInProgressOrComplete,
metaFields,
i18n,
onClick
}) {
if (!uploadInProgressOrComplete &&
metaFields &&
metaFields.length > 0) {
return (
<button
class="uppy-u-reset uppy-DashboardItem-action uppy-DashboardItem-action--edit"
type="button"
aria-label={i18n('editFile') + ' ' + file.meta.name}
title={i18n('editFile')}
onclick={() => onClick()}
>
{iconPencil()}
</button>
)
}
return null
}

const renderRemoveButton = (props) => (
props.showRemoveButton &&
function RemoveButton ({ i18n, onClick }) {
return (
<button
class="uppy-u-reset uppy-DashboardItem-action uppy-DashboardItem-action--remove"
type="button"
aria-label={props.i18n('removeFile')}
title={props.i18n('removeFile')}
onclick={() => props.removeFile(props.file.id)}
aria-label={i18n('removeFile')}
title={i18n('removeFile')}
onclick={() => onClick()}
>
{iconCross()}
</button>
)
)
}

const copyLinkToClipboard = (event, props) =>
copyToClipboard(props.file.uploadURL, props.i18n('copyLinkToClipboardFallback'))
Expand All @@ -41,9 +52,8 @@ const copyLinkToClipboard = (event, props) =>
// avoid losing focus
.then(() => event.target.focus({ preventScroll: true }))

const renderCopyLinkButton = (props) => (
props.showLinkToFileUploadResult &&
props.file.uploadURL &&
function CopyLinkButton (props) {
return (
<button
class="uppy-u-reset uppy-DashboardItem-action uppy-DashboardItem-action--copyLink"
type="button"
Expand All @@ -53,14 +63,41 @@ const renderCopyLinkButton = (props) => (
>
{iconCopyLink()}
</button>
)
)
}

module.exports = function Buttons (props) {
const {
file,
uploadInProgressOrComplete,
metaFields,
showLinkToFileUploadResult,
showRemoveButton,
i18n,
removeFile,
toggleFileCard
} = props

return (
<div className="uppy-DashboardItem-actionWrapper">
{renderEditButton(props)}
{renderCopyLinkButton(props)}
{renderRemoveButton(props)}
<EditButton
i18n={i18n}
file={file}
uploadInProgressOrComplete={uploadInProgressOrComplete}
metaFields={metaFields}
onClick={() => toggleFileCard(file.id)}
/>
{showLinkToFileUploadResult && file.uploadURL ? (
<CopyLinkButton i18n={i18n} />
) : null}
{showRemoveButton ? (
<RemoveButton
i18n={i18n}
info={props.info}
log={props.log}
onClick={() => removeFile(file.id)}
/>
) : null}
</div>
)
}
34 changes: 31 additions & 3 deletions packages/@uppy/dashboard/src/components/FileItem/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ module.exports = class FileItem extends Component {
return !shallowEqual(this.props, nextProps)
}

componentDidMount () {
const file = this.props.file
if (!file.preview) {
this.props.handleRequestThumbnail(file)
}
}

componentWillUnmount () {
const file = this.props.file
if (!file.preview) {
this.props.handleCancelThumbnail(file)
}
}

render () {
const file = this.props.file

Expand Down Expand Up @@ -38,17 +52,31 @@ module.exports = class FileItem extends Component {
})

return (
<li class={dashboardItemClass} id={`uppy_${file.id}`}>
<div
class={dashboardItemClass}
id={`uppy_${file.id}`}
role={this.props.role}
>
<div class="uppy-DashboardItem-preview">
<FilePreviewAndLink
file={file}
showLinkToFileUploadResult={this.props.showLinkToFileUploadResult}
/>
<FileProgress
{...this.props}
file={file}
error={error}
isUploaded={isUploaded}

hideRetryButton={this.props.hideRetryButton}
hidePauseResumeCancelButtons={this.props.hidePauseResumeCancelButtons}

resumableUploads={this.props.resumableUploads}
individualCancellation={this.props.individualCancellation}

pauseUpload={this.props.pauseUpload}
cancelUpload={this.props.cancelUpload}
retryUpload={this.props.retryUpload}
i18n={this.props.i18n}
/>
</div>

Expand Down Expand Up @@ -76,7 +104,7 @@ module.exports = class FileItem extends Component {
info={this.props.info}
/>
</div>
</li>
</div>
)
}
}
5 changes: 4 additions & 1 deletion packages/@uppy/dashboard/src/components/FileItem/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
border-bottom: 1px solid $gray-200;
padding: 10px;
padding-right: 0;

[data-uppy-theme="dark"] & {
border-bottom: 1px solid $gray-800;
}
Expand All @@ -24,18 +24,21 @@
float: left;
margin: 5px $rl-margin;
padding: 0;
/* When changing width: also update `itemsPerRow` values in `src/components/Dashboard.js`. */
width: calc(33.333% - #{$rl-margin} - #{$rl-margin});
height: 215px;
border-bottom: 0;
}

.uppy-size--lg & {
margin: 5px $rl-margin;
/* When changing width: also update `itemsPerRow` values in `src/components/Dashboard.js`. */
width: calc(25% - #{$rl-margin} - #{$rl-margin});
height: 190px;
}

.uppy-size--xl & {
/* When changing width: also update `itemsPerRow` values in `src/components/Dashboard.js`. */
width: calc(20% - #{$rl-margin} - #{$rl-margin});
height: 210px;
}
Expand Down
67 changes: 53 additions & 14 deletions packages/@uppy/dashboard/src/components/FileList.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
const FileItem = require('./FileItem/index.js')
const VirtualList = require('./VirtualList')
const classNames = require('classnames')
const { h } = require('preact')

module.exports = (props) => {
const dashboardFilesClass = classNames({
'uppy-Dashboard-files': true,
'uppy-Dashboard-files--noFiles': props.totalFileCount === 0
function chunks (list, size) {
const chunked = []
let currentChunk = []
list.forEach((item, i) => {
if (currentChunk.length < size) {
currentChunk.push(item)
} else {
chunked.push(currentChunk)
currentChunk = [item]
}
})
if (currentChunk.length) chunked.push(currentChunk)
return chunked
}

module.exports = (props) => {
const noFiles = props.totalFileCount === 0
const dashboardFilesClass = classNames(
'uppy-Dashboard-files',
{ 'uppy-Dashboard-files--noFiles': noFiles }
)

// It's not great that this is hardcoded!
// It's ESPECIALLY not great that this is checking against `itemsPerRow`!
const rowHeight = props.itemsPerRow === 1
// Mobile
? 71
// 190px height + 2 * 5px margin
: 200

const fileProps = {
// FIXME This is confusing, it's actually the Dashboard's plugin ID
Expand All @@ -32,22 +57,36 @@ module.exports = (props) => {
cancelUpload: props.cancelUpload,
toggleFileCard: props.toggleFileCard,
removeFile: props.removeFile,
handleRequestThumbnail: props.handleRequestThumbnail
handleRequestThumbnail: props.handleRequestThumbnail,
handleCancelThumbnail: props.handleCancelThumbnail
}

function renderItem (fileID) {
const rows = chunks(Object.keys(props.files), props.itemsPerRow)

function renderRow (row) {
return (
<FileItem
key={fileID}
{...fileProps}
file={props.files[fileID]}
/>
// The `role="presentation` attribute ensures that the list items are properly associated with the `VirtualList` element
// We use the first file ID as the key—this should not change across scroll rerenders
<div role="presentation" key={row[0]}>
{row.map((fileID) => (
<FileItem
key={fileID}
{...fileProps}
role="listitem"
file={props.files[fileID]}
/>
))}
</div>
)
}

return (
<ul class={dashboardFilesClass}>
{Object.keys(props.files).map(renderItem)}
</ul>
<VirtualList
class={dashboardFilesClass}
role="list"
data={rows}
renderRow={renderRow}
rowHeight={rowHeight}
/>
)
}
Loading

0 comments on commit 0cc2c36

Please sign in to comment.