From 337bbd582b4d097f73f08603464c699d04a0e694 Mon Sep 17 00:00:00 2001
From: Paul Kaplan <pkaplan@media.mit.edu>
Date: Mon, 5 Nov 2018 13:33:34 -0500
Subject: [PATCH] Add basic "show more" pagination for the backpack

---
 src/components/backpack/backpack.css | 13 +++++
 src/components/backpack/backpack.jsx | 19 +++++++-
 src/containers/backpack.jsx          | 72 ++++++++++++++++++++--------
 src/lib/backpack-api.js              | 20 ++++----
 4 files changed, 92 insertions(+), 32 deletions(-)

diff --git a/src/components/backpack/backpack.css b/src/components/backpack/backpack.css
index e12fe8008..6eddfa905 100644
--- a/src/components/backpack/backpack.css
+++ b/src/components/backpack/backpack.css
@@ -76,3 +76,16 @@
 .backpack-item img {
     mix-blend-mode: multiply; /* Make white transparent for thumnbnails */
 }
+
+.more {
+    background: $motion-primary;
+    color: $ui-white;
+    border: none;
+    outline: none;
+    font-weight: bold;
+    border-radius: 0.5rem;
+    font-size: 0.85rem;
+    padding: 0.5rem;
+    margin: 0.5rem;
+    cursor: pointer;
+}
diff --git a/src/components/backpack/backpack.jsx b/src/components/backpack/backpack.jsx
index f5f450039..12fed8238 100644
--- a/src/components/backpack/backpack.jsx
+++ b/src/components/backpack/backpack.jsx
@@ -25,10 +25,12 @@ const Backpack = ({
     error,
     expanded,
     loading,
+    showMore,
     onToggle,
     onDelete,
     onMouseEnter,
-    onMouseLeave
+    onMouseLeave,
+    onMore
 }) => (
     <div className={styles.backpackContainer}>
         <div
@@ -98,6 +100,18 @@ const Backpack = ({
                                         onDeleteButtonClick={onDelete}
                                     />
                                 ))}
