mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-09 01:13:55 +00:00
Feat/55 v2 (#225)
* chore: upgrade relevant libraries * feat(skip): add skip styling to paginated items * chore: upgrade relevant libraries * Fix: issue with text colour (#214) * fix: adjust text colour from context * fix: issue with wrong proptype * fix issue with vite migration (#217) * hotfix: 1.8.2 issues vite migration * fix: file import options * feat(55): styling * refactor: remove onhover option for entry block * refactor: relocate logging provider * refactor: convert to typescript * feat(55): add event editor * refactor: extract event actions * fix: bad import * feat(55): redesign components * fix: issues with not awaiting async * refactor: replace socket with subscription * refactor: remove unused * style: small tweaks * refactor: extract event actions * refactor: cleanup debug * refactor: chakra imports * fix: optimistic mutations * refactor: handle promise rejections * refactor: validation * fix: rq optimistic mutations * feat: add playback feedback to block * refactor: no optimistic adding of events * refactor: simplify cursor state * styles: cleanup editor style * refactor: cleanup duration update * fix: revert package upgrade (issues with vitest) * fix: prevent cyclic imports * refactor: extract data fetcher * refactor: typescript migration * chore: update tests
This commit is contained in:
+17
-13
@@ -3,29 +3,29 @@
|
||||
"version": "1.8.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^2.3.2",
|
||||
"@chakra-ui/react": "2.3.4",
|
||||
"@dnd-kit/core": "^6.0.5",
|
||||
"@dnd-kit/sortable": "^7.0.1",
|
||||
"@dnd-kit/utilities": "^3.2.0",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"@tanstack/react-query": "^4.1.3",
|
||||
"@tanstack/react-query-devtools": "^4.0.10",
|
||||
"@tanstack/react-query": "^4.10.3",
|
||||
"@tanstack/react-query-devtools": "^4.11.0",
|
||||
"autosize": "^5.0.1",
|
||||
"axios": "^0.27.2",
|
||||
"color": "^4.2.3",
|
||||
"framer-motion": "^7.3.2",
|
||||
"jotai": "^1.7.8",
|
||||
"framer-motion": "^7.5.3",
|
||||
"jotai": "^1.8.5",
|
||||
"luxon": "^3.0.1",
|
||||
"react": "^18.1.0",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-fast-compare": "^3.2.0",
|
||||
"react-qr-code": "^2.0.5",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-table": "^7.7.0",
|
||||
"socket.io-client": "^4.5.1",
|
||||
"socket.io-client": "^4.5.2",
|
||||
"typeface-open-sans": "^1.1.13",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
@@ -56,12 +56,13 @@
|
||||
"@testing-library/user-event": "^14.1.1",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/node": "^18.7.16",
|
||||
"@types/react": "^18.0.19",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-beautiful-dnd": "^13.1.2",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.37.0",
|
||||
"@typescript-eslint/parser": "^5.37.0",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-jest": "^27.0.4",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
@@ -76,9 +77,12 @@
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-standard-scss": "^4.0.0",
|
||||
"typescript": "^4.8.3",
|
||||
"vite": "^3.1.1",
|
||||
"vite": "^3.1.6",
|
||||
"vite-plugin-svgr": "^2.2.1",
|
||||
"vite-tsconfig-paths": "^3.5.0",
|
||||
"vitest": "^0.23.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/@types/react": "18.0.21"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-18
@@ -1,18 +1,19 @@
|
||||
import { Suspense, useCallback, useEffect } from 'react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
|
||||
import { AppContextProvider } from 'common/context/AppContext';
|
||||
import { LoggingProvider } from 'common/context/LoggingContext';
|
||||
import SocketProvider from 'common/context/socketContext';
|
||||
|
||||
import { AppContextProvider } from './common/context/AppContext';
|
||||
import SocketProvider from './common/context/socketContext';
|
||||
import { ontimeQueryClient } from './common/queryClient';
|
||||
import theme from './theme/theme';
|
||||
import AppRouter from './AppRouter';
|
||||
|
||||
// Load Open Sans typeface
|
||||
import('typeface-open-sans');
|
||||
export const ontimeQueryClient = new QueryClient();
|
||||
|
||||
function App() {
|
||||
|
||||
@@ -45,20 +46,22 @@ function App() {
|
||||
return (
|
||||
<ChakraProvider resetCSS theme={theme}>
|
||||
<SocketProvider>
|
||||
<QueryClientProvider client={ontimeQueryClient}>
|
||||
<AppContextProvider>
|
||||
<BrowserRouter>
|
||||
<div className='App'>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={null}>
|
||||
<AppRouter />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</AppContextProvider>
|
||||
</QueryClientProvider>
|
||||
<LoggingProvider>
|
||||
<QueryClientProvider client={ontimeQueryClient}>
|
||||
<AppContextProvider>
|
||||
<BrowserRouter>
|
||||
<div className='App'>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={null}>
|
||||
<AppRouter />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</AppContextProvider>
|
||||
</QueryClientProvider>
|
||||
</LoggingProvider>
|
||||
</SocketProvider>
|
||||
</ChakraProvider>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { lazy, useEffect } from 'react';
|
||||
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { ALIASES } from './common/api/apiConstants';
|
||||
import { getAliases } from './common/api/ontimeApi';
|
||||
import { useFetch } from './common/hooks/useFetch';
|
||||
import { useSocketProvider } from './common/hooks/useSocketProvider';
|
||||
import useAliases from './common/hooks-query/useAliases';
|
||||
import withSocket from './features/viewers/ViewWrapper';
|
||||
|
||||
const Editor = lazy(() => import('features/editors/ProtectedEditor'));
|
||||
@@ -39,7 +37,7 @@ const Info = lazy(() => import('features/info/InfoExport'));
|
||||
|
||||
export default function AppRouter() {
|
||||
useSocketProvider();
|
||||
const { data } = useFetch(ALIASES, getAliases);
|
||||
const { data } = useAliases();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
@@ -6,13 +6,19 @@ const publicLocation = 'public';
|
||||
const pipLocation = 'pip';
|
||||
const studioLocation = 'studio';
|
||||
const cuesheetLocation = 'cuesheet';
|
||||
const countdownLocation = 'countdown';
|
||||
const clockLocation = 'clock';
|
||||
const lowerLocation = 'lower';
|
||||
|
||||
export const viewerLocations = [
|
||||
{ link: speakerLocation, label: 'Stage timer' },
|
||||
{ link: clockLocation, label: 'Clock' },
|
||||
{ link: minimalLocation, label: 'Minimal timer' },
|
||||
{ link: speakerLocation, label: 'Speaker Screen' },
|
||||
{ link: smLocation, label: 'Backstage Screen' },
|
||||
{ link: publicLocation, label: 'Public Screen' },
|
||||
{ link: smLocation, label: 'Backstage screen' },
|
||||
{ link: publicLocation, label: 'Public screen' },
|
||||
{ link: lowerLocation, label: 'Lower thirds' },
|
||||
{ link: pipLocation, label: 'Picture in Picture' },
|
||||
{ link: studioLocation, label: 'Studio Clock' },
|
||||
{ link: cuesheetLocation, label: 'Cuesheet' }
|
||||
];
|
||||
{ link: studioLocation, label: 'Studio clock' },
|
||||
{ link: countdownLocation, label: 'Countdown' },
|
||||
{ link: cuesheetLocation, label: 'Cuesheet' },
|
||||
];
|
||||
|
||||
@@ -2,8 +2,9 @@ export const STATIC_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 EVENTS_TABLE_KEY = 'events';
|
||||
export const EVENTS_TABLE = [EVENTS_TABLE_KEY];
|
||||
export const APP_INFO = ['appinfo'];
|
||||
export const OSC_SETTINGS = ['oscSettings'];
|
||||
export const APP_SETTINGS = ['appSettings'];
|
||||
export const VIEW_SETTINGS = ['viewSettings'];
|
||||
@@ -25,7 +26,6 @@ export 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 stylesPath = 'external/styles/override.css';
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { EventDataType } from '../models/EventData.type';
|
||||
|
||||
import { eventURL } from './apiConstants';
|
||||
|
||||
/**
|
||||
* @description HTTP request to fetch event data
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const fetchEvent = async () => {
|
||||
export async function fetchEvent(): Promise<EventDataType> {
|
||||
const res = await axios.get(eventURL);
|
||||
return res.data;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate event data
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const postEvent = async (data) => axios.post(eventURL, data);
|
||||
export async function postEvent(data: EventDataType) {
|
||||
return axios.post(eventURL, data);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { eventsURL } from './apiConstants';
|
||||
|
||||
/**
|
||||
* @description HTTP request to fetch all events
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const fetchAllEvents = async () => {
|
||||
const res = await axios.get(eventsURL);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to post new event
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const requestPost = async (data) => axios.post(eventsURL, data);
|
||||
|
||||
/**
|
||||
* @description HTTP request to put new event
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const requestPut = async (data) => axios.put(eventsURL, data);
|
||||
|
||||
/**
|
||||
* @description HTTP request to modify event
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const requestPatch = async (data) => axios.patch(eventsURL, data);
|
||||
|
||||
/**
|
||||
* @description HTTP request to reorder events
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const requestReorder = async (data) => axios.patch(`${eventsURL}/reorder`, data);
|
||||
|
||||
/**
|
||||
* @description HTTP request to request application of delay
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const requestApplyDelay = async (eventId) => axios.patch(`${eventsURL}/applydelay/${eventId}`);
|
||||
|
||||
/**
|
||||
* @description HTTP request to delete given event
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const requestDelete = async (eventId) => axios.delete(`${eventsURL}/${eventId}`);
|
||||
|
||||
/**
|
||||
* @description HTTP request to delete all events
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const requestDeleteAll = async () => axios.delete(`${eventsURL}/all`);
|
||||
@@ -0,0 +1,70 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { OntimeEventEntry } from '../models/EventTypes';
|
||||
|
||||
import { eventsURL } from './apiConstants';
|
||||
|
||||
/**
|
||||
* @description HTTP request to fetch all events
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function fetchAllEvents(): Promise<OntimeEventEntry[]> {
|
||||
const res = await axios.get(eventsURL);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to post new event
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function requestPostEvent(data: OntimeEventEntry) {
|
||||
return axios.post(eventsURL, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to put new event
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function requestPutEvent(data: OntimeEventEntry) {
|
||||
return axios.put(eventsURL, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to modify event
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function requestPatchEvent(data: OntimeEventEntry) {
|
||||
return axios.patch(eventsURL, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to reorder events
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function requestReorderEvent(data: OntimeEventEntry) {
|
||||
return axios.patch(`${eventsURL}/reorder`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to request application of delay
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function requestApplyDelay(eventId: string) {
|
||||
return axios.patch(`${eventsURL}/applydelay/${eventId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to delete given event
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function requestDelete(eventId: string) {
|
||||
return axios.delete(`${eventsURL}/${eventId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to delete all events
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function requestDeleteAll() {
|
||||
return axios.delete(`${eventsURL}/all`);
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { ontimeURL } from './apiConstants';
|
||||
|
||||
/**
|
||||
* @description placeholder information for ontimeInfo
|
||||
* @type {{settings: {serverPort: number, version: string}, networkInterfaces: *[]}}
|
||||
*/
|
||||
export const ontimePlaceholderInfo = {
|
||||
networkInterfaces: [],
|
||||
settings: {
|
||||
version: '',
|
||||
serverPort: 4001,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @description placeholder information for ontimeSettings
|
||||
* @type {{pinCode: null}}
|
||||
*/
|
||||
export const ontimePlaceholderSettings = {
|
||||
app: 'ontime',
|
||||
version: 1,
|
||||
serverPort: 4001,
|
||||
lock: null,
|
||||
pinCode: null,
|
||||
timeFormat: '24',
|
||||
};
|
||||
|
||||
/**
|
||||
* @description placeholder information for eventSettings
|
||||
* @type {{backstageInfo: string, endMessage: string, publicInfo: string, title: string, url: string}}
|
||||
*/
|
||||
export const eventPlaceholderSettings = {
|
||||
title: '',
|
||||
url: '',
|
||||
publicInfo: '',
|
||||
backstageInfo: '',
|
||||
endMessage: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* @description placeholder information for userFields
|
||||
* @type {{user1: string, user2: string, user0: string, user9: string, user7: string, user8: string, user5: string, user6: string, user3: string, user4: string}}
|
||||
*/
|
||||
export const userFieldsPlaceholder = {
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* @description placeholder information for oscSettings
|
||||
* @type {{targetIP: string, port: string, portOut: string, enabled: boolean}}
|
||||
*/
|
||||
export const oscPlaceholderSettings = {
|
||||
port: '',
|
||||
portOut: '',
|
||||
targetIP: '',
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* @description placeholder information for view settings
|
||||
* @type {{overrideCSS: boolean}}
|
||||
*/
|
||||
export const viewsPlaceholder = {
|
||||
overrideStyles: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* @description placeholder information for httpSettings
|
||||
* @type {{onStart: {url: string, enabled: boolean}, onLoad: {url: string, enabled: boolean}, onPause: {url: string, enabled: boolean}, onFinish: {url: string, enabled: boolean}, onUpdate: {url: string, enabled: boolean}, onStop: {url: string, enabled: boolean}}}
|
||||
*/
|
||||
export const httpPlaceholder = {
|
||||
onLoad: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStart: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onUpdate: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onPause: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStop: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onFinish: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @description ontime utility variables
|
||||
* @type {[{name: string, description: string}, {name: string, description: string}, {name: string, description: string}, {name: string, description: string}, {name: string, description: string}, null, null]}
|
||||
*/
|
||||
export const ontimeVars = [
|
||||
{
|
||||
name: '$timer',
|
||||
description: 'Current running timer',
|
||||
},
|
||||
{
|
||||
name: '$title',
|
||||
description: 'Current title',
|
||||
},
|
||||
{
|
||||
name: '$presenter',
|
||||
description: 'Current timer',
|
||||
},
|
||||
{
|
||||
name: '$subtitle',
|
||||
description: 'Current subtitle',
|
||||
},
|
||||
{
|
||||
name: '$next-title',
|
||||
description: 'Next title',
|
||||
},
|
||||
{
|
||||
name: '$next-presenter',
|
||||
description: 'Next timer',
|
||||
},
|
||||
{
|
||||
name: '$next-subtitle',
|
||||
description: 'Next subtitle',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve application settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getSettings = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/settings`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate application settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const postSettings = async (data) => axios.post(`${ontimeURL}/settings`, data);
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve application info
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getInfo = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/info`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve view settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getView = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/views`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate view settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const postView = async (data) => axios.post(`${ontimeURL}/views`, data);
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve aliases
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getAliases = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/aliases`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate aliases
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const postAliases = async (data) => axios.post(`${ontimeURL}/aliases`, data);
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve user fields
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getUserFields = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/userfields`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate user fields
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const postUserFields = async (data) => axios.post(`${ontimeURL}/userfields`, data);
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve osc settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getOSC = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/osc`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate osc settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const postOSC = async (data) => axios.post(`${ontimeURL}/osc`, data);
|
||||
|
||||
/**
|
||||
* @description HTTP request to download db
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const downloadEvents = async () => {
|
||||
await axios({
|
||||
url: `${ontimeURL}/db`,
|
||||
method: 'GET',
|
||||
responseType: 'blob', // important
|
||||
}).then((response) => {
|
||||
const headerLine = response.headers['Content-Disposition'];
|
||||
let filename = 'events.json';
|
||||
|
||||
// try and get the filename from the response
|
||||
if (headerLine != null) {
|
||||
const startFileNameIndex = headerLine.indexOf('"') + 1;
|
||||
const endFileNameIndex = headerLine.lastIndexOf('"');
|
||||
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
|
||||
}
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/json' }));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to upload events db
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const uploadEvents = async (file, setProgress, options) => {
|
||||
const formData = new FormData();
|
||||
formData.append('userFile', file);
|
||||
const onlyEvents = options?.onlyEvents;
|
||||
await axios
|
||||
.post(`${ontimeURL}/db?onlyEvents=${onlyEvents}`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const complete = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
setProgress(complete);
|
||||
},
|
||||
})
|
||||
.then((response) => response.data.id);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to upload events
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const uploadEventsWithPath = async (filepath) =>
|
||||
axios.post(`${ontimeURL}/dbpath`, { path: filepath });
|
||||
@@ -0,0 +1,157 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { URLAliasType } from '../models/Alias.type';
|
||||
import { InfoType } from '../models/Info.types';
|
||||
import { OntimeSettingsType } from '../models/OntimeSettings.type';
|
||||
import { OscSettingsType } from '../models/OscSettings.type';
|
||||
import { UserFieldsType } from '../models/UserFields.type';
|
||||
import { ViewSettingsType } from '../models/ViewSettings.type';
|
||||
|
||||
import { ontimeURL } from './apiConstants';
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve application settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function getSettings(): Promise<OntimeSettingsType> {
|
||||
const res = await axios.get(`${ontimeURL}/settings`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate application settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function postSettings(data: OntimeSettingsType) {
|
||||
return axios.post(`${ontimeURL}/settings`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve application info
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function getInfo(): Promise<InfoType> {
|
||||
const res = await axios.get(`${ontimeURL}/info`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve view settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function getView(): Promise<ViewSettingsType> {
|
||||
const res = await axios.get(`${ontimeURL}/views`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate view settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function postView(data: ViewSettingsType) {
|
||||
return axios.post(`${ontimeURL}/views`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve aliases
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function getAliases(): Promise<URLAliasType[]> {
|
||||
const res = await axios.get(`${ontimeURL}/aliases`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate aliases
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function postAliases(data: URLAliasType[]) {
|
||||
return axios.post(`${ontimeURL}/aliases`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve user fields
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function getUserFields(): Promise<UserFieldsType> {
|
||||
const res = await axios.get(`${ontimeURL}/userfields`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate user fields
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function postUserFields(data: UserFieldsType) {
|
||||
return axios.post(`${ontimeURL}/userfields`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve osc settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function getOSC(): Promise<OscSettingsType> {
|
||||
const res = await axios.get(`${ontimeURL}/osc`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to mutate osc settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function postOSC(data: OscSettingsType) {
|
||||
return axios.post(`${ontimeURL}/osc`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description HTTP request to download db
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const downloadEvents = async () => {
|
||||
await axios({
|
||||
url: `${ontimeURL}/db`,
|
||||
method: 'GET',
|
||||
responseType: 'blob', // important
|
||||
}).then((response) => {
|
||||
const headerLine = response.headers['Content-Disposition'];
|
||||
let filename = 'events.json';
|
||||
|
||||
// try and get the filename from the response
|
||||
if (headerLine != null) {
|
||||
const startFileNameIndex = headerLine.indexOf('"') + 1;
|
||||
const endFileNameIndex = headerLine.lastIndexOf('"');
|
||||
filename = headerLine.substring(startFileNameIndex, endFileNameIndex);
|
||||
}
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/json' }));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description HTTP request to upload events db
|
||||
* @return {Promise}
|
||||
*/
|
||||
type UploadEventsOptions = {
|
||||
onlyEvents?: boolean;
|
||||
}
|
||||
export const uploadEvents = async (file: string, setProgress: (value: number) => void, options?: UploadEventsOptions) => {
|
||||
const formData = new FormData();
|
||||
formData.append('userFile', file);
|
||||
const onlyEvents = options?.onlyEvents;
|
||||
await axios
|
||||
.post(`${ontimeURL}/db?onlyEvents=${onlyEvents}`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const complete = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
setProgress(complete);
|
||||
},
|
||||
})
|
||||
.then((response) => response.data.id);
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { playbackURL } from './apiConstants';
|
||||
|
||||
/**
|
||||
* @description HTTP call to start current timer
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getStart = async () => axios.get(`${playbackURL}/start`);
|
||||
|
||||
/**
|
||||
* @description HTTP call to pause current timer
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getPause = async () => axios.get(`${playbackURL}/pause`);
|
||||
|
||||
/**
|
||||
* @description HTTP call to start roll mode
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getRoll = async () => axios.get(`${playbackURL}/roll`);
|
||||
|
||||
/**
|
||||
* @description HTTP call to skip to previous event
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getPrevious = async () => axios.get(`${playbackURL}/previous`);
|
||||
|
||||
/**
|
||||
* @description HTTP call to skip to next event
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getNext = async () => axios.get(`${playbackURL}/next`);
|
||||
|
||||
/**
|
||||
* @description HTTP call to unload current timer
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getUnload = async () => axios.get(`${playbackURL}/unload`);
|
||||
|
||||
/**
|
||||
* @description HTTP call to reload current timer
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const getReload = async () => axios.get(`${playbackURL}/reload`);
|
||||
@@ -1,3 +1,4 @@
|
||||
import { atom } from 'jotai';
|
||||
import { atomWithStorage, selectAtom } from 'jotai/utils';
|
||||
|
||||
export const eventSettingsAtom = atomWithStorage('ontime-eventSettings', {
|
||||
@@ -18,3 +19,5 @@ export const defaultPublicAtom = selectAtom(
|
||||
eventSettingsAtom,
|
||||
(settings) => settings.defaultPublic
|
||||
);
|
||||
|
||||
export const editorEventId = atom<string | null>(null);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/menu';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList, Tooltip } from '@chakra-ui/react';
|
||||
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';
|
||||
@@ -30,10 +28,7 @@ export default function ActionButtons(props: ActionButtonProps) {
|
||||
aria-label='Options'
|
||||
size='xs'
|
||||
icon={<FiPlus />}
|
||||
_expanded={{ bg: 'orange.300', color: 'white' }}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
backgroundColor='orange.200'
|
||||
color='orange.500'
|
||||
colorScheme='blue'
|
||||
/>
|
||||
</Tooltip>
|
||||
<MenuList style={menuStyle}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { IoCheckmarkSharp } from '@react-icons/all-files/io5/IoCheckmarkSharp';
|
||||
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
||||
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { FiUsers } from '@react-icons/all-files/fi/FiUsers';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Button, IconButton } from '@chakra-ui/button';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
@@ -7,8 +6,8 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogOverlay,
|
||||
} from '@chakra-ui/modal';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
Button, IconButton, Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { FiPower } from '@react-icons/all-files/fi/FiPower';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -24,7 +23,7 @@ export default function QuitIconBtn(props) {
|
||||
useEffect(() => {
|
||||
if (window.process?.type === 'renderer') {
|
||||
window.ipcRenderer.on('user-request-shutdown', () => {
|
||||
emitInfo('Shutdown request')
|
||||
emitInfo('Shutdown request');
|
||||
setIsOpen(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { IoTimeOutline } from '@react-icons/all-files/io5/IoTimeOutline';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -25,5 +24,5 @@ export default function RollIconBtn(props) {
|
||||
RollIconBtn.propTypes = {
|
||||
clickhandler: PropTypes.func,
|
||||
active: PropTypes.bool,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IconButtonProps } from '@chakra-ui/react';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IconButton, IconButtonProps, Tooltip } from '@chakra-ui/react';
|
||||
|
||||
interface TooltipActionBtnProps extends IconButtonProps {
|
||||
clickHandler: () => void;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function TooltipLoadingActionBtn(props) {
|
||||
@@ -10,7 +9,7 @@ export default function TooltipLoadingActionBtn(props) {
|
||||
const handleClick = useCallback(() => {
|
||||
setLoading(true);
|
||||
clickHandler();
|
||||
},[clickHandler, setLoading]);
|
||||
}, [clickHandler, setLoading]);
|
||||
|
||||
return (
|
||||
<Tooltip label={tooltip} shouldWrapChildren={loading}>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { IoStop } from '@react-icons/all-files/io5/IoStop';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class ErrorBoundary extends React.Component {
|
||||
render() {
|
||||
if (this.state.errorMessage) {
|
||||
return (
|
||||
<div className={style.errorContainer}>
|
||||
<div className={style.errorContainer} data-testid="error-container">
|
||||
<div>
|
||||
<p className={style.error}>:/</p>
|
||||
<p>Something went wrong</p>
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { useCallback, useContext } from 'react';
|
||||
import EditableTimer from 'common/components/input/EditableTimer';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { LoggingContext } from '../../context/LoggingContext';
|
||||
import { validateTimes } from '../../utils/entryValidator';
|
||||
|
||||
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 = useCallback(
|
||||
() => (entry, val) => {
|
||||
if (val == null || timeStart == null || timeEnd == null) return true;
|
||||
if (timeStart === 0) return true;
|
||||
|
||||
let start = timeStart;
|
||||
let end = timeEnd;
|
||||
if (entry === 'timeStart') {
|
||||
start = val;
|
||||
} else if (entry === 'timeEnd') {
|
||||
end = val;
|
||||
} else return entry === 'durationOverride';
|
||||
|
||||
const valid = validateTimes(start, end);
|
||||
// give warning but not enforce validation
|
||||
if (!valid.value) {
|
||||
emitWarning(`Time Input Warning: ${valid.catch}`);
|
||||
}
|
||||
return valid.value;
|
||||
},
|
||||
[emitWarning, timeEnd, timeStart]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
EventTimes.propTypes = {
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
import { useCallback, useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { LoggingContext } from '../../context/LoggingContext';
|
||||
import { validateTimes } from '../../utils/entryValidator';
|
||||
|
||||
import Times from './Times';
|
||||
import TimesDelayed from './TimesDelayed';
|
||||
|
||||
export default function EventTimesVertical(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 = useCallback(
|
||||
() => (entry, val) => {
|
||||
if (val == null || timeStart == null || timeEnd == null) return true;
|
||||
if (timeStart === 0) return true;
|
||||
|
||||
let start = timeStart;
|
||||
let end = timeEnd;
|
||||
if (entry === 'timeStart') {
|
||||
start = val;
|
||||
} else if (entry === 'timeEnd') {
|
||||
end = val;
|
||||
} else return entry === 'durationOverride';
|
||||
|
||||
const valid = validateTimes(start, end);
|
||||
// give warning but not enforce validation
|
||||
if (!valid.value) {
|
||||
emitWarning(`Time Input Warning: ${valid.catch}`);
|
||||
}
|
||||
return valid.value;
|
||||
},
|
||||
[emitWarning, timeEnd, timeStart]
|
||||
);
|
||||
|
||||
return delay != null && delay !== 0 ? (
|
||||
<TimesDelayed
|
||||
handleValidate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
delay={delay}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
) : (
|
||||
<Times
|
||||
handleValidate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
timeStart={timeStart}
|
||||
timeEnd={timeEnd}
|
||||
duration={duration}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
EventTimesVertical.propTypes = {
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import EditableTimer from '../input/EditableTimer';
|
||||
|
||||
import style from './Times.module.scss';
|
||||
|
||||
export default function Times(props) {
|
||||
const { handleValidate, actionHandler, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.label}>Start</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>End</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Times.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
.label {
|
||||
font-size: 0.75em;
|
||||
color: $label-gray;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { stringFromMillis } from '../../utils/time';
|
||||
import EditableTimer from '../input/EditableTimer';
|
||||
|
||||
import style from './Times.module.scss';
|
||||
|
||||
export default function TimesDelayed(props) {
|
||||
const { handleValidate, actionHandler, delay, timeStart, timeEnd, duration, previousEnd } = props;
|
||||
|
||||
const scheduledStart = stringFromMillis(timeStart, false);
|
||||
const scheduledEnd = stringFromMillis(timeEnd, false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.label}>
|
||||
Start <span>{scheduledStart}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeStart'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeStart}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>
|
||||
End <span>{scheduledEnd}</span>
|
||||
</span>
|
||||
<EditableTimer
|
||||
name='timeEnd'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
<span className={style.label}>Duration</span>
|
||||
<EditableTimer
|
||||
name='durationOverride'
|
||||
validate={handleValidate}
|
||||
actionHandler={actionHandler}
|
||||
time={duration}
|
||||
delay={0}
|
||||
previousEnd={previousEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
TimesDelayed.propTypes = {
|
||||
handleValidate: PropTypes.func.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
delay: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
duration: PropTypes.number,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
input[type="color"] {
|
||||
appearance: none;
|
||||
background-color: $action-blue;
|
||||
cursor: pointer;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Input } from '@chakra-ui/react';
|
||||
|
||||
import style from './ColourInput.module.scss';
|
||||
|
||||
interface ColourInputProps {
|
||||
value: string;
|
||||
handleChange: (newValue: string) => void;
|
||||
}
|
||||
|
||||
export default function ColourInput(props: ColourInputProps) {
|
||||
const { value, handleChange } = props;
|
||||
return (
|
||||
<Input
|
||||
size='sm'
|
||||
variant='filled'
|
||||
className={style.colourInput}
|
||||
type='color'
|
||||
value={value}
|
||||
onChange={(event) => handleChange(event.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,62 +1,86 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Input } from '@chakra-ui/react';
|
||||
import { clamp } from 'common/utils/math';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { clamp } from '../../utils/math';
|
||||
|
||||
import style from './TimeInput.module.css';
|
||||
import style from './TimeInput.module.scss';
|
||||
|
||||
const inputProps = {
|
||||
width: 20,
|
||||
fontWeight: 400,
|
||||
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||
backgroundColor: 'rgba(255,255,255,0.13)',
|
||||
color: '#fff',
|
||||
border: '1px solid #ecc94b55',
|
||||
borderRadius: '8px',
|
||||
variant: 'filled',
|
||||
borderRadius: '3px',
|
||||
placeholder: '-',
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
};
|
||||
|
||||
export default function DelayInput(props) {
|
||||
const { actionHandler, value } = props;
|
||||
const { submitHandler, value } = props;
|
||||
const [_value, setValue] = useState(value);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (value == null) return;
|
||||
setValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
/**
|
||||
* @description Prepare delay value for update
|
||||
* @param {string} value string to be parsed
|
||||
*/
|
||||
const validate = useCallback(
|
||||
(newValue) => {
|
||||
if (newValue === value) return;
|
||||
if (newValue === '') setValue(0);
|
||||
const delayValue = clamp(Number(newValue), -60, 60);
|
||||
|
||||
// convert to ms and updates
|
||||
const msVal = clamp(newValue, -60, 60) * 60000;
|
||||
actionHandler('update', { field: 'duration', value: msVal });
|
||||
if (delayValue === value) return;
|
||||
setValue(delayValue);
|
||||
|
||||
submitHandler(delayValue);
|
||||
},
|
||||
[actionHandler, value]
|
||||
[submitHandler, value]
|
||||
);
|
||||
|
||||
const labelText = `minutes ${value >= 0 ? 'delayed' : 'ahead'}`;
|
||||
/**
|
||||
* @description Handles common keys for submit and cancel
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
const onKeyDownHandler = useCallback((event) => {
|
||||
if (event.key === 'Enter') {
|
||||
inputRef.current.blur();
|
||||
validate(event.target.value);
|
||||
} else if (event.key === 'Escape') {
|
||||
inputRef.current.blur();
|
||||
setValue(value);
|
||||
}
|
||||
}, [validate, value]);
|
||||
|
||||
const labelText = `${Math.abs(value) > 1 ? 'minutes' : 'minute'} ${
|
||||
value >= 0 ? 'delayed' : 'ahead'
|
||||
}`;
|
||||
|
||||
return (
|
||||
<div className={style.timeInput}>
|
||||
<Editable
|
||||
<div className={style.delayInput}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
data-testid='delay-input'
|
||||
className={style.inputField}
|
||||
{...inputProps}
|
||||
value={_value}
|
||||
onChange={(v) => setValue(v)}
|
||||
onSubmit={(v) => handleSubmit(v)}
|
||||
>
|
||||
<EditablePreview />
|
||||
<EditableInput type='number' min='-60' max='60' />
|
||||
</Editable>
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onBlur={() => setValue(value)}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
type='number'
|
||||
/>
|
||||
<span className={style.label}>{labelText}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DelayInput.propTypes = {
|
||||
actionHandler: PropTypes.func,
|
||||
submitHandler: PropTypes.func,
|
||||
value: PropTypes.number,
|
||||
};
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import style from './EditableText.module.scss';
|
||||
|
||||
export default function EditableText(props) {
|
||||
const { label, defaultValue, placeholder, submitHandler, maxchar = 40, ...rest } = props;
|
||||
const [text, setText] = useState(defaultValue || '');
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValue == null) setText('');
|
||||
else setText(defaultValue);
|
||||
}, [defaultValue]);
|
||||
|
||||
const handleSubmit = useCallback((submittedVal) => {
|
||||
// No need to update if it hasnt changed
|
||||
if (submittedVal === defaultValue) return;
|
||||
// submit a cleaned up version of the string
|
||||
const cleanVal = submittedVal.trim();
|
||||
submitHandler(cleanVal);
|
||||
|
||||
if (cleanVal !== submittedVal) {
|
||||
setText(cleanVal);
|
||||
}
|
||||
},[defaultValue, submitHandler]);
|
||||
|
||||
const handleChange = useCallback((val) => {
|
||||
if (val.length < maxchar) setText(val);
|
||||
},[maxchar]);
|
||||
|
||||
return (
|
||||
<div className={style.block}>
|
||||
<span className={style.title}>{label}</span>
|
||||
<Editable
|
||||
onChange={(v) => handleChange(v)}
|
||||
onSubmit={(v) => handleSubmit(v)}
|
||||
value={text}
|
||||
placeholder={placeholder}
|
||||
className={style.inline}
|
||||
{...rest}
|
||||
>
|
||||
<EditablePreview className={text === '' ? style.preview : ''} />
|
||||
<EditableInput />
|
||||
</Editable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EditableText.propTypes = {
|
||||
label: PropTypes.string,
|
||||
defaultValue: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
submitHandler: PropTypes.func.isRequired,
|
||||
maxchar: PropTypes.number,
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
@use '../../../theme/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: $bg-gray-500;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { LoggingContext } from '../../context/LoggingContext';
|
||||
import { forgivingStringToMillis } from '../../utils/dateConfig';
|
||||
import { stringFromMillis } from '../../utils/time';
|
||||
|
||||
import style from './EditableTimer.module.scss';
|
||||
|
||||
export default function EditableTimer(props) {
|
||||
const { name, actionHandler, time = 0, delay, validate, previousEnd } = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
|
||||
const handleSubmit = useCallback((value) => {
|
||||
// Check if there is anything there
|
||||
if (value === '') return false;
|
||||
|
||||
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 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);
|
||||
} else {
|
||||
// convert entered value to milliseconds
|
||||
newValMillis = forgivingStringToMillis(value);
|
||||
}
|
||||
|
||||
// Time now and time submittedVal
|
||||
const originalMillis = time + delay;
|
||||
|
||||
// check if time is different from before
|
||||
if (newValMillis === originalMillis) return false;
|
||||
|
||||
// validate with parent
|
||||
if (!validate(name, newValMillis)) return false;
|
||||
|
||||
// update entry
|
||||
actionHandler('update', { field: name, value: newValMillis });
|
||||
|
||||
return true;
|
||||
},[actionHandler, delay, name, previousEnd, time, validate]);
|
||||
|
||||
// prepare time fields
|
||||
const validateValue = useCallback((value) => {
|
||||
const success = handleSubmit(value);
|
||||
if (success) {
|
||||
const ms = forgivingStringToMillis(value);
|
||||
setValue(stringFromMillis(ms + delay));
|
||||
} else {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
}
|
||||
},[delay, handleSubmit, time]);
|
||||
|
||||
useEffect(() => {
|
||||
if (time == null) return;
|
||||
try {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
} catch (error) {
|
||||
emitError(`Unable to parse date: ${error.text}`);
|
||||
}
|
||||
}, [time, delay, emitError]);
|
||||
|
||||
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={isDelayed ? style.delayedEditable : style.editable}
|
||||
>
|
||||
<EditablePreview />
|
||||
<EditableInput type='text' placeholder='--:--:--' data-testid='editable-timer-input' />
|
||||
</Editable>
|
||||
);
|
||||
}
|
||||
|
||||
EditableTimer.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
time: PropTypes.number,
|
||||
delay: PropTypes.number,
|
||||
validate: PropTypes.func.isRequired,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
@use '../../../theme/main'as *;
|
||||
|
||||
.editable,
|
||||
.delayedEditable {
|
||||
background-color: $input-bg;
|
||||
border: $input-border;
|
||||
width: 6.5em;
|
||||
letter-spacing: 1px;
|
||||
height: fit-content;
|
||||
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.delayedEditable {
|
||||
border: $input-delayed-border;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Input, Textarea } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function TextInput(props) {
|
||||
const { isTextArea, size = 'sm', field, initialText = '', submitHandler } = props;
|
||||
const inputRef = useRef(null);
|
||||
const [text, setText] = useState(initialText);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof initialText === 'undefined') {
|
||||
setText('');
|
||||
} else {
|
||||
setText(initialText);
|
||||
}
|
||||
}, [initialText]);
|
||||
|
||||
/**
|
||||
* @description Handles Input value change
|
||||
* @param {string} newValue
|
||||
*/
|
||||
const handleChange = useCallback(
|
||||
(newValue) => {
|
||||
if (newValue !== text) {
|
||||
setText(newValue);
|
||||
}
|
||||
},
|
||||
[text]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Handles submit events
|
||||
* @param {string} valueToSubmit
|
||||
*/
|
||||
const handleSubmit = useCallback(
|
||||
(valueToSubmit) => {
|
||||
// No need to update if it hasn't changed
|
||||
if (valueToSubmit === initialText) {
|
||||
return;
|
||||
}
|
||||
const cleanVal = valueToSubmit.trim();
|
||||
submitHandler(field, cleanVal);
|
||||
|
||||
if (cleanVal !== valueToSubmit) {
|
||||
setText(cleanVal);
|
||||
}
|
||||
},
|
||||
[field, initialText, submitHandler]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Resets input value to given
|
||||
*/
|
||||
const resetValue = useCallback(async () => {
|
||||
setText(initialText);
|
||||
},[initialText])
|
||||
|
||||
/**
|
||||
* @description Handles common keys for submit and cancel
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
const keyHandler = useCallback(
|
||||
(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
resetValue();
|
||||
} else if (event.key === 'Enter') {
|
||||
if (!isTextArea) {
|
||||
handleSubmit(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
[resetValue, isTextArea, handleSubmit, text]
|
||||
);
|
||||
|
||||
return isTextArea ? (
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
size={size}
|
||||
variant='filled'
|
||||
value={text}
|
||||
onChange={(event) => handleChange(event.target.value)}
|
||||
onBlur={(event) => handleSubmit(event.target.value)}
|
||||
onKeyDown={(event) => keyHandler(event)}
|
||||
data-testid='input-textarea'
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
size={size}
|
||||
variant='filled'
|
||||
value={text}
|
||||
onChange={(event) => handleChange(event.target.value)}
|
||||
onBlur={(event) => handleSubmit(event.target.value)}
|
||||
onKeyDown={(event) => keyHandler(event)}
|
||||
data-testid='input-textfield'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
TextInput.propTypes = {
|
||||
isTextArea: PropTypes.bool,
|
||||
size: PropTypes.string,
|
||||
field: PropTypes.string.isRequired,
|
||||
initialText: PropTypes.string,
|
||||
submitHandler: PropTypes.func,
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { IconButton, Input, InputGroup, InputLeftElement } from '@chakra-ui/react';
|
||||
import { IoLink } from '@react-icons/all-files/io5/IoLink';
|
||||
import { LoggingContext } from 'common/context/LoggingContext';
|
||||
import { forgivingStringToMillis } from 'common/utils/dateConfig';
|
||||
import { stringFromMillis } from 'common/utils/time';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import style from './TimeInput.module.scss';
|
||||
|
||||
export default function TimeInput(props) {
|
||||
const { name, submitHandler, time = 0, delay, placeholder, validationHandler, previousEnd } = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const inputRef = useRef(null);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
/**
|
||||
* @description Resets input value to given
|
||||
*/
|
||||
const resetValue = useCallback(() => {
|
||||
// Todo: check if change is necessary
|
||||
try {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
} catch (error) {
|
||||
emitError(`Unable to parse date: ${error.text}`);
|
||||
}
|
||||
}, [delay, emitError, time]);
|
||||
|
||||
/**
|
||||
* @description Selects input text on focus
|
||||
*/
|
||||
const handleFocus = useCallback(() => {
|
||||
inputRef.current.select();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* @description Submit handler
|
||||
* @param {string} newValue
|
||||
*/
|
||||
const handleSubmit = useCallback(
|
||||
(newValue) => {
|
||||
// Check if there is anything there
|
||||
if (newValue === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let newValMillis = 0;
|
||||
|
||||
// check for known aliases
|
||||
if (newValue === 'p' || newValue === 'prev' || newValue === 'previous') {
|
||||
// string to pass should be the time of the end before
|
||||
if (previousEnd != null) {
|
||||
newValMillis = previousEnd;
|
||||
}
|
||||
} else if (
|
||||
newValue.startsWith('+') ||
|
||||
newValue.startsWith('p+') ||
|
||||
newValue.startsWith('p +')
|
||||
) {
|
||||
// string to pass should add to the end before
|
||||
const val = newValue.substring(1);
|
||||
newValMillis = previousEnd + forgivingStringToMillis(val);
|
||||
} else {
|
||||
// convert entered value to milliseconds
|
||||
newValMillis = forgivingStringToMillis(newValue);
|
||||
}
|
||||
|
||||
// Time now and time submittedVal
|
||||
const originalMillis = time + delay;
|
||||
|
||||
// check if time is different from before
|
||||
if (newValMillis === originalMillis) return false;
|
||||
|
||||
// validate with parent
|
||||
if (!validationHandler(name, newValMillis)) return false;
|
||||
|
||||
// update entry
|
||||
submitHandler(name, newValMillis);
|
||||
|
||||
return true;
|
||||
},
|
||||
[delay, name, previousEnd, submitHandler, time, validationHandler]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Prepare time fields
|
||||
* @param {string} value string to be parsed
|
||||
*/
|
||||
const validateAndSubmit = useCallback(
|
||||
(newValue) => {
|
||||
const success = handleSubmit(newValue);
|
||||
if (success) {
|
||||
const ms = forgivingStringToMillis(newValue);
|
||||
setValue(stringFromMillis(ms + delay));
|
||||
} else {
|
||||
resetValue();
|
||||
}
|
||||
},
|
||||
[delay, handleSubmit, resetValue]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Handles common keys for submit and cancel
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
const onKeyDownHandler = useCallback(
|
||||
(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
inputRef.current.blur();
|
||||
validateAndSubmit(event.target.value);
|
||||
} else if (event.key === 'Tab') {
|
||||
validateAndSubmit(event.target.value);
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
inputRef.current.blur();
|
||||
resetValue();
|
||||
}
|
||||
},
|
||||
[resetValue, validateAndSubmit]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (time == null) return;
|
||||
resetValue();
|
||||
}, [emitError, resetValue, time]);
|
||||
|
||||
const isDelayed = delay != null && delay !== 0;
|
||||
|
||||
return (
|
||||
<InputGroup size='sm' className={`${style.timeInput} ${isDelayed ? style.delayed : ''}`}>
|
||||
<InputLeftElement width='fit-content'>
|
||||
<IconButton
|
||||
size='sm'
|
||||
icon={<IoLink style={{ transform: 'rotate(-45deg)' }} />}
|
||||
aria-label='automate'
|
||||
colorScheme='blue'
|
||||
style={{ borderRadius: '2px', width: 'min-content' }}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
data-testid='time-input'
|
||||
className={style.inputField}
|
||||
type='text'
|
||||
placeholder={placeholder}
|
||||
variant='filled'
|
||||
onFocus={handleFocus}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onBlur={resetValue}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
value={value}
|
||||
maxLength={8}
|
||||
/>
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
||||
|
||||
TimeInput.propTypes = {
|
||||
name: PropTypes.string,
|
||||
submitHandler: PropTypes.func,
|
||||
time: PropTypes.number,
|
||||
delay: PropTypes.number,
|
||||
placeholder: PropTypes.string,
|
||||
validationHandler: PropTypes.func,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
.timeInput {
|
||||
display: flex;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-left: 0.8em;
|
||||
align-self: center;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
@mixin input-field {
|
||||
border: 1px solid transparent;
|
||||
background-color: $input-bg;
|
||||
font-size: 1em;
|
||||
letter-spacing: 1px;
|
||||
|
||||
&:hover {
|
||||
background-color: $input-hover-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.timeInput {
|
||||
width: fit-content !important;
|
||||
|
||||
.inputField {
|
||||
@include input-field;
|
||||
width: 7.5em;
|
||||
padding: 0 0 0 2.6em;
|
||||
}
|
||||
|
||||
&.delayed {
|
||||
.inputField {
|
||||
border: $input-delayed-border;
|
||||
background-color: $input-bg-delayed;
|
||||
|
||||
&:hover {
|
||||
background-color: $input-hover-bg-delayed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delayInput {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.inputField {
|
||||
@include input-field;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-left: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import TextInput from '../TextInput';
|
||||
|
||||
describe('TextInput component', () => {
|
||||
describe('when given props', () => {
|
||||
it('renders correctly', () => {
|
||||
const testField = 'test';
|
||||
const testText = 'Test 123';
|
||||
render(<TextInput field={testField} initialText={testText} />);
|
||||
|
||||
const input = screen.getByTestId('input-textfield');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveValue(testText);
|
||||
});
|
||||
|
||||
it('Handles renders as textarea', () => {
|
||||
const testField = 'test';
|
||||
const testText = 'Test 123';
|
||||
render(<TextInput field={testField} initialText={testText} isTextArea />);
|
||||
|
||||
const input = screen.getByTestId('input-textarea');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveValue(testText);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on status change', () => {
|
||||
it('calls submitHandler on new value', async () => {
|
||||
const testField = 'test';
|
||||
const testText = 'Test 123';
|
||||
const myTypedString = '456';
|
||||
const expectedString = testText + myTypedString;
|
||||
const submitHandler = vi.fn();
|
||||
render(<TextInput field={testField} initialText={testText} submitHandler={submitHandler} />);
|
||||
|
||||
const input = screen.getByTestId('input-textfield');
|
||||
|
||||
// submit without changing value
|
||||
await userEvent.type(input, '{enter}');
|
||||
expect(submitHandler).not.toHaveBeenCalled();
|
||||
|
||||
// on new value we can submit
|
||||
await userEvent.type(input, myTypedString);
|
||||
expect(input).toHaveValue(expectedString);
|
||||
await userEvent.type(input, '{enter}');
|
||||
expect(submitHandler).toHaveBeenCalledWith(testField, expectedString);
|
||||
});
|
||||
|
||||
it('cleans value before submitting', async () => {
|
||||
const testField = 'test';
|
||||
const myTypedString = ' 456 ';
|
||||
const expectedString = '456';
|
||||
const submitHandler = vi.fn();
|
||||
render(<TextInput field={testField} submitHandler={submitHandler} />);
|
||||
|
||||
const input = screen.getByTestId('input-textfield');
|
||||
|
||||
// on new value we can submit
|
||||
await userEvent.type(input, myTypedString);
|
||||
expect(input).toHaveValue(myTypedString);
|
||||
await userEvent.type(input, '{enter}');
|
||||
expect(submitHandler).toHaveBeenCalledWith(testField, expectedString);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles edge cases', () => {
|
||||
it('handles number values', () => {
|
||||
const testField = 'test';
|
||||
const testText = 123;
|
||||
render(<TextInput field={testField} initialText={testText} />);
|
||||
const input = screen.getByTestId('input-textfield');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveValue(`${testText}`);
|
||||
});
|
||||
it('handles null value', () => {
|
||||
const testField = 'test';
|
||||
const testText = null;
|
||||
const expected = '';
|
||||
render(<TextInput field={testField} initialText={testText} />);
|
||||
const input = screen.getByTestId('input-textfield');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveValue(expected);
|
||||
});
|
||||
it('handles undefined value', () => {
|
||||
const testField = 'test';
|
||||
const expected = '';
|
||||
render(<TextInput field={testField} />);
|
||||
const input = screen.getByTestId('input-textfield');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveValue(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Image } from '@chakra-ui/react';
|
||||
import { IconButton, Image } from '@chakra-ui/react';
|
||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
||||
import navlogo from 'assets/images/logos/LOGO-72.png';
|
||||
|
||||
@@ -50,6 +50,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.entry .skip {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.backstage-indicator {
|
||||
color: var(--accent-color-override, $accent-color);
|
||||
margin-left: auto;
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useInterval } from 'common/hooks/useInterval';
|
||||
import { OntimeEvent } from 'common/models/EventTypes';
|
||||
|
||||
import { OntimeEvent } from '../../application-types/event';
|
||||
import Empty from '../state/Empty';
|
||||
|
||||
import TodayItem from './TodayItem';
|
||||
|
||||
import style from './Paginator.module.scss';
|
||||
import './Paginator.scss';
|
||||
|
||||
interface PaginatorProps {
|
||||
events: OntimeEvent[];
|
||||
@@ -80,7 +80,7 @@ export default function Paginator(props: PaginatorProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={style.entries}>
|
||||
<div className='paginator entries'>
|
||||
{page.map((e) => {
|
||||
if (e.id === selectedId) selectedState = 1;
|
||||
else if (selectedState === 1) selectedState = 2;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { formatTime } from '../../utils/time';
|
||||
|
||||
import './Paginator.scss';
|
||||
|
||||
interface TodayItemProps {
|
||||
selected: number;
|
||||
timeStart: number;
|
||||
timeEnd: number;
|
||||
title: string;
|
||||
backstageEvent: boolean;
|
||||
colour: string;
|
||||
skip: boolean;
|
||||
}
|
||||
|
||||
export default function TodayItem(props: TodayItemProps) {
|
||||
const { selected, timeStart, timeEnd, title, backstageEvent, colour, skip } = props;
|
||||
|
||||
// Format timers
|
||||
const start = formatTime(timeStart, { format: 'hh:mm' });
|
||||
const end = formatTime(timeEnd, { format: 'hh:mm' });
|
||||
|
||||
// user colours
|
||||
const userColour = colour !== '' ? colour : 'transparent';
|
||||
|
||||
let selectStyle = 'entry--past';
|
||||
if (selected === 1) selectStyle = 'entry--now';
|
||||
else if (selected === 2) selectStyle = 'entry--future';
|
||||
return (
|
||||
<div className={`entry ${selectStyle} ${skip ? 'skip': ''}}`} style={{ borderLeft: `4px solid ${userColour}` }}>
|
||||
<div className='entry-times'>
|
||||
{`${start} · ${end}`}
|
||||
</div>
|
||||
<div className='entry-title'>{title}</div>
|
||||
{backstageEvent && <div className='backstage-indicator' />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { HStack, PinInput, PinInputField } from '@chakra-ui/react';
|
||||
import { HStack, IconButton, PinInput, PinInputField } from '@chakra-ui/react';
|
||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeEvent, useCallback, useContext, useRef, useState } from 'react';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { formatTime } from '../../utils/time';
|
||||
|
||||
import style from './Paginator.module.scss';
|
||||
|
||||
interface TodayItemProps {
|
||||
selected: number;
|
||||
timeStart: number;
|
||||
timeEnd: number;
|
||||
title: string;
|
||||
backstageEvent: boolean;
|
||||
colour: string;
|
||||
skip: boolean;
|
||||
}
|
||||
|
||||
// Todo: apply skip CSS and selector
|
||||
export default function TodayItem(props: TodayItemProps) {
|
||||
// @ts-ignore
|
||||
const { selected, timeStart, timeEnd, title, backstageEvent, colour, skip } = props;
|
||||
|
||||
// Format timers
|
||||
const start = formatTime(timeStart, { format: 'hh:mm' });
|
||||
const end = formatTime(timeEnd, { format: 'hh:mm' });
|
||||
|
||||
// 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} style={{ borderLeft: `4px solid ${userColour}` }}>
|
||||
<div className={`${style.entryTimes} ${backstageEvent ? style.backstage : ''}`}>
|
||||
{`${start} · ${end}`}
|
||||
</div>
|
||||
<div className={style.entryTitle}>{title}</div>
|
||||
{backstageEvent && <div className={style.backstageInd} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TodayItem.propTypes = {
|
||||
selected: PropTypes.number,
|
||||
timeStart: PropTypes.number,
|
||||
timeEnd: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
backstageEvent: PropTypes.bool,
|
||||
colour: PropTypes.string,
|
||||
};
|
||||
@@ -1,8 +1,6 @@
|
||||
import { createContext, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { APP_SETTINGS } from '../api/apiConstants';
|
||||
import { getSettings } from '../api/ontimeApi';
|
||||
import { useFetch } from '../hooks/useFetch';
|
||||
import useSettings from '../hooks-query/useSettings';
|
||||
|
||||
export const AppContext = createContext({
|
||||
auth: false,
|
||||
@@ -13,7 +11,7 @@ export const AppContext = createContext({
|
||||
|
||||
export const AppContextProvider = ({ children }) => {
|
||||
const [auth, setAuth] = useState(true);
|
||||
const { data } = useFetch(APP_SETTINGS, getSettings);
|
||||
const { data } = useSettings();
|
||||
|
||||
useEffect(() => {
|
||||
if (data == null) return;
|
||||
@@ -22,7 +20,7 @@ export const AppContextProvider = ({ children }) => {
|
||||
if (previousEntry === data?.pinCode) {
|
||||
setAuth(true);
|
||||
} else {
|
||||
sessionStorage.removeItem('ontime-entry')
|
||||
sessionStorage.removeItem('ontime-entry');
|
||||
}
|
||||
} else if (data?.pinCode == null || data?.pinCode === '') {
|
||||
setAuth(true);
|
||||
@@ -49,7 +47,7 @@ export const AppContextProvider = ({ children }) => {
|
||||
setAuth(correct);
|
||||
return correct;
|
||||
},
|
||||
[data]
|
||||
[data],
|
||||
);
|
||||
|
||||
return <AppContext.Provider value={{ auth, validate }}>{children}</AppContext.Provider>;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { ALIASES } from '../api/apiConstants';
|
||||
import { getAliases } from '../api/ontimeApi';
|
||||
|
||||
export default function useAliases() {
|
||||
const {
|
||||
data,
|
||||
status,
|
||||
isError,
|
||||
refetch,
|
||||
} = useQuery(ALIASES, getAliases, { placeholderData: [] });
|
||||
|
||||
return { data, status, isError, refetch };
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { EVENT_TABLE } from '../api/apiConstants';
|
||||
import { fetchEvent } from '../api/eventApi';
|
||||
import { eventDataPlaceholder } from '../models/EventData.type';
|
||||
|
||||
export default function useEvent() {
|
||||
const {
|
||||
data,
|
||||
status,
|
||||
isError,
|
||||
refetch,
|
||||
} = useQuery(EVENT_TABLE, fetchEvent, { placeholderData: eventDataPlaceholder });
|
||||
|
||||
return { data, status, isError, refetch };
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { EVENTS_TABLE } from '../api/apiConstants';
|
||||
import { fetchAllEvents } from '../api/eventsApi';
|
||||
|
||||
export default function useEventsList() {
|
||||
const {
|
||||
data,
|
||||
status,
|
||||
isError,
|
||||
refetch,
|
||||
} = useQuery(EVENTS_TABLE, fetchAllEvents, { placeholderData: [] });
|
||||
|
||||
return { data, status, isError, refetch };
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { APP_INFO } from '../api/apiConstants';
|
||||
import { getInfo } from '../api/ontimeApi';
|
||||
import { ontimePlaceholderInfo } from '../models/Info.types';
|
||||
|
||||
export default function useInfo() {
|
||||
const {
|
||||
data,
|
||||
status,
|
||||
isError,
|
||||
refetch,
|
||||
} = useQuery(APP_INFO, getInfo, { placeholderData: ontimePlaceholderInfo });
|
||||
|
||||
return { data, status, isError, refetch };
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { OSC_SETTINGS } from '../api/apiConstants';
|
||||
import { getOSC } from '../api/ontimeApi';
|
||||
import { oscPlaceholderSettings } from '../models/OscSettings.type';
|
||||
|
||||
export default function useOscSettings() {
|
||||
const {
|
||||
data,
|
||||
status,
|
||||
isError,
|
||||
refetch,
|
||||
} = useQuery(OSC_SETTINGS, getOSC, { placeholderData: oscPlaceholderSettings });
|
||||
|
||||
return { data, status, isError, refetch };
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { APP_SETTINGS } from '../api/apiConstants';
|
||||
import { getSettings } from '../api/ontimeApi';
|
||||
import { ontimePlaceholderSettings } from '../models/OntimeSettings.type';
|
||||
|
||||
export default function useSettings() {
|
||||
const {
|
||||
data,
|
||||
status,
|
||||
isError,
|
||||
refetch,
|
||||
} = useQuery(APP_SETTINGS, getSettings, { placeholderData: ontimePlaceholderSettings });
|
||||
|
||||
return { data, status, isError, refetch };
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { USERFIELDS } from '../api/apiConstants';
|
||||
import { getUserFields } from '../api/ontimeApi';
|
||||
import { userFieldsPlaceholder } from '../models/UserFields.type';
|
||||
|
||||
export default function useUserFields() {
|
||||
const {
|
||||
data,
|
||||
status,
|
||||
isError,
|
||||
refetch,
|
||||
} = useQuery(USERFIELDS, getUserFields, { placeholderData: userFieldsPlaceholder });
|
||||
|
||||
return { data, status, isError, refetch };
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { VIEW_SETTINGS } from '../api/apiConstants';
|
||||
import { getView } from '../api/ontimeApi';
|
||||
import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
|
||||
|
||||
export default function useViewSettings() {
|
||||
const {
|
||||
data,
|
||||
status,
|
||||
isError,
|
||||
refetch,
|
||||
} = useQuery(VIEW_SETTINGS, getView, { placeholderData: viewsSettingsPlaceholder });
|
||||
|
||||
return { data, status, isError, refetch };
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { EVENTS_TABLE, EVENTS_TABLE_KEY } from '../api/apiConstants';
|
||||
import {
|
||||
requestApplyDelay,
|
||||
requestDelete,
|
||||
requestDeleteAll,
|
||||
requestPostEvent,
|
||||
requestPutEvent,
|
||||
requestReorderEvent,
|
||||
} from '../api/eventsApi';
|
||||
import { LoggingContext } from '../context/LoggingContext';
|
||||
|
||||
/**
|
||||
* @description Set of utilities for events
|
||||
*/
|
||||
export const useEventAction = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
|
||||
/**
|
||||
* @description Calls mutation to add new event
|
||||
* @private
|
||||
*/
|
||||
const _addEventMutation = useMutation(requestPostEvent, {
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(EVENTS_TABLE);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Adds new event to list
|
||||
* @param {object} event - Event to be added
|
||||
* @param {object} [options] - Event options
|
||||
*/
|
||||
const addEvent = useCallback(
|
||||
async (event, options) => {
|
||||
const newEvent = { ...event };
|
||||
|
||||
// ************* CHECK OPTIONS
|
||||
// there is an option to pass an index of an array to use as start time
|
||||
if (typeof options?.startIsLastEnd !== 'undefined') {
|
||||
const events = queryClient.getQueryData(EVENTS_TABLE);
|
||||
const previousEvent = events.find((event) => event.id === options.startIsLastEnd);
|
||||
newEvent.timeStart = previousEvent.timeEnd || 0;
|
||||
}
|
||||
|
||||
// hard coding duration value to be as expected for now
|
||||
// this until timeOptions gets implemented
|
||||
if (newEvent.type === 'event') {
|
||||
newEvent.duration = Math.max(0, newEvent.timeEnd - newEvent.timeStart) || 0;
|
||||
}
|
||||
|
||||
try {
|
||||
await _addEventMutation.mutateAsync(newEvent);
|
||||
} catch (error) {
|
||||
emitError(`Error fetching data: ${error.message}`);
|
||||
}
|
||||
},
|
||||
[_addEventMutation, emitError],
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Calls mutation to update existing event
|
||||
* @private
|
||||
*/
|
||||
const _updateEventMutation = useMutation(requestPutEvent, {
|
||||
// we optimistically update here
|
||||
onMutate: async (newEvent) => {
|
||||
// cancel ongoing queries
|
||||
await queryClient.cancelQueries([EVENTS_TABLE_KEY, newEvent.id]);
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousEvent = queryClient.getQueryData([EVENTS_TABLE_KEY, newEvent.id]);
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData([EVENTS_TABLE_KEY, newEvent.id], newEvent);
|
||||
|
||||
// Return a context with the previous and new events
|
||||
return { previousEvent, newEvent };
|
||||
},
|
||||
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
onError: (error, newEvent, context) => {
|
||||
queryClient.setQueryData([EVENTS_TABLE_KEY, context.newEvent.id], context.previousEvent);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
onSettled: async (newEvent) => {
|
||||
await queryClient.invalidateQueries([EVENTS_TABLE_KEY, newEvent.id]);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Updates existing event
|
||||
* @param {object} event - Event to be added
|
||||
*/
|
||||
const updateEvent = useCallback(
|
||||
async (event) => {
|
||||
try {
|
||||
await _updateEventMutation.mutateAsync(event);
|
||||
} catch (error) {
|
||||
emitError(`Error updating event: ${error.message}`);
|
||||
}
|
||||
},
|
||||
[_updateEventMutation, emitError]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Calls mutation to delete an event
|
||||
* @private
|
||||
*/
|
||||
const _deleteEventMutation = useMutation(requestDelete, {
|
||||
// we optimistically update here
|
||||
onMutate: async (eventId) => {
|
||||
// cancel ongoing queries
|
||||
await queryClient.cancelQueries([EVENTS_TABLE_KEY, eventId]);
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
|
||||
|
||||
const filtered = [...previousEvents].filter((e) => e.id !== eventId);
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData(EVENTS_TABLE, filtered);
|
||||
|
||||
// Return a context with the previous and new events
|
||||
return { previousEvents };
|
||||
},
|
||||
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
onError: (error, eventId, context) => {
|
||||
queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(EVENTS_TABLE);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Deletes an event form the list
|
||||
* @param {object} eventId - Event to be deleted
|
||||
*/
|
||||
const deleteEvent = useCallback(
|
||||
async (eventId) => {
|
||||
try {
|
||||
await _deleteEventMutation.mutateAsync(eventId);
|
||||
} catch (error) {
|
||||
emitError(`Error deleting event: ${error.message}`);
|
||||
}
|
||||
},
|
||||
[_deleteEventMutation, emitError],
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Calls mutation to delete all events
|
||||
* @private
|
||||
*/
|
||||
const _deleteAllEventsMutation = useMutation(requestDeleteAll, {
|
||||
// we optimistically update here
|
||||
onMutate: async () => {
|
||||
// cancel ongoing queries
|
||||
await queryClient.cancelQueries(EVENTS_TABLE, { exact: true });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
|
||||
|
||||
const clear = [];
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData(EVENTS_TABLE, clear);
|
||||
|
||||
// Return a context with the previous and new events
|
||||
return { previousEvents };
|
||||
},
|
||||
|
||||
// Mutation fails, rollback undos optimist update
|
||||
onError: (error, eventId, context) => {
|
||||
queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(EVENTS_TABLE);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Deletes all events from list
|
||||
*/
|
||||
const deleteAllEvents = useCallback(async () => {
|
||||
try {
|
||||
await _deleteAllEventsMutation.mutateAsync();
|
||||
} catch (error) {
|
||||
emitError(`Error deleting events: ${error.message}`);
|
||||
}
|
||||
}, [_deleteAllEventsMutation, emitError]);
|
||||
|
||||
/**
|
||||
* @description Calls mutation to apply a delay
|
||||
* @private
|
||||
*/
|
||||
const _applyDelayMutation = useMutation(requestApplyDelay, {
|
||||
// Mutation finished, failed or successful
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(EVENTS_TABLE);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Applies a given delay
|
||||
* @param {object} delayEventId - Id of delay to be applied
|
||||
*/
|
||||
const applyDelay = useCallback(
|
||||
async (delayEventId) => {
|
||||
try {
|
||||
await _applyDelayMutation.mutateAsync(delayEventId);
|
||||
} catch (error) {
|
||||
emitError(`Error applying delay: ${error.message}`);
|
||||
}
|
||||
},
|
||||
[_applyDelayMutation, emitError],
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Calls mutation to reorder an event
|
||||
* @private
|
||||
*/
|
||||
const _reorderEventMutation = useMutation(requestReorderEvent, {
|
||||
// we optimistically update here
|
||||
onMutate: async (data) => {
|
||||
// cancel ongoing queries
|
||||
await queryClient.cancelQueries(EVENTS_TABLE, { exact: true });
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousEvents = queryClient.getQueryData(EVENTS_TABLE);
|
||||
|
||||
const e = [...previousEvents];
|
||||
const [reorderedItem] = e.splice(data.from, 1);
|
||||
e.splice(data.to, 0, reorderedItem);
|
||||
|
||||
// optimistically update object
|
||||
queryClient.setQueryData(EVENTS_TABLE, e);
|
||||
|
||||
// Return a context with the previous and new events
|
||||
return { previousEvents };
|
||||
},
|
||||
|
||||
// Mutation fails, rollback undoes optimist update
|
||||
onError: (error, eventId, context) => {
|
||||
queryClient.setQueryData(EVENTS_TABLE, context.previousEvents);
|
||||
},
|
||||
// Mutation finished, failed or successful
|
||||
// Fetch anyway, just to be sure
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(EVENTS_TABLE);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Reorders a given event
|
||||
* @param {string} eventID - ID of event to reorder
|
||||
* @param {number} from - Current index
|
||||
* @param {number} to - New Index
|
||||
*/
|
||||
const reorderEvent = useCallback(
|
||||
async (eventId, from, to) => {
|
||||
try {
|
||||
const reorderObject = {
|
||||
index: eventId,
|
||||
from: from,
|
||||
to: to,
|
||||
};
|
||||
await _reorderEventMutation.mutateAsync(reorderObject);
|
||||
} catch (error) {
|
||||
emitError(`Error re-ordering event: ${error.message}`);
|
||||
}
|
||||
},
|
||||
[_reorderEventMutation, emitError],
|
||||
);
|
||||
|
||||
return { addEvent, updateEvent, deleteEvent, deleteAllEvents, applyDelay, reorderEvent };
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { QueryFunction, QueryKey, useQuery, UseQueryOptions } from '@tanstack/react-query';
|
||||
|
||||
interface UseFetchState {
|
||||
data: unknown;
|
||||
status: "loading" | "error" | "success";
|
||||
isError: boolean;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
export const useFetch = ( key: QueryKey, fn: QueryFunction, options?: UseQueryOptions): UseFetchState => {
|
||||
const { data, status, isError, refetch } = useQuery(key, fn, {
|
||||
refetchInterval: 10000,
|
||||
cacheTime: Infinity,
|
||||
...options
|
||||
});
|
||||
|
||||
return { data, status, isError, refetch };
|
||||
};
|
||||
@@ -20,8 +20,9 @@ export const useEventListProvider = () => {
|
||||
() => ({
|
||||
selectedEventId: null,
|
||||
nextEventId: null,
|
||||
playback: null,
|
||||
}),
|
||||
[]
|
||||
[],
|
||||
);
|
||||
return data ?? placeholder;
|
||||
};
|
||||
@@ -48,7 +49,7 @@ export const useMessageControlProvider = () => {
|
||||
},
|
||||
onAir: false,
|
||||
}),
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
const returnData = data ?? placeholder;
|
||||
@@ -63,7 +64,7 @@ export const useMessageControlProvider = () => {
|
||||
lowerVisible: (payload) => socket.emit('set-lower-message-visible', payload),
|
||||
onAir: (payload) => socket.emit('set-onAir', payload),
|
||||
}),
|
||||
[socket]
|
||||
[socket],
|
||||
);
|
||||
|
||||
return { data: returnData, setMessage };
|
||||
@@ -82,7 +83,7 @@ export const usePlaybackControlProvider = () => {
|
||||
selectedEventId: null,
|
||||
numEvents: 0,
|
||||
}),
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
const resetData = useCallback(() => {
|
||||
@@ -118,7 +119,7 @@ export const usePlaybackControlProvider = () => {
|
||||
socket.emit('set-delay', amount);
|
||||
},
|
||||
}),
|
||||
[resetData, socket]
|
||||
[resetData, socket],
|
||||
);
|
||||
|
||||
const returnData = data ?? placeholder;
|
||||
@@ -148,7 +149,7 @@ export const useInfoProvider = () => {
|
||||
selectedEventIndex: null,
|
||||
numEvents: 0,
|
||||
}),
|
||||
[]
|
||||
[],
|
||||
);
|
||||
return data ?? placeholder;
|
||||
};
|
||||
@@ -163,7 +164,7 @@ export const useCuesheetProvider = () => {
|
||||
selectedEventId: null,
|
||||
titleNow: '',
|
||||
}),
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
return data ?? placeholder;
|
||||
@@ -183,11 +184,23 @@ export const useTimerProvider = () => {
|
||||
expectedFinish: null,
|
||||
startedAt: null,
|
||||
}),
|
||||
[]
|
||||
[],
|
||||
);
|
||||
return data ?? placeholder;
|
||||
};
|
||||
|
||||
export const useEventProvider = (eventId) => {
|
||||
const socket = useSocket();
|
||||
|
||||
const setPlayback = useMemo(() => ({
|
||||
loadEvent: () => socket.emit('set-loadid', eventId),
|
||||
startEvent: () => socket.emit('set-startid', eventId),
|
||||
pause: () => socket.emit('set-pause'),
|
||||
}), [socket]);
|
||||
|
||||
return { setPlayback };
|
||||
};
|
||||
|
||||
export const useSocketProvider = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const socket = useSocket();
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export type URLAliasType = {
|
||||
enabled: boolean;
|
||||
alias: string;
|
||||
pathAndParams: string;
|
||||
}
|
||||
|
||||
export const aliasPlaceholder: URLAliasType = {
|
||||
enabled: false,
|
||||
alias: '',
|
||||
pathAndParams: '',
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
export type EventDataType = {
|
||||
title: string;
|
||||
url: string;
|
||||
publicInfo: string;
|
||||
backstageInfo: string;
|
||||
endMessage: string;
|
||||
}
|
||||
|
||||
export const eventDataPlaceholder: EventDataType = {
|
||||
title: '',
|
||||
url: '',
|
||||
publicInfo: '',
|
||||
backstageInfo: '',
|
||||
endMessage: '',
|
||||
};
|
||||
+6
-4
@@ -1,19 +1,21 @@
|
||||
export type EventTypes = 'event' | 'delay' | 'block';
|
||||
|
||||
export interface OntimeBaseEvent {
|
||||
type: 'block' | 'event' | 'delay';
|
||||
type: EventTypes;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface OntimeDelay extends OntimeBaseEvent {
|
||||
export type OntimeDelay = OntimeBaseEvent & {
|
||||
type: 'delay';
|
||||
duration: number;
|
||||
revision: number;
|
||||
}
|
||||
|
||||
export interface OntimeBlock extends OntimeBaseEvent {
|
||||
export type OntimeBlock = OntimeBaseEvent & {
|
||||
type: 'block';
|
||||
}
|
||||
|
||||
export interface OntimeEvent extends OntimeBaseEvent {
|
||||
export type OntimeEvent = OntimeBaseEvent & {
|
||||
type: 'event';
|
||||
title: string,
|
||||
subtitle: string,
|
||||
@@ -0,0 +1,26 @@
|
||||
export const httpPlaceholder = {
|
||||
onLoad: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStart: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onUpdate: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onPause: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStop: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onFinish: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { OntimeSettingsType } from './OntimeSettings.type';
|
||||
|
||||
export type InfoType = {
|
||||
networkInterfaces: string[];
|
||||
settings: Pick<OntimeSettingsType, "version" | "serverPort" >
|
||||
}
|
||||
|
||||
export const ontimePlaceholderInfo: InfoType = {
|
||||
networkInterfaces: [],
|
||||
settings: {
|
||||
version: 0,
|
||||
serverPort: 4001,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { TimeFormat } from './OntimeTypes';
|
||||
|
||||
export type OntimeSettingsType = {
|
||||
app: string;
|
||||
version: number;
|
||||
serverPort: number;
|
||||
lock: null | boolean;
|
||||
pinCode: null | number | string;
|
||||
timeFormat: TimeFormat;
|
||||
}
|
||||
|
||||
export const ontimePlaceholderSettings: OntimeSettingsType = {
|
||||
app: 'ontime',
|
||||
version: 1,
|
||||
serverPort: 4001,
|
||||
lock: null,
|
||||
pinCode: null,
|
||||
timeFormat: '24',
|
||||
};
|
||||
@@ -1,20 +1,2 @@
|
||||
type Playstate = 'roll' | 'start' | 'pause' | 'stop';
|
||||
|
||||
export type TimeManager = {
|
||||
clock: number,
|
||||
running: number,
|
||||
isNegative: boolean;
|
||||
startedAt: null | number;
|
||||
expectedFinish: null | number;
|
||||
finished: boolean;
|
||||
playstate: Playstate
|
||||
}
|
||||
|
||||
export type PresenterMessageData = {
|
||||
text: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export type ViewSettings = {
|
||||
overrideStyles: boolean;
|
||||
}
|
||||
export type Playstate = 'roll' | 'start' | 'pause' | 'stop';
|
||||
export type TimeFormat = '12' | '24';
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
export const ontimeVars = [
|
||||
{
|
||||
name: '$timer',
|
||||
description: 'Current running timer',
|
||||
},
|
||||
{
|
||||
name: '$title',
|
||||
description: 'Current title',
|
||||
},
|
||||
{
|
||||
name: '$presenter',
|
||||
description: 'Current timer',
|
||||
},
|
||||
{
|
||||
name: '$subtitle',
|
||||
description: 'Current subtitle',
|
||||
},
|
||||
{
|
||||
name: '$next-title',
|
||||
description: 'Next title',
|
||||
},
|
||||
{
|
||||
name: '$next-presenter',
|
||||
description: 'Next timer',
|
||||
},
|
||||
{
|
||||
name: '$next-subtitle',
|
||||
description: 'Next subtitle',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,13 @@
|
||||
export type OscSettingsType = {
|
||||
port: string;
|
||||
portOut: string;
|
||||
targetIP: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export const oscPlaceholderSettings: OscSettingsType = {
|
||||
port: '',
|
||||
portOut: '',
|
||||
targetIP: '',
|
||||
enabled: false,
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export type PresenterMessageType = {
|
||||
text: string;
|
||||
visible: boolean;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Playstate } from './OntimeTypes';
|
||||
|
||||
export type TimeManagerType = {
|
||||
clock: number,
|
||||
running: number,
|
||||
isNegative: boolean;
|
||||
startedAt: null | number;
|
||||
expectedFinish: null | number;
|
||||
finished: boolean;
|
||||
playstate: Playstate
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export type UserFieldsType = {
|
||||
user0: string;
|
||||
user1: string;
|
||||
user2: string;
|
||||
user3: string;
|
||||
user4: string;
|
||||
user5: string;
|
||||
user6: string;
|
||||
user7: string;
|
||||
user8: string;
|
||||
user9: string;
|
||||
}
|
||||
|
||||
export const userFieldsPlaceholder: UserFieldsType = {
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export type Size = 'xs' | 'sm' | 'md' | 'lg';
|
||||
@@ -0,0 +1,7 @@
|
||||
export type ViewSettingsType = {
|
||||
overrideStyles: boolean;
|
||||
}
|
||||
|
||||
export const viewsSettingsPlaceholder: ViewSettingsType = {
|
||||
overrideStyles: false,
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
export const ontimeQueryClient = new QueryClient();
|
||||
@@ -0,0 +1,27 @@
|
||||
import { calculateDuration, DAY_TO_MS } from '../timesManager';
|
||||
|
||||
describe('calculateDuration()', () => {
|
||||
describe('Given start and end values', () => {
|
||||
it('calculates duration correctly', () => {
|
||||
const testStart = 1;
|
||||
const testEnd = 2;
|
||||
const val = calculateDuration(testStart, testEnd);
|
||||
expect(val).toBe(testEnd - testStart);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Handles edge cases', () => {
|
||||
it('when start is after end', () => {
|
||||
const testStart = 3;
|
||||
const testEnd = 2;
|
||||
const val = calculateDuration(testStart, testEnd);
|
||||
expect(val).toBe(testEnd + DAY_TO_MS - testStart);
|
||||
});
|
||||
it('when both are equal', () => {
|
||||
const testStart = 1;
|
||||
const testEnd = 1;
|
||||
const val = calculateDuration(testStart, testEnd);
|
||||
expect(val).toBe(testEnd - testStart);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OntimeEvent, OntimeEventEntry } from '../application-types/event';
|
||||
import { OntimeEvent, OntimeEventEntry } from '../models/EventTypes';
|
||||
|
||||
import { formatTime } from './time';
|
||||
|
||||
@@ -97,7 +97,8 @@ export const formatEventList = (events: OntimeEvent[], selectedId: string, nextI
|
||||
* @param {object} event
|
||||
* @return {object} clean event
|
||||
*/
|
||||
export const duplicateEvent = (event: OntimeEvent) => {
|
||||
type DuplicatedEvent = OntimeEvent | { after?: string };
|
||||
export const duplicateEvent = (event: OntimeEvent, after?: string): DuplicatedEvent => {
|
||||
return {
|
||||
type: 'event',
|
||||
title: event.title,
|
||||
@@ -109,5 +110,6 @@ export const duplicateEvent = (event: OntimeEvent) => {
|
||||
isPublic: event.isPublic,
|
||||
skip: event.skip,
|
||||
colour: event.colour,
|
||||
after: after,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -21,3 +21,9 @@ export const getAccessibleColour = (bgColour: string): ColourCombination => {
|
||||
}
|
||||
return { backgroundColor: '#000', color: "#fffffa" };
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Creates a list of classnames from array of css module conditions
|
||||
* @param classNames - css modules objects
|
||||
*/
|
||||
export const cx = (...classNames: any[]) => classNames.filter(Boolean).join(" ")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { ontimeQueryClient } from '../../App';
|
||||
import { APP_SETTINGS } from '../api/apiConstants';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
|
||||
import { mth, mtm, mts } from './timeConstants';
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @description Milliseconds in a day
|
||||
*/
|
||||
export const DAY_TO_MS = 86400000;
|
||||
|
||||
/**
|
||||
* @description calculates duration from given values
|
||||
*/
|
||||
export const calculateDuration = (start: number, end: number): number =>
|
||||
start > end ? end + DAY_TO_MS - start : end - start;
|
||||
|
||||
/**
|
||||
* @description Checks which field the value relates to
|
||||
*/
|
||||
export const handleTimeEntry = (field: string, val: number, timeStart: number, timeEnd: number): {start: number, end: number, durationOverride: boolean} => {
|
||||
let start = timeStart;
|
||||
let end = timeEnd;
|
||||
let durationOverride = false;
|
||||
|
||||
if (field === 'timeStart') {
|
||||
start = val;
|
||||
} else if (field === 'timeEnd') {
|
||||
end = val;
|
||||
} else {
|
||||
durationOverride = field === 'durationOverride';
|
||||
}
|
||||
return { start, end, durationOverride };
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Validates time entry
|
||||
*/
|
||||
export const validateEntry = (field: string, value: number, timeStart: number, timeEnd: number): { value: boolean, catch: string } => {
|
||||
const validate = { value: true, catch: '' };
|
||||
|
||||
// 1. if one of times is not entered, anything goes
|
||||
if (value == null || timeStart == null || timeEnd == null) return validate;
|
||||
if (timeStart === 0) return validate;
|
||||
|
||||
// 2. find out what's what
|
||||
const { start, end, durationOverride } = handleTimeEntry(field, value, timeStart, timeEnd);
|
||||
if (durationOverride !== null) {
|
||||
return validate;
|
||||
}
|
||||
|
||||
// 3. validation rules
|
||||
if (start > end) {
|
||||
validate.catch = 'Start time later than end time';
|
||||
}
|
||||
return validate;
|
||||
};
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Editable, EditableInput, EditablePreview } from '@chakra-ui/editable';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { Editable, EditableInput, EditablePreview, IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { IoSunny } from '@react-icons/all-files/io5/IoSunny';
|
||||
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { IoMicOffOutline } from '@react-icons/all-files/io5/IoMicOffOutline';
|
||||
import { IoMicSharp } from '@react-icons/all-files/io5/IoMicSharp';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box } from '@chakra-ui/layout';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { FiArrowUpRight } from '@react-icons/all-files/fi/FiArrowUpRight';
|
||||
|
||||
import ErrorBoundary from '../../../common/components/errorBoundary/ErrorBoundary';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { memo } from 'react';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { Button, Tooltip } from '@chakra-ui/react';
|
||||
import TimerDisplay from 'common/components/countdown/TimerDisplay';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -55,7 +54,6 @@ const PlaybackTimer = (props) => {
|
||||
{isWaiting ? (
|
||||
<div className={style.roll}>
|
||||
<span className={style.rolltag}>Roll: Countdown to start</span>
|
||||
<span className={style.time}>FIX</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box } from '@chakra-ui/layout';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { FiArrowUpRight } from '@react-icons/all-files/fi/FiArrowUpRight';
|
||||
|
||||
import ErrorBoundary from '../../../common/components/errorBoundary/ErrorBoundary';
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
@use '../../../theme/main' as *;
|
||||
/* ============= COMMON ============= */
|
||||
|
||||
.block {
|
||||
margin: 0.2em 0;
|
||||
padding: 0.2em 0.5em;
|
||||
box-sizing: border-box;
|
||||
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
border: $block-border;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 2em auto;
|
||||
align-content: center;
|
||||
align-items: baseline;
|
||||
gap: 0.5em;
|
||||
|
||||
background-color: $block-block-color;
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(107, 70, 193, 0.4) 0%,
|
||||
rgba(128, 90, 213, 0.4) 20%,
|
||||
rgba(107, 70, 193, 0.15) 21%
|
||||
);
|
||||
}
|
||||
|
||||
/* ================ DRAG ================ */
|
||||
.drag {
|
||||
color: rgba(255, 255, 255, 0.67);
|
||||
align-self: center;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
/* ============== ACTION ================ */
|
||||
.actionOverlay {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
align-self: flex-start;
|
||||
padding-top: 0.2em;
|
||||
opacity: 0.8;
|
||||
transition: linear 0.1s;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.block:hover > .actionOverlay {
|
||||
opacity: 1;
|
||||
transition: linear 0.1s;
|
||||
}
|
||||
|
||||
/* NA */
|
||||
.actionOverlay:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
/* ============= COMMON ============= */
|
||||
|
||||
.delay {
|
||||
margin: 0.2em 0;
|
||||
padding: 0.2em 0.5em;
|
||||
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
border: $block-border;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 2em 1fr auto;
|
||||
grid-template-areas: 'drag inpt btns';
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
align-items: baseline;
|
||||
gap: 0.5em;
|
||||
|
||||
background-color: $block-delay-color;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(214, 158, 46, 0.4) 0%,
|
||||
rgba(236, 201, 75, 0.4) 20%,
|
||||
rgba(214, 158, 46, 0.07) 21%
|
||||
);
|
||||
}
|
||||
|
||||
/* ================ DRAG ================ */
|
||||
|
||||
.drag {
|
||||
grid-area: drag;
|
||||
color: $block-icon-drag;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* =============== INPUT ================ */
|
||||
|
||||
.input {
|
||||
grid-area: inpt;
|
||||
}
|
||||
|
||||
/* ============== ACTION ================ */
|
||||
|
||||
.actionOverlay {
|
||||
grid-area: btns;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
align-self: flex-start;
|
||||
padding-top: 0.2em;
|
||||
opacity: 0.8;
|
||||
transition: linear 0.1s;
|
||||
}
|
||||
|
||||
.delay:hover > .actionOverlay {
|
||||
opacity: 1;
|
||||
transition: linear 0.1s;
|
||||
}
|
||||
|
||||
/* NA */
|
||||
.actionOverlay:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -148,10 +148,21 @@ h1 {
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
color: white;
|
||||
transition: bottom 0.3s;
|
||||
|
||||
&.noEvent {
|
||||
bottom: -500px;
|
||||
transition: bottom 0.7s;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header {
|
||||
h1 {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { lazy, useEffect } from 'react';
|
||||
import { useDisclosure } from '@chakra-ui/hooks';
|
||||
import { Box } from '@chakra-ui/layout';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
|
||||
import UploadModal from 'common/components/upload-modal/UploadModal';
|
||||
import ModalManager from 'features/modals/ModalManager';
|
||||
|
||||
import UploadModal from '../../common/components/upload-modal/UploadModal';
|
||||
import { LoggingProvider } from '../../common/context/LoggingContext';
|
||||
import MenuBar from '../menu/MenuBar';
|
||||
|
||||
import styles from './Editor.module.scss';
|
||||
@@ -14,6 +13,7 @@ const EventList = lazy(() => import('features/editors/list/EventListExport'));
|
||||
const TimerControl = lazy(() => import('features/control/playback/TimerControlExport'));
|
||||
const MessageControl = lazy(() => import('features/control/message/MessageControlExport'));
|
||||
const Info = lazy(() => import('features/info/InfoExport'));
|
||||
const EventEditor = lazy(() => import('features/event-editor/EventEditorExport'));
|
||||
|
||||
export default function Editor() {
|
||||
const {
|
||||
@@ -34,7 +34,7 @@ export default function Editor() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LoggingProvider>
|
||||
<>
|
||||
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
|
||||
<ErrorBoundary>
|
||||
<ModalManager isOpen={isSettingsOpen} onClose={onSettingsClose} />
|
||||
@@ -56,6 +56,7 @@ export default function Editor() {
|
||||
<TimerControl />
|
||||
<Info />
|
||||
</div>
|
||||
</LoggingProvider>
|
||||
<EventEditor />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ActionButtons from '../../../common/components/buttons/ActionButtons';
|
||||
import PublicIconBtn from '../../../common/components/buttons/PublicIconBtn';
|
||||
import EventTimes from '../../../common/components/eventTimes/EventTimes';
|
||||
import EditableText from '../../../common/components/input/EditableText';
|
||||
|
||||
import style from './EventBlock.module.scss';
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.provided === nextProps.provided &&
|
||||
prevProps.data.revision === nextProps.data.revision &&
|
||||
prevProps.next === nextProps.next &&
|
||||
prevProps.delay === nextProps.delay &&
|
||||
prevProps.delayValue === nextProps.delayValue &&
|
||||
prevProps.previousEnd === nextProps.previousEnd
|
||||
);
|
||||
};
|
||||
|
||||
function CollapsedBlock(props) {
|
||||
const { provided, data, next, delay, delayValue, previousEnd, actionHandler } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
|
||||
<div className={style.indicators}>
|
||||
<span className={next ? style.next : style.nextDisabled}>Next</span>
|
||||
{delayValue != null && <span className={style.delayValue}>{delayValue}</span>}
|
||||
</div>
|
||||
<EventTimes
|
||||
actionHandler={actionHandler}
|
||||
timeStart={data.timeStart}
|
||||
timeEnd={data.timeEnd}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
className={style.time}
|
||||
/>
|
||||
<div className={style.titleContainer}>
|
||||
<EditableText
|
||||
label='Title'
|
||||
defaultValue={data.title}
|
||||
placeholder='Add Title'
|
||||
submitHandler={(v) => actionHandler('update', { field: 'title', value: v })}
|
||||
/>
|
||||
</div>
|
||||
<HStack spacing='0.5em' className={style.actionOverlay}>
|
||||
<PublicIconBtn actionHandler={actionHandler} active={data.isPublic} />
|
||||
<ActionButtons showAdd showDelay showBlock actionHandler={actionHandler} />
|
||||
</HStack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
CollapsedBlock.propTypes = {
|
||||
provided: PropTypes.any.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
next: PropTypes.bool.isRequired,
|
||||
delay: PropTypes.any,
|
||||
delayValue: PropTypes.string,
|
||||
previousEnd: PropTypes.number.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default memo(CollapsedBlock, areEqual);
|
||||
@@ -1,79 +0,0 @@
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import Icon from '@chakra-ui/icon';
|
||||
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
||||
import { millisToMinutes } from 'common/utils/dateConfig';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CollapseContext } from '../../../common/context/CollapseContext';
|
||||
|
||||
import CollapsedBlock from './CollapsedBlock';
|
||||
import ExpandedBlock from './ExpandedBlock';
|
||||
|
||||
import style from './EventBlock.module.scss';
|
||||
|
||||
export default function EventBlock(props) {
|
||||
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 selectedStyle = selected ? style.active : '';
|
||||
const collapsedStyle = collapsed ? style.collapsed : style.expanded;
|
||||
const classSelect = `${style.event} ${collapsedStyle} ${selectedStyle}`;
|
||||
|
||||
// Calculate delay in min
|
||||
let delayValue = null;
|
||||
if (delay != null && delay !== 0) {
|
||||
delayValue = `${delay >= 0 ? '+' : '-'} ${millisToMinutes(Math.abs(delay))}`;
|
||||
}
|
||||
const handleCollapse = (isCollapsed) => {
|
||||
setCollapsed(data.id, isCollapsed);
|
||||
};
|
||||
|
||||
return (
|
||||
<Draggable key={data.id} draggableId={data.id} index={index}>
|
||||
{(provided) => (
|
||||
<div className={classSelect} {...provided.draggableProps} ref={provided.innerRef}>
|
||||
<Icon
|
||||
className={collapsed ? style.moreCollapsed : style.moreExpanded}
|
||||
as={FiChevronUp}
|
||||
onClick={() => handleCollapse(!collapsed)}
|
||||
/>
|
||||
{collapsed ? (
|
||||
<CollapsedBlock
|
||||
provided={provided}
|
||||
data={data}
|
||||
next={next}
|
||||
delay={delay}
|
||||
delayValue={delayValue}
|
||||
previousEnd={previousEnd}
|
||||
actionHandler={actionHandler}
|
||||
/>
|
||||
) : (
|
||||
<ExpandedBlock
|
||||
provided={provided}
|
||||
eventIndex={eventIndex}
|
||||
data={data}
|
||||
next={next}
|
||||
delay={delay}
|
||||
delayValue={delayValue}
|
||||
previousEnd={previousEnd}
|
||||
actionHandler={actionHandler}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
EventBlock.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
selected: PropTypes.bool.isRequired,
|
||||
delay: PropTypes.number,
|
||||
index: PropTypes.number.isRequired,
|
||||
eventIndex: PropTypes.number.isRequired,
|
||||
previousEnd: PropTypes.number,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
next: PropTypes.bool,
|
||||
};
|
||||
@@ -1,221 +0,0 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
/* ============= COMMON ============= */
|
||||
.event {
|
||||
margin: 0.2em 0;
|
||||
padding: 0.2em 0.5em;
|
||||
box-sizing: border-box;
|
||||
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
background-color: $bg-gray-950;
|
||||
|
||||
display: grid;
|
||||
gap: 0.5em;
|
||||
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: rgba(255, 255, 255, 0.36);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
#00000000 10%,
|
||||
#4bffab10 97%,
|
||||
#227c52aa 98%,
|
||||
#4bffabcc 100%
|
||||
);
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
grid-template-columns: 2em 2em auto auto 1fr auto 3.7em;
|
||||
grid-template-areas: 'drag indi time time text more btns';
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.expanded {
|
||||
grid-template-columns: 2em 2em 2em auto 1fr auto 3.7em;
|
||||
grid-template-rows: repeat(4, 1fr);
|
||||
grid-template-areas:
|
||||
'drag indi time time text more btns'
|
||||
'.... indi time time text .... btns'
|
||||
'.... indi time time text .... btns'
|
||||
'.... indi time time text .... btns';
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ================ DRAG ================ */
|
||||
|
||||
.drag {
|
||||
grid-area: drag;
|
||||
color: rgba(255, 255, 255, 0.67);
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* ============= INDICATORS ============= */
|
||||
.indicators {
|
||||
grid-area: indi;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: flex-start;
|
||||
font-size: 0.75em;
|
||||
overflow: hidden;
|
||||
min-height: 3em;
|
||||
min-width: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.next,
|
||||
.nextDisabled {
|
||||
width: max-content;
|
||||
padding: 0 0.2em;
|
||||
color: $ontime-accent;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.next {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.nextDisabled {
|
||||
opacity: 0.05;
|
||||
}
|
||||
|
||||
.delayValue {
|
||||
color: #d69e2eaa;
|
||||
padding: 0 0.2em;
|
||||
text-align: center;
|
||||
align-self: flex-start;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.next:after,
|
||||
.delayValue:after {
|
||||
content: '\200b';
|
||||
}
|
||||
/* =============== TIMES ================ */
|
||||
|
||||
.time {
|
||||
grid-area: time;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.start {
|
||||
grid-area: star;
|
||||
}
|
||||
|
||||
.end {
|
||||
grid-area: end;
|
||||
}
|
||||
|
||||
.duration {
|
||||
grid-area: dura;
|
||||
}
|
||||
|
||||
.timeExpanded {
|
||||
grid-area: time;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-items: flex-start;
|
||||
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
padding: 0 0.5em 0.5em 0.5em;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.75em;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.calculatedTime {
|
||||
grid-area: time;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
/* =============== TITLES =============== */
|
||||
|
||||
.titleContainer {
|
||||
grid-area: text;
|
||||
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.collapsed .titleContainer {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.expanded .titleContainer {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.oscLabel {
|
||||
color: #4bffabcc;
|
||||
font-size: 0.8em;
|
||||
float: right;
|
||||
padding-right: 1em;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
/* ================ MORE ================ */
|
||||
|
||||
.moreExpanded,
|
||||
.moreCollapsed {
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
grid-area: more;
|
||||
margin-top: 0.2em;
|
||||
align-self: baseline;
|
||||
}
|
||||
|
||||
.moreExpanded {
|
||||
transform: scaleY(-1);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.moreCollapsed {
|
||||
transform: scaleY(1);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
/* ============== ACTION ================ */
|
||||
|
||||
.actionOverlay {
|
||||
grid-area: btns;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
align-self: flex-start;
|
||||
padding-top: 0.2em;
|
||||
opacity: 0.8;
|
||||
transition: linear 0.1s;
|
||||
}
|
||||
|
||||
.expanded > .actionOverlay {
|
||||
flex-direction: column;
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
.event:hover > .actionOverlay {
|
||||
opacity: 1;
|
||||
transition: linear 0.1s;
|
||||
}
|
||||
|
||||
/* NA */
|
||||
.actionOverlay:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
import { VStack } from '@chakra-ui/react';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ActionButtons from '../../../common/components/buttons/ActionButtons';
|
||||
import PublicIconBtn from '../../../common/components/buttons/PublicIconBtn';
|
||||
import TooltipLoadingActionBtn from '../../../common/components/buttons/TooltipLoadingActionBtn';
|
||||
import EventTimesVertical from '../../../common/components/eventTimes/EventTimesVertical';
|
||||
import EditableText from '../../../common/components/input/EditableText';
|
||||
|
||||
import style from './EventBlock.module.scss';
|
||||
|
||||
const areEqual = (prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.provided === nextProps.provided &&
|
||||
prevProps.data.revision === nextProps.data.revision &&
|
||||
prevProps.next === nextProps.next &&
|
||||
prevProps.delay === nextProps.delay &&
|
||||
prevProps.delayValue === nextProps.delayValue &&
|
||||
prevProps.previousEnd === nextProps.previousEnd
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function ExpandedBlock(props) {
|
||||
const { provided, data, eventIndex, next, delay, delayValue, previousEnd, actionHandler } = props;
|
||||
|
||||
const oscid = data?.id || '...';
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
</span>
|
||||
|
||||
<div className={style.indicators}>
|
||||
<span className={next ? style.next : style.nextDisabled}>Next</span>
|
||||
{delayValue != null && <span className={style.delayValue}>{delayValue}</span>}
|
||||
</div>
|
||||
<div className={style.timeExpanded}>
|
||||
<EventTimesVertical
|
||||
actionHandler={actionHandler}
|
||||
timeStart={data.timeStart}
|
||||
timeEnd={data.timeEnd}
|
||||
duration={data.duration}
|
||||
delay={delay}
|
||||
previousEnd={previousEnd}
|
||||
className={style.time}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={style.titleContainer}>
|
||||
<EditableText
|
||||
label='Title'
|
||||
defaultValue={data.title}
|
||||
placeholder='Add Title'
|
||||
submitHandler={(v) => actionHandler('update', { field: 'title', value: v })}
|
||||
/>
|
||||
<EditableText
|
||||
label='Presenter'
|
||||
defaultValue={data.presenter}
|
||||
placeholder='Add Presenter name'
|
||||
submitHandler={(v) => actionHandler('update', { field: 'presenter', value: v })}
|
||||
/>
|
||||
<EditableText
|
||||
label='Subtitle'
|
||||
defaultValue={data.subtitle}
|
||||
placeholder='Add Subtitle'
|
||||
submitHandler={(v) => actionHandler('update', { field: 'subtitle', value: v })}
|
||||
/>
|
||||
<EditableText
|
||||
label='Note'
|
||||
defaultValue={data.note}
|
||||
placeholder='Add Note'
|
||||
style={{ color: '#d69e2e' }}
|
||||
maxchar={160}
|
||||
submitHandler={(v) => actionHandler('update', { field: 'note', value: v })}
|
||||
/>
|
||||
<span className={style.oscLabel}>
|
||||
{`/ontime/goto ${eventIndex + 1} << OSC >> /ontime/gotoid ${oscid}`}
|
||||
</span>
|
||||
</div>
|
||||
<VStack spacing='0.5em' className={style.actionOverlay}>
|
||||
<PublicIconBtn actionHandler={actionHandler} active={data.isPublic} />
|
||||
<ActionButtons showAdd showDelay showBlock actionHandler={actionHandler} />
|
||||
<TooltipLoadingActionBtn
|
||||
clickHandler={() => actionHandler('delete')}
|
||||
icon={<IoRemove />}
|
||||
colorScheme='red'
|
||||
tooltip='Delete'
|
||||
_hover={{ bg: 'red.400' }}
|
||||
/>
|
||||
</VStack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ExpandedBlock.propTypes = {
|
||||
provided: PropTypes.any.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
eventIndex: PropTypes.number.isRequired,
|
||||
next: PropTypes.bool.isRequired,
|
||||
delay: PropTypes.number,
|
||||
delayValue: PropTypes.string,
|
||||
previousEnd: PropTypes.number,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default memo(ExpandedBlock, areEqual);
|
||||
@@ -0,0 +1,36 @@
|
||||
$block-gap: 4px;
|
||||
$element-spacing: 4px;
|
||||
$binder-width: 32px;
|
||||
$clearance: 8px;
|
||||
$block-border-radius: 3px;
|
||||
|
||||
@mixin block-spacing() {
|
||||
margin: 4px 1px;
|
||||
padding: 4px 10px 4px 2px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
@mixin action-overlay() {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
opacity: 0.8;
|
||||
transition: linear 0.1s;
|
||||
}
|
||||
|
||||
@mixin block-hover() {
|
||||
opacity: 1;
|
||||
transition: linear 0.1s;
|
||||
}
|
||||
|
||||
@mixin drag-style() {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
opacity: 0.1;
|
||||
cursor: grab;
|
||||
transition: opacity 0.3s;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
+4
-9
@@ -1,7 +1,7 @@
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ActionButtons from '../../../common/components/buttons/ActionButtons';
|
||||
@@ -15,15 +15,11 @@ export default function BlockBlock(props) {
|
||||
return (
|
||||
<Draggable key={data.id} draggableId={data.id} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
className={style.block}
|
||||
{...provided.draggableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<div className={style.block} {...provided.draggableProps} ref={provided.innerRef}>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
<IoReorderTwo />
|
||||
</span>
|
||||
<HStack spacing='0.5em' className={style.actionOverlay}>
|
||||
<HStack spacing='4px' className={style.actionOverlay}>
|
||||
<TooltipLoadingActionBtn
|
||||
clickHandler={() => actionHandler('delete')}
|
||||
icon={<IoRemove />}
|
||||
@@ -39,7 +35,6 @@ export default function BlockBlock(props) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
BlockBlock.propTypes = {
|
||||
index: PropTypes.number.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
@@ -0,0 +1,42 @@
|
||||
@use '../../../theme/main' as *;
|
||||
@use '../blockMixins' as *;
|
||||
|
||||
/* ============= COMMON ============= */
|
||||
|
||||
.block {
|
||||
@include block-spacing;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
|
||||
border-radius: 2px 2px $block-border-radius $block-border-radius;
|
||||
background-color: rgba(107, 70, 193, 0.4);
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(107, 70, 193, 0.4) 0%,
|
||||
rgba(128, 90, 213, 0.4) 20%,
|
||||
rgba(107, 70, 193, 0.15) 21%
|
||||
);
|
||||
}
|
||||
|
||||
/* ================ DRAG ================ */
|
||||
.drag {
|
||||
@include drag-style;
|
||||
}
|
||||
|
||||
/* ============== ACTION ================ */
|
||||
.actionOverlay {
|
||||
@include action-overlay;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.block:hover > .actionOverlay {
|
||||
@include block-hover;
|
||||
}
|
||||
|
||||
/* NA */
|
||||
.actionOverlay:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
+41
-19
@@ -1,45 +1,68 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import { Button, HStack } from '@chakra-ui/react';
|
||||
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
|
||||
import { FiMoreVertical } from '@react-icons/all-files/fi/FiMoreVertical';
|
||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
||||
import ActionButtons from 'common/components/buttons/ActionButtons';
|
||||
import TooltipLoadingActionBtn from 'common/components/buttons/TooltipLoadingActionBtn';
|
||||
import DelayInput from 'common/components/input/DelayInput';
|
||||
import { useEventAction } from 'common/hooks/useEventAction';
|
||||
import { millisToMinutes } from 'common/utils/dateConfig';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ActionButtons from '../../../common/components/buttons/ActionButtons';
|
||||
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
||||
import TooltipLoadingActionBtn from '../../../common/components/buttons/TooltipLoadingActionBtn';
|
||||
|
||||
import style from './DelayBlock.module.scss';
|
||||
|
||||
export default function DelayBlock(props) {
|
||||
const { eventsHandler, data, index, actionHandler } = props;
|
||||
const { data, index, actionHandler } = props;
|
||||
const { applyDelay, deleteEvent, updateEvent } = useEventAction();
|
||||
|
||||
const applyDelayHandler = useCallback(() => {
|
||||
eventsHandler('applyDelay', { id: data.id, duration: data.duration });
|
||||
}, [data.duration, data.id, eventsHandler]);
|
||||
applyDelay(data.id);
|
||||
}, [data.id, applyDelay]);
|
||||
|
||||
const deleteHandler = useCallback(() => {
|
||||
deleteEvent(data.id);
|
||||
}, [data.id, deleteEvent]);
|
||||
|
||||
const delaySubmitHandler = useCallback(
|
||||
(value) => {
|
||||
const newEvent = {
|
||||
id: data.id,
|
||||
duration: value * 60000,
|
||||
};
|
||||
|
||||
updateEvent(newEvent);
|
||||
},
|
||||
[data.id, updateEvent]
|
||||
);
|
||||
|
||||
const delayValue = data.duration != null ? millisToMinutes(data.duration) : undefined;
|
||||
|
||||
return (
|
||||
<Draggable key={data.id} draggableId={data.id} index={index}>
|
||||
{(provided) => (
|
||||
<div className={style.delay} {...provided.draggableProps} ref={provided.innerRef}>
|
||||
<span className={style.drag} {...provided.dragHandleProps}>
|
||||
<FiMoreVertical />
|
||||
<IoReorderTwo />
|
||||
</span>
|
||||
<DelayInput className={style.input} value={delayValue} actionHandler={actionHandler} />
|
||||
<HStack spacing='0.5em' className={style.actionOverlay}>
|
||||
<TooltipActionBtn
|
||||
clickHandler={applyDelayHandler}
|
||||
icon={<FiCheck />}
|
||||
<DelayInput
|
||||
className={style.input}
|
||||
value={delayValue}
|
||||
submitHandler={delaySubmitHandler}
|
||||
/>
|
||||
<HStack spacing='4px' className={style.actionOverlay}>
|
||||
<Button
|
||||
onClick={applyDelayHandler}
|
||||
size='xs'
|
||||
colorScheme='orange'
|
||||
tooltip='Apply delays'
|
||||
_hover={{ bg: 'orange.400' }}
|
||||
/>
|
||||
leftIcon={<FiCheck />}
|
||||
>
|
||||
Apply delay
|
||||
</Button>
|
||||
<TooltipLoadingActionBtn
|
||||
clickHandler={() => actionHandler('delete')}
|
||||
clickHandler={deleteHandler}
|
||||
icon={<IoRemove />}
|
||||
colorScheme='red'
|
||||
tooltip='Delete'
|
||||
@@ -54,7 +77,6 @@ export default function DelayBlock(props) {
|
||||
}
|
||||
|
||||
DelayBlock.propTypes = {
|
||||
eventsHandler: PropTypes.func.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
actionHandler: PropTypes.func.isRequired,
|
||||
@@ -0,0 +1,51 @@
|
||||
@use '../../../theme/main' as *;
|
||||
@use '../blockMixins' as *;
|
||||
|
||||
/* ============= COMMON ============= */
|
||||
|
||||
.delay {
|
||||
@include block-spacing;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 32px 1fr auto;
|
||||
grid-template-areas: 'drag inpt btns';
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
|
||||
border-radius: $block-border-radius $block-border-radius 2px 2px;
|
||||
background-color: rgba(214, 158, 46, 0.4);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(214, 158, 46, 0.4) 0%,
|
||||
rgba(214, 158, 46, 0.4) 20%,
|
||||
rgba(214, 158, 46, 0.17) 21%
|
||||
);
|
||||
}
|
||||
|
||||
/* ================ DRAG ================ */
|
||||
|
||||
.drag {
|
||||
grid-area: drag;
|
||||
@include drag-style;
|
||||
}
|
||||
|
||||
/* =============== INPUT ================ */
|
||||
|
||||
.input {
|
||||
grid-area: inpt;
|
||||
}
|
||||
|
||||
/* ============== ACTION ================ */
|
||||
|
||||
.actionOverlay {
|
||||
@include action-overlay;
|
||||
}
|
||||
|
||||
.delay:hover > .actionOverlay {
|
||||
@include block-hover;
|
||||
}
|
||||
|
||||
/* NA */
|
||||
.actionOverlay:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
+46
-32
@@ -1,31 +1,62 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Checkbox } from '@chakra-ui/react';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { Checkbox, Tooltip } from '@chakra-ui/react';
|
||||
import { defaultPublicAtom, startTimeIsLastEndAtom } from 'common/atoms/LocalEventSettings';
|
||||
import { LoggingContext } from 'common/context/LoggingContext';
|
||||
import { useEventAction } from 'common/hooks/useEventAction';
|
||||
import { EventTypes } from 'common/models/EventTypes';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
defaultPublicAtom,
|
||||
startTimeIsLastEndAtom,
|
||||
} from '../../../common/atoms/LocalEventSettings';
|
||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||
|
||||
import style from './EntryBlock.module.scss';
|
||||
|
||||
export default function EntryBlock(props) {
|
||||
interface EntryBlockProps {
|
||||
showKbd: boolean;
|
||||
previousId?: string;
|
||||
visible?: boolean;
|
||||
disableAddDelay?: boolean;
|
||||
disableAddBlock: boolean;
|
||||
}
|
||||
|
||||
export default function EntryBlock(props: EntryBlockProps) {
|
||||
const {
|
||||
showKbd,
|
||||
previousId,
|
||||
eventsHandler,
|
||||
visible,
|
||||
visible = true,
|
||||
disableAddDelay = true,
|
||||
disableAddBlock,
|
||||
} = props;
|
||||
const { addEvent } = useEventAction();
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
||||
const defaultPublic = useAtomValue(defaultPublicAtom);
|
||||
const [doStartTime, setStartTime] = useState(startTimeIsLastEnd);
|
||||
const [doPublic, setPublic] = useState(defaultPublic);
|
||||
|
||||
const handleCreateEvent = useCallback((eventType: EventTypes) => {
|
||||
switch (eventType) {
|
||||
case 'event': {
|
||||
const newEvent = { type: 'event', after: previousId, isPublic: doPublic };
|
||||
const options = { startIsLastEnd: doStartTime ? previousId : undefined };
|
||||
addEvent(newEvent, options);
|
||||
break;
|
||||
}
|
||||
case 'delay': {
|
||||
addEvent({ type: 'delay', after: previousId });
|
||||
break;
|
||||
}
|
||||
case 'block': {
|
||||
addEvent({ type: 'block', after: previousId });
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
emitError(`Cannot create unknown event type: ${eventType}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}, [addEvent, doPublic, doStartTime, emitError, previousId]);
|
||||
|
||||
useEffect(() => {
|
||||
setStartTime(startTimeIsLastEnd);
|
||||
}, [startTimeIsLastEnd]);
|
||||
@@ -39,13 +70,7 @@ export default function EntryBlock(props) {
|
||||
<Tooltip label='Add Event' openDelay={tooltipDelayMid}>
|
||||
<span
|
||||
className={style.createEvent}
|
||||
onClick={() =>
|
||||
eventsHandler(
|
||||
'add',
|
||||
{ type: 'event', after: previousId, isPublic: doPublic },
|
||||
{ startIsLastEnd: doStartTime ? previousId : undefined }
|
||||
)
|
||||
}
|
||||
onClick={() => handleCreateEvent('event')}
|
||||
role='button'
|
||||
>
|
||||
E{showKbd && <span className={style.keyboard}>Alt + E</span>}
|
||||
@@ -54,7 +79,7 @@ export default function EntryBlock(props) {
|
||||
<Tooltip label='Add Delay' openDelay={tooltipDelayMid}>
|
||||
<span
|
||||
className={`${style.createDelay} ${disableAddDelay ? style.disabled : ''}`}
|
||||
onClick={() => eventsHandler('add', { type: 'delay', after: previousId })}
|
||||
onClick={() => handleCreateEvent('delay')}
|
||||
role='button'
|
||||
>
|
||||
D{showKbd && <span className={style.keyboard}>Alt + D</span>}
|
||||
@@ -63,7 +88,7 @@ export default function EntryBlock(props) {
|
||||
<Tooltip label='Add Block' openDelay={tooltipDelayMid}>
|
||||
<span
|
||||
className={`${style.createBlock} ${disableAddBlock ? style.disabled : ''}`}
|
||||
onClick={() => eventsHandler('add', { type: 'block', after: previousId })}
|
||||
onClick={() => handleCreateEvent('block')}
|
||||
role='button'
|
||||
>
|
||||
B{showKbd && <span className={style.keyboard}>Alt + B</span>}
|
||||
@@ -74,9 +99,7 @@ export default function EntryBlock(props) {
|
||||
size='sm'
|
||||
colorScheme='blue'
|
||||
isChecked={doStartTime}
|
||||
onChange={(e) => {
|
||||
setStartTime(e.target.checked);
|
||||
}}
|
||||
onChange={(e) => setStartTime(e.target.checked)}
|
||||
>
|
||||
Start time is last end
|
||||
</Checkbox>
|
||||
@@ -92,12 +115,3 @@ export default function EntryBlock(props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EntryBlock.propTypes = {
|
||||
showKbd: PropTypes.bool,
|
||||
eventsHandler: PropTypes.func,
|
||||
visible: PropTypes.bool,
|
||||
previousId: PropTypes.string,
|
||||
disableAddDelay: PropTypes.bool,
|
||||
disableAddBlock: PropTypes.bool,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user