From 089ace562a0e2e40cd539d7cc415447ee58e2230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Sat, 20 Nov 2021 14:17:51 +0100 Subject: [PATCH 01/11] Moved table actions to separate component --- .../Actions/TableActions.module.css | 12 +++ .../src/components/Actions/TableActions.tsx | 79 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 client/src/components/Actions/TableActions.module.css create mode 100644 client/src/components/Actions/TableActions.tsx diff --git a/client/src/components/Actions/TableActions.module.css b/client/src/components/Actions/TableActions.module.css new file mode 100644 index 0000000..69028a9 --- /dev/null +++ b/client/src/components/Actions/TableActions.module.css @@ -0,0 +1,12 @@ +.TableActions { + display: flex; + align-items: center; +} + +.TableAction { + width: 22px; +} + +.TableAction:hover { + cursor: pointer; +} diff --git a/client/src/components/Actions/TableActions.tsx b/client/src/components/Actions/TableActions.tsx new file mode 100644 index 0000000..75613ee --- /dev/null +++ b/client/src/components/Actions/TableActions.tsx @@ -0,0 +1,79 @@ +import { Icon } from '../UI'; +import classes from './TableActions.module.css'; + +interface Entity { + id: number; + name: string; + isPinned: boolean; + isPublic: boolean; +} + +interface Props { + entity: Entity; + deleteHandler: (id: number, name: string) => void; + updateHandler: (id: number) => void; + pinHanlder: (id: number) => void; + changeVisibilty: (id: number) => void; + showPin?: boolean; +} + +export const TableActions = (props: Props): JSX.Element => { + const { + entity, + deleteHandler, + updateHandler, + pinHanlder, + changeVisibilty, + showPin = true, + } = props; + + return ( + + {/* DELETE */} +
deleteHandler(entity.id, entity.name)} + tabIndex={0} + > + +
+ + {/* UPDATE */} +
updateHandler(entity.id)} + tabIndex={0} + > + +
+ + {/* PIN */} + {showPin && ( +
pinHanlder(entity.id)} + tabIndex={0} + > + {entity.isPinned ? ( + + ) : ( + + )} +
+ )} + + {/* VISIBILITY */} +
changeVisibilty(entity.id)} + tabIndex={0} + > + {entity.isPublic ? ( + + ) : ( + + )} +
+ + ); +}; From 8941f8f2f4e29ada18741450fa274861c3bb8510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Sat, 20 Nov 2021 14:18:42 +0100 Subject: [PATCH 02/11] Added support for .ico files --- CHANGELOG.md | 5 +++++ client/src/components/Apps/AppForm/AppForm.tsx | 2 +- client/src/components/Bookmarks/Form/BookmarksForm.tsx | 7 ++++++- client/src/components/Settings/UISettings/UISettings.tsx | 4 +++- client/src/utility/validators.ts | 2 +- middleware/multer.js | 2 +- 6 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8543534..834a186 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### v2.0.2 (TBA) +- Added support for .ico files for custom icons ([#209](https://github.com/pawelmalak/flame/issues/209)) +- Added option to pin apps and categories directly from table view + + ### v2.0.1 (2021-11-19) - Added option to display humidity in the weather widget ([#136](https://github.com/pawelmalak/flame/issues/136)) - Added option to set default theme for all new users ([#165](https://github.com/pawelmalak/flame/issues/165)) diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx index 0b94465..10bc694 100644 --- a/client/src/components/Apps/AppForm/AppForm.tsx +++ b/client/src/components/Apps/AppForm/AppForm.tsx @@ -154,7 +154,7 @@ export const AppForm = ({ app, modalHandler }: Props): JSX.Element => { id="icon" required onChange={(e) => fileChangeHandler(e)} - accept=".jpg,.jpeg,.png,.svg" + accept=".jpg,.jpeg,.png,.svg,.ico" /> { diff --git a/client/src/components/Bookmarks/Form/BookmarksForm.tsx b/client/src/components/Bookmarks/Form/BookmarksForm.tsx index f0a3a43..60cfb5c 100644 --- a/client/src/components/Bookmarks/Form/BookmarksForm.tsx +++ b/client/src/components/Bookmarks/Form/BookmarksForm.tsx @@ -146,6 +146,7 @@ export const BookmarksForm = ({ return ( + {/* NAME */} + {/* URL */} + {/* CATEGORY */} { {/* HIDE HEADER */} - + - {!app ? ( + {!appInUpdate ? ( ) : ( diff --git a/client/src/components/Apps/Apps.tsx b/client/src/components/Apps/Apps.tsx index 7231e18..88db874 100644 --- a/client/src/components/Apps/Apps.tsx +++ b/client/src/components/Apps/Apps.tsx @@ -19,7 +19,6 @@ import { AppForm } from './AppForm/AppForm'; import { AppTable } from './AppTable/AppTable'; // Utils -import { appTemplate } from '../../utility'; import { State } from '../../store/reducers'; import { bindActionCreators } from 'redux'; import { actionCreators } from '../../store'; @@ -37,7 +36,7 @@ export const Apps = (props: Props): JSX.Element => { // Get Redux action creators const dispatch = useDispatch(); - const { getApps } = bindActionCreators(actionCreators, dispatch); + const { getApps, setEditApp } = bindActionCreators(actionCreators, dispatch); // Load apps if array is empty useEffect(() => { @@ -49,8 +48,6 @@ export const Apps = (props: Props): JSX.Element => { // Form const [modalIsOpen, setModalIsOpen] = useState(false); const [showTable, setShowTable] = useState(false); - const [isInUpdate, setIsInUpdate] = useState(false); - const [appInUpdate, setAppInUpdate] = useState(appTemplate); // Observe if user is authenticated -> set default view if not useEffect(() => { @@ -63,28 +60,21 @@ export const Apps = (props: Props): JSX.Element => { // Form actions const toggleModal = (): void => { setModalIsOpen(!modalIsOpen); - setIsInUpdate(false); }; const toggleEdit = (): void => { setShowTable(!showTable); - setIsInUpdate(false); }; const openFormForUpdating = (app: App): void => { - setAppInUpdate(app); - setIsInUpdate(true); + setEditApp(app); setModalIsOpen(true); }; return ( - {!isInUpdate ? ( - - ) : ( - - )} + { {isAuthenticated && (
- + { + setEditApp(null); + toggleModal(); + }} + />
)} diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css index b840a42..c61217d 100644 --- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css @@ -10,6 +10,10 @@ text-transform: uppercase; } +.BookmarkCard h3:hover { + cursor: pointer; +} + .Bookmarks { display: flex; flex-direction: column; diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx index 146bf67..fe6c97d 100644 --- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx @@ -1,14 +1,17 @@ import { Fragment } from 'react'; -import { useSelector } from 'react-redux'; +// 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'; +// Other import classes from './BookmarkCard.module.css'; - import { Icon } from '../../UI'; - import { iconParser, isImage, isSvg, isUrl, urlParser } from '../../../utility'; interface Props { @@ -18,9 +21,19 @@ interface Props { export const BookmarkCard = (props: Props): JSX.Element => { const { config } = useSelector((state: State) => state.config); + const dispatch = useDispatch(); + const { setEditCategory } = bindActionCreators(actionCreators, dispatch); + return (
-

{props.category.name}

+

{ + setEditCategory(props.category); + }} + > + {props.category.name} +

