mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-03 06:28:01 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd1b9bd38c | |||
| b01145ca4b | |||
| 67ddcd8c68 | |||
| 8841a02754 | |||
| 1a0d7810c4 | |||
| f2dd24b74a | |||
| 6a6f68b2f9 | |||
| 906a6631a7 |
@@ -0,0 +1,18 @@
|
||||
version = 1
|
||||
|
||||
test_patterns = [
|
||||
"__mocks__/**",
|
||||
"__tests__/**",
|
||||
"cypress/**",
|
||||
"*.test.*",
|
||||
"*.mock.*",
|
||||
"*.spec.*"
|
||||
]
|
||||
|
||||
[[analyzers]]
|
||||
name = "javascript"
|
||||
enabled = true
|
||||
|
||||
[analyzers.meta]
|
||||
plugins = ["react"]
|
||||
environment = ["nodejs","browser","jest"]
|
||||
@@ -0,0 +1,70 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '33 02 * * 5'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
+13
@@ -1,6 +1,19 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="CssUnknownProperty" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="myCustomPropertiesEnabled" value="true" />
|
||||
<option name="myIgnoreVendorSpecificProperties" value="false" />
|
||||
<option name="myCustomPropertiesList">
|
||||
<value>
|
||||
<list size="3">
|
||||
<item index="0" class="java.lang.String" itemvalue="scrollbar-color" />
|
||||
<item index="1" class="java.lang.String" itemvalue="scrollbar-width" />
|
||||
<item index="2" class="java.lang.String" itemvalue="app-region" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredUrls">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
|
||||
Download the latest releases here
|
||||
- [Windows](https://gitreleases.dev/gh/cpvalente/ontime/latest/ontime-win64.exe)
|
||||
- [Windows](https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe)
|
||||
|
||||
# Ontime
|
||||
Ontime is an application for managing event rundowns and running stage timers.
|
||||
@@ -26,17 +26,18 @@ and extend with using the URL aliases feature
|
||||
```
|
||||
For the presentation views...
|
||||
-------------------------------------------------------------
|
||||
IP.ADDRESS:4001 > Web server default to presenter timer view
|
||||
IP.ADDRESS:4001/preseter > Presenter / Stage timer view
|
||||
IP.ADDRESS:4001/sm > Stage Manager / Backstage view
|
||||
IP.ADDRESS:4001/public > Public / Foyer view
|
||||
IP.ADDRESS:4001/pip > Picture in Picture view
|
||||
IP.ADDRESS:4001/lower > Lower Thirds
|
||||
IP.ADDRESS:4001/studio > Studio Clock
|
||||
IP.ADDRESS:4001 > Web server default to presenter timer view
|
||||
IP.ADDRESS:4001/presenter > Presenter / Stage timer view
|
||||
IP.ADDRESS:4001/sm > Stage Manager / Backstage view
|
||||
IP.ADDRESS:4001/public > Public / Foyer view
|
||||
IP.ADDRESS:4001/pip > Picture in Picture view
|
||||
IP.ADDRESS:4001/lower > Lower Thirds
|
||||
IP.ADDRESS:4001/studio > Studio Clock
|
||||
IP.ADDRESS:4001/cuesheet > Cue Sheet
|
||||
|
||||
...and for the editor (the control interface, same as the app)
|
||||
-------------------------------------------------------------
|
||||
IP.ADDRESS:4001/studio > Studio Clock
|
||||
IP.ADDRESS:4001/studio > Studio Clock
|
||||
|
||||
```
|
||||
|
||||
@@ -48,7 +49,9 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
|
||||
- Backstage Info
|
||||
- Public Info
|
||||
- Picture in Picture
|
||||
- Studio Clock
|
||||
- [x] Configurable realtime Lower Thirds
|
||||
- [x] Cuesheets with additional custom fields
|
||||
- [x] Send live messages to different screen types
|
||||
- [x] Ability to differentiate between backstage and public data
|
||||
- [x] Manage delays workflow
|
||||
@@ -62,7 +65,6 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
|
||||
## Unopinionated
|
||||
We are not interested in forcing workflows and have made ontime, so it is flexible to whichever way you would like to work.
|
||||
|
||||
|
||||
- [x] You do not need an order list to use the timer. Create an empty event and the OSC API works just the same
|
||||
- [x] If you want just the info screens, no need to use the timer!
|
||||
- [x] Don't have or care for a schedule?
|
||||
|
||||
+73
-4
@@ -1,13 +1,82 @@
|
||||
{
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
"react-app/jest",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"react",
|
||||
"testing-library",
|
||||
"jest"
|
||||
],
|
||||
"plugins": ["react", "testing-library", "jest"],
|
||||
"rules": {
|
||||
"jest/no-mocks-import": "warn",
|
||||
"no-useless-concat": "warn",
|
||||
"prefer-template": "warn"
|
||||
|
||||
"prefer-template": "warn",
|
||||
"react/jsx-no-bind": [
|
||||
"error",
|
||||
{
|
||||
"ignoreRefs": true,
|
||||
"allowArrowFunctions": true,
|
||||
"allowFunctions": false,
|
||||
"allowBind": false,
|
||||
"ignoreDOMComponents": true
|
||||
}
|
||||
],
|
||||
"react/jsx-boolean-value": [
|
||||
"error",
|
||||
"never",
|
||||
{
|
||||
"always": []
|
||||
}
|
||||
],
|
||||
"react/jsx-handler-names": [
|
||||
"off",
|
||||
{
|
||||
"eventHandlerPrefix": "handle",
|
||||
"eventHandlerPropPrefix": "on"
|
||||
}
|
||||
],
|
||||
"react/no-danger": "warn",
|
||||
"react/no-deprecated": [
|
||||
"error"
|
||||
],
|
||||
"react/jsx-no-undef": "error",
|
||||
"react/jsx-closing-tag-location": "warn",
|
||||
"react/jsx-no-useless-fragment": "warn",
|
||||
"react/no-unescaped-entities": "error",
|
||||
"react/jsx-tag-spacing": [
|
||||
"error",
|
||||
{
|
||||
"closingSlash": "never",
|
||||
"beforeSelfClosing": "always",
|
||||
"afterOpening": "never",
|
||||
"beforeClosing": "never"
|
||||
}
|
||||
],
|
||||
"react/jsx-space-before-closing": [
|
||||
"off",
|
||||
"always"
|
||||
],
|
||||
"react/jsx-curly-brace-presence": [
|
||||
"error",
|
||||
{
|
||||
"props": "never",
|
||||
"children": "never"
|
||||
}
|
||||
],
|
||||
"react/destructuring-assignment": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"react/no-unsafe": "off",
|
||||
"react/prop-types": [
|
||||
"error",
|
||||
{
|
||||
"ignore": [],
|
||||
"customValidators": [],
|
||||
"skipUndeclared": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+18
-13
@@ -3,29 +3,30 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^1.7.3",
|
||||
"@chakra-ui/react": "^1.8.6",
|
||||
"@dnd-kit/core": "^5.0.3",
|
||||
"@dnd-kit/sortable": "^6.0.1",
|
||||
"@dnd-kit/utilities": "^3.1.0",
|
||||
"@emotion/react": "^11.7.1",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"axios": "^0.24.0",
|
||||
"framer-motion": "^4.1.6",
|
||||
"jotai": "^0.16.5",
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"autosize": "^5.0.1",
|
||||
"axios": "^0.25.0",
|
||||
"color": "^4.2.1",
|
||||
"framer-motion": "4.1.17",
|
||||
"ontime-utils": "link: ../server/utils/",
|
||||
"react": "17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-fast-compare": "^3.2.0",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-qr-code": "2.0.3",
|
||||
"react-query": "^3.34.5",
|
||||
"react-query": "^3.34.12",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-scripts": "5.0.0",
|
||||
"socket.io-client": "4.4.0",
|
||||
"react-table": "^7.7.0",
|
||||
"socket.io-client": "^4.4.1",
|
||||
"typeface-open-sans": "^1.1.13",
|
||||
"use-fit-text": "^2.4.0",
|
||||
"web-vitals": "^1.0.1"
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "set BROWSER=none&&react-scripts start",
|
||||
@@ -47,9 +48,13 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-scripts": "^5.0.0",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"sass": "^1.44.0"
|
||||
"sass": "^1.49.0"
|
||||
}
|
||||
}
|
||||
|
||||
+28
-23
@@ -1,4 +1,4 @@
|
||||
import { lazy, Suspense, useCallback, useEffect } from 'react';
|
||||
import React, { lazy, Suspense, useCallback, useEffect } from 'react';
|
||||
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
import './App.scss';
|
||||
import withSocket from 'features/viewers/ViewWrapper';
|
||||
@@ -7,23 +7,17 @@ import ProtectRoute from './common/components/protectRoute/ProtectRoute';
|
||||
import { useFetch } from './app/hooks/useFetch';
|
||||
import { ALIASES } from './app/api/apiConstants';
|
||||
import { getAliases } from './app/api/ontimeApi';
|
||||
import { TableSettingsProvider } from './app/context/TableSettingsContext';
|
||||
|
||||
const Editor = lazy(() => import('features/editors/Editor'));
|
||||
const Table = lazy(() => import('features/table/TableWrapper'));
|
||||
|
||||
const TimerView = lazy(() =>
|
||||
import('features/viewers/timer/Timer')
|
||||
);
|
||||
const MinimalTimerView = lazy(() =>
|
||||
import('features/viewers/timer/MinimalTimer')
|
||||
);
|
||||
const TimerView = lazy(() => import('features/viewers/timer/Timer'));
|
||||
const MinimalTimerView = lazy(() => import('features/viewers/timer/MinimalTimer'));
|
||||
|
||||
const StageManager = lazy(() =>
|
||||
import('features/viewers/backstage/StageManager')
|
||||
);
|
||||
const StageManager = lazy(() => import('features/viewers/backstage/StageManager'));
|
||||
const Public = lazy(() => import('features/viewers/foh/Public'));
|
||||
const Lower = lazy(() =>
|
||||
import('features/viewers/production/lower/LowerWrapper')
|
||||
);
|
||||
const Lower = lazy(() => import('features/viewers/production/lower/LowerWrapper'));
|
||||
const Pip = lazy(() => import('features/viewers/production/Pip'));
|
||||
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
|
||||
|
||||
@@ -35,6 +29,20 @@ const SLowerThird = withSocket(Lower);
|
||||
const SPip = withSocket(Pip);
|
||||
const SStudio = withSocket(StudioClock);
|
||||
|
||||
const ProtectedEditor = () => (
|
||||
<ProtectRoute>
|
||||
<Editor />
|
||||
</ProtectRoute>
|
||||
);
|
||||
|
||||
const ProtectedTable = () => (
|
||||
<ProtectRoute>
|
||||
<TableSettingsProvider>
|
||||
<Table />
|
||||
</TableSettingsProvider>
|
||||
</ProtectRoute>
|
||||
);
|
||||
|
||||
function App() {
|
||||
const { data } = useFetch(ALIASES, getAliases);
|
||||
const location = useLocation();
|
||||
@@ -48,8 +56,7 @@ function App() {
|
||||
if (e.altKey) {
|
||||
if (e.key === 't' || e.key === 'T') {
|
||||
// if we are in electron
|
||||
if (window.process?.type === undefined) return;
|
||||
if (window.process.type === 'renderer') {
|
||||
if (window.process?.type === 'renderer') {
|
||||
// ask to see debug
|
||||
window.ipcRenderer.send('set-window', 'show-dev');
|
||||
}
|
||||
@@ -101,15 +108,13 @@ function App() {
|
||||
<Route path='/studio' element={<SStudio />} />
|
||||
{/*/!* Lower cannot have fallback *!/*/}
|
||||
<Route path='/lower' element={<SLowerThird />} />
|
||||
|
||||
{/*/!* Protected Routes *!/*/}
|
||||
<Route
|
||||
path='/editor'
|
||||
element={
|
||||
<ProtectRoute>
|
||||
<Editor />
|
||||
</ProtectRoute>
|
||||
}
|
||||
/>
|
||||
<Route path='/editor' element={<ProtectedEditor />} />
|
||||
<Route path='/cuesheet' element={<ProtectedTable />} />
|
||||
<Route path='/cuelist' element={<ProtectedTable />} />
|
||||
<Route path='/table' element={<ProtectedTable />} />
|
||||
|
||||
{/* Send to default if nothing found */}
|
||||
<Route path='*' element={<STimer />} />
|
||||
</Routes>
|
||||
|
||||
+3
-2
@@ -8,7 +8,7 @@
|
||||
body,
|
||||
html,
|
||||
.App {
|
||||
margin: 0px auto;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
overflow: clip;
|
||||
height: 100vh;
|
||||
@@ -23,7 +23,8 @@ option {
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5em;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const NODE_PORT = 4001;
|
||||
export const EVENT_TABLE = 'event';
|
||||
export const ALIASES = 'aliases';
|
||||
export const USERFIELDS = 'userFields';
|
||||
export const EVENTS_TABLE = 'events';
|
||||
export const APP_TABLE = 'appinfo';
|
||||
export const OSC_SETTINGS = 'oscSettings';
|
||||
@@ -13,5 +14,5 @@ const calculateServer = () => {
|
||||
export const serverURL = calculateServer();
|
||||
export const eventURL = serverURL + EVENT_TABLE;
|
||||
export const eventsURL = serverURL + EVENTS_TABLE;
|
||||
export const playbackURL = serverURL + 'playback';
|
||||
export const ontimeURL = serverURL + 'ontime';
|
||||
export const playbackURL = `${serverURL}playback`;
|
||||
export const ontimeURL = `${serverURL}ontime`;
|
||||
|
||||
@@ -7,31 +7,30 @@ export const fetchAllEvents = async () => {
|
||||
};
|
||||
|
||||
export const requestPost = async (data) => {
|
||||
return await axios.post(eventsURL, data);
|
||||
await axios.post(eventsURL, data);
|
||||
};
|
||||
|
||||
export const requestPut = async (data) => {
|
||||
return await axios.put(eventsURL, data);
|
||||
await axios.put(eventsURL, data);
|
||||
};
|
||||
|
||||
export const requestPatch = async (data) => {
|
||||
return await axios.patch(eventsURL, data);
|
||||
await axios.patch(eventsURL, data);
|
||||
};
|
||||
|
||||
export const requestReorder = async (data) => {
|
||||
const action = 'reorder';
|
||||
return await axios.patch(eventsURL + '/' + action, data);
|
||||
await axios.patch(`${eventsURL}/reorder`, data);
|
||||
};
|
||||
|
||||
export const requestApplyDelay = async (eventId) => {
|
||||
const action = 'applydelay';
|
||||
return await axios.patch(eventsURL + '/' + action + '/' + eventId);
|
||||
return await axios.patch(`${eventsURL}/${action}/${eventId}`);
|
||||
};
|
||||
|
||||
export const requestDelete = async (eventId) => {
|
||||
return await axios.delete(eventsURL + '/' + eventId);
|
||||
await axios.delete(`${eventsURL}/${eventId}`);
|
||||
};
|
||||
|
||||
export const requestDeleteAll = async () => {
|
||||
return await axios.delete(eventsURL + '/all');
|
||||
await axios.delete(`${eventsURL}/all`);
|
||||
};
|
||||
|
||||
@@ -21,6 +21,19 @@ export const eventPlaceholderSettings = {
|
||||
endMessage: '',
|
||||
};
|
||||
|
||||
export const userFieldsPlaceholder = {
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
};
|
||||
|
||||
export const oscPlaceholderSettings = {
|
||||
port: '',
|
||||
portOut: '',
|
||||
@@ -92,7 +105,7 @@ export const getSettings = async () => {
|
||||
};
|
||||
|
||||
export const postSettings = async (data) => {
|
||||
return await axios.post(`${ontimeURL}/settings`, data);
|
||||
await axios.post(`${ontimeURL}/settings`, data);
|
||||
};
|
||||
|
||||
export const getInfo = async () => {
|
||||
@@ -101,7 +114,7 @@ export const getInfo = async () => {
|
||||
};
|
||||
|
||||
export const postInfo = async (data) => {
|
||||
return await axios.post(`${ontimeURL}/info`, data);
|
||||
await axios.post(`${ontimeURL}/info`, data);
|
||||
};
|
||||
|
||||
export const getAliases = async () => {
|
||||
@@ -110,7 +123,17 @@ export const getAliases = async () => {
|
||||
};
|
||||
|
||||
export const postAliases = async (data) => {
|
||||
return await axios.post(`${ontimeURL}/aliases`, data);
|
||||
await axios.post(`${ontimeURL}/aliases`, data);
|
||||
};
|
||||
|
||||
|
||||
export const getUserFields = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/userfields`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postUserFields = async (data) => {
|
||||
await axios.post(`${ontimeURL}/userfields`, data);
|
||||
};
|
||||
|
||||
export const getOSC = async () => {
|
||||
@@ -119,7 +142,7 @@ export const getOSC = async () => {
|
||||
};
|
||||
|
||||
export const postOSC = async (data) => {
|
||||
return await axios.post(`${ontimeURL}/osc`, data);
|
||||
await axios.post(`${ontimeURL}/osc`, data);
|
||||
};
|
||||
|
||||
export const downloadEvents = async () => {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
// Exported viewer links
|
||||
const speakerLink = 'http://localhost:4001/speaker';
|
||||
const smLink = 'http://localhost:4001/sm';
|
||||
const publicLink = 'http://localhost:4001/public';
|
||||
const pipLink = 'http://localhost:4001/pip';
|
||||
const studioLink = 'http://localhost:4001/studio';
|
||||
// Exported viewer link location
|
||||
const speakerLocation = 'speaker';
|
||||
const smLocation = 'sm';
|
||||
const publicLocation = 'public';
|
||||
const pipLocation = 'pip';
|
||||
const studioLocation = 'studio';
|
||||
const cuesheetLocation = 'cuesheet';
|
||||
|
||||
export const viewerLinks = [
|
||||
{ link: speakerLink, label: 'Speaker Screen' },
|
||||
{ link: smLink, label: 'Backstage Screen' },
|
||||
{ link: publicLink, label: 'Public Screen' },
|
||||
{ link: pipLink, label: 'Picture in Picture' },
|
||||
{ link: studioLink, label: 'Studio Clock' }
|
||||
export const viewerLocations = [
|
||||
{ link: speakerLocation, label: 'Speaker Screen' },
|
||||
{ link: smLocation, label: 'Backstage Screen' },
|
||||
{ link: publicLocation, label: 'Public Screen' },
|
||||
{ link: pipLocation, label: 'Picture in Picture' },
|
||||
{ link: studioLocation, label: 'Studio Clock' },
|
||||
{ link: cuesheetLocation, label: 'Cuesheet' }
|
||||
];
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import React, { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import { useFetch } from '../hooks/useFetch';
|
||||
import { APP_SETTINGS } from '../api/apiConstants';
|
||||
import { getSettings } from '../api/ontimeApi';
|
||||
@@ -6,11 +6,11 @@ import { getSettings } from '../api/ontimeApi';
|
||||
export const AppContext = createContext({
|
||||
auth: false,
|
||||
data: {
|
||||
pinCode: null
|
||||
}
|
||||
pinCode: null,
|
||||
},
|
||||
});
|
||||
|
||||
export const AppContextProvider = (props) => {
|
||||
export const AppContextProvider = ({ children }) => {
|
||||
const [auth, setAuth] = useState(true);
|
||||
const { data } = useFetch(APP_SETTINGS, getSettings);
|
||||
|
||||
@@ -21,17 +21,16 @@ export const AppContextProvider = (props) => {
|
||||
} else {
|
||||
setAuth(false);
|
||||
}
|
||||
},[data])
|
||||
|
||||
const validate = useCallback((pin) => {
|
||||
const correct = pin === data.pinCode;
|
||||
setAuth(correct);
|
||||
return correct;
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{ auth, validate }}>
|
||||
{props.children}
|
||||
</AppContext.Provider>
|
||||
const validate = useCallback(
|
||||
(pin) => {
|
||||
const correct = pin === data.pinCode;
|
||||
setAuth(correct);
|
||||
return correct;
|
||||
},
|
||||
[data]
|
||||
);
|
||||
};
|
||||
|
||||
return <AppContext.Provider value={{ auth, validate }}>{children}</AppContext.Provider>;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { createContext, useCallback, useMemo, useState } from 'react';
|
||||
import { useLocalStorage } from '../hooks/useLocalStorage';
|
||||
|
||||
export const CursorContext = createContext({
|
||||
cursor: 0,
|
||||
isCursorLocked: false,
|
||||
|
||||
setCursor: () => undefined,
|
||||
moveCursorUp: () => undefined,
|
||||
moveCursorDown: () => undefined,
|
||||
});
|
||||
|
||||
export const CursorProvider = ({ children }) => {
|
||||
const [cursor, setCursor] = useState(0);
|
||||
const [_cursorLocked, _setCursorLocked] = useLocalStorage('isCursorLocked', 'locked');
|
||||
const isCursorLocked = useMemo(() => _cursorLocked === 'locked', [_cursorLocked]);
|
||||
|
||||
const cursorLockedOff = useCallback(() => _setCursorLocked('unlocked'), [_setCursorLocked]);
|
||||
const cursorLockedOn = useCallback(() => _setCursorLocked('locked'), [_setCursorLocked]);
|
||||
|
||||
const moveCursorUp = useCallback(() => {
|
||||
setCursor((prev) => Math.max(prev - 1, 0));
|
||||
}, []);
|
||||
|
||||
const moveCursorDown = useCallback(() => {
|
||||
setCursor((prev) => prev + 1);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* @param {boolean | undefined} newValue
|
||||
*/
|
||||
const toggleCursorLocked = useCallback(
|
||||
(newValue = undefined) => {
|
||||
if (newValue === undefined) {
|
||||
if (isCursorLocked) {
|
||||
cursorLockedOff();
|
||||
} else {
|
||||
cursorLockedOn();
|
||||
}
|
||||
} else if (!newValue) {
|
||||
cursorLockedOff();
|
||||
} else if (newValue) {
|
||||
cursorLockedOn();
|
||||
}
|
||||
},
|
||||
[cursorLockedOff, cursorLockedOn, isCursorLocked]
|
||||
);
|
||||
|
||||
return (
|
||||
<CursorContext.Provider
|
||||
value={{
|
||||
cursor,
|
||||
isCursorLocked,
|
||||
toggleCursorLocked,
|
||||
setCursor,
|
||||
moveCursorUp,
|
||||
moveCursorDown,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CursorContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { createContext, useState } from 'react';
|
||||
|
||||
export const LocalEventSettingsContext = createContext({
|
||||
showQuickEntry: false,
|
||||
starTimeIsLastEnd: true,
|
||||
defaultPublic: true,
|
||||
|
||||
setShowQuickEntry: () => undefined,
|
||||
setStarTimeIsLastEnd: () => undefined,
|
||||
setDefaultPublic: () => undefined,
|
||||
});
|
||||
|
||||
export const LocalEventSettingsProvider = ({ children }) => {
|
||||
const [showQuickEntry, setShowQuickEntry] = useState(false);
|
||||
const [starTimeIsLastEnd, setStarTimeIsLastEnd] = useState(true);
|
||||
const [defaultPublic, setDefaultPublic] = useState(false);
|
||||
|
||||
return (
|
||||
<LocalEventSettingsContext.Provider
|
||||
value={{
|
||||
showQuickEntry,
|
||||
setShowQuickEntry,
|
||||
starTimeIsLastEnd,
|
||||
setStarTimeIsLastEnd,
|
||||
defaultPublic,
|
||||
setDefaultPublic,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LocalEventSettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import { useSocket } from './socketContext';
|
||||
import { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import { generateId } from 'ontime-utils/generate_id';
|
||||
import { nowInMillis, stringFromMillis } from 'ontime-utils/time';
|
||||
|
||||
@@ -8,10 +8,10 @@ export const LoggingContext = createContext({
|
||||
emitInfo: () => undefined,
|
||||
emitWarning: () => undefined,
|
||||
emitError: () => undefined,
|
||||
clearLog: () => undefined
|
||||
clearLog: () => undefined,
|
||||
});
|
||||
|
||||
export const LoggingProvider = (props) => {
|
||||
export const LoggingProvider = ({ children }) => {
|
||||
const MAX_MESSAGES = 100;
|
||||
const socket = useSocket();
|
||||
const [logData, setLogData] = useState([]);
|
||||
@@ -40,57 +40,69 @@ export const LoggingProvider = (props) => {
|
||||
* @param level
|
||||
* @private
|
||||
*/
|
||||
const _send = useCallback((text, level) => {
|
||||
if (socket != null) {
|
||||
const m = {
|
||||
id: generateId(),
|
||||
origin,
|
||||
time: stringFromMillis(nowInMillis()),
|
||||
level,
|
||||
text
|
||||
const _send = useCallback(
|
||||
(text, level) => {
|
||||
if (socket != null) {
|
||||
const m = {
|
||||
id: generateId(),
|
||||
origin,
|
||||
time: stringFromMillis(nowInMillis()),
|
||||
level,
|
||||
text,
|
||||
};
|
||||
setLogData((l) => [m, ...l]);
|
||||
socket.emit('logger', m);
|
||||
}
|
||||
setLogData((l) => [m, ...l]);
|
||||
socket.emit('logger', m);
|
||||
}
|
||||
if (logData.length > MAX_MESSAGES) {
|
||||
setLogData((l) => l.pop());
|
||||
}
|
||||
},[logData, socket]);
|
||||
if (logData.length > MAX_MESSAGES) {
|
||||
setLogData((l) => l.pop());
|
||||
}
|
||||
},
|
||||
[logData, socket]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends a message with level INFO
|
||||
* @param text
|
||||
*/
|
||||
const emitInfo = useCallback((text) => {
|
||||
_send(text, 'INFO');
|
||||
}, [_send]);
|
||||
const emitInfo = useCallback(
|
||||
(text) => {
|
||||
_send(text, 'INFO');
|
||||
},
|
||||
[_send]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends a message with level WARN
|
||||
* @param text
|
||||
*/
|
||||
const emitWarning = useCallback((text) => {
|
||||
_send(text, 'WARN');
|
||||
}, [_send]);
|
||||
const emitWarning = useCallback(
|
||||
(text) => {
|
||||
_send(text, 'WARN');
|
||||
},
|
||||
[_send]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends a message with level ERROR
|
||||
* @param text
|
||||
*/
|
||||
const emitError = useCallback((text) => {
|
||||
_send(text, 'ERROR');
|
||||
}, [_send]);
|
||||
const emitError = useCallback(
|
||||
(text) => {
|
||||
_send(text, 'ERROR');
|
||||
},
|
||||
[_send]
|
||||
);
|
||||
|
||||
/**
|
||||
* Clears running log
|
||||
*/
|
||||
const clearLog = useCallback(() => {
|
||||
setLogData([])
|
||||
setLogData([]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LoggingContext.Provider value = {{ emitInfo, logData, emitWarning, emitError, clearLog }}>
|
||||
{props.children}
|
||||
<LoggingContext.Provider value={{ emitInfo, logData, emitWarning, emitError, clearLog }}>
|
||||
{children}
|
||||
</LoggingContext.Provider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { createContext, useCallback, useState } from 'react';
|
||||
import { useLocalStorage } from '../hooks/useLocalStorage';
|
||||
|
||||
export const TableSettingsContext = createContext({
|
||||
theme: '',
|
||||
showSettings: false,
|
||||
followSelected: false,
|
||||
|
||||
toggleSettings: () => undefined,
|
||||
toggleTheme: () => undefined,
|
||||
toggleFollow: () => undefined,
|
||||
});
|
||||
|
||||
export const TableSettingsProvider = ({ children }) => {
|
||||
const [theme, setTheme] = useLocalStorage('table-color-theme', 'dark');
|
||||
const [followSelected, setFollowSelected] = useLocalStorage('table-follow-selected', false);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
/**
|
||||
* @description Toggles the current value of dark mode
|
||||
* @param {string} val - 'light' or 'dark'
|
||||
*/
|
||||
const toggleTheme = useCallback(
|
||||
(val) => {
|
||||
if (val === undefined) {
|
||||
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
|
||||
} else {
|
||||
setTheme(val);
|
||||
}
|
||||
},
|
||||
[setTheme]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Toggles visibility state for settings
|
||||
* @param {boolean} val - whether the settings window is visible
|
||||
*/
|
||||
const toggleSettings = useCallback(
|
||||
(val) => {
|
||||
if (val === undefined) {
|
||||
setShowSettings((prev) => !prev);
|
||||
} else {
|
||||
setShowSettings(val);
|
||||
}
|
||||
},
|
||||
[setShowSettings]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Toggles follow option
|
||||
* @param {boolean} val - whether the window follows selected event
|
||||
*/
|
||||
const toggleFollow = useCallback(
|
||||
(val) => {
|
||||
if (val === undefined) {
|
||||
setFollowSelected((prev) => !prev);
|
||||
} else {
|
||||
setFollowSelected(val);
|
||||
}
|
||||
},
|
||||
[setFollowSelected]
|
||||
);
|
||||
|
||||
return (
|
||||
<TableSettingsContext.Provider
|
||||
value={{
|
||||
theme,
|
||||
showSettings,
|
||||
followSelected,
|
||||
toggleSettings,
|
||||
toggleTheme,
|
||||
toggleFollow,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TableSettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -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));
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import io from 'socket.io-client';
|
||||
import { serverURL } from 'app/api/apiConstants';
|
||||
|
||||
@@ -8,7 +8,7 @@ export const useSocket = () => {
|
||||
return useContext(SocketContext);
|
||||
};
|
||||
|
||||
function SocketProvider(props) {
|
||||
function SocketProvider({ children }) {
|
||||
const [socket, setSocket] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -17,10 +17,7 @@ function SocketProvider(props) {
|
||||
return () => s.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SocketContext.Provider value={socket}>
|
||||
{props.children}
|
||||
</SocketContext.Provider>
|
||||
);
|
||||
return <SocketContext.Provider value={socket}>{children}</SocketContext.Provider>;
|
||||
}
|
||||
|
||||
export default SocketProvider;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
// Roughly from useHooks - useLocalStorage
|
||||
|
||||
export const useLocalStorage = (key, initialValue) => {
|
||||
const [storedValue, setStoredValue] = useState(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(`ontime-${key}`);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch (error) {
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
const setValue = (value) => {
|
||||
try {
|
||||
// Allow value to be a function so we have same API as useState
|
||||
const valueToStore =
|
||||
value instanceof Function ? value(storedValue) : value;
|
||||
|
||||
setStoredValue(valueToStore);
|
||||
window.localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
return [storedValue, setValue];
|
||||
}
|
||||
@@ -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,38 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
//
|
||||
// useTimeout React Hook
|
||||
//
|
||||
// React hook for delaying calls with time
|
||||
export const useTimeout = (
|
||||
callback, // function to call. No args passed.
|
||||
timeout = 0, // delay, ms (default: immediately put into JS Event Queue)
|
||||
{
|
||||
// manage re-render behavior.
|
||||
// by default, a re-render in your component will re-define the callback,
|
||||
// which will cause this timeout to cancel itself.
|
||||
// to avoid cancelling on re-renders (but still cancel on unmounts),
|
||||
// set `persistRenders: true,`.
|
||||
persistRenders = false,
|
||||
} = {},
|
||||
// These dependencies are injected for testing purposes.
|
||||
// (pure functions - where all dependencies are arguments - is often easier to test)
|
||||
_setTimeout = setTimeout,
|
||||
_clearTimeout = clearTimeout,
|
||||
_useEffect = useEffect
|
||||
) => {
|
||||
let timeoutId;
|
||||
const cancel = () => timeoutId && _clearTimeout(timeoutId);
|
||||
|
||||
_useEffect(
|
||||
() => {
|
||||
timeoutId = _setTimeout(callback, timeout);
|
||||
return cancel;
|
||||
},
|
||||
persistRenders
|
||||
? [_setTimeout, _clearTimeout]
|
||||
: [callback, timeout, _setTimeout, _clearTimeout]
|
||||
);
|
||||
|
||||
return cancel;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiPlus } from 'react-icons/fi';
|
||||
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
|
||||
|
||||
export default function AddIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiChevronsDown } from 'react-icons/fi';
|
||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||
|
||||
export default function ApplyIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickhandler, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Apply delays'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<FiChevronsDown />}
|
||||
size={size || 'xs'}
|
||||
icon={<FiCheck />}
|
||||
colorScheme='orange'
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiMinusCircle } from 'react-icons/fi';
|
||||
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
|
||||
|
||||
export default function BlockIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiChevronsUp } from 'react-icons/fi';
|
||||
import { FiChevronsUp } from '@react-icons/all-files/fi/FiChevronsUp';
|
||||
|
||||
export default function CollapseBtn(props) {
|
||||
const { clickhandler } = props;
|
||||
const { clickhandler, size } = props;
|
||||
return (
|
||||
<Tooltip label='Collapse all'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={<FiChevronsUp />}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import { FiTarget } from 'react-icons/fi';
|
||||
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
|
||||
|
||||
export default function CurrentBtn(props) {
|
||||
const { clickhandler, active } = props;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IoCaretDown } from '@react-icons/all-files/io5/IoCaretDown';
|
||||
|
||||
export default function CursorDownBtn(props) {
|
||||
const { clickhandler, active, ref, size } = props;
|
||||
return (
|
||||
<Tooltip label='Move cursor down Alt + ↓'>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size={size || 'xs'}
|
||||
icon={<IoCaretDown />}
|
||||
color={active ? 'pink.100' : 'pink.300'}
|
||||
borderColor={active ? undefined : 'pink.300'}
|
||||
backgroundColor={active ? 'pink.400' : undefined}
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
+6
-5
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiTarget } from 'react-icons/fi';
|
||||
import { FiTarget } from '@react-icons/all-files/fi/FiTarget';
|
||||
|
||||
export default function LockIconBtn(props) {
|
||||
const { clickhandler, active, ref } = props;
|
||||
export default function CursorLockedBtn(props) {
|
||||
const { clickhandler, active, ref, size } = props;
|
||||
return (
|
||||
<Tooltip label='Lock cursor to current'>
|
||||
<IconButton
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={<FiTarget />}
|
||||
color={active ? 'pink.100' : 'pink.300'}
|
||||
borderColor={active ? undefined : 'pink.300'}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IoCaretUp } from '@react-icons/all-files/io5/IoCaretUp';
|
||||
|
||||
export default function CursorUpBtn(props) {
|
||||
const { clickhandler, active, ref, size } = props;
|
||||
return (
|
||||
<Tooltip label='Move cursor up Alt + ↑'>
|
||||
<IconButton
|
||||
ref={ref}
|
||||
size={size || 'xs'}
|
||||
icon={<IoCaretUp />}
|
||||
color={active ? 'pink.100' : 'pink.300'}
|
||||
borderColor={active ? undefined : 'pink.300'}
|
||||
backgroundColor={active ? 'pink.400' : undefined}
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={clickhandler}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiClock } from 'react-icons/fi';
|
||||
import { FiClock } from '@react-icons/all-files/fi/FiClock';
|
||||
|
||||
export default function DelayIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { useState } from 'react';
|
||||
import { IoRemove } from 'react-icons/io5';
|
||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function DeleteIconBtn(props) {
|
||||
const { actionHandler, ...rest } = props;
|
||||
const { actionHandler, size, ...rest } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
@@ -15,7 +16,7 @@ export default function DeleteIconBtn(props) {
|
||||
return (
|
||||
<Tooltip label='Delete'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={<IoRemove />}
|
||||
colorScheme='red'
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { IoCloseSharp, IoCheckmarkSharp } from 'react-icons/io5';
|
||||
import React from 'react';
|
||||
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
|
||||
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
|
||||
export default function EnableBtn(props) {
|
||||
const { active, text, actionHandler } = props;
|
||||
const { active, text, actionHandler, size } = props;
|
||||
return (
|
||||
<Button
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
leftIcon={active ? <IoCheckmarkSharp /> : <IoCloseSharp />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiChevronsDown } from 'react-icons/fi';
|
||||
import { FiChevronsDown } from '@react-icons/all-files/fi/FiChevronsDown';
|
||||
|
||||
export default function ExpandBtn(props) {
|
||||
const { clickhandler } = props;
|
||||
const { clickhandler, size } = props;
|
||||
return (
|
||||
<Tooltip label='Expand all'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={<FiChevronsDown />}
|
||||
colorScheme='white'
|
||||
variant='outline'
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoPlaySkipForward } from 'react-icons/io5';
|
||||
import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function NextIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickhandler, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Next event' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<Tooltip label='Next event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlaySkipForward size='22px' />}
|
||||
colorScheme='whiteAlpha'
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoMicSharp, IoMicOffOutline } from 'react-icons/io5';
|
||||
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
|
||||
import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function OnAirIconBtn(props) {
|
||||
const { actionHandler, active, ...rest } = props;
|
||||
const { actionHandler, active, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={active ? 'Go Off Air' : 'Go On Air'} openDelay={500}>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={active ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() =>
|
||||
actionHandler('update', { field: 'isPublic', value: !active })
|
||||
}
|
||||
onClick={() => actionHandler('update', { field: 'isPublic', value: !active })}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoPause } from 'react-icons/io5';
|
||||
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function PauseIconBtn(props) {
|
||||
const { clickhandler, active, ...rest } = props;
|
||||
const { clickhandler, active, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Pause timer' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<Tooltip label='Pause timer' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPause size='24px' />}
|
||||
colorScheme='orange'
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoPlaySkipBack } from 'react-icons/io5';
|
||||
import { IoPlaySkipBack } from '@react-icons/all-files/io5/IoPlaySkipBack';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function PrevIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickhandler, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Previous event' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<Tooltip label='Previous event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlaySkipBack size='22px' />}
|
||||
colorScheme='whiteAlpha'
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiUsers } from 'react-icons/fi';
|
||||
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
|
||||
|
||||
export default function PublicIconBtn(props) {
|
||||
const { actionHandler, active, ...rest } = props;
|
||||
const { actionHandler, active, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={active ? 'Make event private' : 'Make event public'}>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={<FiUsers />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() =>
|
||||
actionHandler('update', { field: 'isPublic', value: !active })
|
||||
}
|
||||
onClick={() => actionHandler('update', { field: 'isPublic', value: !active })}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoReload } from 'react-icons/io5';
|
||||
import { IoArrowUndo } from '@react-icons/all-files/io5/IoArrowUndo';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function ReloadIconButton(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickhandler, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Reload event' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<Tooltip label='Reload event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoReload size='22px' />}
|
||||
icon={<IoArrowUndo size='22px' />}
|
||||
colorScheme='whiteAlpha'
|
||||
backgroundColor='#ffffff05'
|
||||
variant='outline'
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoTimeOutline } from 'react-icons/io5';
|
||||
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function RollIconBtn(props) {
|
||||
const { clickhandler, active, ...rest } = props;
|
||||
const { clickhandler, active, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Roll mode' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<Tooltip label='Roll mode' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoTimeOutline size='24px' />}
|
||||
colorScheme='blue'
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoPlay } from 'react-icons/io5';
|
||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function StartIconBtn(props) {
|
||||
const { clickhandler, active, ...rest } = props;
|
||||
const { clickhandler, active, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Start timer' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<Tooltip label='Start timer' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoPlay size='24px' />}
|
||||
colorScheme='green'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiTrash2 } from 'react-icons/fi';
|
||||
import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2';
|
||||
|
||||
export default function TrashIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoStop } from 'react-icons/io5';
|
||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function UnloadIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickhandler, disabled, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Unload event' openDelay={500} shouldWrapChildren={props.disabled}>
|
||||
<Tooltip label='Unload event' openDelay={500} shouldWrapChildren={disabled}>
|
||||
<IconButton
|
||||
icon={<IoStop size='22px' />}
|
||||
colorScheme='red'
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IoSunny } from 'react-icons/io5';
|
||||
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function VisibleIconBtn(props) {
|
||||
const { actionHandler, active, ...rest } = props;
|
||||
const { actionHandler, active, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={active ? 'Make invisible' : 'Make visible'} openDelay={500}>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
icon={<IoSunny size={'18px'}/>}
|
||||
size={size || 'xs'}
|
||||
icon={<IoSunny size='18px' />}
|
||||
colorScheme='blue'
|
||||
variant={active ? 'solid' : 'outline'}
|
||||
onClick={() =>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import style from "../../../features/info/Info.module.scss";
|
||||
import {Icon} from "@chakra-ui/react";
|
||||
import {FiChevronUp} from "react-icons/fi";
|
||||
import {FiChevronUp} from "@react-icons/all-files/fi/FiChevronUp";
|
||||
|
||||
export default function CollapseBar(props) {
|
||||
const {title = 'Collapse bar', isCollapsed = false, onClick}= props;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { formatDisplay } from 'common/utils/dateConfig';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Countdown.module.css';
|
||||
@@ -25,8 +25,8 @@ const Countdown = ({ time, small, isNegative, hideZeroHours }) => {
|
||||
export default memo(Countdown);
|
||||
|
||||
Countdown.propTypes = {
|
||||
time: PropTypes.number.isRequired,
|
||||
time: PropTypes.number,
|
||||
small: PropTypes.bool,
|
||||
isNegative: PropTypes.bool,
|
||||
hideZeroHour: PropTypes.bool,
|
||||
hideZeroHours: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
import React from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useContext } from 'react';
|
||||
import EditableTimer from 'common/input/EditableTimer';
|
||||
import { useContext } from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import { validateTimes } from '../../../app/entryValidator';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -8,6 +8,13 @@ export default function EventTimes(props) {
|
||||
const { actionHandler, delay, timeStart, timeEnd, previousEnd } = props;
|
||||
const { emitWarning } = useContext(LoggingContext);
|
||||
|
||||
/**
|
||||
* This code is duplicated from EventTimesVertical
|
||||
* @description Validates a time input against its pair
|
||||
* @param {string} entry - field to validate: timeStart, timeEnd, durationOverride
|
||||
* @param {number} val - field value
|
||||
* @return {boolean}
|
||||
*/
|
||||
const handleValidate = (entry, val) => {
|
||||
if (val == null || timeStart == null || timeEnd == null) return true;
|
||||
if (timeStart === 0) return true;
|
||||
@@ -18,8 +25,10 @@ export default function EventTimes(props) {
|
||||
start = val;
|
||||
} else if (entry === 'timeEnd') {
|
||||
end = val;
|
||||
} else if (entry === 'durationOverride'){
|
||||
return true;
|
||||
} else {
|
||||
return;
|
||||
return false
|
||||
}
|
||||
|
||||
const valid = validateTimes(start, end);
|
||||
@@ -54,8 +63,8 @@ export default function EventTimes(props) {
|
||||
|
||||
EventTimes.propTypes = {
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
delay: PropTypes.number.isRequired,
|
||||
timeStart: PropTypes.number.isRequired,
|
||||
timeEnd: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext } from 'react';
|
||||
import EditableTimer from 'common/input/EditableTimer';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import { useContext } from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import { validateTimes } from '../../../app/entryValidator';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -56,11 +56,11 @@ const TimesDelayed = (props) => {
|
||||
TimesDelayed.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
delay: PropTypes.number.isRequired,
|
||||
timeStart: PropTypes.number.isRequired,
|
||||
timeEnd: PropTypes.number.isRequired,
|
||||
duration: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
|
||||
const Times = (props) => {
|
||||
@@ -102,16 +102,23 @@ const Times = (props) => {
|
||||
Times.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
timeStart: PropTypes.number.isRequired,
|
||||
timeEnd: PropTypes.number.isRequired,
|
||||
duration: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
|
||||
export default function EventTimesVertical(props) {
|
||||
const { delay, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
const { delay, timeStart, timeEnd, duration, previousEnd, actionHandler } = props;
|
||||
const { emitWarning } = useContext(LoggingContext);
|
||||
|
||||
/**
|
||||
* This code is duplicated from EventTimes
|
||||
* @description Validates a time input against its pair
|
||||
* @param {string} entry - field to validate: timeStart, timeEnd, durationOverride
|
||||
* @param {number} val - field value
|
||||
* @return {boolean}
|
||||
*/
|
||||
const handleValidate = (entry, val) => {
|
||||
if (val == null || timeStart == null || timeEnd == null) return true;
|
||||
if (timeStart === 0) return true;
|
||||
@@ -122,8 +129,10 @@ export default function EventTimesVertical(props) {
|
||||
start = val;
|
||||
} else if (entry === 'timeEnd') {
|
||||
end = val;
|
||||
} else if (entry === 'durationOverride') {
|
||||
return true;
|
||||
} else {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const valid = validateTimes(start, end);
|
||||
@@ -134,10 +143,10 @@ export default function EventTimesVertical(props) {
|
||||
return valid.value;
|
||||
};
|
||||
|
||||
return delay != null && delay > 0 ? (
|
||||
return delay != null && delay !== 0 ? (
|
||||
<TimesDelayed
|
||||
handleValidate={handleValidate}
|
||||
actionHandler={props.actionHandler}
|
||||
actionHandler={actionHandler}
|
||||
delay={delay}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
@@ -147,7 +156,7 @@ export default function EventTimesVertical(props) {
|
||||
) : (
|
||||
<Times
|
||||
handleValidate={handleValidate}
|
||||
actionHandler={props.actionHandler}
|
||||
actionHandler={actionHandler}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
@@ -157,9 +166,10 @@ export default function EventTimesVertical(props) {
|
||||
}
|
||||
|
||||
EventTimesVertical.propTypes = {
|
||||
delay: PropTypes.number.isRequired,
|
||||
timeStart: PropTypes.number.isRequired,
|
||||
timeEnd: PropTypes.number.isRequired,
|
||||
duration: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
};
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import { clamp } from 'app/utils/math';
|
||||
import styles from './MyProgressBar.module.css';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Image } from '@chakra-ui/react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import navlogo from 'assets/images/logos/LOGO-72.png';
|
||||
import style from './NavLogo.module.scss';
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './ProtectRoute.module.scss';
|
||||
import { PinInput, PinInputField } from '@chakra-ui/react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiCheck } from 'react-icons/fi';
|
||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||
import { AppContext } from '../../../app/context/AppContext';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
|
||||
|
||||
export default function ProtectRoute(props) {
|
||||
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
||||
export default function ProtectRoute({ children }) {
|
||||
const isLocal =
|
||||
window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
||||
const [pin, setPin] = useState('');
|
||||
const [failed, setFailed] = useState(false);
|
||||
const { auth, validate } = useContext(AppContext);
|
||||
@@ -23,7 +23,7 @@ export default function ProtectRoute(props) {
|
||||
if (!r) {
|
||||
setFailed(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -56,12 +56,12 @@ export default function ProtectRoute(props) {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
props.children
|
||||
children
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ProtectRoute.propTypes = {
|
||||
children: PropTypes.node.isRequired
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import TodayItem from './TodayItem';
|
||||
import style from './Paginator.module.css';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useInterval } from 'app/hooks/useInterval';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function Paginator(props) {
|
||||
const { events, selectedId } = props;
|
||||
const LIMIT_PER_PAGE = props.limit || 8;
|
||||
const SCROLL_TIME = props.time * 1000 || 10000;
|
||||
const { events, selectedId, limit = 7, time = 10, isBackstage } = props;
|
||||
const LIMIT_PER_PAGE = limit;
|
||||
const SCROLL_TIME = time * 1000 || 10000;
|
||||
const [numEvents, setNumEvents] = useState(0);
|
||||
const [page, setPage] = useState([]);
|
||||
const [pages, setPages] = useState(0);
|
||||
@@ -46,10 +47,7 @@ export default function Paginator(props) {
|
||||
<div className={style.nav}>
|
||||
{pages > 1 &&
|
||||
[...Array(pages)].map((p, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={i === selPage ? style.navItemSelected : style.navItem}
|
||||
/>
|
||||
<div key={i} className={i === selPage ? style.navItemSelected : style.navItem} />
|
||||
))}
|
||||
</div>
|
||||
<div className={style.entries}>
|
||||
@@ -63,6 +61,7 @@ export default function Paginator(props) {
|
||||
timeStart={e.timeStart}
|
||||
timeEnd={e.timeEnd}
|
||||
title={e.title}
|
||||
colour={isBackstage ? e.colour : ''}
|
||||
backstageEvent={!e.isPublic}
|
||||
/>
|
||||
);
|
||||
@@ -71,3 +70,11 @@ export default function Paginator(props) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Paginator.propTypes = {
|
||||
events: PropTypes.array,
|
||||
selectedId: PropTypes.string,
|
||||
limit: PropTypes.number,
|
||||
time: PropTypes.number,
|
||||
isBackstage: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import style from './TitleCard.module.css';
|
||||
|
||||
export default function TitleCard(props) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import style from './TitleSide.module.css';
|
||||
|
||||
export default function TitleSide(props) {
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
import React from 'react';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import style from './Paginator.module.css';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function TodayItem(props) {
|
||||
const { selected, timeStart, timeEnd, title, backstageEvent } = props;
|
||||
const { selected, timeStart, timeEnd, title, backstageEvent, colour } = props;
|
||||
|
||||
// Format timers
|
||||
const start = stringFromMillis(timeStart, false) || '';
|
||||
const end = stringFromMillis(timeEnd, false) || '';
|
||||
|
||||
// user colours
|
||||
const userColour = colour !== '' ? colour : 'transparent';
|
||||
|
||||
// select styling
|
||||
let selectStyle = style.entryPast;
|
||||
if (selected === 1) selectStyle = style.entryNow;
|
||||
else if (selected === 2) selectStyle = style.entryFuture;
|
||||
return (
|
||||
<div className={selectStyle}>
|
||||
<div
|
||||
className={`${style.entryTimes} ${
|
||||
backstageEvent ? style.backstage : undefined
|
||||
}`}
|
||||
>{`${start} · ${end}`}</div>
|
||||
<div className={selectStyle} style={{ borderLeft: `4px solid ${userColour}` }}>
|
||||
<div className={`${style.entryTimes} ${backstageEvent ? style.backstage : undefined}`}>
|
||||
{`${start} · ${end}`}
|
||||
</div>
|
||||
<div className={style.entryTitle}>{title}</div>
|
||||
{backstageEvent && <div className={style.backstageInd}/>}
|
||||
{backstageEvent && <div className={style.backstageInd} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TodayItem.propTypes = {
|
||||
selected: PropTypes.bool,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
backstageEvent: PropTypes.bool,
|
||||
colour: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -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,6 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { clamp } from '../../app/utils/math';
|
||||
import style from './TimeInput.module.css';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const inputProps = {
|
||||
width: 20,
|
||||
@@ -8,46 +10,48 @@ const inputProps = {
|
||||
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||
color: '#fff',
|
||||
border: '1px solid #ecc94b55',
|
||||
borderRadius: '4px',
|
||||
borderRadius: '8px',
|
||||
placeholder: '-',
|
||||
textAlign: 'center',
|
||||
};
|
||||
|
||||
export default function DelayInput(props) {
|
||||
const [value, setValue] = useState(props.value);
|
||||
const { actionHandler, value } = props;
|
||||
const [_value, setValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.value == null) return;
|
||||
setValue(props.value);
|
||||
}, [props.value]);
|
||||
|
||||
const handleChange = (val) => {
|
||||
if (val <= 666) setValue(val);
|
||||
};
|
||||
if (value == null) return;
|
||||
setValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleSubmit = (val) => {
|
||||
if (val === props.value) return;
|
||||
if (val === value) return;
|
||||
if (val === '') setValue(0);
|
||||
if (val >= 0 && val <= 60) {
|
||||
// convert to ms and updates
|
||||
props.actionHandler('update', { field: 'duration', value: val * 60000 });
|
||||
} else {
|
||||
props.actionHandler('update', { field: 'duration', value: 3600000 });
|
||||
}
|
||||
|
||||
// convert to ms and updates
|
||||
const msVal = clamp(val, -60, 60) * 60000;
|
||||
actionHandler('update', { field: 'duration', value: msVal });
|
||||
};
|
||||
|
||||
const labelText = `minutes ${value >= 0 ? 'delayed' : 'ahead'}`;
|
||||
|
||||
return (
|
||||
<div className={style.timeInput}>
|
||||
<Editable
|
||||
{...inputProps}
|
||||
value={value}
|
||||
onChange={(v) => handleChange(v)}
|
||||
value={_value}
|
||||
onChange={(v) => setValue(v)}
|
||||
onSubmit={(v) => handleSubmit(v)}
|
||||
>
|
||||
<EditablePreview />
|
||||
<EditableInput type='number' min='0' max='60' />
|
||||
<EditableInput type='number' min='-60' max='60' />
|
||||
</Editable>
|
||||
<span className={style.label}>{'minutes'}</span>
|
||||
<span className={style.label}>{labelText}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DelayInput.propTypes = {
|
||||
actionHandler: PropTypes.func,
|
||||
value: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import style from './EditableText.module.css';
|
||||
import style from './EditableText.module.scss';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function EditableText(props) {
|
||||
const { label, defaultValue, placeholder, submitHandler, ...rest } = props;
|
||||
const { label, defaultValue, placeholder, submitHandler, maxchar = 40, ...rest } = props;
|
||||
const [text, setText] = useState(defaultValue || '');
|
||||
const maxchar = props.maxchar || 40;
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValue == null) setText('');
|
||||
else setText(defaultValue);
|
||||
}, [defaultValue]);
|
||||
|
||||
const handleSubmit = (submitedVal) => {
|
||||
const handleSubmit = (submittedVal) => {
|
||||
// No need to update if it hasnt changed
|
||||
if (submitedVal === defaultValue) return;
|
||||
submitHandler(submitedVal);
|
||||
if (submittedVal === defaultValue) return;
|
||||
// submit a cleaned up version of the string
|
||||
const cleanVal = submittedVal.trim();
|
||||
submitHandler(cleanVal);
|
||||
|
||||
if (cleanVal !== submittedVal) {
|
||||
setText(cleanVal);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (val) => {
|
||||
@@ -33,12 +39,17 @@ export default function EditableText(props) {
|
||||
className={style.inline}
|
||||
{...rest}
|
||||
>
|
||||
<EditablePreview
|
||||
color={text === '' ? '#666' : 'inherit'}
|
||||
maxWidth='75%'
|
||||
/>
|
||||
<EditableInput overflowX='hidden' maxWidth='75%' />
|
||||
<EditablePreview className={text === '' ? style.preview : ''} />
|
||||
<EditableInput />
|
||||
</Editable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EditableText.propTypes = {
|
||||
label: PropTypes.string,
|
||||
defaultValue: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
submitHandler: PropTypes.func.isRequired,
|
||||
maxchar: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
.title {
|
||||
padding-left: 1em;
|
||||
font-size: 0.75em;
|
||||
color: #aaa;
|
||||
display: inline-block;
|
||||
width: 6em;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: 'block';
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@use '../../styles/main' as *;
|
||||
|
||||
.block {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
padding-left: 1em;
|
||||
font-size: 0.75em;
|
||||
color: $label-gray;
|
||||
display: inline-block;
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.preview {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { forgivingStringToMillis } from '../utils/dateConfig';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
import style from './EditableTimer.module.css';
|
||||
@@ -12,6 +12,16 @@ export default function EditableTimer(props) {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
// prepare time fields
|
||||
const validateValue = (value) => {
|
||||
const success = handleSubmit(value);
|
||||
if (success) {
|
||||
const ms = forgivingStringToMillis(value);
|
||||
setValue(stringFromMillis(ms + delay));
|
||||
} else {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (time == null) return;
|
||||
try {
|
||||
@@ -21,27 +31,19 @@ export default function EditableTimer(props) {
|
||||
}
|
||||
}, [time, delay, emitError]);
|
||||
|
||||
const validateValue = (value) => {
|
||||
const success = handleSubmit(value);
|
||||
if (success) setValue(value);
|
||||
else setValue(stringFromMillis(time + delay, true));
|
||||
};
|
||||
|
||||
const handleSubmit = (value) => {
|
||||
// Check if there is anything there
|
||||
if (value === '') return false;
|
||||
|
||||
let newValMillis;
|
||||
let newValMillis = 0;
|
||||
|
||||
// check for known aliases
|
||||
if (value === 'p' || value === 'prev' || value === 'previous') {
|
||||
// string to pass should be the time of the end before
|
||||
if (previousEnd != null) {
|
||||
newValMillis = previousEnd;
|
||||
} else {
|
||||
newValMillis = 0;
|
||||
}
|
||||
} else if (value.startsWith('+')) {
|
||||
} else if (value.startsWith('+') || value.startsWith('p+') || value.startsWith('p +')) {
|
||||
// string to pass should add to the end before
|
||||
const val = value.substring(1);
|
||||
newValMillis = previousEnd + forgivingStringToMillis(val);
|
||||
@@ -65,16 +67,19 @@ export default function EditableTimer(props) {
|
||||
return true;
|
||||
};
|
||||
|
||||
const isDelayed = delay != null && delay !== 0;
|
||||
|
||||
return (
|
||||
<Editable
|
||||
data-testid='editable-timer'
|
||||
onChange={(v) => setValue(v)}
|
||||
onSubmit={(v) => validateValue(v)}
|
||||
onCancel={() => setValue(stringFromMillis(time + delay, true))}
|
||||
value={value}
|
||||
className={delay > 0 ? style.delayedEditable : style.editable}
|
||||
className={isDelayed ? style.delayedEditable : style.editable}
|
||||
>
|
||||
<EditablePreview />
|
||||
<EditableInput type='text' placeholder='--:--:--' />
|
||||
<EditableInput type='text' placeholder='--:--:--' data-testid='editable-timer-input' />
|
||||
</Editable>
|
||||
);
|
||||
}
|
||||
@@ -82,8 +87,8 @@ export default function EditableTimer(props) {
|
||||
EditableTimer.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
time: PropTypes.number.isRequired,
|
||||
delay: PropTypes.number.isRequired,
|
||||
time: PropTypes.number,
|
||||
delay: PropTypes.number,
|
||||
validate: PropTypes.func.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
.delayedEditable {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
width: 6.5em;
|
||||
letter-spacing: 1px;
|
||||
height: fit-content;
|
||||
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.delayedEditable {
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import { ReactComponent as Emptyimage } from 'assets/images/empty.svg';
|
||||
import style from './Empty.module.css';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
forgivingStringToMillis,
|
||||
formatDisplay,
|
||||
isTimeString,
|
||||
millisToMinutes,
|
||||
millisToSeconds,
|
||||
forgivingStringToMillis,
|
||||
timeStringToMillis,
|
||||
} from '../dateConfig';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
@@ -280,33 +280,87 @@ describe('test isTimeString() function handle different separators', () => {
|
||||
}
|
||||
});
|
||||
|
||||
describe('test timeHelper() function handles separators', () => {
|
||||
const ts = ['1:2:3:10', '2,10', '2.10'];
|
||||
for (const s of ts) {
|
||||
test(`it handles ${s}`, () => {
|
||||
expect(typeof forgivingStringToMillis(s)).toBe('number');
|
||||
});
|
||||
}
|
||||
});
|
||||
describe('test forgivingStringToMillis()', () => {
|
||||
describe('function handles separators', () => {
|
||||
const testData = [
|
||||
{ value: '1:2:3:10', expect: 3723000 },
|
||||
{ value: '2,10', expect: 130000 },
|
||||
{ value: '2.10', expect: 130000 },
|
||||
{ value: '2 10', expect: 130000 },
|
||||
];
|
||||
|
||||
describe('test timeHelper() parses strings correctly', () => {
|
||||
const ts = [
|
||||
{ value: '', expect: 0 },
|
||||
{ value: '0', expect: 0 },
|
||||
{ value: '-0', expect: 0 },
|
||||
{ value: '1', expect: 60 * 1000 },
|
||||
{ value: '-1', expect: 60 * 1000 },
|
||||
{ value: '1.2', expect: 60 * 1000 + 2 * 1000 },
|
||||
{ value: '1.70', expect: 60 * 1000 + 70 * 1000 },
|
||||
{ value: '1.1.1', expect: 60 * 60 * 1000 + 60 * 1000 + 1000 },
|
||||
{ value: '12.1.1', expect: 12 * 60 * 60 * 1000 + 60 * 1000 + 1000 },
|
||||
{ value: '12.55.1', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 1000 },
|
||||
{ value: '12.55.40', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 40 * 1000 },
|
||||
];
|
||||
for (const s of testData) {
|
||||
test(`it handles ${s.value}`, () => {
|
||||
expect(typeof forgivingStringToMillis(s.value)).toBe('number');
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
for (const s of ts) {
|
||||
test(`it handles ${s.value}`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
describe('function handles time with no separators', () => {
|
||||
const testData = [
|
||||
{ value: '000000', expect: 0 },
|
||||
{ value: '000001', expect: 1000 },
|
||||
{ value: '000100', expect: 1000*60 },
|
||||
{ value: '010000', expect: 1000*60*60 },
|
||||
{ value: '230000', expect: 1000*60*60*23 },
|
||||
{ value: '121212', expect: 12*1000+12*60*1000+12*1000*60*60 },
|
||||
];
|
||||
|
||||
for (const s of testData) {
|
||||
test(`it handles ${s.value}`, () => {
|
||||
expect(typeof forgivingStringToMillis(s.value)).toBe('number');
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('parses strings correctly', () => {
|
||||
const ts = [
|
||||
{ value: '', expect: 0 },
|
||||
{ value: '0', expect: 0 },
|
||||
{ value: '-0', expect: 0 },
|
||||
{ value: '1', expect: 60 * 1000 },
|
||||
{ value: '-1', expect: 60 * 1000 },
|
||||
{ value: '1.2', expect: 60 * 1000 + 2 * 1000 },
|
||||
{ value: '1.70', expect: 60 * 1000 + 70 * 1000 },
|
||||
{ value: '1.1.1', expect: 60 * 60 * 1000 + 60 * 1000 + 1000 },
|
||||
{ value: '12.1.1', expect: 12 * 60 * 60 * 1000 + 60 * 1000 + 1000 },
|
||||
{ value: '12.55.1', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 1000 },
|
||||
{ value: '12.55.40', expect: 12 * 60 * 60 * 1000 + 55 * 60 * 1000 + 40 * 1000 },
|
||||
];
|
||||
|
||||
for (const s of ts) {
|
||||
test(`it handles ${s.value}`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('handles overflows', () => {
|
||||
const ts = [
|
||||
// minutes overflow
|
||||
{ value: '120', expect: 1000*60*120 },
|
||||
{ value: '2.0.0', expect: 1000*60*120 },
|
||||
{ value: '99', expect: 1000*60*99 },
|
||||
{ value: '1.39.0', expect: 1000*60*99 },
|
||||
// seconds overflow
|
||||
{ value: '0.120', expect: 120*1000 },
|
||||
{ value: '0.0.120', expect: 120*1000 },
|
||||
{ value: '0.2.0', expect: 120*1000 },
|
||||
{ value: '0.99', expect: 99*1000 },
|
||||
{ value: '0.0.99', expect: 99*1000 },
|
||||
{ value: '0.1.39', expect: 99*1000 },
|
||||
// hours overflow
|
||||
{ value: '25.0.0', expect: 1000*60*60*25 },
|
||||
// hours overflow
|
||||
{ value: '50.0.0', expect: 1000*60*60*50 },
|
||||
];
|
||||
|
||||
for (const s of ts) {
|
||||
test(`it handles ${s.value}`, () => {
|
||||
expect(forgivingStringToMillis(s.value)).toBe(s.expect);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,111 +1,122 @@
|
||||
import {formatEventList, getEventsWithDelay, trimEventlist} from "../eventsManager";
|
||||
import { formatEventList, getEventsWithDelay, trimEventlist } from '../eventsManager';
|
||||
|
||||
test('getEventsWithDelay function', () => {
|
||||
|
||||
const testData = [
|
||||
{
|
||||
"title": "Welcome to Ontime",
|
||||
"timeStart": 28800000,
|
||||
"timeEnd": 30600000,
|
||||
"type": "event",
|
||||
"id": "5946"
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 28800000,
|
||||
timeEnd: 30600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
"duration": 60000,
|
||||
"type": "delay",
|
||||
"id": "24240"
|
||||
duration: 60000,
|
||||
type: 'delay',
|
||||
id: '24240',
|
||||
},
|
||||
{
|
||||
"title": "Unless recalled by the OSC address",
|
||||
"timeStart": 34920000,
|
||||
"timeEnd": 35520000,
|
||||
"type": "event",
|
||||
"id": "8ee5"
|
||||
title: 'Unless recalled by the OSC address',
|
||||
timeStart: 34920000,
|
||||
timeEnd: 35520000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8ee5',
|
||||
},
|
||||
{
|
||||
"title": "Use simpler times to create a timer",
|
||||
"timeStart": 120000,
|
||||
"timeEnd": 720000,
|
||||
"type": "event",
|
||||
"id": "8222"
|
||||
title: 'Use simpler times to create a timer',
|
||||
timeStart: 120000,
|
||||
timeEnd: 720000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8222',
|
||||
},
|
||||
{
|
||||
"duration": 900000,
|
||||
"type": "delay",
|
||||
"revision": 0,
|
||||
"id": "a386"
|
||||
duration: 900000,
|
||||
type: 'delay',
|
||||
revision: 0,
|
||||
id: 'a386',
|
||||
},
|
||||
{
|
||||
"title": "Add delay blocks to affect all events",
|
||||
"timeStart": 37320000,
|
||||
"timeEnd": 38520000,
|
||||
"type": "event",
|
||||
"id": "6dce"
|
||||
title: 'Add delay blocks to affect all events',
|
||||
timeStart: 37320000,
|
||||
timeEnd: 38520000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '6dce',
|
||||
},
|
||||
{
|
||||
"title": "Add and remove events with [+] and [-]",
|
||||
"timeStart": 38520000,
|
||||
"timeEnd": 45120000,
|
||||
"type": "event",
|
||||
"id": "2651"
|
||||
title: 'Add and remove events with [+] and [-]',
|
||||
timeStart: 38520000,
|
||||
timeEnd: 45120000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '2651',
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"id": "e6a1"
|
||||
type: 'block',
|
||||
id: 'e6a1',
|
||||
},
|
||||
{
|
||||
"title": "And control whether they are public",
|
||||
"timeStart": 46800000,
|
||||
"timeEnd": 57600000,
|
||||
"type": "event",
|
||||
"id": "1358"
|
||||
}
|
||||
title: 'And control whether they are public',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '1358',
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
"title": "Welcome to Ontime",
|
||||
"timeStart": 28800000,
|
||||
"timeEnd": 30600000,
|
||||
"type": "event",
|
||||
"id": "5946"
|
||||
title: 'Welcome to Ontime',
|
||||
timeStart: 28800000,
|
||||
timeEnd: 30600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '5946',
|
||||
},
|
||||
{
|
||||
"title": "Unless recalled by the OSC address",
|
||||
"timeStart": 34920000+60000,
|
||||
"timeEnd": 35520000+60000,
|
||||
"type": "event",
|
||||
"id": "8ee5"
|
||||
title: 'Unless recalled by the OSC address',
|
||||
timeStart: 34920000 + 60000,
|
||||
timeEnd: 35520000 + 60000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8ee5',
|
||||
},
|
||||
{
|
||||
"title": "Use simpler times to create a timer",
|
||||
"timeStart": 120000+60000,
|
||||
"timeEnd": 720000+60000,
|
||||
"type": "event",
|
||||
"id": "8222"
|
||||
title: 'Use simpler times to create a timer',
|
||||
timeStart: 120000 + 60000,
|
||||
timeEnd: 720000 + 60000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '8222',
|
||||
},
|
||||
{
|
||||
"title": "Add delay blocks to affect all events",
|
||||
"timeStart": 37320000+60000+900000,
|
||||
"timeEnd": 38520000+60000+900000,
|
||||
"type": "event",
|
||||
"id": "6dce"
|
||||
title: 'Add delay blocks to affect all events',
|
||||
timeStart: 37320000 + 60000 + 900000,
|
||||
timeEnd: 38520000 + 60000 + 900000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '6dce',
|
||||
},
|
||||
{
|
||||
"title": "Add and remove events with [+] and [-]",
|
||||
"timeStart": 38520000+60000+900000,
|
||||
"timeEnd": 45120000+60000+900000,
|
||||
"type": "event",
|
||||
"id": "2651"
|
||||
title: 'Add and remove events with [+] and [-]',
|
||||
timeStart: 38520000 + 60000 + 900000,
|
||||
timeEnd: 45120000 + 60000 + 900000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '2651',
|
||||
},
|
||||
{
|
||||
"title": "And control whether they are public",
|
||||
"timeStart": 46800000,
|
||||
"timeEnd": 57600000,
|
||||
"type": "event",
|
||||
"id": "1358"
|
||||
}
|
||||
]
|
||||
title: 'And control whether they are public',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
colour: '',
|
||||
type: 'event',
|
||||
id: '1358',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getEventsWithDelay(testData)).toStrictEqual(expected);
|
||||
});
|
||||
@@ -135,6 +146,7 @@ describe('getEventsWithDelay edge cases', () => {
|
||||
{
|
||||
"title": "Welcome to Ontime",
|
||||
"timeEnd": 30600000,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"id": "5946"
|
||||
},
|
||||
@@ -147,6 +159,7 @@ describe('getEventsWithDelay edge cases', () => {
|
||||
"title": "Unless recalled by the OSC address",
|
||||
"timeStart": 34920000,
|
||||
"timeEnd": 35520000,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"id": "8ee5"
|
||||
}
|
||||
@@ -155,13 +168,15 @@ describe('getEventsWithDelay edge cases', () => {
|
||||
{
|
||||
"title": "Welcome to Ontime",
|
||||
"timeEnd": 30600000,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"id": "5946"
|
||||
},
|
||||
{
|
||||
"title": "Unless recalled by the OSC address",
|
||||
"timeStart": 34920000+60000,
|
||||
"timeEnd": 35520000+60000,
|
||||
"timeStart": 34920000 + 60000,
|
||||
"timeEnd": 35520000 + 60000,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"id": "8ee5"
|
||||
}
|
||||
@@ -176,6 +191,7 @@ describe('getEventsWithDelay edge cases', () => {
|
||||
"title": "Welcome to Ontime",
|
||||
"timeStart": 28800000,
|
||||
"timeEnd": 30600000,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"id": "5946"
|
||||
},
|
||||
@@ -187,6 +203,7 @@ describe('getEventsWithDelay edge cases', () => {
|
||||
"title": "Unless recalled by the OSC address",
|
||||
"timeStart": 34920000,
|
||||
"timeEnd": 35520000,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"id": "8ee5"
|
||||
}
|
||||
@@ -196,6 +213,7 @@ describe('getEventsWithDelay edge cases', () => {
|
||||
"title": "Welcome to Ontime",
|
||||
"timeStart": 28800000,
|
||||
"timeEnd": 30600000,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"id": "5946"
|
||||
},
|
||||
@@ -203,6 +221,7 @@ describe('getEventsWithDelay edge cases', () => {
|
||||
"title": "Unless recalled by the OSC address",
|
||||
"timeStart": 34920000,
|
||||
"timeEnd": 35520000,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"id": "8ee5"
|
||||
}
|
||||
@@ -216,32 +235,32 @@ describe('test trimEventlist function', () => {
|
||||
|
||||
const limit = 8;
|
||||
const testData = [
|
||||
{id: '1'},
|
||||
{id: '2'},
|
||||
{id: '3'},
|
||||
{id: '4'},
|
||||
{id: '5'},
|
||||
{id: '6'},
|
||||
{id: '7'},
|
||||
{id: '8'},
|
||||
{id: '9'},
|
||||
{id: '10'},
|
||||
{id: '11'},
|
||||
{id: '12'},
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
{ id: '9' },
|
||||
{ id: '10' },
|
||||
{ id: '11' },
|
||||
{ id: '12' },
|
||||
];
|
||||
|
||||
|
||||
test('when we use the first item', () => {
|
||||
const selectedId = '1';
|
||||
const expected = [
|
||||
{id: '1'},
|
||||
{id: '2'},
|
||||
{id: '3'},
|
||||
{id: '4'},
|
||||
{id: '5'},
|
||||
{id: '6'},
|
||||
{id: '7'},
|
||||
{id: '8'},
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
];
|
||||
|
||||
const l = trimEventlist(testData, selectedId, limit);
|
||||
@@ -252,14 +271,14 @@ describe('test trimEventlist function', () => {
|
||||
test('when we use the third item', () => {
|
||||
const selectedId = '3';
|
||||
const expected = [
|
||||
{id: '1'},
|
||||
{id: '2'},
|
||||
{id: '3'},
|
||||
{id: '4'},
|
||||
{id: '5'},
|
||||
{id: '6'},
|
||||
{id: '7'},
|
||||
{id: '8'}
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' }
|
||||
];
|
||||
|
||||
const l = trimEventlist(testData, selectedId, limit);
|
||||
@@ -270,14 +289,14 @@ describe('test trimEventlist function', () => {
|
||||
test('when we use the fourth item', () => {
|
||||
const selectedId = '4';
|
||||
const expected = [
|
||||
{id: '2'},
|
||||
{id: '3'},
|
||||
{id: '4'},
|
||||
{id: '5'},
|
||||
{id: '6'},
|
||||
{id: '7'},
|
||||
{id: '8'},
|
||||
{id: '9'}
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
{ id: '9' }
|
||||
];
|
||||
|
||||
const l = trimEventlist(testData, selectedId, limit);
|
||||
@@ -288,14 +307,14 @@ describe('test trimEventlist function', () => {
|
||||
test('if selected is not found', () => {
|
||||
const selectedId = '15';
|
||||
const expected = [
|
||||
{id: '1'},
|
||||
{id: '2'},
|
||||
{id: '3'},
|
||||
{id: '4'},
|
||||
{id: '5'},
|
||||
{id: '6'},
|
||||
{id: '7'},
|
||||
{id: '8'},
|
||||
{ id: '1' },
|
||||
{ id: '2' },
|
||||
{ id: '3' },
|
||||
{ id: '4' },
|
||||
{ id: '5' },
|
||||
{ id: '6' },
|
||||
{ id: '7' },
|
||||
{ id: '8' },
|
||||
];
|
||||
|
||||
const l = trimEventlist(testData, selectedId, limit);
|
||||
@@ -314,25 +333,27 @@ describe('test formatEvents function', () => {
|
||||
"timeStart": 28800000,
|
||||
"timeEnd": 30600000,
|
||||
"isPublic": false,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"revision": 0,
|
||||
"id": "5946"
|
||||
},
|
||||
{
|
||||
"title": "Unless recalled by the OSC address",
|
||||
"subtitle": "",
|
||||
"presenter": "",
|
||||
"note": "In green, below",
|
||||
"timeStart": 34800000,
|
||||
"timeEnd": 35400000,
|
||||
"isPublic": false,
|
||||
"type": "event",
|
||||
"revision": 0,
|
||||
"id": "8ee5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Unless recalled by the OSC address",
|
||||
"subtitle": "",
|
||||
"presenter": "",
|
||||
"note": "In green, below",
|
||||
"timeStart": 34800000,
|
||||
"timeEnd": 35400000,
|
||||
"isPublic": false,
|
||||
"colour": "",
|
||||
"type": "event",
|
||||
"revision": 0,
|
||||
"id": "8ee5"
|
||||
}
|
||||
];
|
||||
|
||||
test ('it parses correctly', () => {
|
||||
test('it parses correctly', () => {
|
||||
const selectedId = 'otherEvent';
|
||||
const nextId = 'notHere';
|
||||
const expected = [
|
||||
@@ -342,6 +363,7 @@ describe('test formatEvents function', () => {
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: ""
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
@@ -349,6 +371,7 @@ describe('test formatEvents function', () => {
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: "",
|
||||
},
|
||||
|
||||
]
|
||||
@@ -357,7 +380,7 @@ describe('test formatEvents function', () => {
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
test ('it handles selected correctly', () => {
|
||||
test('it handles selected correctly', () => {
|
||||
const selectedId = '5946';
|
||||
const nextId = '8ee5';
|
||||
const expected = [
|
||||
@@ -367,6 +390,7 @@ describe('test formatEvents function', () => {
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: true,
|
||||
isNext: false,
|
||||
colour: "",
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
@@ -374,15 +398,16 @@ describe('test formatEvents function', () => {
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: false,
|
||||
isNext: true,
|
||||
colour: "",
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
const parsed = formatEventList(testEvent, selectedId, nextId,true);
|
||||
const parsed = formatEventList(testEvent, selectedId, nextId, true);
|
||||
expect(parsed).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
test ('it handles next correctly', () => {
|
||||
test('it handles next correctly', () => {
|
||||
const selectedId = '8ee5';
|
||||
const nextId = 'notHere';
|
||||
|
||||
@@ -393,6 +418,7 @@ describe('test formatEvents function', () => {
|
||||
title: 'Welcome to Ontime',
|
||||
isNow: false,
|
||||
isNext: false,
|
||||
colour: "",
|
||||
},
|
||||
{
|
||||
id: '8ee5',
|
||||
@@ -400,6 +426,7 @@ describe('test formatEvents function', () => {
|
||||
title: 'Unless recalled by the OSC address',
|
||||
isNow: true,
|
||||
isNext: false,
|
||||
colour: "",
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
@@ -86,28 +86,39 @@ const parse = (valueAsString) => {
|
||||
|
||||
/**
|
||||
* @description Parses a time string to millis
|
||||
* @param string - time string
|
||||
* @param {string} value - time string
|
||||
* @returns {number} - time string in millis
|
||||
*/
|
||||
export const forgivingStringToMillis = (string) => {
|
||||
export const forgivingStringToMillis = (value) => {
|
||||
let millis = 0;
|
||||
|
||||
// split string at known separators : , .
|
||||
const separatorRegex = /[\s,:.]+/;
|
||||
const [first, second, third] = string.split(separatorRegex);
|
||||
const [first, second, third] = value.split(separatorRegex);
|
||||
|
||||
if (first != null && second != null && third != null) {
|
||||
// if string has three sections, treat as [hours] [minutes] [seconds]
|
||||
millis = parse(first) * mth;
|
||||
millis += parse(second) * mtm;
|
||||
millis += parse(third) * mts;
|
||||
} else if (third == null) {
|
||||
} else if (first != null && second != null && third == null) {
|
||||
// if string has two sections, treat as [minutes] [seconds]
|
||||
millis = parse(first) * mtm;
|
||||
millis += parse(second) * mts;
|
||||
} else if (second == null) {
|
||||
// if string has one section, treat as [minutes]
|
||||
millis = parse(first) * mtm;
|
||||
} else if (first != null && second == null && third == null) {
|
||||
// if string has one section,
|
||||
// could be a complete string like 121010 - 12:10:10
|
||||
if (first.length === 6) {
|
||||
const hours = first.substring(0, 2);
|
||||
const minutes = first.substring(2, 4);
|
||||
const seconds = first.substring(4);
|
||||
millis = parse(hours) * mth;
|
||||
millis += parse(minutes) * mtm;
|
||||
millis += parse(seconds) * mts;
|
||||
} else {
|
||||
// otherwise lets treat as [minutes]
|
||||
millis = parse(first) * mtm;
|
||||
}
|
||||
}
|
||||
return millis;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ import { stringFromMillis } from 'ontime-utils/time';
|
||||
* @returns {Object[]} Filtered events with calculated delays
|
||||
*/
|
||||
export const getEventsWithDelay = (events) => {
|
||||
|
||||
if (events == null) return [];
|
||||
|
||||
const unfilteredEvents = [...events];
|
||||
@@ -43,8 +42,11 @@ export const trimEventlist = (events, selectedId, limit) => {
|
||||
if (limit != null) {
|
||||
while (trimmedEvents.length > limit) {
|
||||
const idx = trimmedEvents.findIndex((e) => e.id === selectedId);
|
||||
if (idx <= BEFORE) { trimmedEvents.pop(); }
|
||||
else { trimmedEvents.shift(); }
|
||||
if (idx <= BEFORE) {
|
||||
trimmedEvents.pop();
|
||||
} else {
|
||||
trimmedEvents.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
return trimmedEvents;
|
||||
@@ -75,10 +77,9 @@ export const formatEventList = (events, selectedId, nextId, showEnd = false) =>
|
||||
title: g.title,
|
||||
isNow: g.id === selectedId,
|
||||
isNext: g.id === nextId,
|
||||
colour: g.colour,
|
||||
});
|
||||
}
|
||||
|
||||
return formattedEvents;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Returns hostname
|
||||
* @type {string}
|
||||
*/
|
||||
export const host = window?.location?.host;
|
||||
|
||||
/**
|
||||
* Open an external URLs: specifically for a electron / browser case
|
||||
* If electron: ask main process to call a new browser window
|
||||
* If browser: open in new tab
|
||||
* @param url
|
||||
*/
|
||||
export function openLink(url) {
|
||||
if (window.process?.type === 'renderer') {
|
||||
window.ipcRenderer.send('send-to-link', url);
|
||||
} else {
|
||||
window.open(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles opening external links
|
||||
* @param event
|
||||
* @param location
|
||||
*/
|
||||
export function handleLinks(event, location) {
|
||||
// we handle the link manually
|
||||
event.preventDefault();
|
||||
openLink(`http://${host}/${location}`);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSocket } from 'app/context/socketContext';
|
||||
import VisibleIconBtn from 'common/components/buttons/VisibleIconBtn';
|
||||
import OnAirIconBtn from '../../common/components/buttons/OnAirIconBtn';
|
||||
@@ -10,14 +10,14 @@ const inputProps = {
|
||||
};
|
||||
|
||||
const InputRow = (props) => {
|
||||
const { label, placeholder, text, visible } = props;
|
||||
const { label, placeholder, text, visible, actionHandler, changeHandler } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.label}>{label}</span>
|
||||
<div className={style.inputItems}>
|
||||
<Editable
|
||||
onChange={(event) => props.changeHandler(event)}
|
||||
onChange={(event) => changeHandler(event)}
|
||||
value={text}
|
||||
placeholder={placeholder}
|
||||
className={style.inline}
|
||||
@@ -28,7 +28,7 @@ const InputRow = (props) => {
|
||||
</Editable>
|
||||
<VisibleIconBtn
|
||||
active={visible || undefined}
|
||||
actionHandler={props.actionHandler}
|
||||
actionHandler={actionHandler}
|
||||
{...inputProps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import style from './PlaybackControl.module.scss';
|
||||
import StartIconBtn from 'common/components/buttons/StartIconBtn';
|
||||
@@ -69,20 +69,20 @@ const Transport = (props) => {
|
||||
};
|
||||
|
||||
const PlaybackButtons = (props) => {
|
||||
const { playback, selectedId, noEvents } = props;
|
||||
const { playback, selectedId, noEvents, playbackControl } = props;
|
||||
return (
|
||||
<>
|
||||
<Playback
|
||||
playback={playback}
|
||||
selectedId={selectedId}
|
||||
noEvents={noEvents}
|
||||
playbackControl={props.playbackControl}
|
||||
playbackControl={playbackControl}
|
||||
/>
|
||||
<Transport
|
||||
playback={playback}
|
||||
selectedId={selectedId}
|
||||
noEvents={noEvents}
|
||||
playbackControl={props.playbackControl}
|
||||
playbackControl={playbackControl}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import style from './PlaybackControl.module.scss';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSocket } from 'app/context/socketContext';
|
||||
import PlaybackButtons from './PlaybackButtons';
|
||||
import PlaybackTimer from './PlaybackTimer';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import style from './PlaybackControl.module.scss';
|
||||
import Countdown from 'common/components/countdown/Countdown';
|
||||
import { stringFromMillis } from 'ontime-utils/time';
|
||||
@@ -18,6 +19,14 @@ const areEqual = (prevProps, nextProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const incrementProps = {
|
||||
size: 'sm',
|
||||
width: '2.9em',
|
||||
colorScheme: 'whiteAlpha',
|
||||
variant: 'outline',
|
||||
_focus: { boxShadow: 'none' },
|
||||
};
|
||||
|
||||
const PlaybackTimer = (props) => {
|
||||
const { timer, playback, handleIncrement, selectedId } = props;
|
||||
const started = stringFromMillis(timer.startedAt, true);
|
||||
@@ -26,14 +35,6 @@ const PlaybackTimer = (props) => {
|
||||
const isWaiting = timer.secondary > 0 && timer.running == null;
|
||||
const disableButtons = selectedId == null || isRolling;
|
||||
|
||||
const incrementProps = {
|
||||
size: 'sm',
|
||||
width: '2.9em',
|
||||
colorScheme: 'whiteAlpha',
|
||||
variant: 'outline',
|
||||
_focus: { boxShadow: 'none' },
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={style.timeContainer}>
|
||||
@@ -56,7 +57,7 @@ const PlaybackTimer = (props) => {
|
||||
{isWaiting ? (
|
||||
<div className={style.roll}>
|
||||
<span className={style.rolltag}>Roll: Countdown to start</span>
|
||||
<span className={style.time}>{''}</span>
|
||||
<span className={style.time}>FIX</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -72,7 +73,7 @@ const PlaybackTimer = (props) => {
|
||||
)}
|
||||
<div className={style.btn}>
|
||||
<Tooltip
|
||||
label={'Remove 1 minute'}
|
||||
label='Remove 1 minute'
|
||||
delay={500}
|
||||
shouldWrapChildren={disableButtons}
|
||||
>
|
||||
@@ -85,7 +86,7 @@ const PlaybackTimer = (props) => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label={'Add 1 minute'}
|
||||
label='Add 1 minute'
|
||||
delay={500}
|
||||
shouldWrapChildren={disableButtons}
|
||||
>
|
||||
@@ -98,7 +99,7 @@ const PlaybackTimer = (props) => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label={'Remove 5 minutes'}
|
||||
label='Remove 5 minutes'
|
||||
delay={500}
|
||||
shouldWrapChildren={disableButtons}
|
||||
>
|
||||
@@ -111,7 +112,7 @@ const PlaybackTimer = (props) => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label={'Add 5 minutes'}
|
||||
label='Add 5 minutes'
|
||||
delay={500}
|
||||
shouldWrapChildren={disableButtons}
|
||||
>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { FiMoreVertical } from 'react-icons/fi';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
|
||||
import ActionButtons from '../list/ActionButtons';
|
||||
import style from './BlockBlock.module.css';
|
||||
import style from './BlockBlock.module.scss';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function BlockBlock(props) {
|
||||
|
||||
+3
-3
@@ -1,3 +1,4 @@
|
||||
@use '../../../styles/main' as *;
|
||||
/* ============= COMMON ============= */
|
||||
|
||||
.block {
|
||||
@@ -8,7 +9,7 @@
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border: $block-border;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 2em auto;
|
||||
@@ -16,7 +17,7 @@
|
||||
align-items: baseline;
|
||||
gap: 0.5em;
|
||||
|
||||
background-color: #805ad5;
|
||||
background-color: $block-block-color;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(107, 70, 193, 0.4) 0%,
|
||||
@@ -34,7 +35,6 @@
|
||||
|
||||
/* ============== ACTION ================ */
|
||||
.actionOverlay {
|
||||
display: flex;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { FiMoreVertical } from 'react-icons/fi';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import { millisToMinutes } from 'common/utils/dateConfig';
|
||||
import ActionButtons from '../list/ActionButtons';
|
||||
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
|
||||
import ApplyIconBtn from 'common/components/buttons/ApplyIconBtn';
|
||||
import DelayInput from 'common/input/DelayInput';
|
||||
import style from './DelayBlock.module.css';
|
||||
import style from './DelayBlock.module.scss';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function DelayBlock(props) {
|
||||
|
||||
+5
-3
@@ -1,3 +1,5 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
/* ============= COMMON ============= */
|
||||
|
||||
.delay {
|
||||
@@ -7,7 +9,7 @@
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border: $block-border;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 2em 1fr auto;
|
||||
@@ -17,7 +19,7 @@
|
||||
align-items: baseline;
|
||||
gap: 0.5em;
|
||||
|
||||
background-color: #ecc94b;
|
||||
background-color: $block-delay-color;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(214, 158, 46, 0.4) 0%,
|
||||
@@ -30,7 +32,7 @@
|
||||
|
||||
.drag {
|
||||
grid-area: drag;
|
||||
color: rgba(255, 255, 255, 0.67);
|
||||
color: $block-icon-drag;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { lazy, useEffect } from 'react';
|
||||
import React, { lazy, useEffect } from 'react';
|
||||
import { Box } from '@chakra-ui/layout';
|
||||
import { useDisclosure } from '@chakra-ui/hooks';
|
||||
import styles from './Editor.module.scss';
|
||||
import MenuBar from 'features/menu/MenuBar';
|
||||
import ModalManager from 'features/modals/ModalManager';
|
||||
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
|
||||
import { LoggingProvider } from '../../app/context/LoggingContext';
|
||||
import { LocalEventSettingsProvider } from '../../app/context/LocalEventSettingsContext';
|
||||
import { CursorProvider } from '../../app/context/CursorContext';
|
||||
import { CollapseProvider } from '../../app/context/CollapseContext';
|
||||
import styles from './Editor.module.scss';
|
||||
|
||||
const EventListWrapper = lazy(() =>
|
||||
import('features/editors/list/EventListWrapper')
|
||||
);
|
||||
const EventListWrapper = lazy(() => import('features/editors/list/EventListWrapper'));
|
||||
const PlaybackControl = lazy(() => import('features/control/PlaybackControl'));
|
||||
const MessageControl = lazy(() => import('features/control/MessageControl'));
|
||||
const Info = lazy(() => import('features/info/Info'));
|
||||
@@ -24,53 +25,59 @@ export default function Editor() {
|
||||
|
||||
return (
|
||||
<LoggingProvider>
|
||||
<ErrorBoundary>
|
||||
<ModalManager isOpen={isOpen} onClose={onClose} />
|
||||
</ErrorBoundary>
|
||||
<LocalEventSettingsProvider>
|
||||
<ErrorBoundary>
|
||||
<ModalManager isOpen={isOpen} onClose={onClose} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<div className={styles.mainContainer}>
|
||||
<Box id='settings' className={styles.settings}>
|
||||
<ErrorBoundary>
|
||||
<MenuBar onOpen={onOpen} isOpen={isOpen} />
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
<div className={styles.mainContainer}>
|
||||
<CursorProvider>
|
||||
<CollapseProvider>
|
||||
<Box id='settings' className={styles.settings}>
|
||||
<ErrorBoundary>
|
||||
<MenuBar onOpen={onOpen} isOpen={isOpen} />
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
|
||||
<Box className={styles.editor}>
|
||||
<h1>Event List</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<EventListWrapper />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
<Box className={styles.editor}>
|
||||
<h1>Event List</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<EventListWrapper />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
</CollapseProvider>
|
||||
</CursorProvider>
|
||||
|
||||
<Box className={styles.messages}>
|
||||
<h1>Display Messages</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<MessageControl />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
<Box className={styles.messages}>
|
||||
<h1>Display Messages</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<MessageControl />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Box className={styles.playback}>
|
||||
<h1>Timer Control</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<PlaybackControl />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
<Box className={styles.playback}>
|
||||
<h1>Timer Control</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<PlaybackControl />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Box className={styles.info}>
|
||||
<h1>Info</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<Info />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
<Box className={styles.info}>
|
||||
<h1>Info</h1>
|
||||
<div className={styles.content}>
|
||||
<ErrorBoundary>
|
||||
<Info />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
</LocalEventSettingsProvider>
|
||||
</LoggingProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { Checkbox } from '@chakra-ui/react';
|
||||
import style from './EntryBlock.module.scss';
|
||||
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
|
||||
|
||||
export default function EntryBlock(props) {
|
||||
const { showKbd, index, eventsHandler } = props;
|
||||
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
|
||||
const [doStartTime, setStartTime] = useState(starTimeIsLastEnd);
|
||||
const [doPublic, setPublic] = useState(defaultPublic);
|
||||
|
||||
useEffect(() => {
|
||||
setStartTime(starTimeIsLastEnd);
|
||||
}, [starTimeIsLastEnd]);
|
||||
|
||||
useEffect(() => {
|
||||
setPublic(defaultPublic);
|
||||
}, [defaultPublic]);
|
||||
|
||||
return (
|
||||
<div className={style.create}>
|
||||
<Tooltip label='Add Event' openDelay={300}>
|
||||
<span
|
||||
className={style.createEvent}
|
||||
onClick={() =>
|
||||
eventsHandler(
|
||||
'add',
|
||||
{ type: 'event', order: index + 1, isPublic: doPublic },
|
||||
{ startIsLastEnd: doStartTime ? index : undefined }
|
||||
)
|
||||
}
|
||||
>
|
||||
E{showKbd && <span className={style.keyboard}>Alt + E</span>}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add Delay' openDelay={300}>
|
||||
<span
|
||||
className={style.createDelay}
|
||||
onClick={() => eventsHandler('add', { type: 'delay', order: index + 1 })}
|
||||
>
|
||||
D{showKbd && <span className={style.keyboard}>Alt + D</span>}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label='Add Block' openDelay={300}>
|
||||
<span
|
||||
className={style.createBlock}
|
||||
onClick={() => eventsHandler('add', { type: 'block', order: index + 1 })}
|
||||
>
|
||||
B{showKbd && <span className={style.keyboard}>Alt + B</span>}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<div className={style.options}>
|
||||
<Checkbox
|
||||
size='sm'
|
||||
colorScheme='blue'
|
||||
isChecked={doStartTime}
|
||||
onChange={(e) => setStartTime(e.target.checked)}
|
||||
>
|
||||
Start time is last end
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
size='sm'
|
||||
colorScheme='blue'
|
||||
isChecked={doPublic}
|
||||
onChange={(e) => setPublic(e.target.checked)}
|
||||
>
|
||||
Default public
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
@use '../../../styles/main' as *;
|
||||
|
||||
.create {
|
||||
padding: 0 0.5em;
|
||||
box-sizing: border-box;
|
||||
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
|
||||
position: relative;
|
||||
height: 20px;
|
||||
margin: -8px 0;
|
||||
transition: height 0.1s ease;
|
||||
|
||||
* {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
height: calc(2.5em + 20px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5%;
|
||||
transition: height 0.15s ease;
|
||||
|
||||
* {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.createEvent,
|
||||
.createDelay,
|
||||
.createBlock {
|
||||
width: auto;
|
||||
padding: 0 16px;
|
||||
height: 24px;
|
||||
text-align: center;
|
||||
vertical-align: center;
|
||||
font-weight: 600;
|
||||
line-height: 21px;
|
||||
border-radius: 4px;
|
||||
opacity: 0.6;
|
||||
|
||||
.keyboard {
|
||||
margin-left: 4px;
|
||||
padding: 0 4px;
|
||||
color: #ccc;
|
||||
border-radius: 2px;
|
||||
font-family: Monospaced, sans-serif;
|
||||
background-color: rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
}
|
||||
|
||||
.createEvent {
|
||||
border: 1px solid #2b6cb0;
|
||||
color: lighten(#2b6cb0, 30%);
|
||||
|
||||
&:hover {
|
||||
background-color: #2b6cb0;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.createDelay {
|
||||
border: 1px solid #ecc94b;;
|
||||
color: lighten(#ecc94b, 30%);
|
||||
|
||||
&:hover {
|
||||
background-color: #ecc94b;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.createBlock {
|
||||
border: 1px solid #805ad5;
|
||||
color: lighten(#805ad5, 30%);
|
||||
|
||||
&:hover {
|
||||
background-color: #805ad5;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import Icon from '@chakra-ui/icon';
|
||||
import { FiChevronUp, FiMoreVertical } from 'react-icons/fi';
|
||||
import { useMemo } from 'react';
|
||||
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import EventTimes from 'common/components/eventTimes/EventTimes';
|
||||
import EventTimesVertical from 'common/components/eventTimes/EventTimesVertical';
|
||||
@@ -9,38 +10,31 @@ import ActionButtons from '../list/ActionButtons';
|
||||
import PublicIconBtn from 'common/components/buttons/PublicIconBtn';
|
||||
import DeleteIconBtn from 'common/components/buttons/DeleteIconBtn';
|
||||
import { millisToMinutes } from 'common/utils/dateConfig';
|
||||
import style from './EventBlock.module.css';
|
||||
import { HandleCollapse, SelectCollapse } from 'app/context/collapseAtom';
|
||||
import { useAtom } from 'jotai';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CollapseContext } from '../../../app/context/CollapseContext';
|
||||
import style from './EventBlock.module.css';
|
||||
|
||||
const ExpandedBlock = (props) => {
|
||||
const { provided, data, eventIndex, next, delay, delayValue, previousEnd, actionHandler } = props;
|
||||
|
||||
const oscid = data?.id || '...';
|
||||
|
||||
// if end is before, assume is the day after
|
||||
const duration =
|
||||
data.timeStart > data.timeEnd
|
||||
? data.timeEnd + 86400000 - data.timeStart
|
||||
: data.timeEnd - data.timeStart;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
|
||||
<div className={style.indicators}>
|
||||
<span className={next ? style.next : style.nextDisabled}>Next</span>
|
||||
{delayValue != null && <span className={style.delayValue}>+ {delayValue}</span>}
|
||||
{delayValue != null && <span className={style.delayValue}>{delayValue}</span>}
|
||||
</div>
|
||||
<div className={style.timeExpanded}>
|
||||
<EventTimesVertical
|
||||
actionHandler={actionHandler}
|
||||
timeStart={data.timeStart}
|
||||
timeEnd={data.timeEnd}
|
||||
duration={duration}
|
||||
duration={data.duration}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
className={style.time}
|
||||
@@ -93,8 +87,8 @@ ExpandedBlock.propTypes = {
|
||||
eventIndex: PropTypes.number.isRequired,
|
||||
next: PropTypes.bool.isRequired,
|
||||
delay: PropTypes.number,
|
||||
delayValue: PropTypes.number,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
delayValue: PropTypes.string,
|
||||
previousEnd: PropTypes.number,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -104,12 +98,12 @@ const CollapsedBlock = (props) => {
|
||||
return (
|
||||
<>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
|
||||
<div className={style.indicators}>
|
||||
<span className={next ? style.next : style.nextDisabled}>Next</span>
|
||||
{delayValue != null && <span className={style.delayValue}>+ {delayValue}</span>}
|
||||
{delayValue != null && <span className={style.delayValue}>{delayValue}</span>}
|
||||
</div>
|
||||
<EventTimes
|
||||
actionHandler={actionHandler}
|
||||
@@ -140,25 +134,27 @@ CollapsedBlock.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
next: PropTypes.bool.isRequired,
|
||||
delay: PropTypes.any,
|
||||
delayValue: PropTypes.any,
|
||||
delayValue: PropTypes.string,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default function EventBlock(props) {
|
||||
const { data, selected, delay, index, eventIndex, previousEnd, actionHandler } = props;
|
||||
const [collapsed] = useAtom(useMemo(() => SelectCollapse(data.id), [data.id]));
|
||||
const [, setCollapsed] = useAtom(HandleCollapse);
|
||||
const { data, selected, delay, index, eventIndex, previousEnd, actionHandler, next } = props;
|
||||
const { isCollapsed, setCollapsed } = useContext(CollapseContext);
|
||||
const collapsed = useMemo(() => isCollapsed(data.id), [data.id, isCollapsed]);
|
||||
|
||||
const isSelected = selected ? style.active : '';
|
||||
const isCollapsed = collapsed ? style.collapsed : style.expanded;
|
||||
const classSelect = `${style.event} ${isCollapsed} ${isSelected}`;
|
||||
const selectedStyle = selected ? style.active : '';
|
||||
const collapsedStyle = collapsed ? style.collapsed : style.expanded;
|
||||
const classSelect = `${style.event} ${collapsedStyle} ${selectedStyle}`;
|
||||
|
||||
// Calculate delay in min
|
||||
const delayValue = delay > 0 ? millisToMinutes(delay) : null;
|
||||
|
||||
let delayValue = null;
|
||||
if (delay != null && delay !== 0) {
|
||||
delayValue = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`;
|
||||
}
|
||||
const handleCollapse = (isCollapsed) => {
|
||||
setCollapsed({ [data.id]: isCollapsed });
|
||||
setCollapsed(data.id, isCollapsed);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -174,7 +170,7 @@ export default function EventBlock(props) {
|
||||
<CollapsedBlock
|
||||
provided={provided}
|
||||
data={data}
|
||||
next={props.next}
|
||||
next={next}
|
||||
delay={delay}
|
||||
delayValue={delayValue}
|
||||
previousEnd={previousEnd}
|
||||
@@ -185,7 +181,7 @@ export default function EventBlock(props) {
|
||||
provided={provided}
|
||||
eventIndex={eventIndex}
|
||||
data={data}
|
||||
next={props.next}
|
||||
next={next}
|
||||
delay={delay}
|
||||
delayValue={delayValue}
|
||||
previousEnd={previousEnd}
|
||||
@@ -204,6 +200,7 @@ EventBlock.propTypes = {
|
||||
delay: PropTypes.number,
|
||||
index: PropTypes.number.isRequired,
|
||||
eventIndex: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
next: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
|
||||
@@ -150,11 +150,13 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.collapsed .titleContasiner {
|
||||
.collapsed .titleContainer {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.expanded .titleContainer {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.oscLabel {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiClock, FiMinusCircle, FiPlus } from 'react-icons/fi';
|
||||
import { FiClock } from '@react-icons/all-files/fi/FiClock';
|
||||
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
|
||||
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function ActionButtons(props) {
|
||||
@@ -21,24 +24,16 @@ export default function ActionButtons(props) {
|
||||
icon={<FiPlus />}
|
||||
_expanded={{ bg: 'orange.300', color: 'white' }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
backgroundColor={'orange.200'}
|
||||
color={'orange.500'}
|
||||
backgroundColor='orange.200'
|
||||
color='orange.500'
|
||||
/>
|
||||
</Tooltip>
|
||||
<MenuList style={menuStyle}>
|
||||
<MenuItem
|
||||
icon={<FiPlus />}
|
||||
onClick={() => actionHandler('event')}
|
||||
isDisabled={!showAdd}
|
||||
>
|
||||
<MenuItem icon={<FiPlus />} onClick={() => actionHandler('event')} isDisabled={!showAdd}>
|
||||
Add Event after
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
icon={<FiClock />}
|
||||
onClick={() => actionHandler('delay')}
|
||||
isDisabled={!showDelay}
|
||||
>
|
||||
<MenuItem icon={<FiClock />} onClick={() => actionHandler('delay')} isDisabled={!showDelay}>
|
||||
Add Delay after
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import React, { createRef, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import style from './List.module.scss';
|
||||
import { createRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSocket } from 'app/context/socketContext';
|
||||
import Empty from 'common/state/Empty';
|
||||
import EventListItem from './EventListItem';
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
import { useAtom } from 'jotai';
|
||||
import { SelectSetting } from 'app/context/settingsAtom';
|
||||
import EntryBlock from '../EntryBlock/EntryBlock';
|
||||
import { CursorContext } from '../../../app/context/CursorContext';
|
||||
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
|
||||
|
||||
export default function EventList(props) {
|
||||
const { events, eventsHandler } = props;
|
||||
const { cursor, moveCursorUp, moveCursorDown, setCursor, isCursorLocked } =
|
||||
useContext(CursorContext);
|
||||
const socket = useSocket();
|
||||
const [selectedId, setSelectedId] = useState(null);
|
||||
const [nextId, setNextId] = useState(null);
|
||||
const [cursor, setCursor] = useState(0);
|
||||
const [cursorSettings] = useAtom(useMemo(() => SelectSetting('cursor'), []));
|
||||
|
||||
const cursorRef = createRef();
|
||||
const { showQuickEntry } = useContext(LocalEventSettingsContext);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback(
|
||||
@@ -26,13 +27,11 @@ export default function EventList(props) {
|
||||
if (e.altKey && (!e.ctrlKey || !e.shiftKey)) {
|
||||
// Arrow down
|
||||
if (e.keyCode === 40) {
|
||||
if (cursor == null) setCursor(0);
|
||||
else if (cursor < events.length - 1) setCursor(cursor + 1);
|
||||
if (cursor < events.length - 1) moveCursorDown();
|
||||
}
|
||||
// Arrow up
|
||||
if (e.keyCode === 38) {
|
||||
if (cursor == null) setCursor(0);
|
||||
else if (cursor > 0) setCursor(cursor - 1);
|
||||
if (cursor > 0) moveCursorUp();
|
||||
}
|
||||
// E
|
||||
if (e.key === 'e' || e.key === 'E') {
|
||||
@@ -54,7 +53,7 @@ export default function EventList(props) {
|
||||
}
|
||||
}
|
||||
},
|
||||
[cursor, events, eventsHandler]
|
||||
[cursor, events.length, eventsHandler, moveCursorDown, moveCursorUp]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -67,7 +66,7 @@ export default function EventList(props) {
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
};
|
||||
}, [handleKeyPress, cursor, events]);
|
||||
}, [handleKeyPress, cursor, events, setCursor]);
|
||||
|
||||
// handle incoming messages
|
||||
useEffect(() => {
|
||||
@@ -101,13 +100,14 @@ export default function EventList(props) {
|
||||
block: 'nearest',
|
||||
inline: 'start',
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [cursor]);
|
||||
|
||||
// if selected event
|
||||
// or cursor settings changed
|
||||
useEffect(() => {
|
||||
// and if we are locked
|
||||
if (cursorSettings !== 'locked' || selectedId == null) return;
|
||||
if (!isCursorLocked || selectedId == null) return;
|
||||
|
||||
// move cursor
|
||||
let gotoIndex = -1;
|
||||
@@ -123,7 +123,8 @@ export default function EventList(props) {
|
||||
// move cursor
|
||||
setCursor(gotoIndex);
|
||||
}
|
||||
}, [selectedId, cursorSettings]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedId, isCursorLocked]);
|
||||
|
||||
if (events.length < 1) {
|
||||
return <Empty text='No Events' />;
|
||||
@@ -155,11 +156,7 @@ export default function EventList(props) {
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<Droppable droppableId='eventlist'>
|
||||
{(provided) => (
|
||||
<div
|
||||
className={style.list}
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<div className={style.list} {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{events.map((e, index) => {
|
||||
if (index === 0) {
|
||||
cumulativeDelay = 0;
|
||||
@@ -174,24 +171,34 @@ export default function EventList(props) {
|
||||
previousEnd = thisEnd;
|
||||
thisEnd = e.timeEnd;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cursor === index ? cursorRef : undefined}
|
||||
key={e.id}
|
||||
className={cursor === index ? style.cursor : undefined}
|
||||
>
|
||||
<EventListItem
|
||||
type={e.type}
|
||||
index={index}
|
||||
eventIndex={eventIndex}
|
||||
data={e}
|
||||
selected={selectedId === e.id}
|
||||
next={nextId === e.id}
|
||||
eventsHandler={eventsHandler}
|
||||
delay={cumulativeDelay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<div key={e.id}>
|
||||
{index === 0 && showQuickEntry && (
|
||||
<EntryBlock index={-1} eventsHandler={eventsHandler} />
|
||||
)}
|
||||
<div
|
||||
ref={cursor === index ? cursorRef : undefined}
|
||||
className={cursor === index ? style.cursor : undefined}
|
||||
>
|
||||
<EventListItem
|
||||
type={e.type}
|
||||
index={index}
|
||||
eventIndex={eventIndex}
|
||||
data={e}
|
||||
selected={selectedId === e.id}
|
||||
next={nextId === e.id}
|
||||
eventsHandler={eventsHandler}
|
||||
delay={cumulativeDelay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</div>
|
||||
{showQuickEntry && (
|
||||
<EntryBlock
|
||||
showKbd={index === cursor}
|
||||
index={index}
|
||||
eventsHandler={eventsHandler}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { memo, useCallback, useContext } from 'react';
|
||||
import DelayBlock from '../DelayBlock/DelayBlock';
|
||||
import BlockBlock from '../BlockBlock/BlockBlock';
|
||||
import EventBlock from '../EventBlock/EventBlock';
|
||||
import { memo, useContext } from 'react';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import { LocalEventSettingsContext } from '../../../app/context/LocalEventSettingsContext';
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return (
|
||||
@@ -29,46 +30,79 @@ const EventListItem = (props) => {
|
||||
...rest
|
||||
} = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const { starTimeIsLastEnd, defaultPublic } = useContext(LocalEventSettingsContext);
|
||||
|
||||
/**
|
||||
* @description calculates duration from given options
|
||||
* @param {number} start
|
||||
* @param {number} end
|
||||
* @returns {number}
|
||||
*/
|
||||
const calculateDuration = useCallback(
|
||||
(start, end) => (start > end ? end + 86400000 - start : end - start),
|
||||
[]
|
||||
);
|
||||
// Create / delete new events
|
||||
const actionHandler = (action, payload) => {
|
||||
switch (action) {
|
||||
case 'event':
|
||||
eventsHandler('add', { type: 'event', order: index + 1 });
|
||||
break;
|
||||
case 'delay':
|
||||
eventsHandler('add', { type: 'delay', order: index + 1 });
|
||||
break;
|
||||
case 'block':
|
||||
eventsHandler('add', { type: 'block', order: index + 1 });
|
||||
break;
|
||||
case 'delete':
|
||||
eventsHandler('delete', data.id);
|
||||
break;
|
||||
case 'update':
|
||||
// Handles and filters update requests
|
||||
const { field, value } = payload;
|
||||
if (field === 'durationOverride') {
|
||||
// duration defines timeEnd
|
||||
let end = (data.timeStart += value);
|
||||
const newData = { id: data.id, timeEnd: end };
|
||||
const actionHandler = useCallback(
|
||||
(action, payload) => {
|
||||
switch (action) {
|
||||
case 'event':
|
||||
eventsHandler(
|
||||
'add',
|
||||
{
|
||||
type: 'event',
|
||||
order: index + 1,
|
||||
isPublic: defaultPublic,
|
||||
},
|
||||
{ startIsLastEnd: starTimeIsLastEnd ? index : undefined }
|
||||
);
|
||||
break;
|
||||
case 'delay':
|
||||
eventsHandler('add', { type: 'delay', order: index + 1 });
|
||||
break;
|
||||
case 'block':
|
||||
eventsHandler('add', { type: 'block', order: index + 1 });
|
||||
break;
|
||||
case 'delete':
|
||||
eventsHandler('delete', data.id);
|
||||
break;
|
||||
case 'update':
|
||||
// Handles and filters update requests
|
||||
const { field, value } = payload;
|
||||
const newData = { id: data.id };
|
||||
|
||||
// request update in parent
|
||||
eventsHandler('patch', newData);
|
||||
} else if (field in data) {
|
||||
// create object with new field
|
||||
const newData = { id: data.id, [field]: value };
|
||||
if (field === 'durationOverride') {
|
||||
// duration defines timeEnd
|
||||
newData.timeEnd = data.timeStart += value;
|
||||
|
||||
// request update in parent
|
||||
eventsHandler('patch', newData);
|
||||
} else {
|
||||
emitError(`Unknown field: ${field}`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
// request update in parent
|
||||
eventsHandler('patch', newData);
|
||||
} else if (field === 'timeStart') {
|
||||
newData.duration = calculateDuration(value, data.timeEnd);
|
||||
newData.timeStart = value;
|
||||
// request update in parent
|
||||
eventsHandler('patch', newData);
|
||||
} else if (field === 'timeEnd') {
|
||||
newData.duration = calculateDuration(data.timeStart, value);
|
||||
newData.timeEnd = value;
|
||||
// request update in parent
|
||||
eventsHandler('patch', newData);
|
||||
} else if (field in data) {
|
||||
// create object with new field
|
||||
newData[field] = value;
|
||||
|
||||
// request update in parent
|
||||
eventsHandler('patch', newData);
|
||||
} else {
|
||||
emitError(`Unknown field: ${field}`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[data, defaultPublic, emitError, eventsHandler, index, starTimeIsLastEnd]
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case 'event':
|
||||
@@ -85,9 +119,7 @@ const EventListItem = (props) => {
|
||||
/>
|
||||
);
|
||||
case 'block':
|
||||
return (
|
||||
<BlockBlock index={index} data={data} actionHandler={actionHandler} />
|
||||
);
|
||||
return <BlockBlock index={index} data={data} actionHandler={actionHandler} />;
|
||||
case 'delay':
|
||||
return (
|
||||
<DelayBlock
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { EVENTS_TABLE } from 'app/api/apiConstants';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import {
|
||||
fetchAllEvents,
|
||||
requestApplyDelay,
|
||||
requestDelete,
|
||||
requestDeleteAll,
|
||||
requestPatch,
|
||||
requestPost,
|
||||
requestPut,
|
||||
requestDelete,
|
||||
requestDeleteAll,
|
||||
requestReorder,
|
||||
requestApplyDelay,
|
||||
} from 'app/api/eventsApi.js';
|
||||
import { useFetch } from 'app/hooks/useFetch.js';
|
||||
import EventList from './EventList';
|
||||
import EventListMenu from 'features/menu/EventListMenu.jsx';
|
||||
import { useFetch } from 'app/hooks/useFetch.js';
|
||||
import Empty from 'common/state/Empty';
|
||||
import { EVENTS_TABLE } from 'app/api/apiConstants';
|
||||
import { BatchOperation } from 'app/context/collapseAtom';
|
||||
import { useAtom } from 'jotai';
|
||||
import { LoggingContext } from '../../../app/context/LoggingContext';
|
||||
import { CollapseContext } from '../../../app/context/CollapseContext';
|
||||
|
||||
export default function EventListWrapper() {
|
||||
const [, setCollapsed] = useAtom(BatchOperation);
|
||||
const { expandAll, collapseMultiple } = useContext(CollapseContext);
|
||||
const queryClient = useQueryClient();
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const { data, status, isError, refetch } = useFetch(
|
||||
EVENTS_TABLE,
|
||||
fetchAllEvents
|
||||
);
|
||||
const { data, status, isError, refetch } = useFetch(EVENTS_TABLE, fetchAllEvents);
|
||||
const [events, setEvents] = useState(null);
|
||||
|
||||
const addEvent = useMutation(requestPost, {
|
||||
@@ -72,10 +68,7 @@ export default function EventListWrapper() {
|
||||
queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]);
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousEvent = queryClient.getQueryData([
|
||||
EVENTS_TABLE,
|
||||
newEvent.id,
|
||||
]);
|
||||
const previousEvent = queryClient.getQueryData([EVENTS_TABLE, newEvent.id]);
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
|
||||
@@ -86,10 +79,7 @@ export default function EventListWrapper() {
|
||||
|
||||
// Mutation fails, rollback undos optimist update
|
||||
onError: (error, newEvent, context) => {
|
||||
queryClient.setQueryData(
|
||||
[EVENTS_TABLE, context.newEvent.id],
|
||||
context.previousEvent
|
||||
);
|
||||
queryClient.setQueryData([EVENTS_TABLE, context.newEvent.id], context.previousEvent);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
@@ -105,10 +95,7 @@ export default function EventListWrapper() {
|
||||
queryClient.cancelQueries([EVENTS_TABLE, newEvent.id]);
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousEvent = queryClient.getQueryData([
|
||||
EVENTS_TABLE,
|
||||
newEvent.id,
|
||||
]);
|
||||
const previousEvent = queryClient.getQueryData([EVENTS_TABLE, newEvent.id]);
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData([EVENTS_TABLE, newEvent.id], newEvent);
|
||||
@@ -119,15 +106,16 @@ export default function EventListWrapper() {
|
||||
|
||||
// Mutation fails, rollback undos optimist update
|
||||
onError: (error, newEvent, context) => {
|
||||
queryClient.setQueryData(
|
||||
[EVENTS_TABLE, context.newEvent.id],
|
||||
context.previousEvent
|
||||
);
|
||||
queryClient.setQueryData([EVENTS_TABLE, context.newEvent.id], context.previousEvent);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
onSettled: (newEvent) => {
|
||||
queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
|
||||
if (newEvent) {
|
||||
queryClient.invalidateQueries([EVENTS_TABLE, newEvent.id]);
|
||||
} else {
|
||||
queryClient.invalidateQueries(EVENTS_TABLE);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -237,11 +225,20 @@ export default function EventListWrapper() {
|
||||
|
||||
// Events API
|
||||
const eventsHandler = useCallback(
|
||||
async (action, payload) => {
|
||||
async (action, payload, options = undefined) => {
|
||||
switch (action) {
|
||||
case 'add':
|
||||
try {
|
||||
await addEvent.mutateAsync(payload);
|
||||
let newEvent = { ...payload };
|
||||
// there is an option to pass an index of an array to use as start time
|
||||
if (options?.startIsLastEnd !== undefined) {
|
||||
newEvent.timeStart = data[options.startIsLastEnd].timeEnd || 0;
|
||||
}
|
||||
// hard coding duration value to be as expected for now
|
||||
// this until timeOptions gets implemented
|
||||
// Todo: implement duration options
|
||||
newEvent.duration = Math.max(0, newEvent.timeEnd - newEvent.timeStart) || 0;
|
||||
await addEvent.mutateAsync(newEvent);
|
||||
} catch (error) {
|
||||
emitError(`Error fetching data: ${error.message}`);
|
||||
}
|
||||
@@ -307,11 +304,12 @@ export default function EventListWrapper() {
|
||||
|
||||
case 'collapseall':
|
||||
if (data == null) return;
|
||||
setCollapsed({ clear: true, items: data, isCollapsed: true });
|
||||
collapseMultiple(data);
|
||||
break;
|
||||
|
||||
case 'expandall':
|
||||
if (data == null) return;
|
||||
setCollapsed({ clear: true, items: data, isCollapsed: false });
|
||||
expandAll();
|
||||
break;
|
||||
|
||||
case 'deleteall':
|
||||
@@ -326,6 +324,7 @@ export default function EventListWrapper() {
|
||||
break;
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[data]
|
||||
);
|
||||
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
.cursor {
|
||||
width: 100%;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
#ff7597 2%,
|
||||
#0001 3%,
|
||||
#0001 97%,
|
||||
#ff7597 98%
|
||||
180deg,
|
||||
#ff7597 2%,
|
||||
#0001 3%,
|
||||
#0001 97%,
|
||||
#ff7597 98%
|
||||
);
|
||||
|
||||
border-radius: 14px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useSocket } from 'app/context/socketContext';
|
||||
import style from './Info.module.scss';
|
||||
import InfoTitle from './InfoTitle';
|
||||
@@ -45,10 +45,10 @@ export default function Info() {
|
||||
if (data.total === 0 || data.total == null) {
|
||||
setSelected('No events');
|
||||
} else {
|
||||
const formatedCurrent = `Event ${
|
||||
const formattedCurrent = `Event ${
|
||||
data.index != null ? data.index + 1 : '-'
|
||||
}/${data.total != null ? data.total : '-'}`;
|
||||
setSelected(formatedCurrent);
|
||||
}/${data.total ? data.total : '-'}`;
|
||||
setSelected(formattedCurrent);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -78,12 +78,12 @@ export default function Info() {
|
||||
return (
|
||||
<>
|
||||
<div className={style.main}>
|
||||
<span>{`Running on port 4001`}</span>
|
||||
<span>Running on port 4001</span>
|
||||
<span>{selected}</span>
|
||||
</div>
|
||||
<InfoNif />
|
||||
<InfoTitle title={'Now'} data={titlesNow} roll={playback === 'roll'} />
|
||||
<InfoTitle title={'Next'} data={titlesNext} roll={playback === 'roll'} />
|
||||
<InfoTitle title='Now' data={titlesNow} roll={playback === 'roll'} />
|
||||
<InfoTitle title='Next' data={titlesNext} roll={playback === 'roll'} />
|
||||
<InfoLogger />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import style from './InfoLogger.module.scss';
|
||||
import CollapseBar from "../../common/components/collapseBar/CollapseBar";
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
@@ -54,7 +54,7 @@ export default function InfoLogger() {
|
||||
|
||||
return (
|
||||
<div className={collapsed ? style.container : style.container__expanded}>
|
||||
<CollapseBar title={'Log'} isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)}/>
|
||||
<CollapseBar title='Log' isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)} />
|
||||
{!collapsed && (
|
||||
<>
|
||||
<div className={style.toggleBar}>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { APP_TABLE } from 'app/api/apiConstants';
|
||||
import { getInfo, ontimePlaceholderInfo } from 'app/api/ontimeApi';
|
||||
import { useFetch } from 'app/hooks/useFetch';
|
||||
import style from './Info.module.scss';
|
||||
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
|
||||
import handleLink from '../../common/utils/handleLink';
|
||||
import { openLink } from '../../common/utils/linkUtils';
|
||||
import style from './Info.module.scss';
|
||||
|
||||
export default function InfoNif() {
|
||||
const { data, status } = useFetch(APP_TABLE, getInfo, {
|
||||
@@ -14,26 +14,25 @@ export default function InfoNif() {
|
||||
const baseURL = 'http://__IP__:4001';
|
||||
|
||||
return (
|
||||
<div className={style.container}>
|
||||
<CollapseBar title={'Network Info'} isCollapsed={collapsed} onClick={() => setCollapsed((c) => !c)}/>
|
||||
<div className={style.container}>
|
||||
<CollapseBar
|
||||
title='Network Info'
|
||||
isCollapsed={collapsed}
|
||||
onClick={() => setCollapsed((c) => !c)}
|
||||
/>
|
||||
{!collapsed && (
|
||||
<div>
|
||||
{status === 'success' && (
|
||||
<>
|
||||
{data?.networkInterfaces.map((e) => {
|
||||
return (
|
||||
<a
|
||||
key={e.address}
|
||||
href='#!'
|
||||
onClick={() =>
|
||||
handleLink(baseURL.replace('__IP__', e.address))
|
||||
}
|
||||
className={style.if}
|
||||
>{`${e.name} - ${e.address}`}</a>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{status === 'success' &&
|
||||
data?.networkInterfaces.map((e) => (
|
||||
<a
|
||||
key={e.address}
|
||||
href='#!'
|
||||
onClick={() => openLink(baseURL.replace('__IP__', e.address))}
|
||||
className={style.if}
|
||||
>
|
||||
{`${e.name} - ${e.address}`}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
import { useState } from 'react';
|
||||
import { FiChevronUp } from 'react-icons/fi';
|
||||
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
||||
import style from './Info.module.scss';
|
||||
|
||||
export default function InfoTitle(props) {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { memo, useMemo } from 'react';
|
||||
import React, { memo, useContext } from 'react';
|
||||
import { Divider } from '@chakra-ui/react';
|
||||
import style from './EventListMenu.module.css';
|
||||
import { CursorContext } from '../../app/context/CursorContext';
|
||||
import MenuActionButtons from './MenuActionButtons';
|
||||
import CollapseBtn from 'common/components/buttons/CollapseBtn';
|
||||
import ExpandBtn from 'common/components/buttons/ExpandBtn';
|
||||
import { SelectSetting, HandleOptions } from 'app/context/settingsAtom';
|
||||
import { useAtom } from 'jotai';
|
||||
import LockIconBtn from 'common/components/buttons/LockIconBtn';
|
||||
import CursorUpBtn from '../../common/components/buttons/CursorUpBtn';
|
||||
import CursorDownBtn from '../../common/components/buttons/CursorDownBtn';
|
||||
import CursorLockedBtn from 'common/components/buttons/CursorLockedBtn';
|
||||
import style from './EventListMenu.module.css';
|
||||
import ExpandBtn from '../../common/components/buttons/ExpandBtn';
|
||||
|
||||
const EventListMenu = ({ eventsHandler }) => {
|
||||
const [cursorSettings] = useAtom(useMemo(() => SelectSetting('cursor'), []));
|
||||
const [, SetOption] = useAtom(HandleOptions);
|
||||
const { isCursorLocked, toggleCursorLocked, moveCursorUp, moveCursorDown } =
|
||||
useContext(CursorContext);
|
||||
|
||||
const actionHandler = (action) => {
|
||||
switch (action) {
|
||||
@@ -23,12 +24,14 @@ const EventListMenu = ({ eventsHandler }) => {
|
||||
case 'block':
|
||||
eventsHandler('add', { type: action, order: 0 });
|
||||
break;
|
||||
case 'cursorUp':
|
||||
moveCursorUp();
|
||||
break;
|
||||
case 'cursorDown':
|
||||
moveCursorDown();
|
||||
break;
|
||||
case 'togglelock':
|
||||
let newSet = 'locked';
|
||||
if (cursorSettings === 'locked') {
|
||||
newSet = 'unlocked';
|
||||
}
|
||||
SetOption({ cursor: newSet });
|
||||
toggleCursorLocked();
|
||||
break;
|
||||
case 'deleteall':
|
||||
eventsHandler('deleteall');
|
||||
@@ -40,16 +43,15 @@ const EventListMenu = ({ eventsHandler }) => {
|
||||
|
||||
return (
|
||||
<div className={style.headerButtons}>
|
||||
<CollapseBtn
|
||||
size='sm'
|
||||
clickhandler={() => eventsHandler('collapseall')}
|
||||
/>
|
||||
<ExpandBtn size='sm' clickhandler={() => eventsHandler('expandall')} />
|
||||
<CollapseBtn size='sm' clickhandler={() => eventsHandler('collapseall')} />
|
||||
<Divider orientation='vertical' />
|
||||
<LockIconBtn
|
||||
<CursorUpBtn size='sm' clickhandler={() => actionHandler('cursorUp')} />
|
||||
<CursorDownBtn size='sm' clickhandler={() => actionHandler('cursorDown')} />
|
||||
<CursorLockedBtn
|
||||
size='sm'
|
||||
clickhandler={() => actionHandler('togglelock')}
|
||||
active={cursorSettings === 'locked'}
|
||||
active={isCursorLocked}
|
||||
/>
|
||||
<Divider orientation='vertical' />
|
||||
<MenuActionButtons actionHandler={actionHandler} size='sm' />
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { FiClock, FiMinusCircle, FiPlus, FiTrash2 } from 'react-icons/fi';
|
||||
import { FiClock } from '@react-icons/all-files/fi/FiClock';
|
||||
import { FiMinusCircle } from '@react-icons/all-files/fi/FiMinusCircle';
|
||||
import { FiPlus } from '@react-icons/all-files/fi/FiPlus';
|
||||
import { FiTrash2 } from '@react-icons/all-files/fi/FiTrash2';
|
||||
import { Divider } from '@chakra-ui/layout';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export default function MenuActionButtons(props) {
|
||||
const { actionHandler } = props;
|
||||
const { actionHandler, size } = props;
|
||||
const menuStyle = {
|
||||
color: '#000000',
|
||||
backgroundColor: 'rgba(255,255,255,1)',
|
||||
@@ -17,12 +21,12 @@ export default function MenuActionButtons(props) {
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label='Create Menu'
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={<FiPlus />}
|
||||
_expanded={{ bg: 'orange.300', color: 'white' }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
backgroundColor={'orange.200'}
|
||||
color={'orange.500'}
|
||||
backgroundColor='orange.200'
|
||||
color='orange.500'
|
||||
/>
|
||||
</Tooltip>
|
||||
<MenuList style={menuStyle}>
|
||||
@@ -32,18 +36,11 @@ export default function MenuActionButtons(props) {
|
||||
<MenuItem icon={<FiClock />} onClick={() => actionHandler('delay')}>
|
||||
Add Delay first
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<FiMinusCircle />}
|
||||
onClick={() => actionHandler('block')}
|
||||
>
|
||||
<MenuItem icon={<FiMinusCircle />} onClick={() => actionHandler('block')}>
|
||||
Add Block first
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
icon={<FiTrash2 />}
|
||||
onClick={() => actionHandler('deleteall')}
|
||||
color='red.500'
|
||||
>
|
||||
<MenuItem icon={<FiTrash2 />} onClick={() => actionHandler('deleteall')} color='red.500'>
|
||||
Delete All
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React, { useContext, useRef } from 'react';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { downloadEvents, uploadEvents } from 'app/api/ontimeApi';
|
||||
import { EVENTS_TABLE } from 'app/api/apiConstants';
|
||||
@@ -9,7 +10,6 @@ import QuitIconBtn from './buttons/QuitIconBtn';
|
||||
import style from './MenuBar.module.scss';
|
||||
import HelpIconBtn from './buttons/HelpIconBtn';
|
||||
import UploadIconBtn from './buttons/UploadIconBtn';
|
||||
import { useContext, useRef } from 'react';
|
||||
import { LoggingContext } from '../../app/context/LoggingContext';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -40,7 +40,6 @@ export default function MenuBar(props) {
|
||||
|
||||
const handleUpload = (event) => {
|
||||
const fileUploaded = event.target.files[0];
|
||||
console.log('1', fileUploaded)
|
||||
if (fileUploaded == null) return;
|
||||
|
||||
// Limit file size to 1MB
|
||||
@@ -49,9 +48,6 @@ export default function MenuBar(props) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('2', ! fileUploaded.name.endsWith('.xlsx')
|
||||
|| !fileUploaded.name.endsWith('.json'))
|
||||
|
||||
// Check file extension
|
||||
if (fileUploaded.name.endsWith('.xlsx') || fileUploaded.name.endsWith('.json')) {
|
||||
try {
|
||||
@@ -145,7 +141,7 @@ export default function MenuBar(props) {
|
||||
}
|
||||
|
||||
MenuBar.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
isOpen: PropTypes.bool,
|
||||
onOpen: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { FiDownload } from 'react-icons/fi';
|
||||
import { FiDownload } from '@react-icons/all-files/fi/FiDownload';
|
||||
|
||||
export default function DownloadIconBtn(props) {
|
||||
const { clickhandler, ...rest } = props;
|
||||
const { clickhandler, size, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label='Export event list'>
|
||||
<IconButton
|
||||
size={props.size || 'xs'}
|
||||
size={size || 'xs'}
|
||||
icon={<FiDownload />}
|
||||
colorScheme='white'
|
||||
onClick={clickhandler}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user