Feat/table (#105)

* feat/table add react-table
* feat/table add protected table route
* feat/table upgrade dependencies
* feat/table support parsing of new properties

Co-authored-by: DeepSource Bot <bot@deepsource.io>
This commit is contained in:
Carlos Valente
2022-04-09 22:05:56 +02:00
committed by GitHub
parent 8841a02754
commit 67ddcd8c68
146 changed files with 7423 additions and 5780 deletions
+13 -3
View File
@@ -1,8 +1,18 @@
version = 1
test_patterns = [
"__mocks__/**",
"__tests__/**",
"cypress/**",
"*.test.*",
"*.mock.*",
"*.spec.*"
]
[[analyzers]]
name = "javascript"
enabled = true
name = "javascript"
enabled = true
[analyzers.meta]
plugins = ["react"]
plugins = ["react"]
environment = ["nodejs","browser","jest"]
+13
View File
@@ -1,6 +1,19 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="CssUnknownProperty" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myCustomPropertiesEnabled" value="true" />
<option name="myIgnoreVendorSpecificProperties" value="false" />
<option name="myCustomPropertiesList">
<value>
<list size="3">
<item index="0" class="java.lang.String" itemvalue="scrollbar-color" />
<item index="1" class="java.lang.String" itemvalue="scrollbar-width" />
<item index="2" class="java.lang.String" itemvalue="app-region" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredUrls">
+3 -1
View File
@@ -33,6 +33,7 @@ IP.ADDRESS:4001/public > Public / Foyer view
IP.ADDRESS:4001/pip > Picture in Picture view
IP.ADDRESS:4001/lower > Lower Thirds
IP.ADDRESS:4001/studio > Studio Clock
IP.ADDRESS:4001/cuesheet > Cue Sheet
...and for the editor (the control interface, same as the app)
-------------------------------------------------------------
@@ -48,7 +49,9 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
- Backstage Info
- Public Info
- Picture in Picture
- Studio Clock
- [x] Configurable realtime Lower Thirds
- [x] Cuesheets with additional custom fields
- [x] Send live messages to different screen types
- [x] Ability to differentiate between backstage and public data
- [x] Manage delays workflow
@@ -62,7 +65,6 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
## Unopinionated
We are not interested in forcing workflows and have made ontime, so it is flexible to whichever way you would like to work.
- [x] You do not need an order list to use the timer. Create an empty event and the OSC API works just the same
- [x] If you want just the info screens, no need to use the timer!
- [x] Don't have or care for a schedule?
+73 -4
View File
@@ -1,13 +1,82 @@
{
"extends": [
"react-app",
"react-app/jest"
"react-app/jest",
"plugin:react/recommended"
],
"plugins": [
"react",
"testing-library",
"jest"
],
"plugins": ["react", "testing-library", "jest"],
"rules": {
"jest/no-mocks-import": "warn",
"no-useless-concat": "warn",
"prefer-template": "warn"
"prefer-template": "warn",
"react/jsx-no-bind": [
"error",
{
"ignoreRefs": true,
"allowArrowFunctions": true,
"allowFunctions": false,
"allowBind": false,
"ignoreDOMComponents": true
}
],
"react/jsx-boolean-value": [
"error",
"never",
{
"always": []
}
],
"react/jsx-handler-names": [
"off",
{
"eventHandlerPrefix": "handle",
"eventHandlerPropPrefix": "on"
}
],
"react/no-danger": "warn",
"react/no-deprecated": [
"error"
],
"react/jsx-no-undef": "error",
"react/jsx-closing-tag-location": "warn",
"react/jsx-no-useless-fragment": "warn",
"react/no-unescaped-entities": "error",
"react/jsx-tag-spacing": [
"error",
{
"closingSlash": "never",
"beforeSelfClosing": "always",
"afterOpening": "never",
"beforeClosing": "never"
}
],
"react/jsx-space-before-closing": [
"off",
"always"
],
"react/jsx-curly-brace-presence": [
"error",
{
"props": "never",
"children": "never"
}
],
"react/destructuring-assignment": [
"error",
"always"
],
"react/no-unsafe": "off",
"react/prop-types": [
"error",
{
"ignore": [],
"customValidators": [],
"skipUndeclared": true
}
]
}
}
+17 -12
View File
@@ -3,29 +3,30 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@chakra-ui/react": "^1.7.4",
"@chakra-ui/react": "^1.8.6",
"@dnd-kit/core": "^5.0.3",
"@dnd-kit/sortable": "^6.0.1",
"@dnd-kit/utilities": "^3.1.0",
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@react-icons/all-files": "^4.1.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"axios": "^0.24.0",
"framer-motion": "^4.1.6",
"jotai": "^0.16.5",
"autosize": "^5.0.1",
"axios": "^0.25.0",
"color": "^4.2.1",
"framer-motion": "4.1.17",
"ontime-utils": "link: ../server/utils/",
"react": "17.0.2",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.1",
"react-fast-compare": "^3.2.0",
"react-qr-code": "2.0.3",
"react-query": "^3.34.5",
"react-query": "^3.34.12",
"react-router-dom": "^6.2.1",
"react-scripts": "5.0.0",
"socket.io-client": "4.4.0",
"react-table": "^7.7.0",
"socket.io-client": "^4.4.1",
"typeface-open-sans": "^1.1.13",
"use-fit-text": "^2.4.0",
"web-vitals": "^1.0.1"
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "set BROWSER=none&&react-scripts start",
@@ -47,9 +48,13 @@
]
},
"devDependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^12.1.2",
"@testing-library/react-hooks": "^7.0.2",
"@testing-library/user-event": "^13.5.0",
"prop-types": "^15.8.1",
"react-scripts": "^5.0.0",
"react-test-renderer": "^17.0.2",
"sass": "^1.44.0"
"sass": "^1.49.0"
}
}
+28 -23
View File
@@ -1,4 +1,4 @@
import { lazy, Suspense, useCallback, useEffect } from 'react';
import React, { lazy, Suspense, useCallback, useEffect } from 'react';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import './App.scss';
import withSocket from 'features/viewers/ViewWrapper';
@@ -7,23 +7,17 @@ import ProtectRoute from './common/components/protectRoute/ProtectRoute';
import { useFetch } from './app/hooks/useFetch';
import { ALIASES } from './app/api/apiConstants';
import { getAliases } from './app/api/ontimeApi';
import { TableSettingsProvider } from './app/context/TableSettingsContext';
const Editor = lazy(() => import('features/editors/Editor'));
const Table = lazy(() => import('features/table/TableWrapper'));
const TimerView = lazy(() =>
import('features/viewers/timer/Timer')
);
const MinimalTimerView = lazy(() =>
import('features/viewers/timer/MinimalTimer')
);
const TimerView = lazy(() => import('features/viewers/timer/Timer'));
const MinimalTimerView = lazy(() => import('features/viewers/timer/MinimalTimer'));
const StageManager = lazy(() =>
import('features/viewers/backstage/StageManager')
);
const StageManager = lazy(() => import('features/viewers/backstage/StageManager'));
const Public = lazy(() => import('features/viewers/foh/Public'));
const Lower = lazy(() =>
import('features/viewers/production/lower/LowerWrapper')
);
const Lower = lazy(() => import('features/viewers/production/lower/LowerWrapper'));
const Pip = lazy(() => import('features/viewers/production/Pip'));
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
@@ -35,6 +29,20 @@ const SLowerThird = withSocket(Lower);
const SPip = withSocket(Pip);
const SStudio = withSocket(StudioClock);
const ProtectedEditor = () => (
<ProtectRoute>
<Editor />
</ProtectRoute>
);
const ProtectedTable = () => (
<ProtectRoute>
<TableSettingsProvider>
<Table />
</TableSettingsProvider>
</ProtectRoute>
);
function App() {
const { data } = useFetch(ALIASES, getAliases);
const location = useLocation();
@@ -48,8 +56,7 @@ function App() {
if (e.altKey) {
if (e.key === 't' || e.key === 'T') {
// if we are in electron
if (window.process?.type === undefined) return;
if (window.process.type === 'renderer') {
if (window.process?.type === 'renderer') {
// ask to see debug
window.ipcRenderer.send('set-window', 'show-dev');
}
@@ -101,15 +108,13 @@ function App() {
<Route path='/studio' element={<SStudio />} />
{/*/!* Lower cannot have fallback *!/*/}
<Route path='/lower' element={<SLowerThird />} />
{/*/!* Protected Routes *!/*/}
<Route
path='/editor'
element={
<ProtectRoute>
<Editor />
</ProtectRoute>
}
/>
<Route path='/editor' element={<ProtectedEditor />} />
<Route path='/cuesheet' element={<ProtectedTable />} />
<Route path='/cuelist' element={<ProtectedTable />} />
<Route path='/table' element={<ProtectedTable />} />
{/* Send to default if nothing found */}
<Route path='*' element={<STimer />} />
</Routes>
+3 -2
View File
@@ -8,7 +8,7 @@
body,
html,
.App {
margin: 0px auto;
margin: 0 auto;
overflow: hidden;
overflow: clip;
height: 100vh;
@@ -23,7 +23,8 @@ option {
/* width */
::-webkit-scrollbar {
width: 0.5em;
width: 0.5rem;
height: 0.5rem;
}
/* Track */
+3 -2
View File
@@ -1,6 +1,7 @@
export const NODE_PORT = 4001;
export const EVENT_TABLE = 'event';
export const ALIASES = 'aliases';
export const USERFIELDS = 'userFields';
export const EVENTS_TABLE = 'events';
export const APP_TABLE = 'appinfo';
export const OSC_SETTINGS = 'oscSettings';
@@ -13,5 +14,5 @@ const calculateServer = () => {
export const serverURL = calculateServer();
export const eventURL = serverURL + EVENT_TABLE;
export const eventsURL = serverURL + EVENTS_TABLE;
export const playbackURL = serverURL + 'playback';
export const ontimeURL = serverURL + 'ontime';
export const playbackURL = `${serverURL}playback`;
export const ontimeURL = `${serverURL}ontime`;
+6 -7
View File
@@ -7,20 +7,19 @@ export const fetchAllEvents = async () => {
};
export const requestPost = async (data) => {
return await axios.post(eventsURL, data);
await axios.post(eventsURL, data);
};
export const requestPut = async (data) => {
return await axios.put(eventsURL, data);
await axios.put(eventsURL, data);
};
export const requestPatch = async (data) => {
return await axios.patch(eventsURL, data);
await axios.patch(eventsURL, data);
};
export const requestReorder = async (data) => {
const action = 'reorder';
return await axios.patch(`${eventsURL}/${action}`, data);
await axios.patch(`${eventsURL}/reorder`, data);
};
export const requestApplyDelay = async (eventId) => {
@@ -29,9 +28,9 @@ export const requestApplyDelay = async (eventId) => {
};
export const requestDelete = async (eventId) => {
return await axios.delete(`${eventsURL}/${eventId}`);
await axios.delete(`${eventsURL}/${eventId}`);
};
export const requestDeleteAll = async () => {
return await axios.delete(`${eventsURL}/all`);
await axios.delete(`${eventsURL}/all`);
};
+27 -4
View File
@@ -21,6 +21,19 @@ export const eventPlaceholderSettings = {
endMessage: '',
};
export const userFieldsPlaceholder = {
user0: '',
user1: '',
user2: '',
user3: '',
user4: '',
user5: '',
user6: '',
user7: '',
user8: '',
user9: '',
};
export const oscPlaceholderSettings = {
port: '',
portOut: '',
@@ -92,7 +105,7 @@ export const getSettings = async () => {
};
export const postSettings = async (data) => {
return await axios.post(`${ontimeURL}/settings`, data);
await axios.post(`${ontimeURL}/settings`, data);
};
export const getInfo = async () => {
@@ -101,7 +114,7 @@ export const getInfo = async () => {
};
export const postInfo = async (data) => {
return await axios.post(`${ontimeURL}/info`, data);
await axios.post(`${ontimeURL}/info`, data);
};
export const getAliases = async () => {
@@ -110,7 +123,17 @@ export const getAliases = async () => {
};
export const postAliases = async (data) => {
return await axios.post(`${ontimeURL}/aliases`, data);
await axios.post(`${ontimeURL}/aliases`, data);
};
export const getUserFields = async () => {
const res = await axios.get(`${ontimeURL}/userfields`);
return res.data;
};
export const postUserFields = async (data) => {
await axios.post(`${ontimeURL}/userfields`, data);
};
export const getOSC = async () => {
@@ -119,7 +142,7 @@ export const getOSC = async () => {
};
export const postOSC = async (data) => {
return await axios.post(`${ontimeURL}/osc`, data);
await axios.post(`${ontimeURL}/osc`, data);
};
export const downloadEvents = async () => {
+14 -12
View File
@@ -1,14 +1,16 @@
// Exported viewer links
const speakerLink = 'http://localhost:4001/speaker';
const smLink = 'http://localhost:4001/sm';
const publicLink = 'http://localhost:4001/public';
const pipLink = 'http://localhost:4001/pip';
const studioLink = 'http://localhost:4001/studio';
// Exported viewer link location
const speakerLocation = 'speaker';
const smLocation = 'sm';
const publicLocation = 'public';
const pipLocation = 'pip';
const studioLocation = 'studio';
const cuesheetLocation = 'cuesheet';
export const viewerLinks = [
{ link: speakerLink, label: 'Speaker Screen' },
{ link: smLink, label: 'Backstage Screen' },
{ link: publicLink, label: 'Public Screen' },
{ link: pipLink, label: 'Picture in Picture' },
{ link: studioLink, label: 'Studio Clock' }
export const viewerLocations = [
{ link: speakerLocation, label: 'Speaker Screen' },
{ link: smLocation, label: 'Backstage Screen' },
{ link: publicLocation, label: 'Public Screen' },
{ link: pipLocation, label: 'Picture in Picture' },
{ link: studioLocation, label: 'Studio Clock' },
{ link: cuesheetLocation, label: 'Cuesheet' }
];
+14 -15
View File
@@ -1,4 +1,4 @@
import { createContext, useCallback, useEffect, useState } from 'react';
import React, { createContext, useCallback, useEffect, useState } from 'react';
import { useFetch } from '../hooks/useFetch';
import { APP_SETTINGS } from '../api/apiConstants';
import { getSettings } from '../api/ontimeApi';
@@ -6,11 +6,11 @@ import { getSettings } from '../api/ontimeApi';
export const AppContext = createContext({
auth: false,
data: {
pinCode: null
}
pinCode: null,
},
});
export const AppContextProvider = (props) => {
export const AppContextProvider = ({ children }) => {
const [auth, setAuth] = useState(true);
const { data } = useFetch(APP_SETTINGS, getSettings);
@@ -21,17 +21,16 @@ export const AppContextProvider = (props) => {
} else {
setAuth(false);
}
},[data])
const validate = useCallback((pin) => {
const correct = pin === data.pinCode;
setAuth(correct);
return correct;
}, [data]);
return (
<AppContext.Provider value={{ auth, validate }}>
{props.children}
</AppContext.Provider>
const validate = useCallback(
(pin) => {
const correct = pin === data.pinCode;
setAuth(correct);
return correct;
},
[data]
);
};
return <AppContext.Provider value={{ auth, validate }}>{children}</AppContext.Provider>;
};
+71
View File
@@ -0,0 +1,71 @@
import React, { createContext, useCallback } from 'react';
import { useLocalStorage } from '../hooks/useLocalStorage';
export const CollapseContext = createContext({
collapsed: {},
setCollapsed: () => undefined,
clearCollapsed: () => undefined,
isCollapsed: () => undefined,
});
export const CollapseProvider = ({ children }) => {
const [collapsed, saveCollapsed] = useLocalStorage('collapsed', {});
/**
* @description Sets collapsed state for a single id
* @param {string} - id
* @param {boolean} - collapsed / not collapsed
*/
const setCollapsed = useCallback(
(id, isCollapsed) => {
if (isCollapsed) {
saveCollapsed((prev) => ({ ...prev, [id]: true }));
} else {
saveCollapsed((prev) => {
const newObject = { ...prev };
delete newObject[id];
return { ...newObject };
});
}
},
[saveCollapsed]
);
/**
* @description Clears collapsed state in local-storage
*/
const collapseMultiple = useCallback(
(events) => {
const newOptions = {};
for (const event of events) {
newOptions[event.id] = true;
}
saveCollapsed(newOptions);
},
[saveCollapsed]
);
/**
* @description Clears collapsed state in local-storage
*/
const expandAll = useCallback(() => {
saveCollapsed({});
}, [saveCollapsed]);
/**
* @description Clears collapsed state in local-storage
* @return {boolean} - collapsed / not collapsed
*/
const isCollapsed = useCallback(
(id) => {
return id in collapsed;
},
[collapsed]
);
return (
<CollapseContext.Provider value={{ setCollapsed, collapseMultiple, expandAll, isCollapsed }}>
{children}
</CollapseContext.Provider>
);
};
+3 -3
View File
@@ -1,4 +1,4 @@
import { createContext, useCallback, useMemo, useState } from 'react';
import React, { createContext, useCallback, useMemo, useState } from 'react';
import { useLocalStorage } from '../hooks/useLocalStorage';
export const CursorContext = createContext({
@@ -10,7 +10,7 @@ export const CursorContext = createContext({
moveCursorDown: () => undefined,
});
export const CursorProvider = (props) => {
export const CursorProvider = ({ children }) => {
const [cursor, setCursor] = useState(0);
const [_cursorLocked, _setCursorLocked] = useLocalStorage('isCursorLocked', 'locked');
const isCursorLocked = useMemo(() => _cursorLocked === 'locked', [_cursorLocked]);
@@ -57,7 +57,7 @@ export const CursorProvider = (props) => {
moveCursorDown,
}}
>
{props.children}
{children}
</CursorContext.Provider>
);
};
@@ -1,4 +1,4 @@
import { createContext, useState } from 'react';
import React, { createContext, useState } from 'react';
export const LocalEventSettingsContext = createContext({
showQuickEntry: false,
@@ -10,7 +10,7 @@ export const LocalEventSettingsContext = createContext({
setDefaultPublic: () => undefined,
});
export const LocalEventSettingsProvider = (props) => {
export const LocalEventSettingsProvider = ({ children }) => {
const [showQuickEntry, setShowQuickEntry] = useState(false);
const [starTimeIsLastEnd, setStarTimeIsLastEnd] = useState(true);
const [defaultPublic, setDefaultPublic] = useState(false);
@@ -26,7 +26,7 @@ export const LocalEventSettingsProvider = (props) => {
setDefaultPublic,
}}
>
{props.children}
{children}
</LocalEventSettingsContext.Provider>
);
};
+44 -32
View File
@@ -1,5 +1,5 @@
import React, { createContext, useCallback, useEffect, useState } from 'react';
import { useSocket } from './socketContext';
import { createContext, useCallback, useEffect, useState } from 'react';
import { generateId } from 'ontime-utils/generate_id';
import { nowInMillis, stringFromMillis } from 'ontime-utils/time';
@@ -8,10 +8,10 @@ export const LoggingContext = createContext({
emitInfo: () => undefined,
emitWarning: () => undefined,
emitError: () => undefined,
clearLog: () => undefined
clearLog: () => undefined,
});
export const LoggingProvider = (props) => {
export const LoggingProvider = ({ children }) => {
const MAX_MESSAGES = 100;
const socket = useSocket();
const [logData, setLogData] = useState([]);
@@ -40,57 +40,69 @@ export const LoggingProvider = (props) => {
* @param level
* @private
*/
const _send = useCallback((text, level) => {
if (socket != null) {
const m = {
id: generateId(),
origin,
time: stringFromMillis(nowInMillis()),
level,
text
const _send = useCallback(
(text, level) => {
if (socket != null) {
const m = {
id: generateId(),
origin,
time: stringFromMillis(nowInMillis()),
level,
text,
};
setLogData((l) => [m, ...l]);
socket.emit('logger', m);
}
setLogData((l) => [m, ...l]);
socket.emit('logger', m);
}
if (logData.length > MAX_MESSAGES) {
setLogData((l) => l.pop());
}
},[logData, socket]);
if (logData.length > MAX_MESSAGES) {
setLogData((l) => l.pop());
}
},
[logData, socket]
);
/**
* Sends a message with level INFO
* @param text
*/
const emitInfo = useCallback((text) => {
_send(text, 'INFO');
}, [_send]);
const emitInfo = useCallback(
(text) => {
_send(text, 'INFO');
},
[_send]
);
/**
* Sends a message with level WARN
* @param text
*/
const emitWarning = useCallback((text) => {
_send(text, 'WARN');
}, [_send]);
const emitWarning = useCallback(
(text) => {
_send(text, 'WARN');
},
[_send]
);
/**
* Sends a message with level ERROR
* @param text
*/
const emitError = useCallback((text) => {
_send(text, 'ERROR');
}, [_send]);
const emitError = useCallback(
(text) => {
_send(text, 'ERROR');
},
[_send]
);
/**
* Clears running log
*/
const clearLog = useCallback(() => {
setLogData([])
setLogData([]);
}, []);
return (
<LoggingContext.Provider value = {{ emitInfo, logData, emitWarning, emitError, clearLog }}>
{props.children}
<LoggingContext.Provider value={{ emitInfo, logData, emitWarning, emitError, clearLog }}>
{children}
</LoggingContext.Provider>
)
}
);
};
@@ -0,0 +1,78 @@
import React, { createContext, useCallback, useState } from 'react';
import { useLocalStorage } from '../hooks/useLocalStorage';
export const TableSettingsContext = createContext({
theme: '',
showSettings: false,
followSelected: false,
toggleSettings: () => undefined,
toggleTheme: () => undefined,
toggleFollow: () => undefined,
});
export const TableSettingsProvider = ({ children }) => {
const [theme, setTheme] = useLocalStorage('table-color-theme', 'dark');
const [followSelected, setFollowSelected] = useLocalStorage('table-follow-selected', false);
const [showSettings, setShowSettings] = useState(false);
/**
* @description Toggles the current value of dark mode
* @param {string} val - 'light' or 'dark'
*/
const toggleTheme = useCallback(
(val) => {
if (val === undefined) {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
} else {
setTheme(val);
}
},
[setTheme]
);
/**
* @description Toggles visibility state for settings
* @param {boolean} val - whether the settings window is visible
*/
const toggleSettings = useCallback(
(val) => {
if (val === undefined) {
setShowSettings((prev) => !prev);
} else {
setShowSettings(val);
}
},
[setShowSettings]
);
/**
* @description Toggles follow option
* @param {boolean} val - whether the window follows selected event
*/
const toggleFollow = useCallback(
(val) => {
if (val === undefined) {
setFollowSelected((prev) => !prev);
} else {
setFollowSelected(val);
}
},
[setFollowSelected]
);
return (
<TableSettingsContext.Provider
value={{
theme,
showSettings,
followSelected,
toggleSettings,
toggleTheme,
toggleFollow,
}}
>
{children}
</TableSettingsContext.Provider>
);
};
-60
View File
@@ -1,60 +0,0 @@
const { atom } = require('jotai');
const PATH = 'option-collapse';
const initialValue = {};
// collapse options object, initialised from local storage
// REFRACT: When do we read from local storage?
// on every render?
export const collapseAtom = atom(
(get) => {
const storedOptions = localStorage.getItem(PATH);
if (storedOptions == null) return initialValue;
return JSON.parse(storedOptions);
},
(get, set, newValues) => {
set(collapseAtom, newValues);
localStorage.setItem(PATH, JSON.stringify(newValues));
}
);
// get a single option, if it exists
export const SelectCollapse = (id) => {
return atom((get) => get(collapseAtom)[id]);
};
// change a single item in object
export const HandleCollapse = atom(null, (get, set, payload) => {
const updatedVal = {
...get(collapseAtom),
...payload,
};
set(collapseAtom, updatedVal);
localStorage.setItem(PATH, JSON.stringify(updatedVal));
});
// change collapsed in several items
export const BatchOperation = atom(null, (get, set, payload) => {
let prevOptions = get(collapseAtom);
let newOptions = {};
for (const item of payload.items) {
newOptions[item.id] = payload.isCollapsed;
}
if (payload.clear) {
// clear object
prevOptions = {};
// clear localstorage
localStorage.removeItem(PATH);
}
const options = { ...prevOptions, ...newOptions };
set(collapseAtom, options);
localStorage.setItem(PATH, JSON.stringify(options));
});
-34
View File
@@ -1,34 +0,0 @@
const { atom } = require('jotai');
const PATH = 'option-gen';
const initialValue = {
cursor: 'locked',
};
export const settingsAtom = atom(
(get) => {
const storedOptions = localStorage.getItem(PATH);
if (storedOptions == null) return initialValue;
return JSON.parse(storedOptions);
},
(get, set, newValues) => {
set(settingsAtom, newValues);
localStorage.setItem(PATH, JSON.stringify(newValues));
}
);
// get a single option, if it exists
export const SelectSetting = (setting) => {
return atom((get) => get(settingsAtom)[setting]);
};
// change a single item in object
export const HandleOptions = atom(null, (get, set, payload) => {
const updatedVal = {
...get(settingsAtom),
...payload,
};
set(settingsAtom, updatedVal);
localStorage.setItem(PATH, JSON.stringify(updatedVal));
});
+4 -7
View File
@@ -1,4 +1,4 @@
import { createContext, useContext, useEffect, useState } from 'react';
import React, { createContext, useContext, useEffect, useState } from 'react';
import io from 'socket.io-client';
import { serverURL } from 'app/api/apiConstants';
@@ -8,7 +8,7 @@ export const useSocket = () => {
return useContext(SocketContext);
};
function SocketProvider(props) {
function SocketProvider({ children }) {
const [socket, setSocket] = useState();
useEffect(() => {
@@ -17,10 +17,7 @@ function SocketProvider(props) {
return () => s.disconnect();
}, []);
return (
<SocketContext.Provider value={socket}>
{props.children}
</SocketContext.Provider>
);
return <SocketContext.Provider value={socket}>{children}</SocketContext.Provider>;
}
export default SocketProvider;
+7 -3
View File
@@ -5,7 +5,7 @@ import { useState } from 'react';
export const useLocalStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
const item = window.localStorage.getItem(`ontime-${key}`);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
@@ -14,8 +14,12 @@ export const useLocalStorage = (key, initialValue) => {
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore));
} catch (error) {
console.log(error);
}
+32
View File
@@ -0,0 +1,32 @@
import { useMutation, useQueryClient } from 'react-query';
import { EVENTS_TABLE } from '../api/apiConstants';
export default function useMutateEvents(mutation){
const queryClient = useQueryClient();
return useMutation(mutation, {
onMutate: async (newEvent) => {
// cancel ongoing queries
queryClient.cancelQueries(EVENTS_TABLE, { exact: true });
// Snapshot the previous value
const previousEvent = queryClient.getQueryData([EVENTS_TABLE, newEvent.id]);
// optimistically update object
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
// Return a context with the previous and new event
return { previousEvent, newEvent };
},
// Mutation fails, rollback undoes optimist update
onError: (error, newEvent, context) => {
queryClient.setQueryData([EVENTS_TABLE, context.newEvent.id], context.previousEvent);
},
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: (newEvent) => {
queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
},
});
}
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
export default function ApplyIconBtn(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, size, ...rest } = props;
return (
<Tooltip label='Apply delays'>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiCheck />}
colorScheme='orange'
onClick={clickhandler}
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiChevronsUp } from '@react-icons/all-files/fi/FiChevronsUp';
export default function CollapseBtn(props) {
const { clickhandler } = props;
const { clickhandler, size } = props;
return (
<Tooltip label='Collapse all'>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiChevronsUp />}
colorScheme='white'
variant='outline'
@@ -1,14 +1,15 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { IoCaretDown } from '@react-icons/all-files/io5/IoCaretDown';
export default function CursorDownBtn(props) {
const { clickhandler, active, ref } = props;
const { clickhandler, active, ref, size } = props;
return (
<Tooltip label='Move cursor down Alt + ↓'>
<IconButton
ref={ref}
size={props.size || 'xs'}
size={size || 'xs'}
icon={<IoCaretDown />}
color={active ? 'pink.100' : 'pink.300'}
borderColor={active ? undefined : 'pink.300'}
@@ -1,14 +1,15 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
export default function CursorLockedBtn(props) {
const { clickhandler, active, ref } = props;
const { clickhandler, active, ref, size } = props;
return (
<Tooltip label='Lock cursor to current'>
<IconButton
<IconButton
ref={ref}
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiTarget />}
color={active ? 'pink.100' : 'pink.300'}
borderColor={active ? undefined : 'pink.300'}
@@ -1,14 +1,15 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { IoCaretUp } from '@react-icons/all-files/io5/IoCaretUp';
export default function CursorUpBtn(props) {
const { clickhandler, active, ref } = props;
const { clickhandler, active, ref, size } = props;
return (
<Tooltip label='Move cursor up Alt + ↑'>
<IconButton
ref={ref}
size={props.size || 'xs'}
size={size || 'xs'}
icon={<IoCaretUp />}
color={active ? 'pink.100' : 'pink.300'}
borderColor={active ? undefined : 'pink.300'}
@@ -1,10 +1,11 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { useState } from 'react';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import { Tooltip } from '@chakra-ui/tooltip';
export default function DeleteIconBtn(props) {
const { actionHandler, ...rest } = props;
const { actionHandler, size, ...rest } = props;
const [loading, setLoading] = useState(false);
const handleClick = () => {
@@ -15,7 +16,7 @@ export default function DeleteIconBtn(props) {
return (
<Tooltip label='Delete'>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<IoRemove />}
colorScheme='red'
onClick={handleClick}
@@ -1,12 +1,13 @@
import React from 'react';
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
import { Button } from '@chakra-ui/button';
export default function EnableBtn(props) {
const { active, text, actionHandler } = props;
const { active, text, actionHandler, size } = props;
return (
<Button
size={props.size || 'xs'}
size={size || 'xs'}
leftIcon={active ? <IoCheckmarkSharp /> : <IoCloseSharp />}
colorScheme='blue'
variant={active ? 'solid' : 'outline'}
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiChevronsDown } from '@react-icons/all-files/fi/FiChevronsDown';
export default function ExpandBtn(props) {
const { clickhandler } = props;
const { clickhandler, size } = props;
return (
<Tooltip label='Expand all'>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiChevronsDown />}
colorScheme='white'
variant='outline'
@@ -1,11 +1,12 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
import { Tooltip } from '@chakra-ui/tooltip';
export default function NextIconBtn(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, disabled, ...rest } = props;
return (
<Tooltip label='Next event' openDelay={500} shouldWrapChildren={props.disabled}>
<Tooltip label='Next event' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoPlaySkipForward size='22px' />}
colorScheme='whiteAlpha'
@@ -1,14 +1,15 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
import { Tooltip } from '@chakra-ui/tooltip';
export default function OnAirIconBtn(props) {
const { actionHandler, active, ...rest } = props;
const { actionHandler, active, size, ...rest } = props;
return (
<Tooltip label={active ? 'Go Off Air' : 'Go On Air'} openDelay={500}>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={active ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
colorScheme='blue'
variant={active ? 'solid' : 'outline'}
@@ -1,11 +1,12 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoPause } from '@react-icons/all-files/io5/IoPause';
import { Tooltip } from '@chakra-ui/tooltip';
export default function PauseIconBtn(props) {
const { clickhandler, active, ...rest } = props;
const { clickhandler, active, disabled, ...rest } = props;
return (
<Tooltip label='Pause timer' openDelay={500} shouldWrapChildren={props.disabled}>
<Tooltip label='Pause timer' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoPause size='24px' />}
colorScheme='orange'
@@ -1,11 +1,12 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoPlaySkipBack } from '@react-icons/all-files/io5/IoPlaySkipBack';
import { Tooltip } from '@chakra-ui/tooltip';
export default function PrevIconBtn(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, disabled, ...rest } = props;
return (
<Tooltip label='Previous event' openDelay={500} shouldWrapChildren={props.disabled}>
<Tooltip label='Previous event' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoPlaySkipBack size='22px' />}
colorScheme='whiteAlpha'
@@ -1,19 +1,18 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
export default function PublicIconBtn(props) {
const { actionHandler, active, ...rest } = props;
const { actionHandler, active, size, ...rest } = props;
return (
<Tooltip label={active ? 'Make event private' : 'Make event public'}>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiUsers />}
colorScheme='blue'
variant={active ? 'solid' : 'outline'}
onClick={() =>
actionHandler('update', { field: 'isPublic', value: !active })
}
onClick={() => actionHandler('update', { field: 'isPublic', value: !active })}
_focus={{ boxShadow: 'none' }}
{...rest}
/>
@@ -1,11 +1,12 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoArrowUndo } from '@react-icons/all-files/io5/IoArrowUndo';
import { Tooltip } from '@chakra-ui/tooltip';
export default function ReloadIconButton(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, disabled, ...rest } = props;
return (
<Tooltip label='Reload event' openDelay={500} shouldWrapChildren={props.disabled}>
<Tooltip label='Reload event' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoArrowUndo size='22px' />}
colorScheme='whiteAlpha'
@@ -1,11 +1,12 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
import { Tooltip } from '@chakra-ui/tooltip';
export default function RollIconBtn(props) {
const { clickhandler, active, ...rest } = props;
const { clickhandler, active, disabled, ...rest } = props;
return (
<Tooltip label='Roll mode' openDelay={500} shouldWrapChildren={props.disabled}>
<Tooltip label='Roll mode' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoTimeOutline size='24px' />}
colorScheme='blue'
@@ -1,11 +1,12 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
import { Tooltip } from '@chakra-ui/tooltip';
export default function StartIconBtn(props) {
const { clickhandler, active, ...rest } = props;
const { clickhandler, active, disabled, ...rest } = props;
return (
<Tooltip label='Start timer' openDelay={500} shouldWrapChildren={props.disabled}>
<Tooltip label='Start timer' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoPlay size='24px' />}
colorScheme='green'
@@ -1,11 +1,12 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoStop } from '@react-icons/all-files/io5/IoStop';
import { Tooltip } from '@chakra-ui/tooltip';
export default function UnloadIconBtn(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, disabled, ...rest } = props;
return (
<Tooltip label='Unload event' openDelay={500} shouldWrapChildren={props.disabled}>
<Tooltip label='Unload event' openDelay={500} shouldWrapChildren={disabled}>
<IconButton
icon={<IoStop size='22px' />}
colorScheme='red'
@@ -1,14 +1,15 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
import { Tooltip } from '@chakra-ui/tooltip';
export default function VisibleIconBtn(props) {
const { actionHandler, active, ...rest } = props;
const { actionHandler, active, size, ...rest } = props;
return (
<Tooltip label={active ? 'Make invisible' : 'Make visible'} openDelay={500}>
<IconButton
size={props.size || 'xs'}
icon={<IoSunny size={'18px'}/>}
size={size || 'xs'}
icon={<IoSunny size='18px' />}
colorScheme='blue'
variant={active ? 'solid' : 'outline'}
onClick={() =>
@@ -1,3 +1,4 @@
import React from 'react';
import PropTypes from "prop-types";
import style from "../../../features/info/Info.module.scss";
import {Icon} from "@chakra-ui/react";
@@ -1,4 +1,4 @@
import { memo } from 'react';
import React, { memo } from 'react';
import { formatDisplay } from 'common/utils/dateConfig';
import PropTypes from 'prop-types';
import styles from './Countdown.module.css';
@@ -28,5 +28,5 @@ Countdown.propTypes = {
time: PropTypes.number,
small: PropTypes.bool,
isNegative: PropTypes.bool,
hideZeroHour: PropTypes.bool,
hideZeroHours: PropTypes.bool,
};
@@ -1,3 +1,4 @@
/* eslint-disable react/destructuring-assignment */
import React from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
@@ -1,5 +1,5 @@
import React, { useContext } from 'react';
import EditableTimer from 'common/input/EditableTimer';
import { useContext } from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { validateTimes } from '../../../app/entryValidator';
import PropTypes from 'prop-types';
@@ -8,6 +8,13 @@ export default function EventTimes(props) {
const { actionHandler, delay, timeStart, timeEnd, previousEnd } = props;
const { emitWarning } = useContext(LoggingContext);
/**
* This code is duplicated from EventTimesVertical
* @description Validates a time input against its pair
* @param {string} entry - field to validate: timeStart, timeEnd, durationOverride
* @param {number} val - field value
* @return {boolean}
*/
const handleValidate = (entry, val) => {
if (val == null || timeStart == null || timeEnd == null) return true;
if (timeStart === 0) return true;
@@ -18,8 +25,10 @@ export default function EventTimes(props) {
start = val;
} else if (entry === 'timeEnd') {
end = val;
} else if (entry === 'durationOverride'){
return true;
} else {
return;
return false
}
const valid = validateTimes(start, end);
@@ -54,8 +63,8 @@ export default function EventTimes(props) {
EventTimes.propTypes = {
actionHandler: PropTypes.func.isRequired,
delay: PropTypes.number.isRequired,
timeStart: PropTypes.number.isRequired,
timeEnd: PropTypes.number.isRequired,
previousEnd: PropTypes.number.isRequired,
delay: PropTypes.number,
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
previousEnd: PropTypes.number,
};
@@ -1,6 +1,6 @@
import React, { useContext } from 'react';
import EditableTimer from 'common/input/EditableTimer';
import { stringFromMillis } from 'ontime-utils/time';
import { useContext } from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { validateTimes } from '../../../app/entryValidator';
import PropTypes from 'prop-types';
@@ -56,11 +56,11 @@ const TimesDelayed = (props) => {
TimesDelayed.propTypes = {
handleValidate: PropTypes.func.isRequired,
actionHandler: PropTypes.func.isRequired,
delay: PropTypes.number.isRequired,
timeStart: PropTypes.number.isRequired,
timeEnd: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
previousEnd: PropTypes.number.isRequired,
delay: PropTypes.number,
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
duration: PropTypes.number,
previousEnd: PropTypes.number,
};
const Times = (props) => {
@@ -102,16 +102,23 @@ const Times = (props) => {
Times.propTypes = {
handleValidate: PropTypes.func.isRequired,
actionHandler: PropTypes.func.isRequired,
timeStart: PropTypes.number.isRequired,
timeEnd: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
previousEnd: PropTypes.number.isRequired,
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
duration: PropTypes.number,
previousEnd: PropTypes.number,
};
export default function EventTimesVertical(props) {
const { delay, timeStart, timeEnd, duration, previousEnd } = props;
const { delay, timeStart, timeEnd, duration, previousEnd, actionHandler } = props;
const { emitWarning } = useContext(LoggingContext);
/**
* This code is duplicated from EventTimes
* @description Validates a time input against its pair
* @param {string} entry - field to validate: timeStart, timeEnd, durationOverride
* @param {number} val - field value
* @return {boolean}
*/
const handleValidate = (entry, val) => {
if (val == null || timeStart == null || timeEnd == null) return true;
if (timeStart === 0) return true;
@@ -122,8 +129,10 @@ export default function EventTimesVertical(props) {
start = val;
} else if (entry === 'timeEnd') {
end = val;
} else if (entry === 'durationOverride') {
return true;
} else {
return;
return false;
}
const valid = validateTimes(start, end);
@@ -137,7 +146,7 @@ export default function EventTimesVertical(props) {
return delay != null && delay !== 0 ? (
<TimesDelayed
handleValidate={handleValidate}
actionHandler={props.actionHandler}
actionHandler={actionHandler}
delay={delay}
timeStart={timeStart}
timeEnd={timeEnd}
@@ -147,7 +156,7 @@ export default function EventTimesVertical(props) {
) : (
<Times
handleValidate={handleValidate}
actionHandler={props.actionHandler}
actionHandler={actionHandler}
timeStart={timeStart}
timeEnd={timeEnd}
duration={duration}
@@ -157,9 +166,10 @@ export default function EventTimesVertical(props) {
}
EventTimesVertical.propTypes = {
delay: PropTypes.number.isRequired,
timeStart: PropTypes.number.isRequired,
timeEnd: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
previousEnd: PropTypes.number.isRequired,
};
delay: PropTypes.number,
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
duration: PropTypes.number,
previousEnd: PropTypes.number,
actionHandler: PropTypes.func.isRequired,
};
@@ -1,3 +1,4 @@
import React from 'react';
import { clamp } from 'app/utils/math';
import styles from './MyProgressBar.module.css';
+1 -1
View File
@@ -1,8 +1,8 @@
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from "prop-types";
import { Link } from 'react-router-dom';
import { Image } from '@chakra-ui/react';
import { AnimatePresence, motion } from 'framer-motion';
import { useState, useEffect, useCallback } from 'react';
import navlogo from 'assets/images/logos/LOGO-72.png';
import style from './NavLogo.module.scss';
@@ -1,14 +1,14 @@
import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import style from './ProtectRoute.module.scss';
import { PinInput, PinInputField } from '@chakra-ui/react';
import { IconButton } from '@chakra-ui/button';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
import { AppContext } from '../../../app/context/AppContext';
import { useContext, useEffect, useState } from 'react';
export default function ProtectRoute(props) {
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
export default function ProtectRoute({ children }) {
const isLocal =
window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
const [pin, setPin] = useState('');
const [failed, setFailed] = useState(false);
const { auth, validate } = useContext(AppContext);
@@ -23,7 +23,7 @@ export default function ProtectRoute(props) {
if (!r) {
setFailed(true);
}
}
};
return (
<>
@@ -56,12 +56,12 @@ export default function ProtectRoute(props) {
</div>
</div>
) : (
props.children
children
)}
</>
);
}
ProtectRoute.propTypes = {
children: PropTypes.node.isRequired
children: PropTypes.node.isRequired,
};
@@ -1,12 +1,13 @@
import React, { useEffect, useState } from 'react';
import TodayItem from './TodayItem';
import style from './Paginator.module.css';
import { useEffect, useState } from 'react';
import { useInterval } from 'app/hooks/useInterval';
import PropTypes from 'prop-types';
export default function Paginator(props) {
const { events, selectedId } = props;
const LIMIT_PER_PAGE = props.limit || 8;
const SCROLL_TIME = props.time * 1000 || 10000;
const { events, selectedId, limit = 7, time = 10, isBackstage } = props;
const LIMIT_PER_PAGE = limit;
const SCROLL_TIME = time * 1000 || 10000;
const [numEvents, setNumEvents] = useState(0);
const [page, setPage] = useState([]);
const [pages, setPages] = useState(0);
@@ -46,10 +47,7 @@ export default function Paginator(props) {
<div className={style.nav}>
{pages > 1 &&
[...Array(pages)].map((p, i) => (
<div
key={i}
className={i === selPage ? style.navItemSelected : style.navItem}
/>
<div key={i} className={i === selPage ? style.navItemSelected : style.navItem} />
))}
</div>
<div className={style.entries}>
@@ -63,6 +61,7 @@ export default function Paginator(props) {
timeStart={e.timeStart}
timeEnd={e.timeEnd}
title={e.title}
colour={isBackstage ? e.colour : ''}
backstageEvent={!e.isPublic}
/>
);
@@ -71,3 +70,11 @@ export default function Paginator(props) {
</>
);
}
Paginator.propTypes = {
events: PropTypes.array,
selectedId: PropTypes.string,
limit: PropTypes.number,
time: PropTypes.number,
isBackstage: PropTypes.bool,
};
@@ -1,3 +1,4 @@
import React from 'react';
import style from './TitleCard.module.css';
export default function TitleCard(props) {
@@ -1,3 +1,4 @@
import React from 'react';
import style from './TitleSide.module.css';
export default function TitleSide(props) {
@@ -1,25 +1,38 @@
import React from 'react';
import { stringFromMillis } from 'ontime-utils/time';
import style from './Paginator.module.css';
import PropTypes from 'prop-types';
export default function TodayItem(props) {
const { selected, timeStart, timeEnd, title, backstageEvent } = props;
const { selected, timeStart, timeEnd, title, backstageEvent, colour } = props;
// Format timers
const start = stringFromMillis(timeStart, false) || '';
const end = stringFromMillis(timeEnd, false) || '';
// user colours
const userColour = colour !== '' ? colour : 'transparent';
// select styling
let selectStyle = style.entryPast;
if (selected === 1) selectStyle = style.entryNow;
else if (selected === 2) selectStyle = style.entryFuture;
return (
<div className={selectStyle}>
<div
className={`${style.entryTimes} ${
backstageEvent ? style.backstage : undefined
}`}
>{`${start} · ${end}`}</div>
<div className={selectStyle} style={{ borderLeft: `4px solid ${userColour}` }}>
<div className={`${style.entryTimes} ${backstageEvent ? style.backstage : undefined}`}>
{`${start} · ${end}`}
</div>
<div className={style.entryTitle}>{title}</div>
{backstageEvent && <div className={style.backstageInd}/>}
{backstageEvent && <div className={style.backstageInd} />}
</div>
);
}
TodayItem.propTypes = {
selected: PropTypes.bool,
timeStart: PropTypes.number,
timeEnd: PropTypes.number,
title: PropTypes.string,
backstageEvent: PropTypes.bool,
colour: PropTypes.string,
};
+27
View File
@@ -0,0 +1,27 @@
import React, { useEffect, useRef } from 'react';
import { Textarea } from '@chakra-ui/react';
import autosize from 'autosize/dist/autosize';
export const AutoTextArea = (props) => {
const ref = useRef();
useEffect(() => {
const node = ref.current;
autosize(ref.current);
return () => {
autosize.destroy(node);
};
}, []);
return (
<Textarea
overflow='hidden'
w='100%'
resize='none'
ref={ref}
transition='height none'
{...props}
/>
);
};
+1 -1
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/react';
import { useEffect, useState } from 'react';
import { clamp } from '../../app/utils/math';
import style from './TimeInput.module.css';
import PropTypes from 'prop-types';
+22 -8
View File
@@ -1,21 +1,27 @@
import React, { useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
import { useEffect, useState } from 'react';
import style from './EditableText.module.scss';
import PropTypes from 'prop-types';
export default function EditableText(props) {
const { label, defaultValue, placeholder, submitHandler, ...rest } = props;
const { label, defaultValue, placeholder, submitHandler, maxchar = 40, ...rest } = props;
const [text, setText] = useState(defaultValue || '');
const maxchar = props.maxchar || 40;
useEffect(() => {
if (defaultValue == null) setText('');
else setText(defaultValue);
}, [defaultValue]);
const handleSubmit = (submitedVal) => {
const handleSubmit = (submittedVal) => {
// No need to update if it hasnt changed
if (submitedVal === defaultValue) return;
submitHandler(submitedVal);
if (submittedVal === defaultValue) return;
// submit a cleaned up version of the string
const cleanVal = submittedVal.trim();
submitHandler(cleanVal);
if (cleanVal !== submittedVal) {
setText(cleanVal);
}
};
const handleChange = (val) => {
@@ -33,9 +39,17 @@ export default function EditableText(props) {
className={style.inline}
{...rest}
>
<EditablePreview color={text === '' ? '#666' : 'inherit'} maxWidth='75%' />
<EditableInput overflowX='hidden' maxWidth='75%' />
<EditablePreview className={text === '' ? style.preview : ''} />
<EditableInput />
</Editable>
</div>
);
}
EditableText.propTypes = {
label: PropTypes.string,
defaultValue: PropTypes.string,
placeholder: PropTypes.string,
submitHandler: PropTypes.func.isRequired,
maxchar: PropTypes.number,
};
@@ -1,28 +1,28 @@
.title {
padding-left: 1em;
font-size: 0.75em;
color: #aaa;
display: inline-block;
width: 6em;
}
@use '../../styles/main' as *;
.block {
overflow: hidden;
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
*:nth-of-type(2) {
flex: 1;
width: 20em;
.title {
padding-left: 1em;
font-size: 0.75em;
color: $label-gray;
display: inline-block;
min-width: 6em;
}
}
.preview {
color: #666;
}
.inline {
display: inline;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.inline {
display: inline;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
}
+19 -16
View File
@@ -1,5 +1,5 @@
import React, { useContext, useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
import { useContext, useEffect, useState } from 'react';
import { forgivingStringToMillis } from '../utils/dateConfig';
import { stringFromMillis } from 'ontime-utils/time';
import style from './EditableTimer.module.css';
@@ -12,6 +12,16 @@ export default function EditableTimer(props) {
const [value, setValue] = useState('');
// prepare time fields
const validateValue = (value) => {
const success = handleSubmit(value);
if (success) {
const ms = forgivingStringToMillis(value);
setValue(stringFromMillis(ms + delay));
} else {
setValue(stringFromMillis(time + delay));
}
};
useEffect(() => {
if (time == null) return;
try {
@@ -21,27 +31,19 @@ export default function EditableTimer(props) {
}
}, [time, delay, emitError]);
const validateValue = (value) => {
const success = handleSubmit(value);
if (success) setValue(value);
else setValue(stringFromMillis(time + delay, true));
};
const handleSubmit = (value) => {
// Check if there is anything there
if (value === '') return false;
let newValMillis;
let newValMillis = 0;
// check for known aliases
if (value === 'p' || value === 'prev' || value === 'previous') {
// string to pass should be the time of the end before
if (previousEnd != null) {
newValMillis = previousEnd;
} else {
newValMillis = 0;
}
} else if (value.startsWith('+')) {
} else if (value.startsWith('+') || value.startsWith('p+') || value.startsWith('p +')) {
// string to pass should add to the end before
const val = value.substring(1);
newValMillis = previousEnd + forgivingStringToMillis(val);
@@ -65,10 +67,11 @@ export default function EditableTimer(props) {
return true;
};
const isDelayed = (delay != null && delay !== 0)
const isDelayed = delay != null && delay !== 0;
return (
<Editable
data-testid='editable-timer'
onChange={(v) => setValue(v)}
onSubmit={(v) => validateValue(v)}
onCancel={() => setValue(stringFromMillis(time + delay, true))}
@@ -76,7 +79,7 @@ export default function EditableTimer(props) {
className={isDelayed ? style.delayedEditable : style.editable}
>
<EditablePreview />
<EditableInput type='text' placeholder='--:--:--' />
<EditableInput type='text' placeholder='--:--:--' data-testid='editable-timer-input' />
</Editable>
);
}
@@ -84,8 +87,8 @@ export default function EditableTimer(props) {
EditableTimer.propTypes = {
name: PropTypes.string.isRequired,
actionHandler: PropTypes.func.isRequired,
time: PropTypes.number.isRequired,
delay: PropTypes.number.isRequired,
time: PropTypes.number,
delay: PropTypes.number,
validate: PropTypes.func.isRequired,
previousEnd: PropTypes.number.isRequired,
previousEnd: PropTypes.number,
};
@@ -0,0 +1,32 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import EditableTimer from '../EditableTimer';
const renderEditableTimer = (
name = 'test',
actionHandler = () => undefined,
validate = () => undefined
) => render(<EditableTimer name={name} actionHandler={actionHandler} validate={validate} />);
describe('test EditableTimer component', () => {
const testName = "test";
const actionHandler = jest.fn();
const validate = jest.fn();
renderEditableTimer(testName, actionHandler, validate);
const editableTimer = screen.getByTestId('editable-timer');
const editableInput = screen.getByTestId('editable-timer-input');
it('renders correctly', () => {
expect(editableTimer).toBeInTheDocument();
expect(editableInput).toBeInTheDocument();
userEvent.type(editableInput, 'p');
expect(editableInput).toHaveValue('p');
userEvent.type(editableInput, '{enter}');
// no previous is given, defaults to 0
expect(validate).toHaveBeenCalledWith(testName, 0);
});
});
+1
View File
@@ -1,3 +1,4 @@
import React from 'react';
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
import style from './Empty.module.css';
@@ -1,9 +1,9 @@
import {
forgivingStringToMillis,
formatDisplay,
isTimeString,
millisToMinutes,
millisToSeconds,
forgivingStringToMillis,
timeStringToMillis,
} from '../dateConfig';
import { stringFromMillis } from 'ontime-utils/time';
@@ -280,33 +280,87 @@ describe('test isTimeString() function handle different separators', () => {
}
});
describe('test timeHelper() function handles separators', () => {
const ts = ['1:2:3:10', '2,10', '2.10'];
for (const s of ts) {
test(`it handles ${s}`, () => {
expect(typeof forgivingStringToMillis(s)).toBe('number');
});
}
});
describe('test forgivingStringToMillis()', () => {
describe('function handles separators', () => {
const testData = [
{ value: '1:2:3:10', expect: 3723000 },
{ value: '2,10', expect: 130000 },
{ value: '2.10', expect: 130000 },
{ value: '2 10', expect: 130000 },
];
describe('test timeHelper() parses strings correctly', () => {
const ts = [
{ value: '', expect: 0 },
{ value: '0', expect: 0 },
{ value: '-0', expect: 0 },
{ value: '1', expect: 60 * 1000 },
{ value: '-1', expect: 60 * 1000 },
{ value: '1.2', expect: 60 * 1000 + 2 * 1000 },
{ value: '1.70', expect: 60 * 1000 + 70 * 1000 },
{ value: '1.1.1', expect: 60 * 60 * 1000 + 60 * 1000 + 1000 },
{ value: '12.1.1', expect: 12 * 60 * 60 * 1000 + 60 * 1000 + 1000 },
{ value: '12.55.1', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 1000 },
{ value: '12.55.40', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 40 * 1000 },
];
for (const s of testData) {
test(`it handles ${s.value}`, () => {
expect(typeof forgivingStringToMillis(s.value)).toBe('number');
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
});
}
});
for (const s of ts) {
test(`it handles ${s.value}`, () => {
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
});
}
describe('function handles time with no separators', () => {
const testData = [
{ value: '000000', expect: 0 },
{ value: '000001', expect: 1000 },
{ value: '000100', expect: 1000*60 },
{ value: '010000', expect: 1000*60*60 },
{ value: '230000', expect: 1000*60*60*23 },
{ value: '121212', expect: 12*1000+12*60*1000+12*1000*60*60 },
];
for (const s of testData) {
test(`it handles ${s.value}`, () => {
expect(typeof forgivingStringToMillis(s.value)).toBe('number');
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
});
}
});
describe('parses strings correctly', () => {
const ts = [
{ value: '', expect: 0 },
{ value: '0', expect: 0 },
{ value: '-0', expect: 0 },
{ value: '1', expect: 60 * 1000 },
{ value: '-1', expect: 60 * 1000 },
{ value: '1.2', expect: 60 * 1000 + 2 * 1000 },
{ value: '1.70', expect: 60 * 1000 + 70 * 1000 },
{ value: '1.1.1', expect: 60 * 60 * 1000 + 60 * 1000 + 1000 },
{ value: '12.1.1', expect: 12 * 60 * 60 * 1000 + 60 * 1000 + 1000 },
{ value: '12.55.1', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 1000 },
{ value: '12.55.40', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 40 * 1000 },
];
for (const s of ts) {
test(`it handles ${s.value}`, () => {
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
});
}
});
describe('handles overflows', () => {
const ts = [
// minutes overflow
{ value: '120', expect: 1000*60*120 },
{ value: '2.0.0', expect: 1000*60*120 },
{ value: '99', expect: 1000*60*99 },
{ value: '1.39.0', expect: 1000*60*99 },
// seconds overflow
{ value: '0.120', expect: 120*1000 },
{ value: '0.0.120', expect: 120*1000 },
{ value: '0.2.0', expect: 120*1000 },
{ value: '0.99', expect: 99*1000 },
{ value: '0.0.99', expect: 99*1000 },
{ value: '0.1.39', expect: 99*1000 },
// hours overflow
{ value: '25.0.0', expect: 1000*60*60*25 },
// hours overflow
{ value: '50.0.0', expect: 1000*60*60*50 },
];
for (const s of ts) {
test(`it handles ${s.value}`, () => {
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
});
}
});
});
@@ -1,111 +1,122 @@
import {formatEventList, getEventsWithDelay, trimEventlist} from "../eventsManager";
import { formatEventList, getEventsWithDelay, trimEventlist } from '../eventsManager';
test('getEventsWithDelay function', () => {
const testData = [
{
"title": "Welcome to Ontime",
"timeStart": 28800000,
"timeEnd": 30600000,
"type": "event",
"id": "5946"
title: 'Welcome to Ontime',
timeStart: 28800000,
timeEnd: 30600000,
colour: '',
type: 'event',
id: '5946',
},
{
"duration": 60000,
"type": "delay",
"id": "24240"
duration: 60000,
type: 'delay',
id: '24240',
},
{
"title": "Unless recalled by the OSC address",
"timeStart": 34920000,
"timeEnd": 35520000,
"type": "event",
"id": "8ee5"
title: 'Unless recalled by the OSC address',
timeStart: 34920000,
timeEnd: 35520000,
colour: '',
type: 'event',
id: '8ee5',
},
{
"title": "Use simpler times to create a timer",
"timeStart": 120000,
"timeEnd": 720000,
"type": "event",
"id": "8222"
title: 'Use simpler times to create a timer',
timeStart: 120000,
timeEnd: 720000,
colour: '',
type: 'event',
id: '8222',
},
{
"duration": 900000,
"type": "delay",
"revision": 0,
"id": "a386"
duration: 900000,
type: 'delay',
revision: 0,
id: 'a386',
},
{
"title": "Add delay blocks to affect all events",
"timeStart": 37320000,
"timeEnd": 38520000,
"type": "event",
"id": "6dce"
title: 'Add delay blocks to affect all events',
timeStart: 37320000,
timeEnd: 38520000,
colour: '',
type: 'event',
id: '6dce',
},
{
"title": "Add and remove events with [+] and [-]",
"timeStart": 38520000,
"timeEnd": 45120000,
"type": "event",
"id": "2651"
title: 'Add and remove events with [+] and [-]',
timeStart: 38520000,
timeEnd: 45120000,
colour: '',
type: 'event',
id: '2651',
},
{
"type": "block",
"id": "e6a1"
type: 'block',
id: 'e6a1',
},
{
"title": "And control whether they are public",
"timeStart": 46800000,
"timeEnd": 57600000,
"type": "event",
"id": "1358"
}
title: 'And control whether they are public',
timeStart: 46800000,
timeEnd: 57600000,
colour: '',
type: 'event',
id: '1358',
},
];
const expected = [
{
"title": "Welcome to Ontime",
"timeStart": 28800000,
"timeEnd": 30600000,
"type": "event",
"id": "5946"
title: 'Welcome to Ontime',
timeStart: 28800000,
timeEnd: 30600000,
colour: '',
type: 'event',
id: '5946',
},
{
"title": "Unless recalled by the OSC address",
"timeStart": 34920000+60000,
"timeEnd": 35520000+60000,
"type": "event",
"id": "8ee5"
title: 'Unless recalled by the OSC address',
timeStart: 34920000 + 60000,
timeEnd: 35520000 + 60000,
colour: '',
type: 'event',
id: '8ee5',
},
{
"title": "Use simpler times to create a timer",
"timeStart": 120000+60000,
"timeEnd": 720000+60000,
"type": "event",
"id": "8222"
title: 'Use simpler times to create a timer',
timeStart: 120000 + 60000,
timeEnd: 720000 + 60000,
colour: '',
type: 'event',
id: '8222',
},
{
"title": "Add delay blocks to affect all events",
"timeStart": 37320000+60000+900000,
"timeEnd": 38520000+60000+900000,
"type": "event",
"id": "6dce"
title: 'Add delay blocks to affect all events',
timeStart: 37320000 + 60000 + 900000,
timeEnd: 38520000 + 60000 + 900000,
colour: '',
type: 'event',
id: '6dce',
},
{
"title": "Add and remove events with [+] and [-]",
"timeStart": 38520000+60000+900000,
"timeEnd": 45120000+60000+900000,
"type": "event",
"id": "2651"
title: 'Add and remove events with [+] and [-]',
timeStart: 38520000 + 60000 + 900000,
timeEnd: 45120000 + 60000 + 900000,
colour: '',
type: 'event',
id: '2651',
},
{
"title": "And control whether they are public",
"timeStart": 46800000,
"timeEnd": 57600000,
"type": "event",
"id": "1358"
}
]
title: 'And control whether they are public',
timeStart: 46800000,
timeEnd: 57600000,
colour: '',
type: 'event',
id: '1358',
},
];
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
});
@@ -135,6 +146,7 @@ describe('getEventsWithDelay edge cases', () => {
{
"title": "Welcome to Ontime",
"timeEnd": 30600000,
"colour": "",
"type": "event",
"id": "5946"
},
@@ -147,6 +159,7 @@ describe('getEventsWithDelay edge cases', () => {
"title": "Unless recalled by the OSC address",
"timeStart": 34920000,
"timeEnd": 35520000,
"colour": "",
"type": "event",
"id": "8ee5"
}
@@ -155,13 +168,15 @@ describe('getEventsWithDelay edge cases', () => {
{
"title": "Welcome to Ontime",
"timeEnd": 30600000,
"colour": "",
"type": "event",
"id": "5946"
},
{
"title": "Unless recalled by the OSC address",
"timeStart": 34920000+60000,
"timeEnd": 35520000+60000,
"timeStart": 34920000 + 60000,
"timeEnd": 35520000 + 60000,
"colour": "",
"type": "event",
"id": "8ee5"
}
@@ -176,6 +191,7 @@ describe('getEventsWithDelay edge cases', () => {
"title": "Welcome to Ontime",
"timeStart": 28800000,
"timeEnd": 30600000,
"colour": "",
"type": "event",
"id": "5946"
},
@@ -187,6 +203,7 @@ describe('getEventsWithDelay edge cases', () => {
"title": "Unless recalled by the OSC address",
"timeStart": 34920000,
"timeEnd": 35520000,
"colour": "",
"type": "event",
"id": "8ee5"
}
@@ -196,6 +213,7 @@ describe('getEventsWithDelay edge cases', () => {
"title": "Welcome to Ontime",
"timeStart": 28800000,
"timeEnd": 30600000,
"colour": "",
"type": "event",
"id": "5946"
},
@@ -203,6 +221,7 @@ describe('getEventsWithDelay edge cases', () => {
"title": "Unless recalled by the OSC address",
"timeStart": 34920000,
"timeEnd": 35520000,
"colour": "",
"type": "event",
"id": "8ee5"
}
@@ -216,32 +235,32 @@ describe('test trimEventlist function', () => {
const limit = 8;
const testData = [
{id: '1'},
{id: '2'},
{id: '3'},
{id: '4'},
{id: '5'},
{id: '6'},
{id: '7'},
{id: '8'},
{id: '9'},
{id: '10'},
{id: '11'},
{id: '12'},
{ id: '1' },
{ id: '2' },
{ id: '3' },
{ id: '4' },
{ id: '5' },
{ id: '6' },
{ id: '7' },
{ id: '8' },
{ id: '9' },
{ id: '10' },
{ id: '11' },
{ id: '12' },
];
test('when we use the first item', () => {
const selectedId = '1';
const expected = [
{id: '1'},
{id: '2'},
{id: '3'},
{id: '4'},
{id: '5'},
{id: '6'},
{id: '7'},
{id: '8'},
{ id: '1' },
{ id: '2' },
{ id: '3' },
{ id: '4' },
{ id: '5' },
{ id: '6' },
{ id: '7' },
{ id: '8' },
];
const l = trimEventlist(testData, selectedId, limit);
@@ -252,14 +271,14 @@ describe('test trimEventlist function', () => {
test('when we use the third item', () => {
const selectedId = '3';
const expected = [
{id: '1'},
{id: '2'},
{id: '3'},
{id: '4'},
{id: '5'},
{id: '6'},
{id: '7'},
{id: '8'}
{ id: '1' },
{ id: '2' },
{ id: '3' },
{ id: '4' },
{ id: '5' },
{ id: '6' },
{ id: '7' },
{ id: '8' }
];
const l = trimEventlist(testData, selectedId, limit);
@@ -270,14 +289,14 @@ describe('test trimEventlist function', () => {
test('when we use the fourth item', () => {
const selectedId = '4';
const expected = [
{id: '2'},
{id: '3'},
{id: '4'},
{id: '5'},
{id: '6'},
{id: '7'},
{id: '8'},
{id: '9'}
{ id: '2' },
{ id: '3' },
{ id: '4' },
{ id: '5' },
{ id: '6' },
{ id: '7' },
{ id: '8' },
{ id: '9' }
];
const l = trimEventlist(testData, selectedId, limit);
@@ -288,14 +307,14 @@ describe('test trimEventlist function', () => {
test('if selected is not found', () => {
const selectedId = '15';
const expected = [
{id: '1'},
{id: '2'},
{id: '3'},
{id: '4'},
{id: '5'},
{id: '6'},
{id: '7'},
{id: '8'},
{ id: '1' },
{ id: '2' },
{ id: '3' },
{ id: '4' },
{ id: '5' },
{ id: '6' },
{ id: '7' },
{ id: '8' },
];
const l = trimEventlist(testData, selectedId, limit);
@@ -314,25 +333,27 @@ describe('test formatEvents function', () => {
"timeStart": 28800000,
"timeEnd": 30600000,
"isPublic": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "5946"
},
{
"title": "Unless recalled by the OSC address",
"subtitle": "",
"presenter": "",
"note": "In green, below",
"timeStart": 34800000,
"timeEnd": 35400000,
"isPublic": false,
"type": "event",
"revision": 0,
"id": "8ee5"
}
},
{
"title": "Unless recalled by the OSC address",
"subtitle": "",
"presenter": "",
"note": "In green, below",
"timeStart": 34800000,
"timeEnd": 35400000,
"isPublic": false,
"colour": "",
"type": "event",
"revision": 0,
"id": "8ee5"
}
];
test ('it parses correctly', () => {
test('it parses correctly', () => {
const selectedId = 'otherEvent';
const nextId = 'notHere';
const expected = [
@@ -342,6 +363,7 @@ describe('test formatEvents function', () => {
title: 'Welcome to Ontime',
isNow: false,
isNext: false,
colour: ""
},
{
id: '8ee5',
@@ -349,6 +371,7 @@ describe('test formatEvents function', () => {
title: 'Unless recalled by the OSC address',
isNow: false,
isNext: false,
colour: "",
},
]
@@ -357,7 +380,7 @@ describe('test formatEvents function', () => {
expect(parsed).toStrictEqual(expected);
});
test ('it handles selected correctly', () => {
test('it handles selected correctly', () => {
const selectedId = '5946';
const nextId = '8ee5';
const expected = [
@@ -367,6 +390,7 @@ describe('test formatEvents function', () => {
title: 'Welcome to Ontime',
isNow: true,
isNext: false,
colour: "",
},
{
id: '8ee5',
@@ -374,15 +398,16 @@ describe('test formatEvents function', () => {
title: 'Unless recalled by the OSC address',
isNow: false,
isNext: true,
colour: "",
},
]
const parsed = formatEventList(testEvent, selectedId, nextId,true);
const parsed = formatEventList(testEvent, selectedId, nextId, true);
expect(parsed).toStrictEqual(expected);
});
test ('it handles next correctly', () => {
test('it handles next correctly', () => {
const selectedId = '8ee5';
const nextId = 'notHere';
@@ -393,6 +418,7 @@ describe('test formatEvents function', () => {
title: 'Welcome to Ontime',
isNow: false,
isNext: false,
colour: "",
},
{
id: '8ee5',
@@ -400,6 +426,7 @@ describe('test formatEvents function', () => {
title: 'Unless recalled by the OSC address',
isNow: true,
isNext: false,
colour: "",
},
]
+18 -7
View File
@@ -86,28 +86,39 @@ const parse = (valueAsString) => {
/**
* @description Parses a time string to millis
* @param string - time string
* @param {string} value - time string
* @returns {number} - time string in millis
*/
export const forgivingStringToMillis = (string) => {
export const forgivingStringToMillis = (value) => {
let millis = 0;
// split string at known separators : , .
const separatorRegex = /[\s,:.]+/;
const [first, second, third] = string.split(separatorRegex);
const [first, second, third] = value.split(separatorRegex);
if (first != null && second != null && third != null) {
// if string has three sections, treat as [hours] [minutes] [seconds]
millis = parse(first) * mth;
millis += parse(second) * mtm;
millis += parse(third) * mts;
} else if (third == null) {
} else if (first != null && second != null && third == null) {
// if string has two sections, treat as [minutes] [seconds]
millis = parse(first) * mtm;
millis += parse(second) * mts;
} else if (second == null) {
// if string has one section, treat as [minutes]
millis = parse(first) * mtm;
} else if (first != null && second == null && third == null) {
// if string has one section,
// could be a complete string like 121010 - 12:10:10
if (first.length === 6) {
const hours = first.substring(0, 2);
const minutes = first.substring(2, 4);
const seconds = first.substring(4);
millis = parse(hours) * mth;
millis += parse(minutes) * mtm;
millis += parse(seconds) * mts;
} else {
// otherwise lets treat as [minutes]
millis = parse(first) * mtm;
}
}
return millis;
};
+6 -5
View File
@@ -6,7 +6,6 @@ import { stringFromMillis } from 'ontime-utils/time';
* @returns {Object[]} Filtered events with calculated delays
*/
export const getEventsWithDelay = (events) => {
if (events == null) return [];
const unfilteredEvents = [...events];
@@ -43,8 +42,11 @@ export const trimEventlist = (events, selectedId, limit) => {
if (limit != null) {
while (trimmedEvents.length > limit) {
const idx = trimmedEvents.findIndex((e) => e.id === selectedId);
if (idx <= BEFORE) { trimmedEvents.pop(); }
else { trimmedEvents.shift(); }
if (idx <= BEFORE) {
trimmedEvents.pop();
} else {
trimmedEvents.shift();
}
}
}
return trimmedEvents;
@@ -75,10 +77,9 @@ export const formatEventList = (events, selectedId, nextId, showEnd = false) =>
title: g.title,
isNow: g.id === selectedId,
isNext: g.id === nextId,
colour: g.colour,
});
}
return formattedEvents;
};
-13
View File
@@ -1,13 +0,0 @@
/**
* Handles link to external URLs: specifically for a electron / browser case
* If electron: ask main process to call a new browser window
* If browser: open in new tab
* @param url
*/
export default function handleLink(url) {
if (window.process?.type === 'renderer') {
window.ipcRenderer.send('send-to-link', url);
} else {
window.open(url);
}
}
+30
View File
@@ -0,0 +1,30 @@
/**
* Returns hostname
* @type {string}
*/
export const host = window?.location?.host;
/**
* Open an external URLs: specifically for a electron / browser case
* If electron: ask main process to call a new browser window
* If browser: open in new tab
* @param url
*/
export function openLink(url) {
if (window.process?.type === 'renderer') {
window.ipcRenderer.send('send-to-link', url);
} else {
window.open(url);
}
}
/**
* Handles opening external links
* @param event
* @param location
*/
export function handleLinks(event, location) {
// we handle the link manually
event.preventDefault();
openLink(`http://${host}/${location}`);
}
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
import { useEffect, useState } from 'react';
import { useSocket } from 'app/context/socketContext';
import VisibleIconBtn from 'common/components/buttons/VisibleIconBtn';
import OnAirIconBtn from '../../common/components/buttons/OnAirIconBtn';
@@ -10,14 +10,14 @@ const inputProps = {
};
const InputRow = (props) => {
const { label, placeholder, text, visible } = props;
const { label, placeholder, text, visible, actionHandler, changeHandler } = props;
return (
<>
<span className={style.label}>{label}</span>
<div className={style.inputItems}>
<Editable
onChange={(event) => props.changeHandler(event)}
onChange={(event) => changeHandler(event)}
value={text}
placeholder={placeholder}
className={style.inline}
@@ -28,7 +28,7 @@ const InputRow = (props) => {
</Editable>
<VisibleIconBtn
active={visible || undefined}
actionHandler={props.actionHandler}
actionHandler={actionHandler}
{...inputProps}
/>
</div>
@@ -1,4 +1,4 @@
import { memo } from 'react';
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import style from './PlaybackControl.module.scss';
import StartIconBtn from 'common/components/buttons/StartIconBtn';
@@ -69,20 +69,20 @@ const Transport = (props) => {
};
const PlaybackButtons = (props) => {
const { playback, selectedId, noEvents } = props;
const { playback, selectedId, noEvents, playbackControl } = props;
return (
<>
<Playback
playback={playback}
selectedId={selectedId}
noEvents={noEvents}
playbackControl={props.playbackControl}
playbackControl={playbackControl}
/>
<Transport
playback={playback}
selectedId={selectedId}
noEvents={noEvents}
playbackControl={props.playbackControl}
playbackControl={playbackControl}
/>
</>
);
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import style from './PlaybackControl.module.scss';
import { useEffect, useState } from 'react';
import { useSocket } from 'app/context/socketContext';
import PlaybackButtons from './PlaybackButtons';
import PlaybackTimer from './PlaybackTimer';
@@ -1,3 +1,4 @@
import React from 'react';
import style from './PlaybackControl.module.scss';
import Countdown from 'common/components/countdown/Countdown';
import { stringFromMillis } from 'ontime-utils/time';
@@ -56,7 +57,7 @@ const PlaybackTimer = (props) => {
{isWaiting ? (
<div className={style.roll}>
<span className={style.rolltag}>Roll: Countdown to start</span>
<span className={style.time}>{''}</span>
<span className={style.time}>FIX</span>
</div>
) : (
<>
@@ -72,7 +73,7 @@ const PlaybackTimer = (props) => {
)}
<div className={style.btn}>
<Tooltip
label={'Remove 1 minute'}
label='Remove 1 minute'
delay={500}
shouldWrapChildren={disableButtons}
>
@@ -85,7 +86,7 @@ const PlaybackTimer = (props) => {
</Button>
</Tooltip>
<Tooltip
label={'Add 1 minute'}
label='Add 1 minute'
delay={500}
shouldWrapChildren={disableButtons}
>
@@ -98,7 +99,7 @@ const PlaybackTimer = (props) => {
</Button>
</Tooltip>
<Tooltip
label={'Remove 5 minutes'}
label='Remove 5 minutes'
delay={500}
shouldWrapChildren={disableButtons}
>
@@ -111,7 +112,7 @@ const PlaybackTimer = (props) => {
</Button>
</Tooltip>
<Tooltip
label={'Add 5 minutes'}
label='Add 5 minutes'
delay={500}
shouldWrapChildren={disableButtons}
>
@@ -1,3 +1,4 @@
import React from 'react';
import { Draggable } from 'react-beautiful-dnd';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
@@ -1,3 +1,4 @@
import React from 'react';
import { Draggable } from 'react-beautiful-dnd';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import { millisToMinutes } from 'common/utils/dateConfig';
+17 -14
View File
@@ -1,13 +1,14 @@
import { lazy, useEffect } from 'react';
import React, { lazy, useEffect } from 'react';
import { Box } from '@chakra-ui/layout';
import { useDisclosure } from '@chakra-ui/hooks';
import styles from './Editor.module.scss';
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';
import { CollapseProvider } from '../../app/context/CollapseContext';
import styles from './Editor.module.scss';
const EventListWrapper = lazy(() => import('features/editors/list/EventListWrapper'));
const PlaybackControl = lazy(() => import('features/control/PlaybackControl'));
@@ -31,20 +32,22 @@ export default function Editor() {
<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}>
<CollapseProvider>
<Box id='settings' className={styles.settings}>
<ErrorBoundary>
<EventListWrapper />
<MenuBar onOpen={onOpen} isOpen={isOpen} />
</ErrorBoundary>
</div>
</Box>
</Box>
<Box className={styles.editor}>
<h1>Event List</h1>
<div className={styles.content}>
<ErrorBoundary>
<EventListWrapper />
</ErrorBoundary>
</div>
</Box>
</CollapseProvider>
</CursorProvider>
<Box className={styles.messages}>
@@ -1,7 +1,7 @@
import React, { useContext, useEffect, useState } from 'react';
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) {
@@ -1,7 +1,7 @@
import React, { useContext, useMemo } from 'react';
import Icon from '@chakra-ui/icon';
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
import { useMemo } from 'react';
import { Draggable } from 'react-beautiful-dnd';
import EventTimes from 'common/components/eventTimes/EventTimes';
import EventTimesVertical from 'common/components/eventTimes/EventTimesVertical';
@@ -10,22 +10,15 @@ import ActionButtons from '../list/ActionButtons';
import PublicIconBtn from 'common/components/buttons/PublicIconBtn';
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
import { millisToMinutes } from 'common/utils/dateConfig';
import style from './EventBlock.module.css';
import { HandleCollapse, SelectCollapse } from 'app/context/collapseAtom';
import { useAtom } from 'jotai';
import PropTypes from 'prop-types';
import { CollapseContext } from '../../../app/context/CollapseContext';
import style from './EventBlock.module.css';
const ExpandedBlock = (props) => {
const { provided, data, eventIndex, next, delay, delayValue, previousEnd, actionHandler } = props;
const oscid = data?.id || '...';
// if end is before, assume is the day after
const duration =
data.timeStart > data.timeEnd
? data.timeEnd + 86400000 - data.timeStart
: data.timeEnd - data.timeStart;
return (
<>
<span className={style.drag} {...provided.dragHandleProps}>
@@ -41,7 +34,7 @@ const ExpandedBlock = (props) => {
actionHandler={actionHandler}
timeStart={data.timeStart}
timeEnd={data.timeEnd}
duration={duration}
duration={data.duration}
delay={delay}
previousEnd={previousEnd}
className={style.time}
@@ -95,7 +88,7 @@ ExpandedBlock.propTypes = {
next: PropTypes.bool.isRequired,
delay: PropTypes.number,
delayValue: PropTypes.string,
previousEnd: PropTypes.number.isRequired,
previousEnd: PropTypes.number,
actionHandler: PropTypes.func.isRequired,
};
@@ -147,13 +140,13 @@ CollapsedBlock.propTypes = {
};
export default function EventBlock(props) {
const { data, selected, delay, index, eventIndex, previousEnd, actionHandler } = props;
const [collapsed] = useAtom(useMemo(() => SelectCollapse(data.id), [data.id]));
const [, setCollapsed] = useAtom(HandleCollapse);
const { data, selected, delay, index, eventIndex, previousEnd, actionHandler, next } = props;
const { isCollapsed, setCollapsed } = useContext(CollapseContext);
const collapsed = useMemo(() => isCollapsed(data.id), [data.id, isCollapsed]);
const isSelected = selected ? style.active : '';
const isCollapsed = collapsed ? style.collapsed : style.expanded;
const classSelect = `${style.event} ${isCollapsed} ${isSelected}`;
const selectedStyle = selected ? style.active : '';
const collapsedStyle = collapsed ? style.collapsed : style.expanded;
const classSelect = `${style.event} ${collapsedStyle} ${selectedStyle}`;
// Calculate delay in min
let delayValue = null;
@@ -161,7 +154,7 @@ export default function EventBlock(props) {
delayValue = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`;
}
const handleCollapse = (isCollapsed) => {
setCollapsed({ [data.id]: isCollapsed });
setCollapsed(data.id, isCollapsed);
};
return (
@@ -177,7 +170,7 @@ export default function EventBlock(props) {
<CollapsedBlock
provided={provided}
data={data}
next={props.next}
next={next}
delay={delay}
delayValue={delayValue}
previousEnd={previousEnd}
@@ -188,7 +181,7 @@ export default function EventBlock(props) {
provided={provided}
eventIndex={eventIndex}
data={data}
next={props.next}
next={next}
delay={delay}
delayValue={delayValue}
previousEnd={previousEnd}
@@ -207,6 +200,7 @@ EventBlock.propTypes = {
delay: PropTypes.number,
index: PropTypes.number.isRequired,
eventIndex: PropTypes.number.isRequired,
previousEnd: PropTypes.number.isRequired,
previousEnd: PropTypes.number,
actionHandler: PropTypes.func.isRequired,
next: PropTypes.bool,
};
@@ -1,3 +1,4 @@
import React from 'react';
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
import { IconButton } from '@chakra-ui/button';
import { FiClock } from '@react-icons/all-files/fi/FiClock';
@@ -23,8 +24,8 @@ export default function ActionButtons(props) {
icon={<FiPlus />}
_expanded={{ bg: 'orange.300', color: 'white' }}
_focus={{ boxShadow: 'none' }}
backgroundColor={'orange.200'}
color={'orange.500'}
backgroundColor='orange.200'
color='orange.500'
/>
</Tooltip>
<MenuList style={menuStyle}>
@@ -1,5 +1,5 @@
import React, { createRef, useCallback, useContext, useEffect, useState } from 'react';
import style from './List.module.scss';
import { createRef, useCallback, useContext, useEffect, useState } from 'react';
import { useSocket } from 'app/context/socketContext';
import Empty from 'common/state/Empty';
import EventListItem from './EventListItem';
@@ -1,7 +1,7 @@
import React, { memo, useCallback, useContext } from 'react';
import DelayBlock from '../DelayBlock/DelayBlock';
import BlockBlock from '../BlockBlock/BlockBlock';
import EventBlock from '../EventBlock/EventBlock';
import { memo, useCallback, useContext } from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
@@ -32,6 +32,16 @@ const EventListItem = (props) => {
const { emitError } = useContext(LoggingContext);
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
/**
* @description calculates duration from given options
* @param {number} start
* @param {number} end
* @returns {number}
*/
const calculateDuration = useCallback(
(start, end) => (start > end ? end + 86400000 - start : end - start),
[]
);
// Create / delete new events
const actionHandler = useCallback(
(action, payload) => {
@@ -59,16 +69,27 @@ const EventListItem = (props) => {
case 'update':
// Handles and filters update requests
const { field, value } = payload;
const newData = { id: data.id };
if (field === 'durationOverride') {
// duration defines timeEnd
let end = (data.timeStart += value);
const newData = { id: data.id, timeEnd: end };
newData.timeEnd = data.timeStart += value;
// request update in parent
eventsHandler('patch', newData);
} else if (field === 'timeStart') {
newData.duration = calculateDuration(value, data.timeEnd);
newData.timeStart = value;
// request update in parent
eventsHandler('patch', newData);
} else if (field === 'timeEnd') {
newData.duration = calculateDuration(data.timeStart, value);
newData.timeEnd = value;
// request update in parent
eventsHandler('patch', newData);
} else if (field in data) {
// create object with new field
const newData = { id: data.id, [field]: value };
newData[field] = value;
// request update in parent
eventsHandler('patch', newData);
@@ -1,8 +1,6 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import React, { useCallback, useContext, useEffect, useState } from 'react';
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,
@@ -18,9 +16,10 @@ import { useFetch } from 'app/hooks/useFetch.js';
import EventList from './EventList';
import EventListMenu from 'features/menu/EventListMenu.jsx';
import Empty from 'common/state/Empty';
import { CollapseContext } from '../../../app/context/CollapseContext';
export default function EventListWrapper() {
const [, setCollapsed] = useAtom(BatchOperation);
const { expandAll, collapseMultiple } = useContext(CollapseContext);
const queryClient = useQueryClient();
const { emitError } = useContext(LoggingContext);
const { data, status, isError, refetch } = useFetch(EVENTS_TABLE, fetchAllEvents);
@@ -112,7 +111,11 @@ export default function EventListWrapper() {
// Mutation finished, failed or successful
// Fetch anyway, just to be sure
onSettled: (newEvent) => {
queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
if (newEvent) {
queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
} else {
queryClient.invalidateQueries(EVENTS_TABLE);
}
},
});
@@ -231,6 +234,10 @@ export default function EventListWrapper() {
if (options?.startIsLastEnd !== undefined) {
newEvent.timeStart = data[options.startIsLastEnd].timeEnd || 0;
}
// hard coding duration value to be as expected for now
// this until timeOptions gets implemented
// Todo: implement duration options
newEvent.duration = Math.max(0, newEvent.timeEnd - newEvent.timeStart) || 0;
await addEvent.mutateAsync(newEvent);
} catch (error) {
emitError(`Error fetching data: ${error.message}`);
@@ -297,11 +304,12 @@ export default function EventListWrapper() {
case 'collapseall':
if (data == null) return;
setCollapsed({ clear: true, items: data, isCollapsed: true });
collapseMultiple(data);
break;
case 'expandall':
if (data == null) return;
setCollapsed({ clear: true, items: data, isCollapsed: false });
expandAll();
break;
case 'deleteall':
+7 -7
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import { useSocket } from 'app/context/socketContext';
import style from './Info.module.scss';
import InfoTitle from './InfoTitle';
@@ -45,10 +45,10 @@ export default function Info() {
if (data.total === 0 || data.total == null) {
setSelected('No events');
} else {
const formatedCurrent = `Event ${
const formattedCurrent = `Event ${
data.index != null ? data.index + 1 : '-'
}/${data.total != null ? data.total : '-'}`;
setSelected(formatedCurrent);
}/${data.total ? data.total : '-'}`;
setSelected(formattedCurrent);
}
});
@@ -78,12 +78,12 @@ export default function Info() {
return (
<>
<div className={style.main}>
<span>{`Running on port 4001`}</span>
<span>Running on port 4001</span>
<span>{selected}</span>
</div>
<InfoNif />
<InfoTitle title={'Now'} data={titlesNow} roll={playback === 'roll'} />
<InfoTitle title={'Next'} data={titlesNext} roll={playback === 'roll'} />
<InfoTitle title='Now' data={titlesNow} roll={playback === 'roll'} />
<InfoTitle title='Next' data={titlesNext} roll={playback === 'roll'} />
<InfoLogger />
</>
);
+2 -2
View File
@@ -1,4 +1,4 @@
import { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import style from './InfoLogger.module.scss';
import CollapseBar from "../../common/components/collapseBar/CollapseBar";
import { LoggingContext } from '../../app/context/LoggingContext';
@@ -54,7 +54,7 @@ export default function InfoLogger() {
return (
<div className={collapsed ? style.container : style.container__expanded}>
<CollapseBar title={'Log'} isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)}/>
<CollapseBar title='Log' isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)} />
{!collapsed && (
<>
<div className={style.toggleBar}>
+20 -21
View File
@@ -1,10 +1,10 @@
import { useState } from 'react';
import React, { useState } from 'react';
import { APP_TABLE } from 'app/api/apiConstants';
import { getInfo, ontimePlaceholderInfo } from 'app/api/ontimeApi';
import { useFetch } from 'app/hooks/useFetch';
import style from './Info.module.scss';
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
import handleLink from '../../common/utils/handleLink';
import { openLink } from '../../common/utils/linkUtils';
import style from './Info.module.scss';
export default function InfoNif() {
const { data, status } = useFetch(APP_TABLE, getInfo, {
@@ -14,26 +14,25 @@ export default function InfoNif() {
const baseURL = 'http://__IP__:4001';
return (
<div className={style.container}>
<CollapseBar title={'Network Info'} isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)}/>
<div className={style.container}>
<CollapseBar
title='Network Info'
isCollapsed={collapsed}
onClick={() => setCollapsed((c) => !c)}
/>
{!collapsed && (
<div>
{status === 'success' && (
<>
{data?.networkInterfaces.map((e) => {
return (
<a
key={e.address}
href='#!'
onClick={() =>
handleLink(baseURL.replace('__IP__', e.address))
}
className={style.if}
>{`${e.name} - ${e.address}`}</a>
);
})}
</>
)}
{status === 'success' &&
data?.networkInterfaces.map((e) => (
<a
key={e.address}
href='#!'
onClick={() => openLink(baseURL.replace('__IP__', e.address))}
className={style.if}
>
{`${e.name} - ${e.address}`}
</a>
))}
</div>
)}
</div>
+1 -1
View File
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Icon } from '@chakra-ui/react';
import { useState } from 'react';
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
import style from './Info.module.scss';
+1 -1
View File
@@ -1,4 +1,4 @@
import { memo, useContext } from 'react';
import React, { memo, useContext } from 'react';
import { Divider } from '@chakra-ui/react';
import { CursorContext } from '../../app/context/CursorContext';
import MenuActionButtons from './MenuActionButtons';
@@ -1,3 +1,4 @@
import React from 'react';
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
import { IconButton } from '@chakra-ui/button';
import { FiClock } from '@react-icons/all-files/fi/FiClock';
@@ -8,7 +9,7 @@ import { Divider } from '@chakra-ui/layout';
import { Tooltip } from '@chakra-ui/tooltip';
export default function MenuActionButtons(props) {
const { actionHandler } = props;
const { actionHandler, size } = props;
const menuStyle = {
color: '#000000',
backgroundColor: 'rgba(255,255,255,1)',
@@ -20,12 +21,12 @@ export default function MenuActionButtons(props) {
<MenuButton
as={IconButton}
aria-label='Create Menu'
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiPlus />}
_expanded={{ bg: 'orange.300', color: 'white' }}
_focus={{ boxShadow: 'none' }}
backgroundColor={'orange.200'}
color={'orange.500'}
backgroundColor='orange.200'
color='orange.500'
/>
</Tooltip>
<MenuList style={menuStyle}>
+2 -6
View File
@@ -1,3 +1,4 @@
import React, { useContext, useRef } from 'react';
import { useMutation, useQueryClient } from 'react-query';
import { downloadEvents, uploadEvents } from 'app/api/ontimeApi';
import { EVENTS_TABLE } from 'app/api/apiConstants';
@@ -9,7 +10,6 @@ import QuitIconBtn from './buttons/QuitIconBtn';
import style from './MenuBar.module.scss';
import HelpIconBtn from './buttons/HelpIconBtn';
import UploadIconBtn from './buttons/UploadIconBtn';
import { useContext, useRef } from 'react';
import { LoggingContext } from '../../app/context/LoggingContext';
import PropTypes from 'prop-types';
@@ -40,7 +40,6 @@ export default function MenuBar(props) {
const handleUpload = (event) => {
const fileUploaded = event.target.files[0];
console.log('1', fileUploaded)
if (fileUploaded == null) return;
// Limit file size to 1MB
@@ -49,9 +48,6 @@ export default function MenuBar(props) {
return;
}
console.log('2', ! fileUploaded.name.endsWith('.xlsx')
|| !fileUploaded.name.endsWith('.json'))
// Check file extension
if (fileUploaded.name.endsWith('.xlsx') || fileUploaded.name.endsWith('.json')) {
try {
@@ -145,7 +141,7 @@ export default function MenuBar(props) {
}
MenuBar.propTypes = {
isOpen: PropTypes.bool.isRequired,
isOpen: PropTypes.bool,
onOpen: PropTypes.func.isRequired,
};
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiDownload } from '@react-icons/all-files/fi/FiDownload';
export default function DownloadIconBtn(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, size, ...rest } = props;
return (
<Tooltip label='Export event list'>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiDownload />}
colorScheme='white'
onClick={clickhandler}
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiHelpCircle } from '@react-icons/all-files/fi/FiHelpCircle';
export default function HelpIconBtn(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, size, ...rest } = props;
return (
<Tooltip label='Help'>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiHelpCircle />}
colorScheme='white'
onClick={clickhandler}
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiMaximize } from '@react-icons/all-files/fi/FiMaximize';
export default function MaxIconBtn(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, size, ...rest } = props;
return (
<Tooltip label='Show full window'>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiMaximize />}
colorScheme='white'
onClick={clickhandler}
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiMinimize } from '@react-icons/all-files/fi/FiMinimize';
export default function MinIconBtn(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, size, ...rest } = props;
return (
<Tooltip label='Close to tray'>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiMinimize />}
colorScheme='white'
onClick={clickhandler}
@@ -1,3 +1,4 @@
import React, { useRef, useState } from 'react';
import { Button, IconButton } from '@chakra-ui/button';
import {
AlertDialog,
@@ -8,11 +9,10 @@ import {
AlertDialogOverlay,
} from '@chakra-ui/modal';
import { Tooltip } from '@chakra-ui/tooltip';
import { useRef, useState } from 'react';
import { FiPower } from '@react-icons/all-files/fi/FiPower';
export default function QuitIconBtn(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, size, ...rest } = props;
const [isOpen, setIsOpen] = useState(false);
const onClose = () => setIsOpen(false);
const cancelRef = useRef();
@@ -26,7 +26,7 @@ export default function QuitIconBtn(props) {
<>
<Tooltip label='Quit Application'>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiPower />}
colorScheme='red'
variant='outline'
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiSettings } from '@react-icons/all-files/fi/FiSettings';
export default function SettingsIconBtn(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, size, ...rest } = props;
return (
<Tooltip label='Settings'>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiSettings />}
colorScheme='white'
onClick={clickhandler}
@@ -1,13 +1,14 @@
import React from 'react';
import { IconButton } from '@chakra-ui/button';
import { Tooltip } from '@chakra-ui/tooltip';
import { FiUpload } from '@react-icons/all-files/fi/FiUpload';
export default function UploadIconBtn(props) {
const { clickhandler, ...rest } = props;
const { clickhandler, size, ...rest } = props;
return (
<Tooltip label='Import event list'>
<IconButton
size={props.size || 'xs'}
size={size || 'xs'}
icon={<FiUpload />}
colorScheme='white'
onClick={clickhandler}
+31 -35
View File
@@ -1,20 +1,20 @@
import React, { useContext, useEffect, useState } from 'react';
import { Button, IconButton } from '@chakra-ui/button';
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
import { ModalBody } from '@chakra-ui/modal';
import { Tooltip } from '@chakra-ui/tooltip';
import { Input } from '@chakra-ui/react';
import { getAliases, postAliases } from '../../app/api/ontimeApi';
import { useContext, useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch';
import { ALIASES } from 'app/api/apiConstants';
import style from './Modals.module.scss';
import { viewerLinks } from '../../app/appConstants';
import { viewerLocations } from '../../app/appConstants';
import { LoggingContext } from '../../app/context/LoggingContext';
import { validateAlias } from '../../app/utils/aliases';
import { Tooltip } from '@chakra-ui/tooltip';
import SubmitContainer from './SubmitContainer';
import handleLink from '../../common/utils/handleLink';
import { handleLinks, host, openLink } from '../../common/utils/linkUtils';
import style from './Modals.module.scss';
export default function AliasesModal() {
const { data, status, refetch } = useFetch(ALIASES, getAliases);
@@ -22,7 +22,6 @@ export default function AliasesModal() {
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [aliases, setAliases] = useState([]);
const host = window.location.host;
/**
* Set formdata from server state
@@ -160,23 +159,23 @@ export default function AliasesModal() {
<div className={style.modalFields}>
<div className={style.hSeparator}>Default URLs</div>
<div className={style.blockNotes}>
{viewerLinks.map((l) => (
{viewerLocations.map((l) => (
<a
href={l.link}
target='_blank'
rel='noreferrer'
className={style.flexNote}
key={l.link}
onClick={() => handleLink(`${host}/${l.link}`)}
onClick={(e) => handleLinks(e, l.link)}
>
{`${l.label} - ${l.link}`}
{`${l.label} - http://${host}/${l.link}`}
</a>
))}
</div>
<div className={style.hSeparator}>Custom Aliases</div>
<div className={style.blockNotes}>
<span className={style.inlineFlex}>
<IoInformationCircleOutline color='#2b6cb0' fontSize={'2em'} />
<IoInformationCircleOutline color='#2b6cb0' fontSize='2em' />
URL aliases are useful in two main scenarios
</span>
<span className={style.labelNote}>Complicated URLs</span>
@@ -184,16 +183,16 @@ export default function AliasesModal() {
eg. a lower third url with some custom parameters
<table>
<tbody>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Alias
</td>
<td className={style.labelNote}>Page URL</td>
</tr>
<tr>
<td>mylower</td>
<td>lower?bg=ff2&text=f00&size=0.6&transition=5</td>
</tr>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Alias
</td>
<td className={style.labelNote}>Page URL</td>
</tr>
<tr>
<td>mylower</td>
<td>lower?bg=ff2&text=f00&size=0.6&transition=5</td>
</tr>
</tbody>
</table>
<br />
@@ -202,16 +201,16 @@ export default function AliasesModal() {
eg. an unattended screen that you would need to change route from the app
<table>
<tbody>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Alias
</td>
<td className={style.labelNote}>Page URL</td>
</tr>
<tr>
<td>thirdfloor</td>
<td>public</td>
</tr>
<tr>
<td className={style.labelNote} style={{ width: '30%' }}>
Alias
</td>
<td className={style.labelNote}>Page URL</td>
</tr>
<tr>
<td>thirdfloor</td>
<td>public</td>
</tr>
</tbody>
</table>
</div>
@@ -234,7 +233,7 @@ export default function AliasesModal() {
/>
<Input
size='sm'
fontSize={'0.75em'}
fontSize='0.75em'
variant='flushed'
name='URL'
placeholder='URL (portion after ontime Port)'
@@ -248,10 +247,7 @@ export default function AliasesModal() {
href='#!'
target='_blank'
rel='noreferrer'
onClick={(e) => {
e.preventDefault();
handleLink(`http://${host}/${alias.pathAndParams}`);
}}
onClick={(e) => openLink(e, alias.pathAndParams)}
/>
</Tooltip>
<Tooltip label='Enable alias' openDelay={500}>
@@ -1,7 +1,7 @@
import React, { useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { Checkbox, FormControl, FormLabel, Input, PinInput, PinInputField } from '@chakra-ui/react';
import { getSettings, ontimePlaceholderSettings, postSettings } from 'app/api/ontimeApi';
import { useContext, useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch';
import { APP_SETTINGS } from 'app/api/apiConstants';
import style from './Modals.module.scss';
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { FormLabel, Input, Textarea } from '@chakra-ui/react';
import { fetchEvent, postEvent } from 'app/api/eventApi';
import { useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch';
import { EVENT_TABLE } from 'app/api/apiConstants';
import style from './Modals.module.scss';
@@ -1,12 +1,7 @@
import React, { useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { FormControl, FormLabel, Input, Switch } from '@chakra-ui/react';
import {
getInfo,
httpPlaceholder,
ontimeVars,
postInfo,
} from 'app/api/ontimeApi';
import { useContext, useEffect, useState } from 'react';
import { getInfo, httpPlaceholder, ontimeVars, postInfo } from 'app/api/ontimeApi';
import { useFetch } from 'app/hooks/useFetch';
import { APP_TABLE } from 'app/api/apiConstants';
import style from './Modals.module.scss';
@@ -85,12 +80,11 @@ export default function IntegrationSettingsModal() {
<div className={style.hSeparator}>Ontime event cycle</div>
<div className={style.blockNotes}>
<span className={style.inlineFlex}>
<FiInfo color='#2b6cb0' fontSize={'2em'} />
<FiInfo color='#2b6cb0' fontSize='2em' />
Add HTTP messages that ontime will send during the event cycle
</span>
<span className={style.labelNote}>
You can use variables in the HTTP request URL to send data from
ontime
You can use variables in the HTTP request URL to send data from ontime
</span>
<span className={style.emNote}>
http://127.0.0.1:8088/API/?setHeadline=
@@ -106,7 +100,7 @@ export default function IntegrationSettingsModal() {
<td className={style.labelNote}>Value</td>
</tr>
{ontimeVars.map((v) => (
<tr>
<tr key={v.name}>
<td className={style.labelNote}>{v.name}</td>
<td>{v.description}</td>
</tr>
@@ -115,7 +109,7 @@ export default function IntegrationSettingsModal() {
</table>
</div>
<div className={style.hSeparator}>Send HTTP</div>
<FormLabel style={{paddingLeft:'0.5em'}}>
<FormLabel style={{ paddingLeft: '0.5em' }}>
On Load
<span className={style.labelNote}>
<br />
@@ -154,7 +148,7 @@ export default function IntegrationSettingsModal() {
}}
/>
</div>
<FormLabel style={{paddingLeft:'0.5em'}}>
<FormLabel style={{ paddingLeft: '0.5em' }}>
On Start
<span className={style.labelNote}>
<br />
@@ -193,7 +187,7 @@ export default function IntegrationSettingsModal() {
}}
/>
</div>
<FormLabel style={{paddingLeft:'0.5em'}}>
<FormLabel style={{ paddingLeft: '0.5em' }}>
On Update
<span className={style.labelNote}>
<br />
@@ -232,7 +226,7 @@ export default function IntegrationSettingsModal() {
}}
/>
</FormControl>
<FormLabel style={{paddingLeft:'0.5em'}}>
<FormLabel style={{ paddingLeft: '0.5em' }}>
On Pause
<span className={style.labelNote}>
<br />
@@ -271,7 +265,7 @@ export default function IntegrationSettingsModal() {
}}
/>
</FormControl>
<FormLabel style={{paddingLeft:'0.5em'}}>
<FormLabel style={{ paddingLeft: '0.5em' }}>
On Stop
<span className={style.labelNote}>
<br />
@@ -310,7 +304,7 @@ export default function IntegrationSettingsModal() {
}}
/>
</FormControl>
<FormLabel style={{paddingLeft:'0.5em'}}>
<FormLabel style={{ paddingLeft: '0.5em' }}>
On Finish
<span className={style.labelNote}>
<br />
+8 -2
View File
@@ -1,3 +1,4 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Modal,
@@ -10,8 +11,9 @@ import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/tabs';
import EventSettingsModal from './EventSettingsModal';
import OscSettingsModal from './OscSettingsModal';
import AliasesModal from './AliasesModal';
import IntegrationSettingsModal from './IntegrationSettingsModal';
import AppSettingsModal from './AppSettingsModal';
import TableOptionsModal from './TableOptionsModal';
import IntegrationSettingsModal from './IntegrationSettingsModal';
export default function ModalManager(props) {
const { isOpen, onClose } = props;
@@ -20,7 +22,7 @@ export default function ModalManager(props) {
isOpen={isOpen}
onClose={onClose}
closeOnOverlayClick={false}
motionPreset={'slideInBottom'}
motionPreset='slideInBottom'
size='xl'
scrollBehavior='inside'
>
@@ -34,6 +36,7 @@ export default function ModalManager(props) {
<Tab style={{ fontSize: '0.9em' }}>App Settings</Tab>
<Tab style={{ fontSize: '0.9em' }}>Event Data</Tab>
<Tab style={{ fontSize: '0.9em' }}>URL Aliases</Tab>
<Tab style={{ fontSize: '0.9em' }}>Cuesheet</Tab>
<Tab style={{ fontSize: '0.9em' }}>OSC</Tab>
{/*<Tab style={{ fontSize: '0.9em' }}>Integration</Tab>*/}
</TabList>
@@ -47,6 +50,9 @@ export default function ModalManager(props) {
<TabPanel>
<AliasesModal />
</TabPanel>
<TabPanel>
<TableOptionsModal />
</TabPanel>
<TabPanel>
<OscSettingsModal />
</TabPanel>
@@ -1,7 +1,7 @@
import React, { useContext, useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { FormControl, FormLabel, Input } from '@chakra-ui/react';
import { getOSC, oscPlaceholderSettings, postOSC } from 'app/api/ontimeApi';
import { useContext, useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch';
import { OSC_SETTINGS } from 'app/api/apiConstants';
import style from './Modals.module.scss';
@@ -232,7 +232,7 @@ export default function OscSettingsModal() {
</div>
<div className={style.blockNotes}>
<span className={style.inlineFlex}>
<IoInformationCircleOutline color='#2b6cb0' fontSize={'2em'} />
<IoInformationCircleOutline color='#2b6cb0' fontSize='2em' />
OSC Feedback messages
</span>
<span>
@@ -1,3 +1,4 @@
import React from 'react';
import style from './Modals.module.scss';
import { Button } from '@chakra-ui/button';
import PropTypes from 'prop-types';
@@ -0,0 +1,130 @@
import React, { useEffect, useState } from 'react';
import { ModalBody } from '@chakra-ui/modal';
import { Input } from '@chakra-ui/react';
import { getUserFields, postUserFields, userFieldsPlaceholder } from '../../app/api/ontimeApi';
import { useFetch } from 'app/hooks/useFetch';
import { USERFIELDS } from 'app/api/apiConstants';
import SubmitContainer from './SubmitContainer';
import style from './Modals.module.scss';
import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline';
import { handleLinks, host } from '../../common/utils/linkUtils';
export default function TableOptionsModal() {
const { data, status, refetch } = useFetch(USERFIELDS, getUserFields);
const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [userFields, setUserFields] = useState(userFieldsPlaceholder);
/**
* Set formdata from server state
*/
useEffect(() => {
if (data == null) return;
if (changed) return;
// Todo: we need some validation on API replies
setUserFields(data);
}, [changed, data]);
/**
* Validate and submit data
*/
const submitHandler = async (event) => {
event.preventDefault();
setSubmitting(true);
// validation step makes clean string
const validatedFields = { ...userFields };
let errors = false;
for (const field in validatedFields) {
validatedFields[field] = validatedFields[field].trim();
}
if (!errors) {
await postUserFields(validatedFields);
await refetch();
setChanged(false);
}
setSubmitting(false);
};
/**
* Reverts local state equals to server state
*/
const revert = async () => {
setChanged(false);
await refetch();
};
/**
* Handles change of input field in local state
* @param {string} field - object parameter to update
* @param {string} value - new object parameter value
*/
const handleChange = (field, value) => {
if (value.length < 30) {
const temp = { ...userFields };
temp[field] = value;
setUserFields(temp);
setChanged(true);
}
};
return (
<ModalBody className={style.modalBody}>
<p className={style.notes}>
Options related to cuesheets
<br />
🔥 Changes take effect on save 🔥
</p>
<form onSubmit={submitHandler}>
<div className={style.modalFields}>
<div className={style.hSeparator}>User Fields</div>
<div className={style.blockNotes}>
<span className={style.inlineFlex}>
<IoInformationCircleOutline color='#2b6cb0' fontSize='2em' />
User Fields
</span>
<span>
Userfields facilitate adding custom fields to an event (eg: light, sound, camera).{' '}
<br />
These are available for excel imports and shown in the{' '}
<a
target='_blank'
rel='noreferrer'
href={`http://${host}cuesheet`}
onClick={(e) => handleLinks(e, 'cuesheet')}
>
cuesheet
</a>
</span>
</div>
<div className={style.inlineAliasPlaceholder} style={{ padding: '0.5em 0' }}>
<span className={style.labelNote}>User Field</span>
<span className={style.labelNote}>Display Name</span>
</div>
{Object.keys(userFields).map((field) => (
<div className={style.inlineAlias} key={field}>
<span>{field}</span>
<Input
size='sm'
variant='flushed'
name='Alias'
placeholder={field}
autoComplete='off'
value={userFields[field]}
onChange={(event) => handleChange(field, event.target.value)}
/>
</div>
))}
</div>
<SubmitContainer
revert={revert}
submitting={submitting}
changed={changed}
status={status}
/>
</form>
</ModalBody>
);
}

Some files were not shown because too many files have changed in this diff Show More