Apps reordering with drag-and-drop functionality

This commit is contained in:
unknown 2021-06-18 12:09:59 +02:00
parent 754dc3a7b9
commit 5b900872af
4 changed files with 134 additions and 90 deletions

View file

@ -1 +1 @@
REACT_APP_VERSION=1.3.5
REACT_APP_VERSION=1.3.6

View file

@ -20,10 +20,10 @@
margin-bottom: 20px;
}
.Message span {
.Message a {
color: var(--color-accent);
}
.Message span:hover {
.Message a:hover {
cursor: pointer;
}

View file

@ -1,22 +1,52 @@
import { KeyboardEvent } from 'react';
import { connect } from 'react-redux';
import { App, GlobalState } from '../../../interfaces';
import { pinApp, deleteApp, reorderApp } from '../../../store/actions';
import { Fragment, KeyboardEvent, useState, useEffect } from 'react';
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
import { Link } from 'react-router-dom';
// Redux
import { connect } from 'react-redux';
import { pinApp, deleteApp, reorderApps, updateConfig, createNotification } from '../../../store/actions';
// Typescript
import { App, GlobalState, NewNotification } from '../../../interfaces';
// CSS
import classes from './AppTable.module.css';
// UI
import Icon from '../../UI/Icons/Icon/Icon';
import Table from '../../UI/Table/Table';
// Utils
import { searchConfig } from '../../../utility';
interface ComponentProps {
apps: App[];
pinApp: (app: App) => void;
deleteApp: (id: number) => void;
updateAppHandler: (app: App) => void;
reorderApp: (apps: App[]) => void;
reorderApps: (apps: App[]) => void;
updateConfig: (formData: any) => void;
createNotification: (notification: NewNotification) => void;
}
const AppTable = (props: ComponentProps): JSX.Element => {
const [localApps, setLocalApps] = useState<App[]>([]);
const [isCustomOrder, setIsCustomOrder] = useState<boolean>(false);
// Copy apps array
useEffect(() => {
setLocalApps([...props.apps]);
}, [props.apps])
// Check ordering
useEffect(() => {
const order = searchConfig('useOrdering', '');
if (order === 'orderId') {
setIsCustomOrder(true);
}
}, [])
const deleteAppHandler = (app: App): void => {
const proceed = window.confirm(`Are you sure you want to delete ${app.name} at ${app.url} ?`);
@ -25,6 +55,7 @@ const AppTable = (props: ComponentProps): JSX.Element => {
}
}
// Support keyboard navigation for actions
const keyboardActionHandler = (e: KeyboardEvent, app: App, handler: Function) => {
if (e.key === 'Enter') {
handler(app);
@ -32,88 +63,103 @@ const AppTable = (props: ComponentProps): JSX.Element => {
}
const dragEndHanlder = (result: DropResult): void => {
console.log(result);
if (!isCustomOrder) {
props.createNotification({
title: 'Error',
message: 'Custom order is disabled'
})
return;
}
if (!result.destination) {
return;
}
const tmpApps = [...props.apps];
const tmpApps = [...localApps];
const [movedApp] = tmpApps.splice(result.source.index, 1);
tmpApps.splice(result.destination.index, 0, movedApp);
props.reorderApp(tmpApps);
setLocalApps(tmpApps);
props.reorderApps(tmpApps);
}
return (
<DragDropContext onDragEnd={dragEndHanlder}>
<Droppable droppableId='apps'>
{(provided) => (
<Table headers={[
'Name',
'URL',
'Icon',
'Actions'
]}
innerRef={provided.innerRef}>
{props.apps.map((app: App, index): JSX.Element => {
return (
<Draggable key={app.id} draggableId={app.id.toString()} index={index}>
{(provided, snapshot) => {
const style = {
border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none',
borderRadius: '4px',
...provided.draggableProps.style,
};
<Fragment>
<div className={classes.Message}>
{isCustomOrder
? <p>You can drag and drop single rows to reorder application</p>
: <p>Custom order is disabled. You can change it in <Link to='/settings/other'>settings</Link></p>
}
</div>
<DragDropContext onDragEnd={dragEndHanlder}>
<Droppable droppableId='apps'>
{(provided) => (
<Table headers={[
'Name',
'URL',
'Icon',
'Actions'
]}
innerRef={provided.innerRef}>
{localApps.map((app: App, index): JSX.Element => {
return (
<Draggable key={app.id} draggableId={app.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 style={{width:'200px'}}>{app.name}</td>
<td style={{width:'200px'}}>{app.url}</td>
<td style={{width:'200px'}}>{app.icon}</td>
{!snapshot.isDragging && (
<td className={classes.TableActions}>
<div
className={classes.TableAction}
onClick={() => deleteAppHandler(app)}
onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)}
tabIndex={0}>
<Icon icon='mdiDelete' />
</div>
<div
className={classes.TableAction}
onClick={() => props.updateAppHandler(app)}
onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)}
tabIndex={0}>
<Icon icon='mdiPencil' />
</div>
<div
className={classes.TableAction}
onClick={() => props.pinApp(app)}
onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)}
tabIndex={0}>
{app.isPinned
? <Icon icon='mdiPinOff' color='var(--color-accent)' />
: <Icon icon='mdiPin' />
}
</div>
</td>
)}
</tr>
)
}}
</Draggable>
)
})}
</Table>
)}
</Droppable>
</DragDropContext>
return (
<tr
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
style={style}
>
<td style={{ width:'200px' }}>{app.name}</td>
<td style={{ width:'200px' }}>{app.url}</td>
<td style={{ width:'200px' }}>{app.icon}</td>
{!snapshot.isDragging && (
<td className={classes.TableActions}>
<div
className={classes.TableAction}
onClick={() => deleteAppHandler(app)}
onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)}
tabIndex={0}>
<Icon icon='mdiDelete' />
</div>
<div
className={classes.TableAction}
onClick={() => props.updateAppHandler(app)}
onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)}
tabIndex={0}>
<Icon icon='mdiPencil' />
</div>
<div
className={classes.TableAction}
onClick={() => props.pinApp(app)}
onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)}
tabIndex={0}>
{app.isPinned
? <Icon icon='mdiPinOff' color='var(--color-accent)' />
: <Icon icon='mdiPin' />
}
</div>
</td>
)}
</tr>
)
}}
</Draggable>
)
})}
</Table>
)}
</Droppable>
</DragDropContext>
</Fragment>
)
}
@ -123,4 +169,12 @@ const mapStateToProps = (state: GlobalState) => {
}
}
export default connect(mapStateToProps, { pinApp, deleteApp, reorderApp })(AppTable);
const actions = {
pinApp,
deleteApp,
reorderApps,
updateConfig,
createNotification
}
export default connect(mapStateToProps, actions)(AppTable);

View file

@ -161,15 +161,7 @@ export const reorderApps = (apps: App[]) => async (dispatch: Dispatch) => {
orderId: index + 1
}))
await axios.put<{}>('/api/apps/0/reorder', updateQuery);
dispatch<CreateNotificationAction>({
type: ActionTypes.createNotification,
payload: {
title: 'Success',
message: 'New order saved'
}
})
await axios.put<ApiResponse<{}>>('/api/apps/0/reorder', updateQuery);
dispatch<ReorderAppsAction>({
type: ActionTypes.reorderApps,
@ -189,8 +181,6 @@ export const sortApps = () => async (dispatch: Dispatch) => {
try {
const res = await axios.get<ApiResponse<Config>>('/api/config/useOrdering');
console.log(res.data.data);
dispatch<SortAppsAction>({
type: ActionTypes.sortApps,
payload: res.data.data.value