mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 18:33:53 +00:00
Fix/195 (#200)
* refactor: add typescript dependencies * chore: update chakra-ui * refactor: typescript config * refactor: convert <MenuBar> component to typescript * refactor: convert <TooltipActionBtn> component to typescript * refactor: improve UX in file upload * refactor: upgrade dependencies * refactor: prepare data provider * refactor: prevent importing bad fields * refactor: extract merge to provider * refactor(upload): parser merges only given fields * refactor(upload): add event fields to excel * refactor(upload): improve styling on modal open * style: improve styling in menu * style: prevent global pollution * feat(upload): add upload options * fix: avoid potential bug in log queue
This commit is contained in:
+2
-5
@@ -2,15 +2,12 @@
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"react",
|
||||
"testing-library",
|
||||
"simple-import-sort",
|
||||
"@typescript-eslint"
|
||||
"simple-import-sort"
|
||||
],
|
||||
"rules": {
|
||||
"jest/no-mocks-import": "warn",
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -241,14 +241,21 @@ export const downloadEvents = async () => {
|
||||
* @description HTTP request to upload events db
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const uploadEvents = async (file) => {
|
||||
export const uploadEvents = async (file, setProgress, options) => {
|
||||
const formData = new FormData();
|
||||
formData.append('userFile', file); // appending file
|
||||
await axios.post(`${ontimeURL}/db`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
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,27 +0,0 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function TooltipActionBtn(props) {
|
||||
const { clickHandler, icon, color, size='xs', tooltip, openDelay = 0, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={tooltip} openDelay={openDelay}>
|
||||
<IconButton
|
||||
aria-label={tooltip}
|
||||
size={size}
|
||||
icon={icon}
|
||||
onClick={clickHandler}
|
||||
{...rest}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
TooltipActionBtn.propTypes = {
|
||||
clickHandler: PropTypes.func,
|
||||
icon: PropTypes.element,
|
||||
color: PropTypes.string,
|
||||
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']),
|
||||
tooltip: PropTypes.string,
|
||||
openDelay: PropTypes.number
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { IconButton } from '@chakra-ui/button';
|
||||
import { IconButtonProps } from '@chakra-ui/react';
|
||||
import { Tooltip } from '@chakra-ui/tooltip';
|
||||
|
||||
export type Sizes = 'xs' | 'sm' | 'md' | 'lg';
|
||||
|
||||
interface TooltipActionBtnProps extends IconButtonProps {
|
||||
clickHandler: () => void;
|
||||
tooltip: string;
|
||||
openDelay?: number;
|
||||
}
|
||||
|
||||
export default function TooltipActionBtn(props: TooltipActionBtnProps) {
|
||||
const { clickHandler, icon, size = 'xs', tooltip, openDelay = 0, className, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={tooltip} openDelay={openDelay}>
|
||||
<IconButton
|
||||
{...rest}
|
||||
aria-label={tooltip}
|
||||
size={size}
|
||||
icon={icon}
|
||||
onClick={clickHandler}
|
||||
className={className}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
.modalBody {
|
||||
min-height: 40vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.options {
|
||||
margin-bottom: 1.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.notes {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.info {
|
||||
background-color: $bg-gray;
|
||||
margin: 1em 0;
|
||||
padding: 0.5em;
|
||||
border-radius: 2px;
|
||||
color: black;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.corner {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.infoList {
|
||||
font-size: 0.9em;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.flexColumnLeft {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { ChangeEvent, useCallback, useContext, useRef, useState } from 'react';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import {
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Progress,
|
||||
} from '@chakra-ui/react';
|
||||
import { IoCloseSharp } from '@react-icons/all-files/io5/IoCloseSharp';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { EVENTS_TABLE } from '../../api/apiConstants';
|
||||
import { uploadEvents } from '../../api/ontimeApi';
|
||||
import { LoggingContext } from '../../context/LoggingContext';
|
||||
import TooltipActionBtn from '../buttons/TooltipActionBtn';
|
||||
|
||||
import { validateFile } from './utils';
|
||||
|
||||
import style from './UploadModal.module.scss';
|
||||
|
||||
interface UploadModalProps {
|
||||
onClose: () => void;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const overrideOptionRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFile = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
const fileUploaded = event?.target?.files?.[0];
|
||||
if (!fileUploaded) return;
|
||||
|
||||
const validate = validateFile(fileUploaded);
|
||||
setErrors(validate.errors);
|
||||
|
||||
if (validate.isValid) {
|
||||
setFile(fileUploaded);
|
||||
} else {
|
||||
setFile(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleUpload = useCallback(async () => {
|
||||
if (file) {
|
||||
try {
|
||||
await uploadEvents(file, setProgress, { onlyEvents: overrideOptionRef?.current?.checked });
|
||||
} catch (error) {
|
||||
emitError(`Failed uploading file: ${error}`);
|
||||
} finally {
|
||||
await queryClient.invalidateQueries(EVENTS_TABLE);
|
||||
setFile(null);
|
||||
}
|
||||
}
|
||||
}, [emitError, file, queryClient]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
closeOnOverlayClick={false}
|
||||
motionPreset='slideInBottom'
|
||||
size='xl'
|
||||
scrollBehavior='inside'
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>File upload</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody className={style.modalBody}>
|
||||
<FormControl isInvalid={errors.length > 0}>
|
||||
<FormLabel>Select file to upload</FormLabel>
|
||||
<Input type='file' onChange={handleFile} accept='.json, .xlsx' />
|
||||
{errors.length === 0 ? (
|
||||
<FormHelperText>.XLSX .JSON with max 1MB</FormHelperText>
|
||||
) : (
|
||||
<FormErrorMessage className={style.flexColumnLeft}>
|
||||
{errors.map((error) => (
|
||||
<span key={error}>{error}</span>
|
||||
))}
|
||||
</FormErrorMessage>
|
||||
)}
|
||||
</FormControl>
|
||||
<div className={style.options}>
|
||||
<b>Options</b>
|
||||
<Checkbox ref={overrideOptionRef}>Import only events</Checkbox>
|
||||
<span className={style.notes}>This will prevent overriding user settings</span>
|
||||
</div>
|
||||
{file && (
|
||||
<div className={style.info}>
|
||||
<span>File ready to upload</span>
|
||||
<TooltipActionBtn
|
||||
clickHandler={() => setFile(null)}
|
||||
tooltip='Cancel'
|
||||
aria-label='Cancel'
|
||||
className={style.corner}
|
||||
size='sm'
|
||||
variant='ghosted'
|
||||
icon={<IoCloseSharp />}
|
||||
/>
|
||||
<ul className={style.infoList}>
|
||||
<li>{file.name}</li>
|
||||
<li>{`${(file.size / 1024).toFixed(2)}kb`}</li>
|
||||
<li>{file.type}</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<Progress value={progress} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
colorScheme='blue'
|
||||
disabled={!file || errors.length > 0}
|
||||
onClick={handleUpload}
|
||||
isLoading={progress < 0 && progress >= 100}
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
type ValidationStatus = {
|
||||
errors: string[];
|
||||
isValid: boolean;
|
||||
};
|
||||
|
||||
export function validateFile(file: File): ValidationStatus {
|
||||
const status:ValidationStatus = { errors: [], isValid: true };
|
||||
if (!file) {
|
||||
status.errors.push('No file to upload');
|
||||
status.isValid = false;
|
||||
}
|
||||
|
||||
// Limit file size to 1MB
|
||||
if (file.size > 1000000) {
|
||||
status.errors.push('File size limit (1MB) exceeded');
|
||||
status.isValid = false;
|
||||
}
|
||||
|
||||
// Check file extension
|
||||
if (!file.name.endsWith('.xlsx') && !file.name.endsWith('.json')) {
|
||||
status.errors.push('Unhandled file type');
|
||||
status.isValid = false;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
+40
-15
@@ -1,22 +1,47 @@
|
||||
import { createContext, useCallback, useEffect, useState } from 'react';
|
||||
import { createContext, ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { generateId } from '../utils/generate_id';
|
||||
import { nowInMillis, stringFromMillis } from '../utils/time';
|
||||
|
||||
import { useSocket } from './socketContext';
|
||||
|
||||
export const LoggingContext = createContext({
|
||||
type LOG_LEVEL = 'INFO' | 'WARN' | 'ERROR';
|
||||
type Log = {
|
||||
id: string;
|
||||
origin: string;
|
||||
time: string;
|
||||
level: LOG_LEVEL;
|
||||
text: string;
|
||||
};
|
||||
|
||||
interface LoggingProviderState {
|
||||
logData: Log[];
|
||||
emitInfo: (text: string) => void;
|
||||
emitWarning: (text: string) => void;
|
||||
emitError: (text: string) => void;
|
||||
clearLog: () => void;
|
||||
}
|
||||
|
||||
type LoggingProviderProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const notInitialised = () => {
|
||||
throw new Error("Not initialised");
|
||||
};
|
||||
|
||||
export const LoggingContext = createContext<LoggingProviderState>({
|
||||
logData: [],
|
||||
emitInfo: () => undefined,
|
||||
emitWarning: () => undefined,
|
||||
emitError: () => undefined,
|
||||
clearLog: () => undefined,
|
||||
emitInfo: notInitialised,
|
||||
emitWarning: notInitialised,
|
||||
emitError: notInitialised,
|
||||
clearLog: notInitialised
|
||||
});
|
||||
|
||||
export const LoggingProvider = ({ children }) => {
|
||||
export const LoggingProvider = ({ children }: LoggingProviderProps) => {
|
||||
const MAX_MESSAGES = 100;
|
||||
const socket = useSocket();
|
||||
const [logData, setLogData] = useState([]);
|
||||
const [logData, setLogData] = useState<Log[]>([]);
|
||||
const origin = 'USER';
|
||||
|
||||
// handle incoming messages
|
||||
@@ -26,7 +51,7 @@ export const LoggingProvider = ({ children }) => {
|
||||
// Ask for log data
|
||||
socket.emit('get-logger');
|
||||
|
||||
socket.on('logger', (data) => {
|
||||
socket.on('logger', (data: Log) => {
|
||||
setLogData((l) => [data, ...l]);
|
||||
});
|
||||
|
||||
@@ -43,9 +68,9 @@ export const LoggingProvider = ({ children }) => {
|
||||
* @private
|
||||
*/
|
||||
const _send = useCallback(
|
||||
(text, level) => {
|
||||
(text: string, level: LOG_LEVEL) => {
|
||||
if (socket != null) {
|
||||
const m = {
|
||||
const m: Log = {
|
||||
id: generateId(),
|
||||
origin,
|
||||
time: stringFromMillis(nowInMillis()),
|
||||
@@ -56,7 +81,7 @@ export const LoggingProvider = ({ children }) => {
|
||||
socket.emit('logger', m);
|
||||
}
|
||||
if (logData.length > MAX_MESSAGES) {
|
||||
setLogData((l) => l.pop());
|
||||
setLogData((l) => l.slice(1));
|
||||
}
|
||||
},
|
||||
[logData, socket]
|
||||
@@ -67,7 +92,7 @@ export const LoggingProvider = ({ children }) => {
|
||||
* @param text
|
||||
*/
|
||||
const emitInfo = useCallback(
|
||||
(text) => {
|
||||
(text: string) => {
|
||||
_send(text, 'INFO');
|
||||
},
|
||||
[_send]
|
||||
@@ -78,7 +103,7 @@ export const LoggingProvider = ({ children }) => {
|
||||
* @param text
|
||||
*/
|
||||
const emitWarning = useCallback(
|
||||
(text) => {
|
||||
(text: string) => {
|
||||
_send(text, 'WARN');
|
||||
},
|
||||
[_send]
|
||||
@@ -89,7 +114,7 @@ export const LoggingProvider = ({ children }) => {
|
||||
* @param text
|
||||
*/
|
||||
const emitError = useCallback(
|
||||
(text) => {
|
||||
(text: string) => {
|
||||
_send(text, 'ERROR');
|
||||
},
|
||||
[_send]
|
||||
@@ -1,24 +0,0 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { serverURL } from 'common/api/apiConstants';
|
||||
import io from 'socket.io-client';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const SocketContext = createContext([[], () => {}]);
|
||||
|
||||
export const useSocket = () => {
|
||||
return useContext(SocketContext);
|
||||
};
|
||||
|
||||
function SocketProvider({ children }) {
|
||||
const [socket, setSocket] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
const s = io(serverURL, { transports: ['websocket'] });
|
||||
setSocket(s);
|
||||
return () => s.disconnect();
|
||||
}, []);
|
||||
|
||||
return <SocketContext.Provider value={socket}>{children}</SocketContext.Provider>;
|
||||
}
|
||||
|
||||
export default SocketProvider;
|
||||
@@ -0,0 +1,44 @@
|
||||
// @ts-nocheck
|
||||
import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
|
||||
import { serverURL } from 'common/api/apiConstants';
|
||||
import io, { Socket } from 'socket.io-client';
|
||||
|
||||
interface SocketProviderState {
|
||||
socket: Socket | null;
|
||||
emit: <T>(topic: string, payload?: T) => void;
|
||||
on: <T>(topic: string, callback: (data: T) => void) => void;
|
||||
off: (topic: string) => void;
|
||||
}
|
||||
|
||||
type SocketProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const SocketContext = createContext<SocketProviderState>({
|
||||
socket: null,
|
||||
emit: () => {},
|
||||
on: () => {},
|
||||
off: () => {}
|
||||
});
|
||||
|
||||
export const useSocket = () => {
|
||||
return useContext(SocketContext);
|
||||
};
|
||||
|
||||
function SocketProvider({ children }: SocketProviderProps) {
|
||||
const [socket, setSocket] = useState({} as Socket);
|
||||
|
||||
useEffect(() => {
|
||||
const socketInstance = io(serverURL, { transports: ["websocket"] });
|
||||
setSocket(socketInstance);
|
||||
return () => {
|
||||
socketInstance.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SocketContext.Provider value={socket}>{children}</SocketContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default SocketProvider;
|
||||
@@ -0,0 +1,12 @@
|
||||
// @ts-nocheck
|
||||
export default function useElectronEvent() {
|
||||
const isElectron = window?.process?.type === 'renderer';
|
||||
|
||||
const sendToElectron = (channel: string, args: any) => {
|
||||
if (isElectron) {
|
||||
window?.ipcRenderer.send(channel, args);
|
||||
}
|
||||
};
|
||||
|
||||
return { isElectron, sendToElectron };
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
declare module '*.scss' {
|
||||
const content: Record<string, string>;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare namespace NodeJS {
|
||||
export interface ProcessEnv {
|
||||
type: string
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { lazy } from 'react';
|
||||
import { lazy, useEffect } from 'react';
|
||||
import { useDisclosure } from '@chakra-ui/hooks';
|
||||
import { Box } from '@chakra-ui/layout';
|
||||
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
|
||||
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';
|
||||
|
||||
@@ -15,23 +16,42 @@ const MessageControl = lazy(() => import('features/control/message/MessageContro
|
||||
const Info = lazy(() => import('features/info/InfoExport'));
|
||||
|
||||
export default function Editor() {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const {
|
||||
isOpen: isSettingsOpen,
|
||||
onOpen: onSettingsOpen,
|
||||
onClose: onSettingsClose,
|
||||
} = useDisclosure();
|
||||
|
||||
const {
|
||||
isOpen: isUploadModalOpen,
|
||||
onOpen: onUploadModalOpen,
|
||||
onClose: onUploadModalClose,
|
||||
} = useDisclosure();
|
||||
|
||||
// Set window title
|
||||
document.title = 'ontime - Editor';
|
||||
useEffect(() => {
|
||||
document.title = 'ontime - Editor';
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LoggingProvider>
|
||||
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
|
||||
<ErrorBoundary>
|
||||
<ModalManager isOpen={isOpen} onClose={onClose} />
|
||||
<ModalManager isOpen={isSettingsOpen} onClose={onSettingsClose} />
|
||||
</ErrorBoundary>
|
||||
<div className={styles.mainContainer}>
|
||||
<Box id='settings' className={styles.settings}>
|
||||
<ErrorBoundary>
|
||||
<MenuBar onOpen={onOpen} isOpen={isOpen} onClose={onClose} />
|
||||
<MenuBar
|
||||
onSettingsOpen={onSettingsOpen}
|
||||
isSettingsOpen={isSettingsOpen}
|
||||
onSettingsClose={onSettingsClose}
|
||||
isUploadOpen={isUploadModalOpen}
|
||||
onUploadOpen={onUploadModalOpen}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
<EventList onOpen={onOpen} isOpen={isOpen} onClose={onClose} />
|
||||
<EventList />
|
||||
<MessageControl />
|
||||
<TimerControl />
|
||||
<Info />
|
||||
@@ -85,8 +85,3 @@
|
||||
padding: 0 0.5em;
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
ul > li {
|
||||
font-size: 0.9em;
|
||||
color: $text-white;
|
||||
}
|
||||
|
||||
@@ -85,14 +85,14 @@ const EventListMenu = ({ eventsHandler }) => {
|
||||
clickHandler={() => actionHandler('cursorUp')}
|
||||
icon={<IoCaretUp />}
|
||||
tooltip='Move cursor up Alt + ↑'
|
||||
_hover={{ bg: 'pink.400' }}
|
||||
_hover={{ bg: 'pink.400', color: 'white' }}
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...cursorBtnProps}
|
||||
clickHandler={() => actionHandler('cursorDown')}
|
||||
icon={<IoCaretDown />}
|
||||
tooltip='Move cursor down Alt + ↓'
|
||||
_hover={{ bg: 'pink.400' }}
|
||||
_hover={{ bg: 'pink.400', color: 'white' }}
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...cursorBtnProps}
|
||||
@@ -101,7 +101,8 @@ const EventListMenu = ({ eventsHandler }) => {
|
||||
tooltip='Lock cursor to current'
|
||||
width='3em'
|
||||
backgroundColor={isCursorLocked && 'pink.400'}
|
||||
_hover={{ bg: 'pink.300' }}
|
||||
color={isCursorLocked && 'white'}
|
||||
_hover={{ bg: 'pink.400', color: 'white' }}
|
||||
variant={isCursorLocked ? 'solid' : 'outline'}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
import { useCallback, useContext, useEffect, useRef } from 'react';
|
||||
import { VStack } from '@chakra-ui/react';
|
||||
import { FiDownload } from '@react-icons/all-files/fi/FiDownload';
|
||||
import { FiHelpCircle } from '@react-icons/all-files/fi/FiHelpCircle';
|
||||
import { FiMaximize } from '@react-icons/all-files/fi/FiMaximize';
|
||||
import { FiMinimize } from '@react-icons/all-files/fi/FiMinimize';
|
||||
import { FiSettings } from '@react-icons/all-files/fi/FiSettings';
|
||||
import { FiUpload } from '@react-icons/all-files/fi/FiUpload';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { EVENTS_TABLE } from 'common/api/apiConstants';
|
||||
import { downloadEvents, uploadEvents } from 'common/api/ontimeApi';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import QuitIconBtn from '../../common/components/buttons/QuitIconBtn';
|
||||
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
|
||||
import { LoggingContext } from '../../common/context/LoggingContext';
|
||||
|
||||
import style from './MenuBar.module.scss';
|
||||
|
||||
export default function MenuBar(props) {
|
||||
const { isOpen, onOpen, onClose } = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const hiddenFileInput = useRef(null);
|
||||
const queryClient = useQueryClient();
|
||||
const uploaddb = useMutation(uploadEvents, {
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(EVENTS_TABLE);
|
||||
},
|
||||
});
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (hiddenFileInput && hiddenFileInput.current) {
|
||||
hiddenFileInput.current.click();
|
||||
}
|
||||
}, [hiddenFileInput]);
|
||||
|
||||
const buttonStyle = {
|
||||
fontSize: '1.5em',
|
||||
size: 'lg',
|
||||
colorScheme: 'white',
|
||||
};
|
||||
|
||||
const handleUpload = useCallback(
|
||||
(event) => {
|
||||
const fileUploaded = event.target.files[0];
|
||||
if (fileUploaded == null) return;
|
||||
|
||||
// Limit file size to 1MB
|
||||
if (fileUploaded.size > 1000000) {
|
||||
emitError('Error: File size limit (1MB) exceeded');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check file extension
|
||||
if (fileUploaded.name.endsWith('.xlsx') || fileUploaded.name.endsWith('.json')) {
|
||||
try {
|
||||
uploaddb.mutate(fileUploaded);
|
||||
} catch (error) {
|
||||
emitError(`Failed uploading file: ${error}`);
|
||||
}
|
||||
} else {
|
||||
emitError('Error: File type unknown');
|
||||
}
|
||||
|
||||
// reset input value
|
||||
hiddenFileInput.current.value = '';
|
||||
},
|
||||
[emitError, uploaddb]
|
||||
);
|
||||
|
||||
const handleIPC = useCallback((action) => {
|
||||
// Stop crashes when testing locally
|
||||
if (typeof window.process?.type === 'undefined') {
|
||||
if (action === 'help') {
|
||||
window.open('https://cpvalente.gitbook.io/ontime/');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.process?.type === 'renderer') {
|
||||
switch (action) {
|
||||
case 'min':
|
||||
window.ipcRenderer.send('set-window', 'to-tray');
|
||||
break;
|
||||
case 'max':
|
||||
window.ipcRenderer.send('set-window', 'to-max');
|
||||
break;
|
||||
case 'shutdown':
|
||||
window.ipcRenderer.send('shutdown', 'now');
|
||||
break;
|
||||
case 'help':
|
||||
window.ipcRenderer.send('send-to-link', 'help');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback(
|
||||
(e) => {
|
||||
// handle held key
|
||||
if (e.repeat) return;
|
||||
// check if the alt key is pressed
|
||||
if (e.ctrlKey) {
|
||||
if (e.key === ',') {
|
||||
// if we are in electron
|
||||
if (window.process?.type === undefined) return;
|
||||
if (window.process.type === 'renderer') {
|
||||
// open if not open
|
||||
isOpen ? onClose() : onOpen();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[isOpen, onClose, onOpen]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// attach the event listener
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
// remove the event listener
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
return (
|
||||
<VStack>
|
||||
<QuitIconBtn clickHandler={() => handleIPC('shutdown')} />
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiMaximize />}
|
||||
clickHandler={() => handleIPC('max')}
|
||||
tooltip='Show full window'
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiMinimize />}
|
||||
clickHandler={() => handleIPC('min')}
|
||||
tooltip='Close to tray'
|
||||
/>
|
||||
<div className={style.gap} />
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiHelpCircle />}
|
||||
clickHandler={() => handleIPC('help')}
|
||||
tooltip='Help'
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiSettings />}
|
||||
className={isOpen ? style.open : ''}
|
||||
clickHandler={onOpen}
|
||||
tooltip='Settings'
|
||||
isRound
|
||||
/>
|
||||
<div className={style.gap} />
|
||||
<input
|
||||
type='file'
|
||||
style={{ display: 'none' }}
|
||||
ref={hiddenFileInput}
|
||||
onChange={handleUpload}
|
||||
accept='.json, .xlsx'
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiUpload />}
|
||||
clickHandler={handleClick}
|
||||
tooltip='Import event list'
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiDownload />}
|
||||
clickHandler={downloadEvents}
|
||||
tooltip='Export event list'
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
MenuBar.propTypes = {
|
||||
isOpen: PropTypes.bool,
|
||||
onOpen: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { VStack } from '@chakra-ui/react';
|
||||
import { FiHelpCircle } from '@react-icons/all-files/fi/FiHelpCircle';
|
||||
import { FiMaximize } from '@react-icons/all-files/fi/FiMaximize';
|
||||
import { FiMinimize } from '@react-icons/all-files/fi/FiMinimize';
|
||||
import { FiSave } from '@react-icons/all-files/fi/FiSave';
|
||||
import { FiSettings } from '@react-icons/all-files/fi/FiSettings';
|
||||
import { FiUpload } from '@react-icons/all-files/fi/FiUpload';
|
||||
import { downloadEvents } from 'common/api/ontimeApi';
|
||||
|
||||
import QuitIconBtn from '../../common/components/buttons/QuitIconBtn';
|
||||
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
|
||||
import useElectronEvent from '../../common/hooks/useElectronEvent';
|
||||
|
||||
import style from './MenuBar.module.scss';
|
||||
|
||||
interface MenuBarProps {
|
||||
isSettingsOpen: boolean;
|
||||
onSettingsOpen: () => void;
|
||||
onSettingsClose: () => void;
|
||||
isUploadOpen: boolean;
|
||||
onUploadOpen: () => void;
|
||||
}
|
||||
|
||||
type Actions = 'min' | 'max' | 'shutdown' | 'help';
|
||||
|
||||
const buttonStyle = {
|
||||
fontSize: '1.5em',
|
||||
size: 'lg',
|
||||
colorScheme: 'white',
|
||||
};
|
||||
|
||||
export default function MenuBar(props: MenuBarProps) {
|
||||
const { isSettingsOpen, onSettingsOpen, onSettingsClose, isUploadOpen, onUploadOpen } = props;
|
||||
const { isElectron, sendToElectron } = useElectronEvent();
|
||||
|
||||
const actionHandler = useCallback((action: Actions) => {
|
||||
// Stop crashes when testing locally
|
||||
if (!isElectron) {
|
||||
if (action === 'help') {
|
||||
window.open('https://cpvalente.gitbook.io/ontime/');
|
||||
}
|
||||
} else {
|
||||
switch (action) {
|
||||
case 'min':
|
||||
sendToElectron('set-window', 'to-tray');
|
||||
break;
|
||||
case 'max':
|
||||
sendToElectron('set-window', 'to-max');
|
||||
break;
|
||||
case 'shutdown':
|
||||
sendToElectron('shutdown', 'now');
|
||||
break;
|
||||
case 'help':
|
||||
sendToElectron('send-to-link', 'help');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [sendToElectron, isElectron]);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyPress = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
// skip if not electron
|
||||
if (!isElectron) return;
|
||||
// handle held key
|
||||
if (event.repeat) return;
|
||||
|
||||
// check if the ctrl key is pressed
|
||||
if (event.ctrlKey) {
|
||||
// ctrl + , (settings)
|
||||
if (event.key === ',') {
|
||||
if (isElectron) {
|
||||
// open if not open
|
||||
isSettingsOpen ? onSettingsClose() : onSettingsOpen();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[isElectron, isSettingsOpen, onSettingsClose, onSettingsOpen]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyPress);
|
||||
};
|
||||
}, [handleKeyPress]);
|
||||
|
||||
return (
|
||||
<VStack>
|
||||
<QuitIconBtn clickHandler={() => actionHandler('shutdown')} />
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiMaximize />}
|
||||
clickHandler={() => actionHandler('max')}
|
||||
tooltip='Show full window'
|
||||
aria-label=''
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiMinimize />}
|
||||
clickHandler={() => actionHandler('min')}
|
||||
tooltip='Close to tray'
|
||||
aria-label=''
|
||||
/>
|
||||
<div className={style.gap} />
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiHelpCircle />}
|
||||
clickHandler={() => actionHandler('help')}
|
||||
tooltip='Help'
|
||||
aria-label=''
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiSettings />}
|
||||
className={isSettingsOpen ? style.open : ''}
|
||||
clickHandler={onSettingsOpen}
|
||||
tooltip='Settings'
|
||||
isRound
|
||||
aria-label=''
|
||||
/>
|
||||
<div className={style.gap} />
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiUpload />}
|
||||
className={isUploadOpen ? style.open : ''}
|
||||
clickHandler={onUploadOpen}
|
||||
tooltip='Upload event list'
|
||||
isRound
|
||||
aria-label=''
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
icon={<FiSave />}
|
||||
clickHandler={downloadEvents}
|
||||
tooltip='Export event list'
|
||||
aria-label=''
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
+3
-1
@@ -6,11 +6,13 @@ import MenuBar from '../MenuBar';
|
||||
|
||||
const onOpenHandler = jest.fn();
|
||||
const onCloseHandler = jest.fn();
|
||||
const isOpen = false;
|
||||
const onUploadOpenHandler = jest.fn();
|
||||
|
||||
const renderInMock = () => {
|
||||
render(
|
||||
<QueryClientProvider client={queryClientMock}>
|
||||
<MenuBar onOpen={onOpenHandler} onClose={onCloseHandler} />
|
||||
<MenuBar onOpen={onOpenHandler} onClose={onCloseHandler} isOpen={isOpen} onUploadOpen={onUploadOpenHandler} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Class Event Provider adds functions specific for handling event data
|
||||
*/
|
||||
export class DataProvider {
|
||||
/**
|
||||
* Merges two data objects
|
||||
* @param {object} existing
|
||||
* @param {object} newData
|
||||
*/
|
||||
static safeMerge(existing, newData) {
|
||||
const mergedData = { ...existing };
|
||||
|
||||
if (typeof newData?.events !== 'undefined') {
|
||||
mergedData.events = newData.events;
|
||||
}
|
||||
if (typeof newData?.event !== 'undefined') {
|
||||
mergedData.event = { ...newData.event };
|
||||
}
|
||||
if (typeof newData?.settings !== 'undefined') {
|
||||
mergedData.settings = { ...newData.settings };
|
||||
}
|
||||
if (typeof newData?.osc !== 'undefined') {
|
||||
mergedData.osc = { ...newData.osc };
|
||||
}
|
||||
if (typeof newData?.http !== 'undefined') {
|
||||
mergedData.http = { ...newData.http };
|
||||
}
|
||||
if (typeof newData?.aliases !== 'undefined') {
|
||||
mergedData.aliases = [...newData.aliases];
|
||||
}
|
||||
if (typeof newData?.userFields !== 'undefined') {
|
||||
mergedData.userFields = { ...existing.userFields, ...newData.userFields };
|
||||
}
|
||||
return mergedData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { DataProvider } from '../DataProvider';
|
||||
|
||||
describe('DataProvider', () => {
|
||||
describe('safeMerge()', () => {
|
||||
it('merges two objects ', () => {
|
||||
const oldData = {
|
||||
events: [{ event: 'old event' }],
|
||||
event: {
|
||||
title: 'old title',
|
||||
url: 'old url',
|
||||
endMessage: 'old end message',
|
||||
},
|
||||
osc: {
|
||||
port: 'old port',
|
||||
},
|
||||
settings: {
|
||||
app: 'ontime',
|
||||
version: 1,
|
||||
serverPort: 4001,
|
||||
lock: null,
|
||||
pinCode: null,
|
||||
timeFormat: '24',
|
||||
},
|
||||
userFields: {
|
||||
user0: 'old 0',
|
||||
user1: 'old 1',
|
||||
user2: 'old 2',
|
||||
user3: 'old 3',
|
||||
user4: 'old 4',
|
||||
user5: 'old 5',
|
||||
user6: 'old 6',
|
||||
user7: 'old 7',
|
||||
user8: 'old 8',
|
||||
user9: 'old 9',
|
||||
},
|
||||
};
|
||||
const newData = {
|
||||
events: [{ event: 'new event' }],
|
||||
event: {
|
||||
title: 'new title',
|
||||
url: 'new url',
|
||||
endMessage: 'old end message',
|
||||
publicInfo: 'new public info',
|
||||
},
|
||||
settings: {
|
||||
app: 'ontime',
|
||||
version: 1,
|
||||
serverPort: 4001,
|
||||
lock: null,
|
||||
pinCode: null,
|
||||
timeFormat: '24',
|
||||
},
|
||||
userFields: {
|
||||
user6: 'new 6',
|
||||
user7: 'new 7',
|
||||
user8: 'new 8',
|
||||
user9: 'new 9',
|
||||
},
|
||||
};
|
||||
|
||||
const expected = {
|
||||
events: [{ event: 'new event' }],
|
||||
event: {
|
||||
title: 'new title',
|
||||
url: 'new url',
|
||||
publicInfo: 'new public info',
|
||||
endMessage: 'old end message',
|
||||
},
|
||||
osc: {
|
||||
port: 'old port',
|
||||
},
|
||||
settings: {
|
||||
app: 'ontime',
|
||||
version: 1,
|
||||
serverPort: 4001,
|
||||
lock: null,
|
||||
pinCode: null,
|
||||
timeFormat: '24',
|
||||
},
|
||||
userFields: {
|
||||
user0: 'old 0',
|
||||
user1: 'old 1',
|
||||
user2: 'old 2',
|
||||
user3: 'old 3',
|
||||
user4: 'old 4',
|
||||
user5: 'old 5',
|
||||
user6: 'new 6',
|
||||
user7: 'new 7',
|
||||
user8: 'new 8',
|
||||
user9: 'new 9',
|
||||
},
|
||||
};
|
||||
const merged = DataProvider.safeMerge(oldData, newData);
|
||||
expect(merged).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { networkInterfaces } from 'os';
|
||||
import { fileHandler } from '../utils/parser.js';
|
||||
import { generateId } from '../utils/generate_id.js';
|
||||
import { resolveDbPath } from '../modules/loadDb.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
|
||||
// Create controller for GET request to '/ontime/poll'
|
||||
// Returns data for current state
|
||||
@@ -33,11 +34,7 @@ export const dbDownload = async (req, res) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Controller for POST request to /ontime/db
|
||||
* @returns none
|
||||
*/
|
||||
const upload = async (file, req, res) => {
|
||||
const uploadAndParse = async (file, req, res, options) => {
|
||||
if (!fs.existsSync(file)) {
|
||||
res.status(500).send({ message: 'Upload failed' });
|
||||
return;
|
||||
@@ -50,29 +47,18 @@ const upload = async (file, req, res) => {
|
||||
res.status(400).send({ message: result.message });
|
||||
} else if (result.message === 'success') {
|
||||
// explicitly write objects
|
||||
if (typeof result.data !== 'undefined') {
|
||||
if (typeof result.data?.events !== 'undefined') {
|
||||
data.events = result.data.events;
|
||||
global.timer.setupWithEventList(result.data?.events);
|
||||
}
|
||||
if (typeof result.data?.event !== 'undefined') {
|
||||
data.event = result.data.event;
|
||||
}
|
||||
if (typeof result.data?.settings !== 'undefined') {
|
||||
data.settings = result.data.settings;
|
||||
}
|
||||
if (typeof result.data?.osc !== 'undefined') {
|
||||
data.osc = result.data.osc;
|
||||
}
|
||||
if (typeof result.data?.http !== 'undefined') {
|
||||
data.http = result.data.http;
|
||||
}
|
||||
if (typeof result.data?.aliases !== 'undefined') {
|
||||
data.aliases = result.data.aliases;
|
||||
}
|
||||
if (typeof result.data?.userFields !== 'undefined') {
|
||||
data.userFields = result.data.userFields;
|
||||
if (typeof result !== 'undefined') {
|
||||
if (!options.onlyEvents) {
|
||||
const mergedData = DataProvider.safeMerge(data, result.data);
|
||||
data.event = mergedData.event;
|
||||
data.settings = mergedData.settings;
|
||||
data.osc = mergedData.osc;
|
||||
data.http = mergedData.http;
|
||||
data.aliases = mergedData.aliases;
|
||||
data.userFields = mergedData.userFields;
|
||||
}
|
||||
data.events = result.data.events || [];
|
||||
global.timer.setupWithEventList(result.data.events || []);
|
||||
await db.write();
|
||||
}
|
||||
res.sendStatus(200);
|
||||
@@ -297,9 +283,9 @@ export const dbUpload = async (req, res) => {
|
||||
res.status(400).send({ message: 'File not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const options = req.query;
|
||||
const file = req.file.path;
|
||||
upload(file, req, res);
|
||||
uploadAndParse(file, req, res, options);
|
||||
};
|
||||
|
||||
// Create controller for POST request to '/ontime/dbpath'
|
||||
@@ -309,5 +295,5 @@ export const dbPathToUpload = async (req, res) => {
|
||||
res.status(400).send({ message: 'Path to file not found' });
|
||||
return;
|
||||
}
|
||||
upload(req.body.path, req, res);
|
||||
uploadAndParse(req.body.path, req, res);
|
||||
};
|
||||
|
||||
@@ -564,6 +564,9 @@ describe('test parseExcel function', () => {
|
||||
[],
|
||||
['Event Name', 'Test Event'],
|
||||
['Event URL', 'www.carlosvalente.com'],
|
||||
['Public Info', 'test public info'],
|
||||
['Backstage Info', 'test backstage info'],
|
||||
['End Message', 'test end message'],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
@@ -640,6 +643,14 @@ describe('test parseExcel function', () => {
|
||||
[],
|
||||
];
|
||||
|
||||
const expectedParsedEvent = {
|
||||
title: 'Test Event',
|
||||
url: 'www.carlosvalente.com',
|
||||
publicInfo: 'test public info',
|
||||
backstageInfo: 'test backstage info',
|
||||
endMessage: 'test end message',
|
||||
};
|
||||
|
||||
const expectedParsedEvents = [
|
||||
{
|
||||
timeStart: 25200000,
|
||||
@@ -681,6 +692,7 @@ describe('test parseExcel function', () => {
|
||||
|
||||
const parsedData = await parseExcel_v1(testdata);
|
||||
|
||||
expect(parsedData.event).toStrictEqual(expectedParsedEvent);
|
||||
expect(parsedData.events).toBeDefined();
|
||||
expect(parsedData.events.title).toBe(expectedParsedEvents.title);
|
||||
expect(parsedData.events.presenter).toBe(expectedParsedEvents.presenter);
|
||||
|
||||
@@ -68,6 +68,9 @@ export const parseExcel_v1 = async (excelData) => {
|
||||
.forEach((row) => {
|
||||
let eventTitleNext = false;
|
||||
let eventUrlNext = false;
|
||||
let publicInfoNext = false;
|
||||
let backstageInfoNext = false;
|
||||
let endMessageNext = false;
|
||||
const event = {};
|
||||
|
||||
row.forEach((column, j) => {
|
||||
@@ -78,6 +81,15 @@ export const parseExcel_v1 = async (excelData) => {
|
||||
} else if (eventUrlNext) {
|
||||
eventData.url = column;
|
||||
eventUrlNext = false;
|
||||
} else if (publicInfoNext) {
|
||||
eventData.publicInfo = column;
|
||||
publicInfoNext = false;
|
||||
} else if (backstageInfoNext) {
|
||||
eventData.backstageInfo = column;
|
||||
backstageInfoNext = false;
|
||||
} else if (endMessageNext) {
|
||||
eventData.endMessage = column;
|
||||
endMessageNext = false;
|
||||
} else if (j === timeStartIndex) {
|
||||
event.timeStart = parseExcelDate(column);
|
||||
} else if (j === timeEndIndex) {
|
||||
@@ -128,6 +140,15 @@ export const parseExcel_v1 = async (excelData) => {
|
||||
case 'event url':
|
||||
eventUrlNext = true;
|
||||
break;
|
||||
case 'public info':
|
||||
publicInfoNext = true;
|
||||
break;
|
||||
case 'backstage info':
|
||||
backstageInfoNext = true;
|
||||
break;
|
||||
case 'end message':
|
||||
endMessageNext = true;
|
||||
break;
|
||||
case 'time start':
|
||||
case 'start':
|
||||
timeStartIndex = j;
|
||||
@@ -329,7 +350,6 @@ export const fileHandler = async (file) => {
|
||||
let res = {};
|
||||
|
||||
// check which file type are we dealing with
|
||||
|
||||
if (file.endsWith('.xlsx')) {
|
||||
try {
|
||||
const excelData = xlsx
|
||||
@@ -341,7 +361,10 @@ export const fileHandler = async (file) => {
|
||||
// we only look at worksheets called ontime or event schedule
|
||||
if (excelData?.data) {
|
||||
const dataFromExcel = await parseExcel_v1(excelData.data);
|
||||
res.data = await parseJson_v1(dataFromExcel);
|
||||
res.data = {};
|
||||
res.data.events = parseEvents_v1(dataFromExcel);
|
||||
res.data.event = parseEvent_v1(dataFromExcel, true);
|
||||
res.data.userFields = parseUserFields_v1(dataFromExcel);
|
||||
res.message = 'success';
|
||||
} else {
|
||||
console.log('Error: No sheets found named ontime or event schedule');
|
||||
|
||||
@@ -14,37 +14,41 @@ export const parseEvents_v1 = (data) => {
|
||||
if ('events' in data) {
|
||||
console.log('Found events definition, importing...');
|
||||
const events = [];
|
||||
const ids = [];
|
||||
for (const e of data.events) {
|
||||
// cap number of events
|
||||
if (events.length >= MAX_EVENTS) {
|
||||
console.log(`ERROR: Reached limit number of ${MAX_EVENTS} events`);
|
||||
break;
|
||||
}
|
||||
|
||||
// double check unique ids
|
||||
if (ids.indexOf(e?.id) !== -1) {
|
||||
console.log('ERROR: ID collision on import, skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e.type === 'event') {
|
||||
const event = validateEvent_v1(e);
|
||||
if (event != null) {
|
||||
events.push(event);
|
||||
ids.push(event.id);
|
||||
try {
|
||||
const ids = [];
|
||||
for (const e of data.events) {
|
||||
// cap number of events
|
||||
if (events.length >= MAX_EVENTS) {
|
||||
console.log(`ERROR: Reached limit number of ${MAX_EVENTS} events`);
|
||||
break;
|
||||
}
|
||||
|
||||
// double check unique ids
|
||||
if (ids.indexOf(e?.id) !== -1) {
|
||||
console.log('ERROR: ID collision on import, skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e.type === 'event') {
|
||||
const event = validateEvent_v1(e);
|
||||
if (event != null) {
|
||||
events.push(event);
|
||||
ids.push(event.id);
|
||||
}
|
||||
} else if (e.type === 'delay') {
|
||||
events.push({
|
||||
...delayDef,
|
||||
duration: e.duration,
|
||||
id: e.id || generateId(),
|
||||
});
|
||||
} else if (e.type === 'block') {
|
||||
events.push({ ...blockDef, id: e.id || generateId() });
|
||||
} else {
|
||||
console.log('ERROR: undefined event type, skipping');
|
||||
}
|
||||
} else if (e.type === 'delay') {
|
||||
events.push({
|
||||
...delayDef,
|
||||
duration: e.duration,
|
||||
id: e.id || generateId(),
|
||||
});
|
||||
} else if (e.type === 'block') {
|
||||
events.push({ ...blockDef, id: e.id || generateId() });
|
||||
} else {
|
||||
console.log('ERROR: undefined event type, skipping');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error ${error}`);
|
||||
}
|
||||
// write to db
|
||||
newEvents = events;
|
||||
@@ -73,7 +77,7 @@ export const parseEvent_v1 = (data, enforce) => {
|
||||
endMessage: e.endMessage || dbModelv1.event.endMessage,
|
||||
};
|
||||
} else if (enforce) {
|
||||
newEvent = dbModelv1.event;
|
||||
newEvent = { ...dbModelv1.event };
|
||||
console.log(`Created event object in db`);
|
||||
}
|
||||
return newEvent;
|
||||
@@ -137,7 +141,7 @@ export const parseOsc_v1 = (data, enforce) => {
|
||||
...osc,
|
||||
};
|
||||
} else if (enforce) {
|
||||
newOsc = dbModelv1.osc;
|
||||
newOsc = { ...dbModelv1.osc };
|
||||
console.log(`Created OSC object in db`);
|
||||
}
|
||||
return newOsc;
|
||||
@@ -165,7 +169,7 @@ export const parseHttp_v1 = (data, enforce) => {
|
||||
...http,
|
||||
};
|
||||
} else if (enforce) {
|
||||
newHttp.http = dbModelv1.http;
|
||||
newHttp.http = { ...dbModelv1.http };
|
||||
console.log(`Created http object in db`);
|
||||
}
|
||||
return newHttp;
|
||||
@@ -181,23 +185,27 @@ export const parseAliases_v1 = (data) => {
|
||||
if ('aliases' in data) {
|
||||
console.log('Found Aliases definition, importing...');
|
||||
const ids = [];
|
||||
for (const a of data.aliases) {
|
||||
// double check unique ids
|
||||
if (ids.indexOf(a?.id) !== -1) {
|
||||
console.log('ERROR: ID collision on import, skipping');
|
||||
continue;
|
||||
}
|
||||
const newAlias = {
|
||||
id: a.id || generateId(),
|
||||
enabled: a.enabled || false,
|
||||
alias: a.alias || '',
|
||||
pathAndParams: a.pathAndParams || '',
|
||||
};
|
||||
try {
|
||||
for (const a of data.aliases) {
|
||||
// double check unique ids
|
||||
if (ids.indexOf(a?.id) !== -1) {
|
||||
console.log('ERROR: ID collision on import, skipping');
|
||||
continue;
|
||||
}
|
||||
const newAlias = {
|
||||
id: a.id || generateId(),
|
||||
enabled: a.enabled || false,
|
||||
alias: a.alias || '',
|
||||
pathAndParams: a.pathAndParams || '',
|
||||
};
|
||||
|
||||
ids.push(newAlias.id);
|
||||
newAliases.push(newAlias);
|
||||
ids.push(newAlias.id);
|
||||
newAliases.push(newAlias);
|
||||
}
|
||||
console.log(`Uploaded ${newAliases?.length || 0} alias(es)`);
|
||||
} catch (error) {
|
||||
console.log(`Error: ${error}`);
|
||||
}
|
||||
console.log(`Uploaded ${newAliases?.length || 0} alias(es)`);
|
||||
}
|
||||
return newAliases;
|
||||
};
|
||||
@@ -208,19 +216,23 @@ export const parseAliases_v1 = (data) => {
|
||||
* @returns {object} - event object data
|
||||
*/
|
||||
export const parseUserFields_v1 = (data) => {
|
||||
const newUserFields = dbModelv1.userFields;
|
||||
const newUserFields = { ...dbModelv1.userFields };
|
||||
|
||||
if ('userFields' in data) {
|
||||
console.log('Found User Fields definition, importing...');
|
||||
// we will only be importing the fields we know, so look for that
|
||||
let fieldsFound = 0;
|
||||
for (const n in newUserFields) {
|
||||
if (n in data.userFields) {
|
||||
fieldsFound++;
|
||||
newUserFields[n] = data.userFields[n];
|
||||
try {
|
||||
let fieldsFound = 0;
|
||||
for (const n in newUserFields) {
|
||||
if (n in data.userFields) {
|
||||
fieldsFound++;
|
||||
newUserFields[n] = data.userFields[n];
|
||||
}
|
||||
}
|
||||
console.log(`Uploaded ${fieldsFound} user fields`);
|
||||
} catch (error) {
|
||||
console.log(`Error: ${error}`);
|
||||
}
|
||||
console.log(`Uploaded ${fieldsFound} user fields`);
|
||||
}
|
||||
return { ...dbModelv1.userFields, ...newUserFields };
|
||||
return { ...newUserFields };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user