Compare commits

..

74 Commits

Author SHA1 Message Date
arc-alex c6f61b16cb remove console.log 2024-01-06 18:26:37 +01:00
arc-alex 08a32a7265 add ExcelImport 2024-01-06 18:21:22 +01:00
arc-alex 341d554bc5 add link to docs 2024-01-06 16:23:43 +01:00
arc-alex 4e49ca3ebe useEffect on is open 2024-01-06 15:41:55 +01:00
arc-alex 19536c3ea8 Reareange everything to match the 5 steps 2024-01-06 15:38:16 +01:00
arc-alex dc8b55aa53 use maybeAxiosError 2024-01-03 01:54:56 +01:00
arc-alex cb6dee552f display errors in UI 2024-01-03 01:52:30 +01:00
arc-alex ba89673d5f clarify updateSheetState 2024-01-03 00:55:31 +01:00
arc-alex a719f5827e use the send-to-link function 2024-01-02 15:57:14 +01:00
arc-alex 7fcf8153af cleanup and rename 2024-01-02 15:49:10 +01:00
arc-alex e0b2ea8f5c sort import and fix variant 2023-12-22 14:25:22 +01:00
arc-alex be92fa43fa allow x as boolean true from sheet 2023-12-22 14:17:36 +01:00
arc-alex fda1dbdd5f add some comments 2023-12-22 13:57:21 +01:00
arc-alex 8ae1c6de42 use api directly 2023-12-19 13:33:57 +01:00
Carlos Valente fa56f48461 refactor: get data from query 2023-12-19 12:34:00 +01:00
arc-alex 364d698bbc dont save sheet settings 2023-12-18 14:23:03 +01:00
arc-alex 8fa7392698 dont save 2023-12-16 23:56:15 +01:00
arc-alex 2464ac1bd9 change some naming 2023-12-16 23:37:44 +01:00
Carlos Valente a6683fd2cd style: small UI suggestion 2023-12-15 22:25:26 +01:00
arc-alex d2c6d46585 use exist function befor pull or push 2023-12-15 13:40:53 +01:00
Carlos Valente 95d7b61529 refactor: move fetching to react query 2023-12-15 10:49:52 +01:00
arc-alex 884947374d improve get state check 2023-12-13 18:56:49 +01:00
arc-alex e52ff58d74 coerce boolean types from sheet 2023-12-13 17:37:33 +01:00
arc-alex 0024b02638 small refinment 2023-12-13 17:31:33 +01:00
arc-alex d763f3f6b9 open using external browser 2023-12-12 15:46:29 +01:00
arc-alex fdee764c28 fix: deepsource recommendations 2023-12-12 15:06:16 +01:00
arc-alex a750c04bad try to forward err to the user 2023-12-12 14:37:32 +01:00
arc-alex a7a7a7d886 fix key check and ensureDirectory on startup 2023-12-12 14:25:22 +01:00
arc-alex a98bd0f4a4 ensure sheets folder 2023-12-12 11:52:04 +01:00
arc-alex c5b8a133b5 Merge commit master into google-sheets-lite 2023-12-12 11:43:10 +01:00
arc-alex b37494109a ifx: use template string not concatination 2023-11-27 23:27:10 +01:00
arc-alex aaa1fb9368 fix: saveClientSecrets type object 2023-11-27 23:24:36 +01:00
arc-alex cb198bc3fe comments 2023-11-26 17:29:12 +01:00
arc-alex 6880f459b6 check keys of client secrets 2023-11-26 17:11:55 +01:00
arc-alex 7b34c8f3b1 push project data 2023-11-26 16:48:34 +01:00
arc-alex c714727801 useEffect 2023-11-26 14:26:05 +01:00
arc-alex 8fc70fa162 cellRequenstFromEvent change 2023-11-26 14:23:12 +01:00
arc-alex 54c4a7f68c add test 2023-11-26 14:16:50 +01:00
arc-alex 9820c71a2d move util functions to separat file for testing 2023-11-25 20:03:30 +01:00
arc-alex 2ff7d0f546 pass lint 2023-11-25 19:56:19 +01:00
arc-alex f29ed1b05a types for getA1Notation function 2023-11-25 19:54:33 +01:00
arc-alex b1de7ffe5b Merge branch 'master' into google-sheets-lite 2023-11-25 19:39:53 +01:00
arc-alex 46d17e0570 extend authServer timeout if opened again 2023-11-24 21:17:30 +01:00
arc-alex 38e3ef7586 dont save login token Between Sessions 2023-11-24 20:07:50 +01:00
arc-alex e8bcbb4291 clean up sheet flow 2023-11-24 17:47:21 +01:00
arc-alex 111df94d6d parseGoogleSheet from db 2023-11-24 16:49:37 +01:00
arc-alex d32d8a547d lint 2023-11-23 18:38:01 +01:00
arc-alex 5bc2c2f241 remove unused 2023-11-23 17:55:22 +01:00
arc-alex 28533464d4 add db test for google sheet 2023-11-23 17:49:08 +01:00
arc-alex 4f1fa2053f sheet settings with feedback 2023-11-23 17:17:13 +01:00
arc-alex b15b4b48a7 add googleSheet to db model 2023-11-23 14:53:22 +01:00
arc-alex ef2fa99673 finde correct range 2023-11-19 20:15:07 +01:00
arc-alex 85ee9de528 lint 2023-11-19 19:39:13 +01:00
arc-alex e52a75db7d test sheet exist and get worksheet id 2023-11-19 19:24:56 +01:00
arc-alex 07ebb5a321 fix merge 2023-11-19 19:24:31 +01:00
arc-alex 071cae4cc6 Merge remote-tracking branch 'org/master' into google-sheets-lite 2023-11-19 18:28:57 +01:00
arc-alex 543e365f82 push to sheet 2023-11-19 17:04:14 +01:00
arc-alex a7c9ce876c get cell location when from parseExcel 2023-11-19 17:03:50 +01:00
arc-alex ae4e2ceafe push function 2023-11-18 21:41:40 +01:00
arc-alex b77db4cbf6 rename to pull 2023-11-18 21:17:44 +01:00
arc-alex fe67e65a95 return auth url on first clik 2023-11-18 21:15:39 +01:00
arc-alex 785695c01c clear rundownState on modal close 2023-11-18 21:01:32 +01:00
arc-alex 65efebe180 Preview Sheet 2023-11-18 20:58:57 +01:00
arc-alex 52a9c85dbc remove duplicate routs 2023-11-18 17:57:13 +01:00
arc-alex 6b2b6dc6dd auth flow 2023-11-18 17:53:08 +01:00
arc-alex 31b46bbb34 handle client secret upload 2023-11-18 17:24:29 +01:00
arc-alex 38cf3a7b63 ensure sheets directory 2023-11-18 17:23:19 +01:00
arc-alex 81cf30daa0 use only sheets portion of google api 2023-11-17 16:15:28 +01:00
Carlos Valente e9683c6cab wip: placeholder sheets modal 2023-11-17 15:21:19 +01:00
arc-alex 370777326d add jsdoc comments 2023-11-15 10:26:04 +01:00
arc-alex 309f622077 update lock file 2023-11-15 10:09:50 +01:00
arc-alex 285b05bd65 comment 2023-11-15 09:06:03 +01:00
arc-alex 05207189cf add google sheet functions 2023-11-15 09:06:03 +01:00
arc-alex d70bb2174a install google apis 2023-11-15 09:06:03 +01:00
53 changed files with 322 additions and 616 deletions
+1 -13
View File
@@ -44,8 +44,7 @@ jobs:
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Build and push stable release
if: github.event.release.prerelease == false
- name: Build and push Docker images
uses: docker/build-push-action@v4.0.0
with:
context: .
@@ -55,14 +54,3 @@ jobs:
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
- name: Build and push pre-release
if: github.event.release.prerelease == true
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
# Push is a shorthand for --output=type=registry
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:nightly
-1
View File
@@ -89,7 +89,6 @@ While it should allow for a generic setup, it might need to be modified to fit y
From the project root, run the following commands
- __Install the project dependencies__ by running `pnpm i`
- __Build packages__ by running `pnpm build:localdocker`
- __Build docker image from__ by running `docker build -t getontime/ontime`
- __Run docker image from compose__ by running `docker-compose up -d`
+1 -1
View File
@@ -4,7 +4,7 @@
## Download the latest releases here
<div style="display: flex; justify-content: space-around">
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS-arm64.dmg"><img alt="Download MacOS" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/mac-download.png"/></a>
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-macOS.dmg"><img alt="Download MacOS" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/mac-download.png"/></a>
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-win64.exe"><img alt="Download Windows" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/win-download.png"/></a>
<a href="https://github.com/cpvalente/ontime/releases/latest/download/ontime-linux.AppImage"><img alt="Download Linux" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/linux-download.png"/></a>
<a href="https://hub.docker.com/r/getontime/ontime"><img alt="Get from Dockerhub" src="https://github.com/cpvalente/ontime/blob/master/.github/aux-images/dockerhub.png"/></a>
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-ui",
"version": "2.28.17",
"version": "2.21.3",
"private": true,
"dependencies": {
"@chakra-ui/react": "^2.7.0",
@@ -40,7 +40,6 @@
"build:local": "cross-env NODE_ENV=local vite build",
"build:electron": "cross-env NODE_ENV=local vite build",
"build:docker": "vite build",
"build:localdocker": "cross-env NODE_ENV=local vite build",
"lint": "eslint . --quiet",
"lint-staged": "eslint",
"test": "vitest",
+2 -2
View File
@@ -253,7 +253,7 @@ export const uploadSheetClientFile = async (file: File) => {
const formData = new FormData();
formData.append('userFile', file);
const res = await axios
.post(`${ontimeURL}/sheet/clientsecret`, formData, {
.post(`${ontimeURL}/sheet/clientsecrect`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
@@ -266,7 +266,7 @@ export const uploadSheetClientFile = async (file: File) => {
* @description STEP 1 test
*/
export const getClientSecrect = async () => {
const response = await axios.get(`${ontimeURL}/sheet/clientsecret`);
const response = await axios.get(`${ontimeURL}/sheet/clientsecrect`);
return response.data;
};
@@ -17,8 +17,9 @@ interface TimeInputProps {
time?: number;
delay?: number;
placeholder: string;
validationHandler: (entry: TimeEntryField, val: number) => boolean;
previousEnd?: number;
tooltip?: string;
warning?: string;
}
function ButtonInitial(name: TimeEntryField) {
@@ -28,15 +29,25 @@ function ButtonInitial(name: TimeEntryField) {
return '';
}
function ButtonTooltip(name: TimeEntryField, tooltip?: string) {
if (name === 'timeStart') return `Start${tooltip ? `: ${tooltip}` : ''}`;
if (name === 'timeEnd') return `End${tooltip ? `: ${tooltip}` : ''}`;
if (name === 'durationOverride') return `Duration${tooltip ? `: ${tooltip}` : ''}`;
function ButtonTooltip(name: TimeEntryField, warning?: string) {
if (name === 'timeStart') return `Start${warning ? `: ${warning}` : ''}`;
if (name === 'timeEnd') return `End${warning ? `: ${warning}` : ''}`;
if (name === 'durationOverride') return `Duration${warning ? `: ${warning}` : ''}`;
return '';
}
export default function TimeInput(props: TimeInputProps) {
const { id, name, submitHandler, time = 0, delay = 0, placeholder, previousEnd = 0 } = props;
const {
id,
name,
submitHandler,
time = 0,
delay = 0,
placeholder,
validationHandler,
previousEnd = 0,
warning,
} = props;
const { emitError } = useEmitLog();
const inputRef = useRef<HTMLInputElement | null>(null);
const [value, setValue] = useState<string>('');
@@ -92,12 +103,15 @@ export default function TimeInput(props: TimeInputProps) {
// check if time is different from before
if (newValMillis === time) return false;
// validate with parent
if (!validationHandler(name, newValMillis)) return false;
// update entry
submitHandler(name, newValMillis);
return true;
},
[name, previousEnd, submitHandler, time],
[name, previousEnd, submitHandler, time, validationHandler],
);
/**
@@ -157,11 +171,11 @@ export default function TimeInput(props: TimeInputProps) {
const isDelayed = delay !== 0;
const inputClasses = cx([style.timeInput, isDelayed ? style.delayed : null]);
const buttonClasses = cx([style.inputButton, isDelayed ? style.delayed : null]);
const buttonClasses = cx([style.inputButton, isDelayed ? style.delayed : null, warning ? style.warn : null]);
const TooltipLabel = useMemo(() => {
return ButtonTooltip(name, '');
}, [name]);
return ButtonTooltip(name, warning);
}, [name, warning]);
const ButtonText = useMemo(() => {
return ButtonInitial(name);
@@ -380,12 +380,5 @@ export const getOperatorOptions = (userFields: UserFields, timeFormat: TimeForma
user9: userFields.user9 || 'user9',
},
},
{
id: 'shouldEdit',
title: 'Edit user field',
description: 'Allows editing an events user field by long pressing on it. Needs a selected highlighted field',
type: 'boolean',
defaultValue: false,
},
];
};
@@ -1,4 +1,7 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-nocheck -- working on it
import { useMutation, useQuery } from '@tanstack/react-query';
import { OSCSettings } from 'ontime-types';
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { OSC_SETTINGS } from '../api/apiConstants';
@@ -10,10 +13,7 @@ import { ontimeQueryClient } from '../queryClient';
export default function useOscSettings() {
const { data, status, isFetching, isError, refetch } = useQuery({
queryKey: OSC_SETTINGS,
queryFn: async () => {
const oscData = await getOSC();
return { ...oscData, portIn: String(oscData.portIn), portOut: String(oscData.portOut) };
},
queryFn: getOSC,
placeholderData: oscPlaceholderSettings,
retry: 5,
retryDelay: (attempt: number) => attempt * 2500,
@@ -21,7 +21,8 @@ export default function useOscSettings() {
networkMode: 'always',
});
return { data: data ?? oscPlaceholderSettings, status, isFetching, isError, refetch };
// we need to jump through some hoops because of the type op port
return { data: data! as unknown as OSCSettings, status, isFetching, isError, refetch };
}
export function useOscSettingsMutation() {
@@ -1 +1,44 @@
export type TimeEntryField = 'timeStart' | 'timeEnd' | 'durationOverride';
/**
* @description Checks which field the value relates to
*/
export const handleTimeEntry = (
field: TimeEntryField,
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: TimeEntryField,
value: number,
timeStart: number,
timeEnd: number,
): { value: boolean; warnings: { start?: string; end?: string; duration?: string } } => {
const validate = { value: true, warnings: { start: '', end: '', duration: '' } };
const { start, end } = handleTimeEntry(field, value, timeStart, timeEnd);
if (end < start) {
validate.warnings.start = 'Start time later than end time';
}
return validate;
};
@@ -1,12 +1,13 @@
import { memo } from 'react';
import { memo, useState } from 'react';
import { Select, Switch } from '@chakra-ui/react';
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
import { calculateDuration, dayInMs, millisToString } from 'ontime-utils';
import { calculateDuration, millisToString } from 'ontime-utils';
import TimeInput from '../../../common/components/input/time-input/TimeInput';
import { useEventAction } from '../../../common/hooks/useEventAction';
import { millisToDelayString } from '../../../common/utils/dateConfig';
import { cx } from '../../../common/utils/styleUtils';
import { TimeEntryField, validateEntry } from '../../../common/utils/timesManager';
import style from '../EventEditor.module.scss';
@@ -28,13 +29,21 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
const { eventId, timeStart, timeEnd, duration, delay, isPublic, endAction, timerType } = props;
const { updateEvent } = useEventAction();
const [warning, setWarnings] = useState({ start: '', end: '', duration: '' });
const timerValidationHandler = (entry: TimeEntryField, val: number) => {
const valid = validateEntry(entry, val, timeStart, timeEnd);
setWarnings((prev) => ({ ...prev, ...valid.warnings }));
return valid.value;
};
const handleSubmit = (field: TimeActions, value: number | string | boolean) => {
const newEventData: Partial<OntimeEvent> = { id: eventId };
switch (field) {
case 'durationOverride': {
// duration defines timeEnd
newEventData.duration = value as number;
newEventData.timeEnd = timeStart + ((value as number) % dayInMs);
newEventData.timeEnd = timeStart + (value as number);
break;
}
case 'timeStart': {
@@ -78,9 +87,11 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
id='timeStart'
name='timeStart'
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
time={timeStart}
delay={delay}
placeholder='Start'
warning={warning.start}
/>
<label className={inputTimeLabels} htmlFor='timeEnd'>
{endLabel}
@@ -89,9 +100,11 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
id='timeEnd'
name='timeEnd'
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
time={timeEnd}
delay={delay}
placeholder='End'
warning={warning.end}
/>
<label className={style.inputLabel} htmlFor='durationOverride'>
Duration
@@ -100,8 +113,10 @@ const EventEditorTimes = (props: EventEditorTimesProps) => {
id='durationOverride'
name='durationOverride'
submitHandler={handleSubmit}
validationHandler={timerValidationHandler}
time={duration}
placeholder='Duration'
warning={warning.duration}
/>
</div>
<div className={style.timeSettings}>
+2 -3
View File
@@ -1,7 +1,6 @@
import { memo, useCallback, useEffect, useState } from 'react';
import { VStack } from '@chakra-ui/react';
import { IoCloud } from '@react-icons/all-files/io5/IoCloud';
import { IoCloudOutline } from '@react-icons/all-files/io5/IoCloudOutline';
import { IoCalendarOutline } from '@react-icons/all-files/io5/IoCalendarOutline';
import { IoColorWand } from '@react-icons/all-files/io5/IoColorWand';
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
@@ -183,7 +182,7 @@ const MenuBar = (props: MenuBarProps) => {
<TooltipActionBtn
{...buttonStyle}
isDisabled={appMode === AppMode.Run}
icon={isSheetsOpen ? <IoCloud /> : <IoCloudOutline />}
icon={<IoCalendarOutline />}
className={isSheetsOpen ? style.open : ''}
clickHandler={onSheetsOpen}
tooltip='Sheets'
@@ -1,3 +1,5 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-nocheck -- working on it
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { FormControl, Input, Switch } from '@chakra-ui/react';
@@ -57,6 +59,7 @@ export default function OscSettings() {
};
const resetForm = () => {
// @ts-expect-error -- we know the types dont match
reset(data);
};
@@ -119,7 +119,6 @@ export default function AppSettingsModal() {
<option value='en'>English</option>
<option value='fr'>French</option>
<option value='de'>German</option>
<option value='it'>Italian</option>
<option value='no'>Norwegian</option>
<option value='pt'>Portuguese</option>
<option value='es'>Spanish</option>
@@ -58,7 +58,7 @@ export default function SheetsModal(props: SheetsModalProps) {
const [id, setSheetId] = useState('');
const [worksheet, setWorksheet] = useState('');
const [worksheetOptions, setWorksheetOptions] = useState<string[]>([]);
const [worksheetOptions, setWorksheetOptions] = useState(new Array<string>());
const [direction, setDirection] = useState('none');
const excelFileOptions = useRef<ExcelImportMap>(defaultExcelImportMap);
@@ -66,19 +66,19 @@ export default function SheetsModal(props: SheetsModalProps) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [state, setState] = useState({
clientSecret: { complete: false, message: '' },
authenticate: { complete: false, message: '' },
id: { complete: false, message: '' },
worksheet: { complete: false, message: '' },
pullPush: { complete: false, message: '' },
clientSecret: { complet: false, message: '' },
authenticate: { complet: false, message: '' },
id: { complet: false, message: '' },
worksheet: { complet: false, message: '' },
pullPush: { complet: false, message: '' },
});
useEffect(() => {
if (isOpen) {
setDirection('none');
testClientSecret();
if (state.clientSecret.complete) testAuthentication();
if (state.authenticate.complete) testSheetId();
testClientSecrect();
if (state.clientSecret.complet) testAuthentication();
if (state.authenticate.complet) testSheetId();
}
}, [isOpen]);
@@ -97,44 +97,44 @@ export default function SheetsModal(props: SheetsModalProps) {
const handleFile = (event: ChangeEvent<HTMLInputElement>) => {
if (!event.target.files?.length) {
setState({
clientSecret: { complete: false, message: 'Missing file' },
authenticate: { complete: false, message: '' },
id: { complete: false, message: '' },
worksheet: { complete: false, message: '' },
pullPush: { complete: false, message: '' },
clientSecret: { complet: false, message: 'Missing file' },
authenticate: { complet: false, message: '' },
id: { complet: false, message: '' },
worksheet: { complet: false, message: '' },
pullPush: { complet: false, message: '' },
});
return;
}
const selectedFile = event.target.files[0];
uploadSheetClientFile(selectedFile)
.then(() => {
setState({ ...state, clientSecret: { complete: true, message: '' } });
setState({ ...state, clientSecret: { complet: true, message: '' } });
})
.catch((err) => {
const message = maybeAxiosError(err);
setState({
clientSecret: { complete: false, message },
authenticate: { complete: false, message: '' },
id: { complete: false, message: '' },
worksheet: { complete: false, message: '' },
pullPush: { complete: false, message: '' },
clientSecret: { complet: false, message },
authenticate: { complet: false, message: '' },
id: { complet: false, message: '' },
worksheet: { complet: false, message: '' },
pullPush: { complet: false, message: '' },
});
});
};
const testClientSecret = () => {
const testClientSecrect = () => {
getClientSecrect()
.then(() => {
setState({ ...state, clientSecret: { complete: true, message: '' } });
setState({ ...state, clientSecret: { complet: true, message: '' } });
})
.catch((err) => {
const message = maybeAxiosError(err);
setState({
clientSecret: { complete: false, message },
authenticate: { complete: false, message: '' },
id: { complete: false, message: '' },
worksheet: { complete: false, message: '' },
pullPush: { complete: false, message: '' },
clientSecret: { complet: false, message },
authenticate: { complet: false, message: '' },
id: { complet: false, message: '' },
worksheet: { complet: false, message: '' },
pullPush: { complet: false, message: '' },
});
});
};
@@ -150,10 +150,10 @@ export default function SheetsModal(props: SheetsModalProps) {
const message = maybeAxiosError(err);
setState({
...state,
authenticate: { complete: false, message },
id: { complete: false, message: '' },
worksheet: { complete: false, message: '' },
pullPush: { complete: false, message: '' },
authenticate: { complet: false, message },
id: { complet: false, message: '' },
worksheet: { complet: false, message: '' },
pullPush: { complet: false, message: '' },
});
});
};
@@ -161,16 +161,16 @@ export default function SheetsModal(props: SheetsModalProps) {
const testAuthentication = () => {
getAuthentication()
.then(() => {
setState({ ...state, authenticate: { complete: true, message: '' } });
setState({ ...state, authenticate: { complet: true, message: '' } });
})
.catch((error) => {
const message = maybeAxiosError(error);
setState({
...state,
authenticate: { complete: false, message },
id: { complete: false, message: '' },
worksheet: { complete: false, message: '' },
pullPush: { complete: false, message: '' },
authenticate: { complet: false, message },
id: { complet: false, message: '' },
worksheet: { complet: false, message: '' },
pullPush: { complet: false, message: '' },
});
});
};
@@ -179,16 +179,16 @@ export default function SheetsModal(props: SheetsModalProps) {
const testSheetId = () => {
postId(id)
.then((data) => {
setState({ ...state, id: { complete: true, message: '' } });
setState({ ...state, id: { complet: true, message: '' } });
setWorksheetOptions(data.worksheetOptions);
})
.catch((err) => {
const message = maybeAxiosError(err);
setState({
...state,
id: { complete: false, message },
worksheet: { complete: false, message: '' },
pullPush: { complete: false, message: '' },
id: { complet: false, message },
worksheet: { complet: false, message: '' },
pullPush: { complet: false, message: '' },
});
setWorksheetOptions([]);
});
@@ -200,11 +200,11 @@ export default function SheetsModal(props: SheetsModalProps) {
setWorksheet(value);
postWorksheet(id, worksheet)
.then(() => {
setState({ ...state, worksheet: { complete: true, message: '' } });
setState({ ...state, worksheet: { complet: true, message: '' } });
})
.catch((err) => {
const message = maybeAxiosError(err);
setState({ ...state, worksheet: { complete: false, message }, pullPush: { complete: false, message: '' } });
setState({ ...state, worksheet: { complet: false, message }, pullPush: { complet: false, message: '' } });
});
};
@@ -225,21 +225,16 @@ export default function SheetsModal(props: SheetsModalProps) {
.catch((error) => {
const message = maybeAxiosError(error);
setDirection('none');
setState({ ...state, pullPush: { complete: false, message } });
setState({ ...state, pullPush: { complet: false, message } });
});
};
const handlePushData = () => {
postPushSheet(id, excelFileOptions.current)
.then(() => {
setDirection('none');
setState({ ...state, pullPush: { complete: true, message: '' } });
})
.catch((error) => {
const message = maybeAxiosError(error);
setDirection('none');
setState({ ...state, pullPush: { complete: false, message } });
});
postPushSheet(id, excelFileOptions.current).catch((error) => {
const message = maybeAxiosError(error);
setDirection('none');
setState({ ...state, pullPush: { complet: false, message } });
});
};
//GET preview
@@ -279,7 +274,7 @@ export default function SheetsModal(props: SheetsModalProps) {
>
<ModalOverlay />
<ModalContent>
<ModalHeader>Rundown from sheets (experimental)</ModalHeader>
<ModalHeader>Rundown from sheets</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Alert status='info' variant='ontime-on-light-info'>
@@ -287,20 +282,22 @@ export default function SheetsModal(props: SheetsModalProps) {
<div style={{ display: 'flex', flexDirection: 'column' }}>
<AlertTitle>Sync with Google Sheets</AlertTitle>
<AlertDescription>
<ModalLink href='https://ontime.gitbook.io/v2/features/google-sheet'>
<ModalLink href='https://ontime.gitbook.io/v2/features/google-sheet-experimental'>
For more information, see the docs
</ModalLink>
</AlertDescription>
</div>
</Alert>
{!rundown ? (
direction === 'up' || direction === 'down' ? (
direction === 'up' ? (
<div>{direction}</div>
) : direction === 'down' ? (
<ExcelFileOptions optionsRef={excelFileOptions} updateOptions={updateExcelFileOptions} />
) : (
<>
<Step
title='1 - Upload OAuth 2.0 Client ID'
completed={state.clientSecret.complete}
completed={state.clientSecret.complet}
disabled={false}
error={state.clientSecret.message}
>
@@ -314,9 +311,9 @@ export default function SheetsModal(props: SheetsModalProps) {
/>
<div style={{ display: 'flex', gap: '1em' }}>
<Button size='sm' variant='ontime-subtle-on-light' onClick={handleClick}>
{state.clientSecret.complete ? 'Reupload Client ID' : 'Upload Client ID'}
{state.clientSecret.complet ? 'Reupload Client ID' : 'Upload Client ID'}
</Button>
<Button size='sm' variant='ontime-ghosted-on-light' onClick={testClientSecret}>
<Button size='sm' variant='ontime-ghosted-on-light' onClick={testClientSecrect}>
Retry Client ID
</Button>
</div>
@@ -324,8 +321,8 @@ export default function SheetsModal(props: SheetsModalProps) {
<Step
title='2 - Authenticate with Google'
completed={state.authenticate.complete}
disabled={!state.clientSecret.complete}
completed={state.authenticate.complet}
disabled={!state.clientSecret.complet}
error={state.authenticate.message}
>
<div style={{ display: 'flex', gap: '1em' }}>
@@ -333,7 +330,7 @@ export default function SheetsModal(props: SheetsModalProps) {
size='sm'
variant='ontime-subtle-on-light'
onClick={handleAuthenticate}
isDisabled={!state.clientSecret.complete}
isDisabled={!state.clientSecret.complet}
>
Authenticate
</Button>
@@ -341,7 +338,7 @@ export default function SheetsModal(props: SheetsModalProps) {
size='sm'
variant='ontime-ghosted-on-light'
onClick={testAuthentication}
isDisabled={!state.clientSecret.complete}
isDisabled={!state.clientSecret.complet}
>
Retry Connection
</Button>
@@ -350,8 +347,8 @@ export default function SheetsModal(props: SheetsModalProps) {
<Step
title='3 - Add Document ID'
completed={state.id.complete}
disabled={!state.authenticate.complete}
completed={state.id.complet}
disabled={!state.authenticate.complet}
error={state.id.message}
>
<HStack>
@@ -359,17 +356,11 @@ export default function SheetsModal(props: SheetsModalProps) {
type='text'
size='sm'
variant='ontime-filled-on-light'
isDisabled={!state.authenticate.complete}
disabled={!state.authenticate.complet}
value={id}
onChange={(event) => setSheetId(event.target.value)}
/>
<Button
isDisabled={!state.authenticate.complete}
size='sm'
variant='ontime-subtle-on-light'
padding='0 2em'
onClick={testSheetId}
>
<Button size='sm' variant='ontime-subtle-on-light' padding='0 2em' onClick={testSheetId}>
Connect
</Button>
</HStack>
@@ -377,12 +368,12 @@ export default function SheetsModal(props: SheetsModalProps) {
<Step
title='4 - Select Worksheet to import'
completed={state.worksheet.complete}
completed={state.worksheet.complet}
disabled={worksheetOptions.length == 0}
>
<Select
size='sm'
isDisabled={worksheetOptions.length == 0}
disabled={worksheetOptions.length == 0}
placeholder='Select a worksheet'
onChange={(event) => testWorksheet(event.target.value)}
value={worksheet}
@@ -395,10 +386,10 @@ export default function SheetsModal(props: SheetsModalProps) {
</Select>
</Step>
<Step title='5 - Upload / Download rundown' completed={false} disabled={!state.worksheet.complete}>
<Step title='5 - Upload / Download rundown' completed={false} disabled={!state.worksheet.complet}>
<div style={{ display: 'flex', gap: '1em' }}>
<Button
isDisabled={!state.worksheet.complete}
disabled={!state.worksheet.complet}
variant='ontime-subtle-on-light'
padding='0 2em'
onClick={() => setDirection('up')}
@@ -406,7 +397,7 @@ export default function SheetsModal(props: SheetsModalProps) {
Upload
</Button>
<Button
isDisabled={!state.worksheet.complete}
disabled={!state.worksheet.complet}
variant='ontime-subtle-on-light'
padding='0 2em'
onClick={() => setDirection('down')}
@@ -456,7 +447,7 @@ export default function SheetsModal(props: SheetsModalProps) {
Go Back
</Button>
<Button variant='ontime-filled' padding='0 2em' onClick={handlePullData}>
Preview
Review
</Button>
</div>
) : (
@@ -25,7 +25,6 @@
display: inline;
color: $gray-500;
text-transform: uppercase;
cursor: pointer;
}
.check {
@@ -123,10 +123,7 @@ export default function Operator() {
}
// get fields which the user subscribed to
const shouldEdit = searchParams.get('shouldEdit');
const subscribe = searchParams.get('subscribe') as keyof UserFields | null;
const canEdit = shouldEdit && subscribe;
const main = searchParams.get('main') as keyof TitleFields | null;
const secondary = searchParams.get('secondary') as keyof TitleFields | null;
const subscribedAlias = subscribe ? userFields[subscribe] : '';
@@ -155,7 +152,7 @@ export default function Operator() {
lastId={lastEvent?.id}
/>
{canEdit && (
{subscribe && (
<div className={`${style.editPrompt} ${showEditPrompt ? style.show : undefined}`}>
Press and hold to edit user field
</div>
@@ -196,7 +193,7 @@ export default function Operator() {
showSeconds={showSeconds}
isPast={isPast}
selectedRef={isSelected ? selectedRef : undefined}
onLongPress={canEdit ? handleEdit : () => undefined}
onLongPress={subscribe ? handleEdit : () => undefined}
/>
);
}
@@ -198,9 +198,7 @@ export default function Rundown(props: RundownProps) {
if (index === 0) {
eventIndex = 0;
}
let isFirstEvent = false;
if (entry.type === SupportedEvent.Event) {
isFirstEvent = eventIndex === 0;
eventIndex++;
previousEnd = thisEnd;
thisEnd = entry.timeEnd;
@@ -222,7 +220,6 @@ export default function Rundown(props: RundownProps) {
<RundownEntry
type={entry.type}
isPast={isPast}
isFirstEvent={isFirstEvent}
data={entry}
selected={isSelected}
hasCursor={hasCursor}
@@ -19,7 +19,6 @@ export type EventItemActions = 'set-cursor' | 'event' | 'delay' | 'block' | 'del
interface RundownEntryProps {
type: SupportedEvent;
isPast: boolean;
isFirstEvent: boolean;
data: OntimeRundownEntry;
selected: boolean;
hasCursor: boolean;
@@ -32,19 +31,8 @@ interface RundownEntryProps {
}
export default function RundownEntry(props: RundownEntryProps) {
const {
isPast,
data,
selected,
hasCursor,
next,
previousEnd,
previousEventId,
playback,
isRolling,
disableEdit,
isFirstEvent,
} = props;
const { isPast, data, selected, hasCursor, next, previousEnd, previousEventId, playback, isRolling, disableEdit } =
props;
const { emitError } = useEmitLog();
const { addEvent, updateEvent, deleteEvent, swapEvents } = useEventAction();
@@ -112,7 +100,7 @@ export default function RundownEntry(props: RundownEntryProps) {
}
case 'clone': {
const newEvent = cloneEvent(data as OntimeEvent, data.id);
const rundown = ontimeQueryClient.getQueryData<GetRundownCached>(RUNDOWN)?.rundown ?? [];
const rundown = ontimeQueryClient.getQueryData<GetRundownCached>(RUNDOWN)?.rundown ?? []
newEvent.cue = getCueCandidate(rundown, data.id);
addEvent(newEvent);
break;
@@ -188,7 +176,6 @@ export default function RundownEntry(props: RundownEntryProps) {
isRolling={isRolling}
actionHandler={actionHandler}
disableEdit={disableEdit}
isFirstEvent={isFirstEvent}
/>
);
} else if (data.type === SupportedEvent.Block) {
@@ -10,11 +10,11 @@ $skip-opacity: 0.1;
display: grid;
grid-template-areas:
'binder ... ... ...'
'binder pb-actions times actions'
'binder pb-actions title next-ind'
'binder pb-actions estatus estatus'
'binder ... ... ...';
"binder ... ... ..."
"binder pb-actions times actions"
"binder pb-actions title next"
"binder pb-actions estatus estatus"
"binder ... ... ...";
grid-template-columns: $block-binder-width auto 1fr auto;
grid-template-rows: 0.25rem 2.25rem 2.25rem 2.25rem 0.25rem;
@@ -53,18 +53,8 @@ $skip-opacity: 0.1;
outline: 1px solid $block-cursor-color;
}
/* we stop the eventActions from having opacity to fix issue with dropdown drawing order */
&.past:not(.skip) {
.delayNote,
.statusElements,
.eventTitle,
.eventNote,
.eventTimers,
.eventStatus,
.playbackActions,
.binder {
opacity: 0.4;
}
opacity: 0.6;
}
&.skip {
@@ -170,8 +160,8 @@ $skip-opacity: 0.1;
grid-area: estatus;
display: grid;
grid-template-areas:
'notes status'
'progb progb';
"notes status"
"progb progb";
gap: 2px;
grid-template-rows: auto 0.25rem;
align-items: center;
@@ -190,8 +180,9 @@ $skip-opacity: 0.1;
overflow-y: hidden;
}
.nextTag {
grid-area: next-ind;
grid-area: next;
font-size: 1rem;
color: $orange-500;
letter-spacing: 0.03px;
@@ -199,31 +190,6 @@ $skip-opacity: 0.1;
text-align: right;
}
.indicators {
grid-area: next-ind;
display: flex;
justify-content: flex-end;
align-items: center;
gap: 0.5rem;
.indicator {
background-color: transparent;
margin: 0.4rem;
margin-right: 0;
border-radius: 0.7rem;
width: 0.7rem;
height: 0.7rem;
}
.indicator.delay {
background-color: $ontime-delay;
}
.indicator.nextDay,
.indicator.overlap,
.indicator.spacing {
background-color: $gray-600;
}
}
.eventStatus {
grid-area: status;
display: flex;
@@ -232,6 +198,7 @@ $skip-opacity: 0.1;
gap: 0.5rem;
color: var(--status-color-override, $gray-500);
.statusIcon {
width: 1rem;
height: 1rem;
@@ -50,7 +50,6 @@ interface EventBlockProps {
},
) => void;
disableEdit: boolean;
isFirstEvent: boolean;
}
export default function EventBlock(props: EventBlockProps) {
@@ -77,7 +76,6 @@ export default function EventBlock(props: EventBlockProps) {
isRolling,
actionHandler,
disableEdit,
isFirstEvent,
} = props;
const { selectedEventId, setSelectedEventId, clearSelectedEventId } = useEventIdSwapping();
const moveCursorTo = useAppMode((state) => state.setCursor);
@@ -212,7 +210,6 @@ export default function EventBlock(props: EventBlockProps) {
isRolling={isRolling}
actionHandler={actionHandler}
disableEdit={disableEdit}
isFirstEvent={isFirstEvent}
/>
)}
</div>
@@ -11,11 +11,9 @@ import { IoPlaySkipForward } from '@react-icons/all-files/io5/IoPlaySkipForward'
import { IoStop } from '@react-icons/all-files/io5/IoStop';
import { IoTime } from '@react-icons/all-files/io5/IoTime';
import { EndAction, Playback, TimerType } from 'ontime-types';
import { millisToString } from 'ontime-utils';
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
import { useAppMode } from '../../../common/stores/appModeStore';
import { millisToDelayString } from '../../../common/utils/dateConfig';
import { tooltipDelayMid } from '../../../ontimeConfig';
import EditableBlockTitle from '../common/EditableBlockTitle';
import { EventItemActions } from '../RundownEntry';
@@ -55,7 +53,6 @@ interface EventBlockInnerProps {
isRolling: boolean;
actionHandler: (action: EventItemActions, payload?: any) => void;
disableEdit: boolean;
isFirstEvent: boolean;
}
const EventBlockInner = (props: EventBlockInnerProps) => {
@@ -79,7 +76,6 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
isRolling,
actionHandler,
disableEdit,
isFirstEvent,
} = props;
const [renderInner, setRenderInner] = useState(false);
@@ -107,19 +103,6 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
playBtnStyles._hover = {};
}
const delayedStart = Math.max(0, timeStart + delay);
const newTime = millisToString(delayedStart);
const delayTime = delay !== 0 ? millisToDelayString(delay) : null;
const overlap = previousEnd - timeStart;
const overlapTime = !isFirstEvent
? overlap > 0
? `Overlapping ${millisToDelayString(overlap)}`
: overlap < 0
? `Spacing ${millisToDelayString(overlap)}`
: null
: null;
return !renderInner ? null : (
<>
<EventBlockTimers
@@ -131,35 +114,10 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
previousEnd={previousEnd}
/>
<EditableBlockTitle title={title} eventId={eventId} placeholder='Event title' className={style.eventTitle} />
{next ? (
{next && (
<Tooltip label='Next event' {...tooltipProps}>
<span className={style.nextTag}>UP NEXT</span>
</Tooltip>
) : (
<span className={style.indicators}>
{delayTime && (
<Tooltip
label={
<div>
{delayTime} <br />
New Time: {newTime}
</div>
}
>
<div className={`${style.indicator} ${style.delay}`} />
</Tooltip>
)}
{overlapTime && (
<Tooltip label={overlapTime}>
<div className={`${style.indicator} ${overlap > 0 ? style.overlap : style.spacing}`} />
</Tooltip>
)}
{timeStart > timeEnd && (
<Tooltip label='Start time is later than end'>
<div className={`${style.indicator} ${style.nextDay}`} />
</Tooltip>
)}
</span>
)}
<EventBlockPlayback
eventId={eventId}
@@ -1,10 +1,11 @@
import { memo } from 'react';
import { memo, useCallback, useState } from 'react';
import { OntimeEvent } from 'ontime-types';
import { calculateDuration, dayInMs, millisToString } from 'ontime-utils';
import { calculateDuration, millisToString } from 'ontime-utils';
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
import { useEventAction } from '../../../../common/hooks/useEventAction';
import { millisToDelayString } from '../../../../common/utils/dateConfig';
import { TimeEntryField, validateEntry } from '../../../../common/utils/timesManager';
import style from '../EventBlock.module.scss';
@@ -23,13 +24,15 @@ const EventBlockTimers = (props: EventBlockTimerProps) => {
const { eventId, timeStart, timeEnd, duration, delay, previousEnd } = props;
const { updateEvent } = useEventAction();
const [warning, setWarnings] = useState({ start: '', end: '', duration: '' });
const handleSubmit = (field: TimeActions, value: number) => {
const newEventData: Partial<OntimeEvent> = { id: eventId };
switch (field) {
case 'durationOverride': {
// duration defines timeEnd
newEventData.duration = value;
newEventData.timeEnd = timeStart + ((value as number) % dayInMs);
newEventData.timeEnd = timeStart + value;
break;
}
case 'timeStart': {
@@ -46,6 +49,21 @@ const EventBlockTimers = (props: EventBlockTimerProps) => {
updateEvent(newEventData);
};
/**
* @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 handleValidation = useCallback(
(field: TimeEntryField, value: number) => {
const valid = validateEntry(field, value, timeStart, timeEnd);
setWarnings((prev) => ({ ...prev, ...valid.warnings }));
return valid.value;
},
[timeEnd, timeStart],
);
const delayedStart = Math.max(0, timeStart + delay);
const newTime = millisToString(delayedStart);
const delayTime = delay !== 0 ? millisToDelayString(delay) : null;
@@ -55,26 +73,32 @@ const EventBlockTimers = (props: EventBlockTimerProps) => {
<TimeInput
name='timeStart'
submitHandler={handleSubmit}
validationHandler={handleValidation}
time={timeStart}
delay={delay}
placeholder='Start'
previousEnd={previousEnd}
warning={warning.start}
/>
<TimeInput
name='timeEnd'
submitHandler={handleSubmit}
validationHandler={handleValidation}
time={timeEnd}
delay={delay}
placeholder='End'
previousEnd={previousEnd}
warning={warning.end}
/>
<TimeInput
name='durationOverride'
submitHandler={handleSubmit}
validationHandler={handleValidation}
time={duration}
delay={0}
placeholder='Duration'
previousEnd={previousEnd}
warning={warning.duration}
/>
{delayTime && (
<div className={style.delayNote}>
@@ -126,11 +126,10 @@ export default function MinimalTimer(props: MinimalTimerProps) {
const hideEndMessage = searchParams.get('hideendmessage');
userOptions.hideEndMessage = isStringBoolean(hideEndMessage);
const timerIsTimeOfDay = time.timerType === TimerType.Clock;
const showOverlay = pres.text !== '' && pres.visible;
const isPlaying = time.playback !== Playback.Pause;
const isNegative = (time.current ?? 0) < 0 && !timerIsTimeOfDay && time.timerType !== TimerType.CountUp;
const isNegative =
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
const showEndMessage = (time.current ?? 0) < 0 && viewSettings.endMessage && !hideEndMessage;
const finished = time.playback === Playback.Play && (time.current ?? 0) < 0 && time.startedAt;
const showFinished = finished && !userOptions?.hideOvertime && (time.timerType !== TimerType.Clock || showEndMessage);
@@ -141,9 +140,13 @@ export default function MinimalTimer(props: MinimalTimerProps) {
const showBlinking = pres.timerBlink;
const showBlackout = pres.timerBlackout;
let timerColor = viewSettings.normalColor;
if (!timerIsTimeOfDay && showProgress && showWarning) timerColor = viewSettings.warningColor;
if (!timerIsTimeOfDay && showProgress && showDanger) timerColor = viewSettings.dangerColor;
const timerColor = userOptions.textColour
? userOptions.textColour
: showProgress && showDanger
? viewSettings.dangerColor
: showProgress && showWarning
? viewSettings.warningColor
: viewSettings.normalColor;
const stageTimer = getTimerByType(time);
let display = formatTimerDisplay(stageTimer);
@@ -13,7 +13,7 @@ $half-hours: min(1.5vh, 10px);
$size-min: min(2.5vh, 18px);
$half-min: min(1.25vh, 9px);
$red-active: #c53030;
$red-idle: #300000;
$red-idle: #000000;
$cyan-active: #0ff;
$cyan-idle: #0aa;
@@ -109,8 +109,7 @@ $cyan-idle: #0aa;
line-height: 1em;
&--overtime {
color: var(--studio-active, $red-active);
filter: brightness(0.9);
color: darken($red-active, 10%);
}
}
@@ -134,7 +133,7 @@ $cyan-idle: #0aa;
color: var(--studio-active, $red-active);
&--idle {
color: var(--studio-idle, $red-idle);
color: var(--studio-idle, $red-active);
}
}
@@ -90,9 +90,8 @@ export default function Timer(props: TimerProps) {
const showOverlay = pres.text !== '' && pres.visible;
const isPlaying = time.playback !== Playback.Pause;
const timerIsTimeOfDay = time.timerType === TimerType.Clock;
const isNegative = (time.current ?? 0) < 0 && !timerIsTimeOfDay && time.timerType !== TimerType.CountUp;
const isNegative =
(time.current ?? 0) < 0 && time.timerType !== TimerType.Clock && time.timerType !== TimerType.CountUp;
const finished = time.playback === Playback.Play && (time.current ?? 0) < 0 && time.startedAt;
const totalTime = (time.duration ?? 0) + (time.addedTime ?? 0);
@@ -106,9 +105,12 @@ export default function Timer(props: TimerProps) {
const showClock = time.timerType !== TimerType.Clock;
const showExternal = external.visible && external.text;
let timerColor = viewSettings.normalColor;
if (!timerIsTimeOfDay && showProgress && showWarning) timerColor = viewSettings.warningColor;
if (!timerIsTimeOfDay && showProgress && showDanger) timerColor = viewSettings.dangerColor;
const timerColor =
showProgress && showDanger
? viewSettings.dangerColor
: showProgress && showWarning
? viewSettings.warningColor
: viewSettings.normalColor;
const stageTimer = getTimerByType(time);
let display = formatTimerDisplay(stageTimer);
@@ -171,7 +173,7 @@ export default function Timer(props: TimerProps) {
{!userOptions.hideProgress && (
<MultiPartProgressBar
className={isPlaying ? 'progress-container' : 'progress-container progress-container--paused'}
now={timerIsTimeOfDay ? null : time.current}
now={time.current}
complete={totalTime}
normalColor={viewSettings.normalColor}
warning={viewSettings.warningThreshold}
+1 -26
View File
@@ -12,38 +12,13 @@ import './index.scss';
const container = document.getElementById('root');
const root = createRoot(container as Element);
// https://docs.sentry.io/platforms/javascript/configuration/filtering/#decluttering-sentry
const sentryRecommendedIgnore = [
// Random plugins/extensions
'top.GLOBALS',
// See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error.html
'originalCreateNotification',
'canvas.contentDocument',
'MyApp_RemoveAllHighlights',
'http://tt.epicplay.com',
"Can't find variable: ZiteReader",
'jigsaw is not defined',
'ComboSearch is not defined',
'http://loading.retry.widdit.com/',
'atomicFindClose',
// Facebook borked
'fb_xd_fragment',
// ISP "optimizing" proxy - `Cache-Control: no-transform` seems to
// reduce this. (thanks @acdha)
// See http://stackoverflow.com/questions/4113268
'bmi_SafeAddOnload',
'EBCallBackMessageReceived',
// See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
'conduitPage',
];
Sentry.init({
dsn: 'https://5e4d2c4b57ab409cb98d4c08b2014755@o4504288369836032.ingest.sentry.io/4504288371343360',
integrations: [new BrowserTracing()],
tracesSampleRate: 0.3,
release: ONTIME_VERSION,
enabled: import.meta.env.PROD,
ignoreErrors: [...sentryRecommendedIgnore, 'Unable to preload CSS', 'Failed to fetch dynamically imported module'],
ignoreErrors: ['top.GLOBALS', 'Unable to preload CSS', 'Failed to fetch dynamically imported module'],
denyUrls: [/extensions\//i, /^chrome:\/\//i, /^chrome-extension:\/\//i],
});
@@ -6,7 +6,6 @@ import { langDe } from './languages/de';
import { langEn } from './languages/en';
import { langEs } from './languages/es';
import { langFr } from './languages/fr';
import { langIt } from './languages/it';
import { langNo } from './languages/no';
import { langPt } from './languages/pt';
import { langSv } from './languages/sv';
@@ -15,7 +14,6 @@ const translationsList = {
en: langEn,
es: langEs,
fr: langFr,
it: langIt,
de: langDe,
no: langNo,
pt: langPt,
@@ -1,19 +0,0 @@
import { TranslationObject } from './en';
export const langIt: TranslationObject = {
'common.end_time': 'Ora di Fine',
'common.expected_finish': 'Fine Prevista',
'common.now': 'Adesso',
'common.next': 'Prossimo',
'common.public_message': 'Messaggio pubblico',
'common.start_time': 'Ora di Inizio',
'common.stage_timer': 'Orologio Palco',
'common.started_at': 'Iniziato Alle',
'common.time_now': 'Ora attuale',
'countdown.ended': 'Evento finito alle',
'countdown.running': 'Evento in corso',
'countdown.select_event': 'Seleziona un evento da seguire',
'countdown.to_start': 'Tempo alla partenza',
'countdown.waiting': 'In attesa dell\'inizio dell\'evento',
'countdown.overtime': 'in ritardo',
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime",
"version": "2.28.17",
"version": "2.21.3",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
+1 -2
View File
@@ -2,7 +2,7 @@
"name": "ontime-server",
"type": "module",
"main": "src/index.ts",
"version": "2.28.17",
"version": "2.21.3",
"exports": "./src/index.js",
"dependencies": {
"@googleapis/sheets": "^5.0.5",
@@ -55,7 +55,6 @@
"build:electron": "pnpm prebuild && esbuild src/app.ts --log-level=error --platform=node --format=cjs --bundle --minify --outfile=dist/index.cjs",
"build:local": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --bundle --minify --outfile=dist/index.cjs",
"build:docker": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --minify --bundle --outfile=dist/docker.cjs",
"build:localdocker": "NODE_ENV=local pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --minify --bundle --outfile=dist/docker.cjs",
"build:debug": "pnpm prebuild && esbuild src/app.ts --platform=node --format=cjs --bundle --outfile=dist/index.cjs",
"lint": "eslint . --quiet",
"lint-staged": "eslint",
+2 -15
View File
@@ -8,14 +8,7 @@ import cors from 'cors';
// import utils
import { join, resolve } from 'path';
import {
currentDirectory,
environment,
isProduction,
resolveExternalsDirectory,
resolveStylesDirectory,
resolvedPath,
} from './setup.js';
import { currentDirectory, environment, externalsStartDirectory, isProduction, resolvedPath } from './setup.js';
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
// Import Routes
@@ -42,7 +35,6 @@ import { eventStore, getInitialPayload } from './stores/EventStore.js';
import { PlaybackService } from './services/PlaybackService.js';
import { RestorePoint, restoreService } from './services/RestoreService.js';
import { messageService } from './services/message-service/MessageService.js';
import { populateDemo } from './modules/loadDemo.js';
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
@@ -72,11 +64,7 @@ app.use('/ontime', ontimeRouter);
app.use('/playback', playbackRouter);
// serve static - css
app.use('/external/styles', express.static(resolveStylesDirectory));
app.use('/external/', express.static(resolveExternalsDirectory));
app.use('/external', (req, res) => {
res.status(404).send(`${req.originalUrl} not found`);
});
app.use('/external', express.static(externalsStartDirectory));
// serve static - react, in dev/test mode we fetch the React app from module
const reactAppPath = join(currentDirectory, resolvedPath());
@@ -138,7 +126,6 @@ export const initAssets = async () => {
checkStart(OntimeStartOrder.InitAssets);
await dbLoadingProcess;
populateStyles();
populateDemo();
};
/**
-4
View File
@@ -8,9 +8,5 @@ export const config = {
directory: 'styles',
filename: 'override.css',
},
demo: {
directory: 'demo',
filename: ['app.js', 'index.html', 'styles.css'],
},
restoreFile: 'ontime.restore',
};
@@ -139,10 +139,11 @@ export function dispatchFromAdapter(
throw new Error(`Event index not recognised or out of range ${eventIndex}`);
}
// Indexes in frontend are 1 based
const success = PlaybackService.startByIndex(eventIndex - 1);
if (!success) {
throw new Error(`Event index not recognised or out of range ${eventIndex}`);
try {
// Indexes in frontend are 1 based
PlaybackService.startByIndex(eventIndex - 1);
} catch (error) {
throw new Error(`Error loading event:: ${error}`);
}
break;
}
@@ -193,8 +194,11 @@ export function dispatchFromAdapter(
if (isNaN(time)) {
throw new Error(`Time not recognised ${payload}`);
}
PlaybackService.addTime(time);
try {
PlaybackService.addTime(time);
} catch (error) {
throw new Error(`Could not add time: ${error}`);
}
break;
}
//deprecated
@@ -204,7 +208,11 @@ export function dispatchFromAdapter(
throw new Error(`Delay time not recognised ${payload}`);
}
PlaybackService.setDelay(delayTime);
try {
PlaybackService.setDelay(delayTime);
} catch (error) {
throw new Error(`Could not add delay: ${error}`);
}
break;
}
case 'gotoindex':
@@ -214,10 +222,11 @@ export function dispatchFromAdapter(
throw new Error(`Event index not recognised or out of range ${eventIndex}`);
}
// Indexes in frontend are 1 based
const success = PlaybackService.loadByIndex(eventIndex - 1);
if (!success) {
throw new Error(`Event index not recognised or out of range ${eventIndex}`);
try {
// Indexes in frontend are 1 based
PlaybackService.loadByIndex(eventIndex - 1);
} catch (error) {
throw new Error(`Event index not recognised or out of range ${error}`);
}
break;
}
@@ -227,9 +236,10 @@ export function dispatchFromAdapter(
throw new Error(`Event ID not recognised: ${payload}`);
}
const success = PlaybackService.loadById(payload.toString().toLowerCase());
if (!success) {
throw new Error(`Event ID not found: ${payload}`);
try {
PlaybackService.loadById(payload.toString().toLowerCase());
} catch (error) {
throw new Error(`OSC IN: error calling goto ${error}`);
}
break;
}
@@ -239,9 +249,10 @@ export function dispatchFromAdapter(
throw new Error(`Event cue not recognised: ${payload}`);
}
const success = PlaybackService.loadByCue(payload);
if (!success) {
throw new Error(`Event cue not found: ${payload}`);
try {
PlaybackService.loadByCue(payload);
} catch (error) {
throw new Error(`OSC IN: error calling goto ${error}`);
}
break;
}
@@ -10,7 +10,7 @@ import { DataProvider } from '../classes/data-provider/DataProvider.js';
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
import { PlaybackService } from '../services/PlaybackService.js';
import { eventStore } from '../stores/EventStore.js';
import { isDocker, resolveDbPath, resolveStylesPath } from '../setup.js';
import { isDocker, pathToStartStyles, resolveDbPath } from '../setup.js';
import { oscIntegration } from '../services/integration-service/OscIntegration.js';
import { httpIntegration } from '../services/integration-service/HttpIntegration.js';
import { logger } from '../classes/Logger.js';
@@ -119,7 +119,7 @@ export const getInfo = async (req: Request, res: Response<GetInfo>) => {
// get nif and inject localhost
const ni = getNetworkInterfaces();
ni.unshift({ name: 'localhost', address: '127.0.0.1' });
const cssOverride = resolveStylesPath;
const cssOverride = pathToStartStyles;
// send object with network information
res.status(200).send({
@@ -485,7 +485,7 @@ export async function uploadSheetClientFile(req, res) {
*/
export const getClientSecrect = async (req, res) => {
try {
const clientSecrectExists = await sheet.testClientSecret();
const clientSecrectExists = await sheet.testClientSecrect();
if (clientSecrectExists) {
res.status(200).send();
} else {
-67
View File
@@ -1,67 +0,0 @@
/*eslint-env browser*/
/**
* This is a very minimal example for a websocket client
* You could use this as a starting point to creating your own interfaces
*/
const mts = 1000; // millis to seconds
const mtm = 1000 * 60; // millis to minutes
const mth = 1000 * 60 * 60; // millis to hours
const leftPad = (number) => {
return Math.floor(number).toString().padStart(2, '0');
};
let reconnectTimeout;
const reconnectInterval = 1000;
let reconnectAttempts = 0;
const connectSocket = () => {
const websocket = new WebSocket(`ws://${window.location.hostname}:${window.location.port}/ws`);
websocket.onopen = () => {
clearTimeout(reconnectTimeout);
reconnectAttempts = 0;
console.info('WebSocket connected');
};
websocket.onclose = () => {
console.warn('WebSocket disconnected');
reconnectTimeout = setTimeout(() => {
console.warn(`WebSocket: attempting reconnect ${reconnectAttempts}`);
if (websocket && websocket.readyState === WebSocket.CLOSED) {
reconnectAttempts += 1;
connectSocket();
}
}, reconnectInterval);
};
websocket.onerror = (error) => {
console.error('WebSocket error:', error);
};
websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
// all objects from ontime are structured with type and payload
const { type, payload } = data;
// we only need to read message type of ontime
if (type === 'ontime') {
// destructure known data from ontime
// see https://cpvalente.gitbook.io/ontime/control-and-feedback/websocket-api
const { timer, playback } = payload;
const timerElement = document.getElementById('timer');
if (playback == 'stop') {
timerElement.innerText = '--:--:--';
} else {
const millis = Math.abs(timer.current);
const isNegative = timer.current < 0;
timerElement.innerText = `${isNegative ? '-' : ''}${leftPad(millis / mth)}:${leftPad(
(millis % mth) / mtm,
)}:${leftPad((millis % mtm) / mts)}`;
}
}
};
};
connectSocket();
-14
View File
@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>ontime demo</title>
<link href="./styles.css" rel="stylesheet" />
</head>
<body>
<div id="timer"></div>
<script src="./app.js" type="module"></script>
</html>
-16
View File
@@ -1,16 +0,0 @@
body {
overflow: hidden;
margin: 0;
}
div {
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
color: azure;
font-family: monospace;
font-size: 20vw;
background-color: black;
}
-21
View File
@@ -1,21 +0,0 @@
import { copyFile } from 'fs/promises';
import { pathToStartDemo, resolveDemoDirectory, resolveDemoPath } from '../setup.js';
import { ensureDirectory } from '../utils/fileManagement.js';
/**
* @description ensures directories exist and populates demo folder
*/
export const populateDemo = () => {
ensureDirectory(resolveDemoDirectory);
// even if demo exist we want to use startup demo
try {
Promise.all(
resolveDemoPath.map((to, index) => {
const from = pathToStartDemo[index];
copyFile(from, to);
}),
);
} catch (_) {
/* we do not handle this */
}
};
+8 -3
View File
@@ -4,15 +4,20 @@ import { ensureDirectory } from '../utils/fileManagement.js';
/**
* @description ensures directories exist and populates stylesheet
* @return {string} - path to stylesheet file
*/
export const populateStyles = () => {
const stylesInDisk = resolveStylesPath;
ensureDirectory(resolveStylesDirectory);
// if styles doesn't exist we want to use startup stylesheet
if (!existsSync(resolveStylesPath)) {
// if stylesInDisk doesn't exist we want to use startup stylesheet
if (!existsSync(stylesInDisk)) {
try {
copyFileSync(pathToStartStyles, resolveStylesPath);
copyFileSync(pathToStartStyles, stylesInDisk);
} catch (_) {
/* we do not handle this */
}
}
return stylesInDisk;
};
+4 -4
View File
@@ -22,12 +22,12 @@ import {
previewExcel,
postHTTP,
getAuthenticationUrl,
uploadSheetClientFile as uploadClientSecret,
uploadSheetClientFile,
pullSheet,
pushSheet,
postId,
getAuthentication,
getClientSecrect as getClientSecret,
getClientSecrect,
} from '../controllers/ontimeController.js';
import {
@@ -108,8 +108,8 @@ router.post('/http', validateHTTP, postHTTP);
router.post('/new', projectSanitiser, postNew);
//SETP-1
router.post('/sheet/clientsecret', uploadFile, uploadClientSecret);
router.get('/sheet/clientsecret', uploadFile, getClientSecret);
router.post('/sheet/clientsecrect', uploadFile, uploadSheetClientFile);
router.get('/sheet/clientsecrect', uploadFile, getClientSecrect);
//SETP-2
router.get('/sheet/authentication/url', getAuthenticationUrl);
@@ -528,76 +528,6 @@ describe('test that roll behaviour multi day event edge cases', () => {
});
});
// test getRollTimers() on issue #757
describe('it handles timeEnd over day', () => {
it('ignores events with timeEnd larger than a day', () => {
const testRundown = [
{
title: 'Setup',
subtitle: '',
presenter: '',
note: 'BMA, Nebelfluid, Shooter, Akkus, UHF11, Getränke, Kaffee, Strom, ELA, Text für Holger',
endAction: 'play-next',
timerType: 'count-down',
timeStart: 66600000,
timeEnd: 68400000,
duration: 1800000,
isPublic: false,
skip: false,
colour: '#2fa9e5',
user0: '',
user1: '',
user2: '',
user3: '',
user4: '',
user5: '',
user6: '',
user7: '',
user8: '',
user9: '',
type: 'event',
revision: 0,
cue: 'PRE',
id: 'b2f8d',
},
{
title: 'Künstliche Intelligenz',
subtitle: ' -> Vorstellung Maske',
presenter: 'Engel und Teufel',
note: 'Melli Kleben! Sofa',
endAction: 'play-next',
timerType: 'count-down',
timeStart: 86100000,
// timeEnd: 1020000, <--- this would have been equivalent
timeEnd: 87420000,
duration: 1320000,
isPublic: true,
skip: false,
colour: '#a8ec31',
user0: 'UHF1 Melli (Korsett)',
user1: 'UHF2 Reinhold (unter Flügel)',
user2: 'UHF3 Oli (Sport-Unterhose Rechts)',
user3: '',
user4: '',
user5: '',
user6: '',
user7: '',
user8: '',
user9: '',
type: 'event',
revision: 0,
cue: '16',
id: '8b970',
},
];
const timeNow = 64488675; // 17:55-something
const timers = getRollTimers(testRundown as OntimeEvent[], timeNow);
expect(timers.currentEvent).toBeNull();
});
});
// test normaliseEndTime() on issue #58
test('test typical scenarios', () => {
const t1 = {
+1 -2
View File
@@ -79,8 +79,7 @@ export const getRollTimers = (rundown: OntimeEvent[], timeNow: number) => {
const normalEnd = normaliseEndTime(event.timeStart, event.timeEnd);
const hasNotEnded = normalEnd > timeNow;
// TODO: we will likely want a better solution than the modulus here
const isFromDayBefore = normalEnd > dayInMs && timeNow < event.timeEnd % dayInMs;
const isFromDayBefore = normalEnd > dayInMs && timeNow < event.timeEnd;
const hasStarted = isFromDayBefore || timeNow >= event.timeStart;
if (normalEnd <= timeNow) {
+1 -17
View File
@@ -68,8 +68,7 @@ export const currentDirectory = dirname(__dirname);
const testDbStartDirectory = isTest ? '../' : getAppDataPath();
export const externalsStartDirectory = isProduction ? getAppDataPath() : join(currentDirectory, 'external');
//TODO: we only need one when they are all in the same folder
export const resolveExternalsDirectory = join(isProduction ? getAppDataPath() : currentDirectory, 'external');
// path to public db
export const resolveDbDirectory = join(
testDbStartDirectory,
@@ -81,26 +80,11 @@ export const pathToStartDb = isTest
? join(currentDirectory, '../', config.database.testdb, config.database.filename)
: join(currentDirectory, '/preloaded-db/', config.database.filename);
//TODO: move all static files to the external directory
// path to public styles
export const resolveStylesDirectory = join(externalsStartDirectory, config.styles.directory);
export const resolveStylesPath = join(resolveStylesDirectory, config.styles.filename);
export const pathToStartStyles = join(currentDirectory, '/external/styles/', config.styles.filename);
// path to public demo
export const resolveDemoDirectory = join(
externalsStartDirectory,
isProduction ? '/external/' : '', //move to external folde in production
config.demo.directory,
);
export const resolveDemoPath = config.demo.filename.map((file) => {
return join(resolveDemoDirectory, file);
});
export const pathToStartDemo = config.demo.filename.map((file) => {
return join(currentDirectory, '/external/demo/', file);
});
// path to restore file
export const resolveRestoreFile = join(getAppDataPath(), config.restoreFile);
@@ -1,5 +1,5 @@
import { millisToString } from 'ontime-utils';
import { getA1Notation, cellRequestFromEvent, cellRequenstFromProjectData } from '../sheetUtils.js';
import { getA1Notation, cellRequenstFromEvent, cellRequenstFromProjectData } from '../sheetUtils.js';
import { EndAction, OntimeRundownEntry, ProjectData, SupportedEvent, TimerType } from 'ontime-types';
describe('getA1Notation()', () => {
@@ -75,7 +75,7 @@ describe('cellRequenstFromEvent()', () => {
revision: { row: 1, col: 38 },
id: { row: 1, col: 39 },
};
const result = cellRequestFromEvent(event, 1, 1234, metadata);
const result = cellRequenstFromEvent(event, 1, 1234, metadata);
expect(result.updateCells.rows[0].values[5].userEnteredValue.stringValue).toStrictEqual(event.note);
});
@@ -136,7 +136,7 @@ describe('cellRequenstFromEvent()', () => {
revision: { row: 1, col: 38 },
id: { row: 1, col: 39 },
};
const result = cellRequestFromEvent(event, 1, 1234, metadata).updateCells.rows[0].values[10].userEnteredValue
const result = cellRequenstFromEvent(event, 1, 1234, metadata).updateCells.rows[0].values[10].userEnteredValue
.stringValue;
expect(result).toStrictEqual(millisToString(event.duration));
});
@@ -198,7 +198,7 @@ describe('cellRequenstFromEvent()', () => {
revision: { row: 1, col: 38 },
id: { row: 1, col: 39 },
};
const result = cellRequestFromEvent(event, 1, 1234, metadata);
const result = cellRequenstFromEvent(event, 1, 1234, metadata);
expect(result.updateCells.rows[0].values[11].userEnteredValue.stringValue).toStrictEqual('x');
expect(result.updateCells.rows[0].values[12].userEnteredValue.stringValue).toStrictEqual('');
});
@@ -238,7 +238,7 @@ describe('cellRequenstFromEvent()', () => {
subtitle: { row: 1, col: 10 },
user0: { row: 1, col: 16 },
};
const result = cellRequestFromEvent(event, 1, 1234, metadata);
const result = cellRequenstFromEvent(event, 1, 1234, metadata);
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(event.cue);
expect(result.updateCells.rows[0].values[6].userEnteredValue.stringValue).toStrictEqual(event.title);
expect(result.updateCells.rows[0].values[10].userEnteredValue.stringValue).toStrictEqual(event.subtitle);
@@ -279,7 +279,7 @@ describe('cellRequenstFromEvent()', () => {
subtitle: { row: 1, col: 10 },
user0: { row: 1, col: 16 },
};
const result = cellRequestFromEvent(event, 1, 1234, metadata);
const result = cellRequenstFromEvent(event, 1, 1234, metadata);
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(event.cue);
expect(result.updateCells.rows[0].values[1].userEnteredValue.stringValue).toStrictEqual(event.title);
expect(result.updateCells.rows[0].values[5].userEnteredValue.stringValue).toStrictEqual(event.subtitle);
@@ -320,9 +320,9 @@ describe('cellRequenstFromEvent()', () => {
subtitle: { row: 1, col: 10 },
user0: { row: 10, col: 16 },
};
const result1 = cellRequestFromEvent(event, 1, 1234, metadata);
const result1 = cellRequenstFromEvent(event, 1, 1234, metadata);
expect(result1.updateCells.start.sheetId).toStrictEqual(1234);
const result2 = cellRequestFromEvent(event, 10, 1234, metadata);
const result2 = cellRequenstFromEvent(event, 10, 1234, metadata);
expect(result2.updateCells.start.rowIndex).toStrictEqual(21);
expect(result2.updateCells.start.columnIndex).toStrictEqual(5);
expect(result2.updateCells.fields).toStrictEqual('userEnteredValue');
-3
View File
@@ -51,9 +51,6 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
const projectMetadata = {};
const rundownMetadata = {};
const importMap: ExcelImportMap = { ...defaultExcelImportMap, ...options };
for (const [key, value] of Object.entries(importMap)) {
importMap[key] = value.toLocaleLowerCase();
}
const projectData: Partial<ProjectData> = {
title: '',
description: '',
+2 -2
View File
@@ -36,7 +36,7 @@ export function getA1Notation(row: number, column: number): string {
* @param {any} metadata - object with all the cell positions of the title of each attribute
* @returns {sheets_v4.Schema} - list of update requests
*/
export function cellRequestFromEvent(
export function cellRequenstFromEvent(
event: OntimeRundownEntry,
index: number,
worksheetId: number,
@@ -50,7 +50,7 @@ export function cellRequestFromEvent(
const titleCol = tmp[0][1].col;
for (const [index, e] of tmp.entries()) {
if (index !== 0) {
if (index != 0) {
const prevCol = tmp[index - 1][1].col;
const thisCol = e[1].col;
const diff = thisCol - prevCol;
+19 -17
View File
@@ -10,7 +10,7 @@ import { logger } from '../classes/Logger.js';
import { DataProvider } from '../classes/data-provider/DataProvider.js';
import { getAppDataPath } from '../setup.js';
import { ensureDirectory } from './fileManagement.js';
import { cellRequestFromEvent, cellRequenstFromProjectData, getA1Notation } from './sheetUtils.js';
import { cellRequenstFromEvent, cellRequenstFromProjectData, getA1Notation } from './sheetUtils.js';
import { parseExcel } from './parser.js';
import { parseProject, parseRundown, parseUserFields } from './parserFunctions.js';
import { ExcelImportMap } from 'ontime-utils';
@@ -81,7 +81,7 @@ class Sheet {
/**
* @description SETP 1 - test that the saved object is pressent
*/
testClientSecret() {
testClientSecrect() {
return Sheet.clientSecret !== null;
}
@@ -197,7 +197,7 @@ class Sheet {
if (ref.credentials.expiry_date > 10000) {
return true;
} else {
throw new Error('Unable to use access token');
throw new Error('Unable to use accese token');
}
} else {
throw new Error('Unable to authenticate');
@@ -272,14 +272,14 @@ class Sheet {
public async push(id: string, options: ExcelImportMap) {
const { worksheetId, range } = await this.exist(id, options.worksheet);
const readResponse = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.values.get({
const rq = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.values.get({
spreadsheetId: id,
valueRenderOption: 'FORMATTED_VALUE',
majorDimension: 'ROWS',
range: range,
});
if (readResponse.status === 200) {
const { rundownMetadata, projectMetadata } = parseExcel(readResponse.data.values, options);
if (rq.status === 200) {
const { rundownMetadata, projectMetadata } = parseExcel(rq.data.values, options);
const rundown = DataProvider.getRundown();
const projectData = DataProvider.getProjectData();
const titleRow = Object.values(rundownMetadata)[0]['row'];
@@ -317,13 +317,13 @@ class Sheet {
//update the corresponding row with event data
rundown.forEach((entry, index) =>
updateRundown.push(cellRequestFromEvent(entry, index, worksheetId, rundownMetadata)),
updateRundown.push(cellRequenstFromEvent(entry, index, worksheetId, rundownMetadata)),
);
//update project data
updateRundown.push(cellRequenstFromProjectData(projectData, worksheetId, projectMetadata));
const writeResponse = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.batchUpdate({
const writeResponds = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.batchUpdate({
spreadsheetId: id,
requestBody: {
includeSpreadsheetInResponse: false,
@@ -332,18 +332,18 @@ class Sheet {
},
});
if (writeResponse.status === 200) {
logger.info(LogOrigin.Server, `Sheet: write: ${writeResponse.statusText}`);
if (writeResponds.status == 200) {
logger.info(LogOrigin.Server, `Sheet: write: ${writeResponds.statusText}`);
} else {
throw new Error(`Sheet: write failed: ${writeResponse.statusText}`);
throw new Error(`Sheet: write faild: ${writeResponds.statusText}`);
}
} else {
throw new Error(`Sheet: read failed: ${readResponse.statusText}`);
throw new Error(`Sheet: read faild: ${rq.statusText}`);
}
}
/**
* @description STEP 5 - Downpload the rundown from sheet
* @description SETP 5 - Downpload the rundown from sheet
* @param {string} id - id of the sheet https://docs.google.com/spreadsheets/d/[[spreadsheetId]]/edit#gid=0
* @param {ExcelImportMap} options
* @returns {Promise<Partial<ResponseOK>>}
@@ -354,25 +354,27 @@ class Sheet {
const res: Partial<ResponseOK> = {};
const googleResponse = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.values.get({
const rq = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.values.get({
spreadsheetId: id,
valueRenderOption: 'FORMATTED_VALUE',
majorDimension: 'ROWS',
range,
});
if (googleResponse.status === 200) {
if (rq.status === 200) {
res.data = {};
const dataFromSheet = parseExcel(googleResponse.data.values, options);
const dataFromSheet = parseExcel(rq.data.values, options);
res.data.rundown = parseRundown(dataFromSheet);
if (res.data.rundown.length < 1) {
throw new Error(`Sheet: Could not find data to import in the worksheet`);
}
console.log(dataFromSheet.project);
console.log(dataFromSheet.userFields);
res.data.project = parseProject(dataFromSheet);
res.data.userFields = parseUserFields(dataFromSheet);
return res;
} else {
throw new Error(`Sheet: read failed: ${googleResponse.statusText}`);
throw new Error(`Sheet: read faild: ${rq.statusText}`);
}
}
}
+1 -1
View File
@@ -99,7 +99,7 @@
},
"settings": {
"app": "ontime",
"version": "2.24.8",
"version": "2.21.3",
"serverPort": 4001,
"editorKey": null,
"operatorKey": null,
+1 -1
View File
@@ -3,7 +3,7 @@ version: "3"
services:
ontime:
container_name: ontime
image: getontime/ontime:latest
image: getontime/ontime:beta_v2
ports:
- "4001:4001/tcp"
- "8888:8888/udp"
+2 -4
View File
@@ -1,9 +1,9 @@
{
"name": "ontime",
"version": "2.28.17",
"version": "2.21.3",
"description": "Time keeping for live events",
"keywords": [
"lightdev",
"lighdev",
"ontime",
"timer",
"rundown"
@@ -30,8 +30,6 @@
"build": "turbo run build",
"build:local": "turbo run build:local",
"build:electron": "turbo run build:electron",
"build:docker": "turbo run build:docker",
"build:localdocker": "turbo run build:localdocker",
"dist-win": "turbo run dist-win",
"dist-mac": "turbo run dist-mac",
"dist-linux": "turbo run dist-linux",
@@ -93,17 +93,6 @@ describe('validateTimes()', () => {
expect(timeEnd).toBe(10);
expect(duration).toBe(10);
});
it('ensures values dont overflow dayMs', () => {
const start = 86100000;
const endOverDay = 87420000;
const durationNormal = 1320000;
const { timeStart, timeEnd, duration } = validateTimes(start, endOverDay, durationNormal);
expect(timeStart).toBe(start);
expect(timeEnd).toBe(1020000);
expect(duration).toBe(durationNormal);
});
});
describe('calculateDuration()', () => {
@@ -52,9 +52,9 @@ function convertToInteger(value: unknown): number {
* @param _duration
*/
export function validateTimes(_start?: unknown, _end?: unknown, _duration?: unknown) {
const timeStart = convertToInteger(_start) % dayInMs;
const timeEnd = convertToInteger(_end) % dayInMs;
const duration = convertToInteger(_duration) % dayInMs;
const timeStart = convertToInteger(_start);
const timeEnd = convertToInteger(_end);
const duration = convertToInteger(_duration);
if (_start != null && _end != null) {
// Case 1. if we have start and end, duration must be derived
-1
View File
@@ -26,7 +26,6 @@
"build:local": {},
"build:electron": {},
"build:docker": {},
"build:localdocker": {},
"e2e": {
"dependsOn": ["^build"]
},