-
{props.app.name}
-
{displayUrl}
+
+
{props.category.name}
+
+ {props.apps.map((app: App) => {
+ const [displayUrl, redirectUrl] = urlParser(app.url);
+
+ let iconEl: JSX.Element;
+ const { icon } = app;
+
+ if (/.(jpeg|jpg|png)$/i.test(icon)) {
+ iconEl = (
+
+ );
+ } else if (/.(svg)$/i.test(icon)) {
+ iconEl = (
+
+
+
+ );
+ } else {
+ iconEl =
;
+ }
+
+ return (
+
+ {iconEl}
+
+
{app.name}
+ {displayUrl}
+
+
+ )
+ })}
-
- );
-};
+
+ )
+}
export default AppCard;
diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx
index d44418e..83dca72 100644
--- a/client/src/components/Apps/AppForm/AppForm.tsx
+++ b/client/src/components/Apps/AppForm/AppForm.tsx
@@ -1,59 +1,77 @@
-import { useState, useEffect, ChangeEvent, SyntheticEvent } from 'react';
+import { ChangeEvent, Dispatch, Fragment, SetStateAction, SyntheticEvent, useEffect, useState } from 'react';
import { connect } from 'react-redux';
-import { addApp, updateApp } from '../../../store/actions';
-import { App, NewApp } from '../../../interfaces';
-import classes from './AppForm.module.css';
-
-import ModalForm from '../../UI/Forms/ModalForm/ModalForm';
-import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
+import { App, Category, GlobalState, NewApp, NewCategory, NewNotification } from '../../../interfaces';
+import {
+ addApp,
+ addAppCategory,
+ createNotification,
+ getAppCategories,
+ updateApp,
+ updateAppCategory,
+} from '../../../store/actions';
import Button from '../../UI/Buttons/Button/Button';
+import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
+import ModalForm from '../../UI/Forms/ModalForm/ModalForm';
+import { ContentType } from '../Apps';
+import classes from './AppForm.module.css';
interface ComponentProps {
modalHandler: () => void;
- addApp: (formData: NewApp | FormData) => any;
- updateApp: (id: number, formData: NewApp | FormData) => any;
+ contentType: ContentType;
+ categories: Category[];
+ category?: Category;
app?: App;
+ addAppCategory: (formData: NewCategory) => void;
+ addApp: (formData: NewApp | FormData) => void;
+ updateAppCategory: (id: number, formData: NewCategory) => void;
+ updateApp: (id: number, formData: NewApp | FormData, previousCategoryId: number) => void;
+ createNotification: (notification: NewNotification) => void;
}
const AppForm = (props: ComponentProps): JSX.Element => {
const [useCustomIcon, toggleUseCustomIcon] = useState
(false);
const [customIcon, setCustomIcon] = useState(null);
- const [formData, setFormData] = useState({
+ const [categoryData, setCategoryData] = useState({
+ name: '',
+ type: 'apps'
+ })
+
+ const [appData, setAppData] = useState({
name: '',
url: '',
+ categoryId: -1,
icon: '',
});
+ // Load category data if provided for editing
+ useEffect(() => {
+ if (props.category) {
+ setCategoryData({ name: props.category.name, type: props.category.type });
+ } else {
+ setCategoryData({ name: '', type: "apps" });
+ }
+ }, [props.category]);
+
+ // Load app data if provided for editing
useEffect(() => {
if (props.app) {
- setFormData({
+ setAppData({
name: props.app.name,
url: props.app.url,
+ categoryId: props.app.categoryId,
icon: props.app.icon,
});
} else {
- setFormData({
+ setAppData({
name: '',
url: '',
+ categoryId: -1,
icon: '',
});
}
}, [props.app]);
- const inputChangeHandler = (e: ChangeEvent): void => {
- setFormData({
- ...formData,
- [e.target.name]: e.target.value,
- });
- };
-
- const fileChangeHandler = (e: ChangeEvent): void => {
- if (e.target.files) {
- setCustomIcon(e.target.files[0]);
- }
- };
-
const formSubmitHandler = (e: SyntheticEvent): void => {
e.preventDefault();
@@ -63,132 +81,241 @@ const AppForm = (props: ComponentProps): JSX.Element => {
if (customIcon) {
data.append('icon', customIcon);
}
- data.append('name', formData.name);
- data.append('url', formData.url);
+ data.append('name', appData.name);
+ data.append('url', appData.url);
+ data.append('categoryId', appData.categoryId.toString());
return data;
};
- if (!props.app) {
- if (customIcon) {
- const data = createFormData();
- props.addApp(data);
- } else {
- props.addApp(formData);
+ if (!props.category && !props.app) {
+ // Add new
+ if (props.contentType === ContentType.category) {
+ // Add category
+ props.addAppCategory(categoryData);
+ setCategoryData({ name: '', type: 'apps' });
+ } else if (props.contentType === ContentType.app) {
+ // Add app
+ if (appData.categoryId === -1) {
+ props.createNotification({
+ title: 'Error',
+ message: 'Please select category'
+ })
+ return;
+ }
+ if (customIcon) {
+ const data = createFormData();
+ props.addApp(data);
+ } else {
+ props.addApp(appData);
+ }
+ setAppData({
+ name: '',
+ url: '',
+ categoryId: appData.categoryId,
+ icon: ''
+ })
}
} else {
- if (customIcon) {
- const data = createFormData();
- props.updateApp(props.app.id, data);
- } else {
- props.updateApp(props.app.id, formData);
- props.modalHandler();
+ // Update
+ if (props.contentType === ContentType.category && props.category) {
+ // Update category
+ props.updateAppCategory(props.category.id, categoryData);
+ setCategoryData({ name: '', type: 'apps' });
+ } else if (props.contentType === ContentType.app && props.app) {
+ // Update app
+ if (customIcon) {
+ const data = createFormData();
+ props.updateApp(props.app.id, data, props.app.categoryId);
+ } else {
+ props.updateApp(props.app.id, appData, props.app.categoryId);
+ props.modalHandler();
+ }
}
- }
- setFormData({
- name: '',
- url: '',
- icon: '',
- });
- };
+ setAppData({
+ name: '',
+ url: '',
+ categoryId: -1,
+ icon: ''
+ });
+
+ setCustomIcon(null);
+ }
+ }
+
+ const inputChangeHandler = (e: ChangeEvent, setDataFunction: Dispatch>, data: any): void => {
+ setDataFunction({
+ ...data,
+ [e.target.name]: e.target.value
+ })
+ }
+
+ const fileChangeHandler = (e: ChangeEvent): void => {
+ if (e.target.files) {
+ setCustomIcon(e.target.files[0]);
+ }
+ }
+
+ let button = Submit
+
+ if (!props.category && !props.app) {
+ if (props.contentType === ContentType.category) {
+ button = Add new category ;
+ } else {
+ button = Add new app ;
+ }
+ } else if (props.category) {
+ button = Update category
+ } else if (props.app) {
+ button = Update app
+ }
return (
-
- App Name
- inputChangeHandler(e)}
- />
-
-
- App URL
- inputChangeHandler(e)}
- />
-
-
- {' '}
- Check supported URL formats
-
-
-
- {!useCustomIcon ? (
- // use mdi icon
-
- App Icon
- inputChangeHandler(e)}
- />
-
- Use icon name from MDI.
-
- {' '}
- Click here for reference
-
-
- toggleUseCustomIcon(!useCustomIcon)}
- className={classes.Switch}
- >
- Switch to custom icon upload
-
-
- ) : (
- // upload custom icon
-
- App Icon
- fileChangeHandler(e)}
- accept=".jpg,.jpeg,.png,.svg"
- />
- {
- setCustomIcon(null);
- toggleUseCustomIcon(!useCustomIcon);
- }}
- className={classes.Switch}
- >
- Switch to MDI
-
-
- )}
- {!props.app ? (
- Add new application
- ) : (
- Update application
- )}
+ {props.contentType === ContentType.category
+ ? (
+
+
+ Category Name
+ inputChangeHandler(e, setCategoryData, categoryData)}
+ />
+
+
+ )
+ : (
+
+
+ App Name
+ inputChangeHandler(e, setAppData, appData)}
+ />
+
+
+ App URL
+ inputChangeHandler(e, setAppData, appData)}
+ />
+
+
+ {' '}Check supported URL formats
+
+
+
+
+ App Category
+ inputChangeHandler(e, setAppData, appData)}
+ value={appData.categoryId}
+ >
+ Select category
+ {props.categories.map((category: Category): JSX.Element => {
+ return (
+
+ {category.name}
+
+ )
+ })}
+
+
+ {!useCustomIcon
+ // use mdi icon
+ ? (
+ App Icon
+ inputChangeHandler(e, setAppData, appData)}
+ />
+
+ Use icon name from MDI.
+
+ {' '}Click here for reference
+
+
+ toggleUseCustomIcon(!useCustomIcon)}
+ className={classes.Switch}>
+ Switch to custom icon upload
+
+ )
+ // upload custom icon
+ : (
+ App Icon
+ fileChangeHandler(e)}
+ accept=".jpg,.jpeg,.png,.svg"
+ />
+ toggleUseCustomIcon(!useCustomIcon)}
+ className={classes.Switch}>
+ Switch to MDI
+
+ )
+ }
+
+ )
+ }
+ {button}
);
};
-export default connect(null, { addApp, updateApp })(AppForm);
+const mapStateToProps = (state: GlobalState) => {
+ return {
+ categories: state.app.categories
+ }
+}
+
+const dispatchMap = {
+ getAppCategories,
+ addAppCategory,
+ addApp,
+ updateAppCategory,
+ updateApp,
+ createNotification
+}
+
+export default connect(mapStateToProps, dispatchMap)(AppForm);
diff --git a/client/src/components/Apps/AppGrid/AppGrid.tsx b/client/src/components/Apps/AppGrid/AppGrid.tsx
index 30d5c8c..a29adad 100644
--- a/client/src/components/Apps/AppGrid/AppGrid.tsx
+++ b/client/src/components/Apps/AppGrid/AppGrid.tsx
@@ -1,28 +1,29 @@
-import classes from './AppGrid.module.css';
import { Link } from 'react-router-dom';
-import { App } from '../../../interfaces/App';
+import { App, Category } from '../../../interfaces';
import AppCard from '../AppCard/AppCard';
+import classes from './AppGrid.module.css';
interface ComponentProps {
- apps: App[];
- totalApps?: number;
+ categories: Category[];
+ apps: App[]
+ totalCategories?: number;
searching: boolean;
}
const AppGrid = (props: ComponentProps): JSX.Element => {
let apps: JSX.Element;
- if (props.apps.length > 0) {
+ if (props.categories.length > 0) {
apps = (
- {props.apps.map((app: App): JSX.Element => {
- return
;
+ {props.categories.map((category: Category): JSX.Element => {
+ return
app.categoryId === category.id)} />
})}
);
} else {
- if (props.totalApps) {
+ if (props.totalCategories) {
if (props.searching) {
apps = (
@@ -32,7 +33,7 @@ const AppGrid = (props: ComponentProps): JSX.Element => {
} else {
apps = (
- There are no pinned applications. You can pin them from the{' '}
+ There are no pinned application categories. You can pin them from the{' '}
/applications menu
);
@@ -40,7 +41,7 @@ const AppGrid = (props: ComponentProps): JSX.Element => {
} else {
apps = (
- You don't have any applications. You can add a new one from{' '}
+ You don't have any applications. You can add a new one from the{' '}
/applications menu
);
diff --git a/client/src/components/Apps/AppTable/AppTable.tsx b/client/src/components/Apps/AppTable/AppTable.tsx
index 6ef6e6c..eb97b84 100644
--- a/client/src/components/Apps/AppTable/AppTable.tsx
+++ b/client/src/components/Apps/AppTable/AppTable.tsx
@@ -1,73 +1,101 @@
-import { Fragment, KeyboardEvent, useState, useEffect } from 'react';
-import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
+import { Fragment, KeyboardEvent, useEffect, useState } from 'react';
+import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
+import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
-// Redux
-import { connect } from 'react-redux';
-import { pinApp, deleteApp, reorderApps, updateConfig, createNotification } from '../../../store/actions';
-
-// Typescript
-import { App, GlobalState, NewNotification } from '../../../interfaces';
-
-// CSS
-import classes from './AppTable.module.css';
-
-// UI
+import { App, Category, NewNotification } from '../../../interfaces';
+import {
+ createNotification,
+ deleteApp,
+ deleteAppCategory,
+ pinApp,
+ pinAppCategory,
+ reorderAppCategories,
+ reorderApps,
+ updateConfig,
+} from '../../../store/actions';
+import { searchConfig } from '../../../utility';
import Icon from '../../UI/Icons/Icon/Icon';
import Table from '../../UI/Table/Table';
-
-// Utils
-import { searchConfig } from '../../../utility';
+import { ContentType } from '../Apps';
+import classes from './AppTable.module.css';
interface ComponentProps {
+ contentType: ContentType;
+ categories: Category[];
apps: App[];
+ pinAppCategory: (category: Category) => void;
+ deleteAppCategory: (id: number) => void;
+ reorderAppCategories: (categories: Category[]) => void;
+ updateHandler: (data: Category | App) => void;
pinApp: (app: App) => void;
deleteApp: (id: number) => void;
- updateAppHandler: (app: App) => void;
reorderApps: (apps: App[]) => void;
updateConfig: (formData: any) => void;
createNotification: (notification: NewNotification) => void;
}
const AppTable = (props: ComponentProps): JSX.Element => {
+ const [localCategories, setLocalCategories] = useState([]);
const [localApps, setLocalApps] = useState([]);
const [isCustomOrder, setIsCustomOrder] = useState(false);
+ // Copy categories array
+ useEffect(() => {
+ setLocalCategories([...props.categories]);
+ }, [props.categories]);
+
// Copy apps array
useEffect(() => {
setLocalApps([...props.apps]);
- }, [props.apps])
+ }, [props.apps]);
// Check ordering
useEffect(() => {
- const order = searchConfig('useOrdering', '');
+ const order = searchConfig("useOrdering", "");
- if (order === 'orderId') {
+ if (order === "orderId") {
setIsCustomOrder(true);
}
- }, [])
+ }, []);
+
+ const deleteCategoryHandler = (category: Category): void => {
+ const proceed = window.confirm(
+ `Are you sure you want to delete ${category.name}? It will delete ALL assigned apps`
+ );
+
+ if (proceed) {
+ props.deleteAppCategory(category.id);
+ }
+ };
const deleteAppHandler = (app: App): void => {
- const proceed = window.confirm(`Are you sure you want to delete ${app.name} at ${app.url} ?`);
+ const proceed = window.confirm(
+ `Are you sure you want to delete ${app.name} at ${app.url} ?`
+ );
if (proceed) {
props.deleteApp(app.id);
}
- }
+ };
// Support keyboard navigation for actions
- const keyboardActionHandler = (e: KeyboardEvent, app: App, handler: Function) => {
- if (e.key === 'Enter') {
- handler(app);
+ const keyboardActionHandler = (
+ e: KeyboardEvent,
+ object: any,
+ handler: Function
+ ) => {
+ if (e.key === "Enter") {
+ handler(object);
}
- }
+ };
- const dragEndHanlder = (result: DropResult): void => {
+ const dragEndHandler = (result: DropResult): void => {
if (!isCustomOrder) {
props.createNotification({
- title: 'Error',
- message: 'Custom order is disabled'
- })
+ title: "Error",
+ message: "Custom order is disabled",
+ });
return;
}
@@ -75,106 +103,251 @@ const AppTable = (props: ComponentProps): JSX.Element => {
return;
}
- const tmpApps = [...localApps];
- const [movedApp] = tmpApps.splice(result.source.index, 1);
- tmpApps.splice(result.destination.index, 0, movedApp);
+ if (props.contentType === ContentType.app) {
+ const tmpApps = [...localApps];
+ const [movedApp] = tmpApps.splice(result.source.index, 1);
+ tmpApps.splice(result.destination.index, 0, movedApp);
- setLocalApps(tmpApps);
- props.reorderApps(tmpApps);
- }
+ setLocalApps(tmpApps);
+ props.reorderApps(tmpApps);
+ } else if (props.contentType === ContentType.category) {
+ const tmpCategories = [...localCategories];
+ const [movedCategory] = tmpCategories.splice(result.source.index, 1);
+ tmpCategories.splice(result.destination.index, 0, movedCategory);
- return (
-
-
- {isCustomOrder
- ?
You can drag and drop single rows to reorder application
- :
Custom order is disabled. You can change it in settings
- }
-
-
-
- {(provided) => (
-
- {localApps.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 (
-
- {app.name}
- {app.url}
- {app.icon}
- {!snapshot.isDragging && (
-
- 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
- ?
- :
- }
-
-
- )}
-
- )
- }}
-
- )
- })}
-
+ setLocalCategories(tmpCategories);
+ props.reorderAppCategories(tmpCategories);
+ }
+ };
+
+ if (props.contentType === ContentType.category) {
+ return (
+
+
+ {isCustomOrder ? (
+
You can drag and drop single rows to reorder categories
+ ) : (
+
+ Custom order is disabled. You can change it in{" "}
+ settings
+
)}
-
-
-
- )
-}
+
+
+
+ {(provided) => (
+
+ {localCategories.map(
+ (category: Category, index): JSX.Element => {
+ return (
+
+ {(provided, snapshot) => {
+ const style = {
+ border: snapshot.isDragging
+ ? "1px solid var(--color-accent)"
+ : "none",
+ borderRadius: "4px",
+ ...provided.draggableProps.style,
+ };
-const mapStateToProps = (state: GlobalState) => {
- return {
- apps: state.app.apps
+ return (
+
+ {category.name}
+ {!snapshot.isDragging && (
+
+
+ deleteCategoryHandler(category)
+ }
+ onKeyDown={(e) =>
+ keyboardActionHandler(
+ e,
+ category,
+ deleteCategoryHandler
+ )
+ }
+ tabIndex={0}
+ >
+
+
+
+ props.updateHandler(category)
+ }
+ tabIndex={0}
+ >
+
+
+ props.pinAppCategory(category)}
+ onKeyDown={(e) =>
+ keyboardActionHandler(
+ e,
+ category,
+ props.pinAppCategory
+ )
+ }
+ tabIndex={0}
+ >
+ {category.isPinned ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+
+ );
+ }}
+
+ );
+ }
+ )}
+
+ )}
+
+
+
+ );
+ } else {
+ return (
+
+
+ {isCustomOrder ? (
+
You can drag and drop single rows to reorder application
+ ) : (
+
+ Custom order is disabled. You can change it in{" "}
+ settings
+
+ )}
+
+
+
+ {(provided) => (
+
+ {localApps.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,
+ };
+
+ const category = localCategories.find((category: Category) => category.id === app.categoryId);
+ const categoryName = category?.name;
+
+ return (
+
+ {app.name}
+ {app.url}
+ {app.icon}
+ {categoryName}
+ {!snapshot.isDragging && (
+
+ deleteAppHandler(app)}
+ onKeyDown={(e) =>
+ keyboardActionHandler(
+ e,
+ app,
+ deleteAppHandler
+ )
+ }
+ tabIndex={0}
+ >
+
+
+ props.updateHandler(app)}
+ onKeyDown={(e) =>
+ keyboardActionHandler(
+ e,
+ app,
+ props.updateHandler
+ )
+ }
+ tabIndex={0}
+ >
+
+
+ props.pinApp(app)}
+ onKeyDown={(e) =>
+ keyboardActionHandler(e, app, props.pinApp)
+ }
+ tabIndex={0}
+ >
+ {app.isPinned ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+
+ );
+ }}
+
+ );
+ })}
+
+ )}
+
+
+
+ );
}
-}
+};
const actions = {
+ pinAppCategory,
+ deleteAppCategory,
+ reorderAppCategories,
pinApp,
deleteApp,
reorderApps,
updateConfig,
- createNotification
-}
+ createNotification,
+};
-export default connect(mapStateToProps, actions)(AppTable);
\ No newline at end of file
+export default connect(null, actions)(AppTable);
diff --git a/client/src/components/Apps/Apps.tsx b/client/src/components/Apps/Apps.tsx
index 751a196..f2049e4 100644
--- a/client/src/components/Apps/Apps.tsx
+++ b/client/src/components/Apps/Apps.tsx
@@ -1,45 +1,63 @@
import { useEffect, useState } from 'react';
+import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
-// Redux
-import { connect } from 'react-redux';
-import { getApps } from '../../store/actions';
-
-// Typescript
-import { App, GlobalState } from '../../interfaces';
-
-// CSS
-import classes from './Apps.module.css';
-
-// UI
-import { Container } from '../UI/Layout/Layout';
-import Headline from '../UI/Headlines/Headline/Headline';
-import Spinner from '../UI/Spinner/Spinner';
+import { App, Category, GlobalState } from '../../interfaces';
+import { getAppCategories, getApps } from '../../store/actions';
import ActionButton from '../UI/Buttons/ActionButton/ActionButton';
+import Headline from '../UI/Headlines/Headline/Headline';
+import { Container } from '../UI/Layout/Layout';
import Modal from '../UI/Modal/Modal';
-
-// Subcomponents
-import AppGrid from './AppGrid/AppGrid';
+import Spinner from '../UI/Spinner/Spinner';
import AppForm from './AppForm/AppForm';
+import AppGrid from './AppGrid/AppGrid';
+import classes from './Apps.module.css';
import AppTable from './AppTable/AppTable';
-interface ComponentProps {
- getApps: Function;
- apps: App[];
+interface ComponentProps {
loading: boolean;
+ categories: Category[];
+ getAppCategories: () => void;
+ apps: App[];
+ getApps: () => void;
searching: boolean;
}
+export enum ContentType {
+ category,
+ app
+}
+
const Apps = (props: ComponentProps): JSX.Element => {
- const { getApps, apps, loading, searching = false } = props;
+ const {
+ apps,
+ getApps,
+ getAppCategories,
+ categories,
+ loading,
+ searching = false
+ } = props;
const [modalIsOpen, setModalIsOpen] = useState(false);
+ const [formContentType, setFormContentType] = useState(ContentType.category);
const [isInEdit, setIsInEdit] = useState(false);
+ const [tableContentType, setTableContentType] = useState(ContentType.category);
const [isInUpdate, setIsInUpdate] = useState(false);
+ const [categoryInUpdate, setCategoryInUpdate] = useState({
+ name: "",
+ id: -1,
+ isPinned: false,
+ orderId: 0,
+ type: "apps",
+ bookmarks: [],
+ createdAt: new Date(),
+ updatedAt: new Date()
+ })
const [appInUpdate, setAppInUpdate] = useState({
- name: 'string',
- url: 'string',
- icon: 'string',
+ name: "string",
+ url: "string",
+ categoryId: -1,
+ icon: "string",
isPinned: false,
orderId: 0,
id: 0,
@@ -53,60 +71,93 @@ const Apps = (props: ComponentProps): JSX.Element => {
}
}, [getApps]);
+ useEffect(() => {
+ if (categories.length === 0) {
+ getAppCategories();
+ }
+ }, [getAppCategories])
+
const toggleModal = (): void => {
setModalIsOpen(!modalIsOpen);
- setIsInUpdate(false);
- };
+ // setIsInUpdate(false);
+ }
- const toggleEdit = (): void => {
- setIsInEdit(!isInEdit);
+ const addActionHandler = (contentType: ContentType) => {
+ setFormContentType(contentType);
setIsInUpdate(false);
- };
+ toggleModal();
+ }
- const toggleUpdate = (app: App): void => {
- setAppInUpdate(app);
+ const editActionHandler = (contentType: ContentType) => {
+ // We"re in the edit mode and the same button was clicked - go back to list
+ if (isInEdit && contentType === tableContentType) {
+ setIsInEdit(false);
+ } else {
+ setIsInEdit(true);
+ setTableContentType(contentType);
+ }
+ }
+
+ const instanceOfCategory = (object: any): object is Category => {
+ return "apps" in object;
+ }
+
+ const goToUpdateMode = (data: Category | App): void => {
setIsInUpdate(true);
- setModalIsOpen(true);
- };
+ if (instanceOfCategory(data)) {
+ setFormContentType(ContentType.category);
+ setCategoryInUpdate(data);
+ } else {
+ setFormContentType(ContentType.app);
+ setAppInUpdate(data);
+ }
+ toggleModal();
+ }
return (
-
+
{!isInUpdate ? (
-
+
) : (
-
+ formContentType === ContentType.category ? (
+
+ ) : (
+
+ )
)}
Go back}
+ subtitle={( Go back)}
/>
-
-
+
addActionHandler(ContentType.category)} />
+ addActionHandler(ContentType.app)} />
+ editActionHandler(ContentType.category)} />
+ editActionHandler(ContentType.app)} />
-
- {loading ? (
-
- ) : !isInEdit ? (
-
- ) : (
-
- )}
-
+ {loading ? (
+
+ ) : (!isInEdit ? (
+
+ ) : (
+
+ )
+ )}
);
};
const mapStateToProps = (state: GlobalState) => {
return {
- apps: state.app.apps,
loading: state.app.loading,
- };
-};
+ categories: state.app.categories,
+ apps: state.app.apps,
+ }
+}
-export default connect(mapStateToProps, { getApps })(Apps);
+export default connect(mapStateToProps, { getApps, getAppCategories })(Apps);
diff --git a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx
index 5162c89..2cc51c6 100644
--- a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx
+++ b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx
@@ -1,40 +1,19 @@
-// React
-import {
- useState,
- SyntheticEvent,
- Fragment,
- ChangeEvent,
- useEffect,
-} from 'react';
-
-// Redux
+import { ChangeEvent, Dispatch, Fragment, SetStateAction, SyntheticEvent, useEffect, useState } from 'react';
import { connect } from 'react-redux';
+
+import { Bookmark, Category, GlobalState, NewBookmark, NewCategory, NewNotification } from '../../../interfaces';
import {
- getCategories,
- addCategory,
addBookmark,
- updateCategory,
- updateBookmark,
+ addBookmarkCategory,
createNotification,
+ getBookmarkCategories,
+ updateBookmark,
+ updateBookmarkCategory,
} from '../../../store/actions';
-
-// Typescript
-import {
- Bookmark,
- Category,
- GlobalState,
- NewBookmark,
- NewCategory,
- NewNotification,
-} from '../../../interfaces';
-import { ContentType } from '../Bookmarks';
-
-// UI
-import ModalForm from '../../UI/Forms/ModalForm/ModalForm';
-import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
import Button from '../../UI/Buttons/Button/Button';
-
-// CSS
+import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
+import ModalForm from '../../UI/Forms/ModalForm/ModalForm';
+import { ContentType } from '../Bookmarks';
import classes from './BookmarkForm.module.css';
interface ComponentProps {
@@ -43,28 +22,28 @@ interface ComponentProps {
categories: Category[];
category?: Category;
bookmark?: Bookmark;
- addCategory: (formData: NewCategory) => void;
+ addBookmarkCategory: (formData: NewCategory) => void;
addBookmark: (formData: NewBookmark | FormData) => void;
- updateCategory: (id: number, formData: NewCategory) => void;
+ updateBookmarkCategory: (id: number, formData: NewCategory) => void;
updateBookmark: (
- id: number,
- formData: NewBookmark | FormData,
+ id: number,
+ formData: NewBookmark | FormData,
category: {
- prev: number;
- curr: number;
- }
- ) => void;
+ prev: number,
+ curr: number
+ }) => void;
createNotification: (notification: NewNotification) => void;
}
const BookmarkForm = (props: ComponentProps): JSX.Element => {
- const [useCustomIcon, toggleUseCustomIcon] = useState(false);
+ const [useCustomIcon, setUseCustomIcon] = useState(false);
const [customIcon, setCustomIcon] = useState(null);
- const [categoryName, setCategoryName] = useState({
+ const [categoryData, setCategoryData] = useState({
name: '',
- });
+ type: 'bookmarks'
+ })
- const [formData, setFormData] = useState({
+ const [bookmarkData, setBookmarkData] = useState({
name: '',
url: '',
categoryId: -1,
@@ -74,23 +53,23 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
// Load category data if provided for editing
useEffect(() => {
if (props.category) {
- setCategoryName({ name: props.category.name });
+ setCategoryData({ name: props.category.name, type: props.category.type });
} else {
- setCategoryName({ name: '' });
+ setCategoryData({ name: '', type: "bookmarks" });
}
}, [props.category]);
// Load bookmark data if provided for editing
useEffect(() => {
if (props.bookmark) {
- setFormData({
+ setBookmarkData({
name: props.bookmark.name,
url: props.bookmark.url,
categoryId: props.bookmark.categoryId,
icon: props.bookmark.icon,
});
} else {
- setFormData({
+ setBookmarkData({
name: '',
url: '',
categoryId: -1,
@@ -107,9 +86,9 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
if (customIcon) {
data.append('icon', customIcon);
}
- data.append('name', formData.name);
- data.append('url', formData.url);
- data.append('categoryId', `${formData.categoryId}`);
+ data.append('name', bookmarkData.name);
+ data.append('url', bookmarkData.url);
+ data.append('categoryId', `${bookmarkData.categoryId}`);
return data;
};
@@ -118,11 +97,11 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
// Add new
if (props.contentType === ContentType.category) {
// Add category
- props.addCategory(categoryName);
- setCategoryName({ name: '' });
+ props.addBookmarkCategory(categoryData);
+ setCategoryData({ name: '', type: 'bookmarks' });
} else if (props.contentType === ContentType.bookmark) {
// Add bookmark
- if (formData.categoryId === -1) {
+ if (bookmarkData.categoryId === -1) {
props.createNotification({
title: 'Error',
message: 'Please select category',
@@ -134,14 +113,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
const data = createFormData();
props.addBookmark(data);
} else {
- props.addBookmark(formData);
+ props.addBookmark(bookmarkData);
}
-
- setFormData({
+
+ setBookmarkData({
name: '',
url: '',
- categoryId: formData.categoryId,
- icon: '',
+ categoryId: bookmarkData.categoryId,
+ icon: ''
});
// setCustomIcon(null);
@@ -150,24 +129,32 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
// Update
if (props.contentType === ContentType.category && props.category) {
// Update category
- props.updateCategory(props.category.id, categoryName);
- setCategoryName({ name: '' });
+ props.updateBookmarkCategory(props.category.id, categoryData);
+ setCategoryData({ name: '', type: 'bookmarks' });
} else if (props.contentType === ContentType.bookmark && props.bookmark) {
// Update bookmark
if (customIcon) {
const data = createFormData();
- props.updateBookmark(props.bookmark.id, data, {
- prev: props.bookmark.categoryId,
- curr: formData.categoryId,
- });
+ props.updateBookmark(
+ props.bookmark.id,
+ data,
+ {
+ prev: props.bookmark.categoryId,
+ curr: bookmarkData.categoryId
+ }
+ )
} else {
- props.updateBookmark(props.bookmark.id, formData, {
- prev: props.bookmark.categoryId,
- curr: formData.categoryId,
- });
+ props.updateBookmark(
+ props.bookmark.id,
+ bookmarkData,
+ {
+ prev: props.bookmark.categoryId,
+ curr: bookmarkData.categoryId
+ }
+ );
}
- setFormData({
+ setBookmarkData({
name: '',
url: '',
categoryId: -1,
@@ -181,18 +168,16 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
}
};
- const inputChangeHandler = (e: ChangeEvent): void => {
- setFormData({
- ...formData,
- [e.target.name]: e.target.value,
+ const inputChangeHandler = (e: ChangeEvent, setDataFunction: Dispatch>, data: any): void => {
+ setDataFunction({
+ ...data,
+ [e.target.name]: e.target.value
});
};
- const selectChangeHandler = (e: ChangeEvent): void => {
- setFormData({
- ...formData,
- categoryId: parseInt(e.target.value),
- });
+ const toggleUseCustomIcon = (): void => {
+ setUseCustomIcon(!useCustomIcon);
+ setCustomIcon(null);
};
const fileChangeHandler = (e: ChangeEvent): void => {
@@ -230,8 +215,8 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
id="categoryName"
placeholder="Social Media"
required
- value={categoryName.name}
- onChange={(e) => setCategoryName({ name: e.target.value })}
+ value={categoryData.name}
+ onChange={(e) => inputChangeHandler(e, setCategoryData, categoryData)}
/>
@@ -245,8 +230,8 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
id="name"
placeholder="Reddit"
required
- value={formData.name}
- onChange={(e) => inputChangeHandler(e)}
+ value={bookmarkData.name}
+ onChange={(e) => inputChangeHandler(e, setBookmarkData, bookmarkData)}
/>
@@ -257,8 +242,8 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
id="url"
placeholder="reddit.com"
required
- value={formData.url}
- onChange={(e) => inputChangeHandler(e)}
+ value={bookmarkData.url}
+ onChange={(e) => inputChangeHandler(e, setBookmarkData, bookmarkData)}
/>
{
name="categoryId"
id="categoryId"
required
- onChange={(e) => selectChangeHandler(e)}
- value={formData.categoryId}
+ value={bookmarkData.categoryId}
+ onChange={(e) => inputChangeHandler(e, setBookmarkData, bookmarkData)}
>
Select category
{props.categories.map((category: Category): JSX.Element => {
@@ -295,12 +280,12 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
Bookmark Icon (optional)
inputChangeHandler(e)}
+ type='text'
+ name='icon'
+ id='icon'
+ placeholder='book-open-outline'
+ value={bookmarkData.icon}
+ onChange={(e) => inputChangeHandler(e, setBookmarkData, bookmarkData)}
/>
Use icon name from MDI.
@@ -310,7 +295,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
toggleUseCustomIcon(!useCustomIcon)}
+ onClick={() => toggleUseCustomIcon()}
className={classes.Switch}
>
Switch to custom icon upload
@@ -328,10 +313,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
accept=".jpg,.jpeg,.png,.svg"
/>
{
- setCustomIcon(null);
- toggleUseCustomIcon(!useCustomIcon);
- }}
+ onClick={() => toggleUseCustomIcon()}
className={classes.Switch}
>
Switch to MDI
@@ -352,10 +334,10 @@ const mapStateToProps = (state: GlobalState) => {
};
const dispatchMap = {
- getCategories,
- addCategory,
+ getBookmarkCategories,
+ addBookmarkCategory,
addBookmark,
- updateCategory,
+ updateBookmarkCategory,
updateBookmark,
createNotification,
};
diff --git a/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx b/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx
index bf17c81..8a7db3b 100644
--- a/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx
+++ b/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx
@@ -1,10 +1,8 @@
import { Link } from 'react-router-dom';
-import classes from './BookmarkGrid.module.css';
-
import { Category } from '../../../interfaces';
-
import BookmarkCard from '../BookmarkCard/BookmarkCard';
+import classes from './BookmarkGrid.module.css';
interface ComponentProps {
categories: Category[];
@@ -37,7 +35,7 @@ const BookmarkGrid = (props: ComponentProps): JSX.Element => {
if (props.totalCategories) {
bookmarks = (
- There are no pinned categories. You can pin them from the{' '}
+ There are no pinned bookmark categories. You can pin them from the{' '}
/bookmarks menu
);
diff --git a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx
index 02779d5..577b33f 100644
--- a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx
+++ b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx
@@ -1,34 +1,31 @@
-import { KeyboardEvent, useState, useEffect, Fragment } from 'react';
-import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
+import { Fragment, KeyboardEvent, useEffect, useState } from 'react';
+import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
+import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
-// Redux
-import { connect } from 'react-redux';
-import { pinCategory, deleteCategory, deleteBookmark, createNotification, reorderCategories } from '../../../store/actions';
-
-// Typescript
import { Bookmark, Category, NewNotification } from '../../../interfaces';
-import { ContentType } from '../Bookmarks';
-
-// CSS
-import classes from './BookmarkTable.module.css';
-
-// UI
-import Table from '../../UI/Table/Table';
-import Icon from '../../UI/Icons/Icon/Icon';
-
-// Utils
+import {
+ createNotification,
+ deleteBookmark,
+ deleteBookmarkCategory,
+ pinBookmarkCategory,
+ reorderBookmarkCategories,
+} from '../../../store/actions';
import { searchConfig } from '../../../utility';
+import Icon from '../../UI/Icons/Icon/Icon';
+import Table from '../../UI/Table/Table';
+import { ContentType } from '../Bookmarks';
+import classes from './BookmarkTable.module.css';
interface ComponentProps {
contentType: ContentType;
categories: Category[];
- pinCategory: (category: Category) => void;
- deleteCategory: (id: number) => void;
+ pinBookmarkCategory: (category: Category) => void;
+ deleteBookmarkCategory: (id: number) => void;
updateHandler: (data: Category | Bookmark) => void;
deleteBookmark: (bookmarkId: number, categoryId: number) => void;
createNotification: (notification: NewNotification) => void;
- reorderCategories: (categories: Category[]) => void;
+ reorderBookmarkCategories: (categories: Category[]) => void;
}
const BookmarkTable = (props: ComponentProps): JSX.Element => {
@@ -38,45 +35,53 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
// Copy categories array
useEffect(() => {
setLocalCategories([...props.categories]);
- }, [props.categories])
+ }, [props.categories]);
// Check ordering
useEffect(() => {
- const order = searchConfig('useOrdering', '');
+ const order = searchConfig("useOrdering", "");
- if (order === 'orderId') {
+ if (order === "orderId") {
setIsCustomOrder(true);
}
- })
+ }, []);
const deleteCategoryHandler = (category: Category): void => {
- const proceed = window.confirm(`Are you sure you want to delete ${category.name}? It will delete ALL assigned bookmarks`);
+ const proceed = window.confirm(
+ `Are you sure you want to delete ${category.name}? It will delete ALL assigned bookmarks`
+ );
if (proceed) {
- props.deleteCategory(category.id);
+ props.deleteBookmarkCategory(category.id);
}
- }
+ };
const deleteBookmarkHandler = (bookmark: Bookmark): void => {
- const proceed = window.confirm(`Are you sure you want to delete ${bookmark.name}?`);
+ const proceed = window.confirm(
+ `Are you sure you want to delete ${bookmark.name}?`
+ );
if (proceed) {
props.deleteBookmark(bookmark.id, bookmark.categoryId);
}
- }
+ };
- const keyboardActionHandler = (e: KeyboardEvent, category: Category, handler: Function) => {
- if (e.key === 'Enter') {
+ const keyboardActionHandler = (
+ e: KeyboardEvent,
+ category: Category,
+ handler: Function
+ ) => {
+ if (e.key === "Enter") {
handler(category);
}
- }
+ };
- const dragEndHanlder = (result: DropResult): void => {
+ const dragEndHandler = (result: DropResult): void => {
if (!isCustomOrder) {
props.createNotification({
- title: 'Error',
- message: 'Custom order is disabled'
- })
+ title: "Error",
+ message: "Custom order is disabled",
+ });
return;
}
@@ -85,141 +90,170 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
}
const tmpCategories = [...localCategories];
- const [movedApp] = tmpCategories.splice(result.source.index, 1);
- tmpCategories.splice(result.destination.index, 0, movedApp);
+ const [movedCategory] = tmpCategories.splice(result.source.index, 1);
+ tmpCategories.splice(result.destination.index, 0, movedCategory);
setLocalCategories(tmpCategories);
- props.reorderCategories(tmpCategories);
- }
+ props.reorderBookmarkCategories(tmpCategories);
+ };
if (props.contentType === ContentType.category) {
return (
- {isCustomOrder
- ?
You can drag and drop single rows to reorder categories
- :
Custom order is disabled. You can change it in settings
- }
+ {isCustomOrder ? (
+
You can drag and drop single rows to reorder categories
+ ) : (
+
+ Custom order is disabled. You can change it in{" "}
+ settings
+
+ )}
-
-
+
+
{(provided) => (
-
- {localCategories.map((category: Category, index): JSX.Element => {
- return (
-
- {(provided, snapshot) => {
- const style = {
- border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none',
- borderRadius: '4px',
- ...provided.draggableProps.style,
- };
+
+ {localCategories.map(
+ (category: Category, index): JSX.Element => {
+ return (
+
+ {(provided, snapshot) => {
+ const style = {
+ border: snapshot.isDragging
+ ? "1px solid var(--color-accent)"
+ : "none",
+ borderRadius: "4px",
+ ...provided.draggableProps.style,
+ };
- return (
-
- {category.name}
- {!snapshot.isDragging && (
-
- deleteCategoryHandler(category)}
- onKeyDown={(e) => keyboardActionHandler(e, category, deleteCategoryHandler)}
- tabIndex={0}>
-
-
- props.updateHandler(category)}
- tabIndex={0}>
-
-
- props.pinCategory(category)}
- onKeyDown={(e) => keyboardActionHandler(e, category, props.pinCategory)}
- tabIndex={0}>
- {category.isPinned
- ?
- :
- }
-
-
- )}
-
- )
- }}
-
- )
- })}
+ return (
+
+ {category.name}
+ {!snapshot.isDragging && (
+
+
+ deleteCategoryHandler(category)
+ }
+ onKeyDown={(e) =>
+ keyboardActionHandler(
+ e,
+ category,
+ deleteCategoryHandler
+ )
+ }
+ tabIndex={0}
+ >
+
+
+
+ props.updateHandler(category)
+ }
+ tabIndex={0}
+ >
+
+
+ props.pinBookmarkCategory(category)}
+ onKeyDown={(e) =>
+ keyboardActionHandler(
+ e,
+ category,
+ props.pinBookmarkCategory
+ )
+ }
+ tabIndex={0}
+ >
+ {category.isPinned ? (
+
+ ) : (
+
+ )}
+
+
+ )}
+
+ );
+ }}
+
+ );
+ }
+ )}
)}
- )
+ );
} else {
- const bookmarks: {bookmark: Bookmark, categoryName: string}[] = [];
+ const bookmarks: { bookmark: Bookmark; categoryName: string }[] = [];
props.categories.forEach((category: Category) => {
category.bookmarks.forEach((bookmark: Bookmark) => {
bookmarks.push({
bookmark,
- categoryName: category.name
+ categoryName: category.name,
});
- })
- })
+ });
+ });
return (
-
- {bookmarks.map((bookmark: {bookmark: Bookmark, categoryName: string}) => {
- return (
-
- {bookmark.bookmark.name}
- {bookmark.bookmark.url}
- {bookmark.bookmark.icon}
- {bookmark.categoryName}
-
- deleteBookmarkHandler(bookmark.bookmark)}
- tabIndex={0}>
-
-
- props.updateHandler(bookmark.bookmark)}
- tabIndex={0}>
-
-
-
-
- )
- })}
+
+ {bookmarks.map(
+ (bookmark: { bookmark: Bookmark; categoryName: string }) => {
+ return (
+
+ {bookmark.bookmark.name}
+ {bookmark.bookmark.url}
+ {bookmark.bookmark.icon}
+ {bookmark.categoryName}
+
+ deleteBookmarkHandler(bookmark.bookmark)}
+ tabIndex={0}
+ >
+
+
+ props.updateHandler(bookmark.bookmark)}
+ tabIndex={0}
+ >
+
+
+
+
+ );
+ }
+ )}
- )
+ );
}
-}
+};
const actions = {
- pinCategory,
- deleteCategory,
+ pinBookmarkCategory,
+ deleteBookmarkCategory,
deleteBookmark,
createNotification,
- reorderCategories
-}
+ reorderBookmarkCategories,
+};
-export default connect(null, actions)(BookmarkTable);
\ No newline at end of file
+export default connect(null, actions)(BookmarkTable);
diff --git a/client/src/components/Bookmarks/Bookmarks.tsx b/client/src/components/Bookmarks/Bookmarks.tsx
index 88d9cdb..af658b7 100644
--- a/client/src/components/Bookmarks/Bookmarks.tsx
+++ b/client/src/components/Bookmarks/Bookmarks.tsx
@@ -1,25 +1,23 @@
import { useEffect, useState } from 'react';
-import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
-import { getCategories } from '../../store/actions';
+import { Link } from 'react-router-dom';
-import classes from './Bookmarks.module.css';
-
-import { Container } from '../UI/Layout/Layout';
-import Headline from '../UI/Headlines/Headline/Headline';
+import { Bookmark, Category, GlobalState } from '../../interfaces';
+import { getBookmarkCategories } from '../../store/actions';
import ActionButton from '../UI/Buttons/ActionButton/ActionButton';
-
-import BookmarkGrid from './BookmarkGrid/BookmarkGrid';
-import { Category, GlobalState, Bookmark } from '../../interfaces';
-import Spinner from '../UI/Spinner/Spinner';
+import Headline from '../UI/Headlines/Headline/Headline';
+import { Container } from '../UI/Layout/Layout';
import Modal from '../UI/Modal/Modal';
+import Spinner from '../UI/Spinner/Spinner';
import BookmarkForm from './BookmarkForm/BookmarkForm';
+import BookmarkGrid from './BookmarkGrid/BookmarkGrid';
+import classes from './Bookmarks.module.css';
import BookmarkTable from './BookmarkTable/BookmarkTable';
interface ComponentProps {
loading: boolean;
categories: Category[];
- getCategories: () => void;
+ getBookmarkCategories: () => void;
searching: boolean;
}
@@ -29,7 +27,7 @@ export enum ContentType {
}
const Bookmarks = (props: ComponentProps): JSX.Element => {
- const { getCategories, categories, loading, searching = false } = props;
+ const { getBookmarkCategories, categories, loading, searching = false } = props;
const [modalIsOpen, setModalIsOpen] = useState(false);
const [formContentType, setFormContentType] = useState(ContentType.category);
@@ -43,6 +41,7 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
id: -1,
isPinned: false,
orderId: 0,
+ type: 'bookmarks',
bookmarks: [],
createdAt: new Date(),
updatedAt: new Date(),
@@ -59,9 +58,9 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
useEffect(() => {
if (categories.length === 0) {
- getCategories();
+ getBookmarkCategories();
}
- }, [getCategories]);
+ }, [getBookmarkCategories]);
const toggleModal = (): void => {
setModalIsOpen(!modalIsOpen);
@@ -169,4 +168,4 @@ const mapStateToProps = (state: GlobalState) => {
};
};
-export default connect(mapStateToProps, { getCategories })(Bookmarks);
+export default connect(mapStateToProps, { getBookmarkCategories })(Bookmarks);
diff --git a/client/src/components/Home/Home.tsx b/client/src/components/Home/Home.tsx
index fd711aa..088e056 100644
--- a/client/src/components/Home/Home.tsx
+++ b/client/src/components/Home/Home.tsx
@@ -1,53 +1,44 @@
-import { useState, useEffect, Fragment } from 'react';
+import { Fragment, useEffect, useState } from 'react';
+import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
-// Redux
-import { connect } from 'react-redux';
-import { getApps, getCategories } from '../../store/actions';
-
-// Typescript
+import { App, Bookmark, Category } from '../../interfaces';
import { GlobalState } from '../../interfaces/GlobalState';
-import { App, Category } from '../../interfaces';
-
-// UI
-import Icon from '../UI/Icons/Icon/Icon';
-import { Container } from '../UI/Layout/Layout';
-import SectionHeadline from '../UI/Headlines/SectionHeadline/SectionHeadline';
-import Spinner from '../UI/Spinner/Spinner';
-
-// CSS
-import classes from './Home.module.css';
-
-// Components
+import { getAppCategories, getApps, getBookmarkCategories } from '../../store/actions';
+import { searchConfig } from '../../utility';
import AppGrid from '../Apps/AppGrid/AppGrid';
import BookmarkGrid from '../Bookmarks/BookmarkGrid/BookmarkGrid';
-import WeatherWidget from '../Widgets/WeatherWidget/WeatherWidget';
import SearchBar from '../SearchBar/SearchBar';
-
-// Functions
-import { greeter } from './functions/greeter';
+import SectionHeadline from '../UI/Headlines/SectionHeadline/SectionHeadline';
+import Icon from '../UI/Icons/Icon/Icon';
+import { Container } from '../UI/Layout/Layout';
+import Spinner from '../UI/Spinner/Spinner';
+import WeatherWidget from '../Widgets/WeatherWidget/WeatherWidget';
import { dateTime } from './functions/dateTime';
-
-// Utils
-import { searchConfig } from '../../utility';
+import { greeter } from './functions/greeter';
+import classes from './Home.module.css';
interface ComponentProps {
- getApps: Function;
- getCategories: Function;
+ getApps: () => void;
+ getAppCategories: () => void;
+ getBookmarkCategories: () => void;
appsLoading: boolean;
+ bookmarkCategoriesLoading: boolean;
+ appCategories: Category[];
apps: App[];
- categoriesLoading: boolean;
- categories: Category[];
+ bookmarkCategories: Category[];
}
const Home = (props: ComponentProps): JSX.Element => {
const {
+ getAppCategories,
getApps,
+ getBookmarkCategories,
+ appCategories,
apps,
+ bookmarkCategories,
appsLoading,
- getCategories,
- categories,
- categoriesLoading,
+ bookmarkCategoriesLoading,
} = props;
const [header, setHeader] = useState({
@@ -58,7 +49,14 @@ const Home = (props: ComponentProps): JSX.Element => {
// Local search query
const [localSearch, setLocalSearch] = useState(null);
- // Load applications
+ // Load app categories
+ useEffect(() => {
+ if (appCategories.length === 0) {
+ getAppCategories();
+ }
+ }, [getAppCategories]);
+
+ // Load apps
useEffect(() => {
if (apps.length === 0) {
getApps();
@@ -67,10 +65,10 @@ const Home = (props: ComponentProps): JSX.Element => {
// Load bookmark categories
useEffect(() => {
- if (categories.length === 0) {
- getCategories();
+ if (bookmarkCategories.length === 0) {
+ getBookmarkCategories();
}
- }, [getCategories]);
+ }, [getBookmarkCategories]);
// Refresh greeter and time
useEffect(() => {
@@ -90,13 +88,20 @@ const Home = (props: ComponentProps): JSX.Element => {
}, []);
// Search bookmarks
- const searchBookmarks = (query: string): Category[] => {
- const category = { ...categories[0] };
- category.name = 'Search Results';
- category.bookmarks = categories
- .map(({ bookmarks }) => bookmarks)
- .flat()
- .filter(({ name }) => new RegExp(query, 'i').test(name));
+ const searchBookmarks = (query: string, categoriesToSearch: Category[]): Category[] => {
+ const category: Category = {
+ name: "Search Results",
+ type: categoriesToSearch[0].type,
+ isPinned: true,
+ bookmarks: categoriesToSearch
+ .map((c: Category) => c.bookmarks)
+ .flat()
+ .filter((bookmark: Bookmark) => new RegExp(query, 'i').test(bookmark.name)),
+ id: 0,
+ orderId: 0,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ };
return [category];
};
@@ -131,14 +136,15 @@ const Home = (props: ComponentProps): JSX.Element => {
) : (
category.isPinned)}
apps={
!localSearch
- ? apps.filter(({ isPinned }) => isPinned)
- : apps.filter(({ name }) =>
- new RegExp(localSearch, 'i').test(name)
+ ? apps.filter((app: App) => app.isPinned)
+ : apps.filter((app: App) =>
+ new RegExp(localSearch, 'i').test(app.name)
)
}
- totalApps={apps.length}
+ totalCategories={appCategories.length}
searching={!!localSearch}
/>
)}
@@ -151,16 +157,16 @@ const Home = (props: ComponentProps): JSX.Element => {
{searchConfig('hideCategories', 0) !== 1 ? (
- {categoriesLoading ? (
+ {bookmarkCategoriesLoading ? (
) : (
isPinned)
- : searchBookmarks(localSearch)
+ ? bookmarkCategories.filter((category: Category) => category.isPinned)
+ : searchBookmarks(localSearch, bookmarkCategories)
}
- totalCategories={categories.length}
+ totalCategories={bookmarkCategories.length}
searching={!!localSearch}
/>
)}
@@ -178,11 +184,12 @@ const Home = (props: ComponentProps): JSX.Element => {
const mapStateToProps = (state: GlobalState) => {
return {
+ appCategories: state.app.categories,
appsLoading: state.app.loading,
apps: state.app.apps,
- categoriesLoading: state.bookmark.loading,
- categories: state.bookmark.categories,
- };
-};
+ bookmarkCategoriesLoading: state.bookmark.loading,
+ bookmarkCategories: state.bookmark.categories,
+ }
+}
-export default connect(mapStateToProps, { getApps, getCategories })(Home);
+export default connect(mapStateToProps, { getApps, getAppCategories, getBookmarkCategories })(Home);
diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx
index c3525f8..7d2e83b 100644
--- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx
+++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx
@@ -1,34 +1,25 @@
-import { useState, useEffect, ChangeEvent, FormEvent } from 'react';
-
-// Redux
+import { ChangeEvent, FormEvent, useEffect, useState } from 'react';
import { connect } from 'react-redux';
+
+import { GlobalState, NewNotification, SettingsForm } from '../../../interfaces';
import {
createNotification,
- updateConfig,
+ sortAppCategories,
sortApps,
- sortCategories,
+ sortBookmarkCategories,
+ updateConfig,
} from '../../../store/actions';
-
-// Typescript
-import {
- GlobalState,
- NewNotification,
- SettingsForm,
-} from '../../../interfaces';
-
-// UI
-import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
-import Button from '../../UI/Buttons/Button/Button';
-import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline';
-
-// Utils
import { searchConfig } from '../../../utility';
+import Button from '../../UI/Buttons/Button/Button';
+import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
+import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline';
interface ComponentProps {
createNotification: (notification: NewNotification) => void;
updateConfig: (formData: SettingsForm) => void;
+ sortAppCategories: () => void;
sortApps: () => void;
- sortCategories: () => void;
+ sortBookmarkCategories: () => void;
loading: boolean;
}
@@ -79,9 +70,10 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
// Update local page title
document.title = formData.customTitle;
- // Sort apps and categories with new settings
+ // Apply new sort settings
+ props.sortAppCategories();
props.sortApps();
- props.sortCategories();
+ props.sortBookmarkCategories();
};
// Input handler
@@ -293,7 +285,8 @@ const actions = {
createNotification,
updateConfig,
sortApps,
- sortCategories,
+ sortAppCategories,
+ sortBookmarkCategories
};
export default connect(mapStateToProps, actions)(OtherSettings);
diff --git a/client/src/interfaces/App.ts b/client/src/interfaces/App.ts
index e4314a5..16b1443 100644
--- a/client/src/interfaces/App.ts
+++ b/client/src/interfaces/App.ts
@@ -3,6 +3,7 @@ import { Model } from '.';
export interface App extends Model {
name: string;
url: string;
+ categoryId: number;
icon: string;
isPinned: boolean;
orderId: number;
@@ -11,5 +12,6 @@ export interface App extends Model {
export interface NewApp {
name: string;
url: string;
+ categoryId: number;
icon: string;
}
\ No newline at end of file
diff --git a/client/src/interfaces/Category.ts b/client/src/interfaces/Category.ts
index 0f9f8f9..0f4f734 100644
--- a/client/src/interfaces/Category.ts
+++ b/client/src/interfaces/Category.ts
@@ -1,7 +1,8 @@
-import { Model, Bookmark } from '.';
+import { Bookmark, Model } from '.';
export interface Category extends Model {
name: string;
+ type: string;
isPinned: boolean;
orderId: number;
bookmarks: Bookmark[];
@@ -9,4 +10,5 @@ export interface Category extends Model {
export interface NewCategory {
name: string;
+ type: string;
}
\ No newline at end of file
diff --git a/client/src/store/actions/actionTypes.ts b/client/src/store/actions/actionTypes.ts
index c670b2f..19a23be 100644
--- a/client/src/store/actions/actionTypes.ts
+++ b/client/src/store/actions/actionTypes.ts
@@ -1,44 +1,50 @@
import {
- // Theme
- SetThemeAction,
- // Apps
- GetAppsAction,
- PinAppAction,
AddAppAction,
- DeleteAppAction,
- UpdateAppAction,
- ReorderAppsAction,
- SortAppsAction,
- // Categories
- GetCategoriesAction,
- AddCategoryAction,
- PinCategoryAction,
- DeleteCategoryAction,
- UpdateCategoryAction,
- SortCategoriesAction,
- ReorderCategoriesAction,
- // Bookmarks
+ AddAppCategoryAction,
AddBookmarkAction,
- DeleteBookmarkAction,
- UpdateBookmarkAction,
- // Notifications
- CreateNotificationAction,
+ AddBookmarkCategoryAction,
ClearNotificationAction,
- // Config
+ CreateNotificationAction,
+ DeleteAppAction,
+ DeleteAppCategoryAction,
+ DeleteBookmarkAction,
+ DeleteBookmarkCategoryAction,
+ GetAppCategoriesAction,
+ GetAppsAction,
+ GetBookmarkCategoriesAction,
GetConfigAction,
+ PinAppAction,
+ PinAppCategoryAction,
+ PinBookmarkCategoryAction,
+ ReorderAppCategoriesAction,
+ ReorderAppsAction,
+ ReorderBookmarkCategoriesAction,
+ SetThemeAction,
+ SortAppCategoriesAction,
+ SortAppsAction,
+ SortBookmarkCategoriesAction,
+ UpdateAppAction,
+ UpdateAppCategoryAction,
+ UpdateBookmarkAction,
+ UpdateBookmarkCategoryAction,
UpdateConfigAction,
} from './';
-import {
- AddQueryAction,
- DeleteQueryAction,
- FetchQueriesAction,
- UpdateQueryAction,
-} from './config';
+import { AddQueryAction, DeleteQueryAction, FetchQueriesAction, UpdateQueryAction } from './config';
export enum ActionTypes {
// Theme
setTheme = 'SET_THEME',
- // Apps
+ // App categories
+ getAppCategories = 'GET_APP_CATEGORIES',
+ getAppCategoriesSuccess = 'GET_APP_CATEGORIES_SUCCESS',
+ getAppCategoriesError = 'GET_APP_CATEGORIES_ERROR',
+ addAppCategory = 'ADD_APP_CATEGORY',
+ pinAppCategory = 'PIN_APP_CATEGORY',
+ deleteAppCategory = 'DELETE_APP_CATEGORY',
+ updateAppCategory = 'UPDATE_APP_CATEGORY',
+ sortAppCategories = 'SORT_APP_CATEGORIES',
+ reorderAppCategories = 'REORDER_APP_CATEGORIES',
+ // Apps
getApps = 'GET_APPS',
getAppsSuccess = 'GET_APPS_SUCCESS',
getAppsError = 'GET_APPS_ERROR',
@@ -49,16 +55,16 @@ export enum ActionTypes {
updateApp = 'UPDATE_APP',
reorderApps = 'REORDER_APPS',
sortApps = 'SORT_APPS',
- // Categories
- getCategories = 'GET_CATEGORIES',
- getCategoriesSuccess = 'GET_CATEGORIES_SUCCESS',
- getCategoriesError = 'GET_CATEGORIES_ERROR',
- addCategory = 'ADD_CATEGORY',
- pinCategory = 'PIN_CATEGORY',
- deleteCategory = 'DELETE_CATEGORY',
- updateCategory = 'UPDATE_CATEGORY',
- sortCategories = 'SORT_CATEGORIES',
- reorderCategories = 'REORDER_CATEGORIES',
+ // Bookmark categories
+ getBookmarkCategories = 'GET_BOOKMARK_CATEGORIES',
+ getBookmarkCategoriesSuccess = 'GET_BOOKMARK_CATEGORIES_SUCCESS',
+ getBookmarkCategoriesError = 'GET_BOOKMARK_CATEGORIES_ERROR',
+ addBookmarkCategory = 'ADD_BOOKMARK_CATEGORY',
+ pinBookmarkCategory = 'PIN_BOOKMARK_CATEGORY',
+ deleteBookmarkCategory = 'DELETE_BOOKMARK_CATEGORY',
+ updateBookmarkCategory = 'UPDATE_BOOKMARK_CATEGORY',
+ sortBookmarkCategories = 'SORT_BOOKMARK_CATEGORIES',
+ reorderBookmarkCategories = 'REORDER_BOOKMARK_CATEGORIES',
// Bookmarks
addBookmark = 'ADD_BOOKMARK',
deleteBookmark = 'DELETE_BOOKMARK',
@@ -77,7 +83,15 @@ export enum ActionTypes {
export type Action =
// Theme
- | SetThemeAction
+ SetThemeAction
+ // App categories
+ | GetAppCategoriesAction
+ | AddAppCategoryAction
+ | PinAppCategoryAction
+ | DeleteAppCategoryAction
+ | UpdateAppCategoryAction
+ | SortAppCategoriesAction
+ | ReorderAppCategoriesAction
// Apps
| GetAppsAction
| PinAppAction
@@ -86,14 +100,14 @@ export type Action =
| UpdateAppAction
| ReorderAppsAction
| SortAppsAction
- // Categories
- | GetCategoriesAction
- | AddCategoryAction
- | PinCategoryAction
- | DeleteCategoryAction
- | UpdateCategoryAction
- | SortCategoriesAction
- | ReorderCategoriesAction
+ // Bookmark categories
+ | GetBookmarkCategoriesAction
+ | AddBookmarkCategoryAction
+ | PinBookmarkCategoryAction
+ | DeleteBookmarkCategoryAction
+ | UpdateBookmarkCategoryAction
+ | SortBookmarkCategoriesAction
+ | ReorderBookmarkCategoriesAction
// Bookmarks
| AddBookmarkAction
| DeleteBookmarkAction
diff --git a/client/src/store/actions/app.ts b/client/src/store/actions/app.ts
index 3a8e7d5..6b1ad31 100644
--- a/client/src/store/actions/app.ts
+++ b/client/src/store/actions/app.ts
@@ -1,32 +1,96 @@
import axios from 'axios';
import { Dispatch } from 'redux';
+
+import { ApiResponse, App, Category, Config, NewApp, NewCategory } from '../../interfaces';
import { ActionTypes } from './actionTypes';
-import { App, ApiResponse, NewApp, Config } from '../../interfaces';
import { CreateNotificationAction } from './notification';
export interface GetAppsAction {
- type: ActionTypes.getApps | ActionTypes.getAppsSuccess | ActionTypes.getAppsError;
+ type:
+ | ActionTypes.getApps
+ | ActionTypes.getAppsSuccess
+ | ActionTypes.getAppsError;
payload: T;
}
export const getApps = () => async (dispatch: Dispatch) => {
dispatch>({
type: ActionTypes.getApps,
- payload: undefined
+ payload: undefined,
});
try {
- const res = await axios.get>('/api/apps');
+ const res = await axios.get>("/api/apps");
dispatch>({
type: ActionTypes.getAppsSuccess,
- payload: res.data.data
- })
+ payload: res.data.data,
+ });
} catch (err) {
console.log(err);
}
+};
+
+export interface GetAppCategoriesAction {
+ type:
+ | ActionTypes.getAppCategories
+ | ActionTypes.getAppCategoriesSuccess
+ | ActionTypes.getAppCategoriesSuccess;
+ payload: T;
}
+export const getAppCategories = () => async (dispatch: Dispatch) => {
+ dispatch>({
+ type: ActionTypes.getAppCategories,
+ payload: undefined,
+ });
+
+ try {
+ const res = await axios.get>("/api/categories");
+
+ dispatch>({
+ type: ActionTypes.getAppCategoriesSuccess,
+ payload: res.data.data.filter(
+ (category: Category) => category.type === "apps"
+ ),
+ });
+ } catch (err) {
+ console.log(err);
+ }
+};
+
+export interface AddAppCategoryAction {
+ type: ActionTypes.addAppCategory;
+ payload: Category;
+}
+
+export const addAppCategory =
+ (formData: NewCategory) => async (dispatch: Dispatch) => {
+ try {
+ const res = await axios.post>(
+ "/api/categories",
+ formData
+ );
+
+ dispatch({
+ type: ActionTypes.createNotification,
+ payload: {
+ title: "Success",
+ message: `Category ${formData.name} created`,
+ },
+ });
+
+ dispatch({
+ type: ActionTypes.addAppCategory,
+ payload: res.data.data,
+ });
+
+ dispatch(sortAppCategories());
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
export interface PinAppAction {
type: ActionTypes.pinApp;
payload: App;
@@ -35,26 +99,30 @@ export interface PinAppAction {
export const pinApp = (app: App) => async (dispatch: Dispatch) => {
try {
const { id, isPinned, name } = app;
- const res = await axios.put>(`/api/apps/${id}`, { isPinned: !isPinned });
+ const res = await axios.put>(`/api/apps/${id}`, {
+ isPinned: !isPinned,
+ });
- const status = isPinned ? 'unpinned from Homescreen' : 'pinned to Homescreen';
+ const status = isPinned
+ ? "unpinned from Homescreen"
+ : "pinned to Homescreen";
dispatch({
type: ActionTypes.createNotification,
payload: {
- title: 'Success',
- message: `App ${name} ${status}`
- }
- })
+ title: "Success",
+ message: `App ${name} ${status}`,
+ },
+ });
dispatch({
type: ActionTypes.pinApp,
- payload: res.data.data
- })
+ payload: res.data.data,
+ });
} catch (err) {
console.log(err);
}
-}
+};
export interface AddAppAction {
type: ActionTypes.addAppSuccess;
@@ -63,31 +131,133 @@ export interface AddAppAction {
export const addApp = (formData: NewApp | FormData) => async (dispatch: Dispatch) => {
try {
- const res = await axios.post>('/api/apps', formData);
+ const res = await axios.post>("/api/apps", formData);
dispatch({
type: ActionTypes.createNotification,
payload: {
- title: 'Success',
- message: `App added`
- }
- })
+ title: "Success",
+ message: `App ${res.data.data.name} added`,
+ },
+ });
await dispatch({
type: ActionTypes.addAppSuccess,
- payload: res.data.data
- })
+ payload: res.data.data,
+ });
// Sort apps
- dispatch(sortApps())
+ dispatch(sortApps());
} catch (err) {
console.log(err);
}
+};
+
+/**
+ * PIN CATEGORY
+ */
+export interface PinAppCategoryAction {
+ type: ActionTypes.pinAppCategory;
+ payload: Category;
}
+export const pinAppCategory =
+ (category: Category) => async (dispatch: Dispatch) => {
+ try {
+ const { id, isPinned, name } = category;
+ const res = await axios.put>(
+ `/api/categories/${id}`,
+ { isPinned: !isPinned }
+ );
+
+ const status = isPinned
+ ? "unpinned from Homescreen"
+ : "pinned to Homescreen";
+
+ dispatch({
+ type: ActionTypes.createNotification,
+ payload: {
+ title: "Success",
+ message: `Category ${name} ${status}`,
+ },
+ });
+
+ dispatch({
+ type: ActionTypes.pinAppCategory,
+ payload: res.data.data,
+ });
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+/**
+ * DELETE CATEGORY
+ */
+export interface DeleteAppCategoryAction {
+ type: ActionTypes.deleteAppCategory;
+ payload: number;
+}
+
+export const deleteAppCategory = (id: number) => async (dispatch: Dispatch) => {
+ try {
+ await axios.delete>(`/api/categories/${id}`);
+
+ dispatch({
+ type: ActionTypes.createNotification,
+ payload: {
+ title: "Success",
+ message: `Category deleted`,
+ },
+ });
+
+ dispatch({
+ type: ActionTypes.deleteAppCategory,
+ payload: id,
+ });
+ } catch (err) {
+ console.log(err);
+ }
+};
+
+/**
+ * UPDATE CATEGORY
+ */
+export interface UpdateAppCategoryAction {
+ type: ActionTypes.updateAppCategory;
+ payload: Category;
+}
+
+export const updateAppCategory =
+ (id: number, formData: NewCategory) => async (dispatch: Dispatch) => {
+ try {
+ const res = await axios.put>(
+ `/api/categories/${id}`,
+ formData
+ );
+
+ dispatch({
+ type: ActionTypes.createNotification,
+ payload: {
+ title: "Success",
+ message: `Category ${formData.name} updated`,
+ },
+ });
+
+ dispatch({
+ type: ActionTypes.updateAppCategory,
+ payload: res.data.data,
+ });
+
+ dispatch(sortAppCategories());
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
export interface DeleteAppAction {
- type: ActionTypes.deleteApp,
- payload: number
+ type: ActionTypes.deleteApp;
+ payload: number;
}
export const deleteApp = (id: number) => async (dispatch: Dispatch) => {
@@ -97,80 +267,86 @@ export const deleteApp = (id: number) => async (dispatch: Dispatch) => {
dispatch({
type: ActionTypes.createNotification,
payload: {
- title: 'Success',
- message: 'App deleted'
- }
- })
+ title: "Success",
+ message: "App deleted",
+ },
+ });
dispatch({
type: ActionTypes.deleteApp,
- payload: id
- })
+ payload: id,
+ });
} catch (err) {
console.log(err);
}
-}
+};
export interface UpdateAppAction {
type: ActionTypes.updateApp;
payload: App;
}
-export const updateApp = (id: number, formData: NewApp | FormData) => async (dispatch: Dispatch) => {
- try {
- const res = await axios.put>(`/api/apps/${id}`, formData);
+export const updateApp = (id: number, formData: NewApp | FormData) => async (dispatch: Dispatch) => {
+ try {
+ const res = await axios.put>(
+ `/api/apps/${id}`,
+ formData
+ );
- dispatch({
- type: ActionTypes.createNotification,
- payload: {
- title: 'Success',
- message: `App updated`
- }
- })
+ dispatch({
+ type: ActionTypes.createNotification,
+ payload: {
+ title: "Success",
+ message: `App ${res.data.data.name} updated`,
+ },
+ });
- await dispatch({
- type: ActionTypes.updateApp,
- payload: res.data.data
- })
+ await dispatch({
+ type: ActionTypes.updateApp,
+ payload: res.data.data,
+ });
- // Sort apps
- dispatch(sortApps())
- } catch (err) {
- console.log(err);
- }
-}
+ // Sort apps
+ dispatch(sortApps());
+ } catch (err) {
+ console.log(err);
+ }
+ };
export interface ReorderAppsAction {
type: ActionTypes.reorderApps;
- payload: App[]
+ payload: App[];
}
-interface ReorderQuery {
+interface ReorderAppsQuery {
apps: {
id: number;
orderId: number;
- }[]
+ }[];
}
export const reorderApps = (apps: App[]) => async (dispatch: Dispatch) => {
try {
- const updateQuery: ReorderQuery = { apps: [] }
+ const updateQuery: ReorderAppsQuery = { apps: [] };
- apps.forEach((app, index) => updateQuery.apps.push({
- id: app.id,
- orderId: index + 1
- }))
+ apps.forEach((app, index) => {
+ updateQuery.apps.push({
+ id: app.id,
+ orderId: index + 1,
+ });
+ app.orderId = index + 1;
+ });
- await axios.put>('/api/apps/0/reorder', updateQuery);
+ await axios.put>("/api/apps/0/reorder", updateQuery);
dispatch({
type: ActionTypes.reorderApps,
- payload: apps
- })
+ payload: apps,
+ });
} catch (err) {
console.log(err);
}
-}
+};
export interface SortAppsAction {
type: ActionTypes.sortApps;
@@ -179,13 +355,75 @@ export interface SortAppsAction {
export const sortApps = () => async (dispatch: Dispatch) => {
try {
- const res = await axios.get>('/api/config/useOrdering');
+ const res = await axios.get>("/api/config/useOrdering");
dispatch({
type: ActionTypes.sortApps,
- payload: res.data.data.value
- })
+ payload: res.data.data.value,
+ });
} catch (err) {
console.log(err);
}
-}
\ No newline at end of file
+};
+
+/**
+ * SORT CATEGORIES
+ */
+export interface SortAppCategoriesAction {
+ type: ActionTypes.sortAppCategories;
+ payload: string;
+}
+
+export const sortAppCategories = () => async (dispatch: Dispatch) => {
+ try {
+ const res = await axios.get>("/api/config/useOrdering");
+
+ dispatch({
+ type: ActionTypes.sortAppCategories,
+ payload: res.data.data.value,
+ });
+ } catch (err) {
+ console.log(err);
+ }
+};
+
+/**
+ * REORDER CATEGORIES
+ */
+export interface ReorderAppCategoriesAction {
+ type: ActionTypes.reorderAppCategories;
+ payload: Category[];
+}
+
+interface ReorderCategoriesQuery {
+ categories: {
+ id: number;
+ orderId: number;
+ }[];
+}
+
+export const reorderAppCategories =
+ (categories: Category[]) => async (dispatch: Dispatch) => {
+ try {
+ const updateQuery: ReorderCategoriesQuery = { categories: [] };
+
+ categories.forEach((category, index) =>
+ updateQuery.categories.push({
+ id: category.id,
+ orderId: index + 1,
+ })
+ );
+
+ await axios.put>(
+ "/api/categories/0/reorder",
+ updateQuery
+ );
+
+ dispatch({
+ type: ActionTypes.reorderAppCategories,
+ payload: categories,
+ });
+ } catch (err) {
+ console.log(err);
+ }
+ };
diff --git a/client/src/store/actions/bookmark.ts b/client/src/store/actions/bookmark.ts
index b4b5831..028a97d 100644
--- a/client/src/store/actions/bookmark.ts
+++ b/client/src/store/actions/bookmark.ts
@@ -1,29 +1,30 @@
import axios from 'axios';
import { Dispatch } from 'redux';
+
+import { ApiResponse, Bookmark, Category, Config, NewBookmark, NewCategory } from '../../interfaces';
import { ActionTypes } from './actionTypes';
-import { Category, ApiResponse, NewCategory, Bookmark, NewBookmark, Config } from '../../interfaces';
import { CreateNotificationAction } from './notification';
/**
* GET CATEGORIES
*/
-export interface GetCategoriesAction {
- type: ActionTypes.getCategories | ActionTypes.getCategoriesSuccess | ActionTypes.getCategoriesError;
+export interface GetBookmarkCategoriesAction {
+ type: ActionTypes.getBookmarkCategories | ActionTypes.getBookmarkCategoriesSuccess | ActionTypes.getBookmarkCategoriesError;
payload: T;
}
-export const getCategories = () => async (dispatch: Dispatch) => {
- dispatch>({
- type: ActionTypes.getCategories,
+export const getBookmarkCategories = () => async (dispatch: Dispatch) => {
+ dispatch>({
+ type: ActionTypes.getBookmarkCategories,
payload: undefined
})
try {
const res = await axios.get>('/api/categories');
- dispatch>({
- type: ActionTypes.getCategoriesSuccess,
- payload: res.data.data
+ dispatch>({
+ type: ActionTypes.getBookmarkCategoriesSuccess,
+ payload: res.data.data.filter((category: Category) => category.type === 'bookmarks'),
})
} catch (err) {
console.log(err);
@@ -33,12 +34,12 @@ export const getCategories = () => async (dispatch: Dispatch) => {
/**
* ADD CATEGORY
*/
-export interface AddCategoryAction {
- type: ActionTypes.addCategory,
+export interface AddBookmarkCategoryAction {
+ type: ActionTypes.addBookmarkCategory,
payload: Category
}
-export const addCategory = (formData: NewCategory) => async (dispatch: Dispatch) => {
+export const addBookmarkCategory = (formData: NewCategory) => async (dispatch: Dispatch) => {
try {
const res = await axios.post>('/api/categories', formData);
@@ -50,12 +51,12 @@ export const addCategory = (formData: NewCategory) => async (dispatch: Dispatch)
}
})
- dispatch({
- type: ActionTypes.addCategory,
+ dispatch({
+ type: ActionTypes.addBookmarkCategory,
payload: res.data.data
})
- dispatch(sortCategories());
+ dispatch(sortBookmarkCategories());
} catch (err) {
console.log(err);
}
@@ -93,12 +94,12 @@ export const addBookmark = (formData: NewBookmark | FormData) => async (dispatch
/**
* PIN CATEGORY
*/
-export interface PinCategoryAction {
- type: ActionTypes.pinCategory,
+export interface PinBookmarkCategoryAction {
+ type: ActionTypes.pinBookmarkCategory,
payload: Category
}
-export const pinCategory = (category: Category) => async (dispatch: Dispatch) => {
+export const pinBookmarkCategory = (category: Category) => async (dispatch: Dispatch) => {
try {
const { id, isPinned, name } = category;
const res = await axios.put>(`/api/categories/${id}`, { isPinned: !isPinned });
@@ -113,8 +114,8 @@ export const pinCategory = (category: Category) => async (dispatch: Dispatch) =>
}
})
- dispatch({
- type: ActionTypes.pinCategory,
+ dispatch({
+ type: ActionTypes.pinBookmarkCategory,
payload: res.data.data
})
} catch (err) {
@@ -125,12 +126,12 @@ export const pinCategory = (category: Category) => async (dispatch: Dispatch) =>
/**
* DELETE CATEGORY
*/
-export interface DeleteCategoryAction {
- type: ActionTypes.deleteCategory,
+export interface DeleteBookmarkCategoryAction {
+ type: ActionTypes.deleteBookmarkCategory,
payload: number
}
-export const deleteCategory = (id: number) => async (dispatch: Dispatch) => {
+export const deleteBookmarkCategory = (id: number) => async (dispatch: Dispatch) => {
try {
await axios.delete>(`/api/categories/${id}`);
@@ -142,8 +143,8 @@ export const deleteCategory = (id: number) => async (dispatch: Dispatch) => {
}
})
- dispatch({
- type: ActionTypes.deleteCategory,
+ dispatch({
+ type: ActionTypes.deleteBookmarkCategory,
payload: id
})
} catch (err) {
@@ -154,12 +155,12 @@ export const deleteCategory = (id: number) => async (dispatch: Dispatch) => {
/**
* UPDATE CATEGORY
*/
-export interface UpdateCategoryAction {
- type: ActionTypes.updateCategory,
+export interface UpdateBookmarkCategoryAction {
+ type: ActionTypes.updateBookmarkCategory,
payload: Category
}
-export const updateCategory = (id: number, formData: NewCategory) => async (dispatch: Dispatch) => {
+export const updateBookmarkCategory = (id: number, formData: NewCategory) => async (dispatch: Dispatch) => {
try {
const res = await axios.put>(`/api/categories/${id}`, formData);
@@ -171,12 +172,12 @@ export const updateCategory = (id: number, formData: NewCategory) => async (disp
}
})
- dispatch({
- type: ActionTypes.updateCategory,
+ dispatch({
+ type: ActionTypes.updateBookmarkCategory,
payload: res.data.data
})
- dispatch(sortCategories());
+ dispatch(sortBookmarkCategories());
} catch (err) {
console.log(err);
}
@@ -277,17 +278,17 @@ export const updateBookmark = (
/**
* SORT CATEGORIES
*/
-export interface SortCategoriesAction {
- type: ActionTypes.sortCategories;
+export interface SortBookmarkCategoriesAction {
+ type: ActionTypes.sortBookmarkCategories;
payload: string;
}
-export const sortCategories = () => async (dispatch: Dispatch) => {
+export const sortBookmarkCategories = () => async (dispatch: Dispatch) => {
try {
const res = await axios.get>('/api/config/useOrdering');
- dispatch({
- type: ActionTypes.sortCategories,
+ dispatch({
+ type: ActionTypes.sortBookmarkCategories,
payload: res.data.data.value
})
} catch (err) {
@@ -298,8 +299,8 @@ export const sortCategories = () => async (dispatch: Dispatch) => {
/**
* REORDER CATEGORIES
*/
-export interface ReorderCategoriesAction {
- type: ActionTypes.reorderCategories;
+export interface ReorderBookmarkCategoriesAction {
+ type: ActionTypes.reorderBookmarkCategories;
payload: Category[];
}
@@ -310,7 +311,7 @@ interface ReorderQuery {
}[]
}
-export const reorderCategories = (categories: Category[]) => async (dispatch: Dispatch) => {
+export const reorderBookmarkCategories = (categories: Category[]) => async (dispatch: Dispatch) => {
try {
const updateQuery: ReorderQuery = { categories: [] }
@@ -321,8 +322,8 @@ export const reorderCategories = (categories: Category[]) => async (dispatch: Di
await axios.put>('/api/categories/0/reorder', updateQuery);
- dispatch({
- type: ActionTypes.reorderCategories,
+ dispatch({
+ type: ActionTypes.reorderBookmarkCategories,
payload: categories
})
} catch (err) {
diff --git a/client/src/store/reducers/app.ts b/client/src/store/reducers/app.ts
index 0935819..71164e0 100644
--- a/client/src/store/reducers/app.ts
+++ b/client/src/store/reducers/app.ts
@@ -1,72 +1,152 @@
-import { ActionTypes, Action } from '../actions';
-import { App } from '../../interfaces/App';
+import { App, Category } from '../../interfaces';
import { sortData } from '../../utility';
+import { Action, ActionTypes } from '../actions';
export interface State {
loading: boolean;
- apps: App[];
errors: string | undefined;
+ categories: Category[];
+ apps: App[];
}
const initialState: State = {
loading: true,
+ errors: undefined,
+ categories: [],
apps: [],
- errors: undefined
-}
+};
const getApps = (state: State, action: Action): State => {
return {
...state,
loading: true,
- errors: undefined
- }
-}
+ errors: undefined,
+ };
+};
const getAppsSuccess = (state: State, action: Action): State => {
return {
...state,
loading: false,
- apps: action.payload
- }
-}
+ apps: action.payload,
+ };
+};
const getAppsError = (state: State, action: Action): State => {
return {
...state,
loading: false,
- errors: action.payload
+ errors: action.payload,
+ };
+};
+
+const getCategories = (state: State, action: Action): State => {
+ return {
+ ...state,
+ loading: true,
+ errors: undefined,
+ };
+};
+
+const getCategoriesSuccess = (state: State, action: Action): State => {
+ return {
+ ...state,
+ loading: false,
+ categories: action.payload,
+ };
+};
+
+const addCategory = (state: State, action: Action): State => {
+ return {
+ ...state,
+ categories: [
+ ...state.categories,
+ {
+ ...action.payload,
+ type: "apps",
+ apps: [],
+ },
+ ],
+ };
+};
+
+const pinCategory = (state: State, action: Action): State => {
+ const tmpCategories = [...state.categories];
+ const changedCategory = tmpCategories.find(
+ (category: Category) => category.id === action.payload.id
+ );
+
+ if (changedCategory) {
+ changedCategory.isPinned = action.payload.isPinned;
}
-}
+
+ return {
+ ...state,
+ categories: tmpCategories,
+ };
+};
const pinApp = (state: State, action: Action): State => {
const tmpApps = [...state.apps];
const changedApp = tmpApps.find((app: App) => app.id === action.payload.id);
-
+
if (changedApp) {
changedApp.isPinned = action.payload.isPinned;
}
-
+
return {
...state,
- apps: tmpApps
- }
-}
+ apps: tmpApps,
+ };
+};
const addAppSuccess = (state: State, action: Action): State => {
return {
...state,
- apps: [...state.apps, action.payload]
- }
-}
+ apps: [...state.apps, action.payload],
+ };
+};
-const deleteApp = (state: State, action: Action): State => {
- const tmpApps = [...state.apps].filter((app: App) => app.id !== action.payload);
+const deleteCategory = (state: State, action: Action): State => {
+ const categoryIndex = state.categories.findIndex(
+ (category: Category) => category.id === action.payload
+ );
return {
...state,
- apps: tmpApps
+ categories: [
+ ...state.categories.slice(0, categoryIndex),
+ ...state.categories.slice(categoryIndex + 1),
+ ],
+ };
+};
+
+const updateCategory = (state: State, action: Action): State => {
+ const tmpCategories = [...state.categories];
+ const categoryInUpdate = tmpCategories.find(
+ (category: Category) => category.id === action.payload.id
+ );
+
+ if (categoryInUpdate) {
+ categoryInUpdate.name = action.payload.name;
}
-}
+
+ return {
+ ...state,
+ categories: tmpCategories,
+ };
+};
+
+const deleteApp = (state: State, action: Action): State => {
+ const tmpApps = [...state.apps].filter(
+ (app: App) => app.id !== action.payload
+ );
+
+ return {
+ ...state,
+ apps: tmpApps,
+ };
+};
const updateApp = (state: State, action: Action): State => {
const tmpApps = [...state.apps];
@@ -76,43 +156,96 @@ const updateApp = (state: State, action: Action): State => {
appInUpdate.name = action.payload.name;
appInUpdate.url = action.payload.url;
appInUpdate.icon = action.payload.icon;
+ appInUpdate.categoryId = action.payload.categoryId;
}
return {
...state,
- apps: tmpApps
- }
-}
+ apps: tmpApps,
+ };
+};
+
+const sortAppCategories = (state: State, action: Action): State => {
+ const sortedCategories = sortData(state.categories, action.payload);
+
+ return {
+ ...state,
+ categories: sortedCategories,
+ };
+};
+
+const reorderCategories = (state: State, action: Action): State => {
+ return {
+ ...state,
+ categories: action.payload,
+ };
+};
const reorderApps = (state: State, action: Action): State => {
return {
...state,
- apps: action.payload
- }
-}
+ apps: action.payload,
+ };
+};
const sortApps = (state: State, action: Action): State => {
+ // const tmpCategories = [...state.categories];
+
+ // tmpCategories.forEach((category: Category) => {
+ // category.apps = sortData(category.apps, action.payload);
+ // });
+
+ // return {
+ // ...state,
+ // categories: tmpCategories,
+ // };
const sortedApps = sortData(state.apps, action.payload);
return {
...state,
- apps: sortedApps
- }
-}
+ apps: sortedApps,
+ };
+};
const appReducer = (state = initialState, action: Action) => {
switch (action.type) {
- case ActionTypes.getApps: return getApps(state, action);
- case ActionTypes.getAppsSuccess: return getAppsSuccess(state, action);
- case ActionTypes.getAppsError: return getAppsError(state, action);
- case ActionTypes.pinApp: return pinApp(state, action);
- case ActionTypes.addAppSuccess: return addAppSuccess(state, action);
- case ActionTypes.deleteApp: return deleteApp(state, action);
- case ActionTypes.updateApp: return updateApp(state, action);
- case ActionTypes.reorderApps: return reorderApps(state, action);
- case ActionTypes.sortApps: return sortApps(state, action);
- default: return state;
+ case ActionTypes.getAppCategories:
+ return getCategories(state, action);
+ case ActionTypes.getAppCategoriesSuccess:
+ return getCategoriesSuccess(state, action);
+ case ActionTypes.getApps:
+ return getApps(state, action);
+ case ActionTypes.getAppsSuccess:
+ return getAppsSuccess(state, action);
+ case ActionTypes.getAppsError:
+ return getAppsError(state, action);
+ case ActionTypes.addAppCategory:
+ return addCategory(state, action);
+ case ActionTypes.addAppSuccess:
+ return addAppSuccess(state, action);
+ case ActionTypes.pinAppCategory:
+ return pinCategory(state, action);
+ case ActionTypes.pinApp:
+ return pinApp(state, action);
+ case ActionTypes.deleteAppCategory:
+ return deleteCategory(state, action);
+ case ActionTypes.updateAppCategory:
+ return updateCategory(state, action);
+ case ActionTypes.deleteApp:
+ return deleteApp(state, action);
+ case ActionTypes.updateApp:
+ return updateApp(state, action);
+ case ActionTypes.sortAppCategories:
+ return sortAppCategories(state, action);
+ case ActionTypes.reorderAppCategories:
+ return reorderCategories(state, action);
+ case ActionTypes.sortApps:
+ return sortApps(state, action);
+ case ActionTypes.reorderApps:
+ return reorderApps(state, action);
+ default:
+ return state;
}
-}
+};
-export default appReducer;
\ No newline at end of file
+export default appReducer;
diff --git a/client/src/store/reducers/bookmark.ts b/client/src/store/reducers/bookmark.ts
index a554d6e..704df2b 100644
--- a/client/src/store/reducers/bookmark.ts
+++ b/client/src/store/reducers/bookmark.ts
@@ -1,6 +1,6 @@
-import { ActionTypes, Action } from '../actions';
-import { Category, Bookmark } from '../../interfaces';
+import { Bookmark, Category } from '../../interfaces';
import { sortData } from '../../utility';
+import { Action, ActionTypes } from '../actions';
export interface State {
loading: boolean;
@@ -37,6 +37,7 @@ const addCategory = (state: State, action: Action): State => {
...state.categories,
{
...action.payload,
+ type: 'bookmarks',
bookmarks: []
}
]
@@ -142,7 +143,7 @@ const updateBookmark = (state: State, action: Action): State => {
}
}
-const sortCategories = (state: State, action: Action): State => {
+const sortBookmarkCategories = (state: State, action: Action): State => {
const sortedCategories = sortData(state.categories, action.payload);
return {
@@ -160,17 +161,17 @@ const reorderCategories = (state: State, action: Action): State => {
const bookmarkReducer = (state = initialState, action: Action) => {
switch (action.type) {
- case ActionTypes.getCategories: return getCategories(state, action);
- case ActionTypes.getCategoriesSuccess: return getCategoriesSuccess(state, action);
- case ActionTypes.addCategory: return addCategory(state, action);
+ case ActionTypes.getBookmarkCategories: return getCategories(state, action);
+ case ActionTypes.getBookmarkCategoriesSuccess: return getCategoriesSuccess(state, action);
+ case ActionTypes.addBookmarkCategory: return addCategory(state, action);
case ActionTypes.addBookmark: return addBookmark(state, action);
- case ActionTypes.pinCategory: return pinCategory(state, action);
- case ActionTypes.deleteCategory: return deleteCategory(state, action);
- case ActionTypes.updateCategory: return updateCategory(state, action);
+ case ActionTypes.pinBookmarkCategory: return pinCategory(state, action);
+ case ActionTypes.deleteBookmarkCategory: return deleteCategory(state, action);
+ case ActionTypes.updateBookmarkCategory: return updateCategory(state, action);
case ActionTypes.deleteBookmark: return deleteBookmark(state, action);
case ActionTypes.updateBookmark: return updateBookmark(state, action);
- case ActionTypes.sortCategories: return sortCategories(state, action);
- case ActionTypes.reorderCategories: return reorderCategories(state, action);
+ case ActionTypes.sortBookmarkCategories: return sortBookmarkCategories(state, action);
+ case ActionTypes.reorderBookmarkCategories: return reorderCategories(state, action);
default: return state;
}
}
diff --git a/controllers/category.js b/controllers/category.js
index 0f1af58..39d208c 100644
--- a/controllers/category.js
+++ b/controllers/category.js
@@ -42,12 +42,16 @@ exports.getCategories = asyncWrapper(async (req, res, next) => {
where: { key: 'useOrdering' },
});
- const orderType = useOrdering ? useOrdering.value : 'createdAt';
+ const orderType = useOrdering ? useOrdering.value : "createdAt";
let categories;
- if (orderType == 'name') {
+ if (orderType == "name") {
categories = await Category.findAll({
include: [
+ {
+ model: App,
+ as: 'apps',
+ },
{
model: Bookmark,
as: 'bookmarks',
@@ -58,6 +62,10 @@ exports.getCategories = asyncWrapper(async (req, res, next) => {
} else {
categories = await Category.findAll({
include: [
+ {
+ model: App,
+ as: 'apps',
+ },
{
model: Bookmark,
as: 'bookmarks',
@@ -80,6 +88,10 @@ exports.getCategory = asyncWrapper(async (req, res, next) => {
const category = await Category.findOne({
where: { id: req.params.id },
include: [
+ {
+ model: App,
+ as: 'apps',
+ },
{
model: Bookmark,
as: 'bookmarks',
@@ -134,6 +146,10 @@ exports.deleteCategory = asyncWrapper(async (req, res, next) => {
const category = await Category.findOne({
where: { id: req.params.id },
include: [
+ {
+ model: App,
+ as: 'apps',
+ },
{
model: Bookmark,
as: 'bookmarks',
@@ -150,6 +166,12 @@ exports.deleteCategory = asyncWrapper(async (req, res, next) => {
);
}
+ category.apps.forEach(async (app) => {
+ await App.destroy({
+ where: { id: app.id },
+ });
+ });
+
category.bookmarks.forEach(async (bookmark) => {
await Bookmark.destroy({
where: { id: bookmark.id },
diff --git a/models/App.js b/models/App.js
index f521955..03f5dc5 100644
--- a/models/App.js
+++ b/models/App.js
@@ -10,6 +10,10 @@ const App = sequelize.define('App', {
type: DataTypes.STRING,
allowNull: false
},
+ categoryId: {
+ type: DataTypes.INTEGER,
+ allowNull: false
+ },
icon: {
type: DataTypes.STRING,
allowNull: false,
diff --git a/models/Category.js b/models/Category.js
index 9c9eda6..75bda95 100644
--- a/models/Category.js
+++ b/models/Category.js
@@ -6,6 +6,10 @@ const Category = sequelize.define('Category', {
type: DataTypes.STRING,
allowNull: false
},
+ type: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
isPinned: {
type: DataTypes.BOOLEAN,
defaultValue: false
diff --git a/models/associateModels.js b/models/associateModels.js
index d1b86c1..b3375cc 100644
--- a/models/associateModels.js
+++ b/models/associateModels.js
@@ -1,7 +1,17 @@
const Category = require('./Category');
+const App = require('./App');
const Bookmark = require('./Bookmark');
const associateModels = () => {
+
+ // Category <> App
+ Category.hasMany(App, {
+ as: 'apps',
+ foreignKey: 'categoryId'
+ });
+ App.belongsTo(Category, { foreignKey: 'categoryId' });
+
+ // Category <> Bookmark
Category.hasMany(Bookmark, {
foreignKey: 'categoryId',
as: 'bookmarks'