mirror of
https://github.com/pawelmalak/flame.git
synced 2024-12-21 09:11:29 +03:00
Sorting and custom ordering for categories
This commit is contained in:
parent
5b900872af
commit
ae3141e37b
@ -1 +1 @@
|
||||
REACT_APP_VERSION=1.3.6
|
||||
REACT_APP_VERSION=1.3.7
|
@ -10,3 +10,20 @@
|
||||
.TableAction:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Message {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: baseline;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.Message a {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.Message a:hover {
|
||||
cursor: pointer;
|
||||
}
|
@ -1,13 +1,25 @@
|
||||
import { ContentType } from '../Bookmarks';
|
||||
import classes from './BookmarkTable.module.css';
|
||||
import { connect } from 'react-redux';
|
||||
import { pinCategory, deleteCategory, deleteBookmark } from '../../../store/actions';
|
||||
import { KeyboardEvent } from 'react';
|
||||
import { KeyboardEvent, useState, useEffect, Fragment } from 'react';
|
||||
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||
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 { Bookmark, Category } from '../../../interfaces';
|
||||
import Icon from '../../UI/Icons/Icon/Icon';
|
||||
|
||||
// Utils
|
||||
import { searchConfig } from '../../../utility';
|
||||
|
||||
interface ComponentProps {
|
||||
contentType: ContentType;
|
||||
categories: Category[];
|
||||
@ -15,9 +27,28 @@ interface ComponentProps {
|
||||
deleteCategory: (id: number) => void;
|
||||
updateHandler: (data: Category | Bookmark) => void;
|
||||
deleteBookmark: (bookmarkId: number, categoryId: number) => void;
|
||||
createNotification: (notification: NewNotification) => void;
|
||||
reorderCategories: (categories: Category[]) => void;
|
||||
}
|
||||
|
||||
const BookmarkTable = (props: ComponentProps): JSX.Element => {
|
||||
const [localCategories, setLocalCategories] = useState<Category[]>([]);
|
||||
const [isCustomOrder, setIsCustomOrder] = useState<boolean>(false);
|
||||
|
||||
// Copy categories array
|
||||
useEffect(() => {
|
||||
setLocalCategories([...props.categories]);
|
||||
}, [props.categories])
|
||||
|
||||
// Check ordering
|
||||
useEffect(() => {
|
||||
const order = searchConfig('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`);
|
||||
|
||||
@ -40,46 +71,100 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
|
||||
}
|
||||
}
|
||||
|
||||
const dragEndHanlder = (result: DropResult): void => {
|
||||
if (!isCustomOrder) {
|
||||
props.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);
|
||||
props.reorderCategories(tmpCategories);
|
||||
}
|
||||
|
||||
if (props.contentType === ContentType.category) {
|
||||
return (
|
||||
<Table headers={[
|
||||
'Name',
|
||||
'Actions'
|
||||
]}>
|
||||
{props.categories.map((category: Category) => {
|
||||
return (
|
||||
<tr key={category.id}>
|
||||
<td>{category.name}</td>
|
||||
<td className={classes.TableActions}>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => deleteCategoryHandler(category)}
|
||||
onKeyDown={(e) => keyboardActionHandler(e, category, deleteCategoryHandler)}
|
||||
tabIndex={0}>
|
||||
<Icon icon='mdiDelete' />
|
||||
</div>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => props.updateHandler(category)}
|
||||
// onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)}
|
||||
tabIndex={0}>
|
||||
<Icon icon='mdiPencil' />
|
||||
</div>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => props.pinCategory(category)}
|
||||
onKeyDown={(e) => keyboardActionHandler(e, category, props.pinCategory)}
|
||||
tabIndex={0}>
|
||||
{category.isPinned
|
||||
? <Icon icon='mdiPinOff' color='var(--color-accent)' />
|
||||
: <Icon icon='mdiPin' />
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</Table>
|
||||
<Fragment>
|
||||
<div className={classes.Message}>
|
||||
{isCustomOrder
|
||||
? <p>You can drag and drop single rows to reorder categories</p>
|
||||
: <p>Custom order is disabled. You can change it in <Link to='/settings/other'>settings</Link></p>
|
||||
}
|
||||
</div>
|
||||
<DragDropContext onDragEnd={dragEndHanlder}>
|
||||
<Droppable droppableId='categories'>
|
||||
{(provided) => (
|
||||
<Table headers={[
|
||||
'Name',
|
||||
'Actions'
|
||||
]}
|
||||
innerRef={provided.innerRef}>
|
||||
{localCategories.map((category: Category, index): JSX.Element => {
|
||||
return (
|
||||
<Draggable key={category.id} draggableId={category.id.toString()} index={index}>
|
||||
{(provided, snapshot) => {
|
||||
const style = {
|
||||
border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none',
|
||||
borderRadius: '4px',
|
||||
...provided.draggableProps.style,
|
||||
};
|
||||
|
||||
return (
|
||||
<tr
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
style={style}
|
||||
>
|
||||
<td>{category.name}</td>
|
||||
{!snapshot.isDragging && (
|
||||
<td className={classes.TableActions}>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => deleteCategoryHandler(category)}
|
||||
onKeyDown={(e) => keyboardActionHandler(e, category, deleteCategoryHandler)}
|
||||
tabIndex={0}>
|
||||
<Icon icon='mdiDelete' />
|
||||
</div>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => props.updateHandler(category)}
|
||||
tabIndex={0}>
|
||||
<Icon icon='mdiPencil' />
|
||||
</div>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => props.pinCategory(category)}
|
||||
onKeyDown={(e) => keyboardActionHandler(e, category, props.pinCategory)}
|
||||
tabIndex={0}>
|
||||
{category.isPinned
|
||||
? <Icon icon='mdiPinOff' color='var(--color-accent)' />
|
||||
: <Icon icon='mdiPin' />
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
)
|
||||
}}
|
||||
</Draggable>
|
||||
)
|
||||
})}
|
||||
</Table>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</Fragment>
|
||||
)
|
||||
} else {
|
||||
const bookmarks: {bookmark: Bookmark, categoryName: string}[] = [];
|
||||
@ -111,14 +196,12 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => deleteBookmarkHandler(bookmark.bookmark)}
|
||||
// onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)}
|
||||
tabIndex={0}>
|
||||
<Icon icon='mdiDelete' />
|
||||
</div>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => props.updateHandler(bookmark.bookmark)}
|
||||
// onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)}
|
||||
tabIndex={0}>
|
||||
<Icon icon='mdiPencil' />
|
||||
</div>
|
||||
@ -131,4 +214,12 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(null, { pinCategory, deleteCategory, deleteBookmark })(BookmarkTable);
|
||||
const actions = {
|
||||
pinCategory,
|
||||
deleteCategory,
|
||||
deleteBookmark,
|
||||
createNotification,
|
||||
reorderCategories
|
||||
}
|
||||
|
||||
export default connect(null, actions)(BookmarkTable);
|
@ -43,6 +43,7 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
|
||||
name: '',
|
||||
id: -1,
|
||||
isPinned: false,
|
||||
orderId: 0,
|
||||
bookmarks: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
|
@ -2,7 +2,7 @@ import { useState, useEffect, ChangeEvent, FormEvent } from 'react';
|
||||
|
||||
// Redux
|
||||
import { connect } from 'react-redux';
|
||||
import { createNotification, updateConfig, sortApps } from '../../../store/actions';
|
||||
import { createNotification, updateConfig, sortApps, sortCategories } from '../../../store/actions';
|
||||
|
||||
// Typescript
|
||||
import { GlobalState, NewNotification, SettingsForm } from '../../../interfaces';
|
||||
@ -18,6 +18,7 @@ interface ComponentProps {
|
||||
createNotification: (notification: NewNotification) => void;
|
||||
updateConfig: (formData: SettingsForm) => void;
|
||||
sortApps: () => void;
|
||||
sortCategories: () => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
@ -52,8 +53,9 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
// Update local page title
|
||||
document.title = formData.customTitle;
|
||||
|
||||
// Get sorted apps
|
||||
// Sort apps and categories with new settings
|
||||
props.sortApps();
|
||||
props.sortCategories();
|
||||
}
|
||||
|
||||
// Input handler
|
||||
@ -143,4 +145,11 @@ const mapStateToProps = (state: GlobalState) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, { createNotification, updateConfig, sortApps })(OtherSettings);
|
||||
const actions = {
|
||||
createNotification,
|
||||
updateConfig,
|
||||
sortApps,
|
||||
sortCategories
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, actions)(OtherSettings);
|
@ -15,6 +15,8 @@ import {
|
||||
PinCategoryAction,
|
||||
DeleteCategoryAction,
|
||||
UpdateCategoryAction,
|
||||
SortCategoriesAction,
|
||||
ReorderCategoriesAction,
|
||||
// Bookmarks
|
||||
AddBookmarkAction,
|
||||
DeleteBookmarkAction,
|
||||
@ -49,6 +51,8 @@ export enum ActionTypes {
|
||||
pinCategory = 'PIN_CATEGORY',
|
||||
deleteCategory = 'DELETE_CATEGORY',
|
||||
updateCategory = 'UPDATE_CATEGORY',
|
||||
sortCategories = 'SORT_CATEGORIES',
|
||||
reorderCategories = 'REORDER_CATEGORIES',
|
||||
// Bookmarks
|
||||
addBookmark = 'ADD_BOOKMARK',
|
||||
deleteBookmark = 'DELETE_BOOKMARK',
|
||||
@ -78,6 +82,8 @@ export type Action =
|
||||
PinCategoryAction |
|
||||
DeleteCategoryAction |
|
||||
UpdateCategoryAction |
|
||||
SortCategoriesAction |
|
||||
ReorderCategoriesAction |
|
||||
// Bookmarks
|
||||
AddBookmarkAction |
|
||||
DeleteBookmarkAction |
|
||||
|
@ -174,7 +174,7 @@ export const reorderApps = (apps: App[]) => async (dispatch: Dispatch) => {
|
||||
|
||||
export interface SortAppsAction {
|
||||
type: ActionTypes.sortApps;
|
||||
payload: {};
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export const sortApps = () => async (dispatch: Dispatch) => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import { Dispatch } from 'redux';
|
||||
import { ActionTypes } from './actionTypes';
|
||||
import { Category, ApiResponse, NewCategory, Bookmark, NewBookmark } from '../../interfaces';
|
||||
import { Category, ApiResponse, NewCategory, Bookmark, NewBookmark, Config } from '../../interfaces';
|
||||
import { CreateNotificationAction } from './notification';
|
||||
|
||||
/**
|
||||
@ -54,6 +54,8 @@ export const addCategory = (formData: NewCategory) => async (dispatch: Dispatch)
|
||||
type: ActionTypes.addCategory,
|
||||
payload: res.data.data
|
||||
})
|
||||
|
||||
dispatch<any>(sortCategories());
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
@ -173,6 +175,8 @@ export const updateCategory = (id: number, formData: NewCategory) => async (disp
|
||||
type: ActionTypes.updateCategory,
|
||||
payload: res.data.data
|
||||
})
|
||||
|
||||
dispatch<any>(sortCategories());
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
@ -262,3 +266,59 @@ export const updateBookmark = (bookmarkId: number, formData: NewBookmark, previo
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SORT CATEGORIES
|
||||
*/
|
||||
export interface SortCategoriesAction {
|
||||
type: ActionTypes.sortCategories;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export const sortCategories = () => async (dispatch: Dispatch) => {
|
||||
try {
|
||||
const res = await axios.get<ApiResponse<Config>>('/api/config/useOrdering');
|
||||
|
||||
dispatch<SortCategoriesAction>({
|
||||
type: ActionTypes.sortCategories,
|
||||
payload: res.data.data.value
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* REORDER CATEGORIES
|
||||
*/
|
||||
export interface ReorderCategoriesAction {
|
||||
type: ActionTypes.reorderCategories;
|
||||
payload: Category[];
|
||||
}
|
||||
|
||||
interface ReorderQuery {
|
||||
categories: {
|
||||
id: number;
|
||||
orderId: number;
|
||||
}[]
|
||||
}
|
||||
|
||||
export const reorderCategories = (categories: Category[]) => async (dispatch: Dispatch) => {
|
||||
try {
|
||||
const updateQuery: ReorderQuery = { categories: [] }
|
||||
|
||||
categories.forEach((category, index) => updateQuery.categories.push({
|
||||
id: category.id,
|
||||
orderId: index + 1
|
||||
}))
|
||||
|
||||
await axios.put<ApiResponse<{}>>('/api/categories/0/reorder', updateQuery);
|
||||
|
||||
dispatch<ReorderCategoriesAction>({
|
||||
type: ActionTypes.reorderCategories,
|
||||
payload: categories
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { ActionTypes, Action } from '../actions';
|
||||
import { Category, Bookmark } from '../../interfaces';
|
||||
import { sortData } from '../../utility';
|
||||
|
||||
export interface State {
|
||||
loading: boolean;
|
||||
@ -141,6 +142,22 @@ const updateBookmark = (state: State, action: Action): State => {
|
||||
}
|
||||
}
|
||||
|
||||
const sortCategories = (state: State, action: Action): State => {
|
||||
const sortedCategories = sortData<Category>(state.categories, action.payload);
|
||||
|
||||
return {
|
||||
...state,
|
||||
categories: sortedCategories
|
||||
}
|
||||
}
|
||||
|
||||
const reorderCategories = (state: State, action: Action): State => {
|
||||
return {
|
||||
...state,
|
||||
categories: action.payload
|
||||
}
|
||||
}
|
||||
|
||||
const bookmarkReducer = (state = initialState, action: Action) => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.getCategories: return getCategories(state, action);
|
||||
@ -152,6 +169,8 @@ const bookmarkReducer = (state = initialState, action: Action) => {
|
||||
case ActionTypes.updateCategory: 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);
|
||||
default: return state;
|
||||
}
|
||||
}
|
||||
|
@ -37,14 +37,32 @@ exports.createCategory = asyncWrapper(async (req, res, next) => {
|
||||
// @route GET /api/categories
|
||||
// @access Public
|
||||
exports.getCategories = asyncWrapper(async (req, res, next) => {
|
||||
const categories = await Category.findAll({
|
||||
include: [{
|
||||
model: Bookmark,
|
||||
as: 'bookmarks'
|
||||
}],
|
||||
order: [[ Sequelize.fn('lower', Sequelize.col('Category.name')), 'ASC' ]]
|
||||
// Get config from database
|
||||
const useOrdering = await Config.findOne({
|
||||
where: { key: 'useOrdering' }
|
||||
});
|
||||
|
||||
const orderType = useOrdering ? useOrdering.value : 'createdAt';
|
||||
let categories;
|
||||
|
||||
if (orderType == 'name') {
|
||||
categories = await Category.findAll({
|
||||
include: [{
|
||||
model: Bookmark,
|
||||
as: 'bookmarks'
|
||||
}],
|
||||
order: [[ Sequelize.fn('lower', Sequelize.col('Category.name')), 'ASC' ]]
|
||||
});
|
||||
} else {
|
||||
categories = await Category.findAll({
|
||||
include: [{
|
||||
model: Bookmark,
|
||||
as: 'bookmarks'
|
||||
}],
|
||||
order: [[ orderType, 'ASC' ]]
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: categories
|
||||
@ -124,3 +142,19 @@ exports.deleteCategory = asyncWrapper(async (req, res, next) => {
|
||||
data: {}
|
||||
})
|
||||
})
|
||||
|
||||
// @desc Reorder categories
|
||||
// @route PUT /api/categories/0/reorder
|
||||
// @access Public
|
||||
exports.reorderCategories = asyncWrapper(async (req, res, next) => {
|
||||
req.body.categories.forEach(async ({ id, orderId }) => {
|
||||
await Category.update({ orderId }, {
|
||||
where: { id }
|
||||
})
|
||||
})
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {}
|
||||
})
|
||||
})
|
@ -6,7 +6,8 @@ const {
|
||||
getCategories,
|
||||
getCategory,
|
||||
updateCategory,
|
||||
deleteCategory
|
||||
deleteCategory,
|
||||
reorderCategories
|
||||
} = require('../controllers/category');
|
||||
|
||||
router
|
||||
@ -20,4 +21,8 @@ router
|
||||
.put(updateCategory)
|
||||
.delete(deleteCategory);
|
||||
|
||||
router
|
||||
.route('/0/reorder')
|
||||
.put(reorderCategories);
|
||||
|
||||
module.exports = router;
|
Loading…
Reference in New Issue
Block a user