Feat/block improvements (#95)

* feat/89-block-improvements style: replace reload icon
* feat/block-feat/block-improvements add cursor buttons
* feat/block-improvements hover to create
* feat/block-improvements change settings in modal
* feat/block-improvements version bump
This commit is contained in:
Carlos Valente
2022-01-22 23:23:31 +01:00
committed by GitHub
parent 906a6631a7
commit 6a6f68b2f9
34 changed files with 652 additions and 255 deletions
@@ -2,7 +2,7 @@ import { Draggable } from 'react-beautiful-dnd';
import { FiMoreVertical } from 'react-icons/fi';
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
import ActionButtons from '../list/ActionButtons';
import style from './BlockBlock.module.css';
import style from './BlockBlock.module.scss';
import PropTypes from 'prop-types';
export default function BlockBlock(props) {
@@ -1,3 +1,4 @@
@use '../../../styles/main' as *;
/* ============= COMMON ============= */
.block {
@@ -8,7 +9,7 @@
width: 100%;
border-radius: 8px;
font-size: 15px;
border: 1px solid rgba(255, 255, 255, 0.05);
border: $block-border;
display: grid;
grid-template-columns: 2em auto;
@@ -16,7 +17,7 @@
align-items: baseline;
gap: 0.5em;
background-color: #805ad5;
background-color: $block-block-color;
background: linear-gradient(
0deg,
rgba(107, 70, 193, 0.4) 0%,
@@ -34,7 +35,6 @@
/* ============== ACTION ================ */
.actionOverlay {
display: flex;
display: flex;
flex-direction: row;
flex-wrap: wrap;
@@ -5,7 +5,7 @@ import ActionButtons from '../list/ActionButtons';
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
import ApplyIconBtn from 'common/components/buttons/ApplyIconBtn';
import DelayInput from 'common/input/DelayInput';
import style from './DelayBlock.module.css';
import style from './DelayBlock.module.scss';
import PropTypes from 'prop-types';
export default function DelayBlock(props) {
@@ -1,3 +1,5 @@
@use '../../../styles/main' as *;
/* ============= COMMON ============= */
.delay {
@@ -7,7 +9,7 @@
width: 100%;
border-radius: 8px;
font-size: 15px;
border: 1px solid rgba(255, 255, 255, 0.05);
border: $block-border;
display: grid;
grid-template-columns: 2em 1fr auto;
@@ -17,7 +19,7 @@
align-items: baseline;
gap: 0.5em;
background-color: #ecc94b;
background-color: $block-delay-color;
background: linear-gradient(
180deg,
rgba(214, 158, 46, 0.4) 0%,
@@ -30,7 +32,7 @@
.drag {
grid-area: drag;
color: rgba(255, 255, 255, 0.67);
color: $block-icon-drag;
align-self: center;
}
+49 -45
View File
@@ -6,10 +6,10 @@ import MenuBar from 'features/menu/MenuBar';
import ModalManager from 'features/modals/ModalManager';
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
import { LoggingProvider } from '../../app/context/LoggingContext';
import { LocalEventSettingsProvider } from '../../app/context/LocalEventSettingsContext';
import { CursorProvider } from '../../app/context/CursorContext';
const EventListWrapper = lazy(() =>
import('features/editors/list/EventListWrapper')
);
const EventListWrapper = lazy(() => import('features/editors/list/EventListWrapper'));
const PlaybackControl = lazy(() => import('features/control/PlaybackControl'));
const MessageControl = lazy(() => import('features/control/MessageControl'));
const Info = lazy(() => import('features/info/Info'));
@@ -24,53 +24,57 @@ export default function Editor() {
return (
<LoggingProvider>
<ErrorBoundary>
<ModalManager isOpen={isOpen} onClose={onClose} />
</ErrorBoundary>
<LocalEventSettingsProvider>
<ErrorBoundary>
<ModalManager isOpen={isOpen} onClose={onClose} />
</ErrorBoundary>
<div className={styles.mainContainer}>
<Box id='settings' className={styles.settings}>
<ErrorBoundary>
<MenuBar onOpen={onOpen} isOpen={isOpen} />
</ErrorBoundary>
</Box>
<div className={styles.mainContainer}>
<CursorProvider>
<Box id='settings' className={styles.settings}>
<ErrorBoundary>
<MenuBar onOpen={onOpen} isOpen={isOpen} />
</ErrorBoundary>
</Box>
<Box className={styles.editor}>
<h1>Event List</h1>
<div className={styles.content}>
<ErrorBoundary>
<EventListWrapper />
</ErrorBoundary>
</div>
</Box>
<Box className={styles.editor}>
<h1>Event List</h1>
<div className={styles.content}>
<ErrorBoundary>
<EventListWrapper />
</ErrorBoundary>
</div>
</Box>
</CursorProvider>
<Box className={styles.messages}>
<h1>Display Messages</h1>
<div className={styles.content}>
<ErrorBoundary>
<MessageControl />
</ErrorBoundary>
</div>
</Box>
<Box className={styles.messages}>
<h1>Display Messages</h1>
<div className={styles.content}>
<ErrorBoundary>
<MessageControl />
</ErrorBoundary>
</div>
</Box>
<Box className={styles.playback}>
<h1>Timer Control</h1>
<div className={styles.content}>
<ErrorBoundary>
<PlaybackControl />
</ErrorBoundary>
</div>
</Box>
<Box className={styles.playback}>
<h1>Timer Control</h1>
<div className={styles.content}>
<ErrorBoundary>
<PlaybackControl />
</ErrorBoundary>
</div>
</Box>
<Box className={styles.info}>
<h1>Info</h1>
<div className={styles.content}>
<ErrorBoundary>
<Info />
</ErrorBoundary>
</div>
</Box>
</div>
<Box className={styles.info}>
<h1>Info</h1>
<div className={styles.content}>
<ErrorBoundary>
<Info />
</ErrorBoundary>
</div>
</Box>
</div>
</LocalEventSettingsProvider>
</LoggingProvider>
);
}
@@ -0,0 +1,73 @@
import { Tooltip } from '@chakra-ui/tooltip';
import { Checkbox } from '@chakra-ui/react';
import style from './EntryBlock.module.scss';
import { useContext, useEffect, useState } from 'react';
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
export default function EntryBlock(props) {
const { showKbd, index, eventsHandler } = props;
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
const [doStartTime, setStartTime] = useState(starTimeIsLastEnd);
const [doPublic, setPublic] = useState(defaultPublic);
useEffect(() => {
setStartTime(starTimeIsLastEnd);
}, [starTimeIsLastEnd]);
useEffect(() => {
setPublic(defaultPublic);
}, [defaultPublic]);
return (
<div className={style.create}>
<Tooltip label='Add Event' openDelay={300}>
<span
className={style.createEvent}
onClick={() =>
eventsHandler(
'add',
{ type: 'event', order: index + 1, isPublic: doPublic },
{ startIsLastEnd: doStartTime ? index : undefined }
)
}
>
E{showKbd && <span className={style.keyboard}>Alt + E</span>}
</span>
</Tooltip>
<Tooltip label='Add Delay' openDelay={300}>
<span
className={style.createDelay}
onClick={() => eventsHandler('add', { type: 'delay', order: index + 1 })}
>
D{showKbd && <span className={style.keyboard}>Alt + D</span>}
</span>
</Tooltip>
<Tooltip label='Add Block' openDelay={300}>
<span
className={style.createBlock}
onClick={() => eventsHandler('add', { type: 'block', order: index + 1 })}
>
B{showKbd && <span className={style.keyboard}>Alt + B</span>}
</span>
</Tooltip>
<div className={style.options}>
<Checkbox
size='sm'
colorScheme='blue'
isChecked={doStartTime}
onChange={(e) => setStartTime(e.target.checked)}
>
Start time is last end
</Checkbox>
<Checkbox
size='sm'
colorScheme='blue'
isChecked={doPublic}
onChange={(e) => setPublic(e.target.checked)}
>
Default public
</Checkbox>
</div>
</div>
);
}
@@ -0,0 +1,92 @@
@use '../../../styles/main' as *;
.create {
padding: 0 0.5em;
box-sizing: border-box;
width: 100%;
border-radius: 8px;
font-size: 12px;
position: relative;
height: 20px;
margin: -8px 0;
transition: height 0.1s ease;
* {
display: none;
}
&:hover {
opacity: 1;
height: calc(2.5em + 20px);
display: flex;
align-items: center;
justify-content: center;
gap: 5%;
transition: height 0.15s ease;
* {
display: flex;
}
}
.createEvent,
.createDelay,
.createBlock {
width: auto;
padding: 0 16px;
height: 24px;
text-align: center;
vertical-align: center;
font-weight: 600;
line-height: 21px;
border-radius: 4px;
opacity: 0.6;
.keyboard {
margin-left: 4px;
padding: 0 4px;
color: #ccc;
border-radius: 2px;
font-family: Monospaced, sans-serif;
background-color: rgba(0, 0, 0, 0.24);
}
}
.createEvent {
border: 1px solid #2b6cb0;
color: lighten(#2b6cb0, 30%);
&:hover {
background-color: #2b6cb0;
opacity: 1;
}
}
.createDelay {
border: 1px solid #ecc94b;;
color: lighten(#ecc94b, 30%);
&:hover {
background-color: #ecc94b;
opacity: 1;
}
}
.createBlock {
border: 1px solid #805ad5;
color: lighten(#805ad5, 30%);
&:hover {
background-color: #805ad5;
opacity: 1;
}
}
.options {
display: flex;
flex-direction: column;
opacity: 0.65;
}
}
@@ -142,7 +142,7 @@
background-color: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 4px;
border-radius: 8px;
width: 100%;
display: block;
@@ -150,11 +150,13 @@
overflow: hidden;
}
.collapsed .titleContasiner {
.collapsed .titleContainer {
height: fit-content;
}
.expanded .titleContainer {
height: 100%;
border-radius: 4px;
}
.oscLabel {
+43 -36
View File
@@ -1,21 +1,22 @@
import style from './List.module.scss';
import { createRef, useCallback, useEffect, useMemo, useState } from 'react';
import { createRef, useCallback, useContext, useEffect, useState } from 'react';
import { useSocket } from 'app/context/socketContext';
import Empty from 'common/state/Empty';
import EventListItem from './EventListItem';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { useAtom } from 'jotai';
import { SelectSetting } from 'app/context/settingsAtom';
import EntryBlock from '../EntryBlock/EntryBlock';
import { CursorContext } from '../../../app/context/CursorContext';
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
export default function EventList(props) {
const { events, eventsHandler } = props;
const { cursor, moveCursorUp, moveCursorDown, setCursor, isCursorLocked } =
useContext(CursorContext);
const socket = useSocket();
const [selectedId, setSelectedId] = useState(null);
const [nextId, setNextId] = useState(null);
const [cursor, setCursor] = useState(0);
const [cursorSettings] = useAtom(useMemo(() => SelectSetting('cursor'), []));
const cursorRef = createRef();
const { showQuickEntry } = useContext(LocalEventSettingsContext);
// Handle keyboard shortcuts
const handleKeyPress = useCallback(
@@ -26,13 +27,11 @@ export default function EventList(props) {
if (e.altKey && (!e.ctrlKey || !e.shiftKey)) {
// Arrow down
if (e.keyCode === 40) {
if (cursor == null) setCursor(0);
else if (cursor < events.length - 1) setCursor(cursor + 1);
if (cursor < events.length - 1) moveCursorDown();
}
// Arrow up
if (e.keyCode === 38) {
if (cursor == null) setCursor(0);
else if (cursor > 0) setCursor(cursor - 1);
if (cursor > 0) moveCursorUp();
}
// E
if (e.key === 'e' || e.key === 'E') {
@@ -54,7 +53,7 @@ export default function EventList(props) {
}
}
},
[cursor, events, eventsHandler]
[cursor, events.length, eventsHandler, moveCursorDown, moveCursorUp]
);
useEffect(() => {
@@ -67,7 +66,7 @@ export default function EventList(props) {
return () => {
document.removeEventListener('keydown', handleKeyPress);
};
}, [handleKeyPress, cursor, events]);
}, [handleKeyPress, cursor, events, setCursor]);
// handle incoming messages
useEffect(() => {
@@ -101,13 +100,14 @@ export default function EventList(props) {
block: 'nearest',
inline: 'start',
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cursor]);
// if selected event
// or cursor settings changed
useEffect(() => {
// and if we are locked
if (cursorSettings !== 'locked' || selectedId == null) return;
if (!isCursorLocked || selectedId == null) return;
// move cursor
let gotoIndex = -1;
@@ -123,7 +123,8 @@ export default function EventList(props) {
// move cursor
setCursor(gotoIndex);
}
}, [selectedId, cursorSettings]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedId, isCursorLocked]);
if (events.length < 1) {
return <Empty text='No Events' />;
@@ -155,11 +156,7 @@ export default function EventList(props) {
<DragDropContext onDragEnd={handleOnDragEnd}>
<Droppable droppableId='eventlist'>
{(provided) => (
<div
className={style.list}
{...provided.droppableProps}
ref={provided.innerRef}
>
<div className={style.list} {...provided.droppableProps} ref={provided.innerRef}>
{events.map((e, index) => {
if (index === 0) {
cumulativeDelay = 0;
@@ -174,24 +171,34 @@ export default function EventList(props) {
previousEnd = thisEnd;
thisEnd = e.timeEnd;
}
return (
<div
ref={cursor === index ? cursorRef : undefined}
key={e.id}
className={cursor === index ? style.cursor : undefined}
>
<EventListItem
type={e.type}
index={index}
eventIndex={eventIndex}
data={e}
selected={selectedId === e.id}
next={nextId === e.id}
eventsHandler={eventsHandler}
delay={cumulativeDelay}
previousEnd={previousEnd}
/>
<div key={e.id}>
{index === 0 && showQuickEntry && (
<EntryBlock index={-1} eventsHandler={eventsHandler} />
)}
<div
ref={cursor === index ? cursorRef : undefined}
className={cursor === index ? style.cursor : undefined}
>
<EventListItem
type={e.type}
index={index}
eventIndex={eventIndex}
data={e}
selected={selectedId === e.id}
next={nextId === e.id}
eventsHandler={eventsHandler}
delay={cumulativeDelay}
previousEnd={previousEnd}
/>
</div>
{showQuickEntry && (
<EntryBlock
showKbd={index === cursor}
index={index}
eventsHandler={eventsHandler}
/>
)}
</div>
);
})}
@@ -3,6 +3,7 @@ import BlockBlock from '../BlockBlock/BlockBlock';
import EventBlock from '../EventBlock/EventBlock';
import { memo, useContext } from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
const areEqual = (prevProps, nextProps) => {
return (
@@ -29,12 +30,21 @@ const EventListItem = (props) => {
...rest
} = props;
const { emitError } = useContext(LoggingContext);
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
// Create / delete new events
const actionHandler = (action, payload) => {
switch (action) {
case 'event':
eventsHandler('add', { type: 'event', order: index + 1 });
eventsHandler(
'add',
{
type: 'event',
order: index + 1,
isPublic: defaultPublic,
},
{ startIsLastEnd: starTimeIsLastEnd ? index : undefined }
);
break;
case 'delay':
eventsHandler('add', { type: 'delay', order: index + 1 });
@@ -85,9 +95,7 @@ const EventListItem = (props) => {
/>
);
case 'block':
return (
<BlockBlock index={index} data={data} actionHandler={actionHandler} />
);
return <BlockBlock index={index} data={data} actionHandler={actionHandler} />;
case 'delay':
return (
<DelayBlock
@@ -1,32 +1,29 @@
import { useMutation, useQueryClient } from 'react-query';
import { useCallback, useContext, useEffect, useState } from 'react';
import {
fetchAllEvents,
requestPatch,
requestPost,
requestPut,
requestDelete,
requestDeleteAll,
requestReorder,
requestApplyDelay,
} from 'app/api/eventsApi.js';
import EventList from './EventList';
import EventListMenu from 'features/menu/EventListMenu.jsx';
import { useFetch } from 'app/hooks/useFetch.js';
import Empty from 'common/state/Empty';
import { useMutation, useQueryClient } from 'react-query';
import { EVENTS_TABLE } from 'app/api/apiConstants';
import { BatchOperation } from 'app/context/collapseAtom';
import { useAtom } from 'jotai';
import { LoggingContext } from '../../../app/context/LoggingContext';
import {
fetchAllEvents,
requestApplyDelay,
requestDelete,
requestDeleteAll,
requestPatch,
requestPost,
requestPut,
requestReorder,
} from 'app/api/eventsApi.js';
import { useFetch } from 'app/hooks/useFetch.js';
import EventList from './EventList';
import EventListMenu from 'features/menu/EventListMenu.jsx';
import Empty from 'common/state/Empty';
export default function EventListWrapper() {
const [, setCollapsed] = useAtom(BatchOperation);
const queryClient = useQueryClient();
const { emitError } = useContext(LoggingContext);
const { data, status, isError, refetch } = useFetch(
EVENTS_TABLE,
fetchAllEvents
);
const { data, status, isError, refetch } = useFetch(EVENTS_TABLE, fetchAllEvents);
const [events, setEvents] = useState(null);
const addEvent = useMutation(requestPost, {
@@ -72,10 +69,7 @@ export default function EventListWrapper() {
queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]);
// Snapshot the previous value
const previousEvent = queryClient.getQueryData([
EVENTS_TABLE,
newEvent.id,
]);
const previousEvent = queryClient.getQueryData([EVENTS_TABLE, newEvent.id]);
// optimistically update object
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
@@ -86,10 +80,7 @@ export default function EventListWrapper() {
// Mutation fails, rollback undos optimist update
onError: (error, newEvent, context) => {
queryClient.setQueryData(
[EVENTS_TABLE, context.newEvent.id],
context.previousEvent
);
queryClient.setQueryData([EVENTS_TABLE, context.newEvent.id], context.previousEvent);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
@@ -105,10 +96,7 @@ export default function EventListWrapper() {
queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]);
// Snapshot the previous value
const previousEvent = queryClient.getQueryData([
EVENTS_TABLE,
newEvent.id,
]);
const previousEvent = queryClient.getQueryData([EVENTS_TABLE, newEvent.id]);
// optimistically update object
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
@@ -119,10 +107,7 @@ export default function EventListWrapper() {
// Mutation fails, rollback undos optimist update
onError: (error, newEvent, context) => {
queryClient.setQueryData(
[EVENTS_TABLE, context.newEvent.id],
context.previousEvent
);
queryClient.setQueryData([EVENTS_TABLE, context.newEvent.id], context.previousEvent);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
@@ -237,11 +222,16 @@ export default function EventListWrapper() {
// Events API
const eventsHandler = useCallback(
async (action, payload) => {
async (action, payload, options = undefined) => {
switch (action) {
case 'add':
try {
await addEvent.mutateAsync(payload);
let newEvent = { ...payload };
// there is an option to pass an index of an array to use as start time
if (options?.startIsLastEnd !== undefined) {
newEvent.timeStart = data[options.startIsLastEnd].timeEnd || 0;
}
await addEvent.mutateAsync(newEvent);
} catch (error) {
emitError(`Error fetching data: ${error.message}`);
}
@@ -326,6 +316,7 @@ export default function EventListWrapper() {
break;
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[data]
);
@@ -23,11 +23,11 @@
.cursor {
width: 100%;
background: linear-gradient(
180deg,
#ff7597 2%,
#0001 3%,
#0001 97%,
#ff7597 98%
180deg,
#ff7597 2%,
#0001 3%,
#0001 97%,
#ff7597 98%
);
border-radius: 14px;