From d09aa8a176f18ff4581a58830ea88067233d8df2 Mon Sep 17 00:00:00 2001 From: Sny Date: Thu, 17 Dec 2020 14:07:46 +0530 Subject: [PATCH] Pins can be dragged/dropped for ordering --- package-lock.json | 78 +++++++++++ package.json | 1 + src/components/common/Pin.jsx | 124 +++++++++++++++++ src/components/common/Pins.jsx | 217 +++++++++++++----------------- src/components/orgs/OrgHome.jsx | 6 + src/components/users/UserHome.jsx | 21 ++- 6 files changed, 320 insertions(+), 127 deletions(-) create mode 100644 src/components/common/Pin.jsx diff --git a/package-lock.json b/package-lock.json index e19cba1e..432d4c6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3684,6 +3684,14 @@ "randomfill": "^1.0.3" } }, + "css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "requires": { + "tiny-invariant": "^1.0.6" + } + }, "css-color-names": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", @@ -8973,6 +8981,11 @@ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true }, + "raf-schd": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz", + "integrity": "sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==" + }, "ramda": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.25.0.tgz", @@ -9026,6 +9039,30 @@ "prop-types": "^15.6.2" } }, + "react-beautiful-dnd": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz", + "integrity": "sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg==", + "requires": { + "@babel/runtime": "^7.8.4", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.1.1", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + } + } + }, "react-dom": { "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", @@ -9068,6 +9105,28 @@ "prop-types": "^15.6.0" } }, + "react-redux": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz", + "integrity": "sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==", + "requires": { + "@babel/runtime": "^7.12.1", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.13.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + } + } + }, "react-router": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", @@ -9268,6 +9327,15 @@ "balanced-match": "^1.0.0" } }, + "redux": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", + "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -10721,6 +10789,11 @@ "whet.extend": "~0.9.9" } }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "table": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", @@ -11487,6 +11560,11 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "use-memo-one": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.1.tgz", + "integrity": "sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ==" + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", diff --git a/package.json b/package.json index 0c186a73..55eb86db 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "prop-types": "^15.7.2", "query-string": "^6.13.7", "react": "^16.14.0", + "react-beautiful-dnd": "^13.0.0", "react-dom": "^16.14.0", "react-image-crop": "^8.6.6", "react-infinite-scroll-component": "^5.1.0", diff --git a/src/components/common/Pin.jsx b/src/components/common/Pin.jsx new file mode 100644 index 00000000..263ad707 --- /dev/null +++ b/src/components/common/Pin.jsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { + Card, CardHeader, CardContent, CardActions, IconButton, Chip, Avatar, Typography, + CircularProgress, +} from '@material-ui/core'; +import { + List as ListIcon, Loyalty as LoyaltyIcon, Home as HomeIcon, + Delete as DeleteIcon, LocalOffer as LocalOfferIcon, Link as LinkIcon, + AccountTreeRounded as TreeIcon +} from '@material-ui/icons'; +import { isEmpty, get, last, compact } from 'lodash'; +import { ORANGE, GREEN } from '../../common/constants'; + +const getIcon = resourceURI => { + if(resourceURI.indexOf('/sources/') >= 0) + return ; + if(resourceURI.indexOf('/collections/') >= 0) + return ; + if(resourceURI.indexOf('/orgs/') >= 0) + return ; +} + +const Pin = ({ pin, canDelete, onDelete }) => { + const mnemonic = last(compact(pin.resource_uri.split('/'))) + const isOrg = pin.resource_uri.indexOf('/sources/') === -1 && pin.resource_uri.indexOf('/collections/') === -1; + return ( + + + {getIcon(pin.resource_uri)} + + } + action={ + canDelete && + onDelete(pin.id)}> + + + } + title={{mnemonic}} + /> + + { + isEmpty(pin.resource) ? +
+ +
: + + {pin.resource.description || pin.resource.full_name || pin.resource.name} + + } +
+ + { + isOrg ? + + + } + style={{border: 'none', margin: '0 5px', padding: '5px'}} + /> + + + } + style={{border: 'none', margin: '0 5px', padding: '5px'}} + /> + + : + + + } + style={{border: 'none', margin: '0 5px', padding: '5px'}} + /> + + + } + style={{border: 'none', margin: '0 5px', padding: '5px'}} + /> + + + } + style={{border: 'none', margin: '0 5px', padding: '5px'}} + /> + + + } + +
+ ) +} + +export default Pin; diff --git a/src/components/common/Pins.jsx b/src/components/common/Pins.jsx index 6734c394..aee06555 100644 --- a/src/components/common/Pins.jsx +++ b/src/components/common/Pins.jsx @@ -1,26 +1,8 @@ import React from 'react'; -import { Link } from 'react-router-dom'; -import { - Card, CardHeader, CardContent, CardActions, IconButton, Chip, Avatar, Typography, - CircularProgress, -} from '@material-ui/core'; -import { - List as ListIcon, Loyalty as LoyaltyIcon, Home as HomeIcon, - Delete as DeleteIcon, LocalOffer as LocalOfferIcon, Link as LinkIcon, - AccountTreeRounded as TreeIcon -} from '@material-ui/icons'; -import { map, isEmpty, get, last, compact, orderBy } from 'lodash'; -import { ORANGE, GREEN } from '../../common/constants'; +import { map, isEmpty, get } from 'lodash'; +import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; import PinIcon from '../common/PinIcon'; - -const getIcon = resourceURI => { - if(resourceURI.indexOf('/sources/') >= 0) - return ; - if(resourceURI.indexOf('/collections/') >= 0) - return ; - if(resourceURI.indexOf('/orgs/') >= 0) - return ; -} +import Pin from './Pin'; const getGridDivision = pinsCount => { let division = 12; @@ -28,121 +10,110 @@ const getGridDivision = pinsCount => { return 6 return Math.floor(division/pinsCount); } +const getItemStyle = (draggableStyle, isDragging) => ({ + // some basic styles to make the items look a bit nicer + userSelect: 'none', + opacity: isDragging ? 0.5 : 1, + // styles we need to apply on draggables + ...draggableStyle +}); +const getListStyle = () => ({ + display: 'flex', + overflow: 'hidden', +}); + +const reorder = (list, startIndex, endIndex) => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + + return result; +}; -const Pins = ({ pins, onDelete, canDelete }) => { - let gridDivision = getGridDivision(pins.length); + +const Pins = ({ pins, onDelete, canDelete, onOrderUpdate }) => { + const [orderedPins, setPins] = React.useState(pins) + let gridDivision = getGridDivision(get(orderedPins, 'length', 0)); const gridClassName = `col-md-${gridDivision}` + + React.useEffect(() => { + setPins(pins) + }, [pins]) + + const onDragEnd = result => { + if (!result.destination) { + return; + } + + const items = reorder( + orderedPins, + result.source.index, + result.destination.index + ); + + onOrderUpdate(parseInt(result.draggableId), result.destination.index) + + setPins(items); + } + return (
{ - !isEmpty(pins) && + !isEmpty(orderedPins) &&

Pinned

} { - map(orderBy(pins, 'id'), pin => { - const mnemonic = last(compact(pin.resource_uri.split('/'))) - const isOrg = pin.resource_uri.indexOf('/sources/') === -1 && pin.resource_uri.indexOf('/collections/') === -1; - return ( -
- - - {getIcon(pin.resource_uri)} - - } - action={ - canDelete && - onDelete(pin.id)}> - - - } - title={{mnemonic}} - /> - - { - isEmpty(pin.resource) ? -
- -
: - - {pin.resource.description || pin.resource.full_name || pin.resource.name} - - } -
- + !isEmpty(orderedPins) && + + + { + (provided, snapshot) => ( +
{ - isOrg ? - - - } - style={{border: 'none', margin: '0 5px', padding: '5px'}} - /> - - - } - style={{border: 'none', margin: '0 5px', padding: '5px'}} - /> - - : - - - } - style={{border: 'none', margin: '0 5px', padding: '5px'}} - /> - - - } - style={{border: 'none', margin: '0 5px', padding: '5px'}} - /> - - - } - style={{border: 'none', margin: '0 5px', padding: '5px'}} - /> - - + map(orderedPins, (pin, index) => { + return ( + + { + (provided, snapshot) => ( +
+
+ +
+ {provided.placeholder} +
+ ) + } +
+ ) + }) } - - -
- ) - }) + {provided.placeholder} +
+ ) + } + + }
); diff --git a/src/components/orgs/OrgHome.jsx b/src/components/orgs/OrgHome.jsx index 873ccd17..c0d3ab95 100644 --- a/src/components/orgs/OrgHome.jsx +++ b/src/components/orgs/OrgHome.jsx @@ -117,6 +117,11 @@ class OrgHome extends React.Component { } } + updatePinOrder = (pinId, newOrder) => { + const service = this.getPinsService(pinId) + service.put({order: newOrder}).then(() => {}) + } + render() { const { org, isLoading, tab, pins } = this.state; const url = this.getURLFromPath() @@ -132,6 +137,7 @@ class OrgHome extends React.Component { pins={pins} onDelete={this.deletePin} canDelete={isCurrentUserMemberOfOrg} + onOrderUpdate={this.updatePinOrder} /> { + const service = this.getPinsService(pinId) + service.put({order: newOrder}).then(() => {}) + } + canActOnPins() { return isAdminUser() || (getCurrentUserUsername() === this.getUsername()) } @@ -113,11 +118,19 @@ class UserHome extends React.Component { const canActOnPins = this.canActOnPins() return (
-
- -
+ { + user && +
+ +
+ }
- +