From 754dc3a7b9f790edc144134ac79efbdff532ca50 Mon Sep 17 00:00:00 2001
From: unknown <pawel999@icloud.com>
Date: Fri, 18 Jun 2021 10:38:05 +0200
Subject: [PATCH] Sorting settings. Sort apps on change/add/update

---
 client/.env                                   |  2 +-
 .../Settings/OtherSettings/OtherSettings.tsx  | 29 +++++++++--
 client/src/interfaces/Category.ts             |  1 +
 client/src/store/actions/actionTypes.ts       |  9 ++--
 client/src/store/actions/app.ts               | 50 ++++++++++++++++---
 client/src/store/reducers/app.ts              | 23 +++++----
 client/src/utility/index.ts                   |  3 +-
 client/src/utility/sortData.ts                | 29 +++++++++++
 models/Category.js                            |  5 ++
 9 files changed, 124 insertions(+), 27 deletions(-)
 create mode 100644 client/src/utility/sortData.ts

diff --git a/client/.env b/client/.env
index 3df1a71..69d1f92 100644
--- a/client/.env
+++ b/client/.env
@@ -1 +1 @@
-REACT_APP_VERSION=1.3.3
\ No newline at end of file
+REACT_APP_VERSION=1.3.5
\ No newline at end of file
diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx
index 5df8be7..50b04b5 100644
--- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx
+++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx
@@ -2,7 +2,7 @@ import { useState, useEffect, ChangeEvent, FormEvent } from 'react';
 
 // Redux
 import { connect } from 'react-redux';
-import { createNotification, updateConfig } from '../../../store/actions';
+import { createNotification, updateConfig, sortApps } from '../../../store/actions';
 
 // Typescript
 import { GlobalState, NewNotification, SettingsForm } from '../../../interfaces';
@@ -17,6 +17,7 @@ import { searchConfig } from '../../../utility';
 interface ComponentProps {
   createNotification: (notification: NewNotification) => void;
   updateConfig: (formData: SettingsForm) => void;
+  sortApps: () => void;
   loading: boolean;
 }
 
@@ -26,7 +27,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
     customTitle: document.title,
     pinAppsByDefault: 1,
     pinCategoriesByDefault: 1,
-    hideHeader: 0
+    hideHeader: 0,
+    useOrdering: 'createdAt'
   })
 
   // Get config
@@ -35,7 +37,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
       customTitle: searchConfig('customTitle', 'Flame'),
       pinAppsByDefault: searchConfig('pinAppsByDefault', 1),
       pinCategoriesByDefault: searchConfig('pinCategoriesByDefault', 1),
-      hideHeader: searchConfig('hideHeader', 0)
+      hideHeader: searchConfig('hideHeader', 0),
+      useOrdering: searchConfig('useOrdering', 'createdAt')
     })
   }, [props.loading]);
 
@@ -46,8 +49,11 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
     // Save settings
     await props.updateConfig(formData);
 
-    // update local page title
+    // Update local page title
     document.title = formData.customTitle;
+
+    // Get sorted apps
+    props.sortApps();
   }
 
   // Input handler
@@ -113,6 +119,19 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
           <option value={0}>False</option>
         </select>
       </InputGroup>
+      <InputGroup>
+        <label htmlFor='useOrdering'>Sorting type</label>
+        <select
+          id='useOrdering'
+          name='useOrdering'
+          value={formData.useOrdering}
+          onChange={(e) => inputChangeHandler(e)}
+        >
+          <option value='createdAt'>By creation date</option>
+          <option value='name'>Alphabetical order</option>
+          <option value='orderId'>Custom order</option>
+        </select>
+      </InputGroup>
     <Button>Save changes</Button>
     </form>
   )
@@ -124,4 +143,4 @@ const mapStateToProps = (state: GlobalState) => {
   }
 }
 
-export default connect(mapStateToProps, { createNotification, updateConfig })(OtherSettings);
\ No newline at end of file
+export default connect(mapStateToProps, { createNotification, updateConfig, sortApps })(OtherSettings);
\ No newline at end of file
diff --git a/client/src/interfaces/Category.ts b/client/src/interfaces/Category.ts
index 926987d..0f9f8f9 100644
--- a/client/src/interfaces/Category.ts
+++ b/client/src/interfaces/Category.ts
@@ -3,6 +3,7 @@ import { Model, Bookmark } from '.';
 export interface Category extends Model {
   name: string;
   isPinned: boolean;
+  orderId: number;
   bookmarks: Bookmark[];
 }
 
