diff --git a/client/package.json b/client/package.json index 6e09a260e..2411db248 100644 --- a/client/package.json +++ b/client/package.json @@ -13,6 +13,7 @@ "date-fns": "^2.20.1", "framer-motion": "^4.1.6", "react": "^17.0.1", + "react-beautiful-dnd": "^13.1.0", "react-dom": "^17.0.1", "react-icons": "^4.2.0", "react-qr-code": "^1.1.1", diff --git a/client/src/app/api/eventsApi.js b/client/src/app/api/eventsApi.js index 03fba6354..c258cc2b9 100644 --- a/client/src/app/api/eventsApi.js +++ b/client/src/app/api/eventsApi.js @@ -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; diff --git a/client/src/app/sampleData.js b/client/src/app/sampleData.js index 170cbe0e6..f443fcb74 100644 --- a/client/src/app/sampleData.js +++ b/client/src/app/sampleData.js @@ -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', diff --git a/client/src/common/components/views/Paginator.jsx b/client/src/common/components/views/Paginator.jsx index 2f9dc0a99..a6207a0f6 100644 --- a/client/src/common/components/views/Paginator.jsx +++ b/client/src/common/components/views/Paginator.jsx @@ -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) {
{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 ( -
- - -
-
+ + {(provided) => ( +
+ + + +
+ + +
+
+ )} +
); } diff --git a/client/src/features/editors/list/DelayBlock.jsx b/client/src/features/editors/list/DelayBlock.jsx index 12f3b8855..960703c6f 100644 --- a/client/src/features/editors/list/DelayBlock.jsx +++ b/client/src/features/editors/list/DelayBlock.jsx @@ -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 ( -
- -
- - -
-
+ + {(provided) => ( +
+ + + + +
+ + +
+
+ )} +
); } diff --git a/client/src/features/editors/list/EventBlock.jsx b/client/src/features/editors/list/EventBlock.jsx index 1914af8a8..14ed785ce 100644 --- a/client/src/features/editors/list/EventBlock.jsx +++ b/client/src/features/editors/list/EventBlock.jsx @@ -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 ( -
- - - -
-
Next
- -
- -
- {more ? ( -
- + {(provided) => ( +
+ + + +
+
Next
+ +
+ +
+ {more ? ( +
+ + + +
+ ) : ( +
+ +
+ )} +
setMore(!more)}> + {more ? : } +
+
+
+ - - +
- ) : ( -
- -
- )} -
setMore(!more)}> - {more ? : }
-
-
- - - -
-
+ )} + ); }; export default memo(EventBlock, areEqual); +// export default EventBlock; diff --git a/client/src/features/editors/list/EventList.jsx b/client/src/features/editors/list/EventList.jsx index dfe2c8ccd..8cacba9df 100644 --- a/client/src/features/editors/list/EventList.jsx +++ b/client/src/features/editors/list/EventList.jsx @@ -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) { /> )} - - {events.map((e, index) => { - if (e.type === 'delay') cumulativeDelay += e.duration; - else if (e.type === 'block') cumulativeDelay = 0; - return ( - - - - {cursor === index && ( - - )} - - - ); - })} + + + {(provided) => ( +
+ {events.map((e, index) => { + if (e.type === 'delay') cumulativeDelay += e.duration; + else if (e.type === 'block') cumulativeDelay = 0; + return ( + + + + {cursor === index && ( + + )} + + + ); + })} + {provided.placeholder} +
+ )} +
+
); } diff --git a/client/src/features/editors/list/EventListWrapper.jsx b/client/src/features/editors/list/EventListWrapper.jsx index b04e6e2d5..9e2e8ff1d 100644 --- a/client/src/features/editors/list/EventListWrapper.jsx +++ b/client/src/features/editors/list/EventListWrapper.jsx @@ -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; diff --git a/client/src/features/editors/list/List.module.css b/client/src/features/editors/list/List.module.css index 779cd3d62..e676a162c 100644 --- a/client/src/features/editors/list/List.module.css +++ b/client/src/features/editors/list/List.module.css @@ -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; diff --git a/client/src/features/editors/list/listUtils.js b/client/src/features/editors/list/listUtils.js index 3df879d06..36e32cddd 100644 --- a/client/src/features/editors/list/listUtils.js +++ b/client/src/features/editors/list/listUtils.js @@ -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); -}; +}; \ No newline at end of file diff --git a/client/yarn.lock b/client/yarn.lock index 1077d6680..e0d883923 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2422,6 +2422,14 @@ dependencies: "@types/node" "*" +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/html-minifier-terser@^5.0.0": version "5.1.1" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#3c9ee980f1a10d6021ae6632ca3e79ca2ec4fb50" @@ -2501,11 +2509,35 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.3.tgz#ef65165aea2924c9359205bf748865b8881753c0" integrity sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA== +"@types/prop-types@*": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + "@types/q@^1.5.1": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== +"@types/react-redux@^7.1.16": + version "7.1.16" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21" + integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + +"@types/react@*": + version "17.0.5" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.5.tgz#3d887570c4489011f75a3fc8f965bf87d09a1bea" + integrity sha512-bj4biDB9ZJmGAYTWSKJly6bMr4BLUiBrx9ujiJEoP9XIDY9CTaPGxE5QWN/1WjpPLzYF7/jRNnV2nNxNe970sw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/resolve@0.0.8": version "0.0.8" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194" @@ -2513,6 +2545,11 @@ dependencies: "@types/node" "*" +"@types/scheduler@*": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" + integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== + "@types/source-list-map@*": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" @@ -4421,7 +4458,7 @@ css-blank-pseudo@^0.1.4: dependencies: postcss "^7.0.5" -css-box-model@1.2.1: +css-box-model@1.2.1, css-box-model@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== @@ -6329,7 +6366,7 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.1: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -8012,6 +8049,11 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= +memoize-one@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" @@ -9913,6 +9955,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +raf-schd@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== + raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" @@ -9962,6 +10009,19 @@ react-app-polyfill@^2.0.0: regenerator-runtime "^0.13.7" whatwg-fetch "^3.4.1" +react-beautiful-dnd@^13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d" + integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA== + dependencies: + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-clientside-effect@^1.2.2: version "1.2.5" resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.5.tgz#e2c4dc3c9ee109f642fac4f5b6e9bf5bcd2219a3" @@ -10035,7 +10095,7 @@ react-icons@^4.2.0: resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.2.0.tgz#6dda80c8a8f338ff96a1851424d63083282630d0" integrity sha512-rmzEDFt+AVXRzD7zDE21gcxyBizD/3NqjbX6cmViAgdqfJ2UiLer8927/QhhrXQV7dEj/1EGuOTPp7JnLYVJKQ== -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: +react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -10063,6 +10123,18 @@ react-query@^3.13.5: broadcast-channel "^3.4.1" match-sorter "^6.0.2" +react-redux@^7.2.0: + version "7.2.4" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" + integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA== + dependencies: + "@babel/runtime" "^7.12.1" + "@types/react-redux" "^7.1.16" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.13.1" + react-refresh@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" @@ -10288,6 +10360,13 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +redux@^4.0.0, redux@^4.0.4: + version "4.1.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4" + integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g== + dependencies: + "@babel/runtime" "^7.9.2" + regenerate-unicode-properties@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" @@ -12038,6 +12117,11 @@ use-callback-ref@^1.2.1, use-callback-ref@^1.2.3: resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5" integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg== +use-memo-one@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20" + integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ== + use-sidecar@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.5.tgz#ffff2a17c1df42e348624b699ba6e5c220527f2b" diff --git a/server/app.js b/server/app.js index 41ab02d90..95d60e390 100644 --- a/server/app.js +++ b/server/app.js @@ -6,6 +6,7 @@ const low = require('lowdb'); const FileSync = require('lowdb/adapters/FileSync'); const adapter = new FileSync(config.database.filename); const db = low(adapter); + // TODO: move defaults to config file db.defaults({ events: [], @@ -65,7 +66,7 @@ app.use((err, req, res, next) => { const server = http.createServer(app); // get data (if any) -const eventlist = db.get('events').sortBy('order').value(); +const eventlist = db.get('events').value(); // init timer global.timer = new EventTimer(server, config); diff --git a/server/controllers/eventsController.js b/server/controllers/eventsController.js index ac7b4196f..ef933eac4 100644 --- a/server/controllers/eventsController.js +++ b/server/controllers/eventsController.js @@ -5,20 +5,6 @@ const db = require('../app.js').db; const { nanoid } = require('nanoid'); const eventDefs = require('../data/eventsDefinition.js'); -// incrementFrom -function incrementFrom(start, incr = 1) { - let entries = db.get('events').sortBy('order').value(); - - entries.map((e) => { - if (e.order < start) return; - db.get('events') - .find({ id: e.id }) - .assign({ order: e.order + incr }) - .update('revision', (n) => n + 1) - .write(); - }); -} - function _getEventsCount() { return db.get('events').size().value(); } @@ -27,17 +13,40 @@ function _pushNew(entry) { return db.get('events').push(entry).write(); } +function _insertAt(entry, index) { + // get events + let events = db.get('events').value(); + let count = events.length; + let order = entry.order; + + // Remove order field from object + delete entry.order; + + // Insert at beggining + if (order === 0) { + events.unshift(entry); + } + + // insert at end + else if (order >= count) { + events.push(entry); + } + + // insert in the middle + else { + events.splice(index, 0, entry); + } + + // save events + db.set('events', events).write(); +} + function _removeById(eventId) { return db.get('events').remove({ id: eventId }).write(); } function getEventEvents() { - return db - .get('events') - .chain() - .filter({ type: 'event' }) - .sortBy('order') - .value(); + return db.get('events').chain().filter({ type: 'event' }).value(); } // Updates timer object @@ -54,7 +63,7 @@ function _updateTimersSingle(id, entry) { // Create controller for GET request to '/events' // Returns - exports.eventsGetAll = async (req, res) => { - const results = db.get('events').sortBy('order').value(); + const results = db.get('events').value(); res.json(results); }; @@ -97,13 +106,11 @@ exports.eventsPost = async (req, res) => { } try { - // increment count if necessary - const c = _getEventsCount(); + // get place where event should be + const index = newEvent.order || 0; - if (newEvent.order < c) incrementFrom(newEvent.order); - - // add new event - _pushNew(newEvent); + // add new event in place + _insertAt(newEvent, index); // update timers _updateTimers(); @@ -111,6 +118,7 @@ exports.eventsPost = async (req, res) => { // reply OK res.sendStatus(201); } catch (error) { + console.log(error); res.status(400).send(error); } }; @@ -134,6 +142,7 @@ exports.eventsPut = async (req, res) => { db.get('events') .find({ id: req.body.id }) .assign({ ...req.body }) + .update('revision', (n) => n + 1) .write(); // update timer @@ -148,30 +157,51 @@ exports.eventsPut = async (req, res) => { // Create controller for PATCH request to '/events/' // Returns - exports.eventsPatch = async (req, res) => { - // no valid params + // Code is the same as put, call that + this.eventsPut(req, res); +}; + +exports.eventsReorder = async (req, res) => { + // TODO: Validate event if (!req.body) { - res.status(400).send(`No object found`); + res.status(400).send(`No object found in request`); + console.log(`No object found in request`); + return; } - let eventId = req.body.id; - if (!eventId) { - res.status(400).send(`No id found`); + const { index, from, to } = req.body; + + console.log(req.body); + + // get events + let events = db.get('events').value(); + let idx = events.findIndex((e) => e.id === index, from); + + // Check if item is at given index + if (idx !== from) { + res.status(400).send(`Id not found at index`); + console.log(`Id not found at index`, idx, from); return; } try { - db.get('events') - .find({ id: req.body.id }) - .assign({ ...req.body }) - .update('revision', (n) => n + 1) - .write(); + // remove item at from + const [reorderedItem] = events.splice(from, 1); + // reinsert item at to + events.splice(to, 0, reorderedItem); + + // save events + db.set('events', events).write(); + + // TODO: would it be more efficient to reorder at timer? // update timer - _updateTimersSingle(req.body.id, req.body); + _updateTimers(); res.sendStatus(200); } catch (error) { + console.log(error); res.status(400).send(error); } }; @@ -187,18 +217,7 @@ exports.eventsDelete = async (req, res) => { } try { - // increment count if necessary - const c = _getEventsCount(); - const e = db.get('events').find({ id: req.params.eventId }).value(); - - if (e == null) { - res.status(400).send(`No match`); - return; - } - - if (c > 0 && e.order < c) incrementFrom(e.order, -1); - - // add new event + // remove new event _removeById(req.params.eventId); // update timer diff --git a/server/data/eventsData.json b/server/data/eventsData.json index 8bd6b1341..f5aadad75 100644 --- a/server/data/eventsData.json +++ b/server/data/eventsData.json @@ -1,13 +1,11 @@ [ { "id":"xxxxx0", - "order": 0, "duration": 300000, "type": "delay" }, { "id":"xxxxx1", - "order": 1, "title": "Is the internet a fad?", "subtitle": "It is", "presenter": "Carlos Valente", @@ -17,13 +15,11 @@ }, { "id":"xxxxx2", - "order": 2, "duration": 1500000, "type": "delay" }, { "id":"xxxxx3", - "order": 3, "title": "Is reddit a dictatorship?", "subtitle": "It is", "presenter": "Carlos Valente", @@ -33,7 +29,6 @@ }, { "id":"xxxxx4", - "order": 4, "title": "Out of words", "subtitle": "", "presenter": "Carlos Valente", @@ -43,7 +38,6 @@ }, { "id":"xxxxx5", - "order": 5, "title": "Really", "subtitle": "", "presenter": "Carlos Valente", @@ -53,7 +47,6 @@ }, { "id":"xxxxx6", - "order": 6, "title": "...", "subtitle": "", "presenter": "Carlos Valente", diff --git a/server/data/eventsDefinition.js b/server/data/eventsDefinition.js index 2599f7330..0765f5b60 100644 --- a/server/data/eventsDefinition.js +++ b/server/data/eventsDefinition.js @@ -1,8 +1,8 @@ const event = { - order: 0, title: '', subtitle: '', presenter: '', + note: '', timeStart: 0, timeEnd: 0, isPublic: false, @@ -11,13 +11,11 @@ const event = { }; const delay = { - order: 0, duration: 0, type: 'delay', }; const block = { - order: 0, type: 'block', }; diff --git a/server/routes/eventsRouter.js b/server/routes/eventsRouter.js index 52099a1bb..2f4515afa 100644 --- a/server/routes/eventsRouter.js +++ b/server/routes/eventsRouter.js @@ -4,22 +4,25 @@ const router = express.Router(); // import events controller const eventsController = require('../controllers/eventsController'); -// create route between controller and '/events' endpoint +// create route between controller and '/events/' endpoint router.get('/', eventsController.eventsGetAll); -// create route between controller and '/events/:id' endpoint +// create route between controller and '/events/:eventId' endpoint router.get('/:eventId', eventsController.eventsGetById); // create route between controller and '/events/' endpoint router.post('/', eventsController.eventsPost); -// create route between controller and '/events/:id' endpoint +// create route between controller and '/events/' endpoint router.put('/', eventsController.eventsPut); -// create route between controller and '/events/:id' endpoint +// create route between controller and '/events/' endpoint router.patch('/', eventsController.eventsPatch); -// create route between controller and '/events/:id' endpoint +// create route between controller and '/events/reorder' endpoint +router.patch('/reorder/', eventsController.eventsReorder); + +// create route between controller and '/events/:eventId' endpoint router.delete('/:eventId', eventsController.eventsDelete); module.exports = router;