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; margin-bottom: 20px;
} }
.Message span { .Message a {
color: var(--color-accent); color: var(--color-accent);
} }
.Message span:hover { .Message a:hover {
cursor: pointer; cursor: pointer;
} }

View File

@ -1,22 +1,52 @@
import { KeyboardEvent } from 'react'; import { Fragment, KeyboardEvent, useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { App, GlobalState } from '../../../interfaces';
import { pinApp, deleteApp, reorderApp } from '../../../store/actions';
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd'; 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'; import classes from './AppTable.module.css';
// UI
import Icon from '../../UI/Icons/Icon/Icon'; import Icon from '../../UI/Icons/Icon/Icon';
import Table from '../../UI/Table/Table'; import Table from '../../UI/Table/Table';
// Utils
import { searchConfig } from '../../../utility';
interface ComponentProps { interface ComponentProps {
apps: App[]; apps: App[];
pinApp: (app: App) => void; pinApp: (app: App) => void;
deleteApp: (id: number) => void; deleteApp: (id: number) => void;
updateAppHandler: (app: App) => 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 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 deleteAppHandler = (app: App): void => {
const proceed = window.confirm(`Are you sure you want to delete ${app.name} at ${app.url} ?`); const proceed = window.confirm(`Are you sure you want to delete ${app.name} at ${app.url} ?`);
@ -25,6 +55,7 @@ const AppTable = (props: ComponentProps): JSX.Element => {
} }
} }
// Support keyboard navigation for actions
const keyboardActionHandler = (e: KeyboardEvent, app: App, handler: Function) => { const keyboardActionHandler = (e: KeyboardEvent, app: App, handler: Function) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
handler(app); handler(app);
@ -32,88 +63,103 @@ const AppTable = (props: ComponentProps): JSX.Element => {
} }
const dragEndHanlder = (result: DropResult): void => { const dragEndHanlder = (result: DropResult): void => {
console.log(result); if (!isCustomOrder) {
props.createNotification({
title: 'Error',
message: 'Custom order is disabled'
})
return;
}
if (!result.destination) { if (!result.destination) {
return; return;
} }
const tmpApps = [...props.apps]; const tmpApps = [...localApps];
const [movedApp] = tmpApps.splice(result.source.index, 1); const [movedApp] = tmpApps.splice(result.source.index, 1);
tmpApps.splice(result.destination.index, 0, movedApp); tmpApps.splice(result.destination.index, 0, movedApp);
props.reorderApp(tmpApps); setLocalApps(tmpApps);
props.reorderApps(tmpApps);
} }
return ( return (
<DragDropContext onDragEnd={dragEndHanlder}> <Fragment>
<Droppable droppableId='apps'> <div className={classes.Message}>
{(provided) => ( {isCustomOrder
<Table headers={[ ? <p>You can drag and drop single rows to reorder application</p>
'Name', : <p>Custom order is disabled. You can change it in <Link to='/settings/other'>settings</Link></p>
'URL', }
'Icon', </div>
'Actions' <DragDropContext onDragEnd={dragEndHanlder}>
]} <Droppable droppableId='apps'>
innerRef={provided.innerRef}> {(provided) => (
{props.apps.map((app: App, index): JSX.Element => { <Table headers={[
return ( 'Name',
<Draggable key={app.id} draggableId={app.id.toString()} index={index}> 'URL',
{(provided, snapshot) => { 'Icon',
const style = { 'Actions'
border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none', ]}
borderRadius: '4px', innerRef={provided.innerRef}>
...provided.draggableProps.style, {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 ( return (
<tr <tr
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
ref={provided.innerRef} ref={provided.innerRef}
style={style} style={style}
> >
<td style={{width:'200px'}}>{app.name}</td> <td style={{ width:'200px' }}>{app.name}</td>
<td style={{width:'200px'}}>{app.url}</td> <td style={{ width:'200px' }}>{app.url}</td>
<td style={{width:'200px'}}>{app.icon}</td> <td style={{ width:'200px' }}>{app.icon}</td>
{!snapshot.isDragging && ( {!snapshot.isDragging && (
<td className={classes.TableActions}> <td className={classes.TableActions}>
<div <div
className={classes.TableAction} className={classes.TableAction}
onClick={() => deleteAppHandler(app)} onClick={() => deleteAppHandler(app)}
onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)} onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)}
tabIndex={0}> tabIndex={0}>
<Icon icon='mdiDelete' /> <Icon icon='mdiDelete' />
</div> </div>
<div <div
className={classes.TableAction} className={classes.TableAction}
onClick={() => props.updateAppHandler(app)} onClick={() => props.updateAppHandler(app)}
onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)} onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)}
tabIndex={0}> tabIndex={0}>
<Icon icon='mdiPencil' /> <Icon icon='mdiPencil' />
</div> </div>
<div <div
className={classes.TableAction} className={classes.TableAction}
onClick={() => props.pinApp(app)} onClick={() => props.pinApp(app)}
onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)} onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)}
tabIndex={0}> tabIndex={0}>
{app.isPinned {app.isPinned
? <Icon icon='mdiPinOff' color='var(--color-accent)' /> ? <Icon icon='mdiPinOff' color='var(--color-accent)' />
: <Icon icon='mdiPin' /> : <Icon icon='mdiPin' />
} }
</div> </div>
</td> </td>
)} )}
</tr> </tr>
) )
}} }}
</Draggable> </Draggable>
) )
})} })}
</Table> </Table>
)} )}
</Droppable> </Droppable>
</DragDropContext> </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 orderId: index + 1
})) }))
await axios.put<{}>('/api/apps/0/reorder', updateQuery); await axios.put<ApiResponse<{}>>('/api/apps/0/reorder', updateQuery);
dispatch<CreateNotificationAction>({
type: ActionTypes.createNotification,
payload: {
title: 'Success',
message: 'New order saved'
}
})
dispatch<ReorderAppsAction>({ dispatch<ReorderAppsAction>({
type: ActionTypes.reorderApps, type: ActionTypes.reorderApps,
@ -189,8 +181,6 @@ export const sortApps = () => async (dispatch: Dispatch) => {
try { try {
const res = await axios.get<ApiResponse<Config>>('/api/config/useOrdering'); const res = await axios.get<ApiResponse<Config>>('/api/config/useOrdering');
console.log(res.data.data);
dispatch<SortAppsAction>({ dispatch<SortAppsAction>({
type: ActionTypes.sortApps, type: ActionTypes.sortApps,
payload: res.data.data.value payload: res.data.data.value