From 5c57c689a8b6e16f8cd91a30cf445d40d5c244de Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 7 Aug 2021 06:10:34 -0700 Subject: [PATCH] add draggable cells --- package.json | 3 + src/components/player/NowPlayingView.tsx | 10 +-- src/components/playlist/PlaylistList.tsx | 8 +- src/components/playlist/PlaylistView.tsx | 10 +-- src/components/starred/StarredView.tsx | 20 ++--- src/components/table/DraggableCell.tsx | 50 ++++++++++++ src/components/table/DraggableHeaderCell.tsx | 50 ++++++++++++ src/components/viewtypes/ListViewType.tsx | 30 ++++++- src/index.tsx | 6 +- yarn.lock | 85 ++++++++++++++------ 10 files changed, 221 insertions(+), 51 deletions(-) create mode 100644 src/components/table/DraggableCell.tsx create mode 100644 src/components/table/DraggableHeaderCell.tsx diff --git a/package.json b/package.json index 5aae674..6e0e30d 100644 --- a/package.json +++ b/package.json @@ -185,6 +185,7 @@ "@types/node": "14.14.10", "@types/randomstring": "^1.1.7", "@types/react": "^16.9.44", + "@types/react-dnd": "^3.0.2", "@types/react-dom": "^16.9.9", "@types/react-redux": "^7.1.18", "@types/react-router-dom": "^5.1.6", @@ -269,6 +270,8 @@ "nanoid": "^3.1.23", "randomstring": "^1.2.1", "react": "^17.0.1", + "react-dnd": "^14.0.2", + "react-dnd-html5-backend": "^14.0.0", "react-dom": "^17.0.1", "react-h5-audio-player": "^3.7.1", "react-helmet-async": "^1.0.9", diff --git a/src/components/player/NowPlayingView.tsx b/src/components/player/NowPlayingView.tsx index eb7e599..6a840d5 100644 --- a/src/components/player/NowPlayingView.tsx +++ b/src/components/player/NowPlayingView.tsx @@ -15,13 +15,13 @@ import Loader from '../loader/Loader'; const tableColumns = [ { - header: '#', + id: '#', dataKey: 'index', alignment: 'center', width: 70, }, { - header: 'Title', + id: 'Title', dataKey: 'title', alignment: 'left', resizable: true, @@ -29,21 +29,21 @@ const tableColumns = [ }, { - header: 'Artist', + id: 'Artist', dataKey: 'artist', alignment: 'center', resizable: true, width: 300, }, { - header: 'Album', + id: 'Album', dataKey: 'album', alignment: 'center', resizable: true, width: 300, }, { - header: 'Duration', + id: 'Duration', dataKey: 'duration', alignment: 'center', resizable: true, diff --git a/src/components/playlist/PlaylistList.tsx b/src/components/playlist/PlaylistList.tsx index 2dde353..d38def4 100644 --- a/src/components/playlist/PlaylistList.tsx +++ b/src/components/playlist/PlaylistList.tsx @@ -10,28 +10,28 @@ import GenericPageHeader from '../layout/GenericPageHeader'; const tableColumns = [ { - header: 'Name', + id: 'Name', dataKey: 'name', alignment: 'left', flexGrow: 2, resizable: false, }, { - header: 'Tracks', + id: 'Tracks', dataKey: 'songCount', alignment: 'center', flexGrow: 1, resizable: false, }, { - header: 'Description', + id: 'Description', dataKey: 'comment', alignment: 'left', flexGrow: 2, resizable: false, }, { - header: 'Created', + id: 'Created', dataKey: 'created', alignment: 'left', flexGrow: 1, diff --git a/src/components/playlist/PlaylistView.tsx b/src/components/playlist/PlaylistView.tsx index 63d746f..0525e77 100644 --- a/src/components/playlist/PlaylistView.tsx +++ b/src/components/playlist/PlaylistView.tsx @@ -22,14 +22,14 @@ interface PlaylistParams { const tableColumns = [ { - header: '#', + id: '#', dataKey: 'index', alignment: 'center', resizable: true, width: 70, }, { - header: 'Title', + id: 'Title', dataKey: 'title', alignment: 'left', resizable: true, @@ -37,21 +37,21 @@ const tableColumns = [ }, { - header: 'Artist', + id: 'Artist', dataKey: 'artist', alignment: 'center', resizable: true, width: 300, }, { - header: 'Album', + id: 'Album', dataKey: 'album', alignment: 'center', resizable: true, width: 300, }, { - header: 'Duration', + id: 'Duration', dataKey: 'duration', alignment: 'center', resizable: true, diff --git a/src/components/starred/StarredView.tsx b/src/components/starred/StarredView.tsx index bf62d37..d4e0ec0 100644 --- a/src/components/starred/StarredView.tsx +++ b/src/components/starred/StarredView.tsx @@ -19,13 +19,13 @@ import ListViewType from '../viewtypes/ListViewType'; const trackTableColumns = [ { - header: '#', + id: '#', dataKey: 'index', alignment: 'center', width: 70, }, { - header: 'Title', + id: 'Title', dataKey: 'title', alignment: 'left', resizable: true, @@ -33,21 +33,21 @@ const trackTableColumns = [ }, { - header: 'Artist', + id: 'Artist', dataKey: 'artist', alignment: 'center', resizable: true, width: 300, }, { - header: 'Album', + id: 'Album', dataKey: 'album', alignment: 'center', resizable: true, width: 300, }, { - header: 'Duration', + id: 'Duration', dataKey: 'duration', alignment: 'center', resizable: true, @@ -57,13 +57,13 @@ const trackTableColumns = [ const albumTableColumns = [ { - header: '#', + id: '#', dataKey: 'index', alignment: 'center', width: 70, }, { - header: 'Title', + id: 'Title', dataKey: 'name', alignment: 'left', resizable: true, @@ -71,21 +71,21 @@ const albumTableColumns = [ }, { - header: 'Artist', + id: 'Artist', dataKey: 'artist', alignment: 'center', resizable: true, width: 300, }, { - header: 'Tracks', + id: 'Tracks', dataKey: 'songCount', alignment: 'center', resizable: true, width: 300, }, { - header: 'Duration', + id: 'Duration', dataKey: 'duration', alignment: 'center', resizable: true, diff --git a/src/components/table/DraggableCell.tsx b/src/components/table/DraggableCell.tsx new file mode 100644 index 0000000..2edac3a --- /dev/null +++ b/src/components/table/DraggableCell.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { useDrag, useDrop } from 'react-dnd'; +import { Table } from 'rsuite'; + +const ItemTypes = { + COLUMN: 'column', + ROW: 'row', +}; + +const DraggableCell = ({ children, onDrag, id, rowData, ...rest }: any) => { + const ref = React.useRef(null); + + const [{ canDrop, isOver }, drop] = useDrop({ + accept: ItemTypes.ROW, + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), + drop(item: any) { + onDrag(item.id, rowData.id); + }, + }); + + const [{ isDragging }, drag] = useDrag({ + type: ItemTypes.ROW, + item: { id: rowData.id, type: ItemTypes.ROW }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }); + + const isActive = canDrop && isOver; + + drag(drop(ref)); + + const styles = { + opacity: isDragging ? 0.5 : 1, + background: isActive ? '#ddd' : undefined, + }; + + return ( + +
+ {children} +
+
+ ); +}; + +export default DraggableCell; diff --git a/src/components/table/DraggableHeaderCell.tsx b/src/components/table/DraggableHeaderCell.tsx new file mode 100644 index 0000000..eab7cbd --- /dev/null +++ b/src/components/table/DraggableHeaderCell.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { useDrag, useDrop } from 'react-dnd'; +import { Table } from 'rsuite'; + +const ItemTypes = { + COLUMN: 'column', + ROW: 'row', +}; + +const DraggableHeaderCell = ({ children, onDrag, id, ...rest }: any) => { + const ref = React.useRef(null); + + const [{ canDrop, isOver }, drop] = useDrop({ + accept: ItemTypes.COLUMN, + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), + drop(item: any) { + onDrag(item.id, id); + }, + }); + + const [{ isDragging }, drag] = useDrag({ + type: ItemTypes.COLUMN, + item: { id, type: ItemTypes.COLUMN }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }); + const isActive = canDrop && isOver; + + drag(drop(ref)); + + return ( + +
+ {children} +
+
+ ); +}; + +export default DraggableHeaderCell; diff --git a/src/components/viewtypes/ListViewType.tsx b/src/components/viewtypes/ListViewType.tsx index 740e395..ca71556 100644 --- a/src/components/viewtypes/ListViewType.tsx +++ b/src/components/viewtypes/ListViewType.tsx @@ -15,9 +15,10 @@ import { import { nanoid } from '@reduxjs/toolkit'; import '../../styles/ListView.global.css'; import { formatSongDuration } from '../../shared/utils'; -import Loader from '../loader/Loader'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { clearSelected } from '../../redux/multiSelectSlice'; +import DraggableHeaderCell from '../table/DraggableHeaderCell'; +import Loader from '../loader/Loader'; declare global { interface Window { @@ -25,23 +26,39 @@ declare global { } } +const sort = (source: any, sourceId: any, targetId: any) => { + const nextData = source.filter((item: any) => item.id !== sourceId); + const dragItem = source.find((item: any) => item.id === sourceId); + const index = nextData.findIndex((item: any) => item.id === targetId); + + nextData.splice(index + 1, 0, dragItem); + return nextData; +}; + const ListViewType = ({ data, handleRowClick, handleRowDoubleClick, tableColumns, + hasDraggableColumns, rowHeight, virtualized, children, }: any) => { const [height, setHeight] = useState(0); const [show, setShow] = useState(false); + const [columns, setColumns] = useState(tableColumns); + const { getHeight } = DOMHelper; const wrapperRef = useRef(null); const multiSelect = useAppSelector((state: any) => state.multiSelect); const playQueue = useAppSelector((state: any) => state.playQueue); const dispatch = useAppDispatch(); + const handleDragColumn = (sourceId: any, targetId: any) => { + setColumns(sort(columns, sourceId, targetId)); + }; + useEffect(() => { function handleResize() { setShow(false); @@ -128,7 +145,7 @@ const ListViewType = ({ affixHorizontalScrollbar shouldUpdateScroll={false} > - {tableColumns.map((column: any) => ( + {columns.map((column: any) => ( - {column.header} + {hasDraggableColumns ? ( + + {column.id} + + ) : ( + {column.id} + )} + {column.dataKey === 'index' ? ( {(rowData: any, rowIndex: any) => { diff --git a/src/index.tsx b/src/index.tsx index 2349f4b..a3ec08d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { render } from 'react-dom'; import { HelmetProvider } from 'react-helmet-async'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; import { Provider } from 'react-redux'; import { store } from './redux/store'; import App from './App'; @@ -8,7 +10,9 @@ import App from './App'; render( - + + + , document.getElementById('root') diff --git a/yarn.lock b/yarn.lock index 737d8a2..ac194e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1432,6 +1432,21 @@ schema-utils "^2.6.5" source-map "^0.7.3" +"@react-dnd/asap@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.0.tgz#b300eeed83e9801f51bd66b0337c9a6f04548651" + integrity sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ== + +"@react-dnd/invariant@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-2.0.0.tgz#09d2e81cd39e0e767d7da62df9325860f24e517e" + integrity sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw== + +"@react-dnd/shallowequal@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a" + integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg== + "@reduxjs/toolkit@^1.6.1": version "1.6.1" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.1.tgz#7bc83b47352a663bf28db01e79d17ba54b98ade9" @@ -1729,16 +1744,16 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== -"@types/node@*", "@types/node@14.14.10": - version "14.14.10" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.10.tgz#5958a82e41863cfc71f2307b3748e3491ba03785" - integrity sha512-J32dgx2hw8vXrSbu4ZlVhn1Nm3GbeCFNw2FWL8S5QKucHGY0cyNwjdQdO+KMBZ4wpmC7KhLCiNsdk1RFRIYUQQ== - -"@types/node@^14.6.2": +"@types/node@*", "@types/node@^14.6.2": version "14.17.9" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.9.tgz#b97c057e6138adb7b720df2bd0264b03c9f504fd" integrity sha512-CMjgRNsks27IDwI785YMY0KLt3co/c0cQ5foxHYv/shC2w8oOnVwz5Ubq1QG5KzrcW+AXk6gzdnxIkDnTvzu3g== +"@types/node@14.14.10": + version "14.14.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.10.tgz#5958a82e41863cfc71f2307b3748e3491ba03785" + integrity sha512-J32dgx2hw8vXrSbu4ZlVhn1Nm3GbeCFNw2FWL8S5QKucHGY0cyNwjdQdO+KMBZ4wpmC7KhLCiNsdk1RFRIYUQQ== + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -1777,6 +1792,13 @@ resolved "https://registry.yarnpkg.com/@types/randomstring/-/randomstring-1.1.7.tgz#e214feeed53a14965d2865f6d8ae99a4ff3a9278" integrity sha512-S6NRYPiH8VGcLW4m9KEMUPtGxXqToCOLLCutQh8sSMaZGrL6/PEQCZAPGBtMP6SKd43ep5eWuPFN732h23h15w== +"@types/react-dnd@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/react-dnd/-/react-dnd-3.0.2.tgz#939e5a8ca5b83f847c3f64dabbe2f49a9fefb192" + integrity sha512-Z1BqHYGFtfSPfWs+kgX4b6wQmwwtqq4/pLo4zdO9xcDUB1ZQP8iWTAYNf3EJ2f0WiVQpSLN8UytP+ILzZHDLYw== + dependencies: + react-dnd "*" + "@types/react-dom@^16.9.9": version "16.9.9" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.9.tgz#d2d0a6f720a0206369ccbefff752ba37b9583136" @@ -4506,6 +4528,15 @@ dmg-license@^1.0.8: smart-buffer "^4.0.2" verror "^1.10.0" +dnd-core@14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-14.0.0.tgz#973ab3470d0a9ac5a0fa9021c4feba93ad12347d" + integrity sha512-wTDYKyjSqWuYw3ZG0GJ7k+UIfzxTNoZLjDrut37PbcPGNfwhlKYlPUqjAKUjOOv80izshUiqusaKgJPItXSevA== + dependencies: + "@react-dnd/asap" "^4.0.0" + "@react-dnd/invariant" "^2.0.0" + redux "^4.0.5" + dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" @@ -5602,7 +5633,7 @@ fast-deep-equal@^1.0.0: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= -fast-deep-equal@^3.1.1: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== @@ -6174,12 +6205,7 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4: - version "4.2.6" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" - integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== - -graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.3, graceful-fs@^4.2.4: version "4.2.8" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== @@ -6709,16 +6735,11 @@ ini@2.0.0: resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== -ini@^1.3.4: +ini@^1.3.4, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== - internal-ip@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" @@ -10054,6 +10075,24 @@ rc@^1.2.7, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-dnd-html5-backend@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.0.tgz#28d660a2ad1e07447c34a65cd25f7de8f1657194" + integrity sha512-2wAQqRFC1hbRGmk6+dKhOXsyQQOn3cN8PSZyOUeOun9J8t3tjZ7PS2+aFu7CVu2ujMDwTJR3VTwZh8pj2kCv7g== + dependencies: + dnd-core "14.0.0" + +react-dnd@*, react-dnd@^14.0.2: + version "14.0.2" + resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-14.0.2.tgz#57266baec92b887301f81fa3b77f87168d159733" + integrity sha512-JoEL78sBCg8SzjOKMlkR70GWaPORudhWuTNqJ56lb2P8Vq0eM2+er3ZrMGiSDhOmzaRPuA9SNBz46nHCrjn11A== + dependencies: + "@react-dnd/invariant" "^2.0.0" + "@react-dnd/shallowequal" "^2.0.0" + dnd-core "14.0.0" + fast-deep-equal "^3.1.3" + hoist-non-react-statics "^3.3.2" + react-dom@^17.0.1: version "17.0.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6" @@ -10355,10 +10394,10 @@ redux-thunk@^2.3.0: resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== -redux@^4.0.0, redux@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4" - integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g== +redux@^4.0.0, redux@^4.0.5, redux@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.1.tgz#76f1c439bb42043f985fbd9bf21990e60bd67f47" + integrity sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw== dependencies: "@babel/runtime" "^7.9.2"