diff --git a/client/src/store/actions/actionTypes.ts b/client/src/store/actions/actionTypes.ts
index 42d863e..769bafa 100644
--- a/client/src/store/actions/actionTypes.ts
+++ b/client/src/store/actions/actionTypes.ts
@@ -7,7 +7,8 @@ import {
   AddAppAction,
   DeleteAppAction,
   UpdateAppAction,
-  ReorderAppAction,
+  ReorderAppsAction,
+  SortAppsAction,
   // Categories
   GetCategoriesAction,
   AddCategoryAction,
@@ -38,7 +39,8 @@ export enum ActionTypes {
   addAppSuccess = 'ADD_APP_SUCCESS',
   deleteApp = 'DELETE_APP',
   updateApp = 'UPDATE_APP',
-  reorderApp = 'REORDER_APP',
+  reorderApps = 'REORDER_APPS',
+  sortApps = 'SORT_APPS',
   // Categories
   getCategories = 'GET_CATEGORIES',
   getCategoriesSuccess = 'GET_CATEGORIES_SUCCESS',
@@ -68,7 +70,8 @@ export type Action =
   AddAppAction |
   DeleteAppAction |
   UpdateAppAction |
-  ReorderAppAction |
+  ReorderAppsAction |
+  SortAppsAction |
   // Categories
   GetCategoriesAction<any> |
   AddCategoryAction |
diff --git a/client/src/store/actions/app.ts b/client/src/store/actions/app.ts
index 5c7db15..8dc9e94 100644
--- a/client/src/store/actions/app.ts
+++ b/client/src/store/actions/app.ts
@@ -1,7 +1,7 @@
 import axios from 'axios';
 import { Dispatch } from 'redux';
 import { ActionTypes } from './actionTypes';
-import { App, ApiResponse, NewApp } from '../../interfaces';
+import { App, ApiResponse, NewApp, Config } from '../../interfaces';
 import { CreateNotificationAction } from './notification';
 
 export interface GetAppsAction<T> {
@@ -73,10 +73,13 @@ export const addApp = (formData: NewApp) => async (dispatch: Dispatch) => {
       }
     })
 
-    dispatch<AddAppAction>({
+    await dispatch<AddAppAction>({
       type: ActionTypes.addAppSuccess,
       payload: res.data.data
     })
+
+    // Sort apps
+    dispatch<any>(sortApps())
   } catch (err) {
     console.log(err);
   }
@@ -125,17 +128,20 @@ export const updateApp = (id: number, formData: NewApp) => async (dispatch: Disp
       }
     })
 
-    dispatch<UpdateAppAction>({
+    await dispatch<UpdateAppAction>({
       type: ActionTypes.updateApp,
       payload: res.data.data
     })
+
+    // Sort apps
+    dispatch<any>(sortApps())
   } catch (err) {
     console.log(err);
   }
 }
 
-export interface ReorderAppAction {
-  type: ActionTypes.reorderApp;
+export interface ReorderAppsAction {
+  type: ActionTypes.reorderApps;
   payload: App[]
 }
 
@@ -146,7 +152,7 @@ interface ReorderQuery {
   }[]
 }
 
