mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-17 13:23:35 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6f61b16cb | |||
| 08a32a7265 | |||
| 341d554bc5 | |||
| 4e49ca3ebe | |||
| 19536c3ea8 | |||
| dc8b55aa53 | |||
| cb6dee552f | |||
| ba89673d5f | |||
| a719f5827e | |||
| 7fcf8153af | |||
| e0b2ea8f5c | |||
| be92fa43fa | |||
| fda1dbdd5f | |||
| 8ae1c6de42 | |||
| fa56f48461 | |||
| 364d698bbc | |||
| 8fa7392698 | |||
| 2464ac1bd9 | |||
| a6683fd2cd | |||
| d2c6d46585 | |||
| 95d7b61529 | |||
| 884947374d | |||
| e52ff58d74 | |||
| 0024b02638 |
@@ -9,6 +9,7 @@ export const HTTP_SETTINGS = ['httpSettings'];
|
|||||||
export const APP_SETTINGS = ['appSettings'];
|
export const APP_SETTINGS = ['appSettings'];
|
||||||
export const VIEW_SETTINGS = ['viewSettings'];
|
export const VIEW_SETTINGS = ['viewSettings'];
|
||||||
export const RUNTIME = ['runtimeStore'];
|
export const RUNTIME = ['runtimeStore'];
|
||||||
|
export const SHEET_STATE = ['sheetState'];
|
||||||
|
|
||||||
const location = window.location;
|
const location = window.location;
|
||||||
const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import {
|
|||||||
Alias,
|
Alias,
|
||||||
DatabaseModel,
|
DatabaseModel,
|
||||||
GetInfo,
|
GetInfo,
|
||||||
GoogleSheet,
|
|
||||||
GoogleSheetState,
|
|
||||||
HttpSettings,
|
HttpSettings,
|
||||||
OntimeRundown,
|
OntimeRundown,
|
||||||
OSCSettings,
|
OSCSettings,
|
||||||
@@ -249,14 +247,13 @@ export async function postNew(initialData: Partial<ProjectData>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description sheet Client File
|
* @description STEP 1
|
||||||
* @return {Promise}
|
|
||||||
*/
|
*/
|
||||||
export const uploadSheetClientFile = async (file: File) => {
|
export const uploadSheetClientFile = async (file: File) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('userFile', file);
|
formData.append('userFile', file);
|
||||||
const res = await axios
|
const res = await axios
|
||||||
.post(`${ontimeURL}/sheet-clientsecrect`, formData, {
|
.post(`${ontimeURL}/sheet/clientsecrect`, formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
@@ -265,44 +262,59 @@ export const uploadSheetClientFile = async (file: File) => {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description STEP 1 test
|
||||||
|
*/
|
||||||
|
export const getClientSecrect = async () => {
|
||||||
|
const response = await axios.get(`${ontimeURL}/sheet/clientsecrect`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description STEP 2
|
||||||
|
*/
|
||||||
export const getSheetsAuthUrl = async () => {
|
export const getSheetsAuthUrl = async () => {
|
||||||
const res = await axios.get(`${ontimeURL}/sheet-authurl`);
|
const response = await axios.get(`${ontimeURL}/sheet/authentication/url`);
|
||||||
return res.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const postPreviewSheet = async () => {
|
/**
|
||||||
const response = await axios.post(`${ontimeURL}/sheet-preview`);
|
* @description STEP 2 test
|
||||||
return response.data.data;
|
*/
|
||||||
|
export const getAuthentication = async () => {
|
||||||
|
const response = await axios.get(`${ontimeURL}/sheet/authentication`);
|
||||||
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const postPushSheet = async () => {
|
/**
|
||||||
const response = await axios.post(`${ontimeURL}/sheet-push`);
|
* @description STEP 3
|
||||||
|
* @returns worksheetOptions
|
||||||
|
*/
|
||||||
|
export const postId = async (id: string) => {
|
||||||
|
const response = await axios.post(`${ontimeURL}/sheet/id`, { id });
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description STEP 4
|
||||||
|
*/
|
||||||
|
export const postWorksheet = async (id: string, worksheet: string) => {
|
||||||
|
const response = await axios.post(`${ontimeURL}/sheet/worksheet`, { id, worksheet });
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description STEP 5
|
||||||
|
*/
|
||||||
|
export const postPreviewSheet = async (id: string, options: ExcelImportMap) => {
|
||||||
|
const response = await axios.post(`${ontimeURL}/sheet/pull`, { id, options });
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description HTTP request to retrieve google sheets settings
|
* @description STEP 5
|
||||||
* @return {Promise}
|
|
||||||
*/
|
*/
|
||||||
export async function getSheetSettings(): Promise<GoogleSheet> {
|
export const postPushSheet = async (id: string, options: ExcelImportMap) => {
|
||||||
const res = await axios.get(`${ontimeURL}/sheet-settings`);
|
const response = await axios.post(`${ontimeURL}/sheet-push`, { id, options });
|
||||||
return res.data;
|
return response.data.data;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to mutate google sheets settings
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function postSheetSettings(data: GoogleSheet): Promise<GoogleSheet> {
|
|
||||||
const res = await axios.post(`${ontimeURL}/sheet-settings`, data);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to retrieve google sheets state
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function getSheetstate(): Promise<GoogleSheetState> {
|
|
||||||
const res = await axios.get(`${ontimeURL}/sheet-state`);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
|
AlertDescription,
|
||||||
|
AlertIcon,
|
||||||
|
AlertTitle,
|
||||||
Button,
|
Button,
|
||||||
|
HStack,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
@@ -9,29 +14,33 @@ import {
|
|||||||
ModalFooter,
|
ModalFooter,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
|
Select,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { IoArrowDownCircleOutline } from '@react-icons/all-files/io5/IoArrowDownCircleOutline';
|
|
||||||
import { IoArrowUpCircleOutline } from '@react-icons/all-files/io5/IoArrowUpCircleOutline';
|
|
||||||
import { IoCheckmarkCircleOutline } from '@react-icons/all-files/io5/IoCheckmarkCircleOutline';
|
|
||||||
import { IoCloseCircleOutline } from '@react-icons/all-files/io5/IoCloseCircleOutline';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { GoogleSheetState, OntimeRundown, ProjectData, UserFields } from 'ontime-types';
|
import { OntimeRundown, ProjectData, UserFields } from 'ontime-types';
|
||||||
|
import { defaultExcelImportMap, ExcelImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { PROJECT_DATA, RUNDOWN, USERFIELDS } from '../../../common/api/apiConstants';
|
import { PROJECT_DATA, RUNDOWN, USERFIELDS } from '../../../common/api/apiConstants';
|
||||||
import { maybeAxiosError } from '../../../common/api/apiUtils';
|
import { maybeAxiosError } from '../../../common/api/apiUtils';
|
||||||
import {
|
import {
|
||||||
|
getAuthentication,
|
||||||
|
getClientSecrect,
|
||||||
getSheetsAuthUrl,
|
getSheetsAuthUrl,
|
||||||
getSheetSettings,
|
|
||||||
getSheetstate,
|
|
||||||
patchData,
|
patchData,
|
||||||
|
postId,
|
||||||
postPreviewSheet,
|
postPreviewSheet,
|
||||||
postPushSheet,
|
postPushSheet,
|
||||||
postSheetSettings,
|
postWorksheet,
|
||||||
uploadSheetClientFile,
|
uploadSheetClientFile,
|
||||||
} from '../../../common/api/ontimeApi';
|
} from '../../../common/api/ontimeApi';
|
||||||
import { projectDataPlaceholder } from '../../../common/models/ProjectData';
|
import { projectDataPlaceholder } from '../../../common/models/ProjectData';
|
||||||
import { userFieldsPlaceholder } from '../../../common/models/UserFields';
|
import { userFieldsPlaceholder } from '../../../common/models/UserFields';
|
||||||
|
import { openLink } from '../../../common/utils/linkUtils';
|
||||||
|
import ModalLink from '../ModalLink';
|
||||||
import PreviewExcel from '../upload-modal/preview/PreviewExcel';
|
import PreviewExcel from '../upload-modal/preview/PreviewExcel';
|
||||||
|
import ExcelFileOptions from '../upload-modal/upload-options/ExcelFileOptions';
|
||||||
|
|
||||||
|
import Step from './Step';
|
||||||
|
|
||||||
interface SheetsModalProps {
|
interface SheetsModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -41,18 +50,37 @@ interface SheetsModalProps {
|
|||||||
export default function SheetsModal(props: SheetsModalProps) {
|
export default function SheetsModal(props: SheetsModalProps) {
|
||||||
const { isOpen, onClose } = props;
|
const { isOpen, onClose } = props;
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [rundown, setRundown] = useState<OntimeRundown | null>(null);
|
const [rundown, setRundown] = useState<OntimeRundown | null>(null);
|
||||||
const [userFields, setUserFields] = useState<UserFields | null>(null);
|
const [userFields, setUserFields] = useState<UserFields | null>(null);
|
||||||
const [project, setProject] = useState<ProjectData | null>(null);
|
const [project, setProject] = useState<ProjectData | null>(null);
|
||||||
|
|
||||||
const [sheetState, setSheetState] = useState<GoogleSheetState>({ auth: false, id: false, worksheet: false });
|
const [id, setSheetId] = useState('');
|
||||||
|
const [worksheet, setWorksheet] = useState('');
|
||||||
|
const [worksheetOptions, setWorksheetOptions] = useState(new Array<string>());
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const [direction, setDirection] = useState('none');
|
||||||
|
const excelFileOptions = useRef<ExcelImportMap>(defaultExcelImportMap);
|
||||||
|
|
||||||
const sheetid = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const worksheet = useRef<HTMLInputElement>(null);
|
|
||||||
|
const [state, setState] = useState({
|
||||||
|
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');
|
||||||
|
testClientSecrect();
|
||||||
|
if (state.clientSecret.complet) testAuthentication();
|
||||||
|
if (state.authenticate.complet) testSheetId();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setRundown(null);
|
setRundown(null);
|
||||||
@@ -60,73 +88,157 @@ export default function SheetsModal(props: SheetsModalProps) {
|
|||||||
setUserFields(null);
|
setUserFields(null);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//SETP-1 Upload Client ID
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFile = async (event: ChangeEvent<HTMLInputElement>) => {
|
const handleFile = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const selectedFile = event?.target?.files?.[0];
|
if (!event.target.files?.length) {
|
||||||
if (selectedFile) {
|
setState({
|
||||||
await uploadSheetClientFile(selectedFile).catch((err) => {
|
clientSecret: { complet: false, message: 'Missing file' },
|
||||||
console.error(err); //TODO: how to show this to the user
|
authenticate: { complet: false, message: '' },
|
||||||
|
id: { complet: false, message: '' },
|
||||||
|
worksheet: { complet: false, message: '' },
|
||||||
|
pullPush: { complet: false, message: '' },
|
||||||
});
|
});
|
||||||
_onChange();
|
return;
|
||||||
|
}
|
||||||
|
const selectedFile = event.target.files[0];
|
||||||
|
uploadSheetClientFile(selectedFile)
|
||||||
|
.then(() => {
|
||||||
|
setState({ ...state, clientSecret: { complet: true, message: '' } });
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const message = maybeAxiosError(err);
|
||||||
|
setState({
|
||||||
|
clientSecret: { complet: false, message },
|
||||||
|
authenticate: { complet: false, message: '' },
|
||||||
|
id: { complet: false, message: '' },
|
||||||
|
worksheet: { complet: false, message: '' },
|
||||||
|
pullPush: { complet: false, message: '' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const testClientSecrect = () => {
|
||||||
|
getClientSecrect()
|
||||||
|
.then(() => {
|
||||||
|
setState({ ...state, clientSecret: { complet: true, message: '' } });
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const message = maybeAxiosError(err);
|
||||||
|
setState({
|
||||||
|
clientSecret: { complet: false, message },
|
||||||
|
authenticate: { complet: false, message: '' },
|
||||||
|
id: { complet: false, message: '' },
|
||||||
|
worksheet: { complet: false, message: '' },
|
||||||
|
pullPush: { complet: false, message: '' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//SETP-2 Authenticate
|
||||||
|
const handleAuthenticate = () => {
|
||||||
|
getSheetsAuthUrl()
|
||||||
|
.then((data) => {
|
||||||
|
openLink(data);
|
||||||
|
window.addEventListener('focus', () => testAuthentication(), { once: true });
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const message = maybeAxiosError(err);
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
authenticate: { complet: false, message },
|
||||||
|
id: { complet: false, message: '' },
|
||||||
|
worksheet: { complet: false, message: '' },
|
||||||
|
pullPush: { complet: false, message: '' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const testAuthentication = () => {
|
||||||
|
getAuthentication()
|
||||||
|
.then(() => {
|
||||||
|
setState({ ...state, authenticate: { complet: true, message: '' } });
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const message = maybeAxiosError(error);
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
authenticate: { complet: false, message },
|
||||||
|
id: { complet: false, message: '' },
|
||||||
|
worksheet: { complet: false, message: '' },
|
||||||
|
pullPush: { complet: false, message: '' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//SETP-3 set sheet ID
|
||||||
|
const testSheetId = () => {
|
||||||
|
postId(id)
|
||||||
|
.then((data) => {
|
||||||
|
setState({ ...state, id: { complet: true, message: '' } });
|
||||||
|
setWorksheetOptions(data.worksheetOptions);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const message = maybeAxiosError(err);
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
id: { complet: false, message },
|
||||||
|
worksheet: { complet: false, message: '' },
|
||||||
|
pullPush: { complet: false, message: '' },
|
||||||
|
});
|
||||||
|
setWorksheetOptions([]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//SETP-4 Select Worksheet
|
||||||
|
const testWorksheet = (value: string) => {
|
||||||
|
excelFileOptions.current.worksheet = value;
|
||||||
|
setWorksheet(value);
|
||||||
|
postWorksheet(id, worksheet)
|
||||||
|
.then(() => {
|
||||||
|
setState({ ...state, worksheet: { complet: true, message: '' } });
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const message = maybeAxiosError(err);
|
||||||
|
setState({ ...state, worksheet: { complet: false, message }, pullPush: { complet: false, message: '' } });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//SETP-5 Upload / Download
|
||||||
|
const updateExcelFileOptions = <T extends keyof ExcelImportMap>(field: T, value: ExcelImportMap[T]) => {
|
||||||
|
if (excelFileOptions.current[field] !== value) {
|
||||||
|
excelFileOptions.current = { ...excelFileOptions.current, [field]: value };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const _onChange = async () => {
|
|
||||||
setSheetState(await getSheetstate());
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getSheetSettings().then((data) => {
|
|
||||||
if (sheetid.current?.value != data.id || worksheet.current?.value != data.worksheet) {
|
|
||||||
_onChange();
|
|
||||||
}
|
|
||||||
if (sheetid.current) {
|
|
||||||
sheetid.current.value = data.id;
|
|
||||||
}
|
|
||||||
if (worksheet.current) {
|
|
||||||
worksheet.current.value = data.worksheet;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const handelSave = () => {
|
|
||||||
postSheetSettings({ id: sheetid.current?.value ?? '', worksheet: worksheet.current?.value ?? '' }).then((data) => {
|
|
||||||
_onChange();
|
|
||||||
if (sheetid.current) {
|
|
||||||
sheetid.current.value = data.id;
|
|
||||||
}
|
|
||||||
if (worksheet.current) {
|
|
||||||
worksheet.current.value = data.worksheet;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAuthenticate = () => {
|
|
||||||
getSheetsAuthUrl().then((data) => {
|
|
||||||
if (data != 'bad') {
|
|
||||||
window.open(data, '_blank', 'noreferrer');
|
|
||||||
//TODO: can we detect when this window is closed
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePullData = () => {
|
const handlePullData = () => {
|
||||||
postPreviewSheet().then((data) => {
|
postPreviewSheet(id, excelFileOptions.current)
|
||||||
setProject(data.project);
|
.then((data) => {
|
||||||
setRundown(data.rundown);
|
setProject(data.project);
|
||||||
setUserFields(data.userFields);
|
setRundown(data.rundown);
|
||||||
});
|
setUserFields(data.userFields);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const message = maybeAxiosError(error);
|
||||||
|
setDirection('none');
|
||||||
|
setState({ ...state, pullPush: { complet: false, message } });
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePushData = () => {
|
const handlePushData = () => {
|
||||||
postPushSheet();
|
postPushSheet(id, excelFileOptions.current).catch((error) => {
|
||||||
|
const message = maybeAxiosError(error);
|
||||||
|
setDirection('none');
|
||||||
|
setState({ ...state, pullPush: { complet: false, message } });
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//GET preview
|
||||||
const handleFinalise = async () => {
|
const handleFinalise = async () => {
|
||||||
// this step is currently only used for excel files, after preview
|
|
||||||
if (rundown && userFields && project) {
|
if (rundown && userFields && project) {
|
||||||
let doClose = false;
|
let doClose = false;
|
||||||
try {
|
try {
|
||||||
@@ -140,8 +252,7 @@ export default function SheetsModal(props: SheetsModalProps) {
|
|||||||
doClose = true;
|
doClose = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = maybeAxiosError(error);
|
const message = maybeAxiosError(error);
|
||||||
console.log(message);
|
console.error(message);
|
||||||
// setErrors(`Failed applying changes ${message}`);
|
|
||||||
} finally {
|
} finally {
|
||||||
if (doClose) {
|
if (doClose) {
|
||||||
handleClose();
|
handleClose();
|
||||||
@@ -163,93 +274,184 @@ export default function SheetsModal(props: SheetsModalProps) {
|
|||||||
>
|
>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>Sheets!</ModalHeader>
|
<ModalHeader>Rundown from sheets</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{rundown && (
|
<Alert status='info' variant='ontime-on-light-info'>
|
||||||
|
<AlertIcon />
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<AlertTitle>Sync with Google Sheets</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
<ModalLink href='https://ontime.gitbook.io/v2/features/google-sheet-experimental'>
|
||||||
|
For more information, see the docs
|
||||||
|
</ModalLink>
|
||||||
|
</AlertDescription>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
{!rundown ? (
|
||||||
|
direction === 'up' ? (
|
||||||
|
<div>{direction}</div>
|
||||||
|
) : direction === 'down' ? (
|
||||||
|
<ExcelFileOptions optionsRef={excelFileOptions} updateOptions={updateExcelFileOptions} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Step
|
||||||
|
title='1 - Upload OAuth 2.0 Client ID'
|
||||||
|
completed={state.clientSecret.complet}
|
||||||
|
disabled={false}
|
||||||
|
error={state.clientSecret.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
ref={fileInputRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
type='file'
|
||||||
|
onChange={handleFile}
|
||||||
|
accept='.json'
|
||||||
|
data-testid='file-input'
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', gap: '1em' }}>
|
||||||
|
<Button size='sm' variant='ontime-subtle-on-light' onClick={handleClick}>
|
||||||
|
{state.clientSecret.complet ? 'Reupload Client ID' : 'Upload Client ID'}
|
||||||
|
</Button>
|
||||||
|
<Button size='sm' variant='ontime-ghosted-on-light' onClick={testClientSecrect}>
|
||||||
|
Retry Client ID
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title='2 - Authenticate with Google'
|
||||||
|
completed={state.authenticate.complet}
|
||||||
|
disabled={!state.clientSecret.complet}
|
||||||
|
error={state.authenticate.message}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', gap: '1em' }}>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-subtle-on-light'
|
||||||
|
onClick={handleAuthenticate}
|
||||||
|
isDisabled={!state.clientSecret.complet}
|
||||||
|
>
|
||||||
|
Authenticate
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-ghosted-on-light'
|
||||||
|
onClick={testAuthentication}
|
||||||
|
isDisabled={!state.clientSecret.complet}
|
||||||
|
>
|
||||||
|
Retry Connection
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title='3 - Add Document ID'
|
||||||
|
completed={state.id.complet}
|
||||||
|
disabled={!state.authenticate.complet}
|
||||||
|
error={state.id.message}
|
||||||
|
>
|
||||||
|
<HStack>
|
||||||
|
<Input
|
||||||
|
type='text'
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-filled-on-light'
|
||||||
|
disabled={!state.authenticate.complet}
|
||||||
|
value={id}
|
||||||
|
onChange={(event) => setSheetId(event.target.value)}
|
||||||
|
/>
|
||||||
|
<Button size='sm' variant='ontime-subtle-on-light' padding='0 2em' onClick={testSheetId}>
|
||||||
|
Connect
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
title='4 - Select Worksheet to import'
|
||||||
|
completed={state.worksheet.complet}
|
||||||
|
disabled={worksheetOptions.length == 0}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
size='sm'
|
||||||
|
disabled={worksheetOptions.length == 0}
|
||||||
|
placeholder='Select a worksheet'
|
||||||
|
onChange={(event) => testWorksheet(event.target.value)}
|
||||||
|
value={worksheet}
|
||||||
|
>
|
||||||
|
{worksheetOptions.map((value) => (
|
||||||
|
<option key={value} value={value}>
|
||||||
|
{value}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title='5 - Upload / Download rundown' completed={false} disabled={!state.worksheet.complet}>
|
||||||
|
<div style={{ display: 'flex', gap: '1em' }}>
|
||||||
|
<Button
|
||||||
|
disabled={!state.worksheet.complet}
|
||||||
|
variant='ontime-subtle-on-light'
|
||||||
|
padding='0 2em'
|
||||||
|
onClick={() => setDirection('up')}
|
||||||
|
>
|
||||||
|
Upload
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={!state.worksheet.complet}
|
||||||
|
variant='ontime-subtle-on-light'
|
||||||
|
padding='0 2em'
|
||||||
|
onClick={() => setDirection('down')}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Step>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
<PreviewExcel
|
<PreviewExcel
|
||||||
rundown={rundown ?? []}
|
rundown={rundown ?? []}
|
||||||
project={project ?? projectDataPlaceholder}
|
project={project ?? projectDataPlaceholder}
|
||||||
userFields={userFields ?? userFieldsPlaceholder}
|
userFields={userFields ?? userFieldsPlaceholder}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!rundown && (
|
|
||||||
<>
|
|
||||||
<Input
|
|
||||||
ref={fileInputRef}
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
type='file'
|
|
||||||
onChange={handleFile}
|
|
||||||
accept='.json'
|
|
||||||
data-testid='file-input'
|
|
||||||
/>
|
|
||||||
<div>Need to add some help here</div>
|
|
||||||
<div>
|
|
||||||
<Button onClick={handleClick}>Upload Client Secrect</Button>
|
|
||||||
</div>
|
|
||||||
<Button variant='ontime-filled' padding='0 2em' onClick={handleAuthenticate}>
|
|
||||||
Authenticate
|
|
||||||
</Button>
|
|
||||||
{sheetState.auth ? <div>You are authenticated</div> : <div>You are not authenticated</div>}
|
|
||||||
<div>
|
|
||||||
<label htmlFor='sheetid'>Sheet ID </label>
|
|
||||||
<Input
|
|
||||||
type='text'
|
|
||||||
ref={sheetid}
|
|
||||||
id='sheetid'
|
|
||||||
width='240px'
|
|
||||||
size='sm'
|
|
||||||
textAlign='right'
|
|
||||||
variant='ontime-filled-on-light'
|
|
||||||
/>
|
|
||||||
{sheetState.id ? <IoCheckmarkCircleOutline /> : <IoCloseCircleOutline />}
|
|
||||||
<br />
|
|
||||||
<label htmlFor='worksheet'>Worksheet </label>
|
|
||||||
<Input
|
|
||||||
type='text'
|
|
||||||
ref={worksheet}
|
|
||||||
id='worksheet'
|
|
||||||
width='240px'
|
|
||||||
size='sm'
|
|
||||||
textAlign='right'
|
|
||||||
variant='ontime-filled-on-light'
|
|
||||||
/>
|
|
||||||
{sheetState.worksheet ? <IoCheckmarkCircleOutline /> : <IoCloseCircleOutline />}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
{!rundown && (
|
{rundown ? (
|
||||||
<div>
|
<div style={{ display: 'flex', gap: '1em' }}>
|
||||||
<Button
|
<Button
|
||||||
variant='ontime-subtle-on-light'
|
onClick={() => {
|
||||||
padding='0 2em'
|
setRundown(null);
|
||||||
onClick={handlePullData}
|
setDirection('none');
|
||||||
rightIcon={<IoArrowDownCircleOutline />}
|
}}
|
||||||
|
variant='ontime-ghost-on-light'
|
||||||
>
|
>
|
||||||
Pull data
|
Go Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant='ontime-filled' padding='0 2em' onClick={handleFinalise}>
|
||||||
variant='ontime-subtle-on-light'
|
Import
|
||||||
padding='0 2em'
|
|
||||||
onClick={handlePushData}
|
|
||||||
rightIcon={<IoArrowUpCircleOutline />}
|
|
||||||
>
|
|
||||||
Push data
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : direction === 'up' ? (
|
||||||
<Button variant='ontime-ghost-on-light'>Reset</Button>
|
<div style={{ display: 'flex', gap: '1em' }}>
|
||||||
{!rundown && (
|
<Button onClick={() => setDirection('none')} variant='ontime-ghost-on-light'>
|
||||||
<Button variant='ontime-filled' padding='0 2em' onClick={handelSave}>
|
Go Back
|
||||||
Save
|
</Button>
|
||||||
</Button>
|
<Button variant='ontime-filled' padding='0 2em' onClick={handlePushData}>
|
||||||
)}
|
Upload
|
||||||
{rundown && (
|
</Button>
|
||||||
<Button variant='ontime-filled' padding='0 2em' onClick={handleFinalise}>
|
</div>
|
||||||
Import
|
) : direction === 'down' ? (
|
||||||
</Button>
|
<div style={{ display: 'flex', gap: '1em' }}>
|
||||||
|
<Button onClick={() => setDirection('none')} variant='ontime-ghost-on-light'>
|
||||||
|
Go Back
|
||||||
|
</Button>
|
||||||
|
<Button variant='ontime-filled' padding='0 2em' onClick={handlePullData}>
|
||||||
|
Review
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
)}
|
)}
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
@use '../../../theme/ontimeColours' as *;
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: inline;
|
||||||
|
color: $gray-500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check {
|
||||||
|
color: $green-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorIcon {
|
||||||
|
color: $red-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorText {
|
||||||
|
color: $red-700;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { PropsWithChildren, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { IoCheckmarkCircle } from '@react-icons/all-files/io5/IoCheckmarkCircle';
|
||||||
|
import { IoCloseCircle } from '@react-icons/all-files/io5/IoCloseCircle';
|
||||||
|
import { IoRadioButtonOffOutline } from '@react-icons/all-files/io5/IoRadioButtonOffOutline';
|
||||||
|
|
||||||
|
import style from './Step.module.scss';
|
||||||
|
|
||||||
|
interface StepProps {
|
||||||
|
title: string;
|
||||||
|
disabled: boolean;
|
||||||
|
completed: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Step(props: PropsWithChildren<StepProps>) {
|
||||||
|
const { title, disabled, completed, error, children } = props;
|
||||||
|
const [collapsed, setCollapsed] = useState(disabled);
|
||||||
|
|
||||||
|
const handleCollapse = () => setCollapsed((prev) => !prev);
|
||||||
|
|
||||||
|
const icon = useMemo(() => {
|
||||||
|
if (completed) return <IoCheckmarkCircle className={style.step} style={{ color: 'green' }} />;
|
||||||
|
if (error) return <IoCloseCircle className={style.step} style={{ color: 'red' }} />;
|
||||||
|
return <IoRadioButtonOffOutline className={style.step} />;
|
||||||
|
}, [completed, error]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (completed) {
|
||||||
|
setCollapsed(true);
|
||||||
|
}
|
||||||
|
}, [completed]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.wrapper}>
|
||||||
|
<div className={style.header} onClick={handleCollapse}>
|
||||||
|
{icon}
|
||||||
|
<span className={style.title}>{title}</span>
|
||||||
|
</div>
|
||||||
|
{!collapsed && (
|
||||||
|
<>
|
||||||
|
{error && <div className={style.errorText}>{error}</div>}
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -138,11 +138,6 @@ function createWindow() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
win.setMenu(null);
|
win.setMenu(null);
|
||||||
|
|
||||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
||||||
shell.openExternal(url);
|
|
||||||
return { action: 'deny' };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.disableHardwareAcceleration();
|
app.disableHardwareAcceleration();
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
UserFields,
|
UserFields,
|
||||||
Alias,
|
Alias,
|
||||||
Settings,
|
Settings,
|
||||||
GoogleSheet,
|
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
|
|
||||||
import { data, db } from '../../modules/loadDb.js';
|
import { data, db } from '../../modules/loadDb.js';
|
||||||
@@ -59,15 +58,6 @@ export class DataProvider {
|
|||||||
await this.persist();
|
await this.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
static getGoogleSheet() {
|
|
||||||
return data.googleSheet;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async setGoogleSheet(newData: GoogleSheet) {
|
|
||||||
data.googleSheet = { ...newData };
|
|
||||||
await this.persist();
|
|
||||||
}
|
|
||||||
|
|
||||||
static getOsc() {
|
static getOsc() {
|
||||||
return data.osc;
|
return data.osc;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ import { DatabaseModel } from 'ontime-types';
|
|||||||
* @param {object} newData
|
* @param {object} newData
|
||||||
*/
|
*/
|
||||||
export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseModel>) {
|
export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseModel>) {
|
||||||
const { rundown, project, settings, googleSheet, viewSettings, osc, aliases, userFields } = newData || {};
|
const { rundown, project, settings, viewSettings, osc, aliases, userFields } = newData || {};
|
||||||
return {
|
return {
|
||||||
...existing,
|
...existing,
|
||||||
rundown: rundown ?? existing.rundown,
|
rundown: rundown ?? existing.rundown,
|
||||||
project: { ...existing.project, ...project },
|
project: { ...existing.project, ...project },
|
||||||
settings: { ...existing.settings, ...settings },
|
settings: { ...existing.settings, ...settings },
|
||||||
googleSheet: { ...existing.googleSheet, ...googleSheet },
|
|
||||||
viewSettings: { ...existing.viewSettings, ...viewSettings },
|
viewSettings: { ...existing.viewSettings, ...viewSettings },
|
||||||
aliases: aliases ?? existing.aliases,
|
aliases: aliases ?? existing.aliases,
|
||||||
userFields: {
|
userFields: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Alias, DatabaseModel, GoogleSheet, OntimeRundown, Settings } from 'ontime-types';
|
import { Alias, DatabaseModel, OntimeRundown, Settings } from 'ontime-types';
|
||||||
import { safeMerge } from '../DataProvider.utils.js';
|
import { safeMerge } from '../DataProvider.utils.js';
|
||||||
|
|
||||||
describe('safeMerge', () => {
|
describe('safeMerge', () => {
|
||||||
@@ -21,10 +21,6 @@ describe('safeMerge', () => {
|
|||||||
timeFormat: '24',
|
timeFormat: '24',
|
||||||
language: 'en',
|
language: 'en',
|
||||||
},
|
},
|
||||||
googleSheet: {
|
|
||||||
worksheet: '1',
|
|
||||||
id: '2',
|
|
||||||
},
|
|
||||||
viewSettings: {
|
viewSettings: {
|
||||||
overrideStyles: false,
|
overrideStyles: false,
|
||||||
endMessage: 'existing endMessage',
|
endMessage: 'existing endMessage',
|
||||||
@@ -101,21 +97,6 @@ describe('safeMerge', () => {
|
|||||||
language: 'pt',
|
language: 'pt',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('merges the google sheet key', () => {
|
|
||||||
const newData = {
|
|
||||||
googleSheet: {
|
|
||||||
id: '4',
|
|
||||||
worksheet: '5',
|
|
||||||
} as GoogleSheet,
|
|
||||||
};
|
|
||||||
const mergedData = safeMerge(existing, newData);
|
|
||||||
expect(mergedData.googleSheet).toEqual({
|
|
||||||
id: '4',
|
|
||||||
worksheet: '5',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('merges the osc key', () => {
|
it('merges the osc key', () => {
|
||||||
const newData = {
|
const newData = {
|
||||||
osc: {
|
osc: {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { runtimeCacheStore } from '../stores/cachingStore.js';
|
|||||||
import { delayedRundownCacheKey } from '../services/rundown-service/delayedRundown.utils.js';
|
import { delayedRundownCacheKey } from '../services/rundown-service/delayedRundown.utils.js';
|
||||||
import { integrationService } from '../services/integration-service/IntegrationService.js';
|
import { integrationService } from '../services/integration-service/IntegrationService.js';
|
||||||
|
|
||||||
import { Sheet } from '../utils/sheetsAuth.js';
|
import { sheet } from '../utils/sheetsAuth.js';
|
||||||
|
|
||||||
// Create controller for GET request to '/ontime/poll'
|
// Create controller for GET request to '/ontime/poll'
|
||||||
// Returns data for current state
|
// Returns data for current state
|
||||||
@@ -458,45 +458,19 @@ export const postNew: RequestHandler = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//SHEET Functions
|
||||||
/**
|
/**
|
||||||
* downloads and parses an sheet
|
* @description SETP-1 POST Client Secrect
|
||||||
* @returns parsed result
|
* @returns parsed result
|
||||||
*/
|
*/
|
||||||
export async function previewSheet(req, res) {
|
export async function uploadSheetClientFile(req, res) {
|
||||||
try {
|
|
||||||
const data = await Sheet.pull();
|
|
||||||
res.status(200).send(data);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).send({ message: error.toString() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* downloads and parses an sheet
|
|
||||||
* @returns parsed result
|
|
||||||
*/
|
|
||||||
export async function pushSheet(req, res) {
|
|
||||||
try {
|
|
||||||
await Sheet.push();
|
|
||||||
res.status(200).send('ok');
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).send({ message: error.toString() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* uploads Client secrets file
|
|
||||||
* @returns parsed result
|
|
||||||
*/
|
|
||||||
export async function uploadGoogleSheetClientFile(req, res) {
|
|
||||||
if (!req.file.path) {
|
if (!req.file.path) {
|
||||||
res.status(400).send({ message: 'File not found' });
|
res.status(400).send({ message: 'File not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = JSON.parse(fs.readFileSync(req.file.path as string, 'utf-8'));
|
const client = JSON.parse(fs.readFileSync(req.file.path as string, 'utf-8'));
|
||||||
await Sheet.saveClientSecrets(client);
|
await sheet.saveClientSecrets(client);
|
||||||
res.status(200).send('OK');
|
res.status(200).send('OK');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).send({ message: error.toString() });
|
res.status(500).send({ message: error.toString() });
|
||||||
@@ -507,51 +481,98 @@ export async function uploadGoogleSheetClientFile(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns link to sheet auth url
|
* @description SETP-1 GET Client Secrect status
|
||||||
*/
|
*/
|
||||||
export async function sheetAuthUrl(req, res) {
|
export const getClientSecrect = async (req, res) => {
|
||||||
const successful = await Sheet.openAuthServer();
|
try {
|
||||||
if (successful === false) {
|
const clientSecrectExists = await sheet.testClientSecrect();
|
||||||
res.status(500).send('bad');
|
if (clientSecrectExists) {
|
||||||
} else {
|
res.status(200).send();
|
||||||
res.status(200).send(successful);
|
} else {
|
||||||
|
res.status(500).send({ message: 'The Client ID does not exist' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: error.toString() });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description SETP-2 GET sheet authentication url
|
||||||
|
*/
|
||||||
|
export async function getAuthenticationUrl(req, res) {
|
||||||
|
try {
|
||||||
|
const authUrl = await sheet.openAuthServer();
|
||||||
|
res.status(200).send(authUrl);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: error.toString() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Get google sheet Settings
|
* @description SETP-2 GET sheet authentication status
|
||||||
* @method GET
|
|
||||||
*/
|
*/
|
||||||
export const getGoogleSheetSettings = async (req, res) => {
|
export const getAuthentication = async (req, res) => {
|
||||||
const sheet = await DataProvider.getGoogleSheet();
|
|
||||||
res.status(200).send(sheet);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Change view Settings
|
|
||||||
* @method POST
|
|
||||||
*/
|
|
||||||
export const postGoogleSheetSettings = async (req, res) => {
|
|
||||||
if (failEmptyObjects(req.body, res)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newData = {
|
await sheet.testAuthentication();
|
||||||
id: req.body.id,
|
res.status(200).send();
|
||||||
worksheet: req.body.worksheet,
|
|
||||||
};
|
|
||||||
await DataProvider.setGoogleSheet(newData);
|
|
||||||
res.status(200).send(newData);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).send({ message: error.toString() });
|
res.status(500).send({ message: error.toString() });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Get google sheet state
|
* @description SETP-3 POST sheet id
|
||||||
* @method GET
|
* @returns list of worksheets
|
||||||
*/
|
*/
|
||||||
export const getGoogleSheetState = async (req, res) => {
|
export const postId = async (req, res) => {
|
||||||
res.status(200).send(await Sheet.getSheetState());
|
try {
|
||||||
|
const { id } = req.body;
|
||||||
|
if (id.lenght < 40) {
|
||||||
|
res.status(400).send({ message: 'ID is usualy 44 characters long' });
|
||||||
|
}
|
||||||
|
const state = await sheet.testSheetId(id);
|
||||||
|
res.status(200).send(state);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: error.toString() });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description SETP-4 POST worksheet
|
||||||
|
*/
|
||||||
|
export const postWorksheet = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { worksheet, id } = req.body;
|
||||||
|
const state = await sheet.testWorksheet(worksheet, id);
|
||||||
|
res.status(200).send(state);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: error.toString() });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description STEP-5 POST download undown to sheet
|
||||||
|
* @returns parsed result
|
||||||
|
*/
|
||||||
|
export async function pullSheet(req, res) {
|
||||||
|
try {
|
||||||
|
const { id, options } = req.body;
|
||||||
|
const data = await sheet.pull(id, options);
|
||||||
|
res.status(200).send(data);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: error.toString() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description STEP-5 POST upload rundown to sheet
|
||||||
|
*/
|
||||||
|
export async function pushSheet(req, res) {
|
||||||
|
try {
|
||||||
|
const { id, options } = req.body;
|
||||||
|
await sheet.push(id, options);
|
||||||
|
res.status(200).send();
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: error.toString() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -153,11 +153,8 @@ export const validatePatchProjectFile = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
//TODO: is thise correct
|
export const validateSheetid = [
|
||||||
export const validateSheetPreview = [
|
body('id').exists().isString(),
|
||||||
body('sheetid').isString().optional({ nullable: false }),
|
|
||||||
body('worksheet').isString().optional({ nullable: false }),
|
|
||||||
body('options').isObject().optional({ nullable: true }),
|
|
||||||
(req, res, next) => {
|
(req, res, next) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
@@ -165,9 +162,19 @@ export const validateSheetPreview = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const validateGoogleSheetSettings = [
|
export const validateWorksheet = [
|
||||||
body('id').isString().optional({ nullable: false }),
|
body('id').exists().isString(),
|
||||||
body('worksheet').isString().optional({ nullable: false }),
|
body('worksheet').exists().isString(),
|
||||||
|
(req, res, next) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const validateSheetOptions = [
|
||||||
|
body('id').exists().isString(),
|
||||||
|
// body('options').exists().isObject(), TODO:
|
||||||
(req, res, next) => {
|
(req, res, next) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
|||||||
@@ -20,10 +20,6 @@ export const dbModel: DatabaseModel = {
|
|||||||
timeFormat: '24',
|
timeFormat: '24',
|
||||||
language: 'en',
|
language: 'en',
|
||||||
},
|
},
|
||||||
googleSheet: {
|
|
||||||
worksheet: '',
|
|
||||||
id: '',
|
|
||||||
},
|
|
||||||
viewSettings: {
|
viewSettings: {
|
||||||
overrideStyles: false,
|
overrideStyles: false,
|
||||||
normalColor: '#ffffffcc',
|
normalColor: '#ffffffcc',
|
||||||
|
|||||||
@@ -21,26 +21,27 @@ import {
|
|||||||
postViewSettings,
|
postViewSettings,
|
||||||
previewExcel,
|
previewExcel,
|
||||||
postHTTP,
|
postHTTP,
|
||||||
sheetAuthUrl,
|
getAuthenticationUrl,
|
||||||
uploadGoogleSheetClientFile,
|
uploadSheetClientFile,
|
||||||
previewSheet,
|
pullSheet,
|
||||||
pushSheet,
|
pushSheet,
|
||||||
getGoogleSheetSettings,
|
postId,
|
||||||
postGoogleSheetSettings,
|
getAuthentication,
|
||||||
getGoogleSheetState,
|
getClientSecrect,
|
||||||
} from '../controllers/ontimeController.js';
|
} from '../controllers/ontimeController.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
validateAliases,
|
validateAliases,
|
||||||
validateGoogleSheetSettings,
|
|
||||||
validateOSC,
|
validateOSC,
|
||||||
validatePatchProjectFile,
|
validatePatchProjectFile,
|
||||||
validateSettings,
|
validateSettings,
|
||||||
validateSheetPreview,
|
|
||||||
validateUserFields,
|
validateUserFields,
|
||||||
viewValidator,
|
viewValidator,
|
||||||
validateHTTP,
|
validateHTTP,
|
||||||
validateOscSubscription,
|
validateOscSubscription,
|
||||||
|
validateSheetid,
|
||||||
|
validateWorksheet,
|
||||||
|
validateSheetOptions,
|
||||||
} from '../controllers/ontimeController.validate.js';
|
} from '../controllers/ontimeController.validate.js';
|
||||||
import { projectSanitiser } from '../controllers/projectController.validate.js';
|
import { projectSanitiser } from '../controllers/projectController.validate.js';
|
||||||
|
|
||||||
@@ -106,23 +107,22 @@ router.post('/http', validateHTTP, postHTTP);
|
|||||||
// create route between controller and '/ontime/new' endpoint
|
// create route between controller and '/ontime/new' endpoint
|
||||||
router.post('/new', projectSanitiser, postNew);
|
router.post('/new', projectSanitiser, postNew);
|
||||||
|
|
||||||
// create route between controller and '/ontime/sheet-client' endpoint
|
//SETP-1
|
||||||
router.post('/sheet-clientsecrect', uploadFile, uploadGoogleSheetClientFile);
|
router.post('/sheet/clientsecrect', uploadFile, uploadSheetClientFile);
|
||||||
|
router.get('/sheet/clientsecrect', uploadFile, getClientSecrect);
|
||||||
|
|
||||||
// create route between controller and '/ontime/sheet-authstatus' endpoint
|
//SETP-2
|
||||||
router.get('/sheet-authurl', sheetAuthUrl);
|
router.get('/sheet/authentication/url', getAuthenticationUrl);
|
||||||
|
router.get('/sheet/authentication', getAuthentication);
|
||||||
|
|
||||||
// create route between controller and '/ontime/preview-sheet' endpoint
|
//STEP-3
|
||||||
router.post('/sheet-preview', validateSheetPreview, previewSheet);
|
router.post('/sheet/id', validateSheetid, postId);
|
||||||
|
|
||||||
// create route between controller and '/ontime/preview-sheet' endpoint
|
//STEP-4
|
||||||
router.post('/sheet-push', pushSheet);
|
router.post('/sheet/worksheet', validateWorksheet, postId);
|
||||||
|
|
||||||
// create route between controller and '/ontime/sheet-settings' endpoint
|
//STEP-5 download and generate preview
|
||||||
router.get('/sheet-settings', getGoogleSheetSettings);
|
router.post('/sheet/pull', validateSheetOptions, pullSheet);
|
||||||
|
|
||||||
// create route between controller and '/ontime/sheet-settings' endpoint
|
//STEP-5 upload
|
||||||
router.post('/sheet-settings', validateGoogleSheetSettings, postGoogleSheetSettings);
|
router.post('/sheet-push', validateSheetOptions, pushSheet);
|
||||||
|
|
||||||
// create route between controller and '/ontime/sheet-state' endpoint
|
|
||||||
router.get('/sheet-state', getGoogleSheetState);
|
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { millisToString } from 'ontime-utils';
|
import { millisToString } from 'ontime-utils';
|
||||||
import { getA1Notation, cellRequenstFromEvent, cellRequenstFromProjectData } from '../googleSheetUtils.js';
|
import { getA1Notation, cellRequenstFromEvent, cellRequenstFromProjectData } from '../sheetUtils.js';
|
||||||
import { EndAction, OntimeRundownEntry, ProjectData, SupportedEvent, TimerType } from 'ontime-types';
|
import { EndAction, OntimeRundownEntry, ProjectData, SupportedEvent, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
describe('getA1Notation()', () => {
|
describe('getA1Notation()', () => {
|
||||||
@@ -34,9 +34,9 @@ import {
|
|||||||
parseSettings,
|
parseSettings,
|
||||||
parseUserFields,
|
parseUserFields,
|
||||||
parseViewSettings,
|
parseViewSettings,
|
||||||
parseGoogleSheet,
|
|
||||||
} from './parserFunctions.js';
|
} from './parserFunctions.js';
|
||||||
import { parseExcelDate } from './time.js';
|
import { parseExcelDate } from './time.js';
|
||||||
|
import { coerceBoolean } from './coerceType.js';
|
||||||
|
|
||||||
export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||||
export const JSON_MIME = 'application/json';
|
export const JSON_MIME = 'application/json';
|
||||||
@@ -277,9 +277,9 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
|||||||
} else if (j === subtitleIndex) {
|
} else if (j === subtitleIndex) {
|
||||||
event.subtitle = makeString(column, '');
|
event.subtitle = makeString(column, '');
|
||||||
} else if (j === isPublicIndex) {
|
} else if (j === isPublicIndex) {
|
||||||
event.isPublic = Boolean(column);
|
event.isPublic = column == 'x' ? true : coerceBoolean(column);
|
||||||
} else if (j === skipIndex) {
|
} else if (j === skipIndex) {
|
||||||
event.skip = Boolean(column);
|
event.skip = column == 'x' ? true : coerceBoolean(column);
|
||||||
} else if (j === notesIndex) {
|
} else if (j === notesIndex) {
|
||||||
event.note = makeString(column, '');
|
event.note = makeString(column, '');
|
||||||
} else if (j === endActionIndex) {
|
} else if (j === endActionIndex) {
|
||||||
@@ -370,8 +370,6 @@ export const parseJson = async (jsonData): Promise<DatabaseModel | null> => {
|
|||||||
returnData.osc = parseOsc(jsonData) ?? dbModel.osc;
|
returnData.osc = parseOsc(jsonData) ?? dbModel.osc;
|
||||||
// Import HTTP settings if any
|
// Import HTTP settings if any
|
||||||
returnData.http = parseHttp(jsonData) ?? dbModel.http;
|
returnData.http = parseHttp(jsonData) ?? dbModel.http;
|
||||||
// Import GoogleSheet settings if any
|
|
||||||
returnData.googleSheet = parseGoogleSheet(jsonData, true);
|
|
||||||
|
|
||||||
return returnData as DatabaseModel;
|
return returnData as DatabaseModel;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { generateId } from 'ontime-utils';
|
import { generateId } from 'ontime-utils';
|
||||||
import {
|
import {
|
||||||
Alias,
|
Alias,
|
||||||
GoogleSheet,
|
|
||||||
OntimeRundown,
|
OntimeRundown,
|
||||||
HttpSettings,
|
HttpSettings,
|
||||||
OSCSettings,
|
OSCSettings,
|
||||||
@@ -333,24 +332,3 @@ export const parseUserFields = (data): UserFields => {
|
|||||||
}
|
}
|
||||||
return { ...newUserFields };
|
return { ...newUserFields };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse Google Sheet portion of an entry
|
|
||||||
* @param {object} data - data object
|
|
||||||
* @param {boolean} enforce - whether to create a definition if one is missing
|
|
||||||
* @returns {object} - event object data
|
|
||||||
*/
|
|
||||||
export const parseGoogleSheet = (data, enforce) => {
|
|
||||||
const newSheet: GoogleSheet = {
|
|
||||||
id: '',
|
|
||||||
worksheet: '',
|
|
||||||
};
|
|
||||||
if ('googleSheet' in data) {
|
|
||||||
console.log('Found Google Sheet definition, importing...');
|
|
||||||
newSheet.id ??= data.googleSheet?.id;
|
|
||||||
newSheet.worksheet ??= data.googleSheet?.worksheet;
|
|
||||||
return newSheet;
|
|
||||||
} else if (enforce) {
|
|
||||||
return newSheet;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
+255
-264
@@ -1,296 +1,120 @@
|
|||||||
import { sheets, sheets_v4 } from '@googleapis/sheets';
|
import { sheets, sheets_v4 } from '@googleapis/sheets';
|
||||||
import { readFile, writeFile } from 'fs/promises';
|
import { writeFile } from 'fs/promises';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
import { OAuth2Client } from 'google-auth-library';
|
import { OAuth2Client } from 'google-auth-library';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import { DatabaseModel, GoogleSheetState, LogOrigin } from 'ontime-types';
|
import { DatabaseModel, LogOrigin } from 'ontime-types';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import { logger } from '../classes/Logger.js';
|
import { logger } from '../classes/Logger.js';
|
||||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||||
import { getAppDataPath } from '../setup.js';
|
import { getAppDataPath } from '../setup.js';
|
||||||
import { ensureDirectory } from './fileManagement.js';
|
import { ensureDirectory } from './fileManagement.js';
|
||||||
import { cellRequenstFromEvent, cellRequenstFromProjectData, getA1Notation } from './googleSheetUtils.js';
|
import { cellRequenstFromEvent, cellRequenstFromProjectData, getA1Notation } from './sheetUtils.js';
|
||||||
import { parseExcel } from './parser.js';
|
import { parseExcel } from './parser.js';
|
||||||
import { parseProject, parseRundown, parseUserFields } from './parserFunctions.js';
|
import { parseProject, parseRundown, parseUserFields } from './parserFunctions.js';
|
||||||
|
import { ExcelImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
type ResponseOK = {
|
type ResponseOK = {
|
||||||
data: Partial<DatabaseModel>;
|
data: Partial<DatabaseModel>;
|
||||||
};
|
};
|
||||||
|
|
||||||
class sheet {
|
class Sheet {
|
||||||
private static client: null | OAuth2Client = null;
|
private static client: null | OAuth2Client = null;
|
||||||
private readonly scope = 'https://www.googleapis.com/auth/spreadsheets';
|
private readonly scope = 'https://www.googleapis.com/auth/spreadsheets';
|
||||||
private readonly sheetsFolder;
|
private readonly sheetsFolder: string;
|
||||||
private readonly client_secret;
|
private readonly clientSecretFile: string;
|
||||||
|
private static clientSecret = null;
|
||||||
private static authUrl: null | string = null;
|
private static authUrl: null | string = null;
|
||||||
private worksheetId = 0;
|
private authServerTimeout;
|
||||||
private sheetId = '';
|
|
||||||
private range = '';
|
private readonly requiredClientKeys = [
|
||||||
|
'client_id',
|
||||||
|
'project_id',
|
||||||
|
'auth_uri',
|
||||||
|
'token_uri',
|
||||||
|
'auth_provider_x509_cert_url',
|
||||||
|
'client_secret',
|
||||||
|
'redirect_uris',
|
||||||
|
];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const appDataPath = getAppDataPath();
|
const appDataPath = getAppDataPath();
|
||||||
if (appDataPath === '') {
|
if (appDataPath === '') {
|
||||||
throw new Error('Could not resolve public folder for platform');
|
throw new Error('Sheet: Could not resolve sheet folser');
|
||||||
}
|
}
|
||||||
this.sheetsFolder = join(appDataPath, 'sheets');
|
this.sheetsFolder = join(appDataPath, 'sheets');
|
||||||
this.client_secret = join(this.sheetsFolder, 'client_secret.json');
|
this.clientSecretFile = join(this.sheetsFolder, 'client_secret.json');
|
||||||
ensureDirectory(this.sheetsFolder);
|
ensureDirectory(this.sheetsFolder);
|
||||||
}
|
|
||||||
|
|
||||||
public async getSheetState(): Promise<GoogleSheetState> {
|
|
||||||
const ret: GoogleSheetState = {
|
|
||||||
auth: false,
|
|
||||||
id: false,
|
|
||||||
worksheet: false,
|
|
||||||
};
|
|
||||||
this.sheetId = '';
|
|
||||||
this.worksheetId = 0;
|
|
||||||
if (!sheet.client) {
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
ret.auth = await this.refreshToken();
|
const secrets = JSON.parse(readFileSync(this.clientSecretFile, 'utf-8'));
|
||||||
if (ret.auth) {
|
const isKeyMissing = this.requiredClientKeys.some((key) => !(key in secrets['installed']));
|
||||||
const settings = DataProvider.getGoogleSheet();
|
if (!isKeyMissing) {
|
||||||
const x = await this.exist(settings.id, settings.worksheet);
|
Sheet.clientSecret = secrets;
|
||||||
if (x === true) {
|
|
||||||
ret.id = true;
|
|
||||||
this.sheetId = settings.id;
|
|
||||||
} else if (x !== false) {
|
|
||||||
ret.id = true;
|
|
||||||
ret.worksheet = true;
|
|
||||||
this.sheetId = settings.id;
|
|
||||||
this.worksheetId = x.worksheetId;
|
|
||||||
this.range = x.range;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (_) {
|
||||||
logger.error(LogOrigin.Server, `Google Sheet: Faild to refresh token ${err}`);
|
/* empty - it is ok thet there is no clientSecret */
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* test existence of sheet and worksheet
|
|
||||||
* @param {string} sheetId - https://docs.google.com/spreadsheets/d/[[spreadsheetId]]/edit#gid=0
|
|
||||||
* @param {string} worksheet - the name of the worksheet containing ontime data
|
|
||||||
* @returns {Promise<false | {worksheetId: number, range: string}>} - false if not found | true if sheetId existes | id of worksheet and rage of worksheet
|
|
||||||
* @throws
|
|
||||||
*/
|
|
||||||
private async exist(
|
|
||||||
sheetId: string,
|
|
||||||
worksheet: string,
|
|
||||||
): Promise<false | true | { worksheetId: number; range: string }> {
|
|
||||||
const spreadsheets = await sheets({ version: 'v4', auth: sheet.client }).spreadsheets.get({
|
|
||||||
spreadsheetId: sheetId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (spreadsheets.status === 200) {
|
|
||||||
const ourWorksheetData = spreadsheets.data.sheets.find((n) => n.properties.title == worksheet);
|
|
||||||
if (ourWorksheetData !== undefined) {
|
|
||||||
const endCell = getA1Notation(
|
|
||||||
ourWorksheetData.properties.gridProperties.rowCount,
|
|
||||||
ourWorksheetData.properties.gridProperties.columnCount,
|
|
||||||
);
|
|
||||||
return { worksheetId: ourWorksheetData.properties.sheetId, range: `${worksheet}!A1:${endCell}` };
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* push sheet
|
|
||||||
* @throws
|
|
||||||
*/
|
|
||||||
public async push() {
|
|
||||||
const { auth, id, worksheet } = await this.getSheetState();
|
|
||||||
if (!auth && !id && !worksheet) {
|
|
||||||
throw new Error(`Sheet not authorized or incorrect ID or worksheet`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rq = await sheets({ version: 'v4', auth: sheet.client }).spreadsheets.values.get({
|
|
||||||
spreadsheetId: this.sheetId,
|
|
||||||
valueRenderOption: 'FORMATTED_VALUE',
|
|
||||||
majorDimension: 'ROWS',
|
|
||||||
range: this.range,
|
|
||||||
});
|
|
||||||
if (rq.status === 200) {
|
|
||||||
const { rundownMetadata, projectMetadata } = parseExcel(rq.data.values);
|
|
||||||
const rundown = DataProvider.getRundown();
|
|
||||||
const projectData = DataProvider.getProjectData();
|
|
||||||
const titleRow = Object.values(rundownMetadata)[0]['row'];
|
|
||||||
|
|
||||||
const updateRundown = Array<sheets_v4.Schema$Request>();
|
|
||||||
|
|
||||||
// we can't delete the last unflozzen row so we create an empty one
|
|
||||||
updateRundown.push({
|
|
||||||
insertDimension: {
|
|
||||||
inheritFromBefore: false,
|
|
||||||
range: {
|
|
||||||
dimension: 'ROWS',
|
|
||||||
startIndex: titleRow + 1,
|
|
||||||
endIndex: titleRow + 2,
|
|
||||||
sheetId: this.worksheetId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
//and delete the rest
|
|
||||||
updateRundown.push({
|
|
||||||
deleteDimension: { range: { dimension: 'ROWS', startIndex: titleRow + 2, sheetId: this.worksheetId } },
|
|
||||||
});
|
|
||||||
// insert the lenght of the rundown
|
|
||||||
updateRundown.push({
|
|
||||||
insertDimension: {
|
|
||||||
inheritFromBefore: false,
|
|
||||||
range: {
|
|
||||||
dimension: 'ROWS',
|
|
||||||
startIndex: titleRow + 1,
|
|
||||||
endIndex: titleRow + rundown.length,
|
|
||||||
sheetId: this.worksheetId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
//update the corresponding row with event data
|
|
||||||
rundown.forEach((entry, index) =>
|
|
||||||
updateRundown.push(cellRequenstFromEvent(entry, index, this.worksheetId, rundownMetadata)),
|
|
||||||
);
|
|
||||||
|
|
||||||
//update project data
|
|
||||||
updateRundown.push(cellRequenstFromProjectData(projectData, this.worksheetId, projectMetadata));
|
|
||||||
|
|
||||||
const writeResponds = await sheets({ version: 'v4', auth: sheet.client }).spreadsheets.batchUpdate({
|
|
||||||
spreadsheetId: this.sheetId,
|
|
||||||
requestBody: {
|
|
||||||
includeSpreadsheetInResponse: false,
|
|
||||||
responseRanges: [this.range],
|
|
||||||
requests: updateRundown,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (writeResponds.status == 200) {
|
|
||||||
logger.info(LogOrigin.Server, `Sheet write: ${writeResponds.statusText}`);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Sheet write faild: ${writeResponds.statusText}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(`Sheet read faild: ${rq.statusText}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* pull sheet
|
* @description SETP 1 - saves secrets object to appdata path as client_secret.json
|
||||||
* @returns {Promise<Partial<ResponseOK>>}
|
|
||||||
* @throws
|
|
||||||
*/
|
|
||||||
public async pull(): Promise<Partial<ResponseOK>> {
|
|
||||||
const { auth, id, worksheet } = await this.getSheetState();
|
|
||||||
if (!auth && !id && !worksheet) {
|
|
||||||
throw new Error(`Sheet not authorized or incorrect ID or worksheet`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res: Partial<ResponseOK> = {};
|
|
||||||
|
|
||||||
const rq = await sheets({ version: 'v4', auth: sheet.client }).spreadsheets.values.get({
|
|
||||||
spreadsheetId: this.sheetId,
|
|
||||||
valueRenderOption: 'FORMATTED_VALUE',
|
|
||||||
majorDimension: 'ROWS',
|
|
||||||
range: this.range,
|
|
||||||
});
|
|
||||||
if (rq.status === 200) {
|
|
||||||
res.data = {};
|
|
||||||
const dataFromSheet = parseExcel(rq.data.values);
|
|
||||||
res.data.rundown = parseRundown(dataFromSheet);
|
|
||||||
if (res.data.rundown.length < 1) {
|
|
||||||
throw new Error(`Could not find data to import in the worksheet`);
|
|
||||||
}
|
|
||||||
res.data.project = parseProject(dataFromSheet);
|
|
||||||
res.data.userFields = parseUserFields(dataFromSheet);
|
|
||||||
return res;
|
|
||||||
} else {
|
|
||||||
throw new Error(`Sheet read faild: ${rq.statusText}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* saves secrets object to appdata path as client_secret.json
|
|
||||||
* @param {object} secrets
|
* @param {object} secrets
|
||||||
* @throws
|
* @throws
|
||||||
*/
|
*/
|
||||||
public async saveClientSecrets(secrets: object) {
|
public async saveClientSecrets(secrets: object) {
|
||||||
sheet.client = null;
|
Sheet.client = null;
|
||||||
sheet.authUrl = null;
|
Sheet.authUrl = null;
|
||||||
if (
|
Sheet.clientSecret = null;
|
||||||
!('client_id' in secrets['installed']) ||
|
|
||||||
!('project_id' in secrets['installed']) ||
|
const isKeyMissing = this.requiredClientKeys.some((key) => !(key in secrets['installed']));
|
||||||
!('auth_uri' in secrets['installed']) ||
|
if (isKeyMissing) {
|
||||||
!('token_uri' in secrets['installed']) ||
|
throw new Error('Client file is missing some keys');
|
||||||
!('auth_provider_x509_cert_url' in secrets['installed']) ||
|
|
||||||
!('client_secret' in secrets['installed']) ||
|
|
||||||
!('redirect_uris' in secrets['installed'])
|
|
||||||
) {
|
|
||||||
throw new Error('Sheet slient secret is missing some keys');
|
|
||||||
}
|
}
|
||||||
await writeFile(this.client_secret, JSON.stringify(secrets), 'utf-8').catch((err) =>
|
|
||||||
logger.error(LogOrigin.Server, `${err}`),
|
await writeFile(this.clientSecretFile, JSON.stringify(secrets), 'utf-8').catch((err) => {
|
||||||
);
|
throw new Error(`Unable to save client file to disk ${err}`);
|
||||||
|
});
|
||||||
|
Sheet.clientSecret = secrets;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* refresh the client token
|
* @description SETP 1 - test that the saved object is pressent
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
*/
|
||||||
async refreshToken(): Promise<boolean> {
|
testClientSecrect() {
|
||||||
if (!sheet.client?.credentials?.refresh_token) return false;
|
return Sheet.clientSecret !== null;
|
||||||
try {
|
|
||||||
const response = await sheet.client.refreshAccessToken();
|
|
||||||
if (response?.credentials) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
logger.info(LogOrigin.Server, 'Sheets token expired');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private authServerTimeout;
|
|
||||||
/**
|
/**
|
||||||
* create local Auth Server
|
* @description SETP 2 - create server to interact with th OAuth2 request
|
||||||
* @returns {Promise<string | false>} - returns url to serve on success
|
* @returns {Promise<string | null>} - returns url path serve on success
|
||||||
* @throws
|
* @throws
|
||||||
*/
|
*/
|
||||||
public async openAuthServer(): Promise<string | false> {
|
async openAuthServer(): Promise<string | null> {
|
||||||
//TODO: this only works on local networks
|
//TODO: this only works on local networks
|
||||||
if (sheet.authUrl) {
|
|
||||||
|
// if the server is allready running retun it
|
||||||
|
if (Sheet.authUrl) {
|
||||||
clearTimeout(this.authServerTimeout);
|
clearTimeout(this.authServerTimeout);
|
||||||
this.authServerTimeout = setTimeout(
|
this.authServerTimeout = setTimeout(
|
||||||
() => {
|
() => {
|
||||||
sheet.authUrl = null;
|
Sheet.authUrl = null;
|
||||||
server.unref;
|
server.unref;
|
||||||
},
|
},
|
||||||
2 * 60 * 1000,
|
2 * 60 * 1000,
|
||||||
);
|
);
|
||||||
return sheet.authUrl;
|
return Sheet.authUrl;
|
||||||
}
|
|
||||||
const creadFile = await readFile(this.client_secret, 'utf-8').catch((err) =>
|
|
||||||
logger.error(LogOrigin.Server, `${err}`),
|
|
||||||
);
|
|
||||||
if (!creadFile) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const keyFile = JSON.parse(creadFile);
|
|
||||||
const keys = keyFile.installed || keyFile.web;
|
|
||||||
if (!keys.redirect_uris || keys.redirect_uris.length === 0) {
|
|
||||||
logger.error(LogOrigin.Server, `${invalidRedirectUri}`);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// create an oAuth client to authorize the API call
|
// Check that Secret is valid
|
||||||
|
const keyFile = Sheet.clientSecret;
|
||||||
|
const keys = keyFile.installed || keyFile.web;
|
||||||
|
if (!keys.redirect_uris || keys.redirect_uris.length === 0) {
|
||||||
|
throw new Error('Sheet: Missing redirect URI');
|
||||||
|
}
|
||||||
const redirectUri = new URL(keys.redirect_uris[0]);
|
const redirectUri = new URL(keys.redirect_uris[0]);
|
||||||
if (redirectUri.hostname !== 'localhost') {
|
if (redirectUri.hostname !== 'localhost') {
|
||||||
throw new Error(invalidRedirectUri);
|
throw new Error('Sheet: Invalid redirect URI');
|
||||||
}
|
}
|
||||||
|
|
||||||
// create an oAuth client to authorize the API call
|
// create an oAuth client to authorize the API call
|
||||||
@@ -299,6 +123,7 @@ class sheet {
|
|||||||
clientSecret: keys.client_secret,
|
clientSecret: keys.client_secret,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// start the server that will recive the codes
|
||||||
const server = http.createServer(async (req, res) => {
|
const server = http.createServer(async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const serverUrl = new URL(req.url, 'http://localhost:3000');
|
const serverUrl = new URL(req.url, 'http://localhost:3000');
|
||||||
@@ -323,7 +148,7 @@ class sheet {
|
|||||||
redirect_uri: redirectUri.toString(),
|
redirect_uri: redirectUri.toString(),
|
||||||
});
|
});
|
||||||
client.credentials = tokens;
|
client.credentials = tokens;
|
||||||
sheet.client = client;
|
Sheet.client = client;
|
||||||
res.end('Authentication successful! Please close this tab and return to OnTime.');
|
res.end('Authentication successful! Please close this tab and return to OnTime.');
|
||||||
logger.info(LogOrigin.Server, `Sheet: Authentication successful`);
|
logger.info(LogOrigin.Server, `Sheet: Authentication successful`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -351,41 +176,207 @@ class sheet {
|
|||||||
access_type: 'offline',
|
access_type: 'offline',
|
||||||
scope: this.scope,
|
scope: this.scope,
|
||||||
});
|
});
|
||||||
sheet.authUrl = authorizeUrl;
|
Sheet.authUrl = authorizeUrl;
|
||||||
this.authServerTimeout = setTimeout(
|
this.authServerTimeout = setTimeout(
|
||||||
() => {
|
() => {
|
||||||
sheet.authUrl = null;
|
Sheet.authUrl = null;
|
||||||
server.unref();
|
server.unref();
|
||||||
},
|
},
|
||||||
2 * 60 * 1000,
|
2 * 60 * 1000,
|
||||||
);
|
);
|
||||||
return authorizeUrl;
|
return authorizeUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description SETP 2 - test that the reciveed OAuth2 is still valid
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
async testAuthentication() {
|
||||||
|
if (Sheet.client) {
|
||||||
|
const ref = await Sheet.client.refreshAccessToken();
|
||||||
|
if (ref.credentials.expiry_date > 10000) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error('Unable to use accese token');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Unable to authenticate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description SETP 3 - test the given sheet id
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
async testSheetId(id: string) {
|
||||||
|
const spreadsheets = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.get({
|
||||||
|
spreadsheetId: id,
|
||||||
|
includeGridData: false,
|
||||||
|
});
|
||||||
|
if (spreadsheets.status != 200) {
|
||||||
|
throw new Error(spreadsheets.statusText);
|
||||||
|
}
|
||||||
|
return { worksheetOptions: spreadsheets.data.sheets.map((i) => i.properties.title) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description SETP 4 - test the given worksheet
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
async testWorksheet(id: string, worksheet: string) {
|
||||||
|
const spreadsheets = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.get({
|
||||||
|
spreadsheetId: id,
|
||||||
|
includeGridData: false,
|
||||||
|
});
|
||||||
|
if (spreadsheets.status != 200) {
|
||||||
|
throw new Error(spreadsheets.statusText);
|
||||||
|
}
|
||||||
|
const worksheetExist = spreadsheets.data.sheets.find((i) => i.properties.title === worksheet);
|
||||||
|
if (!worksheetExist) {
|
||||||
|
throw new Error('Unable to find worksheet');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* test existence of sheet and worksheet
|
||||||
|
* @param {string} sheetId - https://docs.google.com/spreadsheets/d/[[spreadsheetId]]/edit#gid=0
|
||||||
|
* @param {string} worksheet - the name of the worksheet containing ontime data
|
||||||
|
* @returns {Promise<{worksheetId: number, range: string}>} - id of worksheet and rage of worksheet
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
private async exist(sheetId: string, worksheet: string): Promise<{ worksheetId: number; range: string }> {
|
||||||
|
const spreadsheets = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.get({
|
||||||
|
spreadsheetId: sheetId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (spreadsheets.status === 200) {
|
||||||
|
const ourWorksheetData = spreadsheets.data.sheets.find((n) => n.properties.title == worksheet);
|
||||||
|
if (ourWorksheetData !== undefined) {
|
||||||
|
const endCell = getA1Notation(
|
||||||
|
ourWorksheetData.properties.gridProperties.rowCount,
|
||||||
|
ourWorksheetData.properties.gridProperties.columnCount,
|
||||||
|
);
|
||||||
|
return { worksheetId: ourWorksheetData.properties.sheetId, range: `${worksheet}!A1:${endCell}` };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Uable to open spreadsheets');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description SETP 5 - Upload the rundown to sheet
|
||||||
|
* @param {string} id - id of the sheet https://docs.google.com/spreadsheets/d/[[spreadsheetId]]/edit#gid=0
|
||||||
|
* @param {ExcelImportMap} options
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public async push(id: string, options: ExcelImportMap) {
|
||||||
|
const { worksheetId, range } = await this.exist(id, options.worksheet);
|
||||||
|
|
||||||
|
const rq = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.values.get({
|
||||||
|
spreadsheetId: id,
|
||||||
|
valueRenderOption: 'FORMATTED_VALUE',
|
||||||
|
majorDimension: 'ROWS',
|
||||||
|
range: range,
|
||||||
|
});
|
||||||
|
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'];
|
||||||
|
|
||||||
|
const updateRundown = Array<sheets_v4.Schema$Request>();
|
||||||
|
|
||||||
|
// we can't delete the last unflozzen row so we create an empty one
|
||||||
|
updateRundown.push({
|
||||||
|
insertDimension: {
|
||||||
|
inheritFromBefore: false,
|
||||||
|
range: {
|
||||||
|
dimension: 'ROWS',
|
||||||
|
startIndex: titleRow + 1,
|
||||||
|
endIndex: titleRow + 2,
|
||||||
|
sheetId: worksheetId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
//and delete the rest
|
||||||
|
updateRundown.push({
|
||||||
|
deleteDimension: { range: { dimension: 'ROWS', startIndex: titleRow + 2, sheetId: worksheetId } },
|
||||||
|
});
|
||||||
|
// insert the lenght of the rundown
|
||||||
|
updateRundown.push({
|
||||||
|
insertDimension: {
|
||||||
|
inheritFromBefore: false,
|
||||||
|
range: {
|
||||||
|
dimension: 'ROWS',
|
||||||
|
startIndex: titleRow + 1,
|
||||||
|
endIndex: titleRow + rundown.length,
|
||||||
|
sheetId: worksheetId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
//update the corresponding row with event data
|
||||||
|
rundown.forEach((entry, index) =>
|
||||||
|
updateRundown.push(cellRequenstFromEvent(entry, index, worksheetId, rundownMetadata)),
|
||||||
|
);
|
||||||
|
|
||||||
|
//update project data
|
||||||
|
updateRundown.push(cellRequenstFromProjectData(projectData, worksheetId, projectMetadata));
|
||||||
|
|
||||||
|
const writeResponds = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.batchUpdate({
|
||||||
|
spreadsheetId: id,
|
||||||
|
requestBody: {
|
||||||
|
includeSpreadsheetInResponse: false,
|
||||||
|
responseRanges: [range],
|
||||||
|
requests: updateRundown,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (writeResponds.status == 200) {
|
||||||
|
logger.info(LogOrigin.Server, `Sheet: write: ${writeResponds.statusText}`);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Sheet: write faild: ${writeResponds.statusText}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Sheet: read faild: ${rq.statusText}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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>>}
|
||||||
|
* @throws
|
||||||
|
*/
|
||||||
|
public async pull(id: string, options: ExcelImportMap): Promise<Partial<ResponseOK>> {
|
||||||
|
const { range } = await this.exist(id, options.worksheet);
|
||||||
|
|
||||||
|
const res: Partial<ResponseOK> = {};
|
||||||
|
|
||||||
|
const rq = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.values.get({
|
||||||
|
spreadsheetId: id,
|
||||||
|
valueRenderOption: 'FORMATTED_VALUE',
|
||||||
|
majorDimension: 'ROWS',
|
||||||
|
range,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rq.status === 200) {
|
||||||
|
res.data = {};
|
||||||
|
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 faild: ${rq.statusText}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copyright 2020 Google LLC
|
export const sheet = new Sheet();
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
//TODO: add modification notifications as requrirde by the license
|
|
||||||
|
|
||||||
const invalidRedirectUri = `The provided keyfile does not define a valid
|
|
||||||
redirect URI. There must be at least one redirect URI defined, and this sample
|
|
||||||
assumes it redirects to 'http://localhost:3000/oauth2callback'. Please edit
|
|
||||||
your keyfile, and add a 'redirect_uris' section. For example:
|
|
||||||
|
|
||||||
"redirect_uris": [
|
|
||||||
"http://localhost:3000/oauth2callback"
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Sheet = new sheet();
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { OSCSettings } from './core/OscSettings.type.js';
|
|||||||
import { Settings } from './core/Settings.type.js';
|
import { Settings } from './core/Settings.type.js';
|
||||||
import { UserFields } from './core/UserFields.type.js';
|
import { UserFields } from './core/UserFields.type.js';
|
||||||
import { ViewSettings } from './core/Views.type.js';
|
import { ViewSettings } from './core/Views.type.js';
|
||||||
import { GoogleSheet, HttpSettings } from '../index.js';
|
import { HttpSettings } from '../index.js';
|
||||||
|
|
||||||
export type DatabaseModel = {
|
export type DatabaseModel = {
|
||||||
rundown: OntimeRundown;
|
rundown: OntimeRundown;
|
||||||
@@ -14,7 +14,6 @@ export type DatabaseModel = {
|
|||||||
viewSettings: ViewSettings;
|
viewSettings: ViewSettings;
|
||||||
aliases: Alias[];
|
aliases: Alias[];
|
||||||
userFields: UserFields;
|
userFields: UserFields;
|
||||||
googleSheet: GoogleSheet;
|
|
||||||
osc: OSCSettings;
|
osc: OSCSettings;
|
||||||
http: HttpSettings;
|
http: HttpSettings;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
export type GoogleSheet = {
|
|
||||||
worksheet: string;
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GoogleSheetState = {
|
|
||||||
auth: boolean;
|
|
||||||
id: boolean;
|
|
||||||
worksheet: boolean;
|
|
||||||
};
|
|
||||||
@@ -37,9 +37,6 @@ export type { OSCSettings, OscSubscription, OscSubscriptionOptions } from './def
|
|||||||
// ---> HTTP
|
// ---> HTTP
|
||||||
export type { HttpSettings, HttpSubscription, HttpSubscriptionOptions } from './definitions/core/HttpSettings.type.js';
|
export type { HttpSettings, HttpSubscription, HttpSubscriptionOptions } from './definitions/core/HttpSettings.type.js';
|
||||||
|
|
||||||
// ---> Google Sheet
|
|
||||||
export type { GoogleSheet, GoogleSheetState } from './definitions/core/GoogleSheet.type.js';
|
|
||||||
|
|
||||||
// SERVER RESPONSES
|
// SERVER RESPONSES
|
||||||
export type { NetworkInterface, GetInfo } from './api/ontime-controller/BackendResponse.type.js';
|
export type { NetworkInterface, GetInfo } from './api/ontime-controller/BackendResponse.type.js';
|
||||||
export type { GetRundownCached } from './api/rundown-controller/BackendResponse.type.js';
|
export type { GetRundownCached } from './api/rundown-controller/BackendResponse.type.js';
|
||||||
|
|||||||
Reference in New Issue
Block a user