diff --git a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx deleted file mode 100644 index 2cc4878..0000000 --- a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx +++ /dev/null @@ -1,272 +0,0 @@ -import { KeyboardEvent, useState, useEffect, Fragment } from 'react'; -import { - DragDropContext, - Droppable, - Draggable, - DropResult, -} from 'react-beautiful-dnd'; -import { Link } from 'react-router-dom'; - -// Redux -import { useDispatch, useSelector } from 'react-redux'; -import { State } from '../../../store/reducers'; -import { bindActionCreators } from 'redux'; -import { actionCreators } from '../../../store'; - -// Typescript -import { Bookmark, Category } from '../../../interfaces'; -import { ContentType } from '../Bookmarks'; - -// CSS -import classes from './BookmarkTable.module.css'; - -// UI -import { Table, Icon } from '../../UI'; - -interface Props { - contentType: ContentType; - categories: Category[]; - updateHandler: (data: Category | Bookmark) => void; -} - -export const BookmarkTable = (props: Props): JSX.Element => { - const { config } = useSelector((state: State) => state.config); - - const dispatch = useDispatch(); - const { - pinCategory, - deleteCategory, - deleteBookmark, - createNotification, - reorderCategories, - } = bindActionCreators(actionCreators, dispatch); - - const [localCategories, setLocalCategories] = useState([]); - const [isCustomOrder, setIsCustomOrder] = useState(false); - - // Copy categories array - useEffect(() => { - setLocalCategories([...props.categories]); - }, [props.categories]); - - // Check ordering - useEffect(() => { - const order = config.useOrdering; - - 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` - ); - - if (proceed) { - deleteCategory(category.id); - } - }; - - const deleteBookmarkHandler = (bookmark: Bookmark): void => { - const proceed = window.confirm( - `Are you sure you want to delete ${bookmark.name}?` - ); - - if (proceed) { - deleteBookmark(bookmark.id, bookmark.categoryId); - } - }; - - const keyboardActionHandler = ( - e: KeyboardEvent, - category: Category, - handler: Function - ) => { - if (e.key === 'Enter') { - handler(category); - } - }; - - const dragEndHanlder = (result: DropResult): void => { - if (!isCustomOrder) { - createNotification({ - title: 'Error', - message: 'Custom order is disabled', - }); - return; - } - - if (!result.destination) { - return; - } - - const tmpCategories = [...localCategories]; - const [movedApp] = tmpCategories.splice(result.source.index, 1); - tmpCategories.splice(result.destination.index, 0, movedApp); - - setLocalCategories(tmpCategories); - reorderCategories(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, - }; - - return ( - - - - {!snapshot.isDragging && ( - - )} - - ); - }} - - ); - } - )} -
- {category.name} - - {category.isPublic ? 'Visible' : 'Hidden'} - -
- deleteCategoryHandler(category) - } - onKeyDown={(e) => - keyboardActionHandler( - e, - category, - deleteCategoryHandler - ) - } - tabIndex={0} - > - -
-
- props.updateHandler(category) - } - tabIndex={0} - > - -
-
pinCategory(category)} - onKeyDown={(e) => - keyboardActionHandler( - e, - category, - pinCategory - ) - } - tabIndex={0} - > - {category.isPinned ? ( - - ) : ( - - )} -
-
- )} -
-
-
- ); - } else { - const bookmarks: { bookmark: Bookmark; categoryName: string }[] = []; - props.categories.forEach((category: Category) => { - category.bookmarks.forEach((bookmark: Bookmark) => { - bookmarks.push({ - bookmark, - categoryName: category.name, - }); - }); - }); - - return ( - - {bookmarks.map( - (bookmark: { bookmark: Bookmark; categoryName: string }) => { - return ( - - - - - - - - - ); - } - )} -
{bookmark.bookmark.name}{bookmark.bookmark.url}{bookmark.bookmark.icon}{bookmark.bookmark.isPublic ? 'Visible' : 'Hidden'}{bookmark.categoryName} -
deleteBookmarkHandler(bookmark.bookmark)} - tabIndex={0} - > - -
-
props.updateHandler(bookmark.bookmark)} - tabIndex={0} - > - -
-
- ); - } -}; diff --git a/client/src/components/Bookmarks/Table/BookmarksTable.tsx b/client/src/components/Bookmarks/Table/BookmarksTable.tsx new file mode 100644 index 0000000..dd0f447 --- /dev/null +++ b/client/src/components/Bookmarks/Table/BookmarksTable.tsx @@ -0,0 +1,195 @@ +import { useState, useEffect, Fragment } from 'react'; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, +} from 'react-beautiful-dnd'; + +// Redux +import { useDispatch, useSelector } from 'react-redux'; +import { State } from '../../../store/reducers'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../../store'; + +// Typescript +import { Bookmark, Category } from '../../../interfaces'; + +// CSS +import classes from './Table.module.css'; + +// UI +import { Table } from '../../UI'; +import { TableActions } from '../../Actions/TableActions'; +import { bookmarkTemplate } from '../../../utility'; + +interface Props { + openFormForUpdating: (data: Category | Bookmark) => void; +} + +export const BookmarksTable = ({ openFormForUpdating }: Props): JSX.Element => { + const { + bookmarks: { categoryInEdit }, + config: { config }, + } = useSelector((state: State) => state); + + const dispatch = useDispatch(); + const { + deleteBookmark, + updateBookmark, + createNotification, + reorderBookmarks, + } = bindActionCreators(actionCreators, dispatch); + + const [localBookmarks, setLocalBookmarks] = useState([]); + + // Copy bookmarks array + useEffect(() => { + if (categoryInEdit) { + setLocalBookmarks([...categoryInEdit.bookmarks]); + } + }, [categoryInEdit]); + + // Drag and drop handler + const dragEndHanlder = (result: DropResult): void => { + if (config.useOrdering !== 'orderId') { + createNotification({ + title: 'Error', + message: 'Custom order is disabled', + }); + return; + } + + if (!result.destination) { + return; + } + + const tmpBookmarks = [...localBookmarks]; + const [movedBookmark] = tmpBookmarks.splice(result.source.index, 1); + tmpBookmarks.splice(result.destination.index, 0, movedBookmark); + + setLocalBookmarks(tmpBookmarks); + + const categoryId = categoryInEdit?.id || -1; + reorderBookmarks(tmpBookmarks, categoryId); + }; + + // Action hanlders + const deleteBookmarkHandler = (id: number, name: string) => { + const categoryId = categoryInEdit?.id || -1; + + const proceed = window.confirm(`Are you sure you want to delete ${name}?`); + if (proceed) { + deleteBookmark(id, categoryId); + } + }; + + const updateBookmarkHandler = (id: number) => { + const bookmark = + categoryInEdit?.bookmarks.find((b) => b.id === id) || bookmarkTemplate; + + openFormForUpdating(bookmark); + }; + + const changeBookmarkVisibiltyHandler = (id: number) => { + const bookmark = + categoryInEdit?.bookmarks.find((b) => b.id === id) || bookmarkTemplate; + + const categoryId = categoryInEdit?.id || -1; + const [prev, curr] = [categoryId, categoryId]; + + updateBookmark( + id, + { ...bookmark, isPublic: !bookmark.isPublic }, + { prev, curr } + ); + }; + + return ( + + {!categoryInEdit ? ( +
+

