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