diff --git a/client/.env b/client/.env index a96328e..3df1a71 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.3.2 \ No newline at end of file +REACT_APP_VERSION=1.3.3 \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 476f8d5..66b371f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2397,6 +2397,14 @@ "csstype": "^3.0.2" } }, + "@types/react-beautiful-dnd": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz", + "integrity": "sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg==", + "requires": { + "@types/react": "*" + } + }, "@types/react-dom": { "version": "17.0.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.3.tgz", @@ -4614,6 +4622,14 @@ "postcss": "^7.0.5" } }, + "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", @@ -9932,6 +9948,11 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -12300,6 +12321,11 @@ "performance-now": "^2.1.0" } }, + "raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -12362,6 +12388,20 @@ "whatwg-fetch": "^3.4.1" } }, + "react-beautiful-dnd": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz", + "integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==", + "requires": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + } + }, "react-dev-utils": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz", @@ -15077,6 +15117,11 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-memo-one": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz", + "integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==" + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", diff --git a/client/package.json b/client/package.json index 5467e68..832d079 100644 --- a/client/package.json +++ b/client/package.json @@ -11,12 +11,14 @@ "@types/jest": "^26.0.23", "@types/node": "^12.20.12", "@types/react": "^17.0.5", + "@types/react-beautiful-dnd": "^13.0.0", "@types/react-dom": "^17.0.3", "@types/react-redux": "^7.1.16", "@types/react-router-dom": "^5.1.7", "axios": "^0.21.1", "http-proxy-middleware": "^2.0.0", "react": "^17.0.2", + "react-beautiful-dnd": "^13.1.0", "react-dom": "^17.0.2", "react-redux": "^7.2.4", "react-router-dom": "^5.2.0", diff --git a/client/src/components/Apps/AppTable/AppTable.tsx b/client/src/components/Apps/AppTable/AppTable.tsx index 728e079..1d80477 100644 --- a/client/src/components/Apps/AppTable/AppTable.tsx +++ b/client/src/components/Apps/AppTable/AppTable.tsx @@ -1,7 +1,8 @@ import { KeyboardEvent } from 'react'; import { connect } from 'react-redux'; import { App, GlobalState } from '../../../interfaces'; -import { pinApp, deleteApp } from '../../../store/actions'; +import { pinApp, deleteApp, reorderApp } from '../../../store/actions'; +import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd'; import classes from './AppTable.module.css'; import Icon from '../../UI/Icons/Icon/Icon'; @@ -12,6 +13,7 @@ interface ComponentProps { pinApp: (app: App) => void; deleteApp: (id: number) => void; updateAppHandler: (app: App) => void; + reorderApp: (apps: App[]) => void; } const AppTable = (props: ComponentProps): JSX.Element => { @@ -29,49 +31,89 @@ const AppTable = (props: ComponentProps): JSX.Element => { } } + const dragEndHanlder = (result: DropResult): void => { + console.log(result); + + if (!result.destination) { + return; + } + + const tmpApps = [...props.apps]; + const [movedApp] = tmpApps.splice(result.source.index, 1); + tmpApps.splice(result.destination.index, 0, movedApp); + + props.reorderApp(tmpApps); + } + return ( - - {props.apps.map((app: App): JSX.Element => { - return ( - - - - - - - ) - })} -
{app.name}{app.url}{app.icon} -
deleteAppHandler(app)} - onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)} - tabIndex={0}> - -
-
props.updateAppHandler(app)} - onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)} - tabIndex={0}> - -
-
props.pinApp(app)} - onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)} - tabIndex={0}> - {app.isPinned - ? - : - } -
-
+ + + {(provided) => ( + + {props.apps.map((app: App, index): JSX.Element => { + return ( + + {(provided, snapshot) => { + const style = { + border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none', + borderRadius: '4px', + ...provided.draggableProps.style, + }; + + return ( + + + + + {!snapshot.isDragging && ( + + )} + + ) + }} + + ) + })} +
{app.name}{app.url}{app.icon} +
deleteAppHandler(app)} + onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)} + tabIndex={0}> + +
+
props.updateAppHandler(app)} + onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)} + tabIndex={0}> + +
+
props.pinApp(app)} + onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)} + tabIndex={0}> + {app.isPinned + ? + : + } +
+
+ )} +
+
) } @@ -81,4 +123,4 @@ const mapStateToProps = (state: GlobalState) => { } } -export default connect(mapStateToProps, { pinApp, deleteApp })(AppTable); \ No newline at end of file +export default connect(mapStateToProps, { pinApp, deleteApp, reorderApp })(AppTable); \ No newline at end of file diff --git a/client/src/components/UI/Table/Table.module.css b/client/src/components/UI/Table/Table.module.css index 33b712b..9700fc8 100644 --- a/client/src/components/UI/Table/Table.module.css +++ b/client/src/components/UI/Table/Table.module.css @@ -8,15 +8,17 @@ text-align: left; font-size: 16px; color: var(--color-primary); + table-layout: fixed; } .Table th, .Table td { padding: 10px; + overflow: hidden; + text-overflow: ellipsis; } /* Head */ - .Table th { --header-radius: 4px; background-color: var(--color-primary); @@ -34,8 +36,6 @@ } /* Body */ - .Table td { - /* opacity: 0.5; */ transition: all 0.2s; } \ No newline at end of file diff --git a/client/src/components/UI/Table/Table.tsx b/client/src/components/UI/Table/Table.tsx index 882ebfb..d425dc1 100644 --- a/client/src/components/UI/Table/Table.tsx +++ b/client/src/components/UI/Table/Table.tsx @@ -3,11 +3,12 @@ import classes from './Table.module.css'; interface ComponentProps { children: JSX.Element | JSX.Element[]; headers: string[]; + innerRef?: any; } const Table = (props: ComponentProps): JSX.Element => { return ( -
+
diff --git a/client/src/store/actions/actionTypes.ts b/client/src/store/actions/actionTypes.ts index d2cc17e..42d863e 100644 --- a/client/src/store/actions/actionTypes.ts +++ b/client/src/store/actions/actionTypes.ts @@ -7,6 +7,7 @@ import { AddAppAction, DeleteAppAction, UpdateAppAction, + ReorderAppAction, // Categories GetCategoriesAction, AddCategoryAction, @@ -37,6 +38,7 @@ export enum ActionTypes { addAppSuccess = 'ADD_APP_SUCCESS', deleteApp = 'DELETE_APP', updateApp = 'UPDATE_APP', + reorderApp = 'REORDER_APP', // Categories getCategories = 'GET_CATEGORIES', getCategoriesSuccess = 'GET_CATEGORIES_SUCCESS', @@ -66,6 +68,7 @@ export type Action = AddAppAction | DeleteAppAction | UpdateAppAction | + ReorderAppAction | // Categories GetCategoriesAction | AddCategoryAction | diff --git a/client/src/store/actions/app.ts b/client/src/store/actions/app.ts index a651cd2..5c7db15 100644 --- a/client/src/store/actions/app.ts +++ b/client/src/store/actions/app.ts @@ -132,4 +132,36 @@ export const updateApp = (id: number, formData: NewApp) => async (dispatch: Disp } catch (err) { console.log(err); } +} + +export interface ReorderAppAction { + type: ActionTypes.reorderApp; + payload: App[] +} + +interface ReorderQuery { + apps: { + id: number; + orderId: number; + }[] +} + +export const reorderApp = (apps: App[]) => async (dispatch: Dispatch) => { + try { + const updateQuery: ReorderQuery = { apps: [] } + + apps.forEach((app, index) => updateQuery.apps.push({ + id: app.id, + orderId: index + 1 + })) + + await axios.put<{}>('/api/apps/0/reorder', updateQuery); + + dispatch({ + type: ActionTypes.reorderApp, + payload: apps + }) + } catch (err) { + console.log(err); + } } \ No newline at end of file diff --git a/client/src/store/reducers/app.ts b/client/src/store/reducers/app.ts index b445542..dcdb6ef 100644 --- a/client/src/store/reducers/app.ts +++ b/client/src/store/reducers/app.ts @@ -52,8 +52,12 @@ const pinApp = (state: State, action: Action): State => { } const addAppSuccess = (state: State, action: Action): State => { - const tmpApps = [...state.apps, action.payload]; - + const tmpApps: App[] = [...state.apps, action.payload].sort((a: App, b: App) => { + if (a.name.toLowerCase() < b.name.toLowerCase()) { return -1 } + if (a.name.toLowerCase() > b.name.toLowerCase()) { return 1 } + return 0; + }); + return { ...state, apps: tmpApps @@ -85,6 +89,13 @@ const updateApp = (state: State, action: Action): State => { } } +const reorderApp = (state: State, action: Action): State => { + return { + ...state, + apps: action.payload + } +} + const appReducer = (state = initialState, action: Action) => { switch (action.type) { case ActionTypes.getApps: return getApps(state, action); @@ -94,6 +105,7 @@ const appReducer = (state = initialState, action: Action) => { case ActionTypes.addAppSuccess: return addAppSuccess(state, action); case ActionTypes.deleteApp: return deleteApp(state, action); case ActionTypes.updateApp: return updateApp(state, action); + case ActionTypes.reorderApp: return reorderApp(state, action); default: return state; } } diff --git a/controllers/apps.js b/controllers/apps.js index 6ca83d2..95c7fdd 100644 --- a/controllers/apps.js +++ b/controllers/apps.js @@ -36,8 +36,11 @@ exports.createApp = asyncWrapper(async (req, res, next) => { // @route GET /api/apps // @access Public exports.getApps = asyncWrapper(async (req, res, next) => { + // const apps = await App.findAll({ + // order: [[ Sequelize.fn('lower', Sequelize.col('name')), 'ASC' ]] + // }); const apps = await App.findAll({ - order: [[ Sequelize.fn('lower', Sequelize.col('name')), 'ASC' ]] + order: [[ 'orderId', 'ASC' ]] }); res.status(200).json({ @@ -92,6 +95,22 @@ exports.deleteApp = asyncWrapper(async (req, res, next) => { where: { id: req.params.id } }) + res.status(200).json({ + success: true, + data: {} + }) +}) + +// @desc Reorder apps +// @route PUT /api/apps/0/reorder +// @access Public +exports.reorderApps = asyncWrapper(async (req, res, next) => { + req.body.apps.forEach(async ({ id, orderId }) => { + await App.update({ orderId }, { + where: { id } + }) + }) + res.status(200).json({ success: true, data: {} diff --git a/models/App.js b/models/App.js index 8b7a5a3..f521955 100644 --- a/models/App.js +++ b/models/App.js @@ -18,6 +18,11 @@ const App = sequelize.define('App', { isPinned: { type: DataTypes.BOOLEAN, defaultValue: false + }, + orderId: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null } }, { tableName: 'apps' diff --git a/routes/apps.js b/routes/apps.js index d05f988..a0b3f47 100644 --- a/routes/apps.js +++ b/routes/apps.js @@ -6,7 +6,8 @@ const { getApps, getApp, updateApp, - deleteApp + deleteApp, + reorderApps } = require('../controllers/apps'); router @@ -20,4 +21,8 @@ router .put(updateApp) .delete(deleteApp); +router + .route('/0/reorder') + .put(reorderApps); + module.exports = router; \ No newline at end of file