+                                {showMore && (
+                                    <button
+                                        className={styles.more}
+                                        onClick={onMore}
+                                    >
+                                        <FormattedMessage
+                                            defaultMessage="More"
+                                            description="Load more from backpack"
+                                            id="gui.backpack.more"
+                                        />
+                                    </button>
+                                )}
                             </div>
                         ) : (
                             <div className={styles.statusMessage}>
@@ -129,6 +143,7 @@ Backpack.propTypes = {
     expanded: PropTypes.bool,
     loading: PropTypes.bool,
     onDelete: PropTypes.func,
+    onMore: PropTypes.func,
     onMouseEnter: PropTypes.func,
     onMouseLeave: PropTypes.func,
     onToggle: PropTypes.func
@@ -140,6 +155,8 @@ Backpack.defaultProps = {
     dragOver: false,
     expanded: false,
     loading: false,
+    showMore: false,
+    onMore: null,
     onToggle: null
 };
 
diff --git a/src/containers/backpack.jsx b/src/containers/backpack.jsx
index 32c6fea00..a79b43a02 100644
--- a/src/containers/backpack.jsx
+++ b/src/containers/backpack.jsx
@@ -29,11 +29,12 @@ class Backpack extends React.Component {
             'handleToggle',
             'handleDelete',
             'getBackpackAssetURL',
-            'refreshContents',
+            'getContents',
             'handleMouseEnter',
             'handleMouseLeave',
             'handleBlockDragEnd',
-            'handleBlockDragUpdate'
+            'handleBlockDragUpdate',
+            'handleMore'
         ]);
         this.state = {
             // While the DroppableHOC manages drop interactions for asset tiles,
@@ -42,8 +43,8 @@ class Backpack extends React.Component {
             blockDragOutsideWorkspace: false,
             blockDragOverBackpack: false,
             error: false,
-            offset: 0,
             itemsPerPage: 20,
+            moreToLoad: false,
             loading: false,
             expanded: false,
             contents: []
@@ -72,12 +73,12 @@ class Backpack extends React.Component {
     }
     handleToggle () {
         const newState = !this.state.expanded;
-        this.setState({expanded: newState, offset: 0}, () => {
+        this.setState({expanded: newState, contents: []}, () => {
             // Emit resize on window to get blocks to resize
             window.dispatchEvent(new Event('resize'));
         });
         if (newState) {
-            this.refreshContents();
+            this.getContents();
         }
     }
     handleDrop (dragInfo) {
@@ -107,33 +108,57 @@ class Backpack extends React.Component {
                     username: this.props.username,
                     ...payload
                 }))
-                .then(this.refreshContents);
+                .then(item => {
+                    this.setState({
+                        loading: false,
+                        contents: [item].concat(this.state.contents)
+                    });
+                })
+                .catch(() => {
+                    this.setState({error: true, loading: false});
+                });
         });
     }
     handleDelete (id) {
-        deleteBackpackObject({
-            host: this.props.host,
-            token: this.props.token,
-            username: this.props.username,
-            id: id
-        }).then(this.refreshContents);
-    }
-    refreshContents () {
-        if (this.props.token && this.props.username) {
-            this.setState({loading: true, error: false});
-            getBackpackContents({
+        this.setState({loading: true}, () => {
+            deleteBackpackObject({
                 host: this.props.host,
                 token: this.props.token,
                 username: this.props.username,
-                offset: this.state.offset,
-                limit: this.state.itemsPerPage
+                id: id
             })
-                .then(contents => {
-                    this.setState({contents, loading: false});
+                .then(() => {
+                    this.setState({
+                        loading: false,
+                        contents: this.state.contents.filter(o => o.id !== id)
+                    });
                 })
                 .catch(() => {
                     this.setState({error: true, loading: false});
                 });
+        });
+    }
+    getContents () {
+        if (this.props.token && this.props.username) {
+            this.setState({loading: true, error: false}, () => {
+                getBackpackContents({
+                    host: this.props.host,
+                    token: this.props.token,
+                    username: this.props.username,
+                    offset: this.state.contents.length,
+                    limit: this.state.itemsPerPage
+                })
+                    .then(contents => {
+                        this.setState({
+                            contents: this.state.contents.concat(contents),
+                            moreToLoad: contents.length === this.state.itemsPerPage,
+                            loading: false
+                        });
+                    })
+                    .catch(() => {
+                        this.setState({error: true, loading: false});
+                    });
+            });
         }
     }
     handleBlockDragUpdate (isOutsideWorkspace) {
@@ -168,6 +193,9 @@ class Backpack extends React.Component {
             blockDragOutsideWorkspace: false
         });
     }
+    handleMore () {
+        this.getContents();
+    }
     render () {
         return (
             <DroppableBackpack
@@ -176,8 +204,10 @@ class Backpack extends React.Component {
                 error={this.state.error}
                 expanded={this.state.expanded}
                 loading={this.state.loading}
+                showMore={this.state.moreToLoad}
                 onDelete={this.handleDelete}
                 onDrop={this.handleDrop}
+                onMore={this.handleMore}
                 onMouseEnter={this.handleMouseEnter}
                 onMouseLeave={this.handleMouseLeave}
                 onToggle={this.props.host ? this.handleToggle : null}
diff --git a/src/lib/backpack-api.js b/src/lib/backpack-api.js
index 4a6f7f6b2..8a5231843 100644
--- a/src/lib/backpack-api.js
+++ b/src/lib/backpack-api.js
@@ -4,6 +4,14 @@ import soundPayload from './backpack/sound-payload';
 import spritePayload from './backpack/sprite-payload';
 import codePayload from './backpack/code-payload';
 
+// Add a new property for the full thumbnail url, which includes the host.
+// Also include a full body url for loading sprite zips
+// TODO retreiving the images through storage would allow us to remove this.
+const includeFullUrls = (item, host) => Object.assign({}, item, {
+    thumbnailUrl: `${host}/${item.thumbnail}`,
+    bodyUrl: `${host}/${item.body}`
+});
+
 const getBackpackContents = ({
     host,
     username,
@@ -20,15 +28,7 @@ const getBackpackContents = ({
         if (error || response.statusCode !== 200) {
             return reject();
         }
-        // Add a new property for the full thumbnail url, which includes the host.
-        // Also include a full body url for loading sprite zips
-        // TODO retreiving the images through storage would allow us to remove this.
-        return resolve(response.body.map(item => (
-            Object.assign({}, item, {
-                thumbnailUrl: `${host}/${item.thumbnail}`,
-                bodyUrl: `${host}/${item.body}`
-            })
-        )));
+        return resolve(response.body.map(item => includeFullUrls(item, host)));
     });
 });
 
@@ -51,7 +51,7 @@ const saveBackpackObject = ({
         if (error || response.statusCode !== 200) {
             return reject();
         }
-        return resolve(response.body);
+        return resolve(includeFullUrls(response.body, host));
     });
 });
 
-- 
GitLab