+ Switch to grid view and click on the name of category you want to + edit +

+
+ ) : ( +
+

+ Editing bookmarks from {categoryInEdit.name} category +

+
+ )} + + {categoryInEdit && ( + + + {(provided) => ( + + {localBookmarks.map((bookmark, index): JSX.Element => { + return ( + + {(provided, snapshot) => { + const style = { + border: snapshot.isDragging + ? '1px solid var(--color-accent)' + : 'none', + borderRadius: '4px', + ...provided.draggableProps.style, + }; + + return ( + + + + + + + + {!snapshot.isDragging && ( + + )} + + ); + }} + + ); + })} +
{bookmark.name}{bookmark.url}{bookmark.icon} + {bookmark.isPublic ? 'Visible' : 'Hidden'} + + {categoryInEdit.name} +
+ )} +
+
+ )} +
+ ); +}; diff --git a/client/src/components/Bookmarks/Table/CategoryTable.tsx b/client/src/components/Bookmarks/Table/CategoryTable.tsx new file mode 100644 index 0000000..124bd35 --- /dev/null +++ b/client/src/components/Bookmarks/Table/CategoryTable.tsx @@ -0,0 +1,169 @@ +import { useState, useEffect, Fragment } from 'react'; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, +} from 'react-beautiful-dnd'; +import { Link } from 'react-router-dom'; + +// Redux +import { useDispatch, useSelector } from 'react-redux'; +import { State } from '../../../store/reducers'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../../store'; + +// Typescript +import { Bookmark, Category } from '../../../interfaces'; + +// CSS +import classes from './Table.module.css'; + +// UI +import { Table } from '../../UI'; +import { TableActions } from '../../Actions/TableActions'; + +interface Props { + openFormForUpdating: (data: Category | Bookmark) => void; +} + +export const CategoryTable = ({ openFormForUpdating }: Props): JSX.Element => { + const { + config: { config }, + bookmarks: { categories }, + } = useSelector((state: State) => state); + + const dispatch = useDispatch(); + const { + pinCategory, + deleteCategory, + createNotification, + reorderCategories, + updateCategory, + } = bindActionCreators(actionCreators, dispatch); + + const [localCategories, setLocalCategories] = useState([]); + + // Copy categories array + useEffect(() => { + setLocalCategories([...categories]); + }, [categories]); + + // Drag and drop handler + const dragEndHanlder = (result: DropResult): void => { + if (config.useOrdering !== 'orderId') { + createNotification({ + title: 'Error', + message: 'Custom order is disabled', + }); + return; + } + + if (!result.destination) { + return; + } + + const tmpCategories = [...localCategories]; + const [movedCategory] = tmpCategories.splice(result.source.index, 1); + tmpCategories.splice(result.destination.index, 0, movedCategory); + + setLocalCategories(tmpCategories); + reorderCategories(tmpCategories); + }; + + // Action handlers + const deleteCategoryHandler = (id: number, name: string) => { + const proceed = window.confirm( + `Are you sure you want to delete ${name}? It will delete ALL assigned bookmarks` + ); + + if (proceed) { + deleteCategory(id); + } + }; + + const updateCategoryHandler = (id: number) => { + const category = categories.find((c) => c.id === id) as Category; + openFormForUpdating(category); + }; + + const pinCategoryHandler = (id: number) => { + const category = categories.find((c) => c.id === id) as Category; + pinCategory(category); + }; + + const changeCategoryVisibiltyHandler = (id: number) => { + const category = categories.find((c) => c.id === id) as Category; + updateCategory(id, { ...category, isPublic: !category.isPublic }); + }; + + return ( + +
+ {config.useOrdering === 'orderId' ? ( +

You can drag and drop single rows to reorder categories

+ ) : ( +

+ Custom order is disabled. You can change it in the{' '} + settings +

+ )} +
+ + + + {(provided) => ( + + {localCategories.map((category, index): JSX.Element => { + return ( + + {(provided, snapshot) => { + const style = { + border: snapshot.isDragging + ? '1px solid var(--color-accent)' + : 'none', + borderRadius: '4px', + ...provided.draggableProps.style, + }; + + return ( + + + + + {!snapshot.isDragging && ( + + )} + + ); + }} + + ); + })} +
{category.name} + {category.isPublic ? 'Visible' : 'Hidden'} +
+ )} +
+
+
+ ); +}; diff --git a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.module.css b/client/src/components/Bookmarks/Table/Table.module.css similarity index 60% rename from client/src/components/Bookmarks/BookmarkTable/BookmarkTable.module.css rename to client/src/components/Bookmarks/Table/Table.module.css index 8b1e0ed..89ff6ca 100644 --- a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.module.css +++ b/client/src/components/Bookmarks/Table/Table.module.css @@ -1,16 +1,3 @@ -.TableActions { - display: flex; - align-items: center; -} - -.TableAction { - width: 22px; -} - -.TableAction:hover { - cursor: pointer; -} - .Message { width: 100%; display: flex; @@ -20,10 +7,11 @@ margin-bottom: 20px; } -.Message a { +.Message a, +.Message span { color: var(--color-accent); } .Message a:hover { cursor: pointer; -} \ No newline at end of file +} diff --git a/client/src/components/Bookmarks/Table/Table.tsx b/client/src/components/Bookmarks/Table/Table.tsx new file mode 100644 index 0000000..8704fdb --- /dev/null +++ b/client/src/components/Bookmarks/Table/Table.tsx @@ -0,0 +1,20 @@ +import { Category, Bookmark } from '../../../interfaces'; +import { ContentType } from '../Bookmarks'; +import { BookmarksTable } from './BookmarksTable'; +import { CategoryTable } from './CategoryTable'; + +interface Props { + contentType: ContentType; + openFormForUpdating: (data: Category | Bookmark) => void; +} + +export const Table = (props: Props): JSX.Element => { + const tableEl = + props.contentType === ContentType.category ? ( + + ) : ( + + ); + + return tableEl; +}; diff --git a/client/src/store/reducers/app.ts b/client/src/store/reducers/app.ts index 7f10793..3d08727 100644 --- a/client/src/store/reducers/app.ts +++ b/client/src/store/reducers/app.ts @@ -22,77 +22,86 @@ export const appsReducer = ( action: Action ): AppsState => { switch (action.type) { - case ActionType.getApps: + case ActionType.getApps: { return { ...state, loading: true, errors: undefined, }; + } - case ActionType.getAppsSuccess: + case ActionType.getAppsSuccess: { return { ...state, loading: false, apps: action.payload || [], }; + } - case ActionType.pinApp: - const pinnedAppIdx = state.apps.findIndex( + case ActionType.pinApp: { + const appIdx = state.apps.findIndex( (app) => app.id === action.payload.id ); return { ...state, apps: [ - ...state.apps.slice(0, pinnedAppIdx), + ...state.apps.slice(0, appIdx), action.payload, - ...state.apps.slice(pinnedAppIdx + 1), + ...state.apps.slice(appIdx + 1), ], }; + } - case ActionType.addAppSuccess: + case ActionType.addAppSuccess: { return { ...state, apps: [...state.apps, action.payload], }; + } - case ActionType.deleteApp: + case ActionType.deleteApp: { return { ...state, apps: [...state.apps].filter((app) => app.id !== action.payload), }; + } - case ActionType.updateApp: - const updatedAppIdx = state.apps.findIndex( + case ActionType.updateApp: { + const appIdx = state.apps.findIndex( (app) => app.id === action.payload.id ); return { ...state, apps: [ - ...state.apps.slice(0, updatedAppIdx), + ...state.apps.slice(0, appIdx), action.payload, - ...state.apps.slice(updatedAppIdx + 1), + ...state.apps.slice(appIdx + 1), ], }; + } - case ActionType.reorderApps: + case ActionType.reorderApps: { return { ...state, apps: action.payload, }; + } - case ActionType.sortApps: + case ActionType.sortApps: { return { ...state, apps: sortData(state.apps, action.payload), }; + } - case ActionType.setEditApp: + case ActionType.setEditApp: { return { ...state, appInUpdate: action.payload, }; + } default: return state; diff --git a/client/src/store/reducers/auth.ts b/client/src/store/reducers/auth.ts index 4105a0f..2281a86 100644 --- a/client/src/store/reducers/auth.ts +++ b/client/src/store/reducers/auth.ts @@ -22,24 +22,28 @@ export const authReducer = ( token: action.payload, isAuthenticated: true, }; + case ActionType.logout: return { ...state, token: null, isAuthenticated: false, }; + case ActionType.autoLogin: return { ...state, token: action.payload, isAuthenticated: true, }; + case ActionType.authError: return { ...state, token: null, isAuthenticated: false, }; + default: return state; } diff --git a/client/src/store/reducers/config.ts b/client/src/store/reducers/config.ts index 41c57d1..7976919 100644 --- a/client/src/store/reducers/config.ts +++ b/client/src/store/reducers/config.ts @@ -26,26 +26,31 @@ export const configReducer = ( loading: false, config: action.payload, }; + case ActionType.updateConfig: return { ...state, config: action.payload, }; + case ActionType.fetchQueries: return { ...state, customQueries: action.payload, }; + case ActionType.addQuery: return { ...state, customQueries: [...state.customQueries, action.payload], }; + case ActionType.deleteQuery: return { ...state, customQueries: action.payload, }; + case ActionType.updateQuery: return { ...state, diff --git a/client/src/store/reducers/notification.ts b/client/src/store/reducers/notification.ts index 544402f..23d8769 100644 --- a/client/src/store/reducers/notification.ts +++ b/client/src/store/reducers/notification.ts @@ -29,6 +29,7 @@ export const notificationReducer = ( ], idCounter: state.idCounter + 1, }; + case ActionType.clearNotification: return { ...state, diff --git a/client/src/store/reducers/theme.ts b/client/src/store/reducers/theme.ts index ef32495..6db29fe 100644 --- a/client/src/store/reducers/theme.ts +++ b/client/src/store/reducers/theme.ts @@ -24,6 +24,7 @@ export const themeReducer = ( switch (action.type) { case ActionType.setTheme: return { theme: action.payload }; + default: return state; }