-export const reorderApp = (apps: App[]) => async (dispatch: Dispatch) => {
+export const reorderApps = (apps: App[]) => async (dispatch: Dispatch) => {
   try {
     const updateQuery: ReorderQuery = { apps: [] }
 
@@ -157,11 +163,39 @@ export const reorderApp = (apps: App[]) => async (dispatch: Dispatch) => {
 
     await axios.put<{}>('/api/apps/0/reorder', updateQuery);
 
-    dispatch<ReorderAppAction>({
-      type: ActionTypes.reorderApp,
+    dispatch<CreateNotificationAction>({
+      type: ActionTypes.createNotification,
+      payload: {
+        title: 'Success',
+        message: 'New order saved'
+      }
+    })
+
+    dispatch<ReorderAppsAction>({
+      type: ActionTypes.reorderApps,
       payload: apps
     })
   } catch (err) {
     console.log(err);
   }
+}
+
+export interface SortAppsAction {
+  type: ActionTypes.sortApps;
+  payload: {};
+}
+
+export const sortApps = () => async (dispatch: Dispatch) => {
+  try {
+    const res = await axios.get<ApiResponse<Config>>('/api/config/useOrdering');
+
+    console.log(res.data.data);
+
+    dispatch<SortAppsAction>({
+      type: ActionTypes.sortApps,
+      payload: res.data.data.value
+    })
+  } 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 dcdb6ef..0935819 100644
--- a/client/src/store/reducers/app.ts
+++ b/client/src/store/reducers/app.ts
@@ -1,5 +1,6 @@
 import { ActionTypes, Action } from '../actions';
 import { App } from '../../interfaces/App';
+import { sortData } from '../../utility';
 
 export interface State {
   loading: boolean;
@@ -52,15 +53,9 @@ const pinApp = (state: State, action: Action): State => {
 }
 
 const addAppSuccess = (state: State, action: Action): State => {
-  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
+    apps: [...state.apps, action.payload]
   }
 }
 
@@ -89,13 +84,22 @@ const updateApp = (state: State, action: Action): State => {
   }
 }
 
-const reorderApp = (state: State, action: Action): State => {
+const reorderApps = (state: State, action: Action): State => {
   return {
     ...state,
     apps: action.payload
   }
 }
 
+const sortApps = (state: State, action: Action): State => {
+  const sortedApps = sortData<App>(state.apps, action.payload);
+
+  return {
+    ...state,
+    apps: sortedApps
+  }
+}
+
 const appReducer = (state = initialState, action: Action) => {
   switch (action.type) {
     case ActionTypes.getApps: return getApps(state, action);
@@ -105,7 +109,8 @@ 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);
+    case ActionTypes.reorderApps: return reorderApps(state, action);
+    case ActionTypes.sortApps: return sortApps(state, action);
     default: return state;
   }
 }
diff --git a/client/src/utility/index.ts b/client/src/utility/index.ts
index 1422624..a5407b2 100644
--- a/client/src/utility/index.ts
+++ b/client/src/utility/index.ts
@@ -1,4 +1,5 @@
 export * from './iconParser';
 export * from './urlParser';
 export * from './searchConfig';
-export * from './checkVersion';
\ No newline at end of file
+export * from './checkVersion';
+export * from './sortData';
\ No newline at end of file
diff --git a/client/src/utility/sortData.ts b/client/src/utility/sortData.ts
new file mode 100644
index 0000000..c1e9803
--- /dev/null
+++ b/client/src/utility/sortData.ts
@@ -0,0 +1,29 @@
+interface Data {
+  name: string;
+  orderId: number;
+  createdAt: Date;
+}
+
+export const sortData = <T extends Data>(array: T[], field: string): T[] => {
+  const sortedData = array.slice();
+
+  if (field === 'name') {
+    sortedData.sort((a: T, b: T) => {
+      return a.name.localeCompare(b.name, 'en', { sensitivity: 'base' })
+    })
+  } else if (field === 'orderId') {
+    sortedData.sort((a: T, b: T) => {
+      if (a.orderId < b.orderId) { return -1 }
+      if (a.orderId > b.orderId) { return 1 }
+      return 0;
+    })
+  } else {
+    sortedData.sort((a: T, b: T) => {
+      if (a.createdAt < b.createdAt) { return -1 }
+      if (a.createdAt > b.createdAt) { return 1 }
+      return 0;
+    })
+  }
+
+  return sortedData;
+}
\ No newline at end of file
diff --git a/models/Category.js b/models/Category.js
index 5f82633..9c9eda6 100644
--- a/models/Category.js
+++ b/models/Category.js
@@ -9,6 +9,11 @@ const Category = sequelize.define('Category', {
   isPinned: {
     type: DataTypes.BOOLEAN,
     defaultValue: false
+  },
+  orderId: {
+    type: DataTypes.INTEGER,
+    allowNull: true,
+    defaultValue: null
   }
 }, {
   tableName: 'categories'