mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 10:23:54 +00:00
feat: draggable events
This commit is contained in:
@@ -30,6 +30,13 @@ export const requestPatch = async (data) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const requestReorder = async (data) => {
|
||||
const reorder = 'reorder';
|
||||
console.log('debug got reorder with payload', data);
|
||||
const res = await axios.patch(eventsURL + '/' + reorder, data);
|
||||
return res;
|
||||
};
|
||||
|
||||
export const requestDelete = async (eventId) => {
|
||||
const res = await axios.delete(eventsURL + '/' + eventId);
|
||||
return res;
|
||||
|
||||
@@ -12,7 +12,6 @@ export const sampleData = {
|
||||
events: [
|
||||
{
|
||||
id: '1',
|
||||
order: 1,
|
||||
title: 'Is the internet a fad?',
|
||||
subtitle: 'It is',
|
||||
presenter: 'Carlos Valente',
|
||||
@@ -24,13 +23,11 @@ export const sampleData = {
|
||||
},
|
||||
{
|
||||
id: 0.4849093424577693,
|
||||
order: 2,
|
||||
duration: 25*60000,
|
||||
type: 'delay',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
order: 3,
|
||||
title: 'Is reddit a dictatorship?',
|
||||
subtitle: 'It is',
|
||||
presenter: 'Carlos Valente',
|
||||
@@ -42,7 +39,6 @@ export const sampleData = {
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
order: 4,
|
||||
title: 'Out of words',
|
||||
subtitle: '',
|
||||
presenter: 'Carlos Valente',
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useInterval } from '../../../app/hooks/useInterval';
|
||||
export default function Paginator(props) {
|
||||
const { events, selectedId } = props;
|
||||
const LIMIT_PER_PAGE = props.limit || 8;
|
||||
const SCROLL_TIME = (props.time * 1000) || 5000;
|
||||
const SCROLL_TIME = props.time * 1000 || 5000;
|
||||
const SCROLL_PAST = false;
|
||||
const [numEvents, setNumEvents] = useState(0);
|
||||
const [page, setPage] = useState([]);
|
||||
@@ -42,7 +42,7 @@ export default function Paginator(props) {
|
||||
|
||||
if (s.length < 1) return;
|
||||
|
||||
setSelected(s[0].order);
|
||||
setSelected(s[0].id);
|
||||
}, [events, selectedId]);
|
||||
|
||||
// every SCROLL_TIME go to the next array
|
||||
@@ -67,8 +67,8 @@ export default function Paginator(props) {
|
||||
<div className={style.entries}>
|
||||
{page.map((e) => {
|
||||
let selectedState = 0;
|
||||
if (e.order === selected) selectedState = 1;
|
||||
else if (e.order > selected) selectedState = 2;
|
||||
if (e.id === selected) selectedState = 1;
|
||||
else if (e.id > selected) selectedState = 2;
|
||||
return (
|
||||
<TodayItem
|
||||
key={e.id}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { FiMoreVertical } from 'react-icons/fi';
|
||||
import DeleteIconBtn from '../../../common/components/buttons/DeleteIconBtn';
|
||||
import ActionButtons from './ActionButtons';
|
||||
import style from './Block.module.css';
|
||||
@@ -16,16 +18,27 @@ export default function BlockBlock(props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style.block}>
|
||||
<div className={style.actionOverlay}>
|
||||
<DeleteIconBtn clickhandler={deleteHandler} />
|
||||
<ActionButtons
|
||||
showAdd
|
||||
addHandler={addHandler}
|
||||
showDelay
|
||||
delayHandler={delayHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Draggable key={data.id} draggableId={data.id} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
className={style.block}
|
||||
{...provided.draggableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
<div className={style.actionOverlay}>
|
||||
<DeleteIconBtn clickhandler={deleteHandler} />
|
||||
<ActionButtons
|
||||
showAdd
|
||||
addHandler={addHandler}
|
||||
showDelay
|
||||
delayHandler={delayHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { FiMoreVertical } from 'react-icons/fi';
|
||||
import { millisToMinutes } from '../../../common/dateConfig';
|
||||
import TimeInput from '../../../common/input/TimeInput';
|
||||
import style from './Block.module.css';
|
||||
@@ -20,17 +22,26 @@ export default function DelayBlock(props) {
|
||||
eventsHandler('delete', data.id);
|
||||
};
|
||||
|
||||
let delayValue = (data.duration != null) ? millisToMinutes(data.duration) : undefined;
|
||||
let delayValue =
|
||||
data.duration != null ? millisToMinutes(data.duration) : undefined;
|
||||
return (
|
||||
<div className={style.delay}>
|
||||
<TimeInput
|
||||
value={delayValue}
|
||||
submitHandler={submitHandler}
|
||||
/>
|
||||
<div className={style.actionOverlay}>
|
||||
<DeleteIconBtn clickhandler={deleteHandler} />
|
||||
<ActionButtons showAdd addHandler={addHandler} />
|
||||
</div>
|
||||
</div>
|
||||
<Draggable key={data.id} draggableId={data.id} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
className={style.delay}
|
||||
{...provided.draggableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
<TimeInput value={delayValue} submitHandler={submitHandler} />
|
||||
<div className={style.actionOverlay}>
|
||||
<DeleteIconBtn clickhandler={deleteHandler} />
|
||||
<ActionButtons showAdd addHandler={addHandler} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { FiChevronDown, FiChevronUp, FiMoreVertical } from 'react-icons/fi';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import EventTimes from '../../../common/components/eventTimes/EventTimes';
|
||||
import { showErrorToast } from '../../../common/helpers/toastManager';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import EventTimes from '../../../common/components/eventTimes/EventTimes';
|
||||
import style from './Block.module.css';
|
||||
import EditableText from '../../../common/input/EditableText';
|
||||
import DelayValue from '../../../common/input/DelayValue';
|
||||
@@ -10,15 +11,11 @@ import VisibleIconBtn from '../../../common/components/buttons/VisibleIconBtn';
|
||||
import DeleteIconBtn from '../../../common/components/buttons/DeleteIconBtn';
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
let shouldNotRerender =
|
||||
prevProps.data.revision === nextProps.data.revision &&
|
||||
prevProps.selected === nextProps.selected &&
|
||||
prevProps.next === nextProps.next;
|
||||
console.log('debug memo2', shouldNotRerender);
|
||||
return (
|
||||
prevProps.data.revision === nextProps.data.revision &&
|
||||
prevProps.selected === nextProps.selected &&
|
||||
prevProps.next === nextProps.next
|
||||
prevProps.next === nextProps.next &&
|
||||
prevProps.index === nextProps.index
|
||||
);
|
||||
};
|
||||
|
||||
@@ -77,74 +74,86 @@ const EventBlock = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={selected ? style.eventRowActive : style.eventRow}>
|
||||
<span className={style.drag}>
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
<div className={style.indicators}>
|
||||
<div className={next ? style.next : style.nextDisabled}>Next</div>
|
||||
<DelayValue delay={delay} />
|
||||
</div>
|
||||
<EventTimes
|
||||
updateValues={updateValues}
|
||||
timeStart={data.timeStart}
|
||||
timeEnd={data.timeEnd}
|
||||
delay={delay}
|
||||
/>
|
||||
<div className={style.rowDetailed}>
|
||||
{more ? (
|
||||
<div className={style.detailedContainer}>
|
||||
<EditableText
|
||||
label='Title'
|
||||
defaultValue={data.title}
|
||||
placeholder='Add Title'
|
||||
submitHandler={handleTitleSubmit}
|
||||
underlined
|
||||
<Draggable key={data.id} draggableId={data.id} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
className={selected ? style.eventRowActive : style.eventRow}
|
||||
{...provided.draggableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
<div className={style.indicators}>
|
||||
<div className={next ? style.next : style.nextDisabled}>Next</div>
|
||||
<DelayValue delay={delay} />
|
||||
</div>
|
||||
<EventTimes
|
||||
updateValues={updateValues}
|
||||
timeStart={data.timeStart}
|
||||
timeEnd={data.timeEnd}
|
||||
delay={delay}
|
||||
/>
|
||||
<div className={style.rowDetailed}>
|
||||
{more ? (
|
||||
<div className={style.detailedContainer}>
|
||||
<EditableText
|
||||
label='Title'
|
||||
defaultValue={data.title}
|
||||
placeholder='Add Title'
|
||||
submitHandler={handleTitleSubmit}
|
||||
underlined
|
||||
/>
|
||||
<EditableText
|
||||
label='Presenter'
|
||||
defaultValue={data.presenter}
|
||||
placeholder='Add Presenter name'
|
||||
submitHandler={handlePresenterSubmit}
|
||||
underlined
|
||||
/>
|
||||
<EditableText
|
||||
label='Subtitle'
|
||||
defaultValue={data.subtitle}
|
||||
placeholder='Add Subtitle'
|
||||
submitHandler={handleSubtitleSubmit}
|
||||
underlined
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={style.titleContainer}>
|
||||
<EditableText
|
||||
label='Title'
|
||||
defaultValue={data.title}
|
||||
placeholder='Add Title'
|
||||
submitHandler={handleTitleSubmit}
|
||||
isTight
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={style.more} onClick={() => setMore(!more)}>
|
||||
{more ? <FiChevronUp /> : <FiChevronDown />}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.actionOverlay}>
|
||||
<VisibleIconBtn
|
||||
clickhandler={handleVisibleToggle}
|
||||
active={visible}
|
||||
/>
|
||||
<EditableText
|
||||
label='Presenter'
|
||||
defaultValue={data.presenter}
|
||||
placeholder='Add Presenter name'
|
||||
submitHandler={handlePresenterSubmit}
|
||||
underlined
|
||||
/>
|
||||
<EditableText
|
||||
label='Subtitle'
|
||||
defaultValue={data.subtitle}
|
||||
placeholder='Add Subtitle'
|
||||
submitHandler={handleSubtitleSubmit}
|
||||
underlined
|
||||
<DeleteIconBtn clickhandler={deleteHandler} />
|
||||
<ActionButtons
|
||||
showAdd
|
||||
addHandler={addHandler}
|
||||
showDelay
|
||||
delayHandler={delayHandler}
|
||||
showBlock
|
||||
blockHandler={blockHandler}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={style.titleContainer}>
|
||||
<EditableText
|
||||
label='Title'
|
||||
defaultValue={data.title}
|
||||
placeholder='Add Title'
|
||||
submitHandler={handleTitleSubmit}
|
||||
isTight
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={style.more} onClick={() => setMore(!more)}>
|
||||
{more ? <FiChevronUp /> : <FiChevronDown />}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.actionOverlay}>
|
||||
<VisibleIconBtn clickhandler={handleVisibleToggle} active={visible} />
|
||||
<DeleteIconBtn clickhandler={deleteHandler} />
|
||||
<ActionButtons
|
||||
showAdd
|
||||
addHandler={addHandler}
|
||||
showDelay
|
||||
delayHandler={delayHandler}
|
||||
showBlock
|
||||
blockHandler={blockHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(EventBlock, areEqual);
|
||||
// export default EventBlock;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import style from './List.module.css';
|
||||
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { useSocket } from '../../../app/context/socketContext';
|
||||
import tinykeys from 'tinykeys';
|
||||
import Empty from '../../../common/state/Empty';
|
||||
import EventListItem from './EventListItem';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
|
||||
export default function EventList(props) {
|
||||
const { events, eventsHandler } = props;
|
||||
@@ -90,6 +90,19 @@ export default function EventList(props) {
|
||||
},
|
||||
};
|
||||
|
||||
// DND
|
||||
const handleOnDragEnd = (result) => {
|
||||
// drop outside of area
|
||||
if (!result.destination) return;
|
||||
|
||||
// Call API
|
||||
eventsHandler('reorder', {
|
||||
index: result.draggableId,
|
||||
from: result.source.index,
|
||||
to: result.destination.index,
|
||||
});
|
||||
};
|
||||
|
||||
console.log('EventList: events in event list', events);
|
||||
let cumulativeDelay = 0;
|
||||
|
||||
@@ -106,35 +119,47 @@ export default function EventList(props) {
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{events.map((e, index) => {
|
||||
if (e.type === 'delay') cumulativeDelay += e.duration;
|
||||
else if (e.type === 'block') cumulativeDelay = 0;
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<EventListItem
|
||||
type={e.type}
|
||||
index={index}
|
||||
data={e}
|
||||
selected={selected === e.id}
|
||||
next={next === e.id}
|
||||
eventsHandler={eventsHandler}
|
||||
delay={cumulativeDelay}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{cursor === index && (
|
||||
<motion.div
|
||||
className={style.cursor}
|
||||
variants={cursorVariants}
|
||||
initial='hidden'
|
||||
animate='visible'
|
||||
exit='exit'
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<Droppable droppableId='eventlist'>
|
||||
{(provided) => (
|
||||
<div
|
||||
className={style.list}
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{events.map((e, index) => {
|
||||
if (e.type === 'delay') cumulativeDelay += e.duration;
|
||||
else if (e.type === 'block') cumulativeDelay = 0;
|
||||
return (
|
||||
<Fragment key={e.id}>
|
||||
<EventListItem
|
||||
type={e.type}
|
||||
index={index}
|
||||
data={e}
|
||||
selected={selected === e.id}
|
||||
next={next === e.id}
|
||||
eventsHandler={eventsHandler}
|
||||
delay={cumulativeDelay}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{cursor === index && (
|
||||
<motion.div
|
||||
className={style.cursor}
|
||||
variants={cursorVariants}
|
||||
initial='hidden'
|
||||
animate='visible'
|
||||
exit='exit'
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ import {
|
||||
requestPost,
|
||||
requestPut,
|
||||
requestDelete,
|
||||
requestReorder,
|
||||
} from '../../../app/api/eventsApi.js';
|
||||
import EventList from './EventList';
|
||||
import EventListMenu from '../../menu/EventListMenu.jsx';
|
||||
import { showErrorToast } from '../../../common/helpers/toastManager';
|
||||
import { Skeleton } from '@chakra-ui/skeleton';
|
||||
import style from './List.module.css';
|
||||
import { useFetch } from '../../../app/hooks/useFetch.js';
|
||||
|
||||
export default function EventListWrapper() {
|
||||
@@ -21,6 +21,7 @@ export default function EventListWrapper() {
|
||||
eventsNamespace,
|
||||
fetchAllEvents
|
||||
);
|
||||
|
||||
const addEvent = useMutation(requestPost, {
|
||||
// we optimistically update here
|
||||
onMutate: async (newEvent) => {
|
||||
@@ -58,6 +59,7 @@ export default function EventListWrapper() {
|
||||
queryClient.invalidateQueries(eventsNamespace);
|
||||
},
|
||||
});
|
||||
|
||||
const updateEvent = useMutation(requestPut, {
|
||||
// we optimistically update here
|
||||
onMutate: async (newEvent) => {
|
||||
@@ -154,6 +156,37 @@ export default function EventListWrapper() {
|
||||
},
|
||||
});
|
||||
|
||||
const reorderEvent = useMutation(requestReorder, {
|
||||
// we optimistically update here
|
||||
onMutate: async (data) => {
|
||||
// cancel ongoing queries
|
||||
queryClient.cancelQueries(eventsNamespace, { exact: true });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousEvents = queryClient.getQueryData(eventsNamespace);
|
||||
|
||||
const e = [...previousEvents];
|
||||
const [reorderedItem] = e.splice(data.from, 1);
|
||||
e.splice(data.to, 0, reorderedItem);
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData(eventsNamespace, e);
|
||||
|
||||
// Return a context with the previous and new todo
|
||||
return { previousEvents };
|
||||
},
|
||||
|
||||
// Mutation fails, rollback undos optimist update
|
||||
onError: (error, eventId, context) => {
|
||||
queryClient.setQueryData(eventsNamespace, context.previousEvents);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(eventsNamespace);
|
||||
},
|
||||
});
|
||||
|
||||
// Show toasts on errors
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
@@ -200,6 +233,15 @@ export default function EventListWrapper() {
|
||||
showErrorToast('Error deleting event', error.message);
|
||||
}
|
||||
break;
|
||||
case 'reorder':
|
||||
try {
|
||||
let t = Date.now();
|
||||
await reorderEvent.mutateAsync(payload);
|
||||
console.log('debug m reorder', Date.now() - t);
|
||||
} catch (error) {
|
||||
showErrorToast('Error reordering event', error.message);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
showErrorToast('Unrecognised request', action);
|
||||
break;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
@@ -11,6 +10,11 @@
|
||||
height: 73vh;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.empty {
|
||||
opacity: 0.3;
|
||||
align-self: center;
|
||||
|
||||
@@ -3,11 +3,4 @@ export const sortByDate = (arr) => {
|
||||
return new Date(a.timeStart).getTime() - new Date(b.timeStart).getTime();
|
||||
};
|
||||
return arr.sort(sorter);
|
||||
};
|
||||
|
||||
export const sortByOrderVal = (arr) => {
|
||||
const sorter = (a, b) => {
|
||||
return a.order - b.order;
|
||||
};
|
||||
return arr.sort(sorter);
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user