+
{props.category.bookmarks.map((bookmark: Bookmark) => { const redirectUrl = urlParser(bookmark.url)[1]; diff --git a/client/src/components/Bookmarks/Bookmarks.tsx b/client/src/components/Bookmarks/Bookmarks.tsx index ba5570e..f461d48 100644 --- a/client/src/components/Bookmarks/Bookmarks.tsx +++ b/client/src/components/Bookmarks/Bookmarks.tsx @@ -19,9 +19,6 @@ import { Container, Headline, ActionButton, Spinner, Modal } from '../UI'; // Components import { BookmarkGrid } from './BookmarkGrid/BookmarkGrid'; import { Form } from './Form/Form'; - -// Utils -import { bookmarkTemplate, categoryTemplate } from '../../utility'; import { Table } from './Table/Table'; interface Props { @@ -36,13 +33,14 @@ export enum ContentType { export const Bookmarks = (props: Props): JSX.Element => { // Get Redux state const { - bookmarks: { loading, categories }, + bookmarks: { loading, categories, categoryInEdit }, auth: { isAuthenticated }, } = useSelector((state: State) => state); // Get Redux action creators const dispatch = useDispatch(); - const { getCategories } = bindActionCreators(actionCreators, dispatch); + const { getCategories, setEditCategory, setEditBookmark } = + bindActionCreators(actionCreators, dispatch); // Load categories if array is empty useEffect(() => { @@ -55,10 +53,6 @@ export const Bookmarks = (props: Props): JSX.Element => { const [modalIsOpen, setModalIsOpen] = useState(false); const [formContentType, setFormContentType] = useState(ContentType.category); const [isInUpdate, setIsInUpdate] = useState(false); - const [categoryInUpdate, setCategoryInUpdate] = - useState(categoryTemplate); - const [bookmarkInUpdate, setBookmarkInUpdate] = - useState(bookmarkTemplate); // Table const [showTable, setShowTable] = useState(false); @@ -74,6 +68,13 @@ export const Bookmarks = (props: Props): JSX.Element => { } }, [isAuthenticated]); + useEffect(() => { + if (categoryInEdit && !showTable) { + setTableContentType(ContentType.bookmark); + setShowTable(true); + } + }, [categoryInEdit]); + // Form actions const toggleModal = (): void => { setModalIsOpen(!modalIsOpen); @@ -94,10 +95,10 @@ export const Bookmarks = (props: Props): JSX.Element => { if (instanceOfCategory(data)) { setFormContentType(ContentType.category); - setCategoryInUpdate(data); + setEditCategory(data); } else { setFormContentType(ContentType.bookmark); - setBookmarkInUpdate(data); + setEditBookmark(data); } toggleModal(); @@ -121,8 +122,6 @@ export const Bookmarks = (props: Props): JSX.Element => { modalHandler={toggleModal} contentType={formContentType} inUpdate={isInUpdate} - category={categoryInUpdate} - bookmark={bookmarkInUpdate} /> @@ -145,11 +144,11 @@ export const Bookmarks = (props: Props): JSX.Element => { icon="mdiPencil" handler={() => showTableForEditing(ContentType.category)} /> - {/* showTableForEditing(ContentType.bookmark)} - /> */} + />
)} diff --git a/client/src/components/Bookmarks/Form/Form.tsx b/client/src/components/Bookmarks/Form/Form.tsx index 41ed1bb..960e8cf 100644 --- a/client/src/components/Bookmarks/Form/Form.tsx +++ b/client/src/components/Bookmarks/Form/Form.tsx @@ -1,22 +1,26 @@ // Typescript -import { Bookmark, Category } from '../../../interfaces'; import { ContentType } from '../Bookmarks'; // Utils import { CategoryForm } from './CategoryForm'; import { BookmarksForm } from './BookmarksForm'; import { Fragment } from 'react'; +import { useSelector } from 'react-redux'; +import { State } from '../../../store/reducers'; +import { bookmarkTemplate, categoryTemplate } from '../../../utility'; interface Props { modalHandler: () => void; contentType: ContentType; inUpdate?: boolean; - category?: Category; - bookmark?: Bookmark; } export const Form = (props: Props): JSX.Element => { - const { modalHandler, contentType, inUpdate, category, bookmark } = props; + const { categoryInEdit, bookmarkInEdit } = useSelector( + (state: State) => state.bookmarks + ); + + const { modalHandler, contentType, inUpdate } = props; return ( @@ -33,9 +37,15 @@ export const Form = (props: Props): JSX.Element => { // form: update {contentType === ContentType.category ? ( - + ) : ( - + )} )} diff --git a/client/src/store/action-creators/app.ts b/client/src/store/action-creators/app.ts index ddd88fa..7e285b6 100644 --- a/client/src/store/action-creators/app.ts +++ b/client/src/store/action-creators/app.ts @@ -7,6 +7,7 @@ import { GetAppsAction, PinAppAction, ReorderAppsAction, + SetEditAppAction, SortAppsAction, UpdateAppAction, } from '../actions/app'; @@ -196,3 +197,11 @@ export const sortApps = () => async (dispatch: Dispatch) => { console.log(err); } }; + +export const setEditApp = + (app: App | null) => (dispatch: Dispatch) => { + dispatch({ + type: ActionType.setEditApp, + payload: app, + }); + }; diff --git a/client/src/store/action-creators/bookmark.ts b/client/src/store/action-creators/bookmark.ts index 5010bd3..4b1b0d2 100644 --- a/client/src/store/action-creators/bookmark.ts +++ b/client/src/store/action-creators/bookmark.ts @@ -18,6 +18,8 @@ import { GetCategoriesAction, PinCategoryAction, ReorderCategoriesAction, + SetEditBookmarkAction, + SetEditCategoryAction, SortCategoriesAction, UpdateBookmarkAction, UpdateCategoryAction, @@ -319,3 +321,21 @@ export const reorderCategories = console.log(err); } }; + +export const setEditCategory = + (category: Category | null) => + (dispatch: Dispatch) => { + dispatch({ + type: ActionType.setEditCategory, + payload: category, + }); + }; + +export const setEditBookmark = + (bookmark: Bookmark | null) => + (dispatch: Dispatch) => { + dispatch({ + type: ActionType.setEditBookmark, + payload: bookmark, + }); + }; diff --git a/client/src/store/action-types/index.ts b/client/src/store/action-types/index.ts index 58ca529..81e1a84 100644 --- a/client/src/store/action-types/index.ts +++ b/client/src/store/action-types/index.ts @@ -23,6 +23,7 @@ export enum ActionType { updateApp = 'UPDATE_APP', reorderApps = 'REORDER_APPS', sortApps = 'SORT_APPS', + setEditApp = 'SET_EDIT_APP', // CATEGORES getCategories = 'GET_CATEGORIES', getCategoriesSuccess = 'GET_CATEGORIES_SUCCESS', @@ -33,10 +34,12 @@ export enum ActionType { updateCategory = 'UPDATE_CATEGORY', sortCategories = 'SORT_CATEGORIES', reorderCategories = 'REORDER_CATEGORIES', + setEditCategory = 'SET_EDIT_CATEGORY', // BOOKMARKS addBookmark = 'ADD_BOOKMARK', deleteBookmark = 'DELETE_BOOKMARK', updateBookmark = 'UPDATE_BOOKMARK', + setEditBookmark = 'SET_EDIT_BOOKMARK', // AUTH login = 'LOGIN', logout = 'LOGOUT', diff --git a/client/src/store/actions/app.ts b/client/src/store/actions/app.ts index 37f5419..689014a 100644 --- a/client/src/store/actions/app.ts +++ b/client/src/store/actions/app.ts @@ -36,3 +36,8 @@ export interface SortAppsAction { type: ActionType.sortApps; payload: string; } + +export interface SetEditAppAction { + type: ActionType.setEditApp; + payload: App | null; +} diff --git a/client/src/store/actions/bookmark.ts b/client/src/store/actions/bookmark.ts index e4cfcfd..60e7c9f 100644 --- a/client/src/store/actions/bookmark.ts +++ b/client/src/store/actions/bookmark.ts @@ -56,3 +56,13 @@ export interface ReorderCategoriesAction { type: ActionType.reorderCategories; payload: Category[]; } + +export interface SetEditCategoryAction { + type: ActionType.setEditCategory; + payload: Category | null; +} + +export interface SetEditBookmarkAction { + type: ActionType.setEditBookmark; + payload: Bookmark | null; +} diff --git a/client/src/store/actions/index.ts b/client/src/store/actions/index.ts index 02862b6..3193873 100644 --- a/client/src/store/actions/index.ts +++ b/client/src/store/actions/index.ts @@ -24,6 +24,7 @@ import { UpdateAppAction, ReorderAppsAction, SortAppsAction, + SetEditAppAction, } from './app'; import { @@ -37,6 +38,8 @@ import { AddBookmarkAction, DeleteBookmarkAction, UpdateBookmarkAction, + SetEditCategoryAction, + SetEditBookmarkAction, } from './bookmark'; import { @@ -67,6 +70,7 @@ export type Action = | UpdateAppAction | ReorderAppsAction | SortAppsAction + | SetEditAppAction // Categories | GetCategoriesAction | AddCategoryAction @@ -75,10 +79,12 @@ export type Action = | UpdateCategoryAction | SortCategoriesAction | ReorderCategoriesAction + | SetEditCategoryAction // Bookmarks | AddBookmarkAction | DeleteBookmarkAction | UpdateBookmarkAction + | SetEditBookmarkAction // Auth | LoginAction | LogoutAction diff --git a/client/src/store/reducers/app.ts b/client/src/store/reducers/app.ts index e6da902..7f10793 100644 --- a/client/src/store/reducers/app.ts +++ b/client/src/store/reducers/app.ts @@ -7,12 +7,14 @@ interface AppsState { loading: boolean; apps: App[]; errors: string | undefined; + appInUpdate: App | null; } const initialState: AppsState = { loading: true, apps: [], errors: undefined, + appInUpdate: null, }; export const appsReducer = ( @@ -86,6 +88,12 @@ export const appsReducer = ( apps: sortData(state.apps, action.payload), }; + case ActionType.setEditApp: + return { + ...state, + appInUpdate: action.payload, + }; + default: return state; } diff --git a/client/src/store/reducers/bookmark.ts b/client/src/store/reducers/bookmark.ts index b8e7e61..d1fc380 100644 --- a/client/src/store/reducers/bookmark.ts +++ b/client/src/store/reducers/bookmark.ts @@ -1,4 +1,4 @@ -import { Category } from '../../interfaces'; +import { Bookmark, Category } from '../../interfaces'; import { sortData } from '../../utility'; import { ActionType } from '../action-types'; import { Action } from '../actions'; @@ -7,12 +7,16 @@ interface BookmarksState { loading: boolean; errors: string | undefined; categories: Category[]; + categoryInEdit: Category | null; + bookmarkInEdit: Bookmark | null; } const initialState: BookmarksState = { loading: true, errors: undefined, categories: [], + categoryInEdit: null, + bookmarkInEdit: null, }; export const bookmarksReducer = ( @@ -45,19 +49,19 @@ export const bookmarksReducer = ( (category) => category.id === action.payload.categoryId ); + const categoryWithNewBookmark = { + ...state.categories[categoryIdx], + bookmarks: [...state.categories[categoryIdx].bookmarks, action.payload], + }; + return { ...state, categories: [ ...state.categories.slice(0, categoryIdx), - { - ...state.categories[categoryIdx], - bookmarks: [ - ...state.categories[categoryIdx].bookmarks, - action.payload, - ], - }, + categoryWithNewBookmark, ...state.categories.slice(categoryIdx + 1), ], + categoryInEdit: categoryWithNewBookmark, }; case ActionType.pinCategory: @@ -112,47 +116,54 @@ export const bookmarksReducer = ( (category) => category.id === action.payload.categoryId ); + const targetCategory = { + ...state.categories[categoryInUpdateIdx], + bookmarks: state.categories[categoryInUpdateIdx].bookmarks.filter( + (bookmark) => bookmark.id !== action.payload.bookmarkId + ), + }; + return { ...state, categories: [ ...state.categories.slice(0, categoryInUpdateIdx), - { - ...state.categories[categoryInUpdateIdx], - bookmarks: state.categories[categoryInUpdateIdx].bookmarks.filter( - (bookmark) => bookmark.id !== action.payload.bookmarkId - ), - }, + targetCategory, ...state.categories.slice(categoryInUpdateIdx + 1), ], + categoryInEdit: targetCategory, }; case ActionType.updateBookmark: const parentCategoryIdx = state.categories.findIndex( (category) => category.id === action.payload.categoryId ); + const updatedBookmarkIdx = state.categories[ parentCategoryIdx ].bookmarks.findIndex((bookmark) => bookmark.id === action.payload.id); + const categoryWithUpdatedBookmark = { + ...state.categories[parentCategoryIdx], + bookmarks: [ + ...state.categories[parentCategoryIdx].bookmarks.slice( + 0, + updatedBookmarkIdx + ), + action.payload, + ...state.categories[parentCategoryIdx].bookmarks.slice( + updatedBookmarkIdx + 1 + ), + ], + }; + return { ...state, categories: [ ...state.categories.slice(0, parentCategoryIdx), - { - ...state.categories[parentCategoryIdx], - bookmarks: [ - ...state.categories[parentCategoryIdx].bookmarks.slice( - 0, - updatedBookmarkIdx - ), - action.payload, - ...state.categories[parentCategoryIdx].bookmarks.slice( - updatedBookmarkIdx + 1 - ), - ], - }, + categoryWithUpdatedBookmark, ...state.categories.slice(parentCategoryIdx + 1), ], + categoryInEdit: categoryWithUpdatedBookmark, }; case ActionType.sortCategories: @@ -166,6 +177,19 @@ export const bookmarksReducer = ( ...state, categories: action.payload, }; + + case ActionType.setEditCategory: + return { + ...state, + categoryInEdit: action.payload, + }; + + case ActionType.setEditBookmark: + return { + ...state, + bookmarkInEdit: action.payload, + }; + default: return state; } From f1f7b698f8f6ab6755bbc63e66a14a60c5fe852a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Mon, 22 Nov 2021 16:45:59 +0100 Subject: [PATCH 06/11] Db migration to support custom order of bookmarks. Created route to reorder bookmarks. Added more sorting options to bookmark controllers. Simplified ordering in getAllApps controller --- controllers/apps/getAllApps.js | 20 +++++------ controllers/bookmarks/getAllBookmarks.js | 10 +++++- controllers/bookmarks/index.js | 1 + controllers/bookmarks/reorderBookmarks.js | 23 +++++++++++++ controllers/categories/getAllCategories.js | 38 ++++++++------------- controllers/categories/getSingleCategory.js | 10 ++++++ controllers/categories/reorderCategories.js | 1 + db/migrations/04_bookmarks-order.js | 19 +++++++++++ models/Bookmark.js | 5 +++ routes/bookmark.js | 3 ++ 10 files changed, 95 insertions(+), 35 deletions(-) create mode 100644 controllers/bookmarks/reorderBookmarks.js create mode 100644 db/migrations/04_bookmarks-order.js diff --git a/controllers/apps/getAllApps.js b/controllers/apps/getAllApps.js index 04a7585..36f4eb1 100644 --- a/controllers/apps/getAllApps.js +++ b/controllers/apps/getAllApps.js @@ -28,17 +28,15 @@ const getAllApps = asyncWrapper(async (req, res, next) => { // apps visibility const where = req.isAuthenticated ? {} : { isPublic: true }; - if (orderType == 'name') { - apps = await App.findAll({ - order: [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']], - where, - }); - } else { - apps = await App.findAll({ - order: [[orderType, 'ASC']], - where, - }); - } + const order = + orderType == 'name' + ? [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']] + : [[orderType, 'ASC']]; + + apps = await App.findAll({ + order, + where, + }); if (process.env.NODE_ENV === 'production') { // Set header to fetch containers info every time diff --git a/controllers/bookmarks/getAllBookmarks.js b/controllers/bookmarks/getAllBookmarks.js index aece14b..25af9d8 100644 --- a/controllers/bookmarks/getAllBookmarks.js +++ b/controllers/bookmarks/getAllBookmarks.js @@ -1,16 +1,24 @@ const asyncWrapper = require('../../middleware/asyncWrapper'); const Bookmark = require('../../models/Bookmark'); const { Sequelize } = require('sequelize'); +const loadConfig = require('../../utils/loadConfig'); // @desc Get all bookmarks // @route GET /api/bookmarks // @access Public const getAllBookmarks = asyncWrapper(async (req, res, next) => { + const { useOrdering: orderType } = await loadConfig(); + // bookmarks visibility const where = req.isAuthenticated ? {} : { isPublic: true }; + const order = + orderType == 'name' + ? [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']] + : [[orderType, 'ASC']]; + const bookmarks = await Bookmark.findAll({ - order: [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']], + order, where, }); diff --git a/controllers/bookmarks/index.js b/controllers/bookmarks/index.js index f1ef588..5c1bd86 100644 --- a/controllers/bookmarks/index.js +++ b/controllers/bookmarks/index.js @@ -4,4 +4,5 @@ module.exports = { getSingleBookmark: require('./getSingleBookmark'), updateBookmark: require('./updateBookmark'), deleteBookmark: require('./deleteBookmark'), + reorderBookmarks: require('./reorderBookmarks'), }; diff --git a/controllers/bookmarks/reorderBookmarks.js b/controllers/bookmarks/reorderBookmarks.js new file mode 100644 index 0000000..3ea1c35 --- /dev/null +++ b/controllers/bookmarks/reorderBookmarks.js @@ -0,0 +1,23 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const Bookmark = require('../../models/Bookmark'); + +// @desc Reorder bookmarks +// @route PUT /api/bookmarks/0/reorder +// @access Public +const reorderBookmarks = asyncWrapper(async (req, res, next) => { + req.body.bookmarks.forEach(async ({ id, orderId }) => { + await Bookmark.update( + { orderId }, + { + where: { id }, + } + ); + }); + + res.status(200).json({ + success: true, + data: {}, + }); +}); + +module.exports = reorderBookmarks; diff --git a/controllers/categories/getAllCategories.js b/controllers/categories/getAllCategories.js index c42db2d..eb0eb24 100644 --- a/controllers/categories/getAllCategories.js +++ b/controllers/categories/getAllCategories.js @@ -16,29 +16,21 @@ const getAllCategories = asyncWrapper(async (req, res, next) => { // categories visibility const where = req.isAuthenticated ? {} : { isPublic: true }; - if (orderType == 'name') { - categories = await Category.findAll({ - include: [ - { - model: Bookmark, - as: 'bookmarks', - }, - ], - order: [[Sequelize.fn('lower', Sequelize.col('Category.name')), 'ASC']], - where, - }); - } else { - categories = await Category.findAll({ - include: [ - { - model: Bookmark, - as: 'bookmarks', - }, - ], - order: [[orderType, 'ASC']], - where, - }); - } + const order = + orderType == 'name' + ? [[Sequelize.fn('lower', Sequelize.col('bookmarks.name')), 'ASC']] + : [[{ model: Bookmark, as: 'bookmarks' }, orderType, 'ASC']]; + + categories = categories = await Category.findAll({ + include: [ + { + model: Bookmark, + as: 'bookmarks', + }, + ], + order, + where, + }); if (req.isAuthenticated) { output = categories; diff --git a/controllers/categories/getSingleCategory.js b/controllers/categories/getSingleCategory.js index 8eb5fb2..c854627 100644 --- a/controllers/categories/getSingleCategory.js +++ b/controllers/categories/getSingleCategory.js @@ -2,13 +2,22 @@ const asyncWrapper = require('../../middleware/asyncWrapper'); const ErrorResponse = require('../../utils/ErrorResponse'); const Category = require('../../models/Category'); const Bookmark = require('../../models/Bookmark'); +const { Sequelize } = require('sequelize'); +const loadConfig = require('../../utils/loadConfig'); // @desc Get single category // @route GET /api/categories/:id // @access Public const getSingleCategory = asyncWrapper(async (req, res, next) => { + const { useOrdering: orderType } = await loadConfig(); + const visibility = req.isAuthenticated ? {} : { isPublic: true }; + const order = + orderType == 'name' + ? [[Sequelize.fn('lower', Sequelize.col('bookmarks.name')), 'ASC']] + : [[{ model: Bookmark, as: 'bookmarks' }, orderType, 'ASC']]; + const category = await Category.findOne({ where: { id: req.params.id, ...visibility }, include: [ @@ -18,6 +27,7 @@ const getSingleCategory = asyncWrapper(async (req, res, next) => { where: visibility, }, ], + order, }); if (!category) { diff --git a/controllers/categories/reorderCategories.js b/controllers/categories/reorderCategories.js index 492675b..8922125 100644 --- a/controllers/categories/reorderCategories.js +++ b/controllers/categories/reorderCategories.js @@ -1,5 +1,6 @@ const asyncWrapper = require('../../middleware/asyncWrapper'); const Category = require('../../models/Category'); + // @desc Reorder categories // @route PUT /api/categories/0/reorder // @access Public diff --git a/db/migrations/04_bookmarks-order.js b/db/migrations/04_bookmarks-order.js new file mode 100644 index 0000000..26ddd69 --- /dev/null +++ b/db/migrations/04_bookmarks-order.js @@ -0,0 +1,19 @@ +const { DataTypes } = require('sequelize'); +const { INTEGER } = DataTypes; + +const up = async (query) => { + await query.addColumn('bookmarks', 'orderId', { + type: INTEGER, + allowNull: true, + defaultValue: null, + }); +}; + +const down = async (query) => { + await query.removeColumn('bookmarks', 'orderId'); +}; + +module.exports = { + up, + down, +}; diff --git a/models/Bookmark.js b/models/Bookmark.js index 159ea28..197a632 100644 --- a/models/Bookmark.js +++ b/models/Bookmark.js @@ -25,6 +25,11 @@ const Bookmark = sequelize.define( allowNull: true, defaultValue: 1, }, + orderId: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + }, }, { tableName: 'bookmarks', diff --git a/routes/bookmark.js b/routes/bookmark.js index ea1a344..6bc96ad 100644 --- a/routes/bookmark.js +++ b/routes/bookmark.js @@ -10,6 +10,7 @@ const { getSingleBookmark, updateBookmark, deleteBookmark, + reorderBookmarks, } = require('../controllers/bookmarks'); router @@ -23,4 +24,6 @@ router .put(auth, requireAuth, upload, updateBookmark) .delete(auth, requireAuth, deleteBookmark); +router.route('/0/reorder').put(auth, requireAuth, reorderBookmarks); + module.exports = router; From e15c2a2f070dac2a5bc525390449679cb98146a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Mon, 22 Nov 2021 17:15:01 +0100 Subject: [PATCH 07/11] Reorder bookmarks action. Small refactor of state reducers --- client/src/store/action-creators/bookmark.ts | 42 ++++++- client/src/store/action-types/index.ts | 1 + client/src/store/actions/bookmark.ts | 8 ++ client/src/store/actions/index.ts | 2 + client/src/store/reducers/bookmark.ts | 124 +++++++++++-------- 5 files changed, 126 insertions(+), 51 deletions(-) diff --git a/client/src/store/action-creators/bookmark.ts b/client/src/store/action-creators/bookmark.ts index 4b1b0d2..f2a17e5 100644 --- a/client/src/store/action-creators/bookmark.ts +++ b/client/src/store/action-creators/bookmark.ts @@ -1,5 +1,8 @@ import axios from 'axios'; import { Dispatch } from 'redux'; +import { applyAuth } from '../../utility'; +import { ActionType } from '../action-types'; + import { ApiResponse, Bookmark, @@ -8,8 +11,7 @@ import { NewBookmark, NewCategory, } from '../../interfaces'; -import { applyAuth } from '../../utility'; -import { ActionType } from '../action-types'; + import { AddBookmarkAction, AddCategoryAction, @@ -17,6 +19,7 @@ import { DeleteCategoryAction, GetCategoriesAction, PinCategoryAction, + ReorderBookmarksAction, ReorderCategoriesAction, SetEditBookmarkAction, SetEditCategoryAction, @@ -339,3 +342,38 @@ export const setEditBookmark = payload: bookmark, }); }; + +export const reorderBookmarks = + (bookmarks: Bookmark[], categoryId: number) => + async (dispatch: Dispatch) => { + interface ReorderQuery { + bookmarks: { + id: number; + orderId: number; + }[]; + } + + try { + const updateQuery: ReorderQuery = { bookmarks: [] }; + + bookmarks.forEach((bookmark, index) => + updateQuery.bookmarks.push({ + id: bookmark.id, + orderId: index + 1, + }) + ); + + await axios.put>( + '/api/bookmarks/0/reorder', + updateQuery, + { headers: applyAuth() } + ); + + dispatch({ + type: ActionType.reorderBookmarks, + payload: { bookmarks, categoryId }, + }); + } catch (err) { + console.log(err); + } + }; diff --git a/client/src/store/action-types/index.ts b/client/src/store/action-types/index.ts index 81e1a84..80853c4 100644 --- a/client/src/store/action-types/index.ts +++ b/client/src/store/action-types/index.ts @@ -40,6 +40,7 @@ export enum ActionType { deleteBookmark = 'DELETE_BOOKMARK', updateBookmark = 'UPDATE_BOOKMARK', setEditBookmark = 'SET_EDIT_BOOKMARK', + reorderBookmarks = 'REORDER_BOOKMARKS', // AUTH login = 'LOGIN', logout = 'LOGOUT', diff --git a/client/src/store/actions/bookmark.ts b/client/src/store/actions/bookmark.ts index 60e7c9f..d9c89e8 100644 --- a/client/src/store/actions/bookmark.ts +++ b/client/src/store/actions/bookmark.ts @@ -66,3 +66,11 @@ export interface SetEditBookmarkAction { type: ActionType.setEditBookmark; payload: Bookmark | null; } + +export interface ReorderBookmarksAction { + type: ActionType.reorderBookmarks; + payload: { + bookmarks: Bookmark[]; + categoryId: number; + }; +} diff --git a/client/src/store/actions/index.ts b/client/src/store/actions/index.ts index 3193873..22d33d3 100644 --- a/client/src/store/actions/index.ts +++ b/client/src/store/actions/index.ts @@ -40,6 +40,7 @@ import { UpdateBookmarkAction, SetEditCategoryAction, SetEditBookmarkAction, + ReorderBookmarksAction, } from './bookmark'; import { @@ -85,6 +86,7 @@ export type Action = | DeleteBookmarkAction | UpdateBookmarkAction | SetEditBookmarkAction + | ReorderBookmarksAction // Auth | LoginAction | LogoutAction diff --git a/client/src/store/reducers/bookmark.ts b/client/src/store/reducers/bookmark.ts index d1fc380..3c069f0 100644 --- a/client/src/store/reducers/bookmark.ts +++ b/client/src/store/reducers/bookmark.ts @@ -24,32 +24,35 @@ export const bookmarksReducer = ( action: Action ): BookmarksState => { switch (action.type) { - case ActionType.getCategories: + case ActionType.getCategories: { return { ...state, loading: true, errors: undefined, }; + } - case ActionType.getCategoriesSuccess: + case ActionType.getCategoriesSuccess: { return { ...state, loading: false, categories: action.payload, }; + } - case ActionType.addCategory: + case ActionType.addCategory: { return { ...state, categories: [...state.categories, { ...action.payload, bookmarks: [] }], }; + } - case ActionType.addBookmark: + case ActionType.addBookmark: { const categoryIdx = state.categories.findIndex( (category) => category.id === action.payload.categoryId ); - const categoryWithNewBookmark = { + const targetCategory = { ...state.categories[categoryIdx], bookmarks: [...state.categories[categoryIdx].bookmarks, action.payload], }; @@ -58,67 +61,71 @@ export const bookmarksReducer = ( ...state, categories: [ ...state.categories.slice(0, categoryIdx), - categoryWithNewBookmark, + targetCategory, ...state.categories.slice(categoryIdx + 1), ], - categoryInEdit: categoryWithNewBookmark, + categoryInEdit: targetCategory, }; + } - case ActionType.pinCategory: - const pinnedCategoryIdx = state.categories.findIndex( + case ActionType.pinCategory: { + const categoryIdx = state.categories.findIndex( (category) => category.id === action.payload.id ); return { ...state, categories: [ - ...state.categories.slice(0, pinnedCategoryIdx), + ...state.categories.slice(0, categoryIdx), { ...action.payload, - bookmarks: [...state.categories[pinnedCategoryIdx].bookmarks], + bookmarks: [...state.categories[categoryIdx].bookmarks], }, - ...state.categories.slice(pinnedCategoryIdx + 1), + ...state.categories.slice(categoryIdx + 1), ], }; + } - case ActionType.deleteCategory: - const deletedCategoryIdx = state.categories.findIndex( + case ActionType.deleteCategory: { + const categoryIdx = state.categories.findIndex( (category) => category.id === action.payload ); return { ...state, categories: [ - ...state.categories.slice(0, deletedCategoryIdx), - ...state.categories.slice(deletedCategoryIdx + 1), + ...state.categories.slice(0, categoryIdx), + ...state.categories.slice(categoryIdx + 1), ], }; + } - case ActionType.updateCategory: - const updatedCategoryIdx = state.categories.findIndex( + case ActionType.updateCategory: { + const categoryIdx = state.categories.findIndex( (category) => category.id === action.payload.id ); return { ...state, categories: [ - ...state.categories.slice(0, updatedCategoryIdx), + ...state.categories.slice(0, categoryIdx), { ...action.payload, - bookmarks: [...state.categories[updatedCategoryIdx].bookmarks], + bookmarks: [...state.categories[categoryIdx].bookmarks], }, - ...state.categories.slice(updatedCategoryIdx + 1), + ...state.categories.slice(categoryIdx + 1), ], }; + } - case ActionType.deleteBookmark: - const categoryInUpdateIdx = state.categories.findIndex( + case ActionType.deleteBookmark: { + const categoryIdx = state.categories.findIndex( (category) => category.id === action.payload.categoryId ); const targetCategory = { - ...state.categories[categoryInUpdateIdx], - bookmarks: state.categories[categoryInUpdateIdx].bookmarks.filter( + ...state.categories[categoryIdx], + bookmarks: state.categories[categoryIdx].bookmarks.filter( (bookmark) => bookmark.id !== action.payload.bookmarkId ), }; @@ -126,69 +133,88 @@ export const bookmarksReducer = ( return { ...state, categories: [ - ...state.categories.slice(0, categoryInUpdateIdx), + ...state.categories.slice(0, categoryIdx), targetCategory, - ...state.categories.slice(categoryInUpdateIdx + 1), + ...state.categories.slice(categoryIdx + 1), ], categoryInEdit: targetCategory, }; + } - case ActionType.updateBookmark: - const parentCategoryIdx = state.categories.findIndex( + case ActionType.updateBookmark: { + const categoryIdx = state.categories.findIndex( (category) => category.id === action.payload.categoryId ); - const updatedBookmarkIdx = state.categories[ - parentCategoryIdx - ].bookmarks.findIndex((bookmark) => bookmark.id === action.payload.id); + const bookmarkIdx = state.categories[categoryIdx].bookmarks.findIndex( + (bookmark) => bookmark.id === action.payload.id + ); - const categoryWithUpdatedBookmark = { - ...state.categories[parentCategoryIdx], + const targetCategory = { + ...state.categories[categoryIdx], bookmarks: [ - ...state.categories[parentCategoryIdx].bookmarks.slice( - 0, - updatedBookmarkIdx - ), + ...state.categories[categoryIdx].bookmarks.slice(0, bookmarkIdx), action.payload, - ...state.categories[parentCategoryIdx].bookmarks.slice( - updatedBookmarkIdx + 1 - ), + ...state.categories[categoryIdx].bookmarks.slice(bookmarkIdx + 1), ], }; return { ...state, categories: [ - ...state.categories.slice(0, parentCategoryIdx), - categoryWithUpdatedBookmark, - ...state.categories.slice(parentCategoryIdx + 1), + ...state.categories.slice(0, categoryIdx), + targetCategory, + ...state.categories.slice(categoryIdx + 1), ], - categoryInEdit: categoryWithUpdatedBookmark, + categoryInEdit: targetCategory, }; + } - case ActionType.sortCategories: + case ActionType.sortCategories: { return { ...state, categories: sortData(state.categories, action.payload), }; + } - case ActionType.reorderCategories: + case ActionType.reorderCategories: { return { ...state, categories: action.payload, }; + } - case ActionType.setEditCategory: + case ActionType.setEditCategory: { return { ...state, categoryInEdit: action.payload, }; + } - case ActionType.setEditBookmark: + case ActionType.setEditBookmark: { return { ...state, bookmarkInEdit: action.payload, }; + } + + case ActionType.reorderBookmarks: { + const categoryIdx = state.categories.findIndex( + (category) => category.id === action.payload.categoryId + ); + + return { + ...state, + categories: [ + ...state.categories.slice(0, categoryIdx), + { + ...state.categories[categoryIdx], + bookmarks: action.payload.bookmarks, + }, + ...state.categories.slice(categoryIdx + 1), + ], + }; + } default: return state; From a02814aa025591a8a137d6e31eeb9ed77fd83f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Thu, 25 Nov 2021 16:44:24 +0100 Subject: [PATCH 08/11] Split BookmarksTable into separate components. Minor changes to reducers --- .../Bookmarks/BookmarkTable/BookmarkTable.tsx | 272 ------------------ .../Bookmarks/Table/BookmarksTable.tsx | 195 +++++++++++++ .../Bookmarks/Table/CategoryTable.tsx | 169 +++++++++++ .../Table.module.css} | 18 +- .../src/components/Bookmarks/Table/Table.tsx | 20 ++ client/src/store/reducers/app.ts | 39 ++- client/src/store/reducers/auth.ts | 4 + client/src/store/reducers/config.ts | 5 + client/src/store/reducers/notification.ts | 1 + client/src/store/reducers/theme.ts | 1 + 10 files changed, 422 insertions(+), 302 deletions(-) delete mode 100644 client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx create mode 100644 client/src/components/Bookmarks/Table/BookmarksTable.tsx create mode 100644 client/src/components/Bookmarks/Table/CategoryTable.tsx rename client/src/components/Bookmarks/{BookmarkTable/BookmarkTable.module.css => Table/Table.module.css} (60%) create mode 100644 client/src/components/Bookmarks/Table/Table.tsx 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; } From ec5f50aba49712e6cc6d1b0687c9a0b788d20cc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Thu, 25 Nov 2021 16:54:27 +0100 Subject: [PATCH 09/11] Added option to reorder bookmarks --- .../BookmarkCard/BookmarkCard.module.css | 2 +- .../Bookmarks/BookmarkCard/BookmarkCard.tsx | 12 +++++++--- .../Bookmarks/BookmarkGrid/BookmarkGrid.tsx | 22 ++++++++++++++----- client/src/components/Bookmarks/Bookmarks.tsx | 8 ++++++- client/src/components/Home/Home.tsx | 1 + 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css index c61217d..2fd52f0 100644 --- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css @@ -10,7 +10,7 @@ text-transform: uppercase; } -.BookmarkCard h3:hover { +.BookmarkHeader:hover { cursor: pointer; } diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx index fe6c97d..8891460 100644 --- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx @@ -16,9 +16,12 @@ import { iconParser, isImage, isSvg, isUrl, urlParser } from '../../../utility'; interface Props { category: Category; + fromHomepage?: boolean; } export const BookmarkCard = (props: Props): JSX.Element => { + const { category, fromHomepage = false } = props; + const { config } = useSelector((state: State) => state.config); const dispatch = useDispatch(); @@ -27,15 +30,18 @@ export const BookmarkCard = (props: Props): JSX.Element => { return (

{ - setEditCategory(props.category); + if (!fromHomepage) { + setEditCategory(category); + } }} > - {props.category.name} + {category.name}

- {props.category.bookmarks.map((bookmark: Bookmark) => { + {category.bookmarks.map((bookmark: Bookmark) => { const redirectUrl = urlParser(bookmark.url)[1]; let iconEl: JSX.Element = ; diff --git a/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx b/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx index 01a5ed1..7e26c32 100644 --- a/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx +++ b/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx @@ -11,27 +11,39 @@ interface Props { categories: Category[]; totalCategories?: number; searching: boolean; + fromHomepage?: boolean; } export const BookmarkGrid = (props: Props): JSX.Element => { + const { + categories, + totalCategories, + searching, + fromHomepage = false, + } = props; + let bookmarks: JSX.Element; - if (props.categories.length) { - if (props.searching && !props.categories[0].bookmarks.length) { + if (categories.length) { + if (searching && !categories[0].bookmarks.length) { bookmarks = No bookmarks match your search criteria; } else { bookmarks = (
- {props.categories.map( + {categories.map( (category: Category): JSX.Element => ( - + ) )}
); } } else { - if (props.totalCategories) { + if (totalCategories) { bookmarks = ( There are no pinned categories. You can pin them from the{' '} diff --git a/client/src/components/Bookmarks/Bookmarks.tsx b/client/src/components/Bookmarks/Bookmarks.tsx index f461d48..5bbe324 100644 --- a/client/src/components/Bookmarks/Bookmarks.tsx +++ b/client/src/components/Bookmarks/Bookmarks.tsx @@ -69,12 +69,17 @@ export const Bookmarks = (props: Props): JSX.Element => { }, [isAuthenticated]); useEffect(() => { - if (categoryInEdit && !showTable) { + if (categoryInEdit) { setTableContentType(ContentType.bookmark); setShowTable(true); } }, [categoryInEdit]); + useEffect(() => { + setShowTable(false); + setEditCategory(null); + }, []); + // Form actions const toggleModal = (): void => { setModalIsOpen(!modalIsOpen); @@ -108,6 +113,7 @@ export const Bookmarks = (props: Props): JSX.Element => { const showTableForEditing = (contentType: ContentType) => { // We're in the edit mode and the same button was clicked - go back to list if (showTable && contentType === tableContentType) { + setEditCategory(null); setShowTable(false); } else { setShowTable(true); diff --git a/client/src/components/Home/Home.tsx b/client/src/components/Home/Home.tsx index d369aaa..e24e5c0 100644 --- a/client/src/components/Home/Home.tsx +++ b/client/src/components/Home/Home.tsx @@ -151,6 +151,7 @@ export const Home = (): JSX.Element => { } totalCategories={categories.length} searching={!!localSearch} + fromHomepage={true} /> )} From d5610ad6be4317708f5ff3c4c84879a53804cdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Fri, 26 Nov 2021 14:04:46 +0100 Subject: [PATCH 10/11] Fixed bug with alphabetical order not working for bookmarks. Minor changes related to bookmarks form --- client/src/components/Bookmarks/Bookmarks.tsx | 2 +- .../Bookmarks/Form/BookmarksForm.tsx | 8 +++---- .../Bookmarks/Form/CategoryForm.tsx | 2 +- .../Settings/UISettings/UISettings.tsx | 23 ++++++++++++------- client/src/interfaces/Bookmark.ts | 4 +++- client/src/store/action-creators/bookmark.ts | 22 ++++++++++++++++++ client/src/store/action-types/index.ts | 1 + client/src/store/actions/bookmark.ts | 8 +++++++ client/src/store/actions/index.ts | 2 ++ client/src/store/reducers/bookmark.ts | 23 +++++++++++++++++++ .../templateObjects/bookmarkTemplate.ts | 1 + controllers/categories/getAllCategories.js | 10 ++++++-- 12 files changed, 89 insertions(+), 17 deletions(-) diff --git a/client/src/components/Bookmarks/Bookmarks.tsx b/client/src/components/Bookmarks/Bookmarks.tsx index 5bbe324..a708995 100644 --- a/client/src/components/Bookmarks/Bookmarks.tsx +++ b/client/src/components/Bookmarks/Bookmarks.tsx @@ -69,7 +69,7 @@ export const Bookmarks = (props: Props): JSX.Element => { }, [isAuthenticated]); useEffect(() => { - if (categoryInEdit) { + if (categoryInEdit && !modalIsOpen) { setTableContentType(ContentType.bookmark); setShowTable(true); } diff --git a/client/src/components/Bookmarks/Form/BookmarksForm.tsx b/client/src/components/Bookmarks/Form/BookmarksForm.tsx index 60cfb5c..65f658d 100644 --- a/client/src/components/Bookmarks/Form/BookmarksForm.tsx +++ b/client/src/components/Bookmarks/Form/BookmarksForm.tsx @@ -137,11 +137,11 @@ export const BookmarksForm = ({ } modalHandler(); - - setFormData(newBookmarkTemplate); - - setCustomIcon(null); } + + setFormData(newBookmarkTemplate); + + setCustomIcon(null); }; return ( diff --git a/client/src/components/Bookmarks/Form/CategoryForm.tsx b/client/src/components/Bookmarks/Form/CategoryForm.tsx index c7e0105..b7ffb58 100644 --- a/client/src/components/Bookmarks/Form/CategoryForm.tsx +++ b/client/src/components/Bookmarks/Form/CategoryForm.tsx @@ -60,10 +60,10 @@ export const CategoryForm = ({ addCategory(formData); } else { updateCategory(category.id, formData); + modalHandler(); } setFormData(newCategoryTemplate); - modalHandler(); }; return ( diff --git a/client/src/components/Settings/UISettings/UISettings.tsx b/client/src/components/Settings/UISettings/UISettings.tsx index 1e2b42b..075a427 100644 --- a/client/src/components/Settings/UISettings/UISettings.tsx +++ b/client/src/components/Settings/UISettings/UISettings.tsx @@ -16,13 +16,14 @@ import { InputGroup, Button, SettingsHeadline } from '../../UI'; import { otherSettingsTemplate, inputHandler } from '../../../utility'; export const UISettings = (): JSX.Element => { - const { loading, config } = useSelector((state: State) => state.config); + const { + config: { loading, config }, + bookmarks: { categories }, + } = useSelector((state: State) => state); const dispatch = useDispatch(); - const { updateConfig, sortApps, sortCategories } = bindActionCreators( - actionCreators, - dispatch - ); + const { updateConfig, sortApps, sortCategories, sortBookmarks } = + bindActionCreators(actionCreators, dispatch); // Initial state const [formData, setFormData] = useState( @@ -46,9 +47,15 @@ export const UISettings = (): JSX.Element => { // Update local page title document.title = formData.customTitle; - // Sort apps and categories with new settings - sortApps(); - sortCategories(); + // Sort entities with new settings + if (formData.useOrdering !== config.useOrdering) { + sortApps(); + sortCategories(); + + for (let { id } of categories) { + sortBookmarks(id); + } + } }; // Input handler diff --git a/client/src/interfaces/Bookmark.ts b/client/src/interfaces/Bookmark.ts index db10380..858101c 100644 --- a/client/src/interfaces/Bookmark.ts +++ b/client/src/interfaces/Bookmark.ts @@ -8,4 +8,6 @@ export interface NewBookmark { isPublic: boolean; } -export interface Bookmark extends Model, NewBookmark {} +export interface Bookmark extends Model, NewBookmark { + orderId: number; +} diff --git a/client/src/store/action-creators/bookmark.ts b/client/src/store/action-creators/bookmark.ts index f2a17e5..5f077a6 100644 --- a/client/src/store/action-creators/bookmark.ts +++ b/client/src/store/action-creators/bookmark.ts @@ -23,6 +23,7 @@ import { ReorderCategoriesAction, SetEditBookmarkAction, SetEditCategoryAction, + SortBookmarksAction, SortCategoriesAction, UpdateBookmarkAction, UpdateCategoryAction, @@ -100,6 +101,8 @@ export const addBookmark = type: ActionType.addBookmark, payload: res.data.data, }); + + dispatch(sortBookmarks(res.data.data.categoryId)); } catch (err) { console.log(err); } @@ -271,6 +274,8 @@ export const updateBookmark = payload: res.data.data, }); } + + dispatch(sortBookmarks(res.data.data.categoryId)); } catch (err) { console.log(err); } @@ -377,3 +382,20 @@ export const reorderBookmarks = console.log(err); } }; + +export const sortBookmarks = + (categoryId: number) => async (dispatch: Dispatch) => { + try { + const res = await axios.get>('/api/config'); + + dispatch({ + type: ActionType.sortBookmarks, + payload: { + orderType: res.data.data.useOrdering, + categoryId, + }, + }); + } catch (err) { + console.log(err); + } + }; diff --git a/client/src/store/action-types/index.ts b/client/src/store/action-types/index.ts index 80853c4..4be159f 100644 --- a/client/src/store/action-types/index.ts +++ b/client/src/store/action-types/index.ts @@ -41,6 +41,7 @@ export enum ActionType { updateBookmark = 'UPDATE_BOOKMARK', setEditBookmark = 'SET_EDIT_BOOKMARK', reorderBookmarks = 'REORDER_BOOKMARKS', + sortBookmarks = 'SORT_BOOKMARKS', // AUTH login = 'LOGIN', logout = 'LOGOUT', diff --git a/client/src/store/actions/bookmark.ts b/client/src/store/actions/bookmark.ts index d9c89e8..7c9e1f2 100644 --- a/client/src/store/actions/bookmark.ts +++ b/client/src/store/actions/bookmark.ts @@ -74,3 +74,11 @@ export interface ReorderBookmarksAction { categoryId: number; }; } + +export interface SortBookmarksAction { + type: ActionType.sortBookmarks; + payload: { + orderType: string; + categoryId: number; + }; +} diff --git a/client/src/store/actions/index.ts b/client/src/store/actions/index.ts index 22d33d3..bd0b360 100644 --- a/client/src/store/actions/index.ts +++ b/client/src/store/actions/index.ts @@ -41,6 +41,7 @@ import { SetEditCategoryAction, SetEditBookmarkAction, ReorderBookmarksAction, + SortBookmarksAction, } from './bookmark'; import { @@ -87,6 +88,7 @@ export type Action = | UpdateBookmarkAction | SetEditBookmarkAction | ReorderBookmarksAction + | SortBookmarksAction // Auth | LoginAction | LogoutAction diff --git a/client/src/store/reducers/bookmark.ts b/client/src/store/reducers/bookmark.ts index 3c069f0..7f2510c 100644 --- a/client/src/store/reducers/bookmark.ts +++ b/client/src/store/reducers/bookmark.ts @@ -216,6 +216,29 @@ export const bookmarksReducer = ( }; } + case ActionType.sortBookmarks: { + const categoryIdx = state.categories.findIndex( + (category) => category.id === action.payload.categoryId + ); + + const sortedBookmarks = sortData( + state.categories[categoryIdx].bookmarks, + action.payload.orderType + ); + + return { + ...state, + categories: [ + ...state.categories.slice(0, categoryIdx), + { + ...state.categories[categoryIdx], + bookmarks: sortedBookmarks, + }, + ...state.categories.slice(categoryIdx + 1), + ], + }; + } + default: return state; } diff --git a/client/src/utility/templateObjects/bookmarkTemplate.ts b/client/src/utility/templateObjects/bookmarkTemplate.ts index d2b1749..a359392 100644 --- a/client/src/utility/templateObjects/bookmarkTemplate.ts +++ b/client/src/utility/templateObjects/bookmarkTemplate.ts @@ -13,4 +13,5 @@ export const bookmarkTemplate: Bookmark = { id: -1, createdAt: new Date(), updatedAt: new Date(), + orderId: 0, }; diff --git a/controllers/categories/getAllCategories.js b/controllers/categories/getAllCategories.js index eb0eb24..7bde6ba 100644 --- a/controllers/categories/getAllCategories.js +++ b/controllers/categories/getAllCategories.js @@ -18,8 +18,14 @@ const getAllCategories = asyncWrapper(async (req, res, next) => { const order = orderType == 'name' - ? [[Sequelize.fn('lower', Sequelize.col('bookmarks.name')), 'ASC']] - : [[{ model: Bookmark, as: 'bookmarks' }, orderType, 'ASC']]; + ? [ + [Sequelize.fn('lower', Sequelize.col('Category.name')), 'ASC'], + [Sequelize.fn('lower', Sequelize.col('bookmarks.name')), 'ASC'], + ] + : [ + [orderType, 'ASC'], + [{ model: Bookmark, as: 'bookmarks' }, orderType, 'ASC'], + ]; categories = categories = await Category.findAll({ include: [ From fcf2b87d1c100e8b159f3e26d39677010e9551a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Fri, 26 Nov 2021 14:40:52 +0100 Subject: [PATCH 11/11] Pushed version 2.1.0 --- .env | 2 +- CHANGELOG.md | 6 ++++-- client/.env | 2 +- client/src/components/Bookmarks/Form/BookmarksForm.tsx | 3 +-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 9f1bd80..db3637d 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ PORT=5005 NODE_ENV=development -VERSION=2.0.1 +VERSION=2.1.0 PASSWORD=flame_password SECRET=e02eb43d69953658c6d07311d6313f2d4467672cb881f96b29368ba1f3f4da4b \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 27a7641..329b2dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ -### v2.0.2 (TBA) +### v2.1.0 (2021-11-26) +- Added option to set custom order for bookmarks ([#43](https://github.com/pawelmalak/flame/issues/43)) and ([#187](https://github.com/pawelmalak/flame/issues/187)) - Added support for .ico files for custom icons ([#209](https://github.com/pawelmalak/flame/issues/209)) - Empty apps and categories sections will now be hidden from guests ([#210](https://github.com/pawelmalak/flame/issues/210)) - Fixed bug with fahrenheit degrees being displayed as float ([#221](https://github.com/pawelmalak/flame/issues/221)) -- Added option to change visibilty of apps and categories directly from table view +- Fixed bug with alphabetical order not working for bookmarks until the page was refreshed ([#224](https://github.com/pawelmalak/flame/issues/224)) +- Added option to change visibilty of apps, categories and bookmarks directly from table view - Password input will now autofocus when visiting /settings/app ### v2.0.1 (2021-11-19) diff --git a/client/.env b/client/.env index aac1ed1..d6fe0e5 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=2.0.1 \ No newline at end of file +REACT_APP_VERSION=2.1.0 \ No newline at end of file diff --git a/client/src/components/Bookmarks/Form/BookmarksForm.tsx b/client/src/components/Bookmarks/Form/BookmarksForm.tsx index 65f658d..893b334 100644 --- a/client/src/components/Bookmarks/Form/BookmarksForm.tsx +++ b/client/src/components/Bookmarks/Form/BookmarksForm.tsx @@ -139,8 +139,7 @@ export const BookmarksForm = ({ modalHandler(); } - setFormData(newBookmarkTemplate); - + setFormData({ ...newBookmarkTemplate, categoryId: formData.categoryId }); setCustomIcon(null); };