mirror of
https://github.com/cpvalente/ontime.git
synced 2026-07-26 10:38:55 +00:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b37494109a | |||
| aaa1fb9368 | |||
| cb198bc3fe | |||
| 6880f459b6 | |||
| 7b34c8f3b1 | |||
| c714727801 | |||
| 8fc70fa162 | |||
| 54c4a7f68c | |||
| 9820c71a2d | |||
| 2ff7d0f546 | |||
| f29ed1b05a | |||
| b1de7ffe5b | |||
| 46d17e0570 | |||
| 38e3ef7586 | |||
| e8bcbb4291 | |||
| 111df94d6d | |||
| d32d8a547d | |||
| 5bc2c2f241 | |||
| 28533464d4 | |||
| 4f1fa2053f | |||
| b15b4b48a7 | |||
| ef2fa99673 | |||
| 85ee9de528 | |||
| e52a75db7d | |||
| 07ebb5a321 | |||
| 071cae4cc6 | |||
| 543e365f82 | |||
| a7c9ce876c | |||
| ae4e2ceafe | |||
| b77db4cbf6 | |||
| fe67e65a95 | |||
| 785695c01c | |||
| 65efebe180 | |||
| 52a9c85dbc | |||
| 6b2b6dc6dd | |||
| 31b46bbb34 | |||
| 38cf3a7b63 | |||
| 81cf30daa0 | |||
| e9683c6cab | |||
| 370777326d | |||
| 309f622077 | |||
| 285b05bd65 | |||
| 05207189cf | |||
| d70bb2174a |
@@ -3,6 +3,8 @@ import {
|
||||
Alias,
|
||||
DatabaseModel,
|
||||
GetInfo,
|
||||
GoogleSheet,
|
||||
GoogleSheetState,
|
||||
OntimeRundown,
|
||||
OSCSettings,
|
||||
OscSubscription,
|
||||
@@ -227,3 +229,62 @@ export async function getLatestVersion(): Promise<HasUpdate> {
|
||||
export async function postNew(initialData: Partial<ProjectData>) {
|
||||
return axios.post(`${ontimeURL}/new`, initialData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description sheet Client File
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const uploadSheetClientFile = async (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('userFile', file);
|
||||
await axios
|
||||
.post(`${ontimeURL}/sheet-clientsecrect`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
.then((response) => response.data.id);
|
||||
};
|
||||
|
||||
export const getSheetsAuthUrl = async () => {
|
||||
const res = await axios.get(`${ontimeURL}/sheet-authurl`);
|
||||
return res.data;
|
||||
};
|
||||
|
||||
export const postPreviewSheet = async () => {
|
||||
const response = await axios.post(`${ontimeURL}/sheet-preview`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
export const postPushSheet = async () => {
|
||||
const response = await axios.post(`${ontimeURL}/sheet-push`);
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @description HTTP request to retrieve google sheets settings
|
||||
* @return {Promise}
|
||||
*/
|
||||
export async function getSheetSettings(): Promise<GoogleSheet> {
|
||||
const res = await axios.get(`${ontimeURL}/sheet-settings`);
|
||||
return res.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;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary'
|
||||
import MenuBar from '../menu/MenuBar';
|
||||
import AboutModal from '../modals/about-modal/AboutModal';
|
||||
import QuickStart from '../modals/quick-start/QuickStart';
|
||||
import SheetsModal from '../modals/sheets-modal/SheetsModal';
|
||||
import UploadModal from '../modals/upload-modal/UploadModal';
|
||||
|
||||
import styles from './Editor.module.scss';
|
||||
@@ -28,6 +29,7 @@ export default function Editor() {
|
||||
} = useDisclosure();
|
||||
const { isOpen: isAboutModalOpen, onOpen: onAboutModalOpen, onClose: onAboutModalClose } = useDisclosure();
|
||||
const { isOpen: isQuickStartOpen, onOpen: onQuickStartOpen, onClose: onQuickStartClose } = useDisclosure();
|
||||
const { isOpen: isSheetsOpen, onOpen: onSheetsOpen, onClose: onSheetsClose } = useDisclosure();
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
@@ -42,6 +44,7 @@ export default function Editor() {
|
||||
<IntegrationModal onClose={onIntegrationModalClose} isOpen={isIntegrationModalOpen} />
|
||||
<AboutModal onClose={onAboutModalClose} isOpen={isAboutModalOpen} />
|
||||
<SettingsModal isOpen={isSettingsOpen} onClose={onSettingsClose} />
|
||||
<SheetsModal onClose={onSheetsClose} isOpen={isSheetsOpen} />
|
||||
</ErrorBoundary>
|
||||
<div className={styles.mainContainer} data-testid='event-editor'>
|
||||
<div id='settings' className={styles.settings}>
|
||||
@@ -58,6 +61,8 @@ export default function Editor() {
|
||||
onAboutOpen={onAboutModalOpen}
|
||||
isQuickStartOpen={isQuickStartOpen}
|
||||
onQuickStartOpen={onQuickStartOpen}
|
||||
isSheetsOpen={isSheetsOpen}
|
||||
onSheetsOpen={onSheetsOpen}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { VStack } from '@chakra-ui/react';
|
||||
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';
|
||||
@@ -31,6 +32,8 @@ interface MenuBarProps {
|
||||
onAboutOpen: () => void;
|
||||
isQuickStartOpen: boolean;
|
||||
onQuickStartOpen: () => void;
|
||||
isSheetsOpen: boolean;
|
||||
onSheetsOpen: () => void;
|
||||
}
|
||||
|
||||
const buttonStyle = {
|
||||
@@ -58,6 +61,8 @@ const MenuBar = (props: MenuBarProps) => {
|
||||
onAboutOpen,
|
||||
isQuickStartOpen,
|
||||
onQuickStartOpen,
|
||||
isSheetsOpen,
|
||||
onSheetsOpen,
|
||||
} = props;
|
||||
const { isElectron, sendToElectron } = useElectronEvent();
|
||||
|
||||
@@ -174,6 +179,16 @@ const MenuBar = (props: MenuBarProps) => {
|
||||
/>
|
||||
|
||||
<div className={style.gap} />
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
isDisabled={appMode === AppMode.Run}
|
||||
icon={<IoCalendarOutline />}
|
||||
className={isSheetsOpen ? style.open : ''}
|
||||
clickHandler={onSheetsOpen}
|
||||
tooltip='Sheets'
|
||||
aria-label='Sheets'
|
||||
size='sm'
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
isDisabled={appMode === AppMode.Run}
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
import { ChangeEvent, useEffect,useRef, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
} 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 { GoogleSheetState, OntimeRundown, ProjectData, UserFields } from 'ontime-types';
|
||||
|
||||
import { PROJECT_DATA, RUNDOWN, USERFIELDS } from '../../../common/api/apiConstants';
|
||||
import { maybeAxiosError } from '../../../common/api/apiUtils';
|
||||
import {
|
||||
getSheetsAuthUrl,
|
||||
getSheetSettings,
|
||||
getSheetstate,
|
||||
patchData,
|
||||
postPreviewSheet,
|
||||
postPushSheet,
|
||||
postSheetSettings,
|
||||
uploadSheetClientFile,
|
||||
} from '../../../common/api/ontimeApi';
|
||||
import { projectDataPlaceholder } from '../../../common/models/ProjectData';
|
||||
import { userFieldsPlaceholder } from '../../../common/models/UserFields';
|
||||
import PreviewExcel from '../upload-modal/preview/PreviewExcel';
|
||||
|
||||
interface SheetsModalProps {
|
||||
onClose: () => void;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function SheetsModal(props: SheetsModalProps) {
|
||||
const { isOpen, onClose } = props;
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [rundown, setRundown] = useState<OntimeRundown | null>(null);
|
||||
const [userFields, setUserFields] = useState<UserFields | null>(null);
|
||||
const [project, setProject] = useState<ProjectData | null>(null);
|
||||
|
||||
const [sheetState, setSheetState] = useState<GoogleSheetState>({ auth: false, id: false, worksheet: false });
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const sheetid = useRef<HTMLInputElement>(null);
|
||||
const worksheet = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleClose = () => {
|
||||
setRundown(null);
|
||||
setProject(null);
|
||||
setUserFields(null);
|
||||
onClose();
|
||||
};
|
||||
const handleClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleFile = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFile = event?.target?.files?.[0];
|
||||
if (!selectedFile) {
|
||||
return;
|
||||
} else {
|
||||
await uploadSheetClientFile(selectedFile);
|
||||
_onChange();
|
||||
}
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
postPreviewSheet().then((data) => {
|
||||
setProject(data.project);
|
||||
setRundown(data.rundown);
|
||||
setUserFields(data.userFields);
|
||||
});
|
||||
};
|
||||
|
||||
const handlePushData = () => {
|
||||
postPushSheet().then((data) => {
|
||||
console.log(data);
|
||||
});
|
||||
};
|
||||
|
||||
const handleFinalise = async () => {
|
||||
// this step is currently only used for excel files, after preview
|
||||
if (rundown && userFields && project) {
|
||||
let doClose = false;
|
||||
try {
|
||||
await patchData({ rundown, userFields, project });
|
||||
queryClient.setQueryData(RUNDOWN, rundown);
|
||||
queryClient.setQueryData(USERFIELDS, userFields);
|
||||
queryClient.setQueryData(PROJECT_DATA, project);
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [...RUNDOWN, ...USERFIELDS, ...PROJECT_DATA],
|
||||
});
|
||||
doClose = true;
|
||||
} catch (error) {
|
||||
const message = maybeAxiosError(error);
|
||||
console.log(message);
|
||||
// setErrors(`Failed applying changes ${message}`);
|
||||
} finally {
|
||||
if (doClose) {
|
||||
handleClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
isOpen={isOpen}
|
||||
closeOnOverlayClick={false}
|
||||
motionPreset='slideInBottom'
|
||||
size='xl'
|
||||
scrollBehavior='inside'
|
||||
preserveScrollBarGap
|
||||
variant='ontime-upload'
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Sheets!</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
{rundown && (
|
||||
<>
|
||||
<PreviewExcel
|
||||
rundown={rundown ?? []}
|
||||
project={project ?? projectDataPlaceholder}
|
||||
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>
|
||||
<ModalFooter>
|
||||
{!rundown && (
|
||||
<div>
|
||||
<Button
|
||||
variant='ontime-subtle-on-light'
|
||||
padding='0 2em'
|
||||
onClick={handlePullData}
|
||||
rightIcon={<IoArrowDownCircleOutline />}
|
||||
>
|
||||
Pull data
|
||||
</Button>
|
||||
<Button
|
||||
variant='ontime-subtle-on-light'
|
||||
padding='0 2em'
|
||||
onClick={handlePushData}
|
||||
rightIcon={<IoArrowUpCircleOutline />}
|
||||
>
|
||||
Push data
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Button variant='ontime-ghost-on-light'>Reset</Button>
|
||||
{!rundown && (
|
||||
<Button variant='ontime-filled' padding='0 2em' onClick={handelSave}>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
{rundown && (
|
||||
<Button variant='ontime-filled' padding='0 2em' onClick={handleFinalise}>
|
||||
Import
|
||||
</Button>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
"version": "2.16.2",
|
||||
"exports": "./src/index.js",
|
||||
"dependencies": {
|
||||
"@googleapis/sheets": "^5.0.5",
|
||||
"body-parser": "^1.20.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.1",
|
||||
@@ -12,6 +13,7 @@
|
||||
"express-session": "^1.17.3",
|
||||
"express-static-gzip": "^2.1.7",
|
||||
"express-validator": "^6.14.2",
|
||||
"google-auth-library": "^9.2.0",
|
||||
"lowdb": "^5.0.5",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-osc": "^9.0.2",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
UserFields,
|
||||
Alias,
|
||||
Settings,
|
||||
GoogleSheet,
|
||||
} from 'ontime-types';
|
||||
|
||||
import { data, db } from '../../modules/loadDb.js';
|
||||
@@ -58,6 +59,15 @@ export class DataProvider {
|
||||
await this.persist();
|
||||
}
|
||||
|
||||
static getGoogleSheet() {
|
||||
return data.googleSheet;
|
||||
}
|
||||
|
||||
static async setGoogleSheet(newData: GoogleSheet) {
|
||||
data.googleSheet = { ...newData };
|
||||
await this.persist();
|
||||
}
|
||||
|
||||
static getOsc() {
|
||||
return data.osc;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@ import { DatabaseModel } from 'ontime-types';
|
||||
* @param {object} newData
|
||||
*/
|
||||
export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseModel>) {
|
||||
const { rundown, project, settings, viewSettings, osc, aliases, userFields } = newData || {};
|
||||
const { rundown, project, settings, googleSheet, viewSettings, osc, aliases, userFields } = newData || {};
|
||||
return {
|
||||
...existing,
|
||||
rundown: rundown ?? existing.rundown,
|
||||
project: { ...existing.project, ...project },
|
||||
settings: { ...existing.settings, ...settings },
|
||||
googleSheet: { ...existing.googleSheet, ...googleSheet },
|
||||
viewSettings: { ...existing.viewSettings, ...viewSettings },
|
||||
aliases: aliases ?? existing.aliases,
|
||||
userFields: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Alias, DatabaseModel, OntimeRundown, Settings } from 'ontime-types';
|
||||
import { Alias, DatabaseModel, GoogleSheet, OntimeRundown, Settings } from 'ontime-types';
|
||||
import { safeMerge } from '../DataProvider.utils.js';
|
||||
|
||||
describe('safeMerge', () => {
|
||||
@@ -21,6 +21,10 @@ describe('safeMerge', () => {
|
||||
timeFormat: '24',
|
||||
language: 'en',
|
||||
},
|
||||
googleSheet: {
|
||||
worksheet: '1',
|
||||
id: '2',
|
||||
},
|
||||
viewSettings: {
|
||||
overrideStyles: false,
|
||||
endMessage: 'existing endMessage',
|
||||
@@ -98,6 +102,20 @@ describe('safeMerge', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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', () => {
|
||||
const newData = {
|
||||
osc: {
|
||||
|
||||
@@ -17,6 +17,8 @@ import { deepmerge } from 'ontime-utils';
|
||||
import { runtimeCacheStore } from '../stores/cachingStore.js';
|
||||
import { delayedRundownCacheKey } from '../services/rundown-service/delayedRundown.utils.js';
|
||||
|
||||
import { Sheet } from '../utils/sheetsAuth.js';
|
||||
|
||||
// Create controller for GET request to '/ontime/poll'
|
||||
// Returns data for current state
|
||||
export const poll = async (req, res) => {
|
||||
@@ -414,3 +416,101 @@ export const postNew: RequestHandler = async (req, res) => {
|
||||
res.status(400).send({ message: error.toString() });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* downloads and parses an sheet
|
||||
* @returns parsed result
|
||||
*/
|
||||
export async function previewSheet(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) {
|
||||
res.status(400).send({ message: 'File not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const client = JSON.parse(fs.readFileSync(req.file.path as string, 'utf-8'));
|
||||
await Sheet.saveClientSecrets(client);
|
||||
res.status(200).send('OK');
|
||||
} catch (error) {
|
||||
res.status(500).send({ message: error.toString() });
|
||||
}
|
||||
fs.unlink(req.file.path, (err) => {
|
||||
if (err) logger.error(LogOrigin.Server, err.message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns link to sheet auth url
|
||||
*/
|
||||
export async function sheetAuthUrl(req, res) {
|
||||
const successful = await Sheet.openAuthServer();
|
||||
if (successful === false) {
|
||||
res.status(500).send('bad');
|
||||
} else {
|
||||
res.status(200).send(successful);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Get google sheet Settings
|
||||
* @method GET
|
||||
*/
|
||||
export const getGoogleSheetSettings = async (req, res) => {
|
||||
const sheet = 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 {
|
||||
const newData = {
|
||||
id: req.body.id,
|
||||
worksheet: req.body.worksheet,
|
||||
};
|
||||
await DataProvider.setGoogleSheet(newData);
|
||||
res.status(200).send(newData);
|
||||
} catch (error) {
|
||||
res.status(400).send({ message: error.toString() });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Get google sheet state
|
||||
* @method GET
|
||||
*/
|
||||
export const getGoogleSheetState = async (req, res) => {
|
||||
res.status(200).send(await Sheet.getSheetState());
|
||||
};
|
||||
|
||||
@@ -133,3 +133,25 @@ export const validatePatchProjectFile = [
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
//TODO: is thise correct
|
||||
export const validateSheetPreview = [
|
||||
body('sheetid').isString().optional({ nullable: false }),
|
||||
body('worksheet').isString().optional({ nullable: false }),
|
||||
body('options').isObject().optional({ nullable: true }),
|
||||
(req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
export const validateGoogleSheetSettings = [
|
||||
body('id').isString().optional({ nullable: false }),
|
||||
body('worksheet').isString().optional({ nullable: false }),
|
||||
(req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
@@ -20,6 +20,10 @@ export const dbModel: DatabaseModel = {
|
||||
timeFormat: '24',
|
||||
language: 'en',
|
||||
},
|
||||
googleSheet: {
|
||||
worksheet: '',
|
||||
id: '',
|
||||
},
|
||||
viewSettings: {
|
||||
overrideStyles: false,
|
||||
normalColor: '#ffffffcc',
|
||||
|
||||
@@ -19,14 +19,23 @@ import {
|
||||
postUserFields,
|
||||
postViewSettings,
|
||||
previewExcel,
|
||||
sheetAuthUrl,
|
||||
uploadGoogleSheetClientFile,
|
||||
previewSheet,
|
||||
pushSheet,
|
||||
getGoogleSheetSettings,
|
||||
postGoogleSheetSettings,
|
||||
getGoogleSheetState,
|
||||
} from '../controllers/ontimeController.js';
|
||||
|
||||
import {
|
||||
validateAliases,
|
||||
validateGoogleSheetSettings,
|
||||
validateOSC,
|
||||
validateOscSubscription,
|
||||
validatePatchProjectFile,
|
||||
validateSettings,
|
||||
validateSheetPreview,
|
||||
validateUserFields,
|
||||
viewValidator,
|
||||
} from '../controllers/ontimeController.validate.js';
|
||||
@@ -87,3 +96,24 @@ router.post('/osc-subscriptions', validateOscSubscription, postOscSubscriptions)
|
||||
|
||||
// create route between controller and '/ontime/new' endpoint
|
||||
router.post('/new', projectSanitiser, postNew);
|
||||
|
||||
// create route between controller and '/ontime/sheet-client' endpoint
|
||||
router.post('/sheet-clientsecrect', uploadFile, uploadGoogleSheetClientFile);
|
||||
|
||||
// create route between controller and '/ontime/sheet-authstatus' endpoint
|
||||
router.get('/sheet-authurl', sheetAuthUrl);
|
||||
|
||||
// create route between controller and '/ontime/preview-sheet' endpoint
|
||||
router.post('/sheet-preview', validateSheetPreview, previewSheet);
|
||||
|
||||
// create route between controller and '/ontime/preview-sheet' endpoint
|
||||
router.post('/sheet-push', pushSheet);
|
||||
|
||||
// create route between controller and '/ontime/sheet-settings' endpoint
|
||||
router.get('/sheet-settings', getGoogleSheetSettings);
|
||||
|
||||
// create route between controller and '/ontime/sheet-settings' endpoint
|
||||
router.post('/sheet-settings', validateGoogleSheetSettings, postGoogleSheetSettings);
|
||||
|
||||
// create route between controller and '/ontime/sheet-state' endpoint
|
||||
router.get('/sheet-state', getGoogleSheetState);
|
||||
@@ -0,0 +1,433 @@
|
||||
import { millisToString } from 'ontime-utils';
|
||||
import { getA1Notation, cellRequenstFromEvent, cellRequenstFromProjectData } from '../googleSheetUtils.js';
|
||||
import { EndAction, OntimeRundownEntry, ProjectData, SupportedEvent, TimerType } from 'ontime-types';
|
||||
|
||||
describe('getA1Notation()', () => {
|
||||
test('A1', () => {
|
||||
expect(getA1Notation(0, 0)).toStrictEqual('A1');
|
||||
});
|
||||
test('E3', () => {
|
||||
expect(getA1Notation(2, 4)).toStrictEqual('E3');
|
||||
});
|
||||
test('AA100', () => {
|
||||
expect(getA1Notation(99, 26)).toStrictEqual('AA100');
|
||||
});
|
||||
test('can not be negative', () => {
|
||||
expect(() => getA1Notation(-1, 1)).toThrowError('Index can not be less than 0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cellRequenstFromEvent()', () => {
|
||||
test('string to string', () => {
|
||||
const event: OntimeRundownEntry = {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
isPublic: false,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
revision: 0,
|
||||
id: '1358',
|
||||
};
|
||||
const metadata = {
|
||||
type: { row: 1, col: 14 },
|
||||
cue: { row: 1, col: 15 },
|
||||
title: { row: 1, col: 16 },
|
||||
subtitle: { row: 1, col: 17 },
|
||||
presenter: { row: 1, col: 18 },
|
||||
note: { row: 1, col: 19 },
|
||||
timeStart: { row: 1, col: 20 },
|
||||
timeEnd: { row: 1, col: 21 },
|
||||
endAction: { row: 1, col: 22 },
|
||||
timerType: { row: 1, col: 23 },
|
||||
duration: { row: 1, col: 24 },
|
||||
isPublic: { row: 1, col: 25 },
|
||||
skip: { row: 1, col: 26 },
|
||||
colour: { row: 1, col: 27 },
|
||||
user0: { row: 1, col: 28 },
|
||||
user1: { row: 1, col: 29 },
|
||||
user2: { row: 1, col: 30 },
|
||||
user3: { row: 1, col: 31 },
|
||||
user4: { row: 1, col: 32 },
|
||||
user5: { row: 1, col: 33 },
|
||||
user6: { row: 1, col: 34 },
|
||||
user7: { row: 1, col: 35 },
|
||||
user8: { row: 1, col: 36 },
|
||||
user9: { row: 1, col: 37 },
|
||||
revision: { row: 1, col: 38 },
|
||||
id: { row: 1, col: 39 },
|
||||
};
|
||||
const result = cellRequenstFromEvent(event, 1, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[5].userEnteredValue.stringValue).toStrictEqual(event.note);
|
||||
});
|
||||
|
||||
test('numer to timer', () => {
|
||||
const event: OntimeRundownEntry = {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
isPublic: false,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
user0: '',
|
||||
user1: '',
|
||||
user2: '',
|
||||
user3: '',
|
||||
user4: '',
|
||||
user5: '',
|
||||
user6: '',
|
||||
user7: '',
|
||||
user8: '',
|
||||
user9: '',
|
||||
revision: 0,
|
||||
id: '1358',
|
||||
};
|
||||
const metadata = {
|
||||
type: { row: 1, col: 14 },
|
||||
cue: { row: 1, col: 15 },
|
||||
title: { row: 1, col: 16 },
|
||||
subtitle: { row: 1, col: 17 },
|
||||
presenter: { row: 1, col: 18 },
|
||||
note: { row: 1, col: 19 },
|
||||
timeStart: { row: 1, col: 20 },
|
||||
timeEnd: { row: 1, col: 21 },
|
||||
endAction: { row: 1, col: 22 },
|
||||
timerType: { row: 1, col: 23 },
|
||||
duration: { row: 1, col: 24 },
|
||||
isPublic: { row: 1, col: 25 },
|
||||
skip: { row: 1, col: 26 },
|
||||
colour: { row: 1, col: 27 },
|
||||
user0: { row: 1, col: 28 },
|
||||
user1: { row: 1, col: 29 },
|
||||
user2: { row: 1, col: 30 },
|
||||
user3: { row: 1, col: 31 },
|
||||
user4: { row: 1, col: 32 },
|
||||
user5: { row: 1, col: 33 },
|
||||
user6: { row: 1, col: 34 },
|
||||
user7: { row: 1, col: 35 },
|
||||
user8: { row: 1, col: 36 },
|
||||
user9: { row: 1, col: 37 },
|
||||
revision: { row: 1, col: 38 },
|
||||
id: { row: 1, col: 39 },
|
||||
};
|
||||
const result = cellRequenstFromEvent(event, 1, 1234, metadata).updateCells.rows[0].values[10].userEnteredValue
|
||||
.stringValue;
|
||||
expect(result).toStrictEqual(millisToString(event.duration));
|
||||
});
|
||||
|
||||
test('boolean to x', () => {
|
||||
const event: OntimeRundownEntry = {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
user0: 'u',
|
||||
user1: 'u',
|
||||
user2: 'u',
|
||||
user3: 'u',
|
||||
user4: 'u',
|
||||
user5: 'u',
|
||||
user6: 'u',
|
||||
user7: 'u',
|
||||
user8: 'u',
|
||||
user9: 'u',
|
||||
revision: 0,
|
||||
id: '1358',
|
||||
};
|
||||
const metadata = {
|
||||
type: { row: 1, col: 14 },
|
||||
cue: { row: 1, col: 15 },
|
||||
title: { row: 1, col: 16 },
|
||||
subtitle: { row: 1, col: 17 },
|
||||
presenter: { row: 1, col: 18 },
|
||||
note: { row: 1, col: 19 },
|
||||
timeStart: { row: 1, col: 20 },
|
||||
timeEnd: { row: 1, col: 21 },
|
||||
endAction: { row: 1, col: 22 },
|
||||
timerType: { row: 1, col: 23 },
|
||||
duration: { row: 1, col: 24 },
|
||||
isPublic: { row: 1, col: 25 },
|
||||
skip: { row: 1, col: 26 },
|
||||
colour: { row: 1, col: 27 },
|
||||
user0: { row: 1, col: 28 },
|
||||
user1: { row: 1, col: 29 },
|
||||
user2: { row: 1, col: 30 },
|
||||
user3: { row: 1, col: 31 },
|
||||
user4: { row: 1, col: 32 },
|
||||
user5: { row: 1, col: 33 },
|
||||
user6: { row: 1, col: 34 },
|
||||
user7: { row: 1, col: 35 },
|
||||
user8: { row: 1, col: 36 },
|
||||
user9: { row: 1, col: 37 },
|
||||
revision: { row: 1, col: 38 },
|
||||
id: { row: 1, col: 39 },
|
||||
};
|
||||
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('');
|
||||
});
|
||||
|
||||
test('spacing in metadata', () => {
|
||||
const event: OntimeRundownEntry = {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
user0: 'u',
|
||||
user1: 'u',
|
||||
user2: 'u',
|
||||
user3: 'u',
|
||||
user4: 'u',
|
||||
user5: 'u',
|
||||
user6: 'u',
|
||||
user7: 'u',
|
||||
user8: 'u',
|
||||
user9: 'u',
|
||||
revision: 0,
|
||||
id: '1358',
|
||||
};
|
||||
const metadata = {
|
||||
cue: { row: 1, col: 0 },
|
||||
title: { row: 1, col: 6 },
|
||||
subtitle: { row: 1, col: 10 },
|
||||
user0: { row: 1, col: 16 },
|
||||
};
|
||||
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);
|
||||
});
|
||||
|
||||
test('metadata offset from zero', () => {
|
||||
const event: OntimeRundownEntry = {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
user0: 'u',
|
||||
user1: 'u',
|
||||
user2: 'u',
|
||||
user3: 'u',
|
||||
user4: 'u',
|
||||
user5: 'u',
|
||||
user6: 'u',
|
||||
user7: 'u',
|
||||
user8: 'u',
|
||||
user9: 'u',
|
||||
revision: 0,
|
||||
id: '1358',
|
||||
};
|
||||
const metadata = {
|
||||
cue: { row: 1, col: 5 },
|
||||
title: { row: 1, col: 6 },
|
||||
subtitle: { row: 1, col: 10 },
|
||||
user0: { row: 1, col: 16 },
|
||||
};
|
||||
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);
|
||||
});
|
||||
|
||||
test('sheet setup', () => {
|
||||
const event: OntimeRundownEntry = {
|
||||
type: SupportedEvent.Event,
|
||||
cue: '1',
|
||||
title: 'Fancy',
|
||||
subtitle: 'Wow',
|
||||
presenter: 'Mr. Presenter',
|
||||
note: 'Blue button on the right',
|
||||
timeStart: 46800000,
|
||||
timeEnd: 57600000,
|
||||
endAction: EndAction.None,
|
||||
timerType: TimerType.CountDown,
|
||||
duration: 10800000,
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
colour: 'red',
|
||||
user0: 'u',
|
||||
user1: 'u',
|
||||
user2: 'u',
|
||||
user3: 'u',
|
||||
user4: 'u',
|
||||
user5: 'u',
|
||||
user6: 'u',
|
||||
user7: 'u',
|
||||
user8: 'u',
|
||||
user9: 'u',
|
||||
revision: 0,
|
||||
id: '1358',
|
||||
};
|
||||
const metadata = {
|
||||
cue: { row: 10, col: 5 },
|
||||
title: { row: 10, col: 6 },
|
||||
subtitle: { row: 1, col: 10 },
|
||||
user0: { row: 10, col: 16 },
|
||||
};
|
||||
const result1 = cellRequenstFromEvent(event, 1, 1234, metadata);
|
||||
expect(result1.updateCells.start.sheetId).toStrictEqual(1234);
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cellRequenstFromProjectData()', () => {
|
||||
test('string to string', () => {
|
||||
const projectData: ProjectData = {
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
publicUrl: 'Public Url',
|
||||
backstageUrl: 'Backstage Url',
|
||||
publicInfo: 'Public Info',
|
||||
backstageInfo: 'Backstage Info',
|
||||
};
|
||||
const metadata = {
|
||||
title: { row: 0, col: 1 },
|
||||
description: { row: 1, col: 1 },
|
||||
publicUrl: { row: 2, col: 1 },
|
||||
backstageUrl: { row: 3, col: 1 },
|
||||
publicInfo: { row: 4, col: 1 },
|
||||
backstageInfo: { row: 5, col: 1 },
|
||||
};
|
||||
const result = cellRequenstFromProjectData(projectData, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.title);
|
||||
expect(result.updateCells.rows[1].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.description);
|
||||
expect(result.updateCells.rows[2].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.publicUrl);
|
||||
expect(result.updateCells.rows[3].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.backstageUrl);
|
||||
expect(result.updateCells.rows[4].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.publicInfo);
|
||||
expect(result.updateCells.rows[5].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.backstageInfo);
|
||||
});
|
||||
|
||||
test('metadata offset from zero', () => {
|
||||
const projectData: ProjectData = {
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
publicUrl: 'Public Url',
|
||||
backstageUrl: 'Backstage Url',
|
||||
publicInfo: 'Public Info',
|
||||
backstageInfo: 'Backstage Info',
|
||||
};
|
||||
const metadata = {
|
||||
title: { row: 5, col: 10 },
|
||||
description: { row: 6, col: 10 },
|
||||
publicUrl: { row: 7, col: 10 },
|
||||
backstageUrl: { row: 9, col: 10 },
|
||||
publicInfo: { row: 10, col: 10 },
|
||||
backstageInfo: { row: 11, col: 10 },
|
||||
};
|
||||
const result = cellRequenstFromProjectData(projectData, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.title);
|
||||
expect(result.updateCells.rows[1].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.description);
|
||||
expect(result.updateCells.rows[2].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.publicUrl);
|
||||
expect(result.updateCells.rows[4].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.backstageUrl);
|
||||
expect(result.updateCells.rows[5].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.publicInfo);
|
||||
expect(result.updateCells.rows[6].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.backstageInfo);
|
||||
});
|
||||
|
||||
test('spacing in metadata', () => {
|
||||
const projectData: ProjectData = {
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
publicUrl: 'Public Url',
|
||||
backstageUrl: 'Backstage Url',
|
||||
publicInfo: 'Public Info',
|
||||
backstageInfo: 'Backstage Info',
|
||||
};
|
||||
const metadata = {
|
||||
title: { row: 0, col: 1 },
|
||||
description: { row: 1, col: 1 },
|
||||
publicUrl: { row: 2, col: 1 },
|
||||
backstageUrl: { row: 9, col: 1 },
|
||||
publicInfo: { row: 15, col: 1 },
|
||||
backstageInfo: { row: 50, col: 1 },
|
||||
};
|
||||
const result = cellRequenstFromProjectData(projectData, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.title);
|
||||
expect(result.updateCells.rows[1].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.description);
|
||||
expect(result.updateCells.rows[2].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.publicUrl);
|
||||
expect(result.updateCells.rows[9].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.backstageUrl);
|
||||
expect(result.updateCells.rows[15].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.publicInfo);
|
||||
expect(result.updateCells.rows[50].values[0].userEnteredValue.stringValue).toStrictEqual(projectData.backstageInfo);
|
||||
});
|
||||
|
||||
test('sheet setup', () => {
|
||||
const projectData: ProjectData = {
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
publicUrl: 'Public Url',
|
||||
backstageUrl: 'Backstage Url',
|
||||
publicInfo: 'Public Info',
|
||||
backstageInfo: 'Backstage Info',
|
||||
};
|
||||
const metadata = {
|
||||
title: { row: 0, col: 10 },
|
||||
description: { row: 1, col: 10 },
|
||||
publicUrl: { row: 2, col: 10 },
|
||||
backstageUrl: { row: 3, col: 10 },
|
||||
publicInfo: { row: 4, col: 10 },
|
||||
backstageInfo: { row: 5, col: 10 },
|
||||
};
|
||||
const result = cellRequenstFromProjectData(projectData, 1234, metadata);
|
||||
expect(result.updateCells.start.rowIndex).toStrictEqual(0);
|
||||
expect(result.updateCells.start.columnIndex).toStrictEqual(11);
|
||||
expect(result.updateCells.fields).toStrictEqual('userEnteredValue');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
import { sheets_v4 } from '@googleapis/sheets';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
import { OntimeRundownEntry, ProjectData, isOntimeEvent } from 'ontime-types';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} row - The row number of the cell reference. Row 1 is row number 0.
|
||||
* @param {number} column - The column number of the cell reference. A is column number 0.
|
||||
* @returns {string} - Returns a cell reference as a string using A1 Notation
|
||||
* @author https://www.labnol.org/convert-column-a1-notation-210601
|
||||
* @example
|
||||
*
|
||||
* getA1Notation(2, 4) returns "E3"
|
||||
* getA1Notation(99, 26) returns "AA100"
|
||||
*
|
||||
*/
|
||||
export function getA1Notation(row: number, column: number): string {
|
||||
if (row < 0 || column < 0) {
|
||||
throw new Error('Index can not be less than 0');
|
||||
}
|
||||
const a1Notation = [`${row + 1}`];
|
||||
const totalAlphabets = 'Z'.charCodeAt(0) - 'A'.charCodeAt(0) + 1;
|
||||
let block = column;
|
||||
while (block >= 0) {
|
||||
a1Notation.unshift(String.fromCharCode((block % totalAlphabets) + 'A'.charCodeAt(0)));
|
||||
block = Math.floor(block / totalAlphabets) - 1;
|
||||
}
|
||||
return a1Notation.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description - creates updateCells request from ontime event
|
||||
* @param {OntimeRundownEntry} event
|
||||
* @param {number} index - index of the event
|
||||
* @param {number} worksheetId
|
||||
* @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 cellRequenstFromEvent(
|
||||
event: OntimeRundownEntry,
|
||||
index: number,
|
||||
worksheetId: number,
|
||||
metadata,
|
||||
): sheets_v4.Schema$Request {
|
||||
const r: sheets_v4.Schema$CellData[] = [];
|
||||
const tmp = Object.entries(metadata)
|
||||
.filter(([_, value]) => value !== undefined)
|
||||
.sort(([_a, a], [_b, b]) => a['col'] - b['col']) as [string, { col: number; row: number }][];
|
||||
|
||||
const titleCol = tmp[0][1].col;
|
||||
|
||||
for (const [index, e] of tmp.entries()) {
|
||||
if (index != 0) {
|
||||
const prevCol = tmp[index - 1][1].col;
|
||||
const thisCol = e[1].col;
|
||||
const diff = thisCol - prevCol;
|
||||
if (diff > 1) {
|
||||
const fillArr = new Array<(typeof tmp)[0]>(1).fill(['blank', { row: e[1].row, col: prevCol + 1 }]);
|
||||
tmp.splice(index, 0, ...fillArr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tmp.forEach(([key, _]) => {
|
||||
if (isOntimeEvent(event)) {
|
||||
if (key === 'blank') {
|
||||
r.push({});
|
||||
} else if (key === 'colour') {
|
||||
r.push({
|
||||
userEnteredValue: { stringValue: event.colour },
|
||||
});
|
||||
} else if (typeof event[key] === 'number') {
|
||||
r.push({
|
||||
userEnteredValue: { stringValue: millisToString(event[key], true) },
|
||||
});
|
||||
} else if (typeof event[key] === 'string') {
|
||||
r.push({
|
||||
userEnteredValue: { stringValue: event[key] },
|
||||
});
|
||||
} else if (typeof event[key] === 'boolean') {
|
||||
r.push({
|
||||
userEnteredValue: { stringValue: event[key] ? 'x' : '' },
|
||||
});
|
||||
} else {
|
||||
r.push({});
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
updateCells: {
|
||||
start: {
|
||||
sheetId: worksheetId,
|
||||
rowIndex: index + tmp[0][1]['row'] + 1,
|
||||
columnIndex: titleCol,
|
||||
},
|
||||
fields: 'userEnteredValue',
|
||||
rows: [
|
||||
{
|
||||
values: r,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description - creates updateCells request from ontime event
|
||||
* @param {ProjectData} projectData
|
||||
* @param {number} worksheetId
|
||||
* @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 cellRequenstFromProjectData(
|
||||
projectData: ProjectData,
|
||||
worksheetId: number,
|
||||
metadata,
|
||||
): sheets_v4.Schema$Request {
|
||||
const r: sheets_v4.Schema$RowData[] = [];
|
||||
const tmp = Object.entries(metadata)
|
||||
.filter(([_, value]) => value !== undefined)
|
||||
.sort(([_a, a], [_b, b]) => a['col'] - b['col']) as [string, { col: number; row: number }][];
|
||||
|
||||
const minRow = Object.values(metadata).reduce(
|
||||
(accumulator: number, val) => Math.min(accumulator, val['row']),
|
||||
Number.MAX_VALUE,
|
||||
) as number;
|
||||
const minCol = tmp[0][1].col + 1;
|
||||
|
||||
for (const [index, e] of tmp.entries()) {
|
||||
if (index != 0) {
|
||||
const prevRow = tmp[index - 1][1].row;
|
||||
const thisRow = e[1].row;
|
||||
const diff = thisRow - prevRow;
|
||||
if (diff > 1) {
|
||||
const fillArr = new Array<(typeof tmp)[0]>(1).fill(['blank', { row: prevRow + 1, col: e[1].col }]);
|
||||
tmp.splice(index, 0, ...fillArr);
|
||||
}
|
||||
}
|
||||
}
|
||||
tmp.forEach(([key, _]) => {
|
||||
if (key == 'blank') {
|
||||
r.push({});
|
||||
} else {
|
||||
r.push({
|
||||
values: [
|
||||
{
|
||||
userEnteredValue: { stringValue: projectData[key] },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
updateCells: {
|
||||
start: {
|
||||
sheetId: worksheetId,
|
||||
rowIndex: minRow,
|
||||
columnIndex: minCol,
|
||||
},
|
||||
fields: 'userEnteredValue',
|
||||
rows: r,
|
||||
},
|
||||
};
|
||||
}
|
||||
+217
-122
@@ -33,6 +33,7 @@ import {
|
||||
parseSettings,
|
||||
parseUserFields,
|
||||
parseViewSettings,
|
||||
parseGoogleSheet,
|
||||
} from './parserFunctions.js';
|
||||
import { parseExcelDate } from './time.js';
|
||||
|
||||
@@ -46,6 +47,8 @@ export const JSON_MIME = 'application/json';
|
||||
* @returns {object} - parsed object
|
||||
*/
|
||||
export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImportMap>) => {
|
||||
const projectMetadata = {};
|
||||
const rundownMetadata = {};
|
||||
const importMap: ExcelImportMap = { ...defaultExcelImportMap, ...options };
|
||||
const projectData: Partial<ProjectData> = {
|
||||
title: '',
|
||||
@@ -102,140 +105,228 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
let user8Index: number | null = null;
|
||||
let user9Index: number | null = null;
|
||||
|
||||
excelData
|
||||
.filter((e) => e.length > 0)
|
||||
.forEach((row) => {
|
||||
// these fields contain the data to its right
|
||||
let projectTitleNext = false;
|
||||
let projectDescriptionNext = false;
|
||||
let publicUrlNext = false;
|
||||
let publicInfoNext = false;
|
||||
let backstageUrlNext = false;
|
||||
let backstageInfoNext = false;
|
||||
excelData.forEach((row, rowIndex) => {
|
||||
if (row.length === 0) {
|
||||
return;
|
||||
}
|
||||
// these fields contain the data to its right
|
||||
let projectTitleNext = false;
|
||||
let projectDescriptionNext = false;
|
||||
let publicUrlNext = false;
|
||||
let publicInfoNext = false;
|
||||
let backstageUrlNext = false;
|
||||
let backstageInfoNext = false;
|
||||
|
||||
const event: Partial<OntimeEvent> = {};
|
||||
const handlers = {
|
||||
[importMap.projectName]: () => (projectTitleNext = true),
|
||||
[importMap.projectDescription]: () => (projectDescriptionNext = true),
|
||||
[importMap.publicUrl]: () => (publicUrlNext = true),
|
||||
[importMap.publicInfo]: () => (publicInfoNext = true),
|
||||
[importMap.backstageUrl]: () => (backstageUrlNext = true),
|
||||
[importMap.backstageInfo]: () => (backstageInfoNext = true),
|
||||
const event: Partial<OntimeEvent> = {};
|
||||
const handlers = {
|
||||
[importMap.projectName]: (row: number, col: number) => {
|
||||
projectTitleNext = true;
|
||||
projectMetadata['title'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.projectDescription]: (row: number, col: number) => {
|
||||
projectDescriptionNext = true;
|
||||
projectMetadata['description'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.publicUrl]: (row: number, col: number) => {
|
||||
publicUrlNext = true;
|
||||
projectMetadata['publicUrl'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.publicInfo]: (row: number, col: number) => {
|
||||
publicInfoNext = true;
|
||||
projectMetadata['publicInfo'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.backstageUrl]: (row: number, col: number) => {
|
||||
backstageUrlNext = true;
|
||||
projectMetadata['backstageUrl'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.backstageInfo]: (row: number, col: number) => {
|
||||
backstageInfoNext = true;
|
||||
projectMetadata['backstageInfo'] = { row: row, col: col };
|
||||
},
|
||||
|
||||
[importMap.timeStart]: (index: number) => (timeStartIndex = index),
|
||||
[importMap.timeEnd]: (index: number) => (timeEndIndex = index),
|
||||
[importMap.duration]: (index: number) => (durationIndex = index),
|
||||
[importMap.timeStart]: (row: number, col: number) => {
|
||||
timeStartIndex = col;
|
||||
rundownMetadata['timeStart'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.timeEnd]: (row: number, col: number) => {
|
||||
timeEndIndex = col;
|
||||
rundownMetadata['timeEnd'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.duration]: (row: number, col: number) => {
|
||||
durationIndex = col;
|
||||
rundownMetadata['duration'] = { row: row, col: col };
|
||||
},
|
||||
|
||||
[importMap.cue]: (index: number) => (cueIndex = index),
|
||||
[importMap.title]: (index: number) => (titleIndex = index),
|
||||
[importMap.presenter]: (index: number) => (presenterIndex = index),
|
||||
[importMap.subtitle]: (index: number) => (subtitleIndex = index),
|
||||
[importMap.isPublic]: (index: number) => (isPublicIndex = index),
|
||||
[importMap.skip]: (index: number) => (skipIndex = index),
|
||||
[importMap.note]: (index: number) => (notesIndex = index),
|
||||
[importMap.colour]: (index: number) => (colourIndex = index),
|
||||
[importMap.cue]: (row: number, col: number) => {
|
||||
cueIndex = col;
|
||||
rundownMetadata['cue'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.title]: (row: number, col: number) => {
|
||||
titleIndex = col;
|
||||
rundownMetadata['title'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.presenter]: (row: number, col: number) => {
|
||||
presenterIndex = col;
|
||||
rundownMetadata['presenter'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.subtitle]: (row: number, col: number) => {
|
||||
subtitleIndex = col;
|
||||
rundownMetadata['subtitle'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.isPublic]: (row: number, col: number) => {
|
||||
isPublicIndex = col;
|
||||
rundownMetadata['isPublic'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.skip]: (row: number, col: number) => {
|
||||
skipIndex = col;
|
||||
rundownMetadata['skip'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.note]: (row: number, col: number) => {
|
||||
notesIndex = col;
|
||||
rundownMetadata['note'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.colour]: (row: number, col: number) => {
|
||||
colourIndex = col;
|
||||
rundownMetadata['colour'] = { row: row, col: col };
|
||||
},
|
||||
|
||||
[importMap.endAction]: (index: number) => (endActionIndex = index),
|
||||
[importMap.timerType]: (index: number) => (timerTypeIndex = index),
|
||||
[importMap.endAction]: (row: number, col: number) => {
|
||||
endActionIndex = col;
|
||||
rundownMetadata['endAction'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.timerType]: (row: number, col: number) => {
|
||||
timerTypeIndex = col;
|
||||
rundownMetadata['timerType'] = { row: row, col: col };
|
||||
},
|
||||
|
||||
[importMap.user0]: (index: number) => (user0Index = index),
|
||||
[importMap.user1]: (index: number) => (user1Index = index),
|
||||
[importMap.user2]: (index: number) => (user2Index = index),
|
||||
[importMap.user3]: (index: number) => (user3Index = index),
|
||||
[importMap.user4]: (index: number) => (user4Index = index),
|
||||
[importMap.user5]: (index: number) => (user5Index = index),
|
||||
[importMap.user6]: (index: number) => (user6Index = index),
|
||||
[importMap.user7]: (index: number) => (user7Index = index),
|
||||
[importMap.user8]: (index: number) => (user8Index = index),
|
||||
[importMap.user9]: (index: number) => (user9Index = index),
|
||||
} as const;
|
||||
[importMap.user0]: (row: number, col: number) => {
|
||||
user0Index = col;
|
||||
rundownMetadata['user0'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user1]: (row: number, col: number) => {
|
||||
user1Index = col;
|
||||
rundownMetadata['user1'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user2]: (row: number, col: number) => {
|
||||
user2Index = col;
|
||||
rundownMetadata['user2'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user3]: (row: number, col: number) => {
|
||||
user3Index = col;
|
||||
rundownMetadata['user3'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user4]: (row: number, col: number) => {
|
||||
user4Index = col;
|
||||
rundownMetadata['user4'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user5]: (row: number, col: number) => {
|
||||
user5Index = col;
|
||||
rundownMetadata['user5'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user6]: (row: number, col: number) => {
|
||||
user6Index = col;
|
||||
rundownMetadata['user6'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user7]: (row: number, col: number) => {
|
||||
user7Index = col;
|
||||
rundownMetadata['user7'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user8]: (row: number, col: number) => {
|
||||
user8Index = col;
|
||||
rundownMetadata['user8'] = { row: row, col: col };
|
||||
},
|
||||
[importMap.user9]: (row: number, col: number) => {
|
||||
user9Index = col;
|
||||
rundownMetadata['user9'] = { row: row, col: col };
|
||||
},
|
||||
} as const;
|
||||
|
||||
row.forEach((column, j) => {
|
||||
// 1. we check if we have set a flag for a known field
|
||||
if (projectTitleNext) {
|
||||
projectData.title = makeString(column, '');
|
||||
projectTitleNext = false;
|
||||
} else if (projectDescriptionNext) {
|
||||
projectData.description = makeString(column, '');
|
||||
projectDescriptionNext = false;
|
||||
} else if (publicUrlNext) {
|
||||
projectData.publicUrl = makeString(column, '');
|
||||
publicUrlNext = false;
|
||||
} else if (publicInfoNext) {
|
||||
projectData.publicInfo = makeString(column, '');
|
||||
publicInfoNext = false;
|
||||
} else if (backstageUrlNext) {
|
||||
projectData.backstageUrl = makeString(column, '');
|
||||
backstageUrlNext = false;
|
||||
} else if (backstageInfoNext) {
|
||||
projectData.backstageInfo = makeString(column, '');
|
||||
backstageInfoNext = false;
|
||||
} else if (j === timeStartIndex) {
|
||||
event.timeStart = parseExcelDate(column);
|
||||
} else if (j === timeEndIndex) {
|
||||
event.timeEnd = parseExcelDate(column);
|
||||
} else if (j === durationIndex) {
|
||||
event.duration = parseExcelDate(column);
|
||||
} else if (j === titleIndex) {
|
||||
event.title = makeString(column, '');
|
||||
} else if (j === cueIndex) {
|
||||
event.cue = makeString(column, '');
|
||||
} else if (j === presenterIndex) {
|
||||
event.presenter = makeString(column, '');
|
||||
} else if (j === subtitleIndex) {
|
||||
event.subtitle = makeString(column, '');
|
||||
} else if (j === isPublicIndex) {
|
||||
event.isPublic = Boolean(column);
|
||||
} else if (j === skipIndex) {
|
||||
event.skip = Boolean(column);
|
||||
} else if (j === notesIndex) {
|
||||
event.note = makeString(column, '');
|
||||
} else if (j === endActionIndex) {
|
||||
event.endAction = validateEndAction(column);
|
||||
} else if (j === timerTypeIndex) {
|
||||
event.timerType = validateTimerType(column);
|
||||
} else if (j === colourIndex) {
|
||||
event.colour = makeString(column, '');
|
||||
} else if (j === user0Index) {
|
||||
event.user0 = makeString(column, '');
|
||||
} else if (j === user1Index) {
|
||||
event.user1 = makeString(column, '');
|
||||
} else if (j === user2Index) {
|
||||
event.user2 = makeString(column, '');
|
||||
} else if (j === user3Index) {
|
||||
event.user3 = makeString(column, '');
|
||||
} else if (j === user4Index) {
|
||||
event.user4 = makeString(column, '');
|
||||
} else if (j === user5Index) {
|
||||
event.user5 = makeString(column, '');
|
||||
} else if (j === user6Index) {
|
||||
event.user6 = makeString(column, '');
|
||||
} else if (j === user7Index) {
|
||||
event.user7 = makeString(column, '');
|
||||
} else if (j === user8Index) {
|
||||
event.user8 = makeString(column, '');
|
||||
} else if (j === user9Index) {
|
||||
event.user9 = makeString(column, '');
|
||||
} else {
|
||||
// 2. if there is no flag, lets see if we know the field type
|
||||
if (typeof column === 'string') {
|
||||
const col = column.toLowerCase();
|
||||
row.forEach((column, j) => {
|
||||
// 1. we check if we have set a flag for a known field
|
||||
if (projectTitleNext) {
|
||||
projectData.title = makeString(column, '');
|
||||
projectTitleNext = false;
|
||||
} else if (projectDescriptionNext) {
|
||||
projectData.description = makeString(column, '');
|
||||
projectDescriptionNext = false;
|
||||
} else if (publicUrlNext) {
|
||||
projectData.publicUrl = makeString(column, '');
|
||||
publicUrlNext = false;
|
||||
} else if (publicInfoNext) {
|
||||
projectData.publicInfo = makeString(column, '');
|
||||
publicInfoNext = false;
|
||||
} else if (backstageUrlNext) {
|
||||
projectData.backstageUrl = makeString(column, '');
|
||||
backstageUrlNext = false;
|
||||
} else if (backstageInfoNext) {
|
||||
projectData.backstageInfo = makeString(column, '');
|
||||
backstageInfoNext = false;
|
||||
} else if (j === timeStartIndex) {
|
||||
event.timeStart = parseExcelDate(column);
|
||||
} else if (j === timeEndIndex) {
|
||||
event.timeEnd = parseExcelDate(column);
|
||||
} else if (j === durationIndex) {
|
||||
event.duration = parseExcelDate(column);
|
||||
} else if (j === titleIndex) {
|
||||
event.title = makeString(column, '');
|
||||
} else if (j === cueIndex) {
|
||||
event.cue = makeString(column, '');
|
||||
} else if (j === presenterIndex) {
|
||||
event.presenter = makeString(column, '');
|
||||
} else if (j === subtitleIndex) {
|
||||
event.subtitle = makeString(column, '');
|
||||
} else if (j === isPublicIndex) {
|
||||
event.isPublic = Boolean(column);
|
||||
} else if (j === skipIndex) {
|
||||
event.skip = Boolean(column);
|
||||
} else if (j === notesIndex) {
|
||||
event.note = makeString(column, '');
|
||||
} else if (j === endActionIndex) {
|
||||
event.endAction = validateEndAction(column);
|
||||
} else if (j === timerTypeIndex) {
|
||||
event.timerType = validateTimerType(column);
|
||||
} else if (j === colourIndex) {
|
||||
event.colour = makeString(column, '');
|
||||
} else if (j === user0Index) {
|
||||
event.user0 = makeString(column, '');
|
||||
} else if (j === user1Index) {
|
||||
event.user1 = makeString(column, '');
|
||||
} else if (j === user2Index) {
|
||||
event.user2 = makeString(column, '');
|
||||
} else if (j === user3Index) {
|
||||
event.user3 = makeString(column, '');
|
||||
} else if (j === user4Index) {
|
||||
event.user4 = makeString(column, '');
|
||||
} else if (j === user5Index) {
|
||||
event.user5 = makeString(column, '');
|
||||
} else if (j === user6Index) {
|
||||
event.user6 = makeString(column, '');
|
||||
} else if (j === user7Index) {
|
||||
event.user7 = makeString(column, '');
|
||||
} else if (j === user8Index) {
|
||||
event.user8 = makeString(column, '');
|
||||
} else if (j === user9Index) {
|
||||
event.user9 = makeString(column, '');
|
||||
} else {
|
||||
// 2. if there is no flag, lets see if we know the field type
|
||||
if (typeof column === 'string') {
|
||||
const col = column.toLowerCase();
|
||||
|
||||
if (handlers[col]) {
|
||||
handlers[col](j);
|
||||
}
|
||||
// else. we don't know how to handle this column
|
||||
// just ignore it
|
||||
if (handlers[col]) {
|
||||
handlers[col](rowIndex, j);
|
||||
}
|
||||
// else. we don't know how to handle this column
|
||||
// just ignore it
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(event).length > 0) {
|
||||
// if any data was found, push to array
|
||||
rundown.push({ ...event, type: SupportedEvent.Event } as OntimeEvent);
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(event).length > 0) {
|
||||
// if any data was found, push to array
|
||||
rundown.push({ ...event, type: SupportedEvent.Event } as OntimeEvent);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
rundown,
|
||||
project: projectData,
|
||||
@@ -244,6 +335,8 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
version: '2.0.0',
|
||||
},
|
||||
userFields: customUserFields,
|
||||
projectMetadata: projectMetadata,
|
||||
rundownMetadata: rundownMetadata,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -276,6 +369,8 @@ export const parseJson = async (jsonData): Promise<DatabaseModel | null> => {
|
||||
returnData.osc = parseOsc(jsonData) ?? dbModel.osc;
|
||||
// Import HTTP settings if any
|
||||
// returnData.http = parseHttp(jsonData, enforce);
|
||||
// Import GoogleSheet settings if any
|
||||
returnData.googleSheet = parseGoogleSheet(jsonData, true);
|
||||
|
||||
return returnData as DatabaseModel;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { generateId } from 'ontime-utils';
|
||||
import {
|
||||
Alias,
|
||||
GoogleSheet,
|
||||
OntimeRundown,
|
||||
OSCSettings,
|
||||
OscSubscription,
|
||||
@@ -284,3 +285,24 @@ export const parseUserFields = (data): UserFields => {
|
||||
}
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
import { OAuth2Client } from 'google-auth-library';
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import { sheets, sheets_v4 } from '@googleapis/sheets';
|
||||
import http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { logger } from '../classes/Logger.js';
|
||||
import { getAppDataPath } from '../setup.js';
|
||||
import { DatabaseModel, LogOrigin } from 'ontime-types';
|
||||
import { parseExcel } from './parser.js';
|
||||
import { parseProject, parseRundown, parseUserFields } from './parserFunctions.js';
|
||||
import { ensureDirectory } from './fileManagement.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { GoogleSheetState } from 'ontime-types';
|
||||
import { cellRequenstFromEvent, cellRequenstFromProjectData, getA1Notation } from './googleSheetUtils.js';
|
||||
|
||||
type ResponseOK = {
|
||||
data: Partial<DatabaseModel>;
|
||||
};
|
||||
|
||||
class sheet {
|
||||
private static client: null | OAuth2Client = null;
|
||||
private readonly scope = 'https://www.googleapis.com/auth/spreadsheets';
|
||||
private readonly sheetsFolder = `${getAppDataPath()}/sheets`;
|
||||
private readonly client_secret = `${this.sheetsFolder}/client_secret.json`;
|
||||
private static authUrl: null | string = null;
|
||||
private worksheetId: number = 0;
|
||||
private sheetId: string = '';
|
||||
private range: string = '';
|
||||
|
||||
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 {
|
||||
ret.auth = await this.refreshToken();
|
||||
if (ret.auth) {
|
||||
const settings = DataProvider.getGoogleSheet();
|
||||
const x = await this.exist(settings.id, settings.worksheet);
|
||||
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) {
|
||||
logger.error(LogOrigin.Server, `Google Sheet: Faild to refresh token ${err}`);
|
||||
}
|
||||
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 w = spreadsheets.data.sheets.find((p) => p.properties.title == worksheet);
|
||||
if (w !== undefined) {
|
||||
const endCell = getA1Notation(w.properties.gridProperties.rowCount, w.properties.gridProperties.columnCount);
|
||||
return { worksheetId: w.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
|
||||
* @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
|
||||
* @throws
|
||||
*/
|
||||
public async saveClientSecrets(secrets: object) {
|
||||
sheet.client = null;
|
||||
sheet.authUrl = null;
|
||||
ensureDirectory(this.sheetsFolder);
|
||||
if (
|
||||
'client_id' in secrets ||
|
||||
!('project_id' in secrets) ||
|
||||
!('auth_uri' in secrets) ||
|
||||
!('token_uri' in secrets) ||
|
||||
!('auth_provider_x509_cert_url' in secrets) ||
|
||||
!('client_secret' in secrets) ||
|
||||
!('redirect_uris' in secrets)
|
||||
) {
|
||||
throw new Error('Sheet slient secret is missing some keys');
|
||||
}
|
||||
await writeFile(this.client_secret, JSON.stringify(secrets), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* refresh the client token
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async refreshToken(): Promise<boolean> {
|
||||
if (!sheet.client?.credentials?.refresh_token) return false;
|
||||
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
|
||||
* @returns {Promise<string | false>} - returns url to serve on success
|
||||
* @throws
|
||||
*/
|
||||
public async openAuthServer(): Promise<string | false> {
|
||||
//TODO: this only works on local networks
|
||||
if (sheet.authUrl) {
|
||||
clearTimeout(this.authServerTimeout);
|
||||
this.authServerTimeout = setTimeout(
|
||||
() => {
|
||||
sheet.authUrl = null;
|
||||
server.unref;
|
||||
},
|
||||
2 * 60 * 1000,
|
||||
);
|
||||
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
|
||||
const redirectUri = new URL(keys.redirect_uris[0]);
|
||||
if (redirectUri.hostname !== 'localhost') {
|
||||
throw new Error(invalidRedirectUri);
|
||||
}
|
||||
|
||||
// create an oAuth client to authorize the API call
|
||||
const client = new OAuth2Client({
|
||||
clientId: keys.client_id,
|
||||
clientSecret: keys.client_secret,
|
||||
});
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const serverUrl = new URL(req.url, 'http://localhost:3000');
|
||||
if (serverUrl.pathname !== redirectUri.pathname) {
|
||||
res.end('Invalid callback URL');
|
||||
return;
|
||||
}
|
||||
const searchParams = serverUrl.searchParams;
|
||||
if (searchParams.has('error')) {
|
||||
res.end('Authorization rejected.');
|
||||
logger.info(LogOrigin.Server, `Sheet: ${searchParams.get('error')}`);
|
||||
return;
|
||||
}
|
||||
if (!searchParams.has('code')) {
|
||||
res.end('No authentication code provided.');
|
||||
logger.info(LogOrigin.Server, `Sheet: Cannot read authentication code`);
|
||||
return;
|
||||
}
|
||||
const code = searchParams.get('code');
|
||||
const { tokens } = await client.getToken({
|
||||
code: code,
|
||||
redirect_uri: redirectUri.toString(),
|
||||
});
|
||||
client.credentials = tokens;
|
||||
sheet.client = client;
|
||||
res.end('Authentication successful! Please close this tab and return to OnTime.');
|
||||
logger.info(LogOrigin.Server, `Sheet: Authentication successful`);
|
||||
} catch (e) {
|
||||
logger.error(LogOrigin.Server, `Sheet: ${e}`);
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
let listenPort = 3000;
|
||||
if (keyFile.installed) {
|
||||
// Use emphemeral port if not a web client
|
||||
listenPort = 0;
|
||||
} else if (redirectUri.port !== '') {
|
||||
listenPort = Number(redirectUri.port);
|
||||
}
|
||||
//TODO: the server might not start correctly
|
||||
server.listen(listenPort, () => {});
|
||||
const address = server.address();
|
||||
if (typeof address !== 'string') {
|
||||
redirectUri.port = String(address.port);
|
||||
}
|
||||
// open the browser to the authorize url to start the workflow
|
||||
const authorizeUrl = client.generateAuthUrl({
|
||||
redirect_uri: redirectUri.toString(),
|
||||
access_type: 'offline',
|
||||
scope: this.scope,
|
||||
});
|
||||
sheet.authUrl = authorizeUrl;
|
||||
this.authServerTimeout = setTimeout(
|
||||
() => {
|
||||
sheet.authUrl = null;
|
||||
server.unref;
|
||||
},
|
||||
2 * 60 * 1000,
|
||||
);
|
||||
return authorizeUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Copyright 2020 Google LLC
|
||||
// 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,6 +5,7 @@ import { OSCSettings } from './core/OscSettings.type.js';
|
||||
import { Settings } from './core/Settings.type.js';
|
||||
import { UserFields } from './core/UserFields.type.js';
|
||||
import { ViewSettings } from './core/Views.type.js';
|
||||
import { GoogleSheet } from '../index.js';
|
||||
|
||||
export type DatabaseModel = {
|
||||
rundown: OntimeRundown;
|
||||
@@ -13,5 +14,6 @@ export type DatabaseModel = {
|
||||
viewSettings: ViewSettings;
|
||||
aliases: Alias[];
|
||||
userFields: UserFields;
|
||||
googleSheet: GoogleSheet;
|
||||
osc: OSCSettings;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export type GoogleSheet = {
|
||||
worksheet: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type GoogleSheetState = {
|
||||
auth: boolean;
|
||||
id: boolean;
|
||||
worksheet: boolean;
|
||||
};
|
||||
@@ -33,6 +33,9 @@ export type { OSCSettings, OscSubscription, OscSubscriptionOptions } from './def
|
||||
|
||||
// ---> HTTP
|
||||
|
||||
// ---> Google Sheet
|
||||
export type { GoogleSheet, GoogleSheetState } from './definitions/core/GoogleSheet.type.js';
|
||||
|
||||
// SERVER RESPONSES
|
||||
export type { NetworkInterface, GetInfo } from './api/ontime-controller/BackendResponse.type.js';
|
||||
export type { GetRundownCached } from './api/rundown-controller/BackendResponse.type.js';
|
||||
|
||||
Generated
+160
-7
@@ -249,6 +249,9 @@ importers:
|
||||
|
||||
apps/server:
|
||||
dependencies:
|
||||
'@googleapis/sheets':
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5
|
||||
body-parser:
|
||||
specifier: ^1.20.0
|
||||
version: 1.20.1
|
||||
@@ -270,6 +273,9 @@ importers:
|
||||
express-validator:
|
||||
specifier: ^6.14.2
|
||||
version: 6.14.2
|
||||
google-auth-library:
|
||||
specifier: ^9.2.0
|
||||
version: 9.2.0
|
||||
lowdb:
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5
|
||||
@@ -2225,6 +2231,16 @@ packages:
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/@googleapis/sheets@5.0.5:
|
||||
resolution: {integrity: sha512-XMoONmgAJm2jYeTYHX4054VcEkElxlgqmnHvt0wAurzEHoGJLdUHhTAJXGPLgSs4WVMPtgU8HLrmk7/U+Qlw7A==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
dependencies:
|
||||
googleapis-common: 7.0.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@humanwhocodes/config-array@0.11.13:
|
||||
resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@@ -3520,6 +3536,15 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/agent-base@7.1.0:
|
||||
resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==}
|
||||
engines: {node: '>= 14'}
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/ajv-keywords@3.5.2(ajv@6.12.6):
|
||||
resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==}
|
||||
peerDependencies:
|
||||
@@ -3772,13 +3797,16 @@ packages:
|
||||
/base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
|
||||
/big-integer@1.6.51:
|
||||
resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==}
|
||||
engines: {node: '>=0.6'}
|
||||
dev: true
|
||||
|
||||
/bignumber.js@9.1.2:
|
||||
resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==}
|
||||
dev: false
|
||||
|
||||
/binary-extensions@2.2.0:
|
||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3881,6 +3909,10 @@ packages:
|
||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||
dev: true
|
||||
|
||||
/buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
dev: false
|
||||
|
||||
/buffer-equal@1.0.0:
|
||||
resolution: {integrity: sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
@@ -4392,7 +4424,6 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
dev: true
|
||||
|
||||
/decimal.js@10.4.3:
|
||||
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
|
||||
@@ -4623,6 +4654,12 @@ packages:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
dev: true
|
||||
|
||||
/ecdsa-sig-formatter@1.0.11:
|
||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
dev: false
|
||||
@@ -5269,6 +5306,10 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
dev: false
|
||||
|
||||
/extract-zip@2.0.1:
|
||||
resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
|
||||
engines: {node: '>= 10.17.0'}
|
||||
@@ -5526,6 +5567,30 @@ packages:
|
||||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||
dev: true
|
||||
|
||||
/gaxios@6.1.1:
|
||||
resolution: {integrity: sha512-bw8smrX+XlAoo9o1JAksBwX+hi/RG15J+NTSxmNPIclKC3ZVK6C2afwY8OSdRvOK0+ZLecUJYtj2MmjOt3Dm0w==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
https-proxy-agent: 7.0.2
|
||||
is-stream: 2.0.1
|
||||
node-fetch: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/gcp-metadata@6.1.0:
|
||||
resolution: {integrity: sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
gaxios: 6.1.1
|
||||
json-bigint: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/gensync@1.0.0-beta.2:
|
||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -5659,6 +5724,36 @@ packages:
|
||||
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
|
||||
dev: true
|
||||
|
||||
/google-auth-library@9.2.0:
|
||||
resolution: {integrity: sha512-1oV3p0JhNEhVbj26eF3FAJcv9MXXQt4S0wcvKZaDbl4oHq5V3UJoSbsGZGQNcjoCdhW4kDSwOs11wLlHog3fgQ==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
gaxios: 6.1.1
|
||||
gcp-metadata: 6.1.0
|
||||
gtoken: 7.0.1
|
||||
jws: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/googleapis-common@7.0.1:
|
||||
resolution: {integrity: sha512-mgt5zsd7zj5t5QXvDanjWguMdHAcJmmDrF9RkInCecNsyV7S7YtGqm5v2IWONNID88osb7zmx5FtrAP12JfD0w==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
gaxios: 6.1.1
|
||||
google-auth-library: 9.2.0
|
||||
qs: 6.11.0
|
||||
url-template: 2.0.8
|
||||
uuid: 9.0.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/gopd@1.0.1:
|
||||
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
||||
dependencies:
|
||||
@@ -5694,6 +5789,17 @@ packages:
|
||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
||||
dev: true
|
||||
|
||||
/gtoken@7.0.1:
|
||||
resolution: {integrity: sha512-KcFVtoP1CVFtQu0aSk3AyAt2og66PFhZAlkUOuWKwzMLoulHXG5W5wE5xAnHb+yl3/wEFoqGW7/cDGMU8igDZQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
dependencies:
|
||||
gaxios: 6.1.1
|
||||
jws: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/has-bigints@1.0.2:
|
||||
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
|
||||
dev: true
|
||||
@@ -5799,6 +5905,16 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/https-proxy-agent@7.0.2:
|
||||
resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==}
|
||||
engines: {node: '>= 14'}
|
||||
dependencies:
|
||||
agent-base: 7.1.0
|
||||
debug: 4.3.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/human-signals@2.1.0:
|
||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||
engines: {node: '>=10.17.0'}
|
||||
@@ -6077,7 +6193,6 @@ packages:
|
||||
/is-stream@2.0.1:
|
||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/is-stream@3.0.0:
|
||||
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
|
||||
@@ -6282,6 +6397,12 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/json-bigint@1.0.0:
|
||||
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
|
||||
dependencies:
|
||||
bignumber.js: 9.1.2
|
||||
dev: false
|
||||
|
||||
/json-buffer@3.0.1:
|
||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||
dev: true
|
||||
@@ -6335,6 +6456,21 @@ packages:
|
||||
object.assign: 4.1.4
|
||||
dev: true
|
||||
|
||||
/jwa@2.0.0:
|
||||
resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==}
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/jws@4.0.0:
|
||||
resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
|
||||
dependencies:
|
||||
jwa: 2.0.0
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/keyv@4.5.2:
|
||||
resolution: {integrity: sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==}
|
||||
dependencies:
|
||||
@@ -6679,7 +6815,6 @@ packages:
|
||||
|
||||
/ms@2.1.2:
|
||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||
dev: true
|
||||
|
||||
/ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
@@ -6736,6 +6871,18 @@ packages:
|
||||
whatwg-url: 5.0.0
|
||||
dev: true
|
||||
|
||||
/node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
dev: false
|
||||
|
||||
/node-osc@9.0.2:
|
||||
resolution: {integrity: sha512-q+VQL7DMWRL5+yvzRlWVig8BD9raotLs6onHU4e8MaFgxmYuIwcXhsvQeyUZFiKP6y/qGUXU6K0T99gVmISwmA==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
@@ -8123,7 +8270,6 @@ packages:
|
||||
|
||||
/tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
dev: true
|
||||
|
||||
/tr46@3.0.0:
|
||||
resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
|
||||
@@ -8427,6 +8573,10 @@ packages:
|
||||
requires-port: 1.0.0
|
||||
dev: true
|
||||
|
||||
/url-template@2.0.8:
|
||||
resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==}
|
||||
dev: false
|
||||
|
||||
/use-callback-ref@1.3.0(@types/react@18.0.26)(react@18.2.0):
|
||||
resolution: {integrity: sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -8479,6 +8629,11 @@ packages:
|
||||
engines: {node: '>= 0.4.0'}
|
||||
dev: false
|
||||
|
||||
/uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/v8-compile-cache-lib@3.0.1:
|
||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||
dev: true
|
||||
@@ -8831,7 +8986,6 @@ packages:
|
||||
|
||||
/webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
dev: true
|
||||
|
||||
/webidl-conversions@7.0.0:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
@@ -8877,7 +9031,6 @@ packages:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
dev: true
|
||||
|
||||
/which-boxed-primitive@1.0.2:
|
||||
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
|
||||
|
||||
Reference in New Issue